Étape 20 : planches de badges bénévoles (PDF)
Recto personnalisable (fond image + champs positionnés : nom, poste(s)
affectés, catégorie, rôle) via un éditeur visuel glisser-déposer ; verso
fixe, la même image pour tous les bénévoles. Nouvel écran « Badges »
(COORDINATION uniquement, /badges).
Backend : modèle BadgeTemplate/BadgeField (migration 0011), génération
PDF isolée (app/badges/pdf.py, ReportLab — planches recto suivies des
planches verso correspondantes pour une impression recto-verso), route
POST /editions/{id}/badges/generate, tests (module pur + API).
Frontend : client HTTP (upload multipart, réponses binaires images/PDF),
client mock (mode démonstration), éditeur de positionnement en
millimètres (coordonnées partagées avec le backend), écran de génération
avec sélection des bénévoles et téléchargement.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
4bcdafca96
commit
b7fc80a7dc
@@ -1,3 +1,5 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
@@ -15,6 +17,8 @@ import type {
|
||||
AssignmentUpdate,
|
||||
AssignmentValidateResult,
|
||||
AvailabilitySet,
|
||||
BadgeFieldsSet,
|
||||
BadgeGenerateRequest,
|
||||
DayPartCreate,
|
||||
DayPartUpdate,
|
||||
EditionUpdate,
|
||||
@@ -131,6 +135,47 @@ export function useMatrix() {
|
||||
return useQuery({ queryKey: queryKeys.matrix, queryFn: () => api.matrix.get(edition) });
|
||||
}
|
||||
|
||||
/** Configuration du recto/verso des badges (images, champs). */
|
||||
export function useBadgeTemplate() {
|
||||
return useQuery({
|
||||
queryKey: ['badgeTemplate', edition],
|
||||
queryFn: () => api.badges.getTemplate(edition),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* URL objet (aperçu) de l'image recto/verso, ou `null` si non définie.
|
||||
* `refreshKey` (ex. `dataUpdatedAt` de `useBadgeTemplate`) force un nouveau
|
||||
* fetch après un remplacement d'image — un simple `hasImage` ne changerait
|
||||
* pas de valeur entre deux images successives.
|
||||
*/
|
||||
export function useBadgeImageUrl(
|
||||
kind: 'front' | 'back',
|
||||
hasImage: boolean,
|
||||
refreshKey: number | string,
|
||||
) {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
let objectUrl: string | null = null;
|
||||
let cancelled = false;
|
||||
if (!hasImage) {
|
||||
setUrl(null);
|
||||
return;
|
||||
}
|
||||
const fetchBlob = kind === 'front' ? api.badges.frontImageBlob : api.badges.backImageBlob;
|
||||
fetchBlob(edition).then((blob) => {
|
||||
if (cancelled || !blob) return;
|
||||
objectUrl = URL.createObjectURL(blob);
|
||||
setUrl(objectUrl);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (objectUrl) URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [kind, hasImage, refreshKey]);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dernières suggestions du solveur (lecture seule du cache posé par
|
||||
* `useRunSolver`). Ne déclenche jamais de calcul elle-même.
|
||||
@@ -459,3 +504,41 @@ export function useSendPlanningNotification() {
|
||||
api.notifications.sendPlanning(edition, body),
|
||||
});
|
||||
}
|
||||
|
||||
function useInvalidateBadgeTemplate() {
|
||||
const client = useQueryClient();
|
||||
return () => client.invalidateQueries({ queryKey: ['badgeTemplate', edition] });
|
||||
}
|
||||
|
||||
export function useUploadBadgeFrontImage() {
|
||||
const invalidate = useInvalidateBadgeTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => api.badges.uploadFrontImage(edition, file),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUploadBadgeBackImage() {
|
||||
const invalidate = useInvalidateBadgeTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (file: File) => api.badges.uploadBackImage(edition, file),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
/** Remplace l'intégralité des champs du recto (positions, activation). */
|
||||
export function useSetBadgeFields() {
|
||||
const invalidate = useInvalidateBadgeTemplate();
|
||||
return useMutation({
|
||||
mutationFn: (body: BadgeFieldsSet) => api.badges.setFields(edition, body),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
/** Génère la planche PDF ; ne fait que retourner le fichier (le composant
|
||||
* appelant déclenche le téléchargement). */
|
||||
export function useGenerateBadges() {
|
||||
return useMutation({
|
||||
mutationFn: (body: BadgeGenerateRequest) => api.badges.generate(edition, body),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user