import { DAY_PARTS, DEMO_CATEGORIES, FESTIVAL_DAYS, availabilityKey, dayPartHours, } from '@/domain/festival'; import type { Assignment, AssignmentStatus, Position, Volunteer } from '@/domain/types'; import { buildPositions } from './positions'; export interface DemoDataset { positions: Position[]; volunteers: Volunteer[]; assignments: Assignment[]; removed: { volunteerName: string; positionName: string; zone: string; dayShortLabel: string; dayPartLabel: string }[]; nextAssignmentSeq: number; } // Générateur pseudo-aléatoire déterministe (mulberry32), repris du design pour // obtenir exactement le même jeu de démonstration à chaque chargement. function mulberry32(seed: number): () => number { let a = seed; return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const FIRST_NAMES = [ 'Camille', 'Yanis', 'Léa', 'Mathis', 'Soraya', 'Thomas', 'Awa', 'Julien', 'Chloé', 'Nadia', 'Hugo', 'Inès', 'Baptiste', 'Manon', 'Karim', 'Élodie', 'Rémi', 'Sarah', 'Antoine', 'Fanny', 'Malik', 'Clara', 'Victor', 'Jeanne', ]; const LAST_NAMES = [ 'Fournier', 'Berger', 'Moreau', 'Lambert', 'Benali', 'Rey', 'Diallo', 'Chevalier', 'Marchand', 'Perrin', 'Lefèvre', 'Nguyen', 'Rossi', 'Garnier', 'Barbier', 'Vasseur', 'Colin', 'Prévost', 'Renard', 'Dupuy', ]; const VOLUNTEER_COUNT = 64; const NEVER_ASSIGNED_INDEXES = [7, 21, 34, 48, 55]; const OVERLOADED_INDEXES = [3, 9]; function statusFromRoll(roll: number): AssignmentStatus { if (roll < 0.72) return 'CONFIRMED'; if (roll < 0.95) return 'PROPOSED'; return 'DECLINED'; } export function buildDemoDataset(): DemoDataset { const positions = buildPositions(); const rnd = mulberry32(20260715); const volunteers: Volunteer[] = []; for (let i = 0; i < VOLUNTEER_COUNT; i += 1) { const firstName = FIRST_NAMES[i % FIRST_NAMES.length]; const lastName = LAST_NAMES[(i * 7 + 3) % LAST_NAMES.length]; const availability = new Set(); FESTIVAL_DAYS.forEach((day) => { DAY_PARTS.forEach((part) => { const isEdgeDay = day.index === 0 || day.index === 3; if (rnd() < (isEdgeDay ? 0.4 : 0.6)) { availability.add(availabilityKey(day.id, part.code)); } }); }); const preferredCategories = [DEMO_CATEGORIES[Math.floor(rnd() * DEMO_CATEGORIES.length)]]; if (rnd() < 0.45) { preferredCategories.push(DEMO_CATEGORIES[Math.floor(rnd() * DEMO_CATEGORIES.length)]); } const pair = () => `0${Math.floor(rnd() * 90 + 10)}`.slice(-2); const phone = `06 ${pair()} ${pair()} ${pair()} ${pair()}`; volunteers.push({ id: `v${i}`, firstName, lastName, fullName: `${firstName} ${lastName}`, initials: firstName[0] + lastName[0], phone, availability, preferredCategories, }); } // Bénévole de démonstration (« moi ») : disponible partout, demande Bar + Accueil. const me = volunteers[0]; me.preferredCategories = ['BAR', 'WELCOME']; FESTIVAL_DAYS.forEach((day) => DAY_PARTS.forEach((part) => me.availability.add(availabilityKey(day.id, part.code))), ); // Référent de démonstration. volunteers[1].preferredCategories = ['BAR']; // Bénévoles inscrits mais jamais affectés (« oubliés »). NEVER_ASSIGNED_INDEXES.forEach((i) => { volunteers[i].neverAssigned = true; }); const assignments: Assignment[] = []; const busy: Record = {}; const dailyHours: Record = {}; const totalHours: Record = {}; let seq = 1; let slotIndex = 0; positions.forEach((position) => { FESTIVAL_DAYS.forEach((day) => { position.needs .filter((need) => need.dayId === day.id) .forEach((need) => { slotIndex += 1; let target = need.idealStaff; if (slotIndex % 7 === 0) target = Math.max(1, need.minStaff - 1); else if (slotIndex % 5 === 0) target = need.minStaff; else if (slotIndex % 3 === 0) target = Math.max(need.minStaff, need.idealStaff - 1); const hours = dayPartHours(need.dayPartCode); const pool = volunteers .filter( (v) => !v.neverAssigned && !busy[`${v.id}|${day.id}|${need.dayPartCode}`] && (dailyHours[`${v.id}|${day.id}`] ?? 0) + hours <= 8, ) .sort((a, b) => score(b) - score(a)); function score(v: Volunteer): number { const available = v.availability.has(availabilityKey(day.id, need.dayPartCode)) ? 100 : 0; const wanted = v.preferredCategories.includes(position.category) ? 40 : 0; return available + wanted - (totalHours[v.id] ?? 0) * 3; } pool.slice(0, target).forEach((v) => { const status = statusFromRoll(rnd()); assignments.push({ id: `a${seq}`, volunteerId: v.id, positionId: position.id, dayId: day.id, dayPartCode: need.dayPartCode, status, source: 'SOLVER', locked: false, pending: null, }); seq += 1; if (status !== 'DECLINED') { busy[`${v.id}|${day.id}|${need.dayPartCode}`] = true; dailyHours[`${v.id}|${day.id}`] = (dailyHours[`${v.id}|${day.id}`] ?? 0) + hours; totalHours[v.id] = (totalHours[v.id] ?? 0) + hours; } }); }); }); }); // « Moi » : 1 créneau confirmé puis des propositions à confirmer. assignments .filter((a) => a.volunteerId === me.id) .forEach((a, i) => { a.status = i === 0 ? 'CONFIRMED' : 'PROPOSED'; }); // Deux surcharges volontaires : plus de 8 h sur le vendredi. OVERLOADED_INDEXES.forEach((index) => { const volunteer = volunteers[index]; const dayId = 'ven'; const taken = assignments .filter((a) => a.volunteerId === volunteer.id && a.dayId === dayId) .map((a) => a.dayPartCode); const freeCode = ['A', 'S', 'N', 'M'].find((code) => !taken.includes(code)); if (!freeCode) return; const position = positions.find((p) => p.needs.some((n) => n.dayId === dayId && n.dayPartCode === freeCode), ); if (!position) return; assignments.push({ id: `a${seq}`, volunteerId: volunteer.id, positionId: position.id, dayId, dayPartCode: freeCode, status: 'CONFIRMED', source: 'MANUAL', locked: false, pending: null, }); seq += 1; }); // Modifications non encore publiées. assignments.forEach((a, i) => { if (i % 29 === 7) a.pending = 'ADD'; }); const removed = [ { volunteerName: 'Sarah Prévost', positionName: 'Parking', zone: 'Nord', dayShortLabel: 'VEN 17', dayPartLabel: '17–22' }, { volunteerName: 'Karim Benali', positionName: 'Propreté', zone: 'Site', dayShortLabel: 'SAM 18', dayPartLabel: '12–17' }, ]; return { positions, volunteers, assignments, removed, nextAssignmentSeq: seq }; }