Début de journée indépendant de l'ordre des créneaux types
La « journée de festival » commence désormais au début du plus long creux laissé par les créneaux types sur 24 h (l'heure où le dernier se termine), et non plus au début de celui de plus petit `order` : un créneau type d'ordre 0 déplacé à 21:00 faisait commencer chaque journée des grilles à 21:00, jusqu'à 03:00 le lendemain. Même règle côté backend (validation.engine.day_boundary, réutilisée par la génération de créneaux) et frontend (dayBoundaryFromDayParts). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ee1f6ed72e
commit
f671bb85bf
@@ -18,7 +18,7 @@ import type { AccountRead } from '@/api/schema';
|
||||
import { queryKeys } from '@/api/hooks';
|
||||
import { AuthContext, AuthProvider, type AuthContextValue } from '@/auth/auth-context';
|
||||
import { routes } from '@/app/router';
|
||||
import { buildTimeGrid, placeInTimeGrid } from '@/lib/planning';
|
||||
import { buildTimeGrid, dayBoundaryFromDayParts, placeInTimeGrid } from '@/lib/planning';
|
||||
import { BadgesPage } from '@/pages/BadgesPage';
|
||||
import { ConfigPage } from '@/pages/ConfigPage';
|
||||
import { FormBuilderPage } from '@/pages/FormBuilderPage';
|
||||
@@ -424,7 +424,49 @@ function renderPublic(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Début de journée indépendant de `order` (régression : un créneau type
|
||||
// d'ordre 0 déplacé à 21:00 faisait commencer chaque journée à 21:00 dans les
|
||||
// grilles, jusqu'à 03:00 le lendemain) — miroir de
|
||||
// backend/tests/test_validation_engine.py (day_boundary).
|
||||
function checkDayBoundary() {
|
||||
const cases: Array<[Parameters<typeof dayBoundaryFromDayParts>[0], string]> = [
|
||||
[[], '00:00'],
|
||||
[
|
||||
[
|
||||
{ start_time: '08:00:00', end_time: '13:00:00', order: 0 },
|
||||
{ start_time: '13:00:00', end_time: '19:00:00', order: 1 },
|
||||
{ start_time: '19:00:00', end_time: '02:00:00', order: 2 },
|
||||
],
|
||||
'02:00',
|
||||
],
|
||||
[
|
||||
[
|
||||
{ start_time: '21:00:00', end_time: '03:00:00', order: 0 },
|
||||
{ start_time: '10:00:00', end_time: '21:00:00', order: 1 },
|
||||
],
|
||||
'03:00',
|
||||
],
|
||||
[
|
||||
[
|
||||
{ start_time: '06:00:00', end_time: '19:00:00', order: 1 },
|
||||
{ start_time: '19:00:00', end_time: '06:00:00', order: 0 },
|
||||
],
|
||||
'19:00',
|
||||
],
|
||||
];
|
||||
for (const [parts, expected] of cases) {
|
||||
const got = dayBoundaryFromDayParts(parts);
|
||||
if (got !== expected) throw new Error(`attendu ${expected}, obtenu ${got}`);
|
||||
}
|
||||
console.log(`ok ${'dayBoundary'.padEnd(26)} ${cases.length} cas`);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------- //
|
||||
try {
|
||||
checkDayBoundary();
|
||||
} catch (err) {
|
||||
fail('checkDayBoundary', err);
|
||||
}
|
||||
await checkAuth().catch((err) => fail('checkAuth', err));
|
||||
await checkPublicForm().catch((err) => fail('checkPublicForm', err));
|
||||
await checkPositionSheets().catch((err) => fail('checkPositionSheets', err));
|
||||
|
||||
@@ -189,15 +189,49 @@ export interface LatticeDay {
|
||||
columns: LatticeColumn[];
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
/** Frontière du jour (« journée » au sens § 3 du CLAUDE.md) : début du plus
|
||||
* long creux laissé par les créneaux types sur le cadran de 24 h (l'heure où
|
||||
* le dernier de la journée se termine) — même règle que
|
||||
* `validation.engine.day_boundary` côté backend. Indépendant de `order`, qui
|
||||
* ne suit pas les heures quand on les modifie. Sans creux, début du premier
|
||||
* par `order` ; sans créneau type, minuit. */
|
||||
export function dayBoundaryFromDayParts(
|
||||
dayParts: Array<Pick<DayPartRead, 'start_time' | 'order'>>,
|
||||
dayParts: Array<Pick<DayPartRead, 'start_time' | 'end_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);
|
||||
const DAY = 24 * 60;
|
||||
const covered = new Array<boolean>(DAY).fill(false);
|
||||
for (const part of dayParts) {
|
||||
const start = minutesOfDay(part.start_time.slice(0, 5));
|
||||
const end = minutesOfDay(part.end_time.slice(0, 5));
|
||||
const length = (end - start + DAY) % DAY || DAY;
|
||||
for (let offset = 0; offset < length; offset++) covered[(start + offset) % DAY] = true;
|
||||
}
|
||||
const origin = covered.indexOf(true);
|
||||
if (covered.every(Boolean)) {
|
||||
const first = dayParts.reduce((min, part) => (part.order < min.order ? part : min));
|
||||
return first.start_time.slice(0, 5);
|
||||
}
|
||||
// Parcours circulaire depuis une minute couverte : un creux qui enjambe
|
||||
// minuit est vu d'un seul tenant.
|
||||
let bestStart = 0;
|
||||
let bestLength = 0;
|
||||
let runStart = 0;
|
||||
let runLength = 0;
|
||||
for (let offset = 1; offset <= DAY; offset++) {
|
||||
const minute = (origin + offset) % DAY;
|
||||
if (covered[minute]) {
|
||||
runLength = 0;
|
||||
continue;
|
||||
}
|
||||
if (runLength === 0) runStart = minute;
|
||||
runLength += 1;
|
||||
if (runLength > bestLength) {
|
||||
bestStart = runStart;
|
||||
bestLength = runLength;
|
||||
}
|
||||
}
|
||||
return clockLabel(bestStart);
|
||||
}
|
||||
|
||||
/** Construit le treillis de colonnes à partir de créneaux (grille alignée).
|
||||
|
||||
Reference in New Issue
Block a user