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
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
// Rendu headless de chaque écran, avec les données de démonstration
|
|
// préchargées, pour vérifier que la chaîne API → sélecteurs → rendu ne lève pas.
|
|
// Lancement : npx tsx --tsconfig tsconfig.app.json scripts/smoke.tsx
|
|
import { createElement } from 'react';
|
|
import { renderToString } from 'react-dom/server';
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { RouterProvider, createMemoryRouter } from 'react-router-dom';
|
|
|
|
import { EDITION_ID, api } from '@/api';
|
|
import { queryKeys } from '@/api/hooks';
|
|
import { routes } from '@/app/router';
|
|
|
|
const paths = [
|
|
'/grille',
|
|
'/benevoles',
|
|
'/publication',
|
|
'/mobile-benevole',
|
|
'/mobile-referent',
|
|
'/configuration',
|
|
];
|
|
|
|
async function prefetch(client: QueryClient) {
|
|
const e = EDITION_ID;
|
|
await Promise.all([
|
|
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
|
|
client.prefetchQuery({ queryKey: queryKeys.positions, queryFn: () => api.positions.list(e) }),
|
|
client.prefetchQuery({ queryKey: queryKeys.shifts(), queryFn: () => api.shifts.list(e) }),
|
|
client.prefetchQuery({ queryKey: queryKeys.assignments(), queryFn: () => api.assignments.list(e) }),
|
|
client.prefetchQuery({ queryKey: queryKeys.matrix, queryFn: () => api.matrix.get(e) }),
|
|
]);
|
|
}
|
|
|
|
async function checkMockApi() {
|
|
const e = EDITION_ID;
|
|
const [volunteers, positions, shifts, assignments, matrix] = await Promise.all([
|
|
api.volunteers.list(e),
|
|
api.positions.list(e),
|
|
api.shifts.list(e),
|
|
api.assignments.list(e),
|
|
api.matrix.get(e),
|
|
]);
|
|
|
|
const openShift = matrix.positions.flatMap((p) => p.shifts).find((s) => s.confirmed + s.proposed < s.min_staff);
|
|
const candidate = volunteers.find(
|
|
(v) => !assignments.some((a) => a.shift_id === openShift?.shift_id && a.volunteer_id === v.id),
|
|
);
|
|
const validation = openShift && candidate
|
|
? await api.assignments.validate(e, { volunteer_id: candidate.id, shift_id: openShift.shift_id })
|
|
: { blocking: false, violations: [] };
|
|
|
|
let dupError = 'none';
|
|
const existing = assignments[0];
|
|
try {
|
|
await api.assignments.create(e, { volunteer_id: existing.volunteer_id, shift_id: existing.shift_id });
|
|
} catch (err) {
|
|
dupError = err instanceof Error ? err.message : String(err);
|
|
}
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
volunteers: volunteers.length,
|
|
positions: positions.length,
|
|
shifts: shifts.length,
|
|
assignments: assignments.length,
|
|
matrixPositions: matrix.positions.length,
|
|
matrixShifts: matrix.positions.reduce((n, p) => n + p.shifts.length, 0),
|
|
sampleValidation: validation.violations.map((v) => v.code),
|
|
duplicateCreateRejectedWith: dupError,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
let failures = 0;
|
|
|
|
await checkMockApi().catch((err) => {
|
|
failures += 1;
|
|
console.error('FAIL checkMockApi', err);
|
|
});
|
|
|
|
for (const path of paths) {
|
|
try {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
|
await prefetch(client);
|
|
const router = createMemoryRouter(routes, { initialEntries: [path] });
|
|
const html = renderToString(
|
|
createElement(QueryClientProvider, { client }, createElement(RouterProvider, { router })),
|
|
);
|
|
console.log(`ok ${path.padEnd(20)} ${html.length} octets`);
|
|
} catch (error) {
|
|
failures += 1;
|
|
console.error(`FAIL ${path}`, error);
|
|
}
|
|
}
|
|
|
|
process.exit(failures ? 1 : 0);
|