Files
siop2/apps/web/src/api/referentiel.ts
pr-daaif 460ef4a80e feat(r3.4): corrections de recette (8 écarts) + recherche globale ⌘K
Arbitrage du référent sur la revue pixel : tout corriger, activer la
recherche.

- Stock : filtre fournisseur, sous-seuil en tête, « Entrée de stock »
  depuis la liste ; fiche pièce : fournisseur → lien Tiers.
- Statistiques : période 3/6/12 mois (paramètre months au contrat).
- Tiers : rattachements syndic→site (migration r3_recette_fixes,
  Location.partnerId gardé CLIENT), éditable sur la fiche site, seedé.
- Bibliothèque : filtre « Rattaché à » + glisser-déposer (modale
  préremplie, rattachement toujours requis).
- Recherche globale : GET /search (73 opérations) — familles OT/
  ascenseurs/sites filtrées par la matrice, « voir autre » respecté ;
  topbar ⌘K, debounce, résultats groupés, navigation clavier.

74 tests API (8 nouveaux sur le scoping de la recherche), 14/14
Playwright dont un parcours « recette corrigée », 18/18 contrôles en
navigateur réel, zéro erreur console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 02:14:43 +01:00

209 lines
5.8 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type {
AssetComponentCreate,
AssetCreate,
AssetUpdate,
CategoryCreate,
CategoryUpdate,
InvitationCreate,
LocationCreate,
LocationUpdate,
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,
});
}
/** Options minimales (signalement) — accessibles à tout rôle authentifié. */
export function useAssetOptions() {
return useQuery({
queryKey: ['asset-options'],
queryFn: async () => (await unwrap(await api.GET('/assets/options'))).options,
});
}
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 useUpdateLocation(id: string) {
const invalidate = useInvalidate('locations', 'partners');
return useMutation({
mutationFn: async (body: LocationUpdate) =>
unwrap(await api.PATCH('/locations/{id}', { params: { path: { id } }, 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,
});
}