Grille : ordonner les créneaux de nuit après minuit selon la vraie journée

Un créneau type commençant avant la frontière du jour (le plus petit
`order`, ex. « Nuit » 00 h–03 h après « Soirée » 19 h–00 h) était généré
sur la même date calendaire que la soirée elle-même au lieu du lendemain,
et s'affichait donc avant 19 h/22 h/23 h dans la grille des besoins au
lieu d'après 23 h. Corrigé dans la génération des créneaux
(_shiftgen.py, même frontière que validation.engine._day_boundary) et
dans le regroupement/tri de la grille (planning.ts : `dayBoundary`
calculé depuis les créneaux types).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-17 22:59:17 +02:00
co-authored by Claude Sonnet 5
parent f565feb3bc
commit 4bcdafca96
6 changed files with 128 additions and 24 deletions
+53 -11
View File
@@ -2,6 +2,7 @@ import type {
ArtistRead,
AssignmentRead,
AvailabilityRead,
DayPartRead,
MatrixResponse,
PositionCategory,
ShiftRead,
@@ -10,7 +11,7 @@ import type {
} from '@/api/schema';
import { t } from '@/i18n';
import { datePart, hoursBetween, naiveMinutes, timePart } from './time';
import { addDays, datePart, hoursBetween, minutesOfDay, naiveMinutes, timePart } from './time';
export function categoryLabel(category: PositionCategory): string {
return t(`position.category.${category.toLowerCase()}`);
@@ -188,35 +189,76 @@ export interface LatticeDay {
columns: LatticeColumn[];
}
/** Construit le treillis de colonnes à partir de créneaux (grille alignée). */
export function buildLattice(starts: Array<{ starts_at: string; ends_at: string }>): {
/** Frontière du jour (« journée » au sens § 3 du CLAUDE.md) : début du
* créneau type de plus petit `order` — même règle que
* `validation.engine._day_boundary` côté backend. Sans créneau type, minuit. */
export function dayBoundaryFromDayParts(
dayParts: Array<Pick<DayPartRead, 'start_time' | 'order'>>,
): string {
if (dayParts.length === 0) return '00:00';
const first = dayParts.reduce((min, part) => (part.order < min.order ? part : min));
return first.start_time.slice(0, 5);
}
/** Construit le treillis de colonnes à partir de créneaux (grille alignée).
* `dayBoundary` (`HH:MM`, défaut minuit) regroupe et trie les colonnes par
* « journée de festival » plutôt que par date calendaire brute : un créneau
* commençant avant cette heure (ex. 02:00, suite d'une soirée qui a passé
* minuit) est rattaché au jour précédent et trié après 23:00, pas avant
* 19:00 — voir CLAUDE.md § 3 (« journée » pour le cumul des 8 h). */
export function buildLattice(
starts: Array<{ starts_at: string; ends_at: string }>,
dayBoundary = '00:00',
): {
days: LatticeDay[];
columns: LatticeColumn[];
} {
const seen = new Map<string, LatticeColumn>();
const boundaryMinutes = minutesOfDay(dayBoundary);
const seen = new Map<string, LatticeColumn & { festivalDay: string; sortMinutes: number }>();
for (const s of starts) {
const date = datePart(s.starts_at);
const time = timePart(s.starts_at);
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}` });
if (!seen.has(key)) {
const minutes = minutesOfDay(time);
const festivalDay = minutes < boundaryMinutes ? addDays(date, -1) : date;
const sortMinutes = (minutes - boundaryMinutes + 24 * 60) % (24 * 60);
seen.set(key, { key, date, time, endTime, label: `${time}–${endTime}`, festivalDay, sortMinutes });
}
}
const columns = [...seen.values()].sort((a, b) => a.key.localeCompare(b.key));
const ordered = [...seen.values()].sort(
(a, b) => a.festivalDay.localeCompare(b.festivalDay) || a.sortMinutes - b.sortMinutes,
);
const columns: LatticeColumn[] = ordered.map(({ key, date, time, endTime, label }) => ({
key,
date,
time,
endTime,
label,
}));
const days: LatticeDay[] = [];
for (const column of columns) {
let day = days.find((d) => d.date === column.date);
for (const column of ordered) {
let day = days.find((d) => d.date === column.festivalDay);
if (!day) {
day = { date: column.date, columns: [] };
day = { date: column.festivalDay, columns: [] };
days.push(day);
}
day.columns.push(column);
day.columns.push({
key: column.key,
date: column.date,
time: column.time,
endTime: column.endTime,
label: column.label,
});
}
return { days, columns };
}
export function latticeFromMatrix(matrix: MatrixResponse) {
export function latticeFromMatrix(matrix: MatrixResponse, dayBoundary?: string) {
return buildLattice(
matrix.positions.flatMap((p) => p.shifts.map((s) => ({ starts_at: s.starts_at, ends_at: s.ends_at }))),
dayBoundary,
);
}