Grille : colonnes par créneau (pas par heure), largeur adaptative

- `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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-11 20:28:41 +02:00
co-authored by Claude Sonnet 5
parent 55f634e25b
commit 367dfe42eb
5 changed files with 56 additions and 14 deletions
+25 -6
View File
@@ -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. */
+16 -7
View File
@@ -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() {
<LegendItem swatch="bg-paper" glyph="✓">
{t('grille.legend.complete')}
</LegendItem>
<LegendItem swatch="bg-mauve" glyph="▼">
<LegendItem swatch="bg-hatch-mauve" glyph="▼">
{t('grille.legend.overstaffed')}
</LegendItem>
<LegendItem swatch="bg-sand-dark" glyph="·">
@@ -147,9 +153,9 @@ export function GrillePage() {
{shownColumns.map((column) => (
<div
key={column.key}
className="sticky top-[27px] z-[3] border-b-[3px] border-r border-ink bg-sand py-[5px] text-center font-mono text-[9.5px]"
className="sticky top-[27px] z-[3] overflow-hidden text-ellipsis whitespace-nowrap border-b-[3px] border-r border-ink bg-sand px-[2px] py-[5px] text-center font-mono text-[9.5px]"
>
{column.time}
{column.label}
</div>
))}
@@ -232,7 +238,10 @@ function PositionRow({
}) {
const byColumn = new Map<string, MatrixShift>();
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',
)}
+5 -1
View File
@@ -55,7 +55,11 @@ export function VolunteerViewPage() {
const cellByKey = new Map<string, AssignmentRead & { shift: ShiftRead }>();
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,