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

136 lines
4.2 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 function useWorkOrderTableMutations(
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void,
getRow?: (id: string | number) => WorkOrderTableRow | undefined,
) {
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;
for (const op of ops) {
latest = await workOrdersApi.patchBoardField(id, buildBoardPatchPayload(op, latest));
}
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 }) => {
if (error instanceof BoardPatchConflictError && error.currentState) {
onPatch(id, error.currentState);
toast.error(error.message || "Conflict — refreshed with latest data");
} else if (error instanceof BoardPatchValidationError) {
toast.error(error.message);
} else {
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),
};
}