mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-04 20:42:05 +00:00
refactor: enforce dispatch detail quality gates
This commit is contained in:
parent
ea76a5d0ba
commit
09ed3693f6
7 changed files with 113 additions and 115 deletions
|
|
@ -8,11 +8,6 @@
|
||||||
"maxLines": 797,
|
"maxLines": 797,
|
||||||
"reason": "Work-order detail route page; legacy godfile targeted for decomposition into focused state components."
|
"reason": "Work-order detail route page; legacy godfile targeted for decomposition into focused state components."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"path": "src/app/(protected)/workorders/_components/dispatch-detail-modal.tsx",
|
|
||||||
"maxLines": 782,
|
|
||||||
"reason": "Dispatch detail modal; oversized legacy component pending extraction of sections/fields."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"path": "src/app/(protected)/vendors/index.tsx",
|
"path": "src/app/(protected)/vendors/index.tsx",
|
||||||
"maxLines": 612,
|
"maxLines": 612,
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export function DispatchChecklistSection({
|
||||||
/>
|
/>
|
||||||
<Box className="flex-1">
|
<Box className="flex-1">
|
||||||
<Typography variant="body2">{item.itemText}</Typography>
|
<Typography variant="body2">{item.itemText}</Typography>
|
||||||
{item.isCompleted && (item.completedBy || item.completedAt) && (
|
{item.isCompleted && Boolean(item.completedBy || item.completedAt) && (
|
||||||
<Typography variant="caption" color="text.secondary">
|
<Typography variant="caption" color="text.secondary">
|
||||||
{item.completedBy?.trim() || "Unknown"}
|
{item.completedBy?.trim() || "Unknown"}
|
||||||
{item.completedAt ? ` · ${formatDateTime(item.completedAt)}` : ""}
|
{item.completedAt ? ` · ${formatDateTime(item.completedAt)}` : ""}
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,7 @@ export function DispatchDetailsView({
|
||||||
highlight={dispatch.status === "Sent" && !dispatch.acknowledgedAt ? waitColor : undefined}
|
highlight={dispatch.status === "Sent" && !dispatch.acknowledgedAt ? waitColor : undefined}
|
||||||
/>
|
/>
|
||||||
<DetailField label="Vendor Email" value={dispatch.vendorEmail || "—"} />
|
<DetailField label="Vendor Email" value={dispatch.vendorEmail || "—"} />
|
||||||
{dispatch.description && (
|
{Boolean(dispatch.description) && (
|
||||||
<DetailField label="Description" value={dispatch.description} className="sm:col-span-2" />
|
<DetailField label="Description" value={dispatch.description} className="sm:col-span-2" />
|
||||||
)}
|
)}
|
||||||
{!isLocked && (
|
{!isLocked && (
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export function DispatchSignoffsSection({
|
||||||
<Typography variant="subtitle2">{type}</Typography>
|
<Typography variant="subtitle2">{type}</Typography>
|
||||||
{signoff ? (
|
{signoff ? (
|
||||||
<Box className="mt-2">
|
<Box className="mt-2">
|
||||||
{signoff.signatureMethod === "drawn" && signoff.signature && (
|
{signoff.signatureMethod === "drawn" && Boolean(signoff.signature) && (
|
||||||
<img
|
<img
|
||||||
src={signoff.signature}
|
src={signoff.signature}
|
||||||
alt="Signature"
|
alt="Signature"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,78 @@ type DispatchUpliftsSectionProps = {
|
||||||
onDeny: (uplift: UpliftRequest) => void;
|
onDeny: (uplift: UpliftRequest) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpliftCardProps = Omit<DispatchUpliftsSectionProps, "uplifts"> & {
|
||||||
|
uplift: UpliftRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
function UpliftCard({ uplift, isLocked, onApprove, onDeny }: UpliftCardProps) {
|
||||||
|
const canDecide = uplift.canDecide && uplift.status === "Pending" && !isLocked;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box className="rounded border border-border p-3">
|
||||||
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center", flexWrap: "wrap" }}>
|
||||||
|
<Chip label={uplift.status} size="small" />
|
||||||
|
{uplift.requiredTier === 2 && (
|
||||||
|
<Typography variant="caption">Tier 2 · Manager approval</Typography>
|
||||||
|
)}
|
||||||
|
<Typography variant="body2">
|
||||||
|
${Number(uplift.currentNTE ?? 0).toFixed(2)} → ${Number(uplift.requestedNTE).toFixed(2)}{" "}
|
||||||
|
(+${Number(uplift.delta).toFixed(2)})
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
{Boolean(uplift.vendorReason) && (
|
||||||
|
<Typography variant="body2" className="mt-1">
|
||||||
|
<strong>Reason:</strong> {uplift.vendorReason}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{Boolean(uplift.requestedByVendorName || uplift.requestedAt) && (
|
||||||
|
<Typography variant="body2" color="text.secondary" className="mt-1">
|
||||||
|
Requested by {uplift.requestedByVendorName || "vendor"}
|
||||||
|
{uplift.requestedAt ? ` · ${formatDateTime(uplift.requestedAt)}` : ""}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{uplift.status !== "Pending" &&
|
||||||
|
Boolean(uplift.decidedByName || uplift.decidedAt || uplift.decisionNote) && (
|
||||||
|
<Box className="mt-1">
|
||||||
|
{Boolean(uplift.decidedByName || uplift.decidedAt) && (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{uplift.status} by {uplift.decidedByName || "—"}
|
||||||
|
{uplift.decidedAt ? ` · ${formatDateTime(uplift.decidedAt)}` : ""}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{Boolean(uplift.decisionNote) && (
|
||||||
|
<Typography variant="body2" className="mt-0.5">
|
||||||
|
<strong>Note:</strong> {uplift.decisionNote}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{uplift.status === "Pending" && (
|
||||||
|
<Stack direction="row" spacing={1} className="mt-2">
|
||||||
|
{(["approve", "deny"] as const).map((action) => (
|
||||||
|
<Tooltip
|
||||||
|
key={action}
|
||||||
|
title={!canDecide ? `Requires Tier ${uplift.requiredTier} role` : ""}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
color={action === "approve" ? "success" : "error"}
|
||||||
|
variant="contained"
|
||||||
|
disabled={!canDecide}
|
||||||
|
onClick={() => (action === "approve" ? onApprove(uplift) : onDeny(uplift))}
|
||||||
|
>
|
||||||
|
{action === "approve" ? "Approve" : "Deny"}
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DispatchUpliftsSection({
|
export function DispatchUpliftsSection({
|
||||||
uplifts,
|
uplifts,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
|
@ -24,82 +96,15 @@ export function DispatchUpliftsSection({
|
||||||
<Typography color="text.secondary">No uplift requests.</Typography>
|
<Typography color="text.secondary">No uplift requests.</Typography>
|
||||||
) : (
|
) : (
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
{uplifts.map((uplift) => {
|
{uplifts.map((uplift) => (
|
||||||
const canDecide = uplift.canDecide && uplift.status === "Pending" && !isLocked;
|
<UpliftCard
|
||||||
|
key={String(uplift.id)}
|
||||||
return (
|
uplift={uplift}
|
||||||
<Box key={String(uplift.id)} className="rounded border border-border p-3">
|
isLocked={isLocked}
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: "center", flexWrap: "wrap" }}>
|
onApprove={onApprove}
|
||||||
<Chip label={uplift.status} size="small" />
|
onDeny={onDeny}
|
||||||
{uplift.requiredTier === 2 && (
|
/>
|
||||||
<Typography variant="caption">Tier 2 · Manager approval</Typography>
|
))}
|
||||||
)}
|
|
||||||
<Typography variant="body2">
|
|
||||||
${Number(uplift.currentNTE ?? 0).toFixed(2)} → $
|
|
||||||
{Number(uplift.requestedNTE).toFixed(2)} (+$
|
|
||||||
{Number(uplift.delta).toFixed(2)})
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
{uplift.vendorReason && (
|
|
||||||
<Typography variant="body2" className="mt-1">
|
|
||||||
<strong>Reason:</strong> {uplift.vendorReason}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{(uplift.requestedByVendorName || uplift.requestedAt) && (
|
|
||||||
<Typography variant="body2" color="text.secondary" className="mt-1">
|
|
||||||
Requested by {uplift.requestedByVendorName || "vendor"}
|
|
||||||
{uplift.requestedAt ? ` · ${formatDateTime(uplift.requestedAt)}` : ""}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{uplift.status !== "Pending" &&
|
|
||||||
(uplift.decidedByName || uplift.decidedAt || uplift.decisionNote) && (
|
|
||||||
<Box className="mt-1">
|
|
||||||
{(uplift.decidedByName || uplift.decidedAt) && (
|
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{uplift.status} by {uplift.decidedByName || "—"}
|
|
||||||
{uplift.decidedAt ? ` · ${formatDateTime(uplift.decidedAt)}` : ""}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{uplift.decisionNote && (
|
|
||||||
<Typography variant="body2" className="mt-0.5">
|
|
||||||
<strong>Note:</strong> {uplift.decisionNote}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
{uplift.status === "Pending" && (
|
|
||||||
<Stack direction="row" spacing={1} className="mt-2">
|
|
||||||
<Tooltip title={!canDecide ? `Requires Tier ${uplift.requiredTier} role` : ""}>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
color="success"
|
|
||||||
variant="contained"
|
|
||||||
disabled={!canDecide}
|
|
||||||
onClick={() => onApprove(uplift)}
|
|
||||||
>
|
|
||||||
Approve
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title={!canDecide ? `Requires Tier ${uplift.requiredTier} role` : ""}>
|
|
||||||
<span>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
color="error"
|
|
||||||
variant="contained"
|
|
||||||
disabled={!canDecide}
|
|
||||||
onClick={() => onDeny(uplift)}
|
|
||||||
>
|
|
||||||
Deny
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ export function DispatchDetailModal({
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{controller.sigCaptureType && (
|
{controller.sigCaptureType != null && (
|
||||||
<SignatureCapture
|
<SignatureCapture
|
||||||
title={`${controller.sigCaptureType} Sign-off`}
|
title={`${controller.sigCaptureType} Sign-off`}
|
||||||
onClose={() => controller.setSigCaptureType(null)}
|
onClose={() => controller.setSigCaptureType(null)}
|
||||||
|
|
@ -142,7 +142,7 @@ export function DispatchDetailModal({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{controller.upliftAction && (
|
{controller.upliftAction != null && (
|
||||||
<UpliftDecisionDialog
|
<UpliftDecisionDialog
|
||||||
open
|
open
|
||||||
type={controller.upliftAction.type}
|
type={controller.upliftAction.type}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,32 @@ import { toDateInputValue } from "@/lib/time-utils";
|
||||||
import { buildDispatchEditForm } from "@/app/(protected)/workorders/_components/dispatch/dispatch-detail/build-dispatch-edit-form";
|
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";
|
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(
|
export function useDispatchDetailController(
|
||||||
dispatchId: string | number,
|
dispatchId: string | number,
|
||||||
workOrderId: string | number,
|
workOrderId: string | number,
|
||||||
|
|
@ -62,35 +88,7 @@ export function useDispatchDetailController(
|
||||||
|
|
||||||
const isLocked = dispatch ? isDispatchLocked(dispatch) : false;
|
const isLocked = dispatch ? isDispatchLocked(dispatch) : false;
|
||||||
|
|
||||||
const verifyState = useMemo(() => {
|
const verifyState = useMemo(() => buildVerifyState(dispatch), [dispatch]);
|
||||||
if (!dispatch) {
|
|
||||||
return { canVerify: false, missing: [] as string[] };
|
|
||||||
}
|
|
||||||
|
|
||||||
const checklistTotal = dispatch.checklistItems.length;
|
|
||||||
const checklistDone = dispatch.checklistItems.filter((item) => item.isCompleted).length;
|
|
||||||
const signoffCount = dispatch.signoffs.length;
|
|
||||||
const missing: string[] = [];
|
|
||||||
|
|
||||||
if (checklistTotal > 0 && checklistDone < checklistTotal) {
|
|
||||||
missing.push(`${checklistTotal - checklistDone} checklist item(s)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signoffCount < 2) {
|
|
||||||
const types = dispatch.signoffs.map((signoff) => signoff.signoffType);
|
|
||||||
if (!types.includes("Customer")) {
|
|
||||||
missing.push("Customer sign-off");
|
|
||||||
}
|
|
||||||
if (!types.includes("Vendor")) {
|
|
||||||
missing.push("Vendor sign-off");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
missing,
|
|
||||||
canVerify: missing.length === 0 && (checklistTotal > 0 || signoffCount >= 2),
|
|
||||||
};
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const beginEditing = () => {
|
const beginEditing = () => {
|
||||||
if (dispatch) {
|
if (dispatch) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue