mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 23:53:21 +00:00
* 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>
181 lines
5.8 KiB
TypeScript
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>;
|