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": {
"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": [

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();
// ====== 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());

View File

@ -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<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 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,

View File

@ -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 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({
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,

View File

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

View File

@ -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;
}

View File

@ -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<Timesheets> {
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<Timesheets> {
// return this.timesheetsQuery.create(dto);
// }
@Get()
//@RolesAllowed(RoleEnum.ACCOUNTING, RoleEnum.ADMIN, RoleEnum.EMPLOYEE, RoleEnum.HR, RoleEnum.SUPERVISOR)

View File

@ -17,16 +17,16 @@ export class TimesheetsQueryService {
private readonly overtime: OvertimeService,
) {}
async create(dto : CreateTimesheetDto): Promise<Timesheets> {
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<Timesheets> {
// 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<TimesheetPeriodDto> {
//finds the employee