fix(seeds): fix timesheet seeds

This commit is contained in:
Matthieu Haineault 2025-08-29 11:44:04 -04:00
parent 18c1ce38be
commit c52de6ecb8
9 changed files with 116 additions and 107 deletions

View File

@ -425,44 +425,6 @@
} }
}, },
"/timesheets": { "/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": { "get": {
"operationId": "TimesheetsController_getPeriodByQuery", "operationId": "TimesheetsController_getPeriodByQuery",
"parameters": [ "parameters": [

View File

@ -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");

View File

@ -2,26 +2,59 @@ import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient(); 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() { async function main() {
const employees = await prisma.employees.findMany({ select: { id: true } }); 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[] = []; const rows: Prisma.TimesheetsCreateManyInput[] = [];
// 8 timesheets / employee
for (const e of employees) { for (const e of employees) {
for (let i = 0; i < 16; i++) { for (const monday of mondays) {
const is_approved = Math.random() < 0.3; rows.push({
rows.push({ employee_id: e.id, is_approved }); 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) { if (rows.length) {
await prisma.timesheets.createMany({ data: rows }); await prisma.timesheets.createMany({ data: rows, skipDuplicates: true });
} }
const total = await prisma.timesheets.count(); 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()); main().finally(() => prisma.$disconnect());

View File

@ -6,10 +6,12 @@ const prisma = new PrismaClient();
const PREVIOUS_WEEKS = 5; const PREVIOUS_WEEKS = 5;
const INCLUDE_CURRENT = false; const INCLUDE_CURRENT = false;
// Times-only via Date (UTC 1970-01-01)
function timeAt(hour: number, minute: number) { function timeAt(hour: number, minute: number) {
return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); return new Date(Date.UTC(1970, 0, 1, hour, minute, 0));
} }
// Lundi (UTC) de la date fournie
function mondayOfThisWeekUTC(now = new Date()) { function mondayOfThisWeekUTC(now = new Date()) {
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
const day = d.getUTCDay(); const day = d.getUTCDay();
@ -37,6 +39,16 @@ function rndInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min; 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() { async function main() {
// Bank codes utilisés // Bank codes utilisés
const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const; const BANKS = ['G1', 'G56', 'G48', 'G700', 'G105', 'G305', 'G43'] as const;
@ -55,21 +67,8 @@ async function main() {
return; return;
} }
const tsByEmp = new Map<number, { id: number }[]>();
{
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 mondayThisWeek = mondayOfThisWeekUTC();
const mondays: Date[] = []; const mondays: Date[] = [];
if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); if (INCLUDE_CURRENT) mondays.push(mondayThisWeek);
for (let n = 1; n <= PREVIOUS_WEEKS; n++) { for (let n = 1; n <= PREVIOUS_WEEKS; n++) {
mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); mondays.push(mondayNWeeksBefore(mondayThisWeek, n));
@ -83,8 +82,6 @@ async function main() {
for (let ei = 0; ei < employees.length; ei++) { for (let ei = 0; ei < employees.length; ei++) {
const e = employees[ei]; const e = employees[ei];
const tss = tsByEmp.get(e.id) ?? [];
if (!tss.length) continue;
const baseStartHour = 6 + (ei % 5); const baseStartHour = 6 + (ei % 5);
const baseStartMinute = (ei * 15) % 60; const baseStartMinute = (ei * 15) % 60;
@ -92,20 +89,22 @@ async function main() {
for (let di = 0; di < weekDays.length; di++) { for (let di = 0; di < weekDays.length; di++) {
const date = weekDays[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 randomCode = BANKS[Math.floor(Math.random() * BANKS.length)];
const bank_code_id = bcMap.get(randomCode)!; const bank_code_id = bcMap.get(randomCode)!;
// 3) Horaire
const duration = rndInt(4, 10); const duration = rndInt(4, 10);
const dayWeekOffset = (di + wi + (ei % 3)) % 3; const dayWeekOffset = (di + wi + (ei % 3)) % 3;
const startH = Math.min(12, baseStartHour + dayWeekOffset); const startH = Math.min(12, baseStartHour + dayWeekOffset);
const startM = baseStartMinute; const startM = baseStartMinute;
const endH = startH + duration; const endH = startH + duration;
const endM = startM; const endM = startM;
const ts = tss[(di + wi) % tss.length];
await prisma.shifts.create({ await prisma.shifts.create({
data: { data: {
timesheet_id: ts.id, timesheet_id: ts.id,

View File

@ -2,11 +2,11 @@ import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// Lundi (UTC) de la semaine courante // Lundi (UTC) de la date fournie
function mondayOfThisWeekUTC(now = new Date()) { function mondayOfThisWeekUTC(now = new Date()) {
const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... const day = d.getUTCDay();
const diffToMonday = (day + 6) % 7; // 0 si lundi const diffToMonday = (day + 6) % 7;
d.setUTCDate(d.getUTCDate() - diffToMonday); d.setUTCDate(d.getUTCDate() - diffToMonday);
d.setUTCHours(0, 0, 0, 0); d.setUTCHours(0, 0, 0, 0);
return d; return d;
@ -27,7 +27,17 @@ function rndInt(min: number, max: number) {
} }
function rndAmount(minCents: number, maxCents: number) { function rndAmount(minCents: number, maxCents: number) {
const cents = rndInt(minCents, maxCents); 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() { async function main() {
@ -55,43 +65,35 @@ async function main() {
let created = 0; let created = 0;
for (const e of employees) { for (const e of employees) {
// Choisir un timesheet (le plus ancien, ou change 'asc'→'desc' si tu préfères le plus récent) // 1) Semaine courante → assurer le timesheet de la semaine
const ts = await prisma.timesheets.findFirst({ const weekStart = mondayOfThisWeekUTC();
where: { employee_id: e.id }, const ts = await getOrCreateTimesheet(e.id, weekStart);
select: { id: true },
orderBy: { id: 'asc' },
});
if (!ts) continue;
// Si lemployé a déjà une dépense cette semaine, on nen recrée pas (≥1 garanti) // 2) Skip si lemployé a déjà une dépense cette semaine (on garantit ≥1)
const already = await prisma.expenses.findFirst({ const already = await prisma.expenses.findFirst({
where: { where: { timesheet_id: ts.id, date: { gte: monday, lte: friday } },
timesheet_id: ts.id,
date: { gte: monday, lte: friday },
},
select: { id: true }, select: { id: true },
}); });
if (already) continue; 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 randomCode = BANKS[Math.floor(Math.random() * BANKS.length)];
const bank_code_id = bcMap.get(randomCode)!; const bank_code_id = bcMap.get(randomCode)!;
const date = weekDays[Math.floor(Math.random() * weekDays.length)]; const date = weekDays[Math.floor(Math.random() * weekDays.length)];
// Montant aléatoire (ranges par défaut en $ — ajuste au besoin) // 4) Montant varié
// (ex.: G57 plus petit, G517 remboursement plus large)
const amount = const amount =
randomCode === 'G56' randomCode === 'G56'
? rndAmount(1000, 7500) // 10.00..75.00 ? 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({ await prisma.expenses.create({
data: { data: {
timesheet_id: ts.id, timesheet_id: ts.id,
bank_code_id, bank_code_id,
date, date,
amount, // stocké en string amount,
attachement: null, // garde le champ tel quel si typo volontaire attachement: null,
description: `Expense ${randomCode} ${amount}$ (emp ${e.id})`, description: `Expense ${randomCode} ${amount}$ (emp ${e.id})`,
is_approved: Math.random() < 0.6, is_approved: Math.random() < 0.6,
supervisor_comment: Math.random() < 0.2 ? 'OK' : null, supervisor_comment: Math.random() < 0.2 ? 'OK' : null,

View File

@ -149,12 +149,14 @@ model Timesheets {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id]) employee Employees @relation("TimesheetEmployee", fields: [employee_id], references: [id])
employee_id Int employee_id Int
start_date DateTime @db.Date
is_approved Boolean @default(false) is_approved Boolean @default(false)
shift Shifts[] @relation("ShiftTimesheet") shift Shifts[] @relation("ShiftTimesheet")
expense Expenses[] @relation("ExpensesTimesheet") expense Expenses[] @relation("ExpensesTimesheet")
archive TimesheetsArchive[] @relation("TimesheetsToArchive") archive TimesheetsArchive[] @relation("TimesheetsToArchive")
@@unique([employee_id, start_date], name: "employee_id_start_date")
@@map("timesheets") @@map("timesheets")
} }

View File

@ -233,8 +233,7 @@ export class PayPeriodsQueryService {
const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase(); const categorie = (shift.bank_code?.categorie || "REGULAR").toUpperCase();
switch (categorie) { switch (categorie) {
case "EVENING": record.evening_hours += hours; break; case "EVENING": record.evening_hours += hours; break;
case "EMERGENCY": case "EMERGENCY": record.emergency_hours += hours; break;
case "URGENT": record.emergency_hours += hours; break;
case "OVERTIME": record.overtime_hours += hours; break; case "OVERTIME": record.overtime_hours += hours; break;
default: record.regular_hours += hours; break; default: record.regular_hours += hours; break;
} }

View File

@ -20,14 +20,14 @@ export class TimesheetsController {
private readonly timesheetsCommand: TimesheetsCommandService, private readonly timesheetsCommand: TimesheetsCommandService,
) {} ) {}
@Post() // @Post()
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) // //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)
@ApiOperation({ summary: 'Create timesheet' }) // @ApiOperation({ summary: 'Create timesheet' })
@ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto }) // @ApiResponse({ status: 201, description: 'Timesheet created', type: CreateTimesheetDto })
@ApiResponse({ status: 400, description: 'Incomplete task or invalid data' }) // @ApiResponse({ status: 400, description: 'Incomplete task or invalid data' })
create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> { // create(@Body() dto: CreateTimesheetDto): Promise<Timesheets> {
return this.timesheetsQuery.create(dto); // return this.timesheetsQuery.create(dto);
} // }
@Get() @Get()
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR) //@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)

View File

@ -17,16 +17,16 @@ export class TimesheetsQueryService {
private readonly overtime: OvertimeService, private readonly overtime: OvertimeService,
) {} ) {}
async create(dto : CreateTimesheetDto): Promise<Timesheets> { // async create(dto : CreateTimesheetDto): Promise<Timesheets> {
const { employee_id, is_approved } = dto; // const { employee_id, is_approved } = dto;
return this.prisma.timesheets.create({ // return this.prisma.timesheets.create({
data: { employee_id, is_approved: is_approved ?? false }, // data: { employee_id, is_approved: is_approved ?? false },
include: { // include: {
employee: { include: { user: true } // employee: { include: { user: true }
}, // },
}, // },
}); // });
} // }
async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> { async findAll(year: number, period_no: number, email: string): Promise<TimesheetPeriodDto> {
//finds the employee //finds the employee