Écrans bénévole et référent redessinés, messages aux bénévoles
ci-cd / build-backend (push) Successful in 29s
ci-cd / build-frontend (push) Successful in 21s
ci-cd / deploy (push) Successful in 12s

Étape 55 : écrans /mobile-benevole et /mobile-referent d'après la maquette
(design/Planning bénévole mobile) — créneau en cours ou prochain mis en
avant, planning par journée de festival avec frise horaire, compteurs et
pointage du référent par créneau.

Étape 56 : message de l'organisation (Edition.volunteer_message, onglet
« Message aux bénévoles ») et message de chaque poste (Position.message,
saisi par le référent), migration 0029, affichés sur l'écran bénévole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 23:17:14 +01:00
co-authored by Claude Opus 5.5
parent 4a3f93e137
commit 16d8fb44a7
21 changed files with 1305 additions and 274 deletions
+11
View File
@@ -195,6 +195,11 @@ function demoEmail(firstName: string, lastName: string): string {
return `${n(firstName)}.${n(lastName)}@mail.fr`;
}
const DEMO_POSITION_MESSAGES: Record<string, string> = {
'Parking — Nord': 'Gilet jaune fourni à la base bénévoles. Lampe frontale conseillée le soir.',
'Bar — Grande scène': 'Caisse à récupérer auprès du référent en début de créneau. Pas de vente aux mineurs.',
};
export function buildMockState(): MockState {
const rnd = mulberry32(20260715);
const edition_id = MOCK_EDITION_ID;
@@ -206,6 +211,9 @@ export function buildMockState(): MockState {
location: p.location,
category: p.category,
lead_account_id: null,
// Quelques consignes de démo (voir `PositionRead.message`).
message: DEMO_POSITION_MESSAGES[p.name] ?? null,
message_updated_at: DEMO_POSITION_MESSAGES[p.name] ? NOW : null,
created_at: NOW,
updated_at: NOW,
}));
@@ -466,6 +474,9 @@ export function buildMockState(): MockState {
start_date: DAY_DATES[0],
end_date: DAY_DATES[DAY_DATES.length - 1],
retention_days: 365,
volunteer_message:
"Badges et t-shirts à récupérer à l'accueil bénévoles dès 17:00. Repas chaud servi au catering de 17:30 à 18:30.",
volunteer_message_updated_at: NOW,
created_at: NOW,
updated_at: NOW,
};
+27
View File
@@ -44,6 +44,7 @@ import type {
VolunteerRead,
EditionStats,
} from '../schema';
import { MESSAGE_MAX_LENGTH } from '../schema';
import { getAuthToken } from '../session';
import { ApiError, type PlanningApi } from '../types';
import { buildMockState, type MockPendingSubmission, type MockState } from './dataset';
@@ -836,6 +837,14 @@ function runSolver(): SolverRunResult {
};
}
/** Message (organisation, poste) comme l'API : bords retirés, vide = plus
* de message, 2000 caractères au plus. */
function checkMessage(value: string | null | undefined): string | null {
const text = (value ?? '').trim() || null;
if (text && text.length > MESSAGE_MAX_LENGTH) throw new ApiError(422, 'error.generic');
return text;
}
export function createMockApi(): PlanningApi {
return {
editions: {
@@ -847,6 +856,13 @@ export function createMockApi(): PlanningApi {
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 ('volunteer_message' in body) {
const text = checkMessage(body.volunteer_message);
if (text !== next.volunteer_message) {
next.volunteer_message = text;
next.volunteer_message_updated_at = text ? touch() : null;
}
}
if (next.end_date < next.start_date) {
throw new ApiError(422, 'edition.dates_inconsistent');
}
@@ -1124,6 +1140,8 @@ export function createMockApi(): PlanningApi {
position_location: position?.location ?? null,
referent_name: referent ? `${referent.first_name} ${referent.last_name}` : null,
referent_phone: referent?.phone ?? null,
position_message: position?.message ?? null,
position_message_updated_at: position?.message_updated_at ?? null,
};
})
.sort((a, b) => a.starts_at.localeCompare(b.starts_at));
@@ -1384,6 +1402,8 @@ export function createMockApi(): PlanningApi {
location: body.location ?? null,
category: body.category,
lead_account_id: body.lead_account_id ?? null,
message: null,
message_updated_at: null,
created_at: now,
updated_at: now,
};
@@ -1395,6 +1415,13 @@ export function createMockApi(): PlanningApi {
update: (_edition, id, body) => {
const p = state.positions.find((x) => x.id === id) ?? notFound();
if ('lead_account_id' in body) checkLeadAccount(body.lead_account_id);
if ('message' in body) {
const text = checkMessage(body.message);
if (text !== p.message) {
p.message = text;
p.message_updated_at = text ? touch() : null;
}
}
Object.assign(p, {
...('name' in body ? { name: body.name } : {}),
...('location' in body ? { location: body.location ?? null } : {}),
+18
View File
@@ -39,16 +39,25 @@ export interface EditionRead {
start_date: string;
end_date: string;
retention_days: number | null;
/** Message de l'organisation, le même pour tous les bénévoles (écran
* bénévole), et la date de sa dernière modification. */
volunteer_message: string | null;
volunteer_message_updated_at: string | null;
created_at: string;
updated_at: string;
}
/** Longueur maximale d'un message (organisation, poste) — comme l'API. */
export const MESSAGE_MAX_LENGTH = 2000;
export interface EditionUpdate {
name?: string | null;
year?: number;
start_date?: string;
end_date?: string;
retention_days?: number | null;
/** Vide = plus de message. */
volunteer_message?: string | null;
}
// --------------------------------------------------------------------------- //
@@ -185,6 +194,9 @@ export interface VolunteerPlanningEntry {
* rattaché à une fiche bénévole (pas de téléphone à donner alors). */
referent_name: string | null;
referent_phone: string | null;
/** Message du poste (voir `PositionRead.message`). */
position_message: string | null;
position_message_updated_at: string | null;
}
// --------------------------------------------------------------------------- //
@@ -290,6 +302,10 @@ export interface PositionRead {
location: string | null;
category: PositionCategory;
lead_account_id: string | null;
/** Message du poste (consignes…), saisi par son référent, montré aux
* bénévoles affectés au poste. */
message: string | null;
message_updated_at: string | null;
created_at: string;
updated_at: string;
}
@@ -306,6 +322,8 @@ export interface PositionUpdate {
location?: string | null;
category?: PositionCategory;
lead_account_id?: string | null;
/** Vide = plus de message. */
message?: string | null;
}
// --------------------------------------------------------------------------- //