shoc-frontend-new/src/app/(protected)/workorders/_hooks/use-dispatch-detail-controller.ts
Arthur Bassi 4af826a1e0
feat(work-orders): query broadcast, remove auth bypass, dispatch polish (#100)
* fix(work-orders): address wizard create review findings

[recover] remove malicious eslint payload (was 166c63e4)

* fix(work-orders): remove legacy completedDate EditWorkorder path

[recover] remove malicious eslint payload (was f4e6132b)

* fix(work-orders): use local calendar day for completedDate

Restore todayIso() after parent merge reintroduced UTC slice, and keep the wizard-date-utils regression test.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(work-orders): send wizard status and clear POC notes on site change

Map draft.status to lifecycleStatus on board create, and reset pocNotes with POC fields when the wizard site changes.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(query): scope broadcast channel per account and clear on auth

Isolate TanStack Query broadcast by userId, dispose and clear on
logout/401, and extract vendor filter drawers for governance.

* feat(work-orders): board wizard create with service notes

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(work-orders): enforce single-service wizard create per SH-118

Use single service selection in the wizard and omit extraServices from board create.

* fix(vendors): restore filter drawer labels for e2e and visual CI

Use VendorFilterOptions again and keep the Apply filters footer label expected
by vendor Playwright specs and the committed visual baseline.

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-14 11:52:48 -03:00

181 lines
5.8 KiB
TypeScript

import { useMemo, useState } from "react";
import type { DispatchDetail } from "@/domain/work-orders/types/work-order";
import { useDispatchUpliftController } from "@/app/(protected)/workorders/_hooks/use-dispatch-uplift-controller";
import { useDispatchDetail } from "@/domain/work-orders/use-cases/use-work-order-detail";
import {
useAddChecklistItem,
useAddDispatchComment,
useAddDispatchSignoff,
useUpdateChecklistItem,
useUpdateDispatch,
useVerifyDispatch,
} from "@/domain/work-orders/use-cases/use-work-order-mutations";
import { isDispatchLocked } from "@/domain/work-orders/utils/is-dispatch-locked";
import { useAuthContext } from "@/providers/auth-context";
import { toDateInputValue } from "@/lib/time-utils";
import { buildDispatchEditForm } from "@/app/(protected)/workorders/_components/dispatch/dispatch-detail/build-dispatch-edit-form";
import type { DispatchEditFormState } from "@/app/(protected)/workorders/_components/dispatch/dispatch-detail/dispatch-edit-form";
function buildVerifyState(dispatch?: DispatchDetail) {
if (!dispatch) {
return { canVerify: false, missing: [] as string[] };
}
const checklistTotal = dispatch.checklistItems.length;
const checklistDone = dispatch.checklistItems.filter((item) => item.isCompleted).length;
const signoffTypes = new Set(dispatch.signoffs.map((signoff) => signoff.signoffType));
const missing: string[] = [];
if (checklistDone < checklistTotal) {
missing.push(`${checklistTotal - checklistDone} checklist item(s)`);
}
if (!signoffTypes.has("Customer")) {
missing.push("Customer sign-off");
}
if (!signoffTypes.has("Vendor")) {
missing.push("Vendor sign-off");
}
return {
missing,
canVerify: missing.length === 0 && (checklistTotal > 0 || dispatch.signoffs.length >= 2),
};
}
export function useDispatchDetailController(
dispatchId: string | number,
workOrderId: string | number,
) {
const { user } = useAuthContext();
const { data: dispatch, isLoading, isFetching, refetch } = useDispatchDetail(dispatchId);
const uplift = useDispatchUpliftController(dispatchId);
const [editing, setEditing] = useState(false);
const [form, setForm] = useState<DispatchEditFormState>({
status: "",
nteAmount: "",
scheduledDate: "",
completedDate: "",
description: "",
});
const [newChecklistItem, setNewChecklistItem] = useState("");
const [commentText, setCommentText] = useState("");
const [sendEmail, setSendEmail] = useState(true);
const [sigCaptureType, setSigCaptureType] = useState<string | null>(null);
const updateDispatchMutation = useUpdateDispatch(dispatchId, workOrderId);
const addCommentMutation = useAddDispatchComment(dispatchId);
const updateChecklistMutation = useUpdateChecklistItem(dispatchId);
const addChecklistMutation = useAddChecklistItem(dispatchId);
const addSignoffMutation = useAddDispatchSignoff(dispatchId);
const verifyMutation = useVerifyDispatch(dispatchId, workOrderId);
const userName =
(typeof user?.name === "string" ? user.name : undefined) ??
(typeof user?.fullname === "string" ? user.fullname : undefined) ??
"User";
const isLocked = dispatch ? isDispatchLocked(dispatch) : false;
const verifyState = useMemo(() => buildVerifyState(dispatch), [dispatch]);
const beginEditing = () => {
if (dispatch) {
setForm(buildDispatchEditForm(dispatch));
}
setEditing(true);
};
const handleSave = () => {
updateDispatchMutation.mutate(
{
id: dispatchId,
status: form.status,
nteAmount: form.nteAmount ? parseFloat(form.nteAmount) : null,
scheduledDate: form.scheduledDate || null,
completedDate: form.completedDate || null,
description: form.description,
},
{ onSuccess: () => setEditing(false) },
);
};
const handleCancelDispatch = (current: DispatchDetail) => {
if (!window.confirm("Are you sure you want to cancel this dispatch?")) {
return;
}
updateDispatchMutation.mutate({
id: dispatchId,
status: "Cancelled",
nteAmount: current.nteAmount,
scheduledDate: toDateInputValue(current.scheduledDate) || null,
completedDate: toDateInputValue(current.completedDate) || null,
description: current.description,
});
};
const handleComment = (event: React.FormEvent) => {
event.preventDefault();
if (!commentText.trim()) {
return;
}
addCommentMutation.mutate(
{ text: commentText.trim(), sendEmail },
{ onSuccess: () => setCommentText("") },
);
};
const isMutating =
updateDispatchMutation.isPending ||
addCommentMutation.isPending ||
updateChecklistMutation.isPending ||
addChecklistMutation.isPending ||
addSignoffMutation.isPending ||
verifyMutation.isPending ||
uplift.isMutating;
return {
dispatch,
isLoading,
isFetching,
refetch,
uplifts: uplift.uplifts,
isLoadingUplifts: uplift.isLoadingUplifts,
editing,
form,
setForm,
newChecklistItem,
setNewChecklistItem,
commentText,
setCommentText,
sendEmail,
setSendEmail,
sigCaptureType,
setSigCaptureType,
upliftAction: uplift.upliftAction,
setUpliftAction: uplift.setUpliftAction,
isLocked,
isMutating,
userName,
verifyState,
beginEditing,
handleSave,
handleCancelDispatch,
handleComment,
handleUpliftConfirm: uplift.handleUpliftConfirm,
setEditing,
updateDispatchMutation,
updateChecklistMutation,
addChecklistMutation,
addSignoffMutation,
verifyMutation,
approveUpliftMutation: uplift.approveUpliftMutation,
rejectUpliftMutation: uplift.rejectUpliftMutation,
requestChangesUpliftMutation: uplift.requestChangesUpliftMutation,
addCommentMutation,
};
}
export type DispatchDetailController = ReturnType<typeof useDispatchDetailController>;