import { useMutation, useQueryClient } from "@tanstack/react-query"; import { toast } from "react-toastify"; import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api"; import { BoardPatchConflictError, BoardPatchValidationError, } from "@/domain/work-orders/errors/board-patch-errors"; import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper"; import { buildBoardPatchPayload, expandTablePatchToBoardFieldOps, isLocalOnlyTablePatch, } from "@/domain/work-orders/mappers/work-order-board-patch-mapper"; import { mapWorkOrderTableRow } from "@/domain/work-orders/mappers/work-order-mapper"; import type { WorkOrderTablePatch, WorkOrderTableRow, } from "@/domain/work-orders/types/work-order-table-row"; import { deriveDayFields } from "@/domain/work-orders/utils/table-day-utils"; import { enrichTableRow } from "@/domain/work-orders/utils/table-filter-utils"; import { queryKeys } from "@/infra/query-key/query-key"; export type { WorkOrderTablePatch }; export type WorkOrderTablePatchHandlers = { onPatch: (id: string | number, patch: WorkOrderTablePatch) => void; clearPatch: (id: string | number) => void; getRow?: (id: string | number) => WorkOrderTableRow | undefined; }; export function useWorkOrderTableMutations( onPatchOrHandlers: ((id: string | number, patch: WorkOrderTablePatch) => void) | WorkOrderTablePatchHandlers, getRowArg?: (id: string | number) => WorkOrderTableRow | undefined, ) { const handlers: WorkOrderTablePatchHandlers = typeof onPatchOrHandlers === "function" ? { onPatch: onPatchOrHandlers, clearPatch: () => undefined, getRow: getRowArg, } : onPatchOrHandlers; const { onPatch, clearPatch, getRow } = handlers; const queryClient = useQueryClient(); const invalidate = () => { void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all }); }; const patchMutation = useMutation({ mutationFn: async ({ id, patch }: { id: string | number; patch: WorkOrderTablePatch }) => { if (isLocalOnlyTablePatch(patch)) { return { kind: "local" as const, patch }; } const ops = expandTablePatchToBoardFieldOps(patch); if (ops.length === 0) { throw new BoardPatchValidationError( "NoPersistableFields", "These changes cannot be saved on the board. Refresh and edit a supported field.", ); } const current = getRow?.(id); if (!current?.rowVersion) { throw new Error("Missing work order version for board patch. Refresh and retry."); } let latest = current; let completedOps = 0; for (const op of ops) { try { latest = await workOrdersApi.patchBoardField(id, buildBoardPatchPayload(op, latest)); completedOps += 1; } catch (error) { if (completedOps > 0) { const field = op.field; const partial = new Error( `Schedule update partially saved (${completedOps}/${ops.length} fields). Failed on "${field}": ${ error instanceof Error ? error.message : "Unknown error" }`, ); (partial as Error & { partialRow?: WorkOrderTableRow }).partialRow = latest; throw partial; } throw error; } } return { kind: "board" as const, row: latest }; }, onMutate: async ({ id, patch }) => { onPatch(id, patch); }, onSuccess: (result, { id }) => { if (result.kind === "board") { onPatch(id, result.row); } invalidate(); }, onError: (error: Error, { id }) => { const partialRow = (error as Error & { partialRow?: WorkOrderTableRow }).partialRow; if (partialRow) { onPatch(id, partialRow); toast.error(error.message); invalidate(); return; } if (error instanceof BoardPatchConflictError && error.currentState) { onPatch(id, error.currentState); toast.error(error.message || "Conflict — refreshed with latest data"); } else if (error instanceof BoardPatchValidationError) { clearPatch(id); toast.error(error.message); } else { clearPatch(id); toast.error(error.message || "Failed to update work order"); } invalidate(); }, }); const patchField = (id: string | number, patch: WorkOrderTablePatch) => { patchMutation.mutate({ id, patch }); }; return { patchField, isPending: patchMutation.isPending, }; } export function tableRowFromDetail(detail: Awaited>) { return enrichTableRow( mapWorkOrderTableRow({ ...detail, status: mapApiStatusToWizard(detail.status), scheduledOn: detail.scheduledDate, woNumber: detail.internalWONumber || detail.workOrderNumber, dispatcherName: detail.assignedTo, pm: detail.problem, type: detail.source, }), ); } /** Optimistic UI only — persistence auto-schedule is handled by the board PATCH API. */ export function maybeAutoSchedulePatch(row: WorkOrderTableRow): WorkOrderTablePatch { const hasDate = !!row.scheduledOn; const hasDispatcher = !!row.dispatcherId; if (hasDate && hasDispatcher && row.status === "Incomplete") { return { status: "Scheduled" }; } return {}; } export function applySchedulePatch( scheduledOn: string, scheduleWeekOnly: boolean, ): WorkOrderTablePatch { if (scheduleWeekOnly) { return { scheduledOn: "", targetWeek: scheduledOn, scheduleWeekOnly: true, ...deriveDayFields(""), }; } return { scheduledOn, scheduleWeekOnly: false, targetWeek: "", ...deriveDayFields(scheduledOn), }; }