From c52de6ecb881baf2251b90696f12dead81bb3416 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Fri, 29 Aug 2025 11:44:04 -0400 Subject: [PATCH] fix(seeds): fix timesheet seeds --- docs/swagger/swagger-spec.json | 38 -------------- .../migration.sql | 12 +++++ prisma/mock-seeds-scripts/09-timesheets.ts | 49 ++++++++++++++++--- prisma/mock-seeds-scripts/10-shifts.ts | 37 +++++++------- prisma/mock-seeds-scripts/12-expenses.ts | 46 ++++++++--------- prisma/schema.prisma | 2 + .../services/pay-periods-query.service.ts | 3 +- .../controllers/timesheets.controller.ts | 16 +++--- .../services/timesheets-query.service.ts | 20 ++++---- 9 files changed, 116 insertions(+), 107 deletions(-) create mode 100644 prisma/migrations/20250829152939_timesheet_week_unique/migration.sql diff --git a/docs/swagger/swagger-spec.json b/docs/swagger/swagger-spec.json index 41fa85a..430ea23 100644 --- a/docs/swagger/swagger-spec.json +++ b/docs/swagger/swagger-spec.json @@ -425,44 +425,6 @@ } }, "/timesheets": { - "post": { - "operationId": "TimesheetsController_create", - "parameters": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } - }, - "responses": { - "201": { - "description": "Timesheet created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateTimesheetDto" - } - } - } - }, - "400": { - "description": "Incomplete task or invalid data" - } - }, - "security": [ - { - "access-token": [] - } - ], - "summary": "Create timesheet", - "tags": [ - "Timesheets" - ] - }, "get": { "operationId": "TimesheetsController_getPeriodByQuery", "parameters": [ diff --git a/prisma/migrations/20250829152939_timesheet_week_unique/migration.sql b/prisma/migrations/20250829152939_timesheet_week_unique/migration.sql new file mode 100644 index 0000000..8073704 --- /dev/null +++ b/prisma/migrations/20250829152939_timesheet_week_unique/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - A unique constraint covering the columns `[employee_id,start_date]` on the table `timesheets` will be added. If there are existing duplicate values, this will fail. + - Added the required column `start_date` to the `timesheets` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "public"."timesheets" ADD COLUMN "start_date" DATE NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "timesheets_employee_id_start_date_key" ON "public"."timesheets"("employee_id", "start_date"); diff --git a/prisma/mock-seeds-scripts/09-timesheets.ts b/prisma/mock-seeds-scripts/09-timesheets.ts index 1d05345..f926fb2 100644 --- a/prisma/mock-seeds-scripts/09-timesheets.ts +++ b/prisma/mock-seeds-scripts/09-timesheets.ts @@ -2,26 +2,59 @@ import { PrismaClient, Prisma } from '@prisma/client'; const prisma = new PrismaClient(); +// ====== Config ====== +const PREVIOUS_WEEKS = 16; // nombre de semaines à créer (passé) +const INCLUDE_CURRENT = false; // true si tu veux aussi la semaine courante + +// Lundi (UTC) de la semaine courante +function mondayOfThisWeekUTC(now = new Date()) { + const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); + const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... + const diffToMonday = (day + 6) % 7; // 0 si lundi + d.setUTCDate(d.getUTCDate() - diffToMonday); + d.setUTCHours(0, 0, 0, 0); + return d; +} +function mondayNWeeksBefore(monday: Date, n: number) { + const d = new Date(monday); + d.setUTCDate(d.getUTCDate() - n * 7); + return d; +} + async function main() { const employees = await prisma.employees.findMany({ select: { id: true } }); + if (!employees.length) { + console.warn('Aucun employé — rien à insérer.'); + return; + } - // ✅ typer rows pour éviter never[] + // Construit la liste des lundis (1 par semaine) + const mondays: Date[] = []; + const mondayThisWeek = mondayOfThisWeekUTC(); + if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); + for (let n = 1; n <= PREVIOUS_WEEKS; n++) { + mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); + } + + // Prépare les lignes (1 timesheet / employé / semaine) const rows: Prisma.TimesheetsCreateManyInput[] = []; - - // 8 timesheets / employee for (const e of employees) { - for (let i = 0; i < 16; i++) { - const is_approved = Math.random() < 0.3; - rows.push({ employee_id: e.id, is_approved }); + for (const monday of mondays) { + rows.push({ + employee_id: e.id, + start_date: monday, + is_approved: Math.random() < 0.3, + } as Prisma.TimesheetsCreateManyInput); } } + // Insert en bulk et ignore les doublons si déjà présents if (rows.length) { - await prisma.timesheets.createMany({ data: rows }); + await prisma.timesheets.createMany({ data: rows, skipDuplicates: true }); } const total = await prisma.timesheets.count(); - console.log(`✓ Timesheets: ${total} rows (added ${rows.length})`); + console.log(`✓ Timesheets: ${total} rows (ajout potentiel: ${rows.length}, ${INCLUDE_CURRENT ? 'courante +' : ''}${PREVIOUS_WEEKS} semaines)`); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/10-shifts.ts b/prisma/mock-seeds-scripts/10-shifts.ts index dd89b47..9175030 100644 --- a/prisma/mock-seeds-scripts/10-shifts.ts +++ b/prisma/mock-seeds-scripts/10-shifts.ts @@ -6,10 +6,12 @@ const prisma = new PrismaClient(); const PREVIOUS_WEEKS = 5; const INCLUDE_CURRENT = false; +// Times-only via Date (UTC 1970-01-01) function timeAt(hour: number, minute: number) { return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); } +// Lundi (UTC) de la date fournie function mondayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const day = d.getUTCDay(); @@ -37,6 +39,16 @@ function rndInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } +// Helper: garantit le timesheet de la semaine (upsert) +async function getOrCreateTimesheet(employee_id: number, start_date: Date) { + return prisma.timesheets.upsert({ + where: { employee_id_start_date: { employee_id, start_date } }, + update: {}, + create: { employee_id, start_date, is_approved: Math.random() < 0.3 }, + select: { id: true }, + }); +} + async function main() { // Bank codes utilisés const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const; @@ -55,21 +67,8 @@ async function main() { return; } - const tsByEmp = new Map(); - { - const allTs = await prisma.timesheets.findMany({ - where: { employee_id: { in: employees.map(e => e.id) } }, - select: { id: true, employee_id: true }, - orderBy: { id: 'asc' }, - }); - for (const e of employees) { - tsByEmp.set(e.id, allTs.filter(t => t.employee_id === e.id).map(t => ({ id: t.id }))); - } - } - const mondayThisWeek = mondayOfThisWeekUTC(); const mondays: Date[] = []; - if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); for (let n = 1; n <= PREVIOUS_WEEKS; n++) { mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); @@ -83,8 +82,6 @@ async function main() { for (let ei = 0; ei < employees.length; ei++) { const e = employees[ei]; - const tss = tsByEmp.get(e.id) ?? []; - if (!tss.length) continue; const baseStartHour = 6 + (ei % 5); const baseStartMinute = (ei * 15) % 60; @@ -92,20 +89,22 @@ async function main() { for (let di = 0; di < weekDays.length; di++) { const date = weekDays[di]; - // Tirage aléatoire du bank_code + // 1) Trouver/Créer le timesheet de CETTE semaine pour CET employé + const weekStart = mondayOfThisWeekUTC(date); + const ts = await getOrCreateTimesheet(e.id, weekStart); + + // 2) Tirage aléatoire du bank_code const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)]; const bank_code_id = bcMap.get(randomCode)!; + // 3) Horaire const duration = rndInt(4, 10); - const dayWeekOffset = (di + wi + (ei % 3)) % 3; const startH = Math.min(12, baseStartHour + dayWeekOffset); const startM = baseStartMinute; const endH = startH + duration; const endM = startM; - const ts = tss[(di + wi) % tss.length]; - await prisma.shifts.create({ data: { timesheet_id: ts.id, diff --git a/prisma/mock-seeds-scripts/12-expenses.ts b/prisma/mock-seeds-scripts/12-expenses.ts index 3b4534f..848daae 100644 --- a/prisma/mock-seeds-scripts/12-expenses.ts +++ b/prisma/mock-seeds-scripts/12-expenses.ts @@ -2,11 +2,11 @@ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); -// Lundi (UTC) de la semaine courante +// Lundi (UTC) de la date fournie function mondayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); - const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... - const diffToMonday = (day + 6) % 7; // 0 si lundi + const day = d.getUTCDay(); + const diffToMonday = (day + 6) % 7; d.setUTCDate(d.getUTCDate() - diffToMonday); d.setUTCHours(0, 0, 0, 0); return d; @@ -27,7 +27,17 @@ function rndInt(min: number, max: number) { } function rndAmount(minCents: number, maxCents: number) { const cents = rndInt(minCents, maxCents); - return (cents / 100).toFixed(2); // string "123.45" + return (cents / 100).toFixed(2); +} + +// Helper: garantit le timesheet de la semaine (upsert) +async function getOrCreateTimesheet(employee_id: number, start_date: Date) { + return prisma.timesheets.upsert({ + where: { employee_id_start_date: { employee_id, start_date } }, + update: {}, + create: { employee_id, start_date, is_approved: Math.random() < 0.3 }, + select: { id: true }, + }); } async function main() { @@ -55,43 +65,35 @@ async function main() { let created = 0; for (const e of employees) { - // Choisir un timesheet (le plus ancien, ou change 'asc'→'desc' si tu préfères le plus récent) - const ts = await prisma.timesheets.findFirst({ - where: { employee_id: e.id }, - select: { id: true }, - orderBy: { id: 'asc' }, - }); - if (!ts) continue; + // 1) Semaine courante → assurer le timesheet de la semaine + const weekStart = mondayOfThisWeekUTC(); + const ts = await getOrCreateTimesheet(e.id, weekStart); - // Si l’employé a déjà une dépense cette semaine, on n’en recrée pas (≥1 garanti) + // 2) Skip si l’employé a déjà une dépense cette semaine (on garantit ≥1) const already = await prisma.expenses.findFirst({ - where: { - timesheet_id: ts.id, - date: { gte: monday, lte: friday }, - }, + where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } }, select: { id: true }, }); if (already) continue; - // Choix aléatoire du code + jour + // 3) Choix aléatoire du code + jour const randomCode = BANKS[Math.floor(Math.random() * BANKS.length)]; const bank_code_id = bcMap.get(randomCode)!; const date = weekDays[Math.floor(Math.random() * weekDays.length)]; - // Montant aléatoire (ranges par défaut en $ — ajuste au besoin) - // (ex.: G57 plus petit, G517 remboursement plus large) + // 4) Montant varié const amount = randomCode === 'G56' ? rndAmount(1000, 7500) // 10.00..75.00 - : rndAmount(2000, 25000); // 20.00..250.00 pour les autres + : rndAmount(2000, 25000); // 20.00..250.00 await prisma.expenses.create({ data: { timesheet_id: ts.id, bank_code_id, date, - amount, // stocké en string - attachement: null, // garde le champ tel quel si typo volontaire + amount, + attachement: null, description: `Expense ${randomCode} ${amount}$ (emp ${e.id})`, is_approved: Math.random() < 0.6, supervisor_comment: Math.random() < 0.2 ? 'OK' : null, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 613be4a..8d9e700 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -149,12 +149,14 @@ model Timesheets { id Int @id @default(autoincrement()) employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id]) employee_id Int + start_date DateTime @db.Date is_approved Boolean @default(false) shift Shifts[] @relation("ShiftTimesheet") expense Expenses[] @relation("ExpensesTimesheet") archive TimesheetsArchive[] @relation("TimesheetsToArchive") + @@unique([employee_id, start_date], name: "employee_id_start_date") @@map("timesheets") } diff --git a/src/modules/pay-periods/services/pay-periods-query.service.ts b/src/modules/pay-periods/services/pay-periods-query.service.ts index 9e7cb9b..f0f306a 100644 --- a/src/modules/pay-periods/services/pay-periods-query.service.ts +++ b/src/modules/pay-periods/services/pay-periods-query.service.ts @@ -233,8 +233,7 @@ export class PayPeriodsQueryService { const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase(); switch (categorie) { case "EVENING": record.evening_hours += hours; break; - case "EMERGENCY": - case "URGENT": record.emergency_hours += hours; break; + case "EMERGENCY": record.emergency_hours += hours; break; case "OVERTIME": record.overtime_hours += hours; break; default: record.regular_hours += hours; break; } diff --git a/src/modules/timesheets/controllers/timesheets.controller.ts b/src/modules/timesheets/controllers/timesheets.controller.ts index 7280c3c..d575d9b 100644 --- a/src/modules/timesheets/controllers/timesheets.controller.ts +++ b/src/modules/timesheets/controllers/timesheets.controller.ts @@ -20,14 +20,14 @@ export class TimesheetsController { private readonly timesheetsCommand: TimesheetsCommandService, ) {} - @Post() - //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) - @ApiOperation({ summary: 'Create timesheet' }) - @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto }) - @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) - create(@Body() dto: CreateTimesheetDto): Promise { - return this.timesheetsQuery.create(dto); - } + // @Post() + // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) + // @ApiOperation({ summary: 'Create timesheet' }) + // @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto }) + // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) + // create(@Body() dto: CreateTimesheetDto): Promise { + // return this.timesheetsQuery.create(dto); + // } @Get() //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) diff --git a/src/modules/timesheets/services/timesheets-query.service.ts b/src/modules/timesheets/services/timesheets-query.service.ts index 88b725a..c28c8fd 100644 --- a/src/modules/timesheets/services/timesheets-query.service.ts +++ b/src/modules/timesheets/services/timesheets-query.service.ts @@ -17,16 +17,16 @@ export class TimesheetsQueryService { private readonly overtime: OvertimeService, ) {} - async create(dto : CreateTimesheetDto): Promise { - const { employee_id, is_approved } = dto; - return this.prisma.timesheets.create({ - data: { employee_id, is_approved: is_approved ?? false }, - include: { - employee: { include: { user: true } - }, - }, - }); - } + // async create(dto : CreateTimesheetDto): Promise { + // const { employee_id, is_approved } = dto; + // return this.prisma.timesheets.create({ + // data: { employee_id, is_approved: is_approved ?? false }, + // include: { + // employee: { include: { user: true } + // }, + // }, + // }); + // } async findAll(year: number, period_no: number, email: string): Promise { //finds the employee