Import bénévoles : couche API (analyse + exécution) et mock CSV

- `PlanningApi.imports.analyze/execute` : envoi `multipart/form-data`
  (le client HTTP laisse le navigateur poser le Content-Type).
- Types miroir des schémas back-end (`ImportAnalyzeResponse`,
  `ImportMapping`, `ImportReport`, `ImportRowReport`, `ImportIssue`…).
- Hooks `useAnalyzeImport` / `useExecuteImport` ; l'exécution réelle
  invalide les bénévoles et le planning.
- Implémentation mock (mode démo) : lecture CSV, suggestion de mapping,
  simulation/exécution avec rapport ligne par ligne. Excel renvoie
  `import.error.xlsx_demo_only`.

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 21:55:50 +02:00
co-authored by Claude Sonnet 5
parent 06f8736c33
commit df8d6733db
5 changed files with 505 additions and 1 deletions
+18 -1
View File
@@ -5,6 +5,7 @@ import type {
DayPartCreate,
DayPartUpdate,
EditionUpdate,
ImportMapping,
LoginRequest,
MagicLinkRequest,
PlanningNotificationRequest,
@@ -23,10 +24,13 @@ import { ApiError, type PlanningApi } from './types';
export function createHttpApi(baseUrl: string): PlanningApi {
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const token = getAuthToken();
// Pour un envoi `multipart/form-data`, on laisse le navigateur fixer
// lui-même l'en-tête `Content-Type` (avec la frontière).
const isForm = init?.body instanceof FormData;
const res = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
'Content-Type': 'application/json',
...(isForm ? {} : { 'Content-Type': 'application/json' }),
...(token ? { Authorization: `Bearer ${token}` } : {}),
...init?.headers,
},
@@ -93,6 +97,19 @@ export function createHttpApi(baseUrl: string): PlanningApi {
remove: (editionId, id) =>
request(`${base(editionId)}/volunteers/${id}`, { method: 'DELETE' }),
},
imports: {
analyze: (editionId, file: File) => {
const form = new FormData();
form.append('file', file);
return request(`${base(editionId)}/import/analyze`, { method: 'POST', body: form });
},
execute: (editionId, file: File, mapping: ImportMapping) => {
const form = new FormData();
form.append('file', file);
form.append('mapping', JSON.stringify(mapping));
return request(`${base(editionId)}/import/execute`, { method: 'POST', body: form });
},
},
positions: {
list: (editionId) => request(`${base(editionId)}/positions`),
get: (editionId, id) => request(`${base(editionId)}/positions/${id}`),