import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { Alert, Badge, Box, Button, FileButton, Group, Loader, Paper, Progress, Stack, Text, Textarea, ThemeIcon, Tooltip, } from "@mantine/core"; import { AlertCircle, CheckCircle2, Clock, Download, Eye, FileCheck2, FileText, MessageSquareWarning, Upload, UserCheck, } from "lucide-react"; import type { Freight } from "@edr/types"; import { isViewable } from "@edr/ui-common"; import { SectionCard } from "@/components/bookings/detail/SectionCard"; import { QUERY_KEYS } from "@/constants/QUERY_KEYS"; import { contractsService } from "@/services/contracts.service"; import { downloadBookingFile, fetchViewableFile, } from "@/services/files.service"; import { useContractClearanceMutations } from "@/hooks/contracts/useContracts"; import { useFileViewer } from "@/hooks/useFileViewer"; export interface ContractClearanceReviewSectionProps { contractId: string; /** Called after any review/finalize mutation so the parent can refetch. */ onChanged?: () => void; /** Hide the inline progress summary (e.g. when the parent renders its own). */ hideSummary?: boolean; /** * Path A (non-customs): the reviewer is Operations, not GL, and there is no GL * output upload step. Routes review/finalize to the Operations endpoints. */ selfClear?: boolean; /** * Clearance is finalized — render the document outcomes (approved / queried, * by whom, when) but hide all approve / query / finalize actions. */ readOnly?: boolean; /** * Document approvals are locked (e.g. after all docs approved in phased flow) * but queries remain available until {@link queriesLocked} or {@link readOnly}. */ approvalsLocked?: boolean; /** * Pre-clearance finalized — block opening new queries on customer documents. */ queriesLocked?: boolean; /** * ONE_TIME customs contracts use the phased milestone workflow. Hides the * legacy "Finalize document approval" shortcut; booking readiness follows delivery * order (import) or export release. */ phasedCustoms?: boolean; } const STATUS_META: Record< Freight.ContractDocReviewStatus, { label: string; color: string } > = { APPROVED: { label: "Approved", color: "edr-green" }, QUERIED: { label: "Queried", color: "red" }, PENDING: { label: "Pending", color: "gray" }, }; function formatReviewedAt(value?: string | null): string | null { if (!value) return null; const d = new Date(value); if (Number.isNaN(d.getTime())) return null; return d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } /** * Pre-booking clearance review for a CONTRACT. Approve / query each customer * document, upload GL output documents, and finalize once every required * document is approved. When `readOnly` it becomes an audit view: approved / * queried outcomes with reviewer + timestamp, no actions. */ export function ContractClearanceReviewSection({ contractId, onChanged, hideSummary, selfClear = false, readOnly = false, phasedCustoms = false, approvalsLocked = false, queriesLocked = false, }: ContractClearanceReviewSectionProps) { const [queryNotes, setQueryNotes] = useState>({}); const [openQuery, setOpenQuery] = useState>({}); const [uploadingKey, setUploadingKey] = useState(null); const { view, viewer } = useFileViewer(); const reviewerTeam = selfClear ? "Operations" : "Global Logistics"; const { data: clearance, isLoading } = useQuery({ queryKey: QUERY_KEYS.CONTRACTS.clearance(contractId), queryFn: () => contractsService.getClearance(contractId), }); const { reviewDocument, approveAll, uploadOutputDocuments, finalizeClearance } = useContractClearanceMutations(contractId, selfClear); const customerDocs = useMemo( () => (clearance?.documents ?? []).filter((d) => d.uploadedBy === "customer"), [clearance], ); // GL output documents — anything not uploaded by the customer. The backend // tags these `uploadedBy: 'gl'`; matching on "not customer" keeps it robust if // that ever splits into gl_et / gl_dj. const glDocs = useMemo( () => (clearance?.documents ?? []).filter((d) => d.uploadedBy !== "customer"), [clearance], ); const stats = useMemo(() => { const total = customerDocs.length; const approved = customerDocs.filter( (d) => d.reviewStatus === "APPROVED", ).length; const queried = customerDocs.filter( (d) => d.reviewStatus === "QUERIED", ).length; const pending = total - approved - queried; const pct = total === 0 ? 0 : Math.round((approved / total) * 100); return { total, approved, queried, pending, pct }; }, [customerDocs]); // Documents with a file uploaded but not yet approved — "Approve all" targets. const approvableKeys = customerDocs .filter((d) => d.file && d.reviewStatus !== "APPROVED") .map((d) => d.fileKey); const hasDocsAwaitingApproval = customerDocs.some( (d) => d.file && d.reviewStatus !== "APPROVED", ); const effectiveApprovalsLocked = approvalsLocked && !hasDocsAwaitingApproval; if (isLoading || !clearance) { return ( Loading clearance… ); } const handleReview = ( fileKey: string, status: "APPROVED" | "QUERIED", note?: string, ) => reviewDocument.mutate( { fileKey, status, note }, { onSuccess: () => { if (status === "QUERIED") setOpenQuery((o) => ({ ...o, [fileKey]: false })); onChanged?.(); }, }, ); return ( {stats.approved}/{stats.total} approved {!readOnly && !effectiveApprovalsLocked && approvableKeys.length > 0 && ( )} } > {!hideSummary && stats.total > 0 && ( )} {customerDocs.length === 0 ? ( No customer documents are required for this contract. ) : ( customerDocs.map((doc) => ( setOpenQuery((o) => ({ ...o, [doc.fileKey]: open })) } onNote={(v) => setQueryNotes((n) => ({ ...n, [doc.fileKey]: v })) } onApprove={() => handleReview(doc.fileKey, "APPROVED")} onQuery={() => handleReview(doc.fileKey, "QUERIED", queryNotes[doc.fileKey]) } onView={view} busy={reviewDocument.isPending} /> )) )} {glDocs.length > 0 && !phasedCustoms && ( {glDocs.map((doc) => { const isUploading = uploadingKey === doc.fileKey && uploadOutputDocuments.isPending; return ( {doc.label} {doc.required ? " *" : ""} {doc.file ? ( <> {isViewable({ name: doc.file.name, url: "", }) && ( void fetchViewableFile( doc.file!.id, doc.file!.name, ).then(view) } c="edr-green" style={{ display: "flex", background: "transparent", border: "none", cursor: "pointer", }} > )} void downloadBookingFile( doc.file!.id, doc.file!.name, ) } c="edr-green" style={{ display: "flex", background: "transparent", border: "none", cursor: "pointer", }} > ) : ( Not uploaded )} {!readOnly && ( { if (!f) return; setUploadingKey(doc.fileKey); uploadOutputDocuments.mutate( { [doc.fileKey]: f }, { onSuccess: () => { setUploadingKey(null); onChanged?.(); }, onError: () => setUploadingKey(null) }, ); }} accept="application/pdf,image/*" disabled={isUploading} > {(props) => ( )} )} ); })} )} {!readOnly && finalizeClearance.isError && ( }> {finalizeClearance.error instanceof Error ? finalizeClearance.error.message : "Could not finalize document approval."} )} {readOnly ? ( {phasedCustoms ? "Document review is complete. Continue customs milestones in the action panel." : `Clearance was finalized by the ${reviewerTeam} team. This is a read-only record of the approved documents.`} ) : phasedCustoms && effectiveApprovalsLocked ? ( Document review is complete. Use the action panel for declaration, duty, and transit steps {queriesLocked ? ". Pre-clearance is finalized — customer documents can no longer be queried." : " — or open a query above if a customer document needs correction."} ) : phasedCustoms ? ( {clearance.allApproved ? "All required documents are approved. Continue declaration, duty, and transit in the action panel." : "Approve every required document to unlock the customs milestone steps."} ) : ( {clearance.allApproved ? "All required documents are approved — you can finalize." : "Approve every required document to unlock finalization."} )} {viewer} ); } function StatPill({ color, label, value, }: { color: string; label: string; value: number; }) { return ( {value} {label} ); } function DocReviewCard({ doc, reviewerTeam, readOnly, approvalsLocked, queriesLocked, note, queryOpen, onToggleQuery, onNote, onApprove, onQuery, onView, busy, }: { doc: Freight.ContractClearanceDocument; reviewerTeam: string; readOnly: boolean; approvalsLocked: boolean; queriesLocked: boolean; note: string; queryOpen: boolean; onToggleQuery: (open: boolean) => void; onNote: (v: string) => void; onApprove: () => void; onQuery: () => void; onView: (file: { name: string; url: string }) => void; busy: boolean; }) { const status = doc.reviewStatus ?? "PENDING"; const meta = STATUS_META[status]; const hasFile = !!doc.file; const isApproved = status === "APPROVED"; const isQueried = status === "QUERIED"; const reviewedAt = formatReviewedAt(doc.reviewedAt); // Approved cards get a light green gradient + green border so the outcome is // instantly scannable; queried cards get a soft red; pending stay neutral. const cardStyle = isApproved ? { borderColor: "var(--mantine-color-edr-green-3)", background: "linear-gradient(135deg, var(--mantine-color-edr-green-0) 0%, #FFFFFF 72%)", } : isQueried ? { borderColor: "var(--mantine-color-red-2)", background: "linear-gradient(135deg, var(--mantine-color-red-0) 0%, #FFFFFF 78%)", } : { borderColor: "var(--mantine-color-edr-border-6)" }; return ( {isApproved ? : } {doc.label} {doc.required ? " *" : ""} {hasFile ? doc.file!.name : "Not uploaded by customer"} {isApproved && (reviewedAt || reviewerTeam) && ( Approved by {reviewerTeam} {reviewedAt ? ` · ${reviewedAt}` : ""} )} ) : isQueried ? ( ) : ( ) } > {meta.label} {hasFile && isViewable({ name: doc.file!.name, url: "", }) && ( )} {hasFile && ( )} {isQueried && doc.note && ( } p="xs" > {doc.note} )} {!readOnly && hasFile && ( {!queryOpen ? ( {!queriesLocked && ( )} {!isApproved && !approvalsLocked && ( )} ) : ( Describe the problem for the customer