mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 20:51:53 +00:00
feat(r1.2): écrans web du référentiel — 7 écrans fidèles à maquette-r1
- Sites : liste + carte Leaflet/OSM réelle (pins maquette, positions PostGIS), création en modale avec position posée au clic sur la carte - Fiche site : arbre site→zones, appareils rattachés, ajout de zone - Ascenseurs : table dense, filtres site/statut, strie rouge à l'arrêt ; fiche appareil (organes ± ajout/retrait, statut, QR réel) ; création identité → rattachement → organes ; étiquette A6 imprimable (objet papier, window.print n'imprime qu'elle) - Personnes & équipes : invitation → lien d'activation affiché à copier (pas d'email en R1, décision explicite), renvoi de lien, équipes ; page /activation (choix du mot de passe, connexion directe) - Catégories : ajout, renommage inline, désactivation - navigation et actions pilotées par la matrice (usePermissions) — l'API re-vérifie chaque requête - e2e Playwright : recette R1 officielle rejouée intégralement (site → zone → appareil+organe → étiquette → invitation → activation) + parcours « la matrice pilote l'UI » ; purge idempotente en globalSetup - alias Vite @siop/shared → source TS (exports nommés CJS ↯ workspace lié) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
190
apps/web/src/api/referentiel.ts
Normal file
190
apps/web/src/api/referentiel.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import type {
|
||||
AssetComponentCreate,
|
||||
AssetCreate,
|
||||
AssetUpdate,
|
||||
CategoryCreate,
|
||||
CategoryUpdate,
|
||||
InvitationCreate,
|
||||
LocationCreate,
|
||||
TeamCreate,
|
||||
UserUpdate,
|
||||
} from '@siop/shared';
|
||||
import { api } from './client';
|
||||
|
||||
/** Hooks R1 — chaque appel passe par le client typé (règle d'or). */
|
||||
|
||||
async function unwrap<T>(res: { data?: T; error?: unknown; response: Response }): Promise<T> {
|
||||
if (res.error || res.data === undefined) {
|
||||
const message =
|
||||
(res.error as { message?: string } | undefined)?.message ??
|
||||
`Le serveur a répondu ${res.response.status}`;
|
||||
throw new Error(Array.isArray(message) ? message.join(' — ') : message);
|
||||
}
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export function useLocations() {
|
||||
return useQuery({
|
||||
queryKey: ['locations'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/locations'))).locations,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAssets() {
|
||||
return useQuery({
|
||||
queryKey: ['assets'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/assets'))).assets,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAsset(id: string | undefined) {
|
||||
return useQuery({
|
||||
queryKey: ['assets', id],
|
||||
enabled: !!id,
|
||||
queryFn: async () =>
|
||||
unwrap(await api.GET('/assets/{id}', { params: { path: { id: id! } } })),
|
||||
});
|
||||
}
|
||||
|
||||
export function useCategories() {
|
||||
return useQuery({
|
||||
queryKey: ['categories'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/categories'))).categories,
|
||||
});
|
||||
}
|
||||
|
||||
export function useTeams() {
|
||||
return useQuery({
|
||||
queryKey: ['teams'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/teams'))).teams,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUsers() {
|
||||
return useQuery({
|
||||
queryKey: ['users'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/users'))).users,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRoles() {
|
||||
return useQuery({
|
||||
queryKey: ['roles'],
|
||||
queryFn: async () => (await unwrap(await api.GET('/roles'))).roles,
|
||||
});
|
||||
}
|
||||
|
||||
function useInvalidate(...keys: string[]) {
|
||||
const queryClient = useQueryClient();
|
||||
return () =>
|
||||
Promise.all(keys.map((key) => queryClient.invalidateQueries({ queryKey: [key] })));
|
||||
}
|
||||
|
||||
export function useCreateLocation() {
|
||||
const invalidate = useInvalidate('locations');
|
||||
return useMutation({
|
||||
mutationFn: async (body: LocationCreate) =>
|
||||
unwrap(await api.POST('/locations', { body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateAsset() {
|
||||
const invalidate = useInvalidate('assets', 'locations');
|
||||
return useMutation({
|
||||
mutationFn: async (body: AssetCreate) => unwrap(await api.POST('/assets', { body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAsset(id: string) {
|
||||
const invalidate = useInvalidate('assets');
|
||||
return useMutation({
|
||||
mutationFn: async (body: AssetUpdate) =>
|
||||
unwrap(await api.PATCH('/assets/{id}', { params: { path: { id } }, body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAddComponent(assetId: string) {
|
||||
const invalidate = useInvalidate('assets');
|
||||
return useMutation({
|
||||
mutationFn: async (body: AssetComponentCreate) =>
|
||||
unwrap(
|
||||
await api.POST('/assets/{id}/components', {
|
||||
params: { path: { id: assetId } },
|
||||
body,
|
||||
}),
|
||||
),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useRemoveComponent(assetId: string) {
|
||||
const invalidate = useInvalidate('assets');
|
||||
return useMutation({
|
||||
mutationFn: async (componentId: string) => {
|
||||
const res = await api.DELETE('/assets/{id}/components/{componentId}', {
|
||||
params: { path: { id: assetId, componentId } },
|
||||
});
|
||||
if (res.response.status >= 400) throw new Error('Suppression impossible');
|
||||
},
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateCategory() {
|
||||
const invalidate = useInvalidate('categories');
|
||||
return useMutation({
|
||||
mutationFn: async (body: CategoryCreate) =>
|
||||
unwrap(await api.POST('/categories', { body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCategory() {
|
||||
const invalidate = useInvalidate('categories');
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...body }: CategoryUpdate & { id: string }) =>
|
||||
unwrap(await api.PATCH('/categories/{id}', { params: { path: { id } }, body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateTeam() {
|
||||
const invalidate = useInvalidate('teams', 'users');
|
||||
return useMutation({
|
||||
mutationFn: async (body: TeamCreate) => unwrap(await api.POST('/teams', { body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useInviteUser() {
|
||||
const invalidate = useInvalidate('users');
|
||||
return useMutation({
|
||||
mutationFn: async (body: InvitationCreate) =>
|
||||
unwrap(await api.POST('/users/invitations', { body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useResendInvitation() {
|
||||
const invalidate = useInvalidate('users');
|
||||
return useMutation({
|
||||
mutationFn: async (userId: string) =>
|
||||
unwrap(
|
||||
await api.POST('/users/{id}/invitation', { params: { path: { id: userId } } }),
|
||||
),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateUser() {
|
||||
const invalidate = useInvalidate('users', 'teams');
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...body }: UserUpdate & { id: string }) =>
|
||||
unwrap(await api.PATCH('/users/{id}', { params: { path: { id } }, body })),
|
||||
onSuccess: invalidate,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user