Étape 22 : feuilles de poste (PDF), complément des badges

Une page par poste et par jour, listant les créneaux avec les bénévoles
affectés et leur téléphone — à imprimer et afficher sur place. Aucune
nouvelle entité : dérivé de Position/Shift/Assignment/Volunteer
existants (contrairement aux badges, pas de personnalisation à
configurer). Un créneau à besoin 0/0 sans affectation n'apparaît pas.

Backend : app/sheets/pdf.py (ReportLab, Platypus — SimpleDocTemplate/
Table plutôt que le canvas bas niveau des badges, plus adapté à un
rapport texte multi-pages), POST /editions/{id}/position-sheets/generate
(COORDINATION, {position_ids?} — absent = tous les postes).

Frontend : bouton « Feuilles de poste » dans la grille des besoins,
respecte le filtre de catégorie déjà actif à l'écran.

Vérifié : pytest (284 passés), ruff, alembic check, tsc --noEmit,
eslint, vite build, smoke (flux de bout en bout en mode démonstration)
— tous verts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-09-21 21:27:01 +02:00
co-authored by Claude Sonnet 5
parent 111d1e5c4b
commit 40127a2252
17 changed files with 561 additions and 4 deletions
+27
View File
@@ -155,6 +155,32 @@ async function checkPublicForm() {
);
}
/** Feuilles de poste PDF (mode mock) : la sélection filtre bien, l'absence
* de créneau/poste rejette avec le bon code. */
async function checkPositionSheets() {
const positions = await api.positions.list(e);
const first = positions[0];
if (!first) throw new Error('feuilles de poste : aucun poste en démo pour tester');
const blob = await api.positionSheets.generate(e, { position_ids: [first.id] });
if (!(blob instanceof Blob) || blob.size === 0) {
throw new Error('feuilles de poste : PDF vide');
}
let noPositionsRejectedWith = 'non';
try {
await api.positionSheets.generate(e, {
position_ids: ['00000000-0000-0000-0000-000000000000'],
});
} catch (err) {
noPositionsRejectedWith = err instanceof ApiError ? err.code : String(err);
}
console.log(
JSON.stringify({ pdfBytes: blob.size, noPositionsRejectedWith }, null, 2),
);
}
async function prefetchCore(client: QueryClient) {
await Promise.all([
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
@@ -223,6 +249,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));
renderPublic('/connexion');
renderPublic('/connexion/coordination');