2026-07-17 12:01:45 -03:00
|
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { toast } from "react-toastify";
|
|
|
|
|
import { workOrdersApi } from "@/domain/work-orders/api/work-orders-api";
|
|
|
|
|
import {
|
2026-07-17 10:45:59 -03:00
|
|
|
BoardPatchConflictError,
|
|
|
|
|
BoardPatchValidationError,
|
|
|
|
|
} from "@/domain/work-orders/errors/board-patch-errors";
|
|
|
|
|
import { mapApiStatusToWizard } from "@/domain/work-orders/mappers/wizard-draft-mapper";
|
2026-07-17 12:01:45 -03:00
|
|
|
import {
|
2026-07-17 10:45:59 -03:00
|
|
|
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";
|
2026-07-17 12:01:45 -03:00
|
|
|
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";
|
|
|
|
|
|
2026-07-17 10:45:59 -03:00
|
|
|
export type { WorkOrderTablePatch };
|
2026-07-17 12:01:45 -03:00
|
|
|
|
|
|
|
|
export function useWorkOrderTableMutations(
|
|
|
|
|
onPatch: (id: string | number, patch: WorkOrderTablePatch) => void,
|
2026-07-17 10:45:59 -03:00
|
|
|
getRow?: (id: string | number) => WorkOrderTableRow | undefined,
|
2026-07-17 12:01:45 -03:00
|
|
|
) {
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
|
|
const invalidate = () => {
|
|
|
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.workOrders.all });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const patchMutation = useMutation({
|
2026-07-17 10:45:59 -03:00
|
|
|
mutationFn: async ({ id, patch }: { id: string | number; patch: WorkOrderTablePatch }) => {
|
|
|
|
|
if (isLocalOnlyTablePatch(patch)) {
|
|
|
|
|
return { kind: "local" as const, patch };
|
2026-07-17 12:01:45 -03:00
|
|
|
}
|
2026-07-17 10:45:59 -03:00
|
|
|
|
|
|
|
|
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.");
|
2026-07-17 12:01:45 -03:00
|
|
|
}
|
2026-07-17 10:45:59 -03:00
|
|
|
|
|
|
|
|
let latest = current;
|
|
|
|
|
for (const op of ops) {
|
|
|
|
|
latest = await workOrdersApi.patchBoardField(id, buildBoardPatchPayload(op, latest));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { kind: "board" as const, row: latest };
|
2026-07-17 12:01:45 -03:00
|
|
|
},
|
|
|
|
|
onMutate: async ({ id, patch }) => {
|
|
|
|
|
onPatch(id, patch);
|
|
|
|
|
},
|
2026-07-17 10:45:59 -03:00
|
|
|
onSuccess: (result, { id }) => {
|
|
|
|
|
if (result.kind === "board") {
|
|
|
|
|
onPatch(id, result.row);
|
|
|
|
|
}
|
2026-07-17 12:01:45 -03:00
|
|
|
invalidate();
|
|
|
|
|
},
|
2026-07-17 10:45:59 -03:00
|
|
|
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");
|
|
|
|
|
}
|
2026-07-17 12:01:45 -03:00
|
|
|
invalidate();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-17 10:45:59 -03:00
|
|
|
const patchField = (id: string | number, patch: WorkOrderTablePatch) => {
|
|
|
|
|
patchMutation.mutate({ id, patch });
|
2026-07-17 12:01:45 -03:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-17 10:45:59 -03:00
|
|
|
/** Optimistic UI only — persistence auto-schedule is handled by the board PATCH API. */
|
2026-07-17 12:01:45 -03:00
|
|
|
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),
|
|
|
|
|
};
|
|
|
|
|
}
|