mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
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>
This commit is contained in:
235
apps/web/src/api/exploitation.ts
Normal file
235
apps/web/src/api/exploitation.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user