mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
209 lines
5.8 KiB
TypeScript
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,
|
|
});
|
|
}
|