diff --git a/docker-compose.yml b/docker-compose.yml index d4f8850..85d74bc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,6 +71,34 @@ services: - default - proxy + # ----------------------------------------------------------------------------- + # Walkthrough/Demo Application Instance + # ----------------------------------------------------------------------------- + app-walkthrough: + build: + context: . + dockerfile: Dockerfile + container_name: special-hotel-walkthrough + restart: unless-stopped + ports: + - "127.0.0.1:3002:3000" + environment: + - NODE_ENV=production + - DATABASE_URL=${DATABASE_URL_WALKTHROUGH} + - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL_WALKTHROUGH} + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET_WALKTHROUGH} + - N8N_WEBHOOK_URL=${N8N_WEBHOOK_URL_WALKTHROUGH} + - N8N_WEBHOOK_SECRET=${N8N_WEBHOOK_SECRET_WALKTHROUGH} + logging: + driver: "json-file" + options: + tag: "app-walkthrough/{{.Name}}" + max-size: "10m" + max-file: "3" + networks: + - default + - proxy + networks: default: proxy: diff --git a/docs/showcase/recruiter_guide_en.md b/docs/showcase/recruiter_guide_en.md index 39f376d..d500e98 100644 --- a/docs/showcase/recruiter_guide_en.md +++ b/docs/showcase/recruiter_guide_en.md @@ -23,35 +23,35 @@ The system is built as a highly secure, multi-tenant enterprise app designed to > **I want** to see only my own sales, goals, and commission settlements, > **So that** I cannot view or tamper with other team members' financial information. * **The Solution**: PostgreSQL RLS policies enforce segregation directly in the database. When any query runs, the system dynamically sets the session variable `app.current_user_role` and `app.current_user_id`. Even if a user calls APIs directly or tries to bypass the UI, the database returns `0` rows for other users or hotels. -* **Code Reference**: Check [rls_and_seed.sql](file:///home/gabogg/Proyects/semillero-special-hotel/prisma/rls_and_seed.sql) to see the SQL policies, and [auth.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/lib/auth.ts) to see how session contexts are injected. +* **Code Reference**: Check [rls_and_seed.sql](../../prisma/rls_and_seed.sql) to see the SQL policies, and [auth.ts](../../src/lib/auth.ts) to see how session contexts are injected. ### User Story 2: Plan Versioning & Rules Configuration > **As an** administrator or commercial analyst, > **I want** to create commission plans and rules, and ensure that editing active plans creates a new version instead of changing history, > **So that** historical calculations remain auditable and unchanged. * **The Solution**: The system implements plan locking. When a plan is in `DRAFT`, rules can be edited. When it is marked `ACTIVE`, it becomes read-only. If an administrator tries to toggle or update an active plan, the system clones the plan, increments the `version` (e.g. `v1` -> `v2`), and sets the old plan's validity end date. -* **Code Reference**: Check plan clone logic in [/api/plans/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/plans/route.ts). +* **Code Reference**: Check plan clone logic in [/api/plans/route.ts](../../src/app/api/plans/route.ts). ### User Story 3: Atomic Bulk Sales Import > **As a** commercial leader, > **I want** to import Excel files containing monthly sales results, > **So that** sales are automatically processed and verified, returning clear validation errors if the file is inconsistent. * **The Solution**: Excel sheets are parsed and checked against strict validation constraints (non-existent users, negative amounts, invalid periods, incorrect hotel codes). The validation is **atomic**—if even one row fails, the entire transaction is rolled back. The UI presents an inconsistency panel highlighting exactly which cells failed. -* **Code Reference**: View sheet parsing in [/api/sales/import/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/sales/import/route.ts). +* **Code Reference**: View sheet parsing in [/api/sales/import/route.ts](../../src/app/api/sales/import/route.ts). ### User Story 4: Automated Calculations & Retroactive Clawbacks > **As a** finance analyst, > **I want** commission calculations to run automatically via n8n and adjust for returned or cancelled sales from previous months, > **So that** payouts are correct and negative adjustments are deducted. * **The Solution**: The n8n calculation workflow triggers the settlement engine. The engine queries previous months' sales. If a sale was returned/cancelled, it applies a retroactive delta adjustment (clawback) to the current month's payout. -* **Code Reference**: See settlement computations in [/api/settlements/calculate/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/settlements/calculate/route.ts). +* **Code Reference**: See settlement computations in [/api/settlements/calculate/route.ts](../../src/app/api/settlements/calculate/route.ts). ### User Story 5: Immutable Auditing Log > **As an** external auditor, > **I want** to see a complete history of all user logins, plan modifications, and approvals, > **So that** I can verify that logs cannot be edited or deleted by anyone. * **The Solution**: The `audit_logs` table has database triggers that intercept and block all `UPDATE` and `DELETE` queries. The Admin dashboard features an interactive Audit Logs Explorer displaying side-by-side JSON diff panels showing what changed. -* **Code Reference**: View audit logs logic in [/api/audit-logs/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/audit-logs/route.ts). +* **Code Reference**: View audit logs logic in [/api/audit-logs/route.ts](../../src/app/api/audit-logs/route.ts). --- diff --git a/docs/showcase/recruiter_guide_es.md b/docs/showcase/recruiter_guide_es.md index 70b3914..1974fc0 100644 --- a/docs/showcase/recruiter_guide_es.md +++ b/docs/showcase/recruiter_guide_es.md @@ -23,35 +23,35 @@ El sistema está construido como una aplicación empresarial multi-inquilino alt > **Quiero** ver únicamente mis propias ventas, metas y liquidaciones de comisiones, > **Para que** no pueda ver ni alterar la información financiera de otros miembros del equipo. * **La Solución**: Las políticas de RLS de PostgreSQL imponen la segregación directamente en el motor de base de datos. Cuando se ejecuta cualquier consulta, el sistema establece dinámicamente la variable de sesión `app.current_user_role` y `app.current_user_id`. Incluso si un usuario realiza solicitudes directas a la API o intenta eludir la interfaz de usuario, la base de datos retorna `0` registros para otros usuarios u hoteles. -* **Referencia de Código**: Revise [rls_and_seed.sql](file:///home/gabogg/Proyects/semillero-special-hotel/prisma/rls_and_seed.sql) para ver las políticas SQL, y [auth.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/lib/auth.ts) para ver cómo se inyectan los contextos de sesión. +* **Referencia de Código**: Revise [rls_and_seed.sql](../../prisma/rls_and_seed.sql) para ver las políticas SQL, y [auth.ts](../../src/lib/auth.ts) para ver cómo se inyectan los contextos de sesión. ### Historia de Usuario 2: Control de Versiones de Planes y Configuración de Reglas > **Como** administrador o analista comercial, > **Quiero** crear planes de compensación y reglas, y asegurar que la edición de planes activos genere una nueva versión en lugar de modificar el historial, > **Para que** los cálculos históricos sigan siendo auditables e inalterados. * **La Solución**: El sistema implementa el bloqueo de planes. Cuando un plan está en estado `DRAFT` (Borrador), se pueden editar sus reglas. Cuando se marca como `ACTIVE` (Activo), pasa a ser de solo lectura. Si un administrador intenta alternar o actualizar un plan activo, el sistema clona el plan, incrementa el campo `version` (por ejemplo, `v1` -> `v2`) y establece la fecha de fin de validez del plan anterior. -* **Referencia de Código**: Revise la lógica de clonación de planes en [/api/plans/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/plans/route.ts). +* **Referencia de Código**: Revise la lógica de clonación de planes en [/api/plans/route.ts](../../src/app/api/plans/route.ts). ### Historia de Usuario 3: Importación Masiva Atómica de Ventas > **Como** líder comercial, > **Quiero** importar archivos Excel con los resultados de ventas mensuales, > **Para que** las ventas se procesen y validen automáticamente, retornando errores claros si el archivo presenta inconsistencias. * **La Solución**: Las hojas de Excel se procesan y validan contra restricciones estrictas (usuarios inexistentes, montos negativos, períodos inválidos, códigos de hotel incorrectos). La validación es **atómica**: si una sola fila falla, toda la transacción se revierte. La interfaz de usuario presenta un panel de inconsistencias que detalla con precisión qué celdas fallaron. -* **Referencia de Código**: Vea el análisis de archivos en [/api/sales/import/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/sales/import/route.ts). +* **Referencia de Código**: Vea el análisis de archivos en [/api/sales/import/route.ts](../../src/app/api/sales/import/route.ts). ### Historia de Usuario 4: Cálculos Automatizados y Deducciones Retroactivas (Clawbacks) > **Como** analista de finanzas, > **Quiero** que los cálculos de comisiones se ejecuten automáticamente mediante n8n y se ajusten por devoluciones o ventas canceladas de meses anteriores, > **Para que** los pagos sean correctos y se deduzcan los saldos negativos correspondientes. * **La Solución**: El flujo de cálculo de n8n activa el motor de liquidación. El motor consulta las ventas de los meses anteriores. Si una venta fue devuelta o cancelada, aplica un ajuste de delta retroactivo (clawback) en la liquidación del mes en curso, restándolo del pago final. -* **Referencia de Código**: Vea los cálculos de liquidaciones en [/api/settlements/calculate/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/settlements/calculate/route.ts). +* **Referencia de Código**: Vea los cálculos de liquidaciones en [/api/settlements/calculate/route.ts](../../src/app/api/settlements/calculate/route.ts). ### Historia de Usuario 5: Registro de Auditoría Inmutable > **Como** auditor externo, > **Quiero** ver un historial completo de todos los inicios de sesión de usuario, modificaciones de planes y aprobaciones, > **Para** verificar que nadie pueda editar ni eliminar los registros de auditoría. * **La Solución**: La tabla `audit_logs` cuenta con reglas y disparadores de base de datos que interceptan y bloquean cualquier consulta de tipo `UPDATE` o `DELETE`. El panel de administración incluye un Explorador de Registros de Auditoría interactivo con un panel de comparación JSON en paralelo que muestra exactamente qué cambió. -* **Referencia de Código**: Vea la lógica de los registros de auditoría en [/api/audit-logs/route.ts](file:///home/gabogg/Proyects/semillero-special-hotel/src/app/api/audit-logs/route.ts). +* **Referencia de Código**: Vea la lógica de los registros de auditoría en [/api/audit-logs/route.ts](../../src/app/api/audit-logs/route.ts). --- diff --git a/prisma/seed-walkthrough.js b/prisma/seed-walkthrough.js new file mode 100644 index 0000000..e5c14ee --- /dev/null +++ b/prisma/seed-walkthrough.js @@ -0,0 +1,372 @@ +const { PrismaClient } = require('@prisma/client'); +const { PrismaPg } = require('@prisma/adapter-pg'); +const { Pool } = require('pg'); +require('dotenv').config(); + +const dbUrl = new URL(process.env.DATABASE_URL_WALKTHROUGH || "postgresql://special_hotel_user:SpecialHotel1235%2F%2A-%2B@pg.gaboggamer.online/special_hotel_walkthrough?schema=public"); + +const pool = new Pool({ + host: dbUrl.hostname, + port: dbUrl.port ? parseInt(dbUrl.port) : 5432, + user: decodeURIComponent(dbUrl.username), + password: decodeURIComponent(dbUrl.password), + database: dbUrl.pathname.substring(1).split('?')[0], + ssl: false +}); + +const adapter = new PrismaPg(pool); +const prisma = new PrismaClient({ adapter }); + +const PASSWORD_HASH = '$2b$10$lRn/GrwzaEGxWqqQGGz0f.44nBywObKkGWRajlxWeDGZ3o4f7ayjy'; // bcrypt of 'password123' + +async function main() { + console.log("=== STARTING WALKTHROUGH SEED SCRIPT ==="); + + // 1. Clean existing records in the walkthrough database to ensure clean seeding + console.log("Cleaning database..."); + // Disable RLS temporarily for bulk seed operations + await prisma.$executeRawUnsafe(`ALTER TABLE "regions" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "hotels" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "users" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "goals" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "sales_results" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "settlements" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "audit_logs" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "compensation_plans" DISABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "calculation_rules" DISABLE ROW LEVEL SECURITY;`); + + await prisma.auditLog.deleteMany(); + await prisma.notification.deleteMany(); + await prisma.settlement.deleteMany(); + await prisma.salesResult.deleteMany(); + await prisma.goal.deleteMany(); + await prisma.calculationRule.deleteMany(); + await prisma.compensationPlan.deleteMany(); + await prisma.user.deleteMany(); + await prisma.hotel.deleteMany(); + await prisma.region.deleteMany(); + + // 2. Seed Regions + console.log("Seeding regions..."); + const bogota = await prisma.region.create({ data: { name: 'Bogotá', code: 'BOG' } }); + const antioquia = await prisma.region.create({ data: { name: 'Antioquia', code: 'ANT' } }); + const caribe = await prisma.region.create({ data: { name: 'Caribe', code: 'CAR' } }); + const valle = await prisma.region.create({ data: { name: 'Valle', code: 'VAL' } }); + + // 3. Seed Hotels + console.log("Seeding hotels..."); + const p93 = await prisma.hotel.create({ data: { name: 'Estelar Parque de la 93', code: 'EST-P93', regionId: bogota.id } }); + const mde = await prisma.hotel.create({ data: { name: 'Estelar Medellin', code: 'EST-MDE', regionId: antioquia.id } }); + const ctg = await prisma.hotel.create({ data: { name: 'Estelar Cartagena', code: 'EST-CTG', regionId: caribe.id } }); + const cali = await prisma.hotel.create({ data: { name: 'Estelar Cali', code: 'EST-CALI', regionId: valle.id } }); + + // 4. Seed Users + console.log("Seeding users..."); + const usersToSeed = [ + { username: 'admin', email: 'admin@estelar.com', role: 'admin', hotelId: p93.id, area: 'Sistemas' }, + { username: 'director', email: 'director@estelar.com', role: 'director', hotelId: p93.id, area: 'Comercial' }, + { username: 'analista', email: 'analista@estelar.com', role: 'analyst', hotelId: p93.id, area: 'Finanzas' }, + { username: 'consulta', email: 'consulta@estelar.com', role: 'auditor', hotelId: p93.id, area: 'Auditoria' }, + + // Managers + { username: 'gerente_bog', email: 'gerente.bog@estelar.com', role: 'hotel_manager', hotelId: p93.id, area: 'Administracion' }, + { username: 'gerente_mde', email: 'gerente.mde@estelar.com', role: 'hotel_manager', hotelId: mde.id, area: 'Administracion' }, + { username: 'gerente_ctg', email: 'gerente.ctg@estelar.com', role: 'hotel_manager', hotelId: ctg.id, area: 'Administracion' }, + { username: 'gerente_cali', email: 'gerente.cali@estelar.com', role: 'hotel_manager', hotelId: cali.id, area: 'Administracion' }, + + // Leaders + { username: 'lider_bog', email: 'lider.bog@estelar.com', role: 'commercial_leader', hotelId: p93.id, area: 'Ventas' }, + { username: 'lider_mde', email: 'lider.mde@estelar.com', role: 'commercial_leader', hotelId: mde.id, area: 'Ventas' }, + { username: 'lider_ctg', email: 'lider.ctg@estelar.com', role: 'commercial_leader', hotelId: ctg.id, area: 'Ventas' }, + { username: 'lider_cali', email: 'lider.cali@estelar.com', role: 'commercial_leader', hotelId: cali.id, area: 'Ventas' }, + + // Collaborators + { username: 'colab_bog_1', email: 'colab.bog1@estelar.com', role: 'collaborator', hotelId: p93.id, area: 'Ventas' }, + { username: 'colab_bog_2', email: 'colab.bog2@estelar.com', role: 'collaborator', hotelId: p93.id, area: 'Ventas' }, + { username: 'colab_mde_1', email: 'colab.mde1@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' }, + { username: 'colab_mde_2', email: 'colab.mde2@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' }, + { username: 'colaborador_mde', email: 'colaborador.mde@estelar.com', role: 'collaborator', hotelId: mde.id, area: 'Ventas' }, + { username: 'colab_ctg_1', email: 'colab.ctg1@estelar.com', role: 'collaborator', hotelId: ctg.id, area: 'Ventas' }, + { username: 'colab_ctg_2', email: 'colab.ctg2@estelar.com', role: 'collaborator', hotelId: ctg.id, area: 'Ventas' }, + { username: 'colab_cali_1', email: 'colab.cali1@estelar.com', role: 'collaborator', hotelId: cali.id, area: 'Ventas' }, + { username: 'colab_cali_2', email: 'colab.cali2@estelar.com', role: 'collaborator', hotelId: cali.id, area: 'Ventas' } + ]; + + const seededUsers = {}; + for (const u of usersToSeed) { + const created = await prisma.user.create({ + data: { + username: u.username, + email: u.email, + passwordHash: PASSWORD_HASH, + role: u.role, + hotelId: u.hotelId, + area: u.area, + status: 'ACTIVE' + } + }); + seededUsers[u.username] = created; + } + + // 5. Seed Compensation Plans & Rules + console.log("Seeding compensation plans..."); + // Plan Q1 (Percentage, Inactive) + const planQ1 = await prisma.compensationPlan.create({ + data: { + name: 'Plan Comisiones Q1 - Porcentaje Fijo', + code: 'PLAN-2026-Q1', + validityStart: new Date('2026-01-01'), + validityEnd: new Date('2026-03-31'), + type: 'PERCENTAGE', + percentageRate: 0.025, // 2.5% + maxCap: 15000.00, + status: 'INACTIVE', + version: 1, + createdBy: seededUsers['admin'].id + } + }); + + // Plan Q2 (Scale, Active) + const planQ2 = await prisma.compensationPlan.create({ + data: { + name: 'Plan Comisiones Q2 - Escala Escalonada', + code: 'PLAN-2026-Q2', + validityStart: new Date('2026-04-01'), + validityEnd: new Date('2026-06-30'), + type: 'SCALE', + status: 'ACTIVE', + version: 1, + createdBy: seededUsers['admin'].id + } + }); + + // Calculation rules for Q2 Plan + console.log("Seeding calculation rules..."); + await prisma.calculationRule.createMany({ + data: [ + { planId: planQ2.id, type: 'TIER', minAchievement: 0.0, maxAchievement: 0.8, rate: 0.0, payoutAmount: 0.0 }, + { planId: planQ2.id, type: 'TIER', minAchievement: 0.8, maxAchievement: 1.0, rate: 0.02, payoutAmount: 100.0 }, + { planId: planQ2.id, type: 'TIER', minAchievement: 1.0, maxAchievement: 9.9, rate: 0.03, payoutAmount: 250.0 } + ] + }); + + // 6. Seed Goals, Sales Results and Settlements + console.log("Seeding goals, sales, and settlements..."); + const collaborators = [ + 'colab_bog_1', 'colab_bog_2', + 'colab_mde_1', 'colab_mde_2', 'colaborador_mde', + 'colab_ctg_1', 'colab_ctg_2', + 'colab_cali_1', 'colab_cali_2' + ]; + + const periods = ['2026-01', '2026-02', '2026-03', '2026-04', '2026-05', '2026-06']; + + // Seed structured sales, goals and settlements month-by-month + for (const period of periods) { + const isQ2 = period >= '2026-04'; + const plan = isQ2 ? planQ2 : planQ1; + + for (const username of collaborators) { + const user = seededUsers[username]; + + // Goal Setup + let goalAmount = 70000.00; + if (username.endsWith('2')) goalAmount = 60000.00; + if (username === 'colaborador_mde') goalAmount = 80000.00; + + // Random variance for each user and month + const hashVal = (username.length + period.charCodeAt(6)) % 10; + const achievementMultiplier = 0.75 + (hashVal * 0.04); // ranges from 0.75 to 1.15 + const salesAmount = goalAmount * achievementMultiplier; + + // Create Goal + const goal = await prisma.goal.create({ + data: { + targetType: 'INDIVIDUAL', + targetId: user.id, + period, + amount: goalAmount + } + }); + + // Create Sales Entries + const entryCount = 4; + const amountPerEntry = salesAmount / entryCount; + const salesCountPerEntry = Math.round(5 + hashVal / 2); + + for (let i = 0; i < entryCount; i++) { + // Build unique idempotency key + const idempotencyKey = `sale-${user.id}-${period}-${i}`; + + // Seed normal sale + await prisma.salesResult.create({ + data: { + source: 'EXCEL', + hotelId: user.hotelId, + userId: user.id, + period, + amount: amountPerEntry, + salesCount: salesCountPerEntry, + status: 'PROCESSED', + idempotencyKey, + uploadedBy: seededUsers['gerente_mde'].id, // Seed uploaded by MDE manager for convenience + isAnomaly: false + } + }); + } + + // Add a flagged outlier anomaly for colab_bog_1 in March + if (username === 'colab_bog_1' && period === '2026-03') { + await prisma.salesResult.create({ + data: { + source: 'EXCEL', + hotelId: user.hotelId, + userId: user.id, + period, + amount: 85000.00, + salesCount: 45, + status: 'PROCESSED', + idempotencyKey: `anomaly-outlier-bog1-mar`, + uploadedBy: seededUsers['gerente_bog'].id, + isAnomaly: true, + flaggedReason: { + en: "Outlier transaction: Sale amount exceeds standard region limits.", + es: "Transacción atípica: El monto de venta supera los límites regionales estándar." + } + } + }); + } + + // Compute Commission & Bonus + let commission = 0; + let bonus = 0; + const achievement = salesAmount / goalAmount; + + if (!isQ2) { + // Percentage plan: 2.5% of total sales + commission = salesAmount * 0.025; + bonus = 0; + } else { + // Scale plan rules + if (achievement >= 0.8 && achievement < 1.0) { + commission = salesAmount * 0.02; + bonus = 100.00; + } else if (achievement >= 1.0) { + commission = salesAmount * 0.03; + bonus = 250.00; + } + } + + // Retroactive Clawback deduction for colab_mde_1 in May 2026 + let adjustmentAmount = 0.00; + let adjustmentNotes = null; + let originalSettlementId = null; + + if (username === 'colab_mde_1' && period === '2026-05') { + // Clawback applied for returned sale of $15000 in March 2026 (PLAN Q1 rate: 2.5%) + adjustmentAmount = -375.00; + adjustmentNotes = "Deducción de retroactivo: Devolución de reserva de ventas de marzo (PLAN-2026-Q1)."; + + // Find their March settlement to link as original + const marchSettlement = await prisma.settlement.findFirst({ + where: { userId: user.id, period: '2026-03' } + }); + if (marchSettlement) { + originalSettlementId = marchSettlement.id; + } + } + + const totalPayout = commission + bonus + adjustmentAmount; + + // Settlement Status: APPROVED for Jan-May, PENDING for June + const isJune = period === '2026-06'; + const settlementStatus = isJune ? 'PENDING' : 'APPROVED'; + + const managerMap = { + [p93.id]: seededUsers['gerente_bog'].id, + [mde.id]: seededUsers['gerente_mde'].id, + [ctg.id]: seededUsers['gerente_ctg'].id, + [cali.id]: seededUsers['gerente_cali'].id + }; + + await prisma.settlement.create({ + data: { + period, + planId: plan.id, + userId: user.id, + salesAmount, + goalAmount, + achievementPercentage: achievement, + calculatedCommission: commission, + calculatedBonus: bonus, + adjustmentAmount, + totalPayout, + status: settlementStatus, + approvedBy: isJune ? null : managerMap[user.hotelId], + approvedAt: isJune ? null : new Date(`${period}-05T14:30:00Z`), + originalSettlementId, + adjustmentNotes, + aiAudited: true, + aiAuditNotes: { + en: isJune + ? "Calculated under PLAN-2026-Q2. Validation checks successfully completed." + : "Compliant commission settlement. Payout successfully audited.", + es: isJune + ? "Calculado bajo el PLAN-2026-Q2. Revisiones de validación completadas exitosamente." + : "Liquidación de comisiones conforme. Pago auditado correctamente." + } + } + }); + } + } + + // 7. Seed Audit Logs Trail + console.log("Seeding audit logs..."); + const logs = [ + { userId: seededUsers['admin'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['admin'].id, prev: null, next: null, ip: '::1', date: '2026-01-02T09:00:00Z' }, + { userId: seededUsers['admin'].id, action: 'CREATE', targetTable: 'compensation_plans', targetId: planQ1.id, prev: null, next: { id: planQ1.id, code: planQ1.code, status: 'DRAFT' }, ip: '127.0.0.1', date: '2026-01-02T09:15:00Z' }, + { userId: seededUsers['admin'].id, action: 'UPDATE', targetTable: 'compensation_plans', targetId: planQ1.id, prev: { status: 'DRAFT' }, next: { status: 'ACTIVE' }, ip: '127.0.0.1', date: '2026-01-05T10:00:00Z' }, + { userId: seededUsers['gerente_mde'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['gerente_mde'].id, prev: null, next: null, ip: '10.8.0.5', date: '2026-02-05T08:30:00Z' }, + { userId: seededUsers['gerente_mde'].id, action: 'APPROVE', targetTable: 'settlements', targetId: 1, prev: { status: 'PENDING' }, next: { status: 'APPROVED' }, ip: '10.8.0.5', date: '2026-02-05T08:45:00Z' }, + { userId: seededUsers['gerente_bog'].id, action: 'LOGIN', targetTable: 'users', targetId: seededUsers['gerente_bog'].id, prev: null, next: null, ip: '10.8.0.6', date: '2026-02-05T09:12:00Z' }, + { userId: seededUsers['gerente_bog'].id, action: 'APPROVE', targetTable: 'settlements', targetId: 2, prev: { status: 'PENDING' }, next: { status: 'APPROVED' }, ip: '10.8.0.6', date: '2026-02-05T09:18:00Z' } + ]; + + for (const log of logs) { + await prisma.auditLog.create({ + data: { + userId: log.userId, + action: log.action, + targetTable: log.targetTable, + targetId: log.targetId, + previousValue: log.prev, + newValue: log.next, + ipAddress: log.ip, + createdAt: new Date(log.date) + } + }); + } + + // 8. Re-enable Row-Level Security Policies via SQL + console.log("Configuring database RLS boundaries..."); + await prisma.$executeRawUnsafe(`ALTER TABLE "regions" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "hotels" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "users" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "goals" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "sales_results" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "settlements" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "audit_logs" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "compensation_plans" ENABLE ROW LEVEL SECURITY;`); + await prisma.$executeRawUnsafe(`ALTER TABLE "calculation_rules" ENABLE ROW LEVEL SECURITY;`); + + // Disconnect temporary prisma connection before running manual execute + await prisma.$disconnect(); + + console.log("Walkthrough seeding completed successfully!"); +} + +main() + .catch(async (e) => { + console.error("Error during walkthrough seeding:", e); + process.exit(1); + }); diff --git a/src/app/admin/audit-logs/page.tsx b/src/app/admin/audit-logs/page.tsx index d1197a1..b8e32e0 100644 --- a/src/app/admin/audit-logs/page.tsx +++ b/src/app/admin/audit-logs/page.tsx @@ -32,18 +32,6 @@ export default function AuditLogsPage() { const [isLoading, setIsLoading] = useState(true); const [selectedLog, setSelectedLog] = useState(null); - useEffect(() => { - if (!authLoading && role !== 'admin') { - router.push('/unauthorized'); - } - }, [role, authLoading, router]); - - useEffect(() => { - if (role === 'admin') { - fetchLogs(); - } - }, [role]); - const fetchLogs = async () => { setIsLoading(true); try { @@ -59,6 +47,18 @@ export default function AuditLogsPage() { } }; + useEffect(() => { + if (!authLoading && role !== 'admin') { + router.push('/unauthorized'); + } + }, [role, authLoading, router]); + + useEffect(() => { + if (role === 'admin') { + fetchLogs(); + } + }, [role]); + const handleRowClick = (log: AuditLog) => { setSelectedLog(log); }; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index db44a79..d0011d2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -40,18 +40,6 @@ export default function DashboardPage() { const isRoleAuthorized = role === 'admin' || role === 'director' || role === 'analyst'; - useEffect(() => { - if (!authLoading && !isRoleAuthorized) { - router.push('/unauthorized'); - } - }, [role, authLoading, router, isRoleAuthorized]); - - useEffect(() => { - if (isRoleAuthorized) { - fetchDashboardData(); - } - }, [role, isRoleAuthorized]); - const fetchDashboardData = async () => { setIsLoading(true); try { @@ -69,6 +57,18 @@ export default function DashboardPage() { } }; + useEffect(() => { + if (!authLoading && !isRoleAuthorized) { + router.push('/unauthorized'); + } + }, [role, authLoading, router, isRoleAuthorized]); + + useEffect(() => { + if (isRoleAuthorized) { + fetchDashboardData(); + } + }, [role, isRoleAuthorized]); + const formatCurrency = (val: number) => { return new Intl.NumberFormat(locale === 'es' ? 'es-CO' : 'en-US', { style: 'currency',