Files
siop2/apps/web/src/api/exploitation.ts
pr-daaif 6a2dae1710 feat(r2.3): écrans web exploitation — OT, demandes, préventif, compteurs, urgence
- liste OT (filtres, strie rouge, « immédiat ») ; fiche OT : transitions
  pilotées par allowedTransitions, clôture grisée avec la garde expliquée,
  bilan codé (6 selects sur référentiels), checklist Fait→N-A→à faire,
  activité + commentaires, assignation, annulation motivée
- nouvel OT : interrupteur « personne bloquée » qui force la priorité
- demandes : table + panneau d'approbation (priorité, assignation,
  approuver → fiche OT), rejet en modale à motif obligatoire, signalement
  interne ; statut « Résolue » dérivé de l'OT lié
- préventif : tuiles réelles, générer + résumé (« regénérer ne double
  rien »), gabarits administrables ; compteurs : saisie + historique
- tableau de bord réel : bandeau urgence cliquable, KPIs, OT par statut,
  interventions récentes ; accueil dédié aux rôles sans exploitation
- urgence traversante : chip topbar pulsante (60 s), badges de nav
- GET /assets/options (auth seule) : le Demandeur peut désigner l'appareil
  qu'il signale — trou débusqué par l'e2e (49 opérations au contrat)
- génération préventive durcie : collision de référence RETENTÉE (plus de
  saut silencieux), P2003 toléré ; 3 runs Jest complets consécutifs verts
- 9 tests Playwright (recette R2 officielle rejouée intégralement),
  50 tests API (94,6 % / 78,9 %)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:13:20 +01:00

