import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); // ====== Config ====== const PREVIOUS_WEEKS = 5; const INCLUDE_CURRENT = true; const INCR = 15; // incrément ferme de 15 minutes (0.25 h) const DAY_MIN = 5 * 60; // 5h const DAY_MAX = 11 * 60; // 11h const HARD_END = 19 * 60 + 30; // 19:30 // ====== Helpers temps ====== function timeAt(hour: number, minute: number) { return new Date(Date.UTC(1970, 0, 1, hour, minute, 0)); } // 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(); // 0 = Dim d.setUTCDate(d.getUTCDate() - day); // recule jusqu'au dimanche d.setUTCHours(0, 0, 0, 0); return d; } 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; } function clamp(n: number, min: number, max: number) { return Math.min(max, Math.max(min, n)); } function addMinutes(h: number, m: number, delta: number) { const total = h * 60 + m + delta; const hh = Math.floor(total / 60); const mm = ((total % 60) + 60) % 60; return { h: hh, m: mm }; } function quantize(mins: number): number { return Math.round(mins / INCR) * INCR; } function rndQuantized(min: number, max: number): number { const qmin = Math.ceil(min / INCR); const qmax = Math.floor(max / INCR); const q = rndInt(qmin, qmax); return q * INCR; } async function main() { // --- Bank codes (pondérés: surtout G1 = régulier) --- 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' ] as const; const bcRows = await prisma.bankCodes.findMany({ where: { bank_code: { in: BANKS as unknown as string[] } }, select: { id: true, bank_code: true }, }); const bcMap = new Map(bcRows.map(b => [b.bank_code, b.id])); 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}`); } // ====== 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; } let created = 0; // 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 // 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; // 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); // 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; } } } for (let di = 0; di < 5; di++) { const date = days[di]; // Lundi..Vendredi const targetWorkMin = plannedDaily[di]; // 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, 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; // 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); // Fin quantisée + borne max 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)!; // 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 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: bcAfternoonId, 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), 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++; } } } 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, Dim ancre + L→V, 2 shifts/jour, **aucun timesheet créé**})`); } main().finally(() => prisma.$disconnect());