shoc-frontend-new/src/domain/work-orders/use-cases/use-work-order-table-mutations.ts

179 lines
5.7 KiB
TypeScript
Raw Normal View History

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<ReturnType<typeof workOrdersApi.getById>>) {
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),
};
}