236 lines
6.7 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import type {
AssigneesUpdate,
ChecklistPatch,
MeterReadingCreate,
PreventiveGenerate,
ReportUpsert,
RequestApprove,
RequestCreate,
RequestReject,
TaskTemplateCreate,
TaskTemplateUpdate,
TransitionRequest,
WorkOrderCreate,
} from '@siop/shared';
import { api } from './client';
/** Hooks R2 — client typé, invalidations ciblées. */
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;
}
function useInvalidate(...keys: string[]) {
const queryClient = useQueryClient();
return () =>
Promise.all(keys.map((key) => queryClient.invalidateQueries({ queryKey: [key] })));
}
// ————— Ordres de travail —————
export function useWorkOrders(enabled = true) {
return useQuery({
queryKey: ['work-orders'],
enabled,
queryFn: async () => (await unwrap(await api.GET('/work-orders'))).workOrders,
});
}
export function useWorkOrder(id: string | undefined) {
return useQuery({
queryKey: ['work-orders', id],
enabled: !!id,
queryFn: async () =>
unwrap(await api.GET('/work-orders/{id}', { params: { path: { id: id! } } })),
});
}
export function useCreateWorkOrder() {
const invalidate = useInvalidate('work-orders');
return useMutation({
mutationFn: async (body: WorkOrderCreate) =>
unwrap(await api.POST('/work-orders', { body })),
onSuccess: invalidate,
});
}
export function useTransitionWorkOrder(id: string) {
const invalidate = useInvalidate('work-orders', 'requests', 'preventive-status');
return useMutation({
mutationFn: async (body: TransitionRequest) =>
unwrap(await api.POST('/work-orders/{id}/transition', { params: { path: { id } }, body })),
onSuccess: invalidate,
});
}
export function useCommentWorkOrder(id: string) {
const invalidate = useInvalidate('work-orders');
return useMutation({
mutationFn: async (message: string) =>
unwrap(
await api.POST('/work-orders/{id}/comments', {
params: { path: { id } },
body: { message },
}),
),
onSuccess: invalidate,
});
}
export function useSetAssignees(id: string) {
const invalidate = useInvalidate('work-orders');
return useMutation({
mutationFn: async (body: AssigneesUpdate) =>
unwrap(await api.PUT('/work-orders/{id}/assignees', { params: { path: { id } }, body })),
onSuccess: invalidate,
});
}
export function useUpsertReport(id: string) {
const invalidate = useInvalidate('work-orders');
return useMutation({
mutationFn: async (body: ReportUpsert) =>
unwrap(await api.PUT('/work-orders/{id}/report', { params: { path: { id } }, body })),
onSuccess: invalidate,
});
}
export function usePatchChecklist(id: string) {
const invalidate = useInvalidate('work-orders');
return useMutation({
mutationFn: async ({ itemId, ...body }: ChecklistPatch & { itemId: string }) =>
unwrap(
await api.PATCH('/work-orders/{id}/checklist/{itemId}', {
params: { path: { id, itemId } },
body,
}),
),
onSuccess: invalidate,
});
}
// ————— Demandes —————
export function useRequests() {
return useQuery({
queryKey: ['requests'],
queryFn: async () => (await unwrap(await api.GET('/requests'))).requests,
});
}
export function useCreateRequest() {
const invalidate = useInvalidate('requests');
return useMutation({
mutationFn: async (body: RequestCreate) => unwrap(await api.POST('/requests', { body })),
onSuccess: invalidate,
});
}
export function useApproveRequest() {
const invalidate = useInvalidate('requests', 'work-orders');
return useMutation({
mutationFn: async ({ id, ...body }: RequestApprove & { id: string }) =>
unwrap(await api.POST('/requests/{id}/approve', { params: { path: { id } }, body })),
onSuccess: invalidate,
});
}
export function useRejectRequest() {
const invalidate = useInvalidate('requests');
return useMutation({
mutationFn: async ({ id, ...body }: RequestReject & { id: string }) =>
unwrap(await api.POST('/requests/{id}/reject', { params: { path: { id } }, body })),
onSuccess: invalidate,
});
}
// ————— Référentiels du bilan —————
export function useReferenceValues() {
return useQuery({
queryKey: ['reference-values'],
staleTime: 5 * 60_000,
queryFn: async () =>
(await unwrap(await api.GET('/reference-values'))).referenceValues,
});
}
// ————— Préventif —————
export function useTaskTemplates() {
return useQuery({
queryKey: ['task-templates'],
queryFn: async () => (await unwrap(await api.GET('/preventive/templates'))).templates,
});
}
export function useCreateTaskTemplate() {
const invalidate = useInvalidate('task-templates');
return useMutation({
mutationFn: async (body: TaskTemplateCreate) =>
unwrap(await api.POST('/preventive/templates', { body })),
onSuccess: invalidate,
});
}
export function useUpdateTaskTemplate() {
const invalidate = useInvalidate('task-templates');
return useMutation({
mutationFn: async ({ id, ...body }: TaskTemplateUpdate & { id: string }) =>
unwrap(
await api.PATCH('/preventive/templates/{id}', { params: { path: { id } }, body }),
),
onSuccess: invalidate,
});
}
export function usePreventiveStatus(enabled = true) {
return useQuery({
queryKey: ['preventive-status'],
enabled,
queryFn: async () => unwrap(await api.GET('/preventive/status')),
});
}
export function useGeneratePreventive() {
const invalidate = useInvalidate('preventive-status', 'work-orders');
return useMutation({
mutationFn: async (body: PreventiveGenerate) =>
unwrap(await api.POST('/preventive/generate', { body })),
onSuccess: invalidate,
});
}
// ————— Compteurs —————
export function useMeters(assetId: string | undefined) {
return useQuery({
queryKey: ['meters', assetId],
enabled: !!assetId,
queryFn: async () =>
(await unwrap(await api.GET('/assets/{id}/meters', { params: { path: { id: assetId! } } })))
.meters,
});
}
export function useAddMeterReading(assetId: string) {
const invalidate = useInvalidate('meters');
return useMutation({
mutationFn: async (body: MeterReadingCreate) =>
unwrap(
await api.POST('/assets/{id}/meter-readings', {
params: { path: { id: assetId } },
body,
}),
),
onSuccess: invalidate,
});
}