import { useMemo, useState } from 'react'; import { useMatrix } from '@/api/hooks'; import type { MatrixPosition, MatrixShift, PositionCategory } from '@/api/schema'; import { Chip } from '@/components/ui'; import { EmptyNote, ErrorNote, Loading } from '@/components/state'; import { AssignmentPanel } from '@/pages/grille/AssignmentPanel'; import { categoryLabel, latticeFromMatrix, type LatticeColumn } from '@/lib/planning'; import { datePart, shortDayLabel, timePart } from '@/lib/time'; import { t } from '@/i18n'; import { cx } from '@/lib/cx'; export function GrillePage() { const matrix = useMatrix(); const [dayFilter, setDayFilter] = useState<'all' | string>('all'); const [categoryFilter, setCategoryFilter] = useState<'all' | PositionCategory>('all'); const [selectedShiftId, setSelectedShiftId] = useState(null); const lattice = useMemo( () => (matrix.data ? latticeFromMatrix(matrix.data) : { days: [], columns: [] }), [matrix.data], ); if (matrix.isLoading) return ; if (matrix.error) return ; if (!matrix.data) return null; const hasShifts = matrix.data.positions.some((p) => p.shifts.length > 0); if (!hasShifts) { return (
{t('grille.empty')}
); } const categories = [...new Set(matrix.data.positions.map((p) => p.category))].sort(); const shownDays = dayFilter === 'all' ? lattice.days : lattice.days.filter((d) => d.date === dayFilter); const shownColumns = shownDays.flatMap((d) => d.columns); const shownPositions = categoryFilter === 'all' ? matrix.data.positions : matrix.data.positions.filter((p) => p.category === categoryFilter); const gridTemplateColumns = `230px repeat(${shownColumns.length}, 66px)`; const selected = selectedShiftId ? matrix.data.positions .flatMap((p) => p.shifts.map((s) => ({ position: p, shift: s }))) .find((entry) => entry.shift.shift_id === selectedShiftId) ?? null : null; return (
{t('grille.filter.day')} setDayFilter('all')}> {t('filter.all')} {lattice.days.map((day) => ( setDayFilter(day.date)}> {shortDayLabel(`${day.date}T12:00`)} ))}
{t('grille.filter.position')} setCategoryFilter('all')}> {t('filter.all')} {categories.map((category) => ( setCategoryFilter(category)} > {categoryLabel(category).toUpperCase()} ))}
{t('legend.title')} {t('grille.legend.critical')} {t('grille.legend.incomplete')} {t('grille.legend.complete')} {t('grille.legend.overstaffed')} {t('grille.legend.undefined')}
{t('grille.positionsCount', { count: matrix.data.positions.length })}
{shownDays.map((day) => (
{shortDayLabel(`${day.date}T12:00`)}
))}
{t('grille.assignedRequired')}
{shownColumns.map((column) => (
{column.time}
))} {shownPositions.map((position) => ( ))}
{t('grille.footnote')}
); } function LegendItem({ swatch, glyph, children, }: { swatch: string; glyph: string; children: React.ReactNode; }) { return ( {glyph} {children} ); } function PositionRow({ position, columns, selectedShiftId, onSelect, }: { position: MatrixPosition; columns: LatticeColumn[]; selectedShiftId: string | null; onSelect: (id: string) => void; }) { const byColumn = new Map(); for (const shift of position.shifts) { byColumn.set(`${datePart(shift.starts_at)}|${timePart(shift.starts_at)}`, shift); } return ( <>
{position.name} {position.location}
{columns.map((column) => { const shift = byColumn.get(column.key); if (!shift) { return
; } const assigned = shift.assigned; const undefinedNeed = shift.min_staff === 0 && shift.ideal_staff === 0; const isCrit = !undefinedNeed && assigned < shift.min_staff; const isOver = !undefinedNeed && assigned > shift.ideal_staff; const isInc = !undefinedNeed && !isCrit && !isOver && assigned < shift.ideal_staff; const isOk = !undefinedNeed && !isCrit && !isOver && !isInc; const glyph = undefinedNeed ? '·' : isCrit ? '▲' : isOver ? '▼' : isInc ? '◧' : '✓'; const selected = selectedShiftId === shift.shift_id; return (
onSelect(shift.shift_id)} className={cx( 'relative flex h-9 cursor-pointer items-center justify-center gap-[3px] border-b border-r border-ink', undefinedNeed && 'bg-sand-dark hover:bg-sand', isCrit && 'bg-hatch-orange', isOver && 'bg-mauve hover:bg-mauve-hover', isInc && 'bg-yellow-soft hover:bg-yellow', isOk && 'bg-paper hover:bg-green/10', )} title={`${assigned} affecté(s) · min ${shift.min_staff} · idéal ${shift.ideal_staff}`} > {assigned} /{shift.ideal_staff} {glyph} {selected ? ( ) : null}
); })} ); }