Édition éditable (dates) + robustesse de l'écran Postes & créneaux

Back-end :
- GET /editions/{id} (tout compte de l'édition) et PATCH /editions/{id}
  (COORDINATION) : name, year, start_date, end_date, retention_days.
  Refus si end_date < start_date (edition.dates_inconsistent). Tests.

Front-end :
- api : EditionRead/Update, editions.get/update (http + mock), hooks
  useEdition / useUpdateEdition.
- ConfigGrid : le bouton « ajouter un poste » sélectionne le poste créé et
  affiche l'état (pending) + les erreurs (ErrorNote) — avant, un échec ou
  même un succès passaient inaperçus. Renommage du poste + choix de la
  catégorie + suppression dans l'en-tête. Une colonne par jour de l'édition
  (même vide) → on peut ajouter un créneau n'importe quel jour.
- ConfigRules : formulaire d'édition (nom + dates) → c'est là qu'on ajoute
  ou retire des jours ; compteur de jours ; retour d'enregistrement.
- i18n mis à jour.

144 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:02:50 +02:00
co-authored by Claude Sonnet 5
parent 9521034207
commit e0a0c56307
14 changed files with 517 additions and 115 deletions
+14
View File
@@ -10,6 +10,7 @@ import type {
AssignmentCreate,
AssignmentUpdate,
AssignmentValidateResult,
EditionUpdate,
PlanningNotificationRequest,
PositionCreate,
PositionUpdate,
@@ -22,6 +23,7 @@ import type {
const edition = EDITION_ID;
export const queryKeys = {
edition: ['edition', edition] as const,
volunteers: ['volunteers', edition] as const,
positions: ['positions', edition] as const,
shifts: (positionId?: string) => ['shifts', edition, positionId ?? null] as const,
@@ -35,6 +37,10 @@ export const queryKeys = {
// --------------------------------------------------------------------------- //
// Lectures //
// --------------------------------------------------------------------------- //
export function useEdition() {
return useQuery({ queryKey: queryKeys.edition, queryFn: () => api.editions.get(edition) });
}
export function useVolunteers(options?: { enabled?: boolean }) {
return useQuery({
queryKey: queryKeys.volunteers,
@@ -108,6 +114,14 @@ function useInvalidate() {
};
}
export function useUpdateEdition() {
const client = useQueryClient();
return useMutation({
mutationFn: (body: EditionUpdate) => api.editions.update(edition, body),
onSuccess: () => client.invalidateQueries({ queryKey: ['edition', edition] }),
});
}
export function useCreateVolunteer() {
const invalidate = useInvalidate();
return useMutation({
+6
View File
@@ -2,6 +2,7 @@ import type {
AssignmentCreate,
AssignmentUpdate,
AssignmentValidateRequest,
EditionUpdate,
LoginRequest,
MagicLinkRequest,
PlanningNotificationRequest,
@@ -66,6 +67,11 @@ export function createHttpApi(baseUrl: string): PlanningApi {
const base = (editionId: string) => `/editions/${editionId}`;
return {
editions: {
get: (editionId) => request(base(editionId)),
update: (editionId, body: EditionUpdate) =>
request(base(editionId), { method: 'PATCH', body: JSON.stringify(body) }),
},
volunteers: {
list: (editionId) => request(`${base(editionId)}/volunteers`),
get: (editionId, id) => request(`${base(editionId)}/volunteers/${id}`),
+24 -1
View File
@@ -3,6 +3,7 @@ import { hoursBetween } from '@/lib/time';
import type {
AccountRead,
AssignmentRead,
EditionRead,
PositionCategory,
PositionRead,
ShiftRead,
@@ -19,6 +20,7 @@ export const DEMO_COORDINATOR_EMAIL = 'coordination@lessonsdautomne.fr';
export const DEMO_COORDINATOR_PASSWORD = 'demo';
export interface MockState {
edition: EditionRead;
volunteers: VolunteerRead[];
positions: PositionRead[];
shifts: ShiftRead[];
@@ -325,7 +327,28 @@ export function buildMockState(): MockState {
});
});
return { volunteers, positions, shifts, assignments, accounts, passwords, availability, seq };
const edition: EditionRead = {
id: edition_id,
year: 2026,
name: "Les Sons d'Automne",
start_date: DAY_DATES[0],
end_date: DAY_DATES[DAY_DATES.length - 1],
retention_days: 365,
created_at: NOW,
updated_at: NOW,
};
return {
edition,
volunteers,
positions,
shifts,
assignments,
accounts,
passwords,
availability,
seq,
};
}
function shiftsOverlap(a: ShiftRead | undefined, b: ShiftRead): boolean {
+18
View File
@@ -4,6 +4,7 @@ import type {
AccountRead,
AssignmentRead,
AssignmentValidateResult,
EditionUpdate,
MatrixResponse,
PlanningNotificationRequest,
ShiftRead,
@@ -210,6 +211,23 @@ function buildMatrix(): MatrixResponse {
export function createMockApi(): PlanningApi {
return {
editions: {
get: () => delay(state.edition),
update: (_edition, body: EditionUpdate) => {
const next = { ...state.edition };
if ('name' in body) next.name = body.name ?? null;
if (body.year != null) next.year = body.year;
if (body.start_date) next.start_date = body.start_date;
if (body.end_date) next.end_date = body.end_date;
if ('retention_days' in body) next.retention_days = body.retention_days ?? null;
if (next.end_date < next.start_date) {
throw new ApiError(422, 'edition.dates_inconsistent');
}
next.updated_at = new Date().toISOString();
state.edition = next;
return delay(state.edition);
},
},
volunteers: {
list: () =>
delay(
+23
View File
@@ -35,6 +35,29 @@ export interface Violation {
params: Record<string, string | number>;
}
// --------------------------------------------------------------------------- //
// Edition //
// --------------------------------------------------------------------------- //
export interface EditionRead {
id: string;
year: number;
name: string | null;
/** Dates ISO (YYYY-MM-DD). */
start_date: string;
end_date: string;
retention_days: number | null;
created_at: string;
updated_at: string;
}
export interface EditionUpdate {
name?: string | null;
year?: number;
start_date?: string;
end_date?: string;
retention_days?: number | null;
}
// --------------------------------------------------------------------------- //
// Volunteer //
// --------------------------------------------------------------------------- //
+6
View File
@@ -6,6 +6,8 @@ import type {
AssignmentValidateRequest,
AssignmentValidateResult,
AssignmentWriteResult,
EditionRead,
EditionUpdate,
LoginRequest,
MagicLinkRequest,
MatrixResponse,
@@ -30,6 +32,10 @@ import type {
* réel) et mémoire (jeu de démonstration). Les écrans ne dépendent que d'ici.
*/
export interface PlanningApi {
editions: {
get(editionId: string): Promise<EditionRead>;
update(editionId: string, body: EditionUpdate): Promise<EditionRead>;
};
volunteers: {
list(editionId: string): Promise<VolunteerRead[]>;
get(editionId: string, id: string): Promise<VolunteerRead>;