mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-09-30 11:33:12 +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,
|
||||
"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,
|
||||
|
|
|
|||
|
|
@ -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)}` : ""}
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue