diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx index be71b4d9f..1551e5621 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx @@ -1,64 +1,46 @@ -import { useMemo, useRef, useState } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { - Alert, - Anchor, - Badge, Box, Button, - Card, Center, - Divider, - Grid, Group, Loader, Modal, - SimpleGrid, Stack, - Table, Text, TextInput, - ThemeIcon, - Title, } from "@mantine/core"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { AlertCircle, AlertTriangle, - Anchor as AnchorIcon, - ArrowRight, - Building2, - Calendar, + ArrowDownLeft, + ArrowUpRight, Check, CheckCircle2, ClipboardCheck, + Clock, CreditCard, - DollarSign, + Download, FileSignature, FileText, - FileUp, History, - Info, - Layers, - MapPin, - Package, + MessageSquare, PackageCheck, - Ship, + Pencil, + Send, ShieldCheck, - StickyNote, Train, - Truck, Upload, - Weight, - XCircle, + X, + XCircle } from "lucide-react"; -import { format } from "date-fns"; +import { useMemo, useRef, useState, type ReactNode } from "react"; +import { useNavigate, useParams } from "react-router-dom"; -import Breadcrumbs from "@/components/Breadcrumbs"; +import { cn } from "@/lib/utils"; import { api } from "@/services/api"; import type { Freight } from "@edr/types"; -import { cn } from "@/lib/utils"; -import useAuth from "@/hooks/useAuth"; // ─── Constants ─────────────────────────────────────────────────────────────── @@ -70,26 +52,26 @@ const PROGRESS_STAGES = [ { label: "Complete", icon: PackageCheck, statuses: ["COMPLETED", "DELIVERED"] }, ]; -const STATUS_MAP: Record = { - DRAFT: { title: "Drafting Request", description: "Booking is being prepared and has not been submitted.", color: "text-slate-500", stage: 0 }, - CHANGES_REQUESTED: { title: "Changes Requested", description: "Staff has requested changes. Please review and resubmit.", color: "text-amber-600", stage: 0 }, - SUBMITTED: { title: "Submitted", description: "Your booking has been submitted for review.", color: "text-emerald-700", stage: 1 }, - PENDING_APPROVAL: { title: "Pending Approval", description: "Booking is in the approval process.", color: "text-emerald-700", stage: 1 }, - APPROVED_PENDING_SIGNATURE: { title: "Awaiting Signature", description: "Approved — pending contract signature.", color: "text-emerald-700", stage: 2 }, - APPROVED: { title: "Approved", description: "Booking has been fully approved.", color: "text-emerald-700", stage: 2 }, - CONTRACT_READY: { title: "Contract Ready", description: "Contract is available for review and signature.", color: "text-emerald-700", stage: 2 }, - SIGNED_CUSTOMER: { title: "Customer Signed", description: "Your signature has been submitted. Awaiting staff signature.", color: "text-emerald-700", stage: 2 }, - FULLY_EXECUTED: { title: "Fully Executed", description: "Contract has been fully signed and executed.", color: "text-emerald-700", stage: 2 }, - PNR_GENERATED: { title: "PNR Generated", description: "Payment reference number generated.", color: "text-emerald-700", stage: 3 }, - PAYMENT_VERIFICATION_IN_PROGRESS: { title: "Payment Verification", description: "Payment is being verified.", color: "text-emerald-700", stage: 3 }, - PAID: { title: "Paid", description: "Payment has been confirmed.", color: "text-emerald-700", stage: 3 }, - IN_TRANSIT: { title: "Cargo Moving", description: "Shipment is currently moving through the rail network.", color: "text-emerald-700", stage: 3 }, - PENDING_CONSOLIDATION: { title: "Pending Consolidation", description: "Awaiting consolidation partner.", color: "text-emerald-700", stage: 3 }, - CONSOLIDATED: { title: "Consolidated", description: "Cargo has been consolidated with partner shipment.", color: "text-emerald-700", stage: 3 }, - COMPLETED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-700", stage: 4 }, - DELIVERED: { title: "Service Complete", description: "Cargo delivered and service successfully terminated.", color: "text-emerald-700", stage: 4 }, - REJECTED: { title: "Rejected", description: "This booking request has been rejected.", color: "text-red-600", stage: -1 }, - CANCELLED: { title: "Cancelled", description: "This booking process has been terminated.", color: "text-red-600", stage: -1 }, +const STATUS_MAP: Record = { + DRAFT: { title: "Draft — not submitted", description: "This booking is being prepared and hasn’t been submitted for review yet.", stage: 0 }, + CHANGES_REQUESTED: { title: "Changes requested", description: "Staff has requested changes. Please review and resubmit.", stage: 0 }, + SUBMITTED: { title: "Submitted for review", description: "Your booking has been submitted and is awaiting review.", stage: 1 }, + PENDING_APPROVAL: { title: "Pending approval", description: "Your booking is moving through the approval process.", stage: 1 }, + APPROVED_PENDING_SIGNATURE: { title: "Approved — awaiting signature", description: "Approved. Your contract will be ready to sign shortly.", stage: 2 }, + APPROVED: { title: "Approved", description: "Your booking has been fully approved.", stage: 2 }, + CONTRACT_READY: { title: "Contract ready to sign", description: "Your contract is ready. Review and apply your signature to proceed.", stage: 2 }, + SIGNED_CUSTOMER: { title: "Signed — awaiting staff", description: "Your signature has been submitted. Awaiting the final staff signature.", stage: 2 }, + FULLY_EXECUTED: { title: "Contract fully executed", description: "Signed by all parties. You can now proceed to payment.", stage: 2 }, + PNR_GENERATED: { title: "Payment reference generated", description: "A payment reference number has been generated for this booking.", stage: 3 }, + PAYMENT_VERIFICATION_IN_PROGRESS: { title: "Verifying payment", description: "Your payment is being verified.", stage: 3 }, + PAID: { title: "Payment confirmed", description: "Payment has been confirmed for this booking.", stage: 3 }, + IN_TRANSIT: { title: "Cargo moving", description: "Your shipment is currently moving through the rail network.", stage: 3 }, + PENDING_CONSOLIDATION: { title: "Pending consolidation", description: "Awaiting a consolidation partner shipment.", stage: 3 }, + CONSOLIDATED: { title: "Consolidated", description: "Cargo has been consolidated with a partner shipment.", stage: 3 }, + COMPLETED: { title: "Service complete", description: "Cargo delivered and service successfully terminated.", stage: 4 }, + DELIVERED: { title: "Service complete", description: "Cargo delivered and service successfully terminated.", stage: 4 }, + REJECTED: { title: "Booking rejected", description: "This booking request has been rejected.", stage: -1 }, + CANCELLED: { title: "Booking cancelled", description: "This booking process has been terminated.", stage: -1 }, }; const REQUIRED_DOC_FIELDS = [ @@ -99,6 +81,33 @@ const REQUIRED_DOC_FIELDS = [ { key: "letter_of_credit", label: "Letter of Credit / LC" }, ]; +const isNegative = (s: string) => s === "CANCELLED" || s === "REJECTED"; +const isDraftLike = (s: string) => s === "DRAFT" || s === "CHANGES_REQUESTED"; + +function fmtDate(value?: string | null) { + if (!value) return "—"; + const d = new Date(value); + return Number.isNaN(d.getTime()) ? "—" : format(d, "MMM d, yyyy"); +} + +function yardLabel(y?: Freight.IBooking["originYard"]) { + return y?.label ?? y?.code ?? "—"; +} + +function containerSummary(b: Freight.IBooking) { + if (b.containers?.length) { + return b.containers.map((c) => `${c.qty} × ${c.type}`).join(", "); + } + return b.freightType === "BULK" ? "Bulk cargo" : "—"; +} + +function bookingSubtitle(b: Freight.IBooking) { + const cargo = b.freightSubtype || (b.freightType === "BULK" ? "Bulk freight" : "Container freight"); + const load = containerSummary(b); + const route = `${yardLabel(b.originYard)} → ${yardLabel(b.destinationYard)}`; + return [cargo, load, route].filter((p) => p && p !== "—").join(" · "); +} + // ─── Page Entry ─────────────────────────────────────────────────────────────── export default function BookingDetailPage() { @@ -124,43 +133,29 @@ export default function BookingDetailPage() { ); } - if (isError) { + if (isError || !booking) { return ( - - - - - - - Failed to load booking + + + + + + + {isError ? "Failed to load booking" : "Booking not found"} + + {isError && ( {error instanceof Error ? error.message : "An unexpected error occurred."} - - - + )} + + ); } - if (!booking) { - return ( - - - - - - - Booking not found - - - - ); - } - - if (booking.status === "DRAFT" || booking.status === "CHANGES_REQUESTED") { + if (isDraftLike(booking.status)) { return ; } - return ; } @@ -175,7 +170,6 @@ function DraftBookingView({ }) { const navigate = useNavigate(); const queryClient = useQueryClient(); - const { customer } = useAuth(); const fileInputRefs = useRef>({}); const documentsRef = useRef(null); @@ -185,12 +179,10 @@ function DraftBookingView({ const [docError, setDocError] = useState(""); const anyFileSelected = Object.values(selectedFiles).some(Boolean); - const uploadedCodes = useMemo( () => new Set(booking.files?.map((f) => f.code) ?? []), [booking.files], ); - const uploadedCount = REQUIRED_DOC_FIELDS.filter((d) => uploadedCodes.has(d.key)).length; const allDocsUploaded = uploadedCount === REQUIRED_DOC_FIELDS.length; @@ -198,7 +190,6 @@ function DraftBookingView({ ...api.bookings.generatePrice.queryOptions({ input: { id: booking.id } }), enabled: booking.status === "DRAFT" && !booking.pricingBreakdown, }); - const pricing = booking.pricingBreakdown ?? generatedPricing ?? null; const uploadMutation = useMutation({ @@ -240,10 +231,6 @@ function DraftBookingView({ uploadMutation.mutate(filesToUpload); } - function handleCancel() { - cancelMutation.mutate(cancelReason.trim() || "Cancelled by customer"); - } - function handleSubmitRequest() { const missing = REQUIRED_DOC_FIELDS.filter((doc) => !uploadedCodes.has(doc.key)); if (missing.length > 0) { @@ -254,306 +241,142 @@ function DraftBookingView({ submitMutation.mutate(); } - const companyName = (customer as any)?.company?.name ?? "—"; - const companyTin = (customer as any)?.company?.tin ?? "—"; - const contactName = - (customer as any)?.profile - ? `${(customer as any).profile.firstName ?? ""} ${(customer as any).profile.lastName ?? ""}`.trim() || "—" - : "—"; - const contactEmail = (customer as any)?.profile?.email ?? "—"; + const stepDone = allDocsUploaded; + const completeStep = stepDone ? 3 : uploadedCount > 0 ? 2 : 2; return ( - - - + + + } label="Documents" onClick={() => documentsRef.current?.scrollIntoView({ behavior: "smooth" })} /> + } + label="Continue editing" + onClick={() => navigate(`/bookings/${booking.id}/edit`)} + /> + + } + /> - {/* ── Hero ─────────────────────────────────────────────────────── */} - - - - - - - - - - Draft Booking Request + {booking.status === "CHANGES_REQUESTED" && booking.latestChangeRequestNote && ( + } title="Changes requested by staff"> + {booking.latestChangeRequestNote} + + )} + + + + + + {/* Complete your booking */} + + + Complete your booking + + Step {completeStep} of 3 - - {booking.reference} - - - - · - - Created {format(new Date(booking.createdAt), "MMM d, yyyy")} - - - - - + + + navigate(`/bookings/${booking.id}/edit`)}> + Edit + + } + /> + documentsRef.current?.scrollIntoView({ behavior: "smooth" })}> + Upload + + ) + } + /> + + - - - - + - {/* ── Alerts ───────────────────────────────────────────────────── */} - {booking.status === "CHANGES_REQUESTED" && booking.latestChangeRequestNote && ( - } - title="Changes Requested by Staff" - > - {booking.latestChangeRequestNote} - - )} - {uploadMutation.isError && ( - } title="Document upload failed"> - {uploadMutation.error instanceof Error ? uploadMutation.error.message : "An unexpected error occurred."} - - )} - {submitMutation.isError && ( - } title="Submission failed"> - {submitMutation.error instanceof Error ? submitMutation.error.message : "An unexpected error occurred."} - - )} - {cancelMutation.isError && ( - } title="Cancel failed"> - {cancelMutation.error instanceof Error ? cancelMutation.error.message : "An unexpected error occurred."} - - )} + - {/* ── Steps guide ──────────────────────────────────────────────── */} - - - Complete Your Request — 3 Steps - - - {/* Step 1 */} - - - - - - Step 1 - - Review Booking Details - Route, cargo, and service type are set. - - - - {/* Step 2 */} - 0 - ? "border-amber-200 bg-amber-50/30" - : "border-gray-200 bg-white" - }`} - > - - 0 ? "bg-amber-500" : "bg-gray-300" - }`} - > - {allDocsUploaded ? : 2} - - 0 ? "orange.7" : "dimmed"} - > - Step 2 - - - Upload Documents - - {allDocsUploaded - ? "All 4 documents uploaded." - : `${uploadedCount} of ${REQUIRED_DOC_FIELDS.length} documents uploaded.`} - - {!allDocsUploaded && ( - - )} - - - {/* Step 3 */} - - - - 3 - - Step 3 - - Submit Request - - Send your booking to EDR staff for review and approval. - - - - - - - {/* ── Main grid ────────────────────────────────────────────────── */} - - {/* Left: Documents */} - - - - - - - Required Documents - - - All 4 documents are required before you can submit. - - - {allDocsUploaded ? ( - }> - All uploaded - - ) : ( - - {uploadedCount}/{REQUIRED_DOC_FIELDS.length} uploaded - - )} + {/* Documents (uploadable) */} + + + + Documents + + {docError && ( - } mb="md"> + } className="mb-3"> {docError} - + )} - {/* Company info */} - - - - - Company Info (pre-filled from profile) - - - - - - - - - - Update in{" "} - navigate("/settings")}> - Settings - - - - - - - {/* Document slots */} - - {REQUIRED_DOC_FIELDS.map((doc) => { + + {REQUIRED_DOC_FIELDS.map((doc, i) => { const isUploaded = uploadedCodes.has(doc.key); - const selectedFile = selectedFiles[doc.key]; + const selected = selectedFiles[doc.key]; + const file = booking.files?.find((f) => f.code === doc.key); return ( - - - - - {isUploaded ? : } - - - {doc.label} - {isUploaded && ( - Uploaded ✓ - )} - {selectedFile && !isUploaded && ( - {selectedFile.name} - )} - {!isUploaded && !selectedFile && ( - Required · Not yet uploaded - )} - - - {!isUploaded && ( - + ? file?.name ?? "Uploaded" + : selected + ? selected.name + : "Required · not uploaded" + } + status={isUploaded ? "verified" : selected ? "ready" : "missing"} + action={ + isUploaded ? ( + } + /> + ) : ( + <> { fileInputRefs.current[doc.key] = el; }} type="file" @@ -561,178 +384,97 @@ function DraftBookingView({ className="hidden" onChange={(e) => handleFileSelect(doc.key, e.target.files?.[0] ?? null)} /> - - {selectedFile && ( - - )} - - )} - - + + {selected ? "Change" : "Choose"} + + {selected && ( + + )} + + + ) + } + /> ); })} - + {anyFileSelected && ( - - - {uploadMutation.isSuccess && ( - - - Documents uploaded successfully - - )} - + )} - - + + + } + right={ + <> + + + setCancelDialogOpen(true)} /> + + } + /> - {/* Right: Pricing + Booking summary */} - - - {/* Pricing */} - - - - Pricing Estimate - - - Estimated cost based on your current booking details. - - {pricing ? ( - - ) : ( - - - Pricing will be calculated automatically. - - - )} - - - {/* Booking summary */} - - - - Booking Summary - - - - - Route - - - - {booking.originYard?.label ?? booking.originYard?.code ?? "—"} - - - - {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"} - - - - - - - - - - - - - - - - {/* ── Cancel zone ──────────────────────────────────────────────── */} - - - - Danger Zone - - - Cancelling this booking is permanent and cannot be undone. + setCancelDialogOpen(false)} + title={Cancel booking} + radius="lg" + centered + > + + + Are you sure you want to cancel {booking.reference}? This action + cannot be undone. - - - - {/* Cancel modal */} - setCancelDialogOpen(false)} - title={Cancel Booking} - radius="lg" - centered - > - - - Are you sure you want to cancel {booking.reference}? This action - cannot be undone. - - setCancelReason(e.currentTarget.value)} + data-autofocus + /> + + + - - - - - - + onClick={() => cancelMutation.mutate(cancelReason.trim() || "Cancelled by customer")} + disabled={cancelMutation.isPending} + loading={cancelMutation.isPending} + leftSection={!cancelMutation.isPending ? : undefined} + > + Yes, cancel + + + + + ); } @@ -740,6 +482,7 @@ function DraftBookingView({ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { const navigate = useNavigate(); + const status = booking.status as string; const payMutation = useMutation({ mutationFn: () => api.bookings.pay.call({ id: booking.id }), @@ -748,436 +491,776 @@ function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) { }, }); - const normalizedStatus = booking.status as keyof typeof STATUS_MAP; - const statusConfig = STATUS_MAP[normalizedStatus] || STATUS_MAP.DRAFT; - const currentStageIndex = statusConfig.stage; const pricing = booking.pricingBreakdown; + const canPay = status === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID"; return ( - - - - - {/* ── Hero ─────────────────────────────────────────────────────── */} - - - - - - - - - - {booking.freightType === "CONTAINER" ? "Container" : "Bulk"} ·{" "} - {booking.tradeDirection ?? "Booking"} - - - {booking.reference} - - - - · - - - {format( - new Date(booking.scheduledDate ?? booking.createdAt), - "MMM d, yyyy", - )} - - - - - {normalizedStatus === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID" && ( - - )} - - - - {/* ── Contract card ────────────────────────────────────────────── */} - {renderContractCard(booking, navigate)} - - {/* ── Progress & status ────────────────────────────────────────── */} - - - - Booking Progress - - - {/* Stage stepper */} - - {/* Track */} - - = 0 - ? `${(currentStageIndex / (PROGRESS_STAGES.length - 1)) * 100}%` - : "0%" - } /> - - {PROGRESS_STAGES.map((stage, idx) => { - const isCompleted = idx < currentStageIndex; - const isActive = idx === currentStageIndex; - const StageIcon = stage.icon; - return ( - - - {isCompleted ? : } - - - {stage.label} - - - ); - })} - - - {/* Current status banner */} - - - - {normalizedStatus === "CANCELLED" || normalizedStatus === "REJECTED" ? ( - - ) : ( - - )} - - - - {statusConfig.title} - - - {statusConfig.description} - - - {normalizedStatus !== "CANCELLED" && - normalizedStatus !== "DELIVERED" && - normalizedStatus !== "COMPLETED" && ( - - Est. Waiting - 1–2 Working Days - - )} - - - + + } + /> - {/* ── Route + Cargo (2 col) ─────────────────────────────────────── */} - - - - - - Route & Service - + - {/* Origin → Destination */} - - - - - Origin - - - - - - {booking.originYard?.label ?? booking.originYard?.code ?? "—"} - - - - - - - - - Rail - - - - - Destination - - - - - - {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"} - - - - + - - } label="Service" value={booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail & Forwarding" : "Rail Only"} /> - } label="Return" value={booking.equipmentReturn === "WITH_RETURN" ? "With Return" : "Without Return"} /> - } label="Trade" value={booking.tradeDirection === "IMPORT" ? "Import" : "Export"} /> - - - + + - - - - - Cargo Specifications - - - - } label="Freight Type" value={booking.freightType === "BULK" ? "Bulk" : "Break Bulk"} /> - } label="Total Weight" value={`${booking.cargoTotalWeightVgm} t`} /> - } label="Currency" value={booking.paymentCurrency} /> - } label="Hazardous" value={booking.isHazardous ? "Yes" : "No"} /> - - - {booking.containers && booking.containers.length > 0 && ( - <> - - - Load Details - - - - - - Type - Qty - VGM - - - - {booking.containers.map((c, i) => ( - - {c.type} - {c.qty} - {c.vgm}t - - ))} - -
-
- - )} -
-
-
- - {/* ── Mile services + Contract info ─────────────────────────────── */} - - - - - - Mile Services - - - - - First Mile - - - {booking.firstMileEnabled && booking.firstMilePickupAddress - ? booking.firstMilePickupAddress - : "Not requested"} - - - - - Last Mile - - - {booking.lastMileEnabled && booking.lastMileDeliveryAddress - ? booking.lastMileDeliveryAddress - : "Not requested"} - - - - - - - - - - - Contract Info - - - - - - - - Hazardous: {booking.isHazardous ? "Yes" : "No"} - - - Refrigerated: {booking.isRefrigerated ? "Yes" : "No"} - - - - - - - - {/* ── Pricing + Documents ───────────────────────────────────────── */} - {(pricing || (booking.files && booking.files.length > 0)) && ( - - {pricing && ( - - - - - Pricing Breakdown - - - - - )} {booking.files && booking.files.length > 0 && ( - - - - - Uploaded Documents ({booking.files.length}) - - - {booking.files.map((file) => ( - - - - - - {file.name} - {file.code.replace(/_/g, " ")} - - - ))} - - - - )} - - )} - - {/* ── Additional info ───────────────────────────────────────────── */} - {(booking.freightSubtype || booking.financialTerms) && ( - - Additional Information - - {booking.freightSubtype && ( - - - Cargo Description + + + Documents + + {booking.files.length} files - "{booking.freightSubtype}" + + + {booking.files.map((file, i) => ( + } />} + /> + ))} - )} - {booking.financialTerms && ( - <> - {booking.freightSubtype && } - - - Financial Terms - - - - - {booking.financialTerms} - - - - - )} - - - )} -
+ + )} + + + + } + right={ + <> + + + + + } + /> + + ); +} + +// ─── Layout primitives ──────────────────────────────────────────────────────── + +function PageShell({ children }: { children: ReactNode }) { + return ( + + + {children} + ); } -// ─── Contract card ──────────────────────────────────────────────────────────── +function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode }) { + return ( +
+
{left}
+
{right}
+
+ ); +} -function renderContractCard( - booking: Freight.IBooking, - navigate: ReturnType, -) { - const s = booking.status; - if ( - s !== "APPROVED_PENDING_SIGNATURE" && - s !== "CONTRACT_READY" && - s !== "SIGNED_CUSTOMER" && - s !== "FULLY_EXECUTED" - ) { - return null; +const SectionCard = (() => { + type Props = { children: ReactNode; className?: string; ref?: React.Ref }; + const Comp = ({ children, className, ref }: Props) => ( +
+ {children} +
+ ); + return Comp; +})(); + +function CardTitle({ children }: { children: ReactNode }) { + return {children}; +} + +// ─── Page header ────────────────────────────────────────────────────────────── + +function PageHeader({ booking, actions }: { booking: Freight.IBooking; actions: ReactNode }) { + const status = booking.status as string; + const negative = isNegative(status); + const draft = isDraftLike(status); + + const dotColor = negative ? "#C0392B" : draft ? "#94A3B8" : "#0EA371"; + const pillBg = negative ? "#FBEAE7" : draft ? "#F1F4F7" : "#ECF6F1"; + const pillBorder = negative ? "#F3C8C1" : draft ? "#E1E7EE" : "#CDEBDD"; + const pillText = negative ? "#A93226" : draft ? "#475569" : "#0A6F4D"; + const isExport = booking.tradeDirection === "EXPORT"; + + return ( + + + + + {booking.reference} + + + + {status.replace(/_/g, " ").replace(/\b\w/g, (m) => m.toUpperCase())} + + + {isExport ? : } + {isExport ? "Export" : "Import"} + + + {bookingSubtitle(booking)} + + {actions} + + ); +} + +function HeaderButton({ + label, + icon, + onClick, + dark, + green, + disabled, +}: { + label: string; + icon: ReactNode; + onClick?: () => void; + dark?: boolean; + green?: boolean; + disabled?: boolean; +}) { + const base = "inline-flex items-center gap-2 rounded-[10px] px-4 py-[11px] text-[13px] font-bold transition-colors disabled:opacity-60"; + const variant = green + ? "bg-[#0EA371] text-white hover:bg-[#0A8A5F]" + : dark + ? "bg-[#0C1A2B] text-white hover:bg-[#16273A]" + : "border border-[#E6ECF2] bg-white text-[#10202F] hover:border-[#CBD5E1]"; + return ( + + ); +} + +// ─── Status hero + tracker ──────────────────────────────────────────────────── + +function StatusHero({ booking }: { booking: Freight.IBooking }) { + const status = booking.status as string; + const cfg = STATUS_MAP[status] ?? STATUS_MAP.DRAFT; + const negative = isNegative(status); + const draft = isDraftLike(status); + + const tone: "green" | "slate" | "red" = negative ? "red" : draft ? "slate" : "green"; + const tileBg = tone === "red" ? "#FBEAE7" : tone === "slate" ? "#F1F4F7" : "#ECF6F1"; + const tileFg = tone === "red" ? "#C0392B" : tone === "slate" ? "#475569" : "#0EA371"; + const HeroIcon = negative ? AlertTriangle : draft ? FileText : (PROGRESS_STAGES[cfg.stage]?.icon ?? History); + + const chipLabel = draft ? "Last edited" : negative ? "Updated" : "Scheduled"; + const chipValue = fmtDate(draft || negative ? booking.updatedAt : booking.scheduledDate); + + return ( + + + +
+ +
+ + {cfg.title} + {cfg.description} + +
+ + + + {chipLabel} + {chipValue} + + +
+ + + + +
+ ); +} + +function ProgressTracker({ + current, + tone = "green", + negative, +}: { + current: number; + tone?: "green" | "ink"; + negative?: boolean; +}) { + const last = PROGRESS_STAGES.length - 1; + const activeFill = tone === "ink" ? "#0C1A2B" : "#0EA371"; + const activeRing = tone === "ink" ? "#D9E0E7" : "#BFE8D4"; + const activeSub = tone === "ink" ? "#475569" : "#0A6F4D"; + + return ( +
+ {PROGRESS_STAGES.map((stage, idx) => { + const state = idx < current ? "done" : idx === current ? "active" : "idle"; + const Icon = stage.icon; + const reachedLeft = current >= idx && current >= 0; + const reachedRight = current > idx && current >= 0; + return ( +
+
+ +
+ {state === "done" ? ( + + ) : state === "active" ? ( + + ) : null} +
+ +
+ + {stage.label} + + + {state === "done" ? "Completed" : state === "active" ? (negative ? "Stopped" : "In progress") : "Pending"} + +
+ ); + })} +
+ ); +} + +// ─── Shipment details ───────────────────────────────────────────────────────── + +function ShipmentDetailsCard({ booking }: { booking: Freight.IBooking }) { + const rows: [string, string][][] = [ + [ + ["Origin yard", yardLabel(booking.originYard)], + ["Destination yard", yardLabel(booking.destinationYard)], + ], + [ + ["Freight type", booking.freightType === "BULK" ? "Bulk" : "Container"], + ["Commodity", booking.freightSubtype || "—"], + ], + [ + ["Containers / load", containerSummary(booking)], + ["Total weight (VGM)", booking.cargoTotalWeightVgm ? `${booking.cargoTotalWeightVgm} t` : "—"], + ], + [ + ["Service type", booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail + Forwarding" : "Rail only"], + ["Equipment return", booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return"], + ], + [ + ["Trade direction", booking.tradeDirection === "IMPORT" ? "Import" : "Export"], + ["Scheduled date", fmtDate(booking.scheduledDate)], + ], + [ + ["Consolidation", booking.allowConsolidation ? "Allowed" : "Not allowed"], + ["Assigned train", booking.trainId ?? "Not yet assigned"], + ], + ]; + + return ( + + + Shipment Details + + + {booking.contractType === "RENEWAL" ? "Renewal contract" : "New contract"} + + + + {rows.map((pair, i) => ( +
+ {pair.map(([k, v]) => ( +
+ {k} + {v} +
+ ))} +
+ ))} +
+
+ ); +} + +// ─── Documents ──────────────────────────────────────────────────────────────── + +function DocRow({ + title, + meta, + status, + action, + last, +}: { + title: string; + meta: string; + status: "verified" | "ready" | "missing"; + action: ReactNode; + last?: boolean; +}) { + const tileBg = status === "missing" ? "#F1F4F7" : status === "ready" ? "#EAF1FB" : "#F1F4F7"; + const tileFg = status === "ready" ? "#2E5B96" : "#475569"; + + return ( +
+
+ +
+
+ {title} + {meta} +
+ {status === "verified" && ( + + + Verified + + )} + {status === "ready" && ( + + Ready + + )} + {action} +
+ ); +} + +function IconSquare({ icon, href }: { icon: ReactNode; href?: string | null }) { + const cls = + "flex size-[34px] items-center justify-center rounded-[8px] border border-[#E6ECF2] text-[#6B7C8E] transition-colors hover:border-[#CBD5E1] hover:text-[#10202F]"; + if (href) { + return ( + + {icon} + + ); } + return {icon}; +} +function CountChip({ uploaded, total }: { uploaded: number; total: number }) { + const done = uploaded === total; + return ( + + {done && } + {uploaded}/{total} uploaded + + ); +} + +// ─── Steps (draft) ──────────────────────────────────────────────────────────── + +function StepLine({ + index, + title, + desc, + action, + done, + active, + highlight, +}: { + index: number; + title: string; + desc: string; + action?: ReactNode; + done?: boolean; + active?: boolean; + highlight?: boolean; +}) { + return ( +
+
+ {done ? : index} +
+
+ {title} + {desc} +
+ {action} +
+ ); +} + +function StepGhostButton({ children, onClick }: { children: ReactNode; onClick?: () => void }) { + return ( + + ); +} + +// ─── Pricing / estimate (right column) ──────────────────────────────────────── + +type Pricing = Freight.PricingBreakdown | null | undefined; + +function priceLineItems(pricing: Pricing) { + return (pricing?.lineItems ?? []).map((li) => ({ + label: li.description, + value: `${li.amount.toLocaleString()} ${li.currency}`, + })); +} + +function priceTotal(pricing: Pricing) { + if (!pricing) return "—"; + const total = pricing.lineItems.reduce((s, li) => s + li.amount, 0); + return `${total.toLocaleString()} ${pricing.currency}`; +} + +function EstimateCard({ pricing, title, chip }: { pricing: Pricing; title: string; chip: string }) { + const items = priceLineItems(pricing); + return ( + + + {title} + + {chip} + + + + {priceTotal(pricing)} + + A firm price is confirmed after EDR reviews your booking. + + + {items.length > 0 && ( + <> + + + {items.map((it) => ( + + {it.label} + {it.value} + + ))} + + + Estimated total + {priceTotal(pricing)} + + + )} + + ); +} + +function PaymentCard({ booking, pricing }: { booking: Freight.IBooking; pricing: Pricing }) { + const items = priceLineItems(pricing); + const paid = booking.paymentStatus === "PAID"; + const total = priceTotal(pricing); + + return ( + + + Payment + + {paid ? "Paid" : booking.paymentStatus?.replace(/_/g, " ") ?? "Pending"} + + + + {total} + {paid && ( + + Paid · {fmtDate(booking.updatedAt)} + + )} + + {items.length > 0 && ( + <> + + + {items.map((it) => ( + + {it.label} + {it.value} + + ))} + + + Total + {total} + + + )} + + + ); +} + +// ─── Schedule / consignment (right column) ──────────────────────────────────── + +function ScheduleCard({ + booking, + title, + consignment, +}: { + booking: Freight.IBooking; + title: string; + consignment?: boolean; +}) { + const rows: { label: string; value: ReactNode; muted?: boolean }[] = consignment + ? [ + { label: "Consignment ID", value: booking.reference }, + { label: "Service", value: booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail + Forwarding" : "Rail only" }, + { label: "Equipment return", value: booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return" }, + { label: "Assigned train", value: booking.trainId ?? "Not yet assigned", muted: !booking.trainId }, + { label: "Scheduled", value: fmtDate(booking.scheduledDate) }, + { label: "Consolidation", value: booking.allowConsolidation ? "Allowed" : "Not allowed" }, + ] + : [ + { label: "Service", value: booking.serviceType === "RAIL_AND_FORWARDING" ? "Rail + Forwarding" : "Rail only" }, + { label: "Equipment return", value: booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return" }, + { label: "Proposed date", value: fmtDate(booking.scheduledDate) }, + { label: "Assigned train", value: booking.trainId ?? "Not yet assigned", muted: !booking.trainId }, + { label: "Consolidation", value: booking.allowConsolidation ? "Allowed" : "Not allowed" }, + ]; + + return ( + + + {title} + + + {rows.map((r, i) => ( +
+ {r.label} + + {r.value} + +
+ ))} +
+
+ ); +} + +// ─── Activity timeline (readonly) ───────────────────────────────────────────── + +function ActivityCard({ booking }: { booking: Freight.IBooking }) { + const events = [ + booking.signedByCeoAt && { at: booking.signedByCeoAt, title: "Contract fully executed", note: "Signed by all parties" }, + booking.signedByDirectorAt && { at: booking.signedByDirectorAt, title: "Director signed contract", note: "Awaiting final signature" }, + booking.approvedByStaffAt && { at: booking.approvedByStaffAt, title: "Booking approved", note: "Cleared by EDR staff" }, + { at: booking.createdAt, title: "Booking created", note: "Request drafted by customer" }, + ].filter(Boolean) as { at: string; title: string; note: string }[]; + + if (events.length === 0) return null; + + return ( + + + Activity + Full history + + + {events.map((e, i) => { + const first = i === 0; + const lastItem = i === events.length - 1; + return ( +
+
+
+ {first && } +
+ {!lastItem &&
} +
+
+ + + {e.title} + + {first && ( + + Latest + + )} + + + {fmtDate(e.at)} · {e.note} + +
+
+ ); + })} + + + ); +} + +// ─── Support card (dark) ────────────────────────────────────────────────────── + +function SupportCard({ onCancel }: { onCancel?: () => void }) { + return ( +
+ +
+ +
+ + Need help? + EDR operations team + +
+ + Questions about this shipment, documents, or delivery? Our operations team can help. + + + + + +
+ ); +} + +// ─── Contract card (readonly, signature flow) ───────────────────────────────── + +function ContractCard({ + booking, + navigate, +}: { + booking: Freight.IBooking; + navigate: ReturnType; +}) { + const s = booking.status as string; const config: Record = { APPROVED_PENDING_SIGNATURE: { title: "Contract being prepared", @@ -1186,171 +1269,108 @@ function renderContractCard( CONTRACT_READY: { title: "Action required — sign your contract", description: "Your contract is ready. Review the agreement and apply your digital signature to proceed.", - buttonLabel: "View & Sign Contract", + buttonLabel: "View & sign contract", urgent: true, }, SIGNED_CUSTOMER: { title: "You have signed the contract", description: "Your signature has been submitted. Awaiting the final staff signature.", - buttonLabel: "View Contract", + buttonLabel: "View contract", }, FULLY_EXECUTED: { title: "Contract fully executed", description: "The contract has been signed by all parties. You can now proceed to payment.", - buttonLabel: "View Contract", + buttonLabel: "View contract", }, }; - const c = config[s]; - const isUrgent = c.urgent; + if (!c) return null; + const urgent = c.urgent; return ( - - - - - - - - - {c.title} - - - {c.description} - - - - {c.buttonLabel && ( - - )} - - - ); -} - -// ─── Shared sub-components ──────────────────────────────────────────────────── - -function PricingTable({ - pricing, -}: { - pricing: { - lineItems: { description: string; amount: number; currency: string }[]; - totalAmount: number; - currency: string; - }; -}) { - return ( - - - - - Description - Amount - - - - {pricing.lineItems.map((item, i) => ( - - {item.description} - - {item.amount.toLocaleString()} {item.currency} - - - ))} - - Total Estimated Cost - - {pricing.totalAmount.toLocaleString()} {pricing.currency} - - - -
-
- ); -} - -function MiniInfo({ - label, - value, - icon, -}: { - label: string; - value?: string | number | null; - icon?: React.ReactNode; -}) { - return ( - - {icon ? ( - - {icon} - - {label} + +
+ +
+ + + {c.title} -
- ) : ( - - {label} - + + {c.description} + +
+ + {c.buttonLabel && ( + )} - {value ?? "—"} - +
); } -function StatusBadge({ status }: { status: string }) { - const colorMap: Record = { - DRAFT: "gray", - CHANGES_REQUESTED: "yellow", - SUBMITTED: "edr-green", - PENDING_APPROVAL: "edr-green", - APPROVED_PENDING_SIGNATURE: "edr-green", - APPROVED: "edr-green", - CONTRACT_READY: "edr-green", - SIGNED_CUSTOMER: "edr-green", - FULLY_EXECUTED: "edr-green", - PNR_GENERATED: "edr-green", - PAYMENT_VERIFICATION_IN_PROGRESS: "edr-green", - PAID: "edr-green", - CONFIRMED: "edr-green", - IN_TRANSIT: "edr-green", - PENDING_CONSOLIDATION: "edr-green", - CONSOLIDATED: "edr-green", - COMPLETED: "gray", - DELIVERED: "gray", - REJECTED: "red", - CANCELLED: "red", - }; +// ─── Notices ────────────────────────────────────────────────────────────────── +function NoticeBanner({ + tone, + icon, + title, + children, + className, +}: { + tone: "amber" | "red"; + icon: ReactNode; + title?: string; + children: ReactNode; + className?: string; +}) { + const styles = + tone === "amber" + ? "border-[#F6E2BC] bg-[#FDF3E0] text-[#9A5B00]" + : "border-[#F3C8C1] bg-[#FBEAE7] text-[#A93226]"; return ( - - {status.replace(/_/g, " ")} - +
+ {icon} +
+ {title && {title}} + {children} +
+
+ ); +} + +function MutationErrors({ mutations }: { mutations: { isError: boolean; error: unknown }[] }) { + const errored = mutations.filter((m) => m.isError); + if (errored.length === 0) return null; + return ( + <> + {errored.map((m, i) => ( + } title="Something went wrong"> + {m.error instanceof Error ? m.error.message : "An unexpected error occurred."} + + ))} + ); }