fix(seeders): ajusted shifts and expenses seeders

This commit is contained in:
Matthieu Haineault 2025-10-14 13:28:42 -04:00
parent d1c41ea1bd
commit 77e10c67c7
3 changed files with 170 additions and 178 deletions

View File

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

View File

@ -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,16 +59,6 @@ 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;
@ -85,50 +75,62 @@ 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 3545h, multiple de 15 min
// 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
// 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 par pas de 15
const jitter = rndInt(-3, 3) * INCR; // -45..+45
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)
// 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));
// Corriger le petit écart restant (devrait être multiple de 15) en redistribuant ±15
// Fine tuning ±15
let diff = weeklyTargetMin - plannedDaily.reduce((a, b) => a + b, 0);
const step = diff > 0 ? INCR : -INCR;
let guard = 100; // anti-boucle
let guard = 100;
while (diff !== 0 && guard-- > 0) {
for (let d = 0; d < 5 && diff !== 0; d++) {
const next = plannedDaily[d] + step;
@ -139,33 +141,27 @@ async function main() {
}
}
// Upsert du timesheet (semaine)
const ts = await getOrCreateTimesheet(e.id, mondayOfThisWeekUTC(days[0]));
for (let di = 0; di < 5; di++) {
const date = days[di];
const targetWorkMin = plannedDaily[di]; // multiple de 15
const date = days[di]; // Lundi..Vendredi
const targetWorkMin = plannedDaily[di];
// Départ ~ base + jitter (par pas de 15 min aussi)
// Départ ~ base + jitter
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, mais pas avant start+3h ni après start+6h (le tout quantisé 15)
// 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); // 30..120 min en pas de 15
const lunchDur = rndQuantized(30, 120);
const lunchEndMin = lunchStartMin + lunchDur;
// 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);
}
const morningWork = Math.max(0, lunchStartMin - (startH * 60 + startM));
let afternoonWork = Math.max(60, targetWorkMin - morningWork);
if (afternoonWork % INCR !== 0) afternoonWork = quantize(afternoonWork);
// Fin de journée (quantisée par construction)
// Fin quantisée + borne max
const endMinRaw = lunchEndMin + afternoonWork;
const endMin = Math.min(endMinRaw, HARD_END);
@ -181,7 +177,7 @@ async function main() {
data: {
timesheet_id: ts.id,
bank_code_id: bcMorningId,
comment: `Matin J${di + 1} (sem ${monday.toISOString().slice(0, 10)}) emp ${e.id} - ${bcMorningCode}`,
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),
@ -190,7 +186,7 @@ async function main() {
});
created++;
// Shift après-midi (si >= 30 min — sera de toute façon multiple de 15)
// Shift après-midi (si >= 30 min)
const pmDuration = endMin - lunchEndMin;
if (pmDuration >= 30) {
const lunchEndHM = { h: Math.floor(lunchEndMin / 60), m: lunchEndMin % 60 };
@ -199,7 +195,7 @@ async function main() {
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}`,
comment: `Après-midi J${di + 1} (sem ${sunday.toISOString().slice(0, 10)}) emp ${ts.employee_id}${bcAfternoonCode}`,
date,
start_time: timeAt(lunchEndHM.h, lunchEndHM.m),
end_time: timeAt(finalEndHM.h, finalEndHM.m),
@ -208,13 +204,13 @@ async function main() {
});
created++;
} else {
// Fallback très rare : un seul shift couvrant la journée (tout en multiples de 15)
// 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 ${monday.toISOString().slice(0, 10)}) emp ${e.id} — 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),
@ -225,10 +221,9 @@ async function main() {
}
}
}
}
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());

View File

@ -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,7 +119,7 @@ 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) {
@ -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());