From 367dfe42eb0c247aa8b5f9ca9bf949c6c20fab29 Mon Sep 17 00:00:00 2001 From: AdamThomas Date: Fri, 11 Sep 2026 20:28:41 +0200 Subject: [PATCH] =?UTF-8?q?Grille=20:=20colonnes=20par=20cr=C3=A9neau=20(p?= =?UTF-8?q?as=20par=20heure),=20largeur=20adaptative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `buildLattice` (frontend/src/lib/planning.ts) identifie une colonne par (date, heure de début, heure de fin) et non plus par la seule heure de début : deux créneaux qui démarrent en même temps mais durent différemment ont désormais chacun leur colonne, avec un en-tête « début → fin » au lieu du seul début. - Largeur adaptative par jour (`columnWidthPx`) : peu de créneaux dans une journée ⇒ colonnes larges, beaucoup de créneaux ⇒ largeur compacte (défilement horizontal) — fini les colonnes étroites par défaut. - `VolunteerViewPage` mis à jour pour rester synchronisé avec la nouvelle clé de colonne (fonction `buildLattice` partagée). - CLAUDE.md à jour. Corrige au passage `bg-hatch-mauve` (case en sureffectif) : la classe était utilisée depuis le commit précédent mais l'utilitaire Tailwind correspondant n'avait jamais été ajouté à `backgroundImage` — la case retombait silencieusement sur un fond uni. typecheck + lint + build + smoke OK. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM --- CLAUDE.md | 8 ++++++ frontend/src/lib/planning.ts | 31 +++++++++++++++++++----- frontend/src/pages/GrillePage.tsx | 23 ++++++++++++------ frontend/src/pages/VolunteerViewPage.tsx | 6 ++++- frontend/tailwind.config.js | 2 ++ 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4bf1007..851aa6c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -289,6 +289,14 @@ effectivement tourné (`POST .../solver/suggestions`), ses bénévoles retenus passent malgré tout en tête (ce sont eux le résultat d'une vraie optimisation jointe, pas juste un score local). +**Colonnes de la grille des besoins** — une colonne représente un **créneau** +distinct (`starts_at` + `ends_at`), pas seulement une heure de début : deux +créneaux qui démarrent en même temps mais durent différemment ont chacun leur +colonne (`buildLattice` / `LatticeColumn` dans `frontend/src/lib/planning.ts`). +Largeur adaptative par jour (`columnWidthPx`) : peu de créneaux dans une +journée ⇒ colonnes larges, beaucoup de créneaux ⇒ largeur compacte avec +défilement horizontal — jamais de colonnes étroites par défaut. + --- ## 5. Périmètre diff --git a/frontend/src/lib/planning.ts b/frontend/src/lib/planning.ts index 3772567..0b7058f 100644 --- a/frontend/src/lib/planning.ts +++ b/frontend/src/lib/planning.ts @@ -137,11 +137,15 @@ export function candidateChanceScore(params: { return score; } -/** Colonne de la grille : une (date, heure de début) distincte. */ +/** Colonne de la grille : un créneau distinct (date, heure de début, heure de + * fin) — pas seulement une heure de début : deux créneaux qui démarrent à la + * même heure mais durent différemment ont chacun leur colonne. */ export interface LatticeColumn { - key: string; // `YYYY-MM-DD|HH:MM` + key: string; // `YYYY-MM-DD|HH:MM|HH:MM` date: string; time: string; + endTime: string; + label: string; // « 17:00 → 22:00 » } export interface LatticeDay { @@ -150,7 +154,7 @@ export interface LatticeDay { } /** Construit le treillis de colonnes à partir de créneaux (grille alignée). */ -export function buildLattice(starts: Array<{ starts_at: string }>): { +export function buildLattice(starts: Array<{ starts_at: string; ends_at: string }>): { days: LatticeDay[]; columns: LatticeColumn[]; } { @@ -158,8 +162,9 @@ export function buildLattice(starts: Array<{ starts_at: string }>): { for (const s of starts) { const date = datePart(s.starts_at); const time = timePart(s.starts_at); - const key = `${date}|${time}`; - if (!seen.has(key)) seen.set(key, { key, date, time }); + const endTime = timePart(s.ends_at); + const key = `${date}|${time}|${endTime}`; + if (!seen.has(key)) seen.set(key, { key, date, time, endTime, label: `${time}–${endTime}` }); } const columns = [...seen.values()].sort((a, b) => a.key.localeCompare(b.key)); const days: LatticeDay[] = []; @@ -175,7 +180,21 @@ export function buildLattice(starts: Array<{ starts_at: string }>): { } export function latticeFromMatrix(matrix: MatrixResponse) { - return buildLattice(matrix.positions.flatMap((p) => p.shifts.map((s) => ({ starts_at: s.starts_at })))); + return buildLattice( + matrix.positions.flatMap((p) => p.shifts.map((s) => ({ starts_at: s.starts_at, ends_at: s.ends_at }))), + ); +} + +/** + * Largeur (px) des colonnes d'une journée, en fonction du nombre de créneaux + * distincts qu'elle compte : peu de créneaux ⇒ colonnes larges plutôt que + * resserrées ; beaucoup de créneaux ⇒ largeur compacte (défilement horizontal). + */ +export function columnWidthPx(columnsInDay: number): number { + if (columnsInDay <= 2) return 160; + if (columnsInDay <= 4) return 120; + if (columnsInDay <= 6) return 92; + return 66; } /** Heures cumulées par bénévole. */ diff --git a/frontend/src/pages/GrillePage.tsx b/frontend/src/pages/GrillePage.tsx index 8f6627a..b8afdf2 100644 --- a/frontend/src/pages/GrillePage.tsx +++ b/frontend/src/pages/GrillePage.tsx @@ -5,7 +5,7 @@ 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 { categoryLabel, columnWidthPx, latticeFromMatrix, type LatticeColumn } from '@/lib/planning'; import { datePart, shortDayLabel, timePart } from '@/lib/time'; import { t } from '@/i18n'; import { cx } from '@/lib/cx'; @@ -43,7 +43,13 @@ export function GrillePage() { ? matrix.data.positions : matrix.data.positions.filter((p) => p.category === categoryFilter); - const gridTemplateColumns = `230px repeat(${shownColumns.length}, 66px)`; + const gridTemplateColumns = [ + '230px', + ...shownDays.flatMap((day) => { + const width = columnWidthPx(day.columns.length); + return day.columns.map(() => `${width}px`); + }), + ].join(' '); const selected = selectedShiftId ? matrix.data.positions @@ -114,7 +120,7 @@ export function GrillePage() { {t('grille.legend.complete')} - + {t('grille.legend.overstaffed')} @@ -147,9 +153,9 @@ export function GrillePage() { {shownColumns.map((column) => (
- {column.time} + {column.label}
))} @@ -232,7 +238,10 @@ function PositionRow({ }) { const byColumn = new Map(); for (const shift of position.shifts) { - byColumn.set(`${datePart(shift.starts_at)}|${timePart(shift.starts_at)}`, shift); + byColumn.set( + `${datePart(shift.starts_at)}|${timePart(shift.starts_at)}|${timePart(shift.ends_at)}`, + shift, + ); } return ( @@ -272,7 +281,7 @@ function PositionRow({ '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', + isOver && 'bg-hatch-mauve hover:bg-mauve-hover', isInc && 'bg-yellow-soft hover:bg-yellow', isOk && 'bg-paper hover:bg-green/10', )} diff --git a/frontend/src/pages/VolunteerViewPage.tsx b/frontend/src/pages/VolunteerViewPage.tsx index 3c1ad45..1579f7c 100644 --- a/frontend/src/pages/VolunteerViewPage.tsx +++ b/frontend/src/pages/VolunteerViewPage.tsx @@ -55,7 +55,11 @@ export function VolunteerViewPage() { const cellByKey = new Map(); for (const a of own) { const shift = shiftsById.get(a.shift_id); - if (shift) cellByKey.set(`${datePart(shift.starts_at)}|${timePart(shift.starts_at)}`, { ...a, shift }); + if (shift) + cellByKey.set( + `${datePart(shift.starts_at)}|${timePart(shift.starts_at)}|${timePart(shift.ends_at)}`, + { ...a, shift }, + ); } return { volunteer: v, diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ce6863a..6e9eacc 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -37,6 +37,8 @@ export default { // Hachures « sous-effectif critique » 'hatch-orange': 'repeating-linear-gradient(45deg,#FF5C39 0 6px,#E0421F 6px 12px)', + 'hatch-mauve': + 'repeating-linear-gradient(45deg,#CBA6E3 0 6px,#BE93D9 6px 12px)', // Hachures « brouillon » 'hatch-ink': 'repeating-linear-gradient(45deg,#14110E 0 8px,#2A2520 8px 16px)',