refactor: enforce dispatch detail quality gates

This commit is contained in:
Alexandre Brandizzi 2026-07-24 18:51:50 -03:00
parent ea76a5d0ba
commit 09ed3693f6
7 changed files with 113 additions and 115 deletions

View file

@ -8,11 +8,6 @@
"maxLines": 797,
"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",
"maxLines": 612,

View file

@ -42,7 +42,7 @@ export function DispatchChecklistSection({
/>
<Box className="flex-1">
<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">
{item.completedBy?.trim() || "Unknown"}
{item.completedAt ? ` · ${formatDateTime(item.completedAt)}` : ""}

View file

@ -133,7 +133,7 @@ export function DispatchDetailsView({
highlight={dispatch.status === "Sent" && !dispatch.acknowledgedAt ? waitColor : undefined}
/>
<DetailField label="Vendor Email" value={dispatch.vendorEmail || "—"} />
{dispatch.description && (
{Boolean(dispatch.description) && (
<DetailField label="Description" value={dispatch.description} className="sm:col-span-2" />
)}
{!isLocked && (

View file

@ -26,7 +26,7 @@ export function DispatchSignoffsSection({
<Typography variant="subtitle2">{type}</Typography>
{signoff ? (
<Box className="mt-2">
{signoff.signatureMethod === "drawn" && signoff.signature && (
{signoff.signatureMethod === "drawn" && Boolean(signoff.signature) && (
<img
src={signoff.signature}
alt="Signature"

View file

@ -9,6 +9,78 @@ type DispatchUpliftsSectionProps = {
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({
uplifts,
isLocked,
@ -24,82 +96,15 @@ export function DispatchUpliftsSection({
<Typography color="text.secondary">No uplift requests.</Typography>
) : (
<Stack spacing={2}>
{uplifts.map((uplift) => {
const canDecide = uplift.canDecide && uplift.status === "Pending" && !isLocked;
return (
<Box key={String(uplift.id)} 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>
{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>
);
})}
{uplifts.map((uplift) => (
<UpliftCard
key={String(uplift.id)}
uplift={uplift}
isLocked={isLocked}
onApprove={onApprove}
onDeny={onDeny}
/>
))}
</Stack>
)}
</Box>

View file

@ -124,7 +124,7 @@ export function DispatchDetailModal({
</DialogContent>
</Dialog>
{controller.sigCaptureType && (
{controller.sigCaptureType != null && (
<SignatureCapture
title={`${controller.sigCaptureType} Sign-off`}
onClose={() => controller.setSigCaptureType(null)}
@ -142,7 +142,7 @@ export function DispatchDetailModal({
/>
)}
{controller.upliftAction && (
{controller.upliftAction != null && (
<UpliftDecisionDialog
open
type={controller.upliftAction.type}

View file

@ -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 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,
@ -62,35 +88,7 @@ export function useDispatchDetailController(
const isLocked = dispatch ? isDispatchLocked(dispatch) : false;
const verifyState = useMemo(() => {
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 verifyState = useMemo(() => buildVerifyState(dispatch), [dispatch]);
const beginEditing = () => {
if (dispatch) {