Créneaux de nuit rattachés à leur journée de festival

Un créneau saisi « 00:00–01:00 » sur la carte du samedi était enregistré
le samedi à 00:00 (nuit de vendredi à samedi), donc affiché la veille dans
la grille des besoins. Une heure de début avant le début de journée
(premier créneau type) désigne désormais la nuit qui suit : dimanche
00:00, à la suite de la soirée du samedi.

Même « journée de festival » partout (validation/engine.py:day_boundary,
festival_day ; miroir front lib/planning.ts:festivalDay) : écran Postes &
créneaux, feuilles de poste (une seule page pour une soirée qui passe
minuit) et chaînage du solveur (00h–01h enchaîne 23h–00h).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-03 18:47:06 +02:00
co-authored by Claude Opus 5.5
parent c60bc11795
commit a6e7b6e7fb
9 changed files with 153 additions and 18 deletions
+34 -10
View File
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react';
import {
useCreatePosition,
useCreateShift,
useDayParts,
useDeletePosition,
useDeleteShift,
useEdition,
@@ -15,8 +16,13 @@ import {
import type { PositionCategory, PositionRead, ShiftRead } from '@/api/schema';
import { ErrorNote, Loading } from '@/components/state';
import { Btn, HelpTip } from '@/components/ui';
import { categoryLabel } from '@/lib/planning';
import { addDays, hourRangeLabel, hoursBetween, longDayLabel } from '@/lib/time';
import {
categoryLabel,
dayBoundaryFromDayParts,
festivalDay,
shiftDatesForFestivalDay,
} from '@/lib/planning';
import { hourRangeLabel, hoursBetween, longDayLabel } from '@/lib/time';
import { t } from '@/i18n';
import { cx } from '@/lib/cx';
@@ -47,6 +53,8 @@ export function ConfigGrid() {
const editionQuery = useEdition();
const positions = usePositions();
const allShifts = useShifts();
const dayParts = useDayParts();
const dayBoundary = useMemo(() => dayBoundaryFromDayParts(dayParts.data ?? []), [dayParts.data]);
const createPosition = useCreatePosition();
const [selectedId, setSelectedId] = useState<string | null>(null);
@@ -88,10 +96,17 @@ export function ConfigGrid() {
);
}
const shiftsByDay = groupByDay(positionShifts.data ?? []);
const days = editionQuery.data
? enumerateDays(editionQuery.data.start_date, editionQuery.data.end_date)
: [...shiftsByDay.keys()];
const shiftsByDay = groupByDay(positionShifts.data ?? [], dayBoundary);
// Jours de l'édition, plus tout jour de festival qui porterait un créneau
// hors de ces dates (sinon invisible ici, donc impossible à supprimer).
const days = [
...new Set([
...(editionQuery.data
? enumerateDays(editionQuery.data.start_date, editionQuery.data.end_date)
: []),
...shiftsByDay.keys(),
]),
].sort();
return (
<div className="flex min-h-0 flex-1 items-stretch">
@@ -150,6 +165,7 @@ export function ConfigGrid() {
day={day}
positionId={position.id}
shifts={shiftsByDay.get(day) ?? []}
dayBoundary={dayBoundary}
updateShift={updateShift}
deleteShift={deleteShift}
/>
@@ -246,12 +262,14 @@ function DayCard({
day,
positionId,
shifts,
dayBoundary,
updateShift,
deleteShift,
}: {
day: string;
positionId: string;
shifts: ShiftRead[];
dayBoundary: string;
updateShift: ReturnType<typeof useUpdateShift>;
deleteShift: ReturnType<typeof useDeleteShift>;
}) {
@@ -261,9 +279,8 @@ function DayCard({
const addShift = () => {
if (!start || !end) return;
const endDay = end <= start ? addDays(day, 1) : day;
createShift.mutate(
{ position_id: positionId, starts_at: `${day}T${start}:00`, ends_at: `${endDay}T${end}:00` },
{ position_id: positionId, ...shiftDatesForFestivalDay(day, start, end, dayBoundary) },
{ onSuccess: () => { setStart(''); setEnd(''); } },
);
};
@@ -342,15 +359,22 @@ function DayCard({
{t('config.grid.addShift')}
</button>
</div>
{dayBoundary !== '00:00' ? (
<div className="bg-sand px-[9px] pb-[7px] font-mono text-[9.5px] opacity-70">
{t('config.grid.nightHint', { boundary: dayBoundary })}
</div>
) : null}
{createShift.isError ? <ErrorNote error={createShift.error} /> : null}
</div>
);
}
function groupByDay(shifts: ShiftRead[]): Map<string, ShiftRead[]> {
/** Regroupe par journée de festival (pas par date du calendrier) : 00h–01h
* le dimanche reste avec la soirée du samedi, triée après 23h. */
function groupByDay(shifts: ShiftRead[], dayBoundary: string): Map<string, ShiftRead[]> {
const map = new Map<string, ShiftRead[]>();
for (const s of [...shifts].sort((a, b) => a.starts_at.localeCompare(b.starts_at))) {
const day = s.starts_at.slice(0, 10);
const day = festivalDay(s.starts_at, dayBoundary);
map.set(day, [...(map.get(day) ?? []), s]);
}
return map;