targo-backend/prisma/mock-seeds-scripts/10-shifts.ts

235 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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));
}
function mondayOfThisWeekUTC(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);
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);
d.setUTCDate(d.getUTCDate() - n * 7);
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 };
}
// Aligne vers le multiple de INCR le plus proche
function quantize(mins: number): number {
const q = Math.round(mins / INCR) * INCR;
return q;
}
// 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);
const q = rndInt(qmin, qmax);
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 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}`);
}
const employees = await prisma.employees.findMany({ select: { id: true } });
if (!employees.length) {
console.log('Aucun employé — rien à insérer.');
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);
for (let ei = 0; ei < employees.length; ei++) {
const e = employees[ei];
// Cible hebdo 3545h, multiple de 15 min
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;
}
}
}
// 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
// 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, 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)); // 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 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)!;
// 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 ${monday.toISOString().slice(0, 10)}) emp ${e.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 — 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})`);
}
main().finally(() => prisma.$disconnect());