diff --git a/frontend/src/i18n/locales/fr.json b/frontend/src/i18n/locales/fr.json index 98827bb..ae295e6 100644 --- a/frontend/src/i18n/locales/fr.json +++ b/frontend/src/i18n/locales/fr.json @@ -81,6 +81,7 @@ "grille.positionsCount": "{count} POSTES", "grille.assignedRequired": "AFFECTÉS / IDÉAL", "grille.footnote": "Chaque cellule est un créneau. Nombre = bénévoles affectés, / = effectif idéal. Cliquer ouvre le panneau d'affectation à droite.", + "grille.cellPreview.staffing": "{assigned}/{ideal} affecté(s) · min {min}", "grille.empty": "Aucun créneau pour le moment. Créez des postes dans Configuration → « Postes & créneaux » — un créneau est généré par jour et par créneau type.", "grille.panel.empty.title": "Cliquez une cellule de la grille pour affecter des bénévoles.", "grille.panel.empty.body": "Le panneau s'ouvre ici, sans quitter la grille : candidats triés par charge, alertes bloquantes et avertissements du moteur de validation.", diff --git a/frontend/src/pages/GrillePage.tsx b/frontend/src/pages/GrillePage.tsx index 1ccd63b..57a23ff 100644 --- a/frontend/src/pages/GrillePage.tsx +++ b/frontend/src/pages/GrillePage.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react'; -import { useMatrix, useRunSolver } from '@/api/hooks'; +import { useAssignments, useMatrix, useRunSolver, useVolunteers } from '@/api/hooks'; import type { MatrixPosition, MatrixShift, PositionCategory } from '@/api/schema'; import { Chip, HelpTip } from '@/components/ui'; import { EmptyNote, ErrorNote, Loading } from '@/components/state'; @@ -13,6 +13,8 @@ import { cx } from '@/lib/cx'; export function GrillePage() { const matrix = useMatrix(); const runSolver = useRunSolver(); + const assignments = useAssignments(); + const volunteers = useVolunteers(); const [dayFilter, setDayFilter] = useState<'all' | string>('all'); const [categoryFilter, setCategoryFilter] = useState<'all' | PositionCategory>('all'); const [selectedShiftId, setSelectedShiftId] = useState(null); @@ -22,6 +24,24 @@ export function GrillePage() { [matrix.data], ); + // Noms des bénévoles affectés, par créneau — pour l'aperçu au survol d'une + // case (voir `PositionRow`). Pas bloquant : peut rester vide un instant le + // temps que ces requêtes (déjà utilisées par le panneau d'affectation) + // reviennent du cache. + const volunteerNamesByShift = useMemo(() => { + const volunteersById = new Map((volunteers.data ?? []).map((v) => [v.id, v])); + const map = new Map(); + for (const a of assignments.data ?? []) { + const v = volunteersById.get(a.volunteer_id); + if (!v) continue; + const list = map.get(a.shift_id) ?? []; + list.push(`${v.first_name} ${v.last_name}`); + map.set(a.shift_id, list); + } + for (const list of map.values()) list.sort((a, b) => a.localeCompare(b, 'fr')); + return map; + }, [assignments.data, volunteers.data]); + if (matrix.isLoading) return ; if (matrix.error) return ; if (!matrix.data) return null; @@ -167,6 +187,7 @@ export function GrillePage() { columns={shownColumns} selectedShiftId={selectedShiftId} onSelect={setSelectedShiftId} + volunteerNamesByShift={volunteerNamesByShift} /> ))} @@ -229,11 +250,13 @@ function PositionRow({ columns, selectedShiftId, onSelect, + volunteerNamesByShift, }: { position: MatrixPosition; columns: LatticeColumn[]; selectedShiftId: string | null; onSelect: (id: string) => void; + volunteerNamesByShift: Map; }) { const byColumn = new Map(); for (const shift of position.shifts) { @@ -272,25 +295,39 @@ function PositionRow({ ? '◧' : '✓'; const selected = selectedShiftId === shift.shift_id; + const names = volunteerNamesByShift.get(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', + 'group 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-hatch-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} +
+
+ {t('grille.cellPreview.staffing', { + assigned, + ideal: shift.ideal_staff, + min: shift.min_staff, + })} +
+ {names.length > 0 ? ( + names.map((name, i) =>
{name}
) + ) : ( +
{t('grille.panel.noneAssigned')}
+ )} +
{selected ? ( ) : null}