// Bellia — Module RH : planning visuel hebdomadaire avec drag & drop const EMPLOYEES = [ { id: 'e1', name: 'Aïcha Benamar', role: 'Chef de cuisine', dept: 'cuisine', initials: 'AB', c: '#C97849', contractH: 39 }, { id: 'e2', name: 'Pierre Dubois', role: 'Second de cuisine', dept: 'cuisine', initials: 'PD', c: '#7FB069', contractH: 39 }, { id: 'e3', name: 'Léa Moreau', role: 'Cheffe de partie', dept: 'cuisine', initials: 'LM', c: '#D9923B', contractH: 35 }, { id: 'e4', name: 'Karim Bensalem', role: 'Cuisinier', dept: 'cuisine', initials: 'KB', c: '#5B8DEF', contractH: 35 }, { id: 'e5', name: 'Sofia Lopez', role: 'Commis', dept: 'cuisine', initials: 'SL', c: '#A87AC9', contractH: 35 }, { id: 'e6', name: 'Marc Chevalier', role: 'Plongeur', dept: 'cuisine', initials: 'MC', c: '#7BA6B5', contractH: 24 }, { id: 'e7', name: 'Clara Bertin', role: 'Cheffe de rang', dept: 'salle', initials: 'CB', c: '#C97849', contractH: 39 }, { id: 'e8', name: 'Tom Lefèvre', role: 'Serveur', dept: 'salle', initials: 'TL', c: '#7FB069', contractH: 35 }, { id: 'e9', name: 'Yasmine Ferrand', role: 'Serveuse', dept: 'salle', initials: 'YF', c: '#D9923B', contractH: 35 }, { id: 'e10', name: 'Hugo Martin', role: 'Runner', dept: 'salle', initials: 'HM', c: '#5B8DEF', contractH: 24 }, ]; const DAYS = ['Lun. 11', 'Mar. 12', 'Mer. 13', 'Jeu. 14', 'Ven. 15', 'Sam. 16', 'Dim. 17']; const SHIFT_TYPES = { midi: { l: 'Midi', start: '11:00', end: '15:00', hours: 4, bg: C.orangeWh, fg: C.orangeDp, bar: C.orange }, soir: { l: 'Soir', start: '18:30', end: '23:00', hours: 4.5, bg: '#F1E7D5', fg: C.ink2, bar: C.ink2 }, continu: { l: 'Continu', start: '11:00', end: '23:00', hours: 9, bg: '#FBE9D5', fg: C.warn, bar: C.warn }, prep: { l: 'Prep.', start: '08:00', end: '12:00', hours: 4, bg: C.okSft, fg: C.ok, bar: C.ok }, off: { l: 'Repos', start: '', end: '', hours: 0, bg: 'transparent', fg: C.mute, bar: C.line }, }; // Génère un planning de base const initShifts = () => { const out = {}; EMPLOYEES.forEach(e => { out[e.id] = DAYS.map((_, di) => { const isWeekend = di >= 5; const isMonday = di === 0; if (isMonday && e.id === 'e6') return 'off'; if (isMonday && (e.id === 'e3' || e.id === 'e8')) return 'off'; if (di === 2 && e.id === 'e5') return 'off'; if (di === 3 && e.id === 'e10') return 'off'; if (di === 4 && e.id === 'e2') return 'off'; if (e.dept === 'cuisine') { if (e.id === 'e1' || e.id === 'e2') return isWeekend ? 'continu' : (di % 2 === 0 ? 'midi' : 'soir'); if (e.id === 'e6') return isWeekend ? 'soir' : 'midi'; return isWeekend ? 'continu' : (di % 2 === 1 ? 'soir' : 'midi'); } else { if (e.id === 'e7') return isWeekend ? 'continu' : (di % 2 === 0 ? 'soir' : 'midi'); return isWeekend ? 'continu' : (di % 2 === 1 ? 'midi' : 'soir'); } }); }); return out; }; const RhScreen = ({ onNav }) => { const [shifts, setShifts] = React.useState(initShifts); const [dept, setDept] = React.useState('tous'); const [dragging, setDragging] = React.useState(null); // { type, fromEmp?, fromDay? } const [hover, setHover] = React.useState(null); // { emp, day } const [selectedShift, setSelectedShift] = React.useState(null); // { emp, day } const employees = EMPLOYEES.filter(e => dept === 'tous' || e.dept === dept); // Compute totals const empHours = React.useMemo(() => { const out = {}; EMPLOYEES.forEach(e => { out[e.id] = (shifts[e.id] || []).reduce((a, s) => a + SHIFT_TYPES[s].hours, 0); }); return out; }, [shifts]); const dayCoverage = React.useMemo(() => DAYS.map((_, di) => { let midi = 0, soir = 0; EMPLOYEES.forEach(e => { const s = shifts[e.id]?.[di]; if (s === 'midi' || s === 'continu' || s === 'prep') midi++; if (s === 'soir' || s === 'continu') soir++; }); return { midi, soir }; }), [shifts]); const totalHours = Object.values(empHours).reduce((a, b) => a + b, 0); const totalMass = totalHours * 16.5; // taux moyen indicatif // DnD handlers const handleDrop = (toEmp, toDay) => { if (!dragging) return; setShifts(prev => { const next = { ...prev, [toEmp]: [...prev[toEmp]] }; if (dragging.fromEmp != null && dragging.fromDay != null) { // swap const fromShift = next[dragging.fromEmp]?.[dragging.fromDay]; const toShift = next[toEmp]?.[toDay]; next[dragging.fromEmp] = [...next[dragging.fromEmp]]; next[dragging.fromEmp][dragging.fromDay] = toShift; next[toEmp][toDay] = fromShift; } else { next[toEmp][toDay] = dragging.type; } return next; }); setDragging(null); setHover(null); }; return (
}>Suggérer (IA) }>Exporter }>Publier } /> {/* ===== Top bar : KPI + toolbar ===== */}
a + e.contractH, 0))} h contrat`} accent="accent" /> d.midi >= 5).length} / 7 j`} count={dayCoverage.some(d => d.midi < 4) ? 'Sous-effectif détecté' : 'Tous les services couverts'} accent={dayCoverage.some(d => d.midi < 4) ? 'err' : 'ok'} />
{/* Week navigator + filters */}
Sem. 20 · 11 au 17 mai
{[ { id: 'tous', l: 'Toute l\'équipe', n: EMPLOYEES.length }, { id: 'cuisine', l: 'Cuisine', n: EMPLOYEES.filter(e => e.dept === 'cuisine').length }, { id: 'salle', l: 'Salle', n: EMPLOYEES.filter(e => e.dept === 'salle').length }, ].map(t => { const active = dept === t.id; return ( ); })}
{/* ===== Planning grid ===== */}
{/* HEADER ROW */}
Équipe
{DAYS.map((d, i) => (
{d}
{dayCoverage[i].midi}M · {dayCoverage[i].soir}S
))}
Heures sem.
{/* ROWS */} {employees.map((e, ri) => { const hours = empHours[e.id]; const over = hours > e.contractH; const under = hours < e.contractH - 4; return ( {/* employee cell */}
{e.name}
{e.role}
{/* shift cells */}
{DAYS.map((_, di) => { const shift = shifts[e.id]?.[di] || 'off'; const meta = SHIFT_TYPES[shift]; const isHover = hover && hover.emp === e.id && hover.day === di; const isSelected = selectedShift && selectedShift.emp === e.id && selectedShift.day === di; return (
{ ev.preventDefault(); setHover({ emp: e.id, day: di }); }} onDragLeave={() => setHover(h => h && h.emp === e.id && h.day === di ? null : h)} onDrop={() => handleDrop(e.id, di)} onClick={() => setSelectedShift({ emp: e.id, day: di })} style={{ padding: 6, borderLeft: `1px solid ${C.lineSoft}`, minHeight: 64, background: isHover ? C.orangeWh : 'transparent', cursor: 'pointer', display: 'flex', }} > {shift === 'off' ? (
Repos
) : (
{ ev.dataTransfer.effectAllowed = 'move'; setDragging({ type: shift, fromEmp: e.id, fromDay: di }); }} onDragEnd={() => setDragging(null)} style={{ width: '100%', borderRadius: 10, padding: '8px 8px', background: meta.bg, borderLeft: `3px solid ${meta.bar}`, display: 'flex', flexDirection: 'column', gap: 2, cursor: 'grab', boxShadow: isSelected ? `0 0 0 2px ${C.orange}` : 'none', transition: 'transform .12s ease', }} onMouseEnter={ev => ev.currentTarget.style.transform = 'translateY(-1px)'} onMouseLeave={ev => ev.currentTarget.style.transform = 'translateY(0)'} >
{meta.l} {meta.hours}h
{meta.start}–{meta.end}
)}
); })}
{/* hours summary */}
{hours} h
contrat {e.contractH} h
); })}
{/* ===== Bottom: palette + alerts ===== */}
{/* Palette - drag from here */}
Palette de créneaux Glissez sur une case pour ajouter
{['midi', 'soir', 'continu', 'prep'].map(k => { const meta = SHIFT_TYPES[k]; return (
{ ev.dataTransfer.effectAllowed = 'copy'; setDragging({ type: k }); }} onDragEnd={() => setDragging(null)} style={{ padding: '10px 12px', background: meta.bg, borderLeft: `3px solid ${meta.bar}`, borderRadius: 10, cursor: 'grab', display: 'flex', flexDirection: 'column', gap: 4, }}>
{meta.l} {meta.hours} h
{meta.start} – {meta.end}
); })}
Astuce : glissez un créneau d'une cellule à une autre pour échanger.
{/* Alerts panel */}
Alertes du planning
); }; const navBtnStyle = { width: 38, height: 38, borderRadius: 999, background: C.card, border: `1px solid ${C.line}`, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', }; const AlertRow = ({ kind, t, v }) => { const map = { err: { c: C.err, bg: C.errSft, icon: 'alertT' }, warn: { c: C.warn, bg: C.warnSft, icon: 'alertC' }, ok: { c: C.ok, bg: C.okSft, icon: 'checkC' } }; const m = map[kind]; return (
{t}
{v}
); }; window.RhScreen = RhScreen;