Créneaux à la main + grille des besoins en axe des temps (30 min)

Un poste ne génère plus de créneaux à sa création : la coordination crée
elle-même ceux dont elle a besoin, avec ses propres horaires de début et
de fin (ConfigGrid.tsx). La génération depuis les créneaux types reste
disponible en option (bouton « Générer les créneaux »).

La grille des besoins devient un vrai axe des temps : une colonne = 30 min,
bornée par jour à la plage réellement utilisée, avec les cases fusionnées
pour correspondre aux horaires de chaque créneau (buildTimeGrid /
placeInTimeGrid). La « Vue bénévoles » garde son ancien modèle (une colonne
par créneau distinct), plus adapté à son tableau bénévole × créneau.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-22 18:44:20 +02:00
co-authored by Claude Sonnet 5
parent f1d88302a4
commit 15109acfe5
9 changed files with 474 additions and 179 deletions
+53
View File
@@ -18,6 +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 { BadgesPage } from '@/pages/BadgesPage';
import { ConfigPage } from '@/pages/ConfigPage';
import { FormBuilderPage } from '@/pages/FormBuilderPage';
@@ -181,6 +182,57 @@ async function checkPositionSheets() {
);
}
/** Créneaux (mode mock) : un poste créé ne génère plus aucun créneau ; la
* création manuelle à horaires libres fonctionne toujours ; « générer les
* créneaux » (créneaux types) reste disponible en option ; un créneau à
* horaires non alignés (19:15–22:45) se place bien dans la grille demi-heure
* (`buildTimeGrid`/`placeInTimeGrid`), arrondi à la tranche englobante. */
async function checkShiftCreation() {
const position = await api.positions.create(e, { name: 'Smoke poste', category: 'OTHER' });
const shiftsAfterCreate = await api.shifts.list(e, { positionId: position.id });
const edition = await api.editions.get(e);
const shift = await api.shifts.create(e, {
position_id: position.id,
starts_at: `${edition.start_date}T19:15:00`,
ends_at: `${edition.start_date}T22:45:00`,
min_staff: 1,
ideal_staff: 2,
});
const generated = await api.positions.generateShifts(e, position.id);
const grid = buildTimeGrid([shift]);
const day = grid[0];
const placement = day ? placeInTimeGrid(grid, [shift]).get(day.date)?.[0] : undefined;
console.log(
JSON.stringify(
{
shiftsAfterPositionCreate: shiftsAfterCreate.length,
manualShiftTimes: `${shift.starts_at}–${shift.ends_at}`,
generateShiftsStillOptIn: generated.created,
gridSlotCount: day?.slotLabels.length,
gridFirstSlot: day?.slotLabels[0],
placementSpan: placement?.columnSpan,
},
null,
2,
),
);
if (shiftsAfterCreate.length !== 0) {
throw new Error('création de poste : ne devrait générer aucun créneau automatiquement');
}
if (generated.created <= 0) {
throw new Error('generate-shifts : devrait rester disponible en option');
}
// 19:15–22:45 (3 h 30) arrondi aux tranches de 30 min englobantes
// (19:00–23:00) = 8 tranches.
if (day?.slotLabels[0] !== '19:00' || placement?.columnSpan !== 8) {
throw new Error('grille demi-heure : placement inattendu pour un créneau non aligné');
}
}
async function prefetchCore(client: QueryClient) {
await Promise.all([
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
@@ -250,6 +302,7 @@ function renderPublic(path: string) {
await checkAuth().catch((err) => fail('checkAuth', err));
await checkPublicForm().catch((err) => fail('checkPublicForm', err));
await checkPositionSheets().catch((err) => fail('checkPositionSheets', err));
await checkShiftCreation().catch((err) => fail('checkShiftCreation', err));
renderPublic('/connexion');
renderPublic('/connexion/coordination');