From 7c744352d10af36f550449e5c0576b927e4db447 Mon Sep 17 00:00:00 2001 From: Marshal Date: Sun, 28 Jun 2026 15:38:35 +0000 Subject: [PATCH] feat: streamline clearance action handling and enhance document upload process --- .../portal/src/pages/MyPortalPage/actions.ts | 16 +- .../components/ActionNeededSection.tsx | 16 +- .../contracts/ContractClearancePanel.tsx | 40 +- .../pages/contracts/ContractDetailPage.tsx | 178 ++++++-- docs/freight-platform/SYSTEM-FLOW.md | 390 ++++++++++++++++++ 5 files changed, 592 insertions(+), 48 deletions(-) create mode 100644 docs/freight-platform/SYSTEM-FLOW.md diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/actions.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/actions.ts index dddd6540e..cb37ac9a3 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/actions.ts +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/actions.ts @@ -15,10 +15,9 @@ export interface ActionItem { urgent?: boolean; } -const CLEARANCE_UPLOAD_STATUSES = [ - "AWAITING_CLEARANCE_DOCUMENTS", - "CLEARANCE_UNDER_REVIEW", -]; +// Only AWAITING is a pending CUSTOMER action (initial upload or re-upload after a +// query). UNDER_REVIEW is waiting on staff, so it doesn't belong on the card. +const CLEARANCE_ACTION_STATUSES = ["AWAITING_CLEARANCE_DOCUMENTS"]; /** * Derive the list of pending customer actions from the customer's contracts and @@ -43,17 +42,14 @@ export function deriveActionItems( }); continue; } - if (CLEARANCE_UPLOAD_STATUSES.includes(c.status)) { - const queried = c.status === "AWAITING_CLEARANCE_DOCUMENTS"; + if (CLEARANCE_ACTION_STATUSES.includes(c.status)) { items.push({ id: `clearance-${c.id}`, kind: "clearance", reference: c.reference, - description: queried - ? "Clearance document needs correction" - : "Upload clearance documents", + description: "Clearance documents needed", targetId: c.id, - urgent: queried, + urgent: true, }); continue; } diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActionNeededSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActionNeededSection.tsx index 064d518a1..fe7a09253 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActionNeededSection.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActionNeededSection.tsx @@ -145,18 +145,26 @@ export function ActionNeededSection({ items }: ActionNeededSectionProps) { ); diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractClearancePanel.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractClearancePanel.tsx index 508ce1c29..42a0f799e 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractClearancePanel.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractClearancePanel.tsx @@ -111,10 +111,21 @@ export function ContractClearancePanel({ }, }); - const customerDocs = useMemo( - () => - (clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"), - [clearance], + const customerDocs = useMemo(() => { + const docs = (clearance?.documents ?? []).filter( + (d) => d.uploadedBy === "customer", + ); + // Surface queried documents (the ones needing correction) first. + const rank = (s: string | null) => + s === "QUERIED" ? 0 : s === "APPROVED" ? 2 : 1; + return [...docs].sort( + (a, b) => rank(a.reviewStatus) - rank(b.reviewStatus), + ); + }, [clearance]); + + const queriedCount = useMemo( + () => customerDocs.filter((d) => d.reviewStatus === "QUERIED").length, + [customerDocs], ); const glDocs = useMemo( () => (clearance?.documents ?? []).filter((d) => d.uploadedBy !== "customer"), @@ -167,6 +178,18 @@ export function ContractClearancePanel({ const body = ( + {queriedCount > 0 && ( + } + mb="md" + title={`${queriedCount} document${queriedCount > 1 ? "s" : ""} need correction`} + > + Re-upload the highlighted document{queriedCount > 1 ? "s" : ""} below to + continue. The reviewer's note explains what to fix. + + )} {isReady ? ( } mb="md"> {customsPath @@ -201,7 +224,14 @@ export function ContractClearancePanel({ diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx index e5fb8ca93..433711ddc 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractDetailPage.tsx @@ -100,7 +100,9 @@ function groupContractDocuments(files: ContractFile[]): DocGroup[] { const profile: ContractFile[] = []; const clearance: ContractFile[] = []; for (const f of files) { + // Signature images are baked into the contract PDF — don't list them here. if (f.code === "contract") contract.push(f); + else if (f.code.startsWith("signature_")) continue; else if (PROFILE_DOC_CODES.has(f.code)) profile.push(f); else clearance.push(f); } @@ -444,6 +446,20 @@ export default function ContractDetailPage() { ? "The Operations team is reviewing your clearance documents. Re-upload any queried documents to proceed." : "Your clearance documents are approved. You can now create a shipment booking under this contract."} + {contract.status !== "CLEARANCE_READY_FOR_BOOKING" && ( + + )} )} @@ -664,33 +680,75 @@ export default function ContractDetailPage() { )} - {files.length === 0 ? ( - - + {docGroups.length === 0 ? ( + + + + + + No documents yet + - No documents yet. The signed contract and any uploaded - clearance documents will appear here. + The signed contract and any uploaded clearance documents will + appear here. ) : ( - - {docGroups.map((group) => ( - - - - {group.title} - - - {group.files.length} - - - - {group.files.map((file) => ( - - ))} + + {docGroups.map((group) => { + const accent = DOC_GROUP_ACCENT[group.key] ?? GREEN; + const Icon = DOC_GROUP_ICON[group.key] ?? FileText; + return ( + + + + + + + {group.title} + + + {group.files.length} + + + + {group.files.map((file) => ( + + ))} + - - ))} + ); + })} )} @@ -813,6 +871,18 @@ const KEY_FACT_ACCENT: Record = { orange: "#C77F09", }; +// Per-section accent + icon for the Documents tab groups. +const DOC_GROUP_ACCENT: Record = { + contract: GREEN, + profile: "#2B6CB0", + clearance: "#C77F09", +}; +const DOC_GROUP_ICON: Record = { + contract: FileSignature, + profile: FileText, + clearance: Upload, +}; + /** * A pill-style detail tab matching the backoffice booking-requests tabs: an * icon, a label, and an always-visible count badge (shows 0 when empty). @@ -897,6 +967,30 @@ function SectionLabel({ * stored filename and size as secondary text, and view / download actions that * stream through the API by file id. */ +/** Extension → a small colored type chip (PDF red, image green, etc.). */ +function fileTypeChip(name: string, mimeType?: string | null): { + ext: string; + color: string; +} { + const dot = name.lastIndexOf("."); + let ext = dot >= 0 ? name.slice(dot + 1).toUpperCase() : ""; + if (!ext && mimeType) ext = mimeType.split("/")[1]?.toUpperCase() ?? "FILE"; + if (!ext) ext = "FILE"; + const color = + ext === "PDF" + ? "#D64545" + : ["PNG", "JPG", "JPEG", "GIF", "WEBP", "SVG"].includes(ext) + ? "#2F9E6E" + : ["DOC", "DOCX"].includes(ext) + ? "#2B6CB0" + : ["XLS", "XLSX", "CSV"].includes(ext) + ? "#2F855A" + : ["MP4", "WEBM", "MOV"].includes(ext) + ? "#7A40C8" + : "#6B7C8E"; + return { ext: ext.slice(0, 4), color }; +} + function DocFileRow({ file, onView, @@ -905,15 +999,45 @@ function DocFileRow({ onView: (f: ViewableFile) => void; }) { const kind = labelForDocCode(file.code); + const { ext, color } = fileTypeChip(file.name, file.mimeType); + const viewable = isViewable({ + name: file.name, + url: fileViewUrl(file.id), + mimeType: file.mimeType, + }); return ( - + + + + {ext} + + {kind} @@ -925,11 +1049,7 @@ function DocFileRow({ - {isViewable({ - name: file.name, - url: fileViewUrl(file.id), - mimeType: file.mimeType, - }) && ( + {viewable && (