Créneaux types : paramétrage + génération auto des créneaux

Le paramétrage des créneaux se fait dans « Règles & édition » : la coordination
définit des créneaux types (DayPart). À la création d'un poste, un créneau à
besoin 0/0 est généré pour chaque jour de l'édition et chaque créneau type ;
les coordinateurs ajustent ensuite min/idéal.

Back-end :
- DayPart CRUD (GET/POST/PATCH/DELETE /editions/{id}/day-parts). Code figé
  après création (slugifié depuis le libellé si absent), unique par édition.
- Shift.day_part_id (nullable, SET NULL) + migration 0004.
- create_position génère les créneaux ; POST .../positions/{pid}/generate-shifts
  recrée les manquants (idempotent, ne touche pas aux besoins déjà réglés).
- 13 tests (day-parts, génération, extension d'édition, ajout de type).

Front-end :
- api/hooks/mock : dayParts CRUD + positions.generateShifts.
- ConfigRules : panneau « Créneaux types » (libellé + horaires éditables,
  ajout, suppression).
- ConfigGrid : bouton « Générer les créneaux » sur l'en-tête du poste.
- GrillePage : case « à définir » (gris, glyphe ·) quand min=idéal=0, avec
  entrée de légende.

155 tests back OK ; typecheck + lint + build + smoke front 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-10 20:57:51 +02:00
co-authored by Claude Sonnet 5
parent cee581b832
commit f58ec00e30
21 changed files with 834 additions and 16 deletions
+20
View File
@@ -3,6 +3,7 @@ import { hoursBetween } from '@/lib/time';
import type {
AccountRead,
AssignmentRead,
DayPartRead,
EditionRead,
PositionCategory,
PositionRead,
@@ -10,6 +11,10 @@ import type {
VolunteerRead,
} from '../schema';
const PART_LABELS: Record<string, string> = { M: 'Matin', A: 'Après-midi', S: 'Soirée', N: 'Nuit' };
const PART_CODES: Record<string, string> = { M: 'MATIN', A: 'APRES_MIDI', S: 'SOIREE', N: 'NUIT' };
const hhmm = (h: number) => `${String(h % 24).padStart(2, '0')}:00:00`;
// Jeu de démonstration déterministe, mis en forme comme les réponses de l'API.
// Sert quand `VITE_API_MODE=mock` (défaut tant que le back-end n'a pas de seed).
@@ -21,6 +26,7 @@ export const DEMO_COORDINATOR_PASSWORD = 'demo';
export interface MockState {
edition: EditionRead;
dayParts: DayPartRead[];
volunteers: VolunteerRead[];
positions: PositionRead[];
shifts: ShiftRead[];
@@ -126,6 +132,18 @@ export function buildMockState(): MockState {
updated_at: NOW,
}));
const dayParts: DayPartRead[] = PART_BOUNDS.map(([code, start, end], i) => ({
id: `dp_${code}`,
edition_id,
code: PART_CODES[code] ?? code,
label: PART_LABELS[code] ?? code,
start_time: hhmm(start),
end_time: hhmm(end),
order: i,
created_at: NOW,
updated_at: NOW,
}));
const shifts: ShiftRead[] = [];
let shiftSeq = 1;
for (const raw of RAW_POSITIONS) {
@@ -136,6 +154,7 @@ export function buildMockState(): MockState {
id: `s${shiftSeq}`,
edition_id,
position_id: raw.id,
day_part_id: `dp_${code}`,
starts_at: iso(dateStr, bounds[1]),
ends_at: iso(dateStr, bounds[2]),
min_staff,
@@ -341,6 +360,7 @@ export function buildMockState(): MockState {
return {
edition,
dayParts,
volunteers,
positions,
shifts,