Photo du bénévole ajoutée par la coordination
ci-cd / build-backend (push) Successful in 33s
ci-cd / build-frontend (push) Successful in 20s
ci-cd / deploy (push) Successful in 14s

Section « Photo » sur la fiche bénévole (ajouter / remplacer / retirer),
PUT/DELETE /editions/{id}/volunteers/{vid}/photo (COORDINATION),
validation de l'image partagée avec le formulaire public (read_photo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
AdamThomas
2026-10-27 18:17:25 +01:00
co-authored by Claude Opus 5.5
parent 54cba467fc
commit 6892e0307d
12 changed files with 288 additions and 64 deletions
+27 -4
View File
@@ -103,9 +103,15 @@ export function useVolunteerPlanning(volunteerId: string | null | undefined) {
});
}
/** URL objet (aperçu) de la photo d'un bénévole (formulaire public), ou
* `null` si absente. */
export function useVolunteerPhotoUrl(volunteerId: string | null | undefined, hasPhoto: boolean) {
/** URL objet (aperçu) de la photo d'un bénévole (formulaire public ou
* ajoutée par la coordination), ou `null` si absente. `version` (ex.
* `updated_at`) force le rechargement quand la photo est remplacée —
* `hasPhoto` resterait `true` et rien ne changerait sinon. */
export function useVolunteerPhotoUrl(
volunteerId: string | null | undefined,
hasPhoto: boolean,
version?: string,
) {
const [url, setUrl] = useState<string | null>(null);
useEffect(() => {
let objectUrl: string | null = null;
@@ -123,7 +129,7 @@ export function useVolunteerPhotoUrl(volunteerId: string | null | undefined, has
cancelled = true;
if (objectUrl) URL.revokeObjectURL(objectUrl);
};
}, [volunteerId, hasPhoto]);
}, [volunteerId, hasPhoto, version]);
return url;
}
@@ -361,6 +367,23 @@ export function useUpdateVolunteer() {
});
}
export function useSetVolunteerPhoto() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: ({ id, file }: { id: string; file: File }) =>
api.volunteers.setPhoto(edition, id, file),
onSuccess: invalidate.volunteers,
});
}
export function useRemoveVolunteerPhoto() {
const invalidate = useInvalidate();
return useMutation({
mutationFn: (id: string) => api.volunteers.removePhoto(edition, id),
onSuccess: invalidate.volunteers,
});
}
export function useDeleteVolunteer() {
const invalidate = useInvalidate();
return useMutation({
+7
View File
@@ -155,6 +155,13 @@ export function createHttpApi(baseUrl: string): PlanningApi {
planning: (editionId, id) =>
request(`${base(editionId)}/volunteers/${id}/planning`),
photoBlob: (editionId, id) => requestBlob(`${base(editionId)}/volunteers/${id}/photo`),
setPhoto: (editionId, id, file: File) => {
const form = new FormData();
form.append('file', file);
return request(`${base(editionId)}/volunteers/${id}/photo`, { method: 'PUT', body: form });
},
removePhoto: (editionId, id) =>
request(`${base(editionId)}/volunteers/${id}/photo`, { method: 'DELETE' }),
exportXlsx: async (editionId) => {
const blob = await requestBlob(`${base(editionId)}/volunteers/export`);
if (!blob) throw new ApiError(500, 'error.generic', []);
+15
View File
@@ -1069,6 +1069,21 @@ export function createMockApi(): PlanningApi {
if (!dataUrl) return null;
return (await fetch(dataUrl)).blob();
},
setPhoto: async (_edition, id, file) => {
const v = state.volunteers.find((x) => x.id === id) ?? notFound();
if (!['image/png', 'image/jpeg'].includes(file.type)) {
throw new ApiError(422, 'form.photo_type_not_supported');
}
state.photos.set(id, await fileToDataUrl(file));
Object.assign(v, { has_photo: true, updated_at: touch() });
return delay(v);
},
removePhoto: (_edition, id) => {
const v = state.volunteers.find((x) => x.id === id) ?? notFound();
state.photos.delete(id);
Object.assign(v, { has_photo: false, updated_at: touch() });
return delay(v);
},
exportXlsx: () => delay(MOCK_XLSX_BLOB()),
exportMecenatReceipts: () => {
const paid = state.volunteers.some(
+6 -1
View File
@@ -88,8 +88,13 @@ export interface PlanningApi {
remove(editionId: string, id: string): Promise<void>;
/** Planning enrichi (poste, lieu, référent) — écran bénévole. */
planning(editionId: string, id: string): Promise<VolunteerPlanningEntry[]>;
/** Photo transmise via le formulaire public, `null` si absente. */
/** Photo transmise via le formulaire public (ou ajoutée par la
* coordination), `null` si absente. */
photoBlob(editionId: string, id: string): Promise<Blob | null>;
/** Ajoute ou remplace la photo (COORDINATION) — PNG/JPEG. */
setPhoto(editionId: string, id: string, file: File): Promise<VolunteerRead>;
/** Retire la photo (COORDINATION). */
removePhoto(editionId: string, id: string): Promise<VolunteerRead>;
/** Export Excel de la liste des bénévoles (COORDINATION). */
exportXlsx(editionId: string): Promise<Blob>;
/** Reçus fiscaux (cerfa) des bénévoles ayant payé en mécénat, un PDF