235 lines
9.1 KiB
TypeScript
235 lines
9.1 KiB
TypeScript
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 35–45h, 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());
|