From 77e10c67c7f3a1d1dcc92118337069226fc53b58 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 14 Oct 2025 13:28:42 -0400 Subject: [PATCH 1/7] fix(seeders): ajusted shifts and expenses seeders --- prisma/mock-seeds-scripts/09-timesheets.ts | 2 +- prisma/mock-seeds-scripts/10-shifts.ts | 275 ++++++++++----------- prisma/mock-seeds-scripts/12-expenses.ts | 71 +++--- 3 files changed, 170 insertions(+), 178 deletions(-) diff --git a/prisma/mock-seeds-scripts/09-timesheets.ts b/prisma/mock-seeds-scripts/09-timesheets.ts index 00523f8..4160de9 100644 --- a/prisma/mock-seeds-scripts/09-timesheets.ts +++ b/prisma/mock-seeds-scripts/09-timesheets.ts @@ -4,7 +4,7 @@ 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 +const INCLUDE_CURRENT = true; // true si tu veux aussi la semaine courante // Dimanche (UTC) de la semaine courante function sundayOfThisWeekUTC(now = new Date()) { diff --git a/prisma/mock-seeds-scripts/10-shifts.ts b/prisma/mock-seeds-scripts/10-shifts.ts index 9d752b0..8c0e095 100644 --- a/prisma/mock-seeds-scripts/10-shifts.ts +++ b/prisma/mock-seeds-scripts/10-shifts.ts @@ -14,26 +14,29 @@ const HARD_END = 19 * 60 + 30; // 19:30 function timeAt(hour: number, minute: number) { return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); } -function mondayOfThisWeekUTC(now = new Date()) { + +// Ancre SEMAINE = DIMANCHE (UTC) +function sundayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); - const day = d.getUTCDay(); - const diffToMonday = (day + 6) % 7; - d.setUTCDate(d.getUTCDate() - diffToMonday); + const day = d.getUTCDay(); // 0 = Dim + d.setUTCDate(d.getUTCDate() - day); // recule jusqu'au dimanche d.setUTCHours(0, 0, 0, 0); return d; } -function weekDatesFromMonday(monday: Date) { - return Array.from({ length: 5 }, (_, i) => { - const d = new Date(monday); - d.setUTCDate(monday.getUTCDate() + i); - return d; - }); -} -function mondayNWeeksBefore(monday: Date, n: number) { - const d = new Date(monday); +function sundayNWeeksBefore(sunday: Date, n: number) { + const d = new Date(sunday); d.setUTCDate(d.getUTCDate() - n * 7); return d; } +// Génère L→V à partir du dimanche (Lundi = dimanche + 1) +function weekDatesMonToFriFromSunday(sunday: Date) { + return Array.from({ length: 5 }, (_, i) => { + const d = new Date(sunday); + d.setUTCDate(sunday.getUTCDate() + (i + 1)); // +1..+5 + return d; + }); +} + function rndInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } @@ -46,12 +49,9 @@ function addMinutes(h: number, m: number, delta: number) { const mm = ((total % 60) + 60) % 60; return { h: hh, m: mm }; } -// Aligne vers le multiple de INCR le plus proche function quantize(mins: number): number { - const q = Math.round(mins / INCR) * INCR; - return q; + return Math.round(mins / INCR) * INCR; } -// Tire un multiple de INCR dans [min,max] (inclus), supposés entiers minutes function rndQuantized(min: number, max: number): number { const qmin = Math.ceil(min / INCR); const qmax = Math.floor(max / INCR); @@ -59,19 +59,9 @@ function rndQuantized(min: number, max: number): number { return q * INCR; } -// 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 (pondérés: surtout G1 = régulier) --- - const BANKS = ['G1', 'G56', 'G48','G105','G104', 'G305'] as const; + const BANKS = ['G1', 'G56', 'G48', 'G105', 'G104', 'G305'] as const; const WEIGHTED_CODES = [ 'G1','G1','G1','G1','G1','G1','G1','G1','G1','G1','G1','G1', 'G56','G48','G104','G105','G305','G1','G1','G1','G1','G1','G1' @@ -85,150 +75,155 @@ async function main() { for (const c of BANKS) { if (!bcMap.has(c)) throw new Error(`Bank code manquant: ${c}`); } + for (const c of Array.from(new Set(WEIGHTED_CODES))) { + if (!bcMap.has(c)) throw new Error(`Bank code manquant dans WEIGHTED_CODES: ${c}`); + } - const employees = await prisma.employees.findMany({ select: { id: true } }); - if (!employees.length) { - console.log('Aucun employé — rien à insérer.'); + // ====== Fenêtre de semaines à remplir (d'après les timesheets existants) ====== + const sundayThisWeek = sundayOfThisWeekUTC(); + const minSunday = sundayNWeeksBefore(sundayThisWeek, PREVIOUS_WEEKS); + const maxSunday = sundayThisWeek; + + // Récupère les timesheets existants dans la fenêtre (sans en créer) + const timesheets = await prisma.timesheets.findMany({ + where: { + start_date: { + gte: minSunday, + lte: INCLUDE_CURRENT ? maxSunday : sundayNWeeksBefore(maxSunday, 1), // exclut la semaine courante si demandé + }, + }, + select: { id: true, employee_id: true, start_date: true }, + orderBy: [{ start_date: 'desc' }, { employee_id: 'asc' }], + }); + + if (!timesheets.length) { + console.log('Aucun timesheet existant trouvé dans la fenêtre demandée — aucun shift créé.'); return; } - 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)); - let created = 0; - for (let wi = 0; wi < mondays.length; wi++) { - const monday = mondays[wi]; - const days = weekDatesFromMonday(monday); + // Pour chaque timesheet existant, on génère les shifts L→V rattachés à son id + for (const ts of timesheets) { + const sunday = new Date(ts.start_date); // ancre = dimanche + const days = weekDatesMonToFriFromSunday(sunday); // L→V - for (let ei = 0; ei < employees.length; ei++) { - const e = employees[ei]; + // Optionnel : si tu veux éviter de dupliquer des shifts, décommente : + // const existingCount = await prisma.shifts.count({ where: { timesheet_id: ts.id } }); + // if (existingCount > 0) continue; - // Cible hebdo 35–45h, multiple de 15 min - const weeklyTargetMin = rndQuantized(35 * 60, 45 * 60); + // On paramètre le pattern à partir de l'employee_id pour varier un peu + const baseStartH = 7 + (ts.employee_id % 3); // 7,8,9 + const baseStartM = ((ts.employee_id * 15) % 60); // 0,15,30,45 + const weeklyTargetMin = rndQuantized(35 * 60, 45 * 60); - // Start de base (7:00, 7:15, 7:30, 7:45, 8:00, 8:15, 8:30, 8:45, 9:00 ...) - const baseStartH = 7 + (ei % 3); // 7,8,9 - const baseStartM = ( (ei * 15) % 60 ); // aligné 15 min - - // Planification journalière (5 jours) ~8h ± 45 min, quantisée 15 min - const plannedDaily: number[] = []; - for (let d = 0; d < 5; d++) { - const jitter = rndInt(-3, 3) * INCR; // -45..+45 par pas de 15 - const base = 8 * 60 + jitter; - plannedDaily.push(quantize(clamp(base, DAY_MIN, DAY_MAX))); - } - - // Ajuster le 5e jour pour atteindre la cible hebdo exactement (par pas de 15) - const sumFirst4 = plannedDaily.slice(0, 4).reduce((a, b) => a + b, 0); - plannedDaily[4] = quantize(clamp(weeklyTargetMin - sumFirst4, DAY_MIN, DAY_MAX)); - - // Corriger le petit écart restant (devrait être multiple de 15) en redistribuant ±15 - let diff = weeklyTargetMin - plannedDaily.reduce((a, b) => a + b, 0); - const step = diff > 0 ? INCR : -INCR; - let guard = 100; // anti-boucle - while (diff !== 0 && guard-- > 0) { - for (let d = 0; d < 5 && diff !== 0; d++) { - const next = plannedDaily[d] + step; - if (next >= DAY_MIN && next <= DAY_MAX) { - plannedDaily[d] = next; - diff -= step; - } + // Planification journalière (5 jours) ~8h ± 45 min + const plannedDaily: number[] = []; + for (let d = 0; d < 5; d++) { + const jitter = rndInt(-3, 3) * INCR; // -45..+45 + const base = 8 * 60 + jitter; + plannedDaily.push(quantize(clamp(base, DAY_MIN, DAY_MAX))); + } + // Ajuste le 5e jour pour matcher la cible hebdo + const sumFirst4 = plannedDaily.slice(0, 4).reduce((a, b) => a + b, 0); + plannedDaily[4] = quantize(clamp(weeklyTargetMin - sumFirst4, DAY_MIN, DAY_MAX)); + // Fine tuning ±15 + let diff = weeklyTargetMin - plannedDaily.reduce((a, b) => a + b, 0); + const step = diff > 0 ? INCR : -INCR; + let guard = 100; + while (diff !== 0 && guard-- > 0) { + for (let d = 0; d < 5 && diff !== 0; d++) { + const next = plannedDaily[d] + step; + if (next >= DAY_MIN && next <= DAY_MAX) { + plannedDaily[d] = next; + diff -= step; } } + } - // Upsert du timesheet (semaine) - const ts = await getOrCreateTimesheet(e.id, mondayOfThisWeekUTC(days[0])); + for (let di = 0; di < 5; di++) { + const date = days[di]; // Lundi..Vendredi + const targetWorkMin = plannedDaily[di]; - for (let di = 0; di < 5; di++) { - const date = days[di]; - const targetWorkMin = plannedDaily[di]; // multiple de 15 + // Départ ~ base + jitter + const startJitter = rndInt(-1, 2) * INCR; // -15,0,+15,+30 + const { h: startH, m: startM } = addMinutes(baseStartH, baseStartM, startJitter); - // Départ ~ base + jitter (par pas de 15 min aussi) - const startJitter = rndInt(-1, 2) * INCR; // -15,0,+15,+30 - const { h: startH, m: startM } = addMinutes(baseStartH, baseStartM, startJitter); + // Pause: entre 11:00 et 14:00, bornée par start+3h .. start+6h + const earliestLunch = Math.max((startH * 60 + startM) + 3 * 60, 11 * 60); + const latestLunch = Math.min((startH * 60 + startM) + 6 * 60, 14 * 60); + const lunchStartMin = rndQuantized(earliestLunch, latestLunch); + const lunchDur = rndQuantized(30, 120); + const lunchEndMin = lunchStartMin + lunchDur; - // Pause: entre 11:00 et 14:00, mais pas avant start+3h ni après start+6h (le tout quantisé 15) - const earliestLunch = Math.max((startH * 60 + startM) + 3 * 60, 11 * 60); - const latestLunch = Math.min((startH * 60 + startM) + 6 * 60, 14 * 60); - const lunchStartMin = rndQuantized(earliestLunch, latestLunch); - const lunchDur = rndQuantized(30, 120); // 30..120 min en pas de 15 - const lunchEndMin = lunchStartMin + lunchDur; + // Travail = (lunchStart - start) + (end - lunchEnd) + const morningWork = Math.max(0, lunchStartMin - (startH * 60 + startM)); + let afternoonWork = Math.max(60, targetWorkMin - morningWork); + if (afternoonWork % INCR !== 0) afternoonWork = quantize(afternoonWork); - // Travail = (lunchStart - start) + (end - lunchEnd) - const morningWork = Math.max(0, lunchStartMin - (startH * 60 + startM)); // multiple de 15 - let afternoonWork = Math.max(60, targetWorkMin - morningWork); // multiple de 15 (diff de deux multiples de 15) - if (afternoonWork % INCR !== 0) { - // sécurité (ne devrait pas arriver) - afternoonWork = quantize(afternoonWork); - } + // Fin quantisée + borne max + const endMinRaw = lunchEndMin + afternoonWork; + const endMin = Math.min(endMinRaw, HARD_END); - // Fin de journée (quantisée par construction) - const endMinRaw = lunchEndMin + afternoonWork; - const endMin = Math.min(endMinRaw, HARD_END); + // Bank codes variés + const bcMorningCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; + const bcAfternoonCode= WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; + const bcMorningId = bcMap.get(bcMorningCode)!; + const bcAfternoonId = bcMap.get(bcAfternoonCode)!; - // Bank codes variés - const bcMorningCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; - const bcAfternoonCode = WEIGHTED_CODES[rndInt(0, WEIGHTED_CODES.length - 1)]; - const bcMorningId = bcMap.get(bcMorningCode)!; - const bcAfternoonId = bcMap.get(bcAfternoonCode)!; + // Shift matin + const lunchStartHM = { h: Math.floor(lunchStartMin / 60), m: lunchStartMin % 60 }; + await prisma.shifts.create({ + data: { + timesheet_id: ts.id, + bank_code_id: bcMorningId, + comment: `Matin J${di + 1} (sem ${sunday.toISOString().slice(0, 10)}) emp ${ts.employee_id} - ${bcMorningCode}`, + date, + start_time: timeAt(startH, startM), + end_time: timeAt(lunchStartHM.h, lunchStartHM.m), + is_approved: Math.random() < 0.6, + }, + }); + created++; - // Shift matin - const lunchStartHM = { h: Math.floor(lunchStartMin / 60), m: lunchStartMin % 60 }; + // Shift après-midi (si >= 30 min) + const pmDuration = endMin - lunchEndMin; + if (pmDuration >= 30) { + const lunchEndHM = { h: Math.floor(lunchEndMin / 60), m: lunchEndMin % 60 }; + const finalEndHM = { h: Math.floor(endMin / 60), m: endMin % 60 }; await prisma.shifts.create({ data: { timesheet_id: ts.id, - bank_code_id: bcMorningId, - comment: `Matin J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} - ${bcMorningCode}`, + bank_code_id: bcAfternoonId, + comment: `Après-midi J${di + 1} (sem ${sunday.toISOString().slice(0, 10)}) emp ${ts.employee_id} — ${bcAfternoonCode}`, date, - start_time: timeAt(startH, startM), - end_time: timeAt(lunchStartHM.h, lunchStartHM.m), + start_time: timeAt(lunchEndHM.h, lunchEndHM.m), + end_time: timeAt(finalEndHM.h, finalEndHM.m), + is_approved: Math.random() < 0.6, + }, + }); + created++; + } else { + // Fallback: un seul shift couvrant la journée + const fallbackEnd = addMinutes(startH, startM, targetWorkMin + lunchDur); + await prisma.shifts.create({ + data: { + timesheet_id: ts.id, + bank_code_id: bcMap.get('G1')!, + comment: `Fallback J${di + 1} (sem ${sunday.toISOString().slice(0, 10)}) emp ${ts.employee_id} — G1`, + date, + start_time: timeAt(startH, startM), + end_time: timeAt(fallbackEnd.h, fallbackEnd.m), is_approved: Math.random() < 0.6, }, }); created++; - - // Shift après-midi (si >= 30 min — sera de toute façon multiple de 15) - const pmDuration = endMin - lunchEndMin; - if (pmDuration >= 30) { - const lunchEndHM = { h: Math.floor(lunchEndMin / 60), m: lunchEndMin % 60 }; - const finalEndHM = { h: Math.floor(endMin / 60), m: endMin % 60 }; - await prisma.shifts.create({ - data: { - timesheet_id: ts.id, - bank_code_id: bcAfternoonId, - comment: `Après-midi J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — ${bcAfternoonCode}`, - date, - start_time: timeAt(lunchEndHM.h, lunchEndHM.m), - end_time: timeAt(finalEndHM.h, finalEndHM.m), - is_approved: Math.random() < 0.6, - }, - }); - created++; - } else { - // Fallback très rare : un seul shift couvrant la journée (tout en multiples de 15) - const fallbackEnd = addMinutes(startH, startM, targetWorkMin + lunchDur); - await prisma.shifts.create({ - data: { - timesheet_id: ts.id, - bank_code_id: bcMap.get('G1')!, - comment: `Fallback J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} — G1`, - date, - start_time: timeAt(startH, startM), - end_time: timeAt(fallbackEnd.h, fallbackEnd.m), - is_approved: Math.random() < 0.6, - }, - }); - created++; - } } } } const total = await prisma.shifts.count(); - console.log(`✓ Shifts créés: ${created} | total en DB: ${total} (${INCLUDE_CURRENT ? 'inclut semaine courante, ' : ''}${PREVIOUS_WEEKS} sem passées, L→V, 2 shifts/jour, pas de décimaux foireux})`); + console.log(`✓ Shifts créés: ${created} | total en DB: ${total} (${INCLUDE_CURRENT ? 'inclut semaine courante, ' : ''}${PREVIOUS_WEEKS} sem passées, Dim ancre + L→V, 2 shifts/jour, **aucun timesheet créé**})`); } main().finally(() => prisma.$disconnect()); diff --git a/prisma/mock-seeds-scripts/12-expenses.ts b/prisma/mock-seeds-scripts/12-expenses.ts index 926a52f..9b7bf22 100644 --- a/prisma/mock-seeds-scripts/12-expenses.ts +++ b/prisma/mock-seeds-scripts/12-expenses.ts @@ -1,3 +1,4 @@ +import { NotFoundException } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); @@ -7,25 +8,24 @@ const WEEKS_BACK = 4; // 4 semaines avant + semaine courante const INCLUDE_CURRENT = true; // inclure la semaine courante const STEP_CENTS = 25; // montants en quarts de dollar (.00/.25/.50/.75) -// ====== Helpers dates ====== -function mondayOfThisWeekUTC(now = new Date()) { +// ====== Helpers dates (ancre DIMANCHE UTC) ====== +function sundayOfThisWeekUTC(now = new Date()) { const d = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())); - const day = d.getUTCDay(); - const diffToMonday = (day + 6) % 7; - d.setUTCDate(d.getUTCDate() - diffToMonday); + const day = d.getUTCDay(); // 0=Dim, 1=Lun, ... + d.setUTCDate(d.getUTCDate() - day); // recule jusqu'au dimanche d.setUTCHours(0, 0, 0, 0); return d; } -function mondayNWeeksBefore(monday: Date, n: number) { - const d = new Date(monday); - d.setUTCDate(monday.getUTCDate() - n * 7); +function sundayNWeeksBefore(sunday: Date, n: number) { + const d = new Date(sunday); + d.setUTCDate(d.getUTCDate() - n * 7); return d; } -// L→V (UTC minuit) -function weekDatesMonToFri(monday: Date) { +// Génère L→V à partir du dimanche (Lundi = dimanche + 1) +function weekDatesMonToFriFromSunday(sunday: Date) { return Array.from({ length: 5 }, (_, i) => { - const d = new Date(monday); - d.setUTCDate(monday.getUTCDate() + i); + const d = new Date(sunday); + d.setUTCDate(sunday.getUTCDate() + (i + 1)); // +1..+5 return d; }); } @@ -34,7 +34,7 @@ function weekDatesMonToFri(monday: Date) { function rndInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } -// String "xx.yy" à partir de cents ENTiers (jamais de float) +// String "xx.yy" à partir de cents entiers (pas de float binaire en DB) function centsToAmountString(cents: number): string { const sign = cents < 0 ? '-' : ''; const abs = Math.abs(cents); @@ -42,12 +42,9 @@ function centsToAmountString(cents: number): string { const c = abs % 100; return `${sign}${dollars}.${c.toString().padStart(2, '0')}`; } - function to2(value: string): string { - // normalise au cas où (sécurité) return (Math.round(parseFloat(value) * 100) / 100).toFixed(2); } - // Tire un multiple de STEP_CENTS entre minCents et maxCents (inclus) function rndQuantizedCents(minCents: number, maxCents: number, step = STEP_CENTS): number { const qmin = Math.ceil(minCents / step); @@ -59,12 +56,10 @@ function rndAmount(minCents: number, maxCents: number): string { return centsToAmountString(rndQuantizedCents(minCents, maxCents)); } -// ====== Timesheet upsert ====== -async function getOrCreateTimesheet(employee_id: number, start_date: Date) { - return prisma.timesheets.upsert({ +// ====== Lookup timesheet (AUCUNE création ici) ====== +async function findTimesheet(employee_id: number, start_date: Date) { + return prisma.timesheets.findUnique({ where: { employee_id_start_date: { employee_id, start_date } }, - update: {}, - create: { employee_id, start_date, is_approved: Math.random() < 0.3 }, select: { id: true }, }); } @@ -90,21 +85,23 @@ async function main() { return; } - // Liste des lundis (courant + 4 précédents) - const mondayThisWeek = mondayOfThisWeekUTC(); - const mondays: Date[] = []; - if (INCLUDE_CURRENT) mondays.push(mondayThisWeek); - for (let n = 1; n <= WEEKS_BACK; n++) mondays.push(mondayNWeeksBefore(mondayThisWeek, n)); + // Fenêtre de semaines ancrées au DIMANCHE + const sundayThisWeek = sundayOfThisWeekUTC(); + const sundays: Date[] = []; + if (INCLUDE_CURRENT) sundays.push(sundayThisWeek); + for (let n = 1; n <= WEEKS_BACK; n++) sundays.push(sundayNWeeksBefore(sundayThisWeek, n)); let created = 0; - for (const monday of mondays) { - const weekDays = weekDatesMonToFri(monday); + for (const sunday of sundays) { + const weekDays = weekDatesMonToFriFromSunday(sunday); // L→V + const monday = weekDays[0]; const friday = weekDays[4]; for (const e of employees) { - // Upsert timesheet pour CETTE semaine/employee - const ts = await getOrCreateTimesheet(e.id, monday); + // Utiliser le timesheet EXISTANT (ancré au DIMANCHE) + const ts = await findTimesheet(e.id, sunday); + if (!ts) throw new NotFoundException(`Timesheet manquant pour emp ${e.id} @ ${sunday.toISOString().slice(0,10)}`); // Idempotence: si déjà au moins une expense L→V, on skip la semaine const already = await prisma.expenses.findFirst({ @@ -122,22 +119,22 @@ async function main() { const code = BANKS[rndInt(0, BANKS.length - 1)]; const bank_code_id = bcMap.get(code)!; - // Montants (cents) quantisés à 25¢ => aucun flottant binaire plus tard + // Montants (cents) quantisés à 25¢ => aucun flottant binaire en DB let amount: string = '0.00'; let mileage: string = '0.00'; switch (code) { case 'G503': // kilométrage - mileage = to2(rndAmount(1000, 7500)); // 10.00 à 75.00 + mileage = to2(rndAmount(1000, 7500)); // 10.00 à 75.00 break; case 'G502': // per_diem - amount = to2(rndAmount(1500, 3000)); // 15.00 à 30.00 + amount = to2(rndAmount(1500, 3000)); // 15.00 à 30.00 break; - case 'G202': // on_call /prime de garde - amount = to2(rndAmount(2000, 15000)); // 20.00 à 150.00 + case 'G202': // on_call / prime de garde + amount = to2(rndAmount(2000, 15000)); // 20.00 à 150.00 break; case 'G517': // expenses default: - amount = to2(rndAmount(500, 5000)); // 5.00 à 50.00 + amount = to2(rndAmount(500, 5000)); // 5.00 à 50.00 break; } @@ -160,7 +157,7 @@ async function main() { } const total = await prisma.expenses.count(); - console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (sem courante + ${WEEKS_BACK} précédentes, L→V uniquement, montants en quarts de dollar)`); + console.log(`✓ Expenses: ${created} nouvelles lignes, ${total} total rows (ancre dimanche, L→V, sem courante ${INCLUDE_CURRENT ? 'incluse' : 'exclue'} + ${WEEKS_BACK} précédentes)`); } main().finally(() => prisma.$disconnect()); From 9ad4e63485d8ff6a300bce75e2f2d345ad1cd013 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 14 Oct 2025 14:18:31 -0400 Subject: [PATCH 2/7] refactor(shifts): removed return statement of Delete shift function --- package.json | 2 +- src/modules/shifts/services/shifts-command.service.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 76aac8f..635a791 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "seed:12": "tsx prisma/mock-seeds-scripts/12-expenses.ts", "seed:13": "tsx prisma/mock-seeds-scripts/13-expenses-archive.ts", "seed:14": "tsx prisma/mock-seeds-scripts/14-oauth-sessions.ts", - "seed:all": "npm run seed:01 && npm run seed:02 && npm run seed:03 && npm run seed:04 && npm run seed:05 && npm run seed:06 && npm run seed:07 && npm run seed:08 && npm run seed:09 && npm run seed:10 && npm run seed:11 && npm run seed:12 && npm run seed:13 && npm run seed:14", + "seed:all": "npm run seed:01 && npm run seed:02 && npm run seed:03 && npm run seed:09 && npm run seed:10 && npm run seed:12 && npm run seed:14", "db:reseed": "npm run db:reset && npm run seed:all" }, "dependencies": { diff --git a/src/modules/shifts/services/shifts-command.service.ts b/src/modules/shifts/services/shifts-command.service.ts index 06eeece..91fe30d 100644 --- a/src/modules/shifts/services/shifts-command.service.ts +++ b/src/modules/shifts/services/shifts-command.service.ts @@ -164,7 +164,7 @@ export class ShiftsCommandService extends BaseApprovalService { email: string, date_iso: string, dto: UpsertShiftDto, - ): Promise<{ day: DayShiftResponse[]; }>{ + ){ return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); //converts to Date format const employee_id = await this.emailResolver.findIdByEmail(email); @@ -185,8 +185,6 @@ export class ShiftsCommandService extends BaseApprovalService { await tx.shifts.delete({ where: { id: existing.id } }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); - return { day: await this.helpersService.mapDay(fresh_shift)}; }); } } From cefba7a2dd0090350461110e6edd234b24c4298d Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 14 Oct 2025 14:29:37 -0400 Subject: [PATCH 3/7] fix(shifts): fixed findTimesheet to findUnique instead of upsert --- src/modules/shifts/helpers/shifts.helpers.ts | 10 ------- .../shifts/services/shifts-command.service.ts | 27 ++++++++++--------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/modules/shifts/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts index c5da538..6038db7 100644 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ b/src/modules/shifts/helpers/shifts.helpers.ts @@ -18,16 +18,6 @@ export class ShiftsHelpersService { private readonly overtimeService: OvertimeService, ) { } - async findOrUpsertTimesheet(tx: Tx, employee_id: number, date_only: Date) { - const start_of_week = weekStartSunday(date_only); - return tx.timesheets.upsert({ - where: { employee_id_start_date: { employee_id, start_date: start_of_week } }, - update: {}, - create: { employee_id, start_date: start_of_week }, - select: { id: true }, - }); - } - async ensureTimesheet(tx: Tx, employee_id: number, date_only: Date) { const start_of_week = weekStartSunday(date_only); return tx.timesheets.findUnique({ diff --git a/src/modules/shifts/services/shifts-command.service.ts b/src/modules/shifts/services/shifts-command.service.ts index 91fe30d..a7e265a 100644 --- a/src/modules/shifts/services/shifts-command.service.ts +++ b/src/modules/shifts/services/shifts-command.service.ts @@ -84,18 +84,18 @@ export class ShiftsCommandService extends BaseApprovalService { ): Promise<{action: UpsertAction; day: DayShiftResponse[]}> { return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); - const { id: timesheet_id } = await this.helpersService.findOrUpsertTimesheet(tx, employee_id, date_only); - + const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); + if(!timesheet) throw new NotFoundException('Timesheet not found') const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift); const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); + const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); await this.helpersService.assertNoOverlap(day_shifts, new_norm_shift); await tx.shifts.create({ data: { - timesheet_id, + timesheet_id: timesheet.id, date: date_only, start_time: new_norm_shift.start_time, end_time: new_norm_shift.end_time, @@ -106,7 +106,7 @@ export class ShiftsCommandService extends BaseApprovalService { }, }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); + const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); return { action: 'create', day: await this.helpersService.mapDay(fresh_shift)}; }); } @@ -121,20 +121,21 @@ export class ShiftsCommandService extends BaseApprovalService { ): Promise<{ action: UpsertAction; day: DayShiftResponse[];}>{ return this.prisma.$transaction(async (tx) => { const date_only = toDateOnly(date_iso); - const { id: timesheet_id } = await this.helpersService.findOrUpsertTimesheet(tx, employee_id, date_only); + const timesheet = await this.helpersService.ensureTimesheet(tx, employee_id, date_only); + if(!timesheet) throw new NotFoundException('Timesheet not found') const old_norm_shift = await this.helpersService.normalizeRequired(dto.old_shift, 'old_shift'); const new_norm_shift = await this.helpersService.normalizeRequired(dto.new_shift, 'new_shift'); - const old_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, old_norm_shift.type, 'old_shift'); - const new_bank_code_id = await this.helpersService.resolveBankIdRequired(tx, new_norm_shift.type, 'new_shift'); + const old_bank_code = await this.typeResolver.findByType(old_norm_shift.type); + const new_bank_code = await this.typeResolver.findByType(new_norm_shift.type); - const day_shifts = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); + const day_shifts = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); const existing = await this.helpersService.findExactOldShift(tx, { - timesheet_id, + timesheet_id: timesheet.id, date_only, norm: old_norm_shift, - bank_code_id: old_bank_code_id, + bank_code_id: old_bank_code.id, }); if(!existing) throw new NotFoundException('[SHIFT_STALE]- The shift was modified or deleted by someone else'); @@ -147,11 +148,11 @@ export class ShiftsCommandService extends BaseApprovalService { end_time: new_norm_shift.end_time, is_remote: new_norm_shift.is_remote, comment: new_norm_shift.comment ?? null, - bank_code_id: new_bank_code_id, + bank_code_id: new_bank_code.id, }, }); await this.helpersService.afterWriteOvertimeAndLog(tx, employee_id, date_only); - const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet_id, date_only); + const fresh_shift = await this.helpersService.getDayShifts(tx, timesheet.id, date_only); return { action: 'update', day: await this.helpersService.mapDay(fresh_shift)}; }); From 06ad34a4c89750a548d5c97d87a69d8bc557509c Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Tue, 14 Oct 2025 16:43:18 -0400 Subject: [PATCH 4/7] refactor(shared): centralized some small logics --- .../services/expenses-command.service.ts | 4 +- .../shared/interfaces/shifts.interface.ts | 9 ++++ src/modules/shared/shared.module.ts | 2 +- .../utils/resolve-employee-timesheet.utils.ts | 42 ------------------- .../shared/utils/resolve-shifts-id.utils.ts | 29 +++++++++++++ .../shared/utils/resolve-timesheet.utils.ts | 28 +++++++++++++ .../helpers/shifts-date-time-helpers.ts | 2 +- src/modules/shifts/helpers/shifts.helpers.ts | 11 +++-- .../services/timesheets-command.service.ts | 4 +- 9 files changed, 79 insertions(+), 52 deletions(-) create mode 100644 src/modules/shared/interfaces/shifts.interface.ts delete mode 100644 src/modules/shared/utils/resolve-employee-timesheet.utils.ts create mode 100644 src/modules/shared/utils/resolve-shifts-id.utils.ts create mode 100644 src/modules/shared/utils/resolve-timesheet.utils.ts diff --git a/src/modules/expenses/services/expenses-command.service.ts b/src/modules/expenses/services/expenses-command.service.ts index bda3c90..1511eb0 100644 --- a/src/modules/expenses/services/expenses-command.service.ts +++ b/src/modules/expenses/services/expenses-command.service.ts @@ -5,7 +5,7 @@ import { UpsertExpenseDto } from "../dtos/upsert-expense.dto"; import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; import { ExpenseResponse, UpsertAction } from "../types and interfaces/expenses.types.interfaces"; import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.utils"; -import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-employee-timesheet.utils"; +import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; import { BadRequestException, Injectable, @@ -66,7 +66,7 @@ export class ExpensesCommandService extends BaseApprovalService { const employee_id = await this.emailResolver.findIdByEmail(email); //make sure a timesheet existes - const timesheet_id = await this.timesheetsResolver.ensureForDate(employee_id, date_only); + const timesheet_id = await this.timesheetsResolver.findTimesheetIdByEmail(email, date_only); if(!timesheet_id) throw new NotFoundException(`no timesheet found for employee #${employee_id}`) const {id} = timesheet_id; diff --git a/src/modules/shared/interfaces/shifts.interface.ts b/src/modules/shared/interfaces/shifts.interface.ts new file mode 100644 index 0000000..40f897e --- /dev/null +++ b/src/modules/shared/interfaces/shifts.interface.ts @@ -0,0 +1,9 @@ +export interface ShiftKey { + timesheet_id: number; + date: Date; + start_time: Date; + end_time: Date; + bank_code_id: number; + is_remote: boolean; + comment?: string | null; +} \ No newline at end of file diff --git a/src/modules/shared/shared.module.ts b/src/modules/shared/shared.module.ts index adf0b68..8d4aa95 100644 --- a/src/modules/shared/shared.module.ts +++ b/src/modules/shared/shared.module.ts @@ -1,6 +1,6 @@ import { Module } from "@nestjs/common"; import { EmailToIdResolver } from "./utils/resolve-email-id.utils"; -import { EmployeeTimesheetResolver } from "./utils/resolve-employee-timesheet.utils"; +import { EmployeeTimesheetResolver } from "./utils/resolve-timesheet.utils"; import { FullNameResolver } from "./utils/resolve-full-name.utils"; import { BankCodesResolver } from "./utils/resolve-bank-type-id.utils"; import { PrismaModule } from "src/prisma/prisma.module"; diff --git a/src/modules/shared/utils/resolve-employee-timesheet.utils.ts b/src/modules/shared/utils/resolve-employee-timesheet.utils.ts deleted file mode 100644 index eb3b305..0000000 --- a/src/modules/shared/utils/resolve-employee-timesheet.utils.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Prisma, PrismaClient } from "@prisma/client"; -import { weekStartSunday } from "src/modules/shifts/helpers/shifts-date-time-helpers"; -import { PrismaService } from "src/prisma/prisma.service"; - - -type Tx = Prisma.TransactionClient | PrismaClient; - -@Injectable() -export class EmployeeTimesheetResolver { - constructor(private readonly prisma: PrismaService) {} - - //find an existing timesheet linked to the employee - readonly ensureForDate = async (employee_id: number, date: Date, client?: Tx, - ): Promise<{id: number; start_date: Date }> => { - const db = client ?? this.prisma; - const startOfWeek = weekStartSunday(date); - const existing = await db.timesheets.findFirst({ - where: { - employee_id: employee_id, - start_date: startOfWeek, - }, - select: { - id: true, - start_date: true, - }, - }); - if(existing) return existing; - - const created = await db.timesheets.create({ - data: { - employee_id: employee_id, - start_date: startOfWeek, - }, - select: { - id: true, - start_date: true, - }, - }); - return created; - } -} \ No newline at end of file diff --git a/src/modules/shared/utils/resolve-shifts-id.utils.ts b/src/modules/shared/utils/resolve-shifts-id.utils.ts new file mode 100644 index 0000000..4d9d313 --- /dev/null +++ b/src/modules/shared/utils/resolve-shifts-id.utils.ts @@ -0,0 +1,29 @@ +import { Prisma, PrismaClient } from "@prisma/client"; +import { NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ShiftKey } from "../interfaces/shifts.interface"; + +type Tx = Prisma.TransactionClient | PrismaClient; + +export class ShiftIdResolver { + constructor(private readonly prisma: PrismaService) {} + + readonly findShiftIdByData = async ( key: ShiftKey, client?: Tx ): Promise<{id:number}> => { + const db = client ?? this.prisma; + const shift = await db.shifts.findFirst({ + where: { + timesheet_id: key.timesheet_id, + bank_code_id: key.bank_code_id, + date: key.date, + start_time: key.start_time, + end_time: key.end_time, + is_remote: key.is_remote, + comment: key.comment, + }, + select: { id: true }, + }); + + if(!shift) throw new NotFoundException(`shift not found`); + return { id: shift.id }; + }; +} \ No newline at end of file diff --git a/src/modules/shared/utils/resolve-timesheet.utils.ts b/src/modules/shared/utils/resolve-timesheet.utils.ts new file mode 100644 index 0000000..61f4ce6 --- /dev/null +++ b/src/modules/shared/utils/resolve-timesheet.utils.ts @@ -0,0 +1,28 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { Prisma, PrismaClient } from "@prisma/client"; +import { weekStartSunday } from "src/modules/shifts/helpers/shifts-date-time-helpers"; +import { PrismaService } from "src/prisma/prisma.service"; +import { EmailToIdResolver } from "./resolve-email-id.utils"; + + +type Tx = Prisma.TransactionClient | PrismaClient; + +@Injectable() +export class EmployeeTimesheetResolver { + constructor( + private readonly prisma: PrismaService, + private readonly emailResolver: EmailToIdResolver, + ) {} + + readonly findTimesheetIdByEmail = async (email: string, date: Date, client?: Tx): Promise<{id: number}> => { + const db = client ?? this.prisma; + const employee_id = await this.emailResolver.findIdByEmail(email); + const start_date = weekStartSunday(date); + const timesheet = await db.timesheets.findFirst({ + where: { employee_id : employee_id, start_date: start_date }, + select: { id: true }, + }); + if(!timesheet) throw new NotFoundException(`timesheet not found`); + return { id: timesheet.id }; + } +} \ No newline at end of file diff --git a/src/modules/shifts/helpers/shifts-date-time-helpers.ts b/src/modules/shifts/helpers/shifts-date-time-helpers.ts index cf3f26b..8bcd610 100644 --- a/src/modules/shifts/helpers/shifts-date-time-helpers.ts +++ b/src/modules/shifts/helpers/shifts-date-time-helpers.ts @@ -11,7 +11,7 @@ export function toDateOnly(ymd: string): Date { } export function weekStartSunday(date_local: Date): Date { - const start = new Date(date_local.getFullYear(), date_local.getMonth(), date_local.getDate()); + const start = new Date(Date.UTC(date_local.getFullYear(), date_local.getMonth(), date_local.getDate())); const dow = start.getDay(); // 0 = dimanche start.setDate(start.getDate() - dow); start.setHours(0, 0, 0, 0); diff --git a/src/modules/shifts/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts index 6038db7..09a17e1 100644 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ b/src/modules/shifts/helpers/shifts.helpers.ts @@ -114,10 +114,13 @@ export class ShiftsHelpersService { async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { // Switch regular → weekly overtime si > 40h await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); - const [daily, weekly] = await Promise.all([ - this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), - this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), - ]); + const daily = await this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only); + const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); + // const [daily, weekly] = await Promise.all([ + // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), + // this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only), + // ]); + return { daily, weekly }; } async mapDay( diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index 045b7b5..f8142b0 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; -import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-employee-timesheet.utils"; +import { EmployeeTimesheetResolver } from "src/modules/shared/utils/resolve-timesheet.utils"; import { getWeekEnd, getWeekStart } from "src/common/utils/date-utils"; import { parseISODate, parseHHmm } from "../utils-helpers-others/timesheet.helpers"; import { TimesheetsQueryService } from "./timesheets-query.service"; @@ -69,7 +69,7 @@ export class TimesheetsCommandService extends BaseApprovalService{ const start_week = getWeekStart(base, 0); const end_week = getWeekEnd(start_week); - const timesheet = await this.timesheetResolver.ensureForDate(employee_id, base) + const timesheet = await this.timesheetResolver.findTimesheetIdByEmail(email, base) if(!timesheet) throw new NotFoundException(`no timesheet found for employe ${employee_id}`); //validations and insertions From 119116e11dc49d2f08a03829d179273d585e2c2d Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 15 Oct 2025 14:00:28 -0400 Subject: [PATCH 5/7] feat(timesheets): preparing for Express Session requests --- .../services/overtime.service.ts | 2 +- src/modules/shifts/helpers/shifts.helpers.ts | 2 +- src/modules/timesheets/dtos/timesheet.dto.ts | 52 +++++++++++++++++++ .../services/timesheets-command.service.ts | 39 ++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/modules/timesheets/dtos/timesheet.dto.ts diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts index 6c6d1b0..24c7a75 100644 --- a/src/modules/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -14,7 +14,7 @@ export class OvertimeService { constructor(private prisma: PrismaService) {} //calculate daily overtime - async getDailyOvertimeHoursForDay(employee_id: number, date: Date): Promise { + async getDailyOvertimeHours(employee_id: number, date: Date): Promise { const shifts = await this.prisma.shifts.findMany({ where: { date: date, timesheet: { employee_id: employee_id } }, select: { start_time: true, end_time: true }, diff --git a/src/modules/shifts/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts index 09a17e1..6fb216d 100644 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ b/src/modules/shifts/helpers/shifts.helpers.ts @@ -114,7 +114,7 @@ export class ShiftsHelpersService { async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { // Switch regular → weekly overtime si > 40h await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); - const daily = await this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only); + const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); // const [daily, weekly] = await Promise.all([ // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), diff --git a/src/modules/timesheets/dtos/timesheet.dto.ts b/src/modules/timesheets/dtos/timesheet.dto.ts new file mode 100644 index 0000000..22cd1ae --- /dev/null +++ b/src/modules/timesheets/dtos/timesheet.dto.ts @@ -0,0 +1,52 @@ +export class Timesheets { + timesheet_id: number; + days: TimesheetDay[]; + weekly_hours: TotalHours[]; + weekly_expenses: TotalExpenses[]; +} + +export class TimesheetDay { + date: string; + shifts: Shift[]; + expenses: Expense[]; + daily_hours: TotalHours[]; + daily_expenses: TotalExpenses[]; +} + +export class TotalHours { + regular: number; + evening: number; + emergency: number; + overtime: number; + vacation: number; + holiday: number; + sick: number; +} +export class TotalExpenses { + expenses: number; + perd_diem: number; + on_call: number; + mileage: number; +} + +export class Shift { + date: string; + start: string; + end: string; + type: string; + is_remote: boolean; + is_approved: boolean; + shift_id?: number | null; + comment?: string | null; +} + +export class Expense { + date: string; + is_approved: boolean; + comment: string; + amount?: number; + mileage?: number; + attachment?: string; + expense_id?: number | null; + supervisor_comment?: string | null; +} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index f8142b0..179a21a 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -10,6 +10,7 @@ import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.uti import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { TimesheetMap } from "../utils-helpers-others/timesheet.types"; +import { Shift, Expense } from "../dtos/timesheet.dto"; @Injectable() export class TimesheetsCommandService extends BaseApprovalService{ @@ -50,6 +51,44 @@ export class TimesheetsCommandService extends BaseApprovalService{ return timesheet; } +/**_____________________________________________________________________________________________ + create/update/delete shifts and expenses from 1 or many timesheet(s) + + -this function receives an email and an array of timesheets + + -this function will find the timesheets with all shifts and expenses + -this function will calculate total hours, total expenses, filtered by types, + cumulate in daily and weekly. + + -the timesheet_id will be determined using the employee email + -with the timesheet_id, all shifts and expenses will be fetched + + -with shift_id and expense_id, this function will compare both + datas from the DB and from the body of the function and then: + -it will create a shift if no shift is found in the DB + -it will update a shift if a shift is found in the DB + -it will delete a shift if a shift is found and no data is received from the frontend + + This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry + This function will also be used in the modal of the timesheet validation page to + allow a supervisor to enter, modify or delete and entry of a selected employee +_____________________________________________________________________________________________*/ + +async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise { + const employee_id = await this.emailResolver.findIdByEmail(email); + + + return timesheets; +} + +async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {} + +async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {} + + + + + //_____________________________________________________________________________________________ // //_____________________________________________________________________________________________ From a563df0943e1827f2ea32550d09fb776e3e6af14 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 15 Oct 2025 14:08:59 -0400 Subject: [PATCH 6/7] feat(timesheets): preparing for Express Session requests --- .../services/overtime.service.ts | 2 +- src/modules/shifts/helpers/shifts.helpers.ts | 2 +- src/modules/timesheets/dtos/timesheet.dto.ts | 52 +++++++++++++++++++ .../services/timesheets-command.service.ts | 39 ++++++++++++++ 4 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/modules/timesheets/dtos/timesheet.dto.ts diff --git a/src/modules/business-logics/services/overtime.service.ts b/src/modules/business-logics/services/overtime.service.ts index 6c6d1b0..24c7a75 100644 --- a/src/modules/business-logics/services/overtime.service.ts +++ b/src/modules/business-logics/services/overtime.service.ts @@ -14,7 +14,7 @@ export class OvertimeService { constructor(private prisma: PrismaService) {} //calculate daily overtime - async getDailyOvertimeHoursForDay(employee_id: number, date: Date): Promise { + async getDailyOvertimeHours(employee_id: number, date: Date): Promise { const shifts = await this.prisma.shifts.findMany({ where: { date: date, timesheet: { employee_id: employee_id } }, select: { start_time: true, end_time: true }, diff --git a/src/modules/shifts/helpers/shifts.helpers.ts b/src/modules/shifts/helpers/shifts.helpers.ts index 09a17e1..6fb216d 100644 --- a/src/modules/shifts/helpers/shifts.helpers.ts +++ b/src/modules/shifts/helpers/shifts.helpers.ts @@ -114,7 +114,7 @@ export class ShiftsHelpersService { async afterWriteOvertimeAndLog(tx: Tx, employee_id: number, date_only: Date) { // Switch regular → weekly overtime si > 40h await this.overtimeService.transformRegularHoursToWeeklyOvertime(employee_id, date_only, tx); - const daily = await this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only); + const daily = await this.overtimeService.getDailyOvertimeHours(employee_id, date_only); const weekly = await this.overtimeService.getWeeklyOvertimeHours(employee_id, date_only); // const [daily, weekly] = await Promise.all([ // this.overtimeService.getDailyOvertimeHoursForDay(employee_id, date_only), diff --git a/src/modules/timesheets/dtos/timesheet.dto.ts b/src/modules/timesheets/dtos/timesheet.dto.ts new file mode 100644 index 0000000..22cd1ae --- /dev/null +++ b/src/modules/timesheets/dtos/timesheet.dto.ts @@ -0,0 +1,52 @@ +export class Timesheets { + timesheet_id: number; + days: TimesheetDay[]; + weekly_hours: TotalHours[]; + weekly_expenses: TotalExpenses[]; +} + +export class TimesheetDay { + date: string; + shifts: Shift[]; + expenses: Expense[]; + daily_hours: TotalHours[]; + daily_expenses: TotalExpenses[]; +} + +export class TotalHours { + regular: number; + evening: number; + emergency: number; + overtime: number; + vacation: number; + holiday: number; + sick: number; +} +export class TotalExpenses { + expenses: number; + perd_diem: number; + on_call: number; + mileage: number; +} + +export class Shift { + date: string; + start: string; + end: string; + type: string; + is_remote: boolean; + is_approved: boolean; + shift_id?: number | null; + comment?: string | null; +} + +export class Expense { + date: string; + is_approved: boolean; + comment: string; + amount?: number; + mileage?: number; + attachment?: string; + expense_id?: number | null; + supervisor_comment?: string | null; +} \ No newline at end of file diff --git a/src/modules/timesheets/services/timesheets-command.service.ts b/src/modules/timesheets/services/timesheets-command.service.ts index f8142b0..179a21a 100644 --- a/src/modules/timesheets/services/timesheets-command.service.ts +++ b/src/modules/timesheets/services/timesheets-command.service.ts @@ -10,6 +10,7 @@ import { EmailToIdResolver } from "src/modules/shared/utils/resolve-email-id.uti import { BankCodesResolver } from "src/modules/shared/utils/resolve-bank-type-id.utils"; import { PrismaService } from "src/prisma/prisma.service"; import { TimesheetMap } from "../utils-helpers-others/timesheet.types"; +import { Shift, Expense } from "../dtos/timesheet.dto"; @Injectable() export class TimesheetsCommandService extends BaseApprovalService{ @@ -50,6 +51,44 @@ export class TimesheetsCommandService extends BaseApprovalService{ return timesheet; } +/**_____________________________________________________________________________________________ + create/update/delete shifts and expenses from 1 or many timesheet(s) + + -this function receives an email and an array of timesheets + + -this function will find the timesheets with all shifts and expenses + -this function will calculate total hours, total expenses, filtered by types, + cumulate in daily and weekly. + + -the timesheet_id will be determined using the employee email + -with the timesheet_id, all shifts and expenses will be fetched + + -with shift_id and expense_id, this function will compare both + datas from the DB and from the body of the function and then: + -it will create a shift if no shift is found in the DB + -it will update a shift if a shift is found in the DB + -it will delete a shift if a shift is found and no data is received from the frontend + + This function will be used for the Timesheet Page for an employee to enter, modify or delete and entry + This function will also be used in the modal of the timesheet validation page to + allow a supervisor to enter, modify or delete and entry of a selected employee +_____________________________________________________________________________________________*/ + +async findTimesheetsByEmailAndPayPeriod(email: string, year: number, period_no: number, timesheets: Timesheets[]): Promise { + const employee_id = await this.emailResolver.findIdByEmail(email); + + + return timesheets; +} + +async upsertOrDeleteShiftsByEmailAndDate(email:string, shift_ids: Shift[]) {} + +async upsertOrDeleteExpensesByEmailAndDate(email:string, expenses_id: Expense[]) {} + + + + + //_____________________________________________________________________________________________ // //_____________________________________________________________________________________________ From e1b63bd61a68ea22f99f25623e7f221f5ad3b523 Mon Sep 17 00:00:00 2001 From: Matthieu Haineault Date: Wed, 15 Oct 2025 16:36:05 -0400 Subject: [PATCH 7/7] clean(none): clean slate , removed useless code lines --- prisma/schema.prisma | 5 ----- .../dtos => shared/classes}/timesheet.dto.ts | 12 +++++++++--- src/modules/shared/constants/utils.constant.ts | 1 + 3 files changed, 10 insertions(+), 8 deletions(-) rename src/modules/{timesheets/dtos => shared/classes}/timesheet.dto.ts (89%) create mode 100644 src/modules/shared/constants/utils.constant.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a5e8734..afb3fda 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -184,11 +184,6 @@ model TimesheetsArchive { @@map("timesheets_archive") } - - - - - model SchedulePresets { id Int @id @default(autoincrement()) employee Employees @relation("SchedulePreset", fields: [employee_id], references: [id]) diff --git a/src/modules/timesheets/dtos/timesheet.dto.ts b/src/modules/shared/classes/timesheet.dto.ts similarity index 89% rename from src/modules/timesheets/dtos/timesheet.dto.ts rename to src/modules/shared/classes/timesheet.dto.ts index 22cd1ae..4a3f307 100644 --- a/src/modules/timesheets/dtos/timesheet.dto.ts +++ b/src/modules/shared/classes/timesheet.dto.ts @@ -1,3 +1,9 @@ +export class Session { + user_id: number; + +} + + export class Timesheets { timesheet_id: number; days: TimesheetDay[]; @@ -30,9 +36,9 @@ export class TotalExpenses { } export class Shift { - date: string; - start: string; - end: string; + date: Date; + start_time: Date; + end_time: Date; type: string; is_remote: boolean; is_approved: boolean; diff --git a/src/modules/shared/constants/utils.constant.ts b/src/modules/shared/constants/utils.constant.ts new file mode 100644 index 0000000..271bbbf --- /dev/null +++ b/src/modules/shared/constants/utils.constant.ts @@ -0,0 +1 @@ +export const COMMENT_MAX_LENGTH = 280; \ No newline at end of file