Frontend : consommation de l'API CRUD (matrice, affectations, validation)
Remplace le magasin d'état fabriqué par une vraie couche API typée, alignée sur le back-end (Étape 5) : - src/api/ : schema.ts (miroir de backend/app/api/schemas.py), client HTTP, implémentation mock en mémoire (même surface + portage du moteur de validation), sélection mock/http via VITE_API_MODE, hooks React Query (lectures + mutations avec invalidation). - Grille des besoins ← GET /matrix ; treillis de colonnes dérivé des créneaux réels (plus de plages figées). - Panneau d'affectation ← GET /assignments?shift_id, POST /assignments/validate par candidat (alertes bloquantes / avertissements du moteur), POST / PATCH / DELETE /assignments. - Vue par bénévole ← volunteers + shifts + assignments (surcharge / oubliés calculés côté client). - Confirmations (ex-« Publication ») ← affectations PROPOSED ; publication versionnée et emails signalés hors périmètre. - Mobiles ← assignments filtrées ; confirmation = PATCH ; pointage référent local (hors périmètre). - Configuration ← positions & shifts CRUD (min/idéal, ajout/retrait), volunteers CRUD dont suppression RGPD ; messages = maquette locale. - i18n mis à jour, aucun texte d'interface en dur. - Vérifs : build tsc + vite, eslint (0 problème), npm run smoke (API mock + rendu headless des 6 écrans avec données préchargées). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwswqSChc3Rwtu61pRXiY2
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
ac469f0cd1
commit
c9dda65361
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
AssignmentCreate,
|
||||
AssignmentUpdate,
|
||||
AssignmentValidateRequest,
|
||||
PositionCreate,
|
||||
PositionUpdate,
|
||||
ShiftCreate,
|
||||
ShiftUpdate,
|
||||
VolunteerCreate,
|
||||
VolunteerUpdate,
|
||||
} from './schema';
|
||||
import { ApiError, type PlanningApi } from './types';
|
||||
|
||||
// Implémentation HTTP réelle. En dev, Vite proxifie `/api` vers le back-end.
|
||||
export function createHttpApi(baseUrl: string): PlanningApi {
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
...init,
|
||||
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
|
||||
const isJson = res.headers.get('content-type')?.includes('application/json');
|
||||
const body: unknown = isJson ? await res.json() : await res.text();
|
||||
|
||||
if (!res.ok) {
|
||||
const detail =
|
||||
body && typeof body === 'object' && 'detail' in body
|
||||
? (body as { detail: unknown }).detail
|
||||
: body;
|
||||
const code =
|
||||
detail && typeof detail === 'object' && 'code' in detail
|
||||
? String((detail as { code: unknown }).code)
|
||||
: 'error.generic';
|
||||
const violations =
|
||||
detail && typeof detail === 'object' && 'violations' in detail
|
||||
? ((detail as { violations: [] }).violations ?? [])
|
||||
: [];
|
||||
throw new ApiError(res.status, code, violations);
|
||||
}
|
||||
return body as T;
|
||||
}
|
||||
|
||||
const query = (params: Record<string, string | undefined>) => {
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value != null) search.set(key, value);
|
||||
}
|
||||
const str = search.toString();
|
||||
return str ? `?${str}` : '';
|
||||
};
|
||||
|
||||
const base = (editionId: string) => `/editions/${editionId}`;
|
||||
|
||||
return {
|
||||
volunteers: {
|
||||
list: (editionId) => request(`${base(editionId)}/volunteers`),
|
||||
get: (editionId, id) => request(`${base(editionId)}/volunteers/${id}`),
|
||||
create: (editionId, body: VolunteerCreate) =>
|
||||
request(`${base(editionId)}/volunteers`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
update: (editionId, id, body: VolunteerUpdate) =>
|
||||
request(`${base(editionId)}/volunteers/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
remove: (editionId, id) =>
|
||||
request(`${base(editionId)}/volunteers/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
positions: {
|
||||
list: (editionId) => request(`${base(editionId)}/positions`),
|
||||
get: (editionId, id) => request(`${base(editionId)}/positions/${id}`),
|
||||
create: (editionId, body: PositionCreate) =>
|
||||
request(`${base(editionId)}/positions`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
update: (editionId, id, body: PositionUpdate) =>
|
||||
request(`${base(editionId)}/positions/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
remove: (editionId, id) =>
|
||||
request(`${base(editionId)}/positions/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
shifts: {
|
||||
list: (editionId, params) =>
|
||||
request(`${base(editionId)}/shifts${query({ position_id: params?.positionId })}`),
|
||||
get: (editionId, id) => request(`${base(editionId)}/shifts/${id}`),
|
||||
create: (editionId, body: ShiftCreate) =>
|
||||
request(`${base(editionId)}/shifts`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
update: (editionId, id, body: ShiftUpdate) =>
|
||||
request(`${base(editionId)}/shifts/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
remove: (editionId, id) => request(`${base(editionId)}/shifts/${id}`, { method: 'DELETE' }),
|
||||
},
|
||||
assignments: {
|
||||
list: (editionId, params) =>
|
||||
request(
|
||||
`${base(editionId)}/assignments${query({
|
||||
volunteer_id: params?.volunteerId,
|
||||
shift_id: params?.shiftId,
|
||||
})}`,
|
||||
),
|
||||
get: (editionId, id) => request(`${base(editionId)}/assignments/${id}`),
|
||||
create: (editionId, body: AssignmentCreate) =>
|
||||
request(`${base(editionId)}/assignments`, { method: 'POST', body: JSON.stringify(body) }),
|
||||
update: (editionId, id, body: AssignmentUpdate) =>
|
||||
request(`${base(editionId)}/assignments/${id}`, { method: 'PATCH', body: JSON.stringify(body) }),
|
||||
remove: (editionId, id) =>
|
||||
request(`${base(editionId)}/assignments/${id}`, { method: 'DELETE' }),
|
||||
validate: (editionId, body: AssignmentValidateRequest) =>
|
||||
request(`${base(editionId)}/assignments/validate`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
},
|
||||
matrix: {
|
||||
get: (editionId) => request(`${base(editionId)}/matrix`),
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user