diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx
deleted file mode 100644
index db760374f..000000000
--- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage.tsx
+++ /dev/null
@@ -1,1862 +0,0 @@
-import {
- Box,
- Button,
- Center,
- Group,
- Loader,
- Modal,
- Stack,
- Text,
- TextInput,
-} from "@mantine/core";
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { format } from "date-fns";
-import {
- AlertCircle,
- AlertTriangle,
- ArrowDownLeft,
- ArrowUpRight,
- Check,
- CheckCircle2,
- ClipboardCheck,
- Clock,
- CreditCard,
- Download,
- FileSignature,
- FileText,
- History,
- MessageSquare,
- PackageCheck,
- Pencil,
- Send,
- ShieldCheck,
- Train,
- Upload,
- X,
- XCircle,
-} from "lucide-react";
-import { useMemo, useRef, useState, type ReactNode } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-
-import { cn } from "@/lib/utils";
-import { api } from "@/services/api";
-import type { Freight } from "@edr/types";
-
-// ─── Constants ───────────────────────────────────────────────────────────────
-
-const PROGRESS_STAGES = [
- {
- label: "Request",
- icon: FileText,
- statuses: ["DRAFT", "CHANGES_REQUESTED"],
- },
- {
- label: "Submitted",
- icon: ClipboardCheck,
- statuses: ["SUBMITTED", "PENDING_APPROVAL"],
- },
- {
- label: "Approved",
- icon: ShieldCheck,
- statuses: [
- "APPROVED_PENDING_SIGNATURE",
- "APPROVED",
- "CONTRACT_READY",
- "SIGNED_CUSTOMER",
- "FULLY_EXECUTED",
- ],
- },
- {
- label: "In Transit",
- icon: Train,
- statuses: [
- "PNR_GENERATED",
- "PAYMENT_VERIFICATION_IN_PROGRESS",
- "PAID",
- "IN_TRANSIT",
- "PENDING_CONSOLIDATION",
- "CONSOLIDATED",
- ],
- },
- {
- label: "Complete",
- icon: PackageCheck,
- statuses: ["COMPLETED", "DELIVERED"],
- },
-];
-
-const STATUS_MAP: Record<
- string,
- { title: string; description: string; stage: number }
-> = {
- 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 = [
- { key: "commercial_invoice", label: "Commercial Invoice" },
- { key: "packing_list", label: "Packing List" },
- { key: "certificate_of_origin", label: "Certificate of Origin" },
- { 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() {
- const { id } = useParams<{ id: string }>();
- const queryClient = useQueryClient();
-
- const {
- data: booking,
- isLoading,
- isError,
- error,
- } = useQuery(
- api.bookings.get.queryOptions({ input: { id: id! }, enabled: !!id }),
- );
-
- const refetchBooking = () => {
- queryClient.invalidateQueries({
- queryKey: api.bookings.get.queryKey({ id: id! }),
- });
- };
-
- if (isLoading) {
- return (
-
-
-
-
- Loading booking details…
-
-
-
- );
- }
-
- if (isError || !booking) {
- return (
-
-
-
-
-
-
- {isError ? "Failed to load booking" : "Booking not found"}
-
- {isError && (
-
- {error instanceof Error
- ? error.message
- : "An unexpected error occurred."}
-
- )}
-
-
- );
- }
-
- if (isDraftLike(booking.status)) {
- return (
-
- );
- }
- return ;
-}
-
-// ─── Draft View ───────────────────────────────────────────────────────────────
-
-function DraftBookingView({
- booking,
- onBookingUpdated,
-}: {
- booking: Freight.IBooking;
- onBookingUpdated: () => void;
-}) {
- const navigate = useNavigate();
- const queryClient = useQueryClient();
- const fileInputRefs = useRef>({});
- const documentsRef = useRef(null);
-
- const [selectedFiles, setSelectedFiles] = useState<
- Record
- >({});
- const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
- const [cancelReason, setCancelReason] = useState("");
- 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;
-
- const { data: generatedPricing } = useQuery({
- ...api.bookings.generatePrice.queryOptions({ input: { id: booking.id } }),
- enabled: booking.status === "DRAFT" && !booking.pricingBreakdown,
- });
- const pricing = booking.pricingBreakdown ?? generatedPricing ?? null;
-
- const uploadMutation = useMutation({
- mutationFn: (files: Record) =>
- api.bookings.uploadDocuments.call({ id: booking.id, files }),
- onSuccess: () => {
- setSelectedFiles({});
- setDocError("");
- onBookingUpdated();
- },
- });
-
- const submitMutation = useMutation({
- mutationFn: () => api.bookings.submit.call({ id: booking.id }),
- onSuccess: () => {
- onBookingUpdated();
- queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
- },
- });
-
- const cancelMutation = useMutation({
- mutationFn: (reason: string) =>
- api.bookings.cancel.call({ id: booking.id, reason }),
- onSuccess: () => {
- setCancelDialogOpen(false);
- onBookingUpdated();
- },
- });
-
- function handleFileSelect(key: string, file: File | null) {
- setSelectedFiles((prev) => ({ ...prev, [key]: file }));
- }
-
- function handleUploadAll() {
- const filesToUpload: Record = {};
- for (const doc of REQUIRED_DOC_FIELDS) {
- if (selectedFiles[doc.key])
- filesToUpload[doc.key] = selectedFiles[doc.key]!;
- }
- if (Object.keys(filesToUpload).length === 0) return;
- uploadMutation.mutate(filesToUpload);
- }
-
- function handleSubmitRequest() {
- const missing = REQUIRED_DOC_FIELDS.filter(
- (doc) => !uploadedCodes.has(doc.key),
- );
- if (missing.length > 0) {
- setDocError("Please upload all required documents before submitting.");
- documentsRef.current?.scrollIntoView({ behavior: "smooth" });
- return;
- }
- submitMutation.mutate();
- }
-
- const stepDone = allDocsUploaded;
- const completeStep = stepDone ? 3 : uploadedCount > 0 ? 2 : 2;
-
- return (
-
- }
- label="Continue editing"
- onClick={() => navigate(`/bookings/${booking.id}/edit`)}
- />
- }
- />
-
- {booking.status === "CHANGES_REQUESTED" &&
- booking.latestChangeRequestNote && (
- }
- title="Changes requested by staff"
- >
- {booking.latestChangeRequestNote}
-
- )}
-
-
-
-
-
- {/* Complete your booking */}
-
-
- Complete your booking
-
- Step {completeStep} of 3
-
-
-
- navigate(`/bookings/${booking.id}/edit`)}
- >
- Edit
-
- }
- />
-
- documentsRef.current?.scrollIntoView({
- behavior: "smooth",
- })
- }
- >
- Upload
-
- )
- }
- />
-
-
- }
- onClick={handleSubmitRequest}
- disabled={submitMutation.isPending}
- loading={submitMutation.isPending}
- >
- {submitMutation.isPending ? "Submitting…" : "Submit for review"}
-
-
-
-
-
- {/* Documents (uploadable) */}
-
-
-
- Documents
-
-
-
-
- {docError && (
- }
- className="mb-3"
- >
- {docError}
-
- )}
-
-
- {REQUIRED_DOC_FIELDS.map((doc, i) => {
- const isUploaded = uploadedCodes.has(doc.key);
- const selected = selectedFiles[doc.key];
- const file = booking.files?.find((f) => f.code === doc.key);
- return (
- }
- />
- ) : (
- <>
- {
- fileInputRefs.current[doc.key] = el;
- }}
- type="file"
- accept=".pdf,.jpg,.jpeg,.png"
- className="hidden"
- onChange={(e) =>
- handleFileSelect(
- doc.key,
- e.target.files?.[0] ?? null,
- )
- }
- />
-
-
- fileInputRefs.current[doc.key]?.click()
- }
- className="flex h-[34px] items-center gap-1.5 rounded-[9px] border border-[#CBD5E1] bg-white px-3 text-[12.5px] font-bold text-[#475569] transition-colors hover:border-[#94A3B8]"
- >
-
- {selected ? "Change" : "Choose"}
-
- {selected && (
-
- handleFileSelect(doc.key, null)
- }
- className="flex size-[34px] items-center justify-center rounded-[8px] border border-[#E6ECF2] text-[#C0392B] transition-colors hover:bg-[#FBEAE7]"
- >
-
-
- )}
-
- >
- )
- }
- />
- );
- })}
-
-
- {anyFileSelected && (
-
- }
- onClick={handleUploadAll}
- disabled={uploadMutation.isPending}
- loading={uploadMutation.isPending}
- >
- {uploadMutation.isPending
- ? "Uploading…"
- : "Upload selected documents"}
-
- )}
-
- >
- }
- right={
- <>
-
-
- setCancelDialogOpen(true)} />
- >
- }
- />
-
- 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)}
- radius="md"
- data-autofocus
- />
-
- setCancelDialogOpen(false)}
- >
- Keep booking
-
-
- cancelMutation.mutate(
- cancelReason.trim() || "Cancelled by customer",
- )
- }
- disabled={cancelMutation.isPending}
- loading={cancelMutation.isPending}
- leftSection={
- !cancelMutation.isPending ? : undefined
- }
- >
- Yes, cancel
-
-
-
-
-
- );
-}
-
-// ─── Readonly View ────────────────────────────────────────────────────────────
-
-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 }),
- onSuccess: (data) => {
- if (data.redirectUrl) window.location.href = data.redirectUrl;
- },
- });
-
- const pricing = booking.pricingBreakdown;
- const canPay =
- status === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID";
-
- return (
-
-
- {canPay && (
- }
- label={payMutation.isPending ? "Processing…" : "Pay now"}
- onClick={() => payMutation.mutate()}
- disabled={payMutation.isPending}
- />
- )}
- >
- }
- />
-
-
-
-
-
-
-
-
- {booking.files && booking.files.length > 0 && (
-
-
- Documents
-
- {booking.files.length} files
-
-
-
- {booking.files.map((file, i) => (
- }
- />
- }
- />
- ))}
-
-
- )}
-
-
- >
- }
- right={
- <>
-
-
-
- >
- }
- />
-
- );
-}
-
-// ─── Layout primitives ────────────────────────────────────────────────────────
-
-function PageShell({ children }: { children: ReactNode }) {
- return (
-
-
- {children}
-
-
- );
-}
-
-function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode }) {
- return (
-
- );
-}
-
-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 (
-
- {icon}
- {label}
-
- );
-}
-
-// ─── 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 (
-
- {children}
-
- );
-}
-
-// ─── 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}
-
-
- >
- )}
-
-
- Download invoice
-
-
- );
-}
-
-// ─── 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.
-
-
-
-
- Contact
-
-
- {onCancel ? : }
- {onCancel ? "Cancel" : "Report"}
-
-
-
- );
-}
-
-// ─── Contract card (readonly, signature flow) ─────────────────────────────────
-
-function ContractCard({
- booking,
- navigate,
-}: {
- booking: Freight.IBooking;
- navigate: ReturnType;
-}) {
- const s = booking.status as string;
- const config: Record<
- string,
- {
- title: string;
- description: string;
- buttonLabel?: string;
- urgent?: boolean;
- }
- > = {
- APPROVED_PENDING_SIGNATURE: {
- title: "Contract being prepared",
- description:
- "Your booking has been approved. The contract will be available shortly.",
- },
- 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",
- urgent: true,
- },
- SIGNED_CUSTOMER: {
- title: "You have signed the contract",
- description:
- "Your signature has been submitted. Awaiting the final staff signature.",
- 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",
- },
- };
- const c = config[s];
- if (!c) return null;
- const urgent = c.urgent;
-
- return (
-
-
-
-
-
-
-
- {c.title}
-
-
- {c.description}
-
-
-
- {c.buttonLabel && (
-
navigate(`/bookings/${booking.id}/contract`)}
- className={cn(
- "inline-flex items-center gap-2 rounded-[10px] px-4 py-[11px] text-[13px] font-bold transition-colors",
- urgent
- ? "bg-[#0C1A2B] text-white hover:bg-[#16273A]"
- : "bg-[#0EA371] text-white hover:bg-[#0A8A5F]",
- )}
- >
-
- {c.buttonLabel}
-
- )}
-
- );
-}
-
-// ─── 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 (
-
-
{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."}
-
- ))}
- >
- );
-}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx
new file mode 100644
index 000000000..03919c4b9
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/DraftBookingView.tsx
@@ -0,0 +1,421 @@
+import {
+ ActionIcon,
+ Box,
+ Button,
+ Group,
+ Modal,
+ Stack,
+ Text,
+ TextInput,
+} from "@mantine/core";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import {
+ AlertCircle,
+ Download,
+ Pencil,
+ Send,
+ Upload,
+ X,
+ XCircle,
+} from "lucide-react";
+import { useMemo, useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
+
+import { api } from "@/services/api";
+import type { Freight } from "@edr/types";
+
+import { REQUIRED_DOC_FIELDS } from "./constants";
+import { CardTitle, PageShell, SectionCard } from "./components/layout";
+import { CountChip, DocRow, IconSquare } from "./components/Documents";
+import { EstimateCard } from "./components/pricing";
+import { HeaderButton, PageHeader } from "./components/PageHeader";
+import { MutationErrors, NoticeBanner } from "./components/Notices";
+import { ScheduleCard } from "./components/ScheduleCard";
+import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
+import { StatusHero } from "./components/StatusHero";
+import { StepGhostButton, StepLine } from "./components/Steps";
+import { SupportCard } from "./components/SupportCard";
+import { BodyGrid } from "./components/layout";
+
+export function DraftBookingView({
+ booking,
+ onBookingUpdated,
+}: {
+ booking: Freight.IBooking;
+ onBookingUpdated: () => void;
+}) {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const fileInputRefs = useRef>({});
+ const documentsRef = useRef(null);
+
+ const [selectedFiles, setSelectedFiles] = useState<
+ Record
+ >({});
+ const [cancelDialogOpen, setCancelDialogOpen] = useState(false);
+ const [cancelReason, setCancelReason] = useState("");
+ 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;
+
+ const { data: generatedPricing } = useQuery({
+ ...api.bookings.generatePrice.queryOptions({ input: { id: booking.id } }),
+ enabled: booking.status === "DRAFT" && !booking.pricingBreakdown,
+ });
+ const pricing = booking.pricingBreakdown ?? generatedPricing ?? null;
+
+ const uploadMutation = useMutation({
+ mutationFn: (files: Record) =>
+ api.bookings.uploadDocuments.call({ id: booking.id, files }),
+ onSuccess: () => {
+ setSelectedFiles({});
+ setDocError("");
+ onBookingUpdated();
+ },
+ });
+
+ const submitMutation = useMutation({
+ mutationFn: () => api.bookings.submit.call({ id: booking.id }),
+ onSuccess: () => {
+ onBookingUpdated();
+ queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() });
+ },
+ });
+
+ const cancelMutation = useMutation({
+ mutationFn: (reason: string) =>
+ api.bookings.cancel.call({ id: booking.id, reason }),
+ onSuccess: () => {
+ setCancelDialogOpen(false);
+ onBookingUpdated();
+ },
+ });
+
+ function handleFileSelect(key: string, file: File | null) {
+ setSelectedFiles((prev) => ({ ...prev, [key]: file }));
+ }
+
+ function handleUploadAll() {
+ const filesToUpload: Record = {};
+ for (const doc of REQUIRED_DOC_FIELDS) {
+ if (selectedFiles[doc.key])
+ filesToUpload[doc.key] = selectedFiles[doc.key]!;
+ }
+ if (Object.keys(filesToUpload).length === 0) return;
+ uploadMutation.mutate(filesToUpload);
+ }
+
+ function handleSubmitRequest() {
+ const missing = REQUIRED_DOC_FIELDS.filter(
+ (doc) => !uploadedCodes.has(doc.key),
+ );
+ if (missing.length > 0) {
+ setDocError("Please upload all required documents before submitting.");
+ documentsRef.current?.scrollIntoView({ behavior: "smooth" });
+ return;
+ }
+ submitMutation.mutate();
+ }
+
+ const completeStep = allDocsUploaded ? 3 : 2;
+
+ return (
+
+ }
+ label="Continue editing"
+ onClick={() => navigate(`/bookings/${booking.id}/edit`)}
+ />
+ }
+ />
+
+ {booking.status === "CHANGES_REQUESTED" &&
+ booking.latestChangeRequestNote && (
+ }
+ title="Changes requested by staff"
+ >
+ {booking.latestChangeRequestNote}
+
+ )}
+
+
+
+
+
+ {/* Complete your booking */}
+
+
+ Complete your booking
+
+ Step {completeStep} of 3
+
+
+
+ navigate(`/bookings/${booking.id}/edit`)}
+ >
+ Edit
+
+ }
+ />
+
+ documentsRef.current?.scrollIntoView({
+ behavior: "smooth",
+ })
+ }
+ >
+ Upload
+
+ )
+ }
+ />
+
+
+ }
+ onClick={handleSubmitRequest}
+ disabled={submitMutation.isPending}
+ loading={submitMutation.isPending}
+ styles={{ root: { height: 46 }, label: { fontWeight: 700 } }}
+ >
+ {submitMutation.isPending ? "Submitting…" : "Submit for review"}
+
+
+
+
+
+ {/* Documents (uploadable) */}
+
+
+
+ Documents
+
+
+
+
+ {docError && (
+ }
+ style={{ marginBottom: 12 }}
+ >
+ {docError}
+
+ )}
+
+
+ {REQUIRED_DOC_FIELDS.map((doc, i) => {
+ const isUploaded = uploadedCodes.has(doc.key);
+ const selected = selectedFiles[doc.key];
+ const file = booking.files?.find((f) => f.code === doc.key);
+ return (
+ }
+ />
+ ) : (
+ <>
+ {
+ fileInputRefs.current[doc.key] = el;
+ }}
+ type="file"
+ accept=".pdf,.jpg,.jpeg,.png"
+ style={{ display: "none" }}
+ onChange={(e) =>
+ handleFileSelect(
+ doc.key,
+ e.target.files?.[0] ?? null,
+ )
+ }
+ />
+
+ }
+ onClick={() =>
+ fileInputRefs.current[doc.key]?.click()
+ }
+ styles={{
+ root: { height: 34, paddingInline: 12 },
+ label: {
+ fontSize: 12.5,
+ fontWeight: 700,
+ color: "#475569",
+ },
+ }}
+ >
+ {selected ? "Change" : "Choose"}
+
+ {selected && (
+ handleFileSelect(doc.key, null)}
+ style={{ color: "#C0392B" }}
+ >
+
+
+ )}
+
+ >
+ )
+ }
+ />
+ );
+ })}
+
+
+ {anyFileSelected && (
+
+ }
+ onClick={handleUploadAll}
+ disabled={uploadMutation.isPending}
+ loading={uploadMutation.isPending}
+ styles={{ root: { height: 44 }, label: { fontWeight: 700 } }}
+ >
+ {uploadMutation.isPending
+ ? "Uploading…"
+ : "Upload selected documents"}
+
+ )}
+
+ >
+ }
+ right={
+ <>
+
+
+ setCancelDialogOpen(true)} />
+ >
+ }
+ />
+
+ 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)}
+ radius="md"
+ data-autofocus
+ />
+
+ setCancelDialogOpen(false)}
+ >
+ Keep booking
+
+
+ cancelMutation.mutate(
+ cancelReason.trim() || "Cancelled by customer",
+ )
+ }
+ disabled={cancelMutation.isPending}
+ loading={cancelMutation.isPending}
+ leftSection={
+ !cancelMutation.isPending ? : undefined
+ }
+ >
+ Yes, cancel
+
+
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx
new file mode 100644
index 000000000..917894626
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx
@@ -0,0 +1,106 @@
+import { Box, Group, Text } from "@mantine/core";
+import { useMutation } from "@tanstack/react-query";
+import { CreditCard, Download } from "lucide-react";
+import { useNavigate } from "react-router-dom";
+
+import { api } from "@/services/api";
+import type { Freight } from "@edr/types";
+
+import { ActivityCard } from "./components/ActivityCard";
+import { ContractCard } from "./components/ContractCard";
+import { DocRow, IconSquare } from "./components/Documents";
+import { BodyGrid, CardTitle, PageShell, SectionCard } from "./components/layout";
+import { HeaderButton, PageHeader } from "./components/PageHeader";
+import { PaymentCard } from "./components/pricing";
+import { ScheduleCard } from "./components/ScheduleCard";
+import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
+import { StatusHero } from "./components/StatusHero";
+import { SupportCard } from "./components/SupportCard";
+
+export 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 }),
+ onSuccess: (data) => {
+ if (data.redirectUrl) window.location.href = data.redirectUrl;
+ },
+ });
+
+ const pricing = booking.pricingBreakdown;
+ const canPay =
+ status === "FULLY_EXECUTED" && booking.paymentStatus !== "PAID";
+
+ return (
+
+ }
+ label={payMutation.isPending ? "Processing…" : "Pay now"}
+ onClick={() => payMutation.mutate()}
+ disabled={payMutation.isPending}
+ />
+ )
+ }
+ />
+
+
+
+
+
+
+
+
+ {booking.files && booking.files.length > 0 && (
+
+
+ Documents
+
+ {booking.files.length} files
+
+
+
+ {booking.files.map((file, i) => (
+ }
+ />
+ }
+ />
+ ))}
+
+
+ )}
+
+
+ >
+ }
+ right={
+ <>
+
+
+
+ >
+ }
+ />
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ActivityCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ActivityCard.tsx
new file mode 100644
index 000000000..851f5f92c
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ActivityCard.tsx
@@ -0,0 +1,114 @@
+import { Box, Group, Text } from "@mantine/core";
+import { Train } from "lucide-react";
+
+import type { Freight } from "@edr/types";
+
+import { fmtDate } from "../utils";
+import { CardTitle, SectionCard } from "./layout";
+
+export 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}
+
+
+
+ );
+ })}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContractCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContractCard.tsx
new file mode 100644
index 000000000..e9836703c
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ContractCard.tsx
@@ -0,0 +1,103 @@
+import { Box, Button, Group, Paper, Text } from "@mantine/core";
+import { FileSignature } from "lucide-react";
+import type { useNavigate } from "react-router-dom";
+
+import type { Freight } from "@edr/types";
+
+const CONTRACT_CONFIG: Record<
+ string,
+ {
+ title: string;
+ description: string;
+ buttonLabel?: string;
+ urgent?: boolean;
+ }
+> = {
+ APPROVED_PENDING_SIGNATURE: {
+ title: "Contract being prepared",
+ description:
+ "Your booking has been approved. The contract will be available shortly.",
+ },
+ 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",
+ urgent: true,
+ },
+ SIGNED_CUSTOMER: {
+ title: "You have signed the contract",
+ description:
+ "Your signature has been submitted. Awaiting the final staff signature.",
+ 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",
+ },
+};
+
+export function ContractCard({
+ booking,
+ navigate,
+}: {
+ booking: Freight.IBooking;
+ navigate: ReturnType;
+}) {
+ const c = CONTRACT_CONFIG[booking.status as string];
+ if (!c) return null;
+ const urgent = c.urgent;
+
+ return (
+
+
+
+
+
+
+
+
+ {c.title}
+
+
+ {c.description}
+
+
+
+ {c.buttonLabel && (
+ navigate(`/bookings/${booking.id}/contract`)}
+ radius={10}
+ color={urgent ? "#0C1A2B" : "edr-green"}
+ leftSection={ }
+ styles={{
+ root: { height: 42, paddingInline: 16 },
+ label: { fontSize: 13, fontWeight: 700 },
+ }}
+ >
+ {c.buttonLabel}
+
+ )}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Documents.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Documents.tsx
new file mode 100644
index 000000000..616c868ef
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Documents.tsx
@@ -0,0 +1,161 @@
+import { Box, Group, Text } from "@mantine/core";
+import { CheckCircle2, FileText } from "lucide-react";
+import type { ReactNode } from "react";
+
+export function DocRow({
+ title,
+ meta,
+ status,
+ action,
+ last,
+}: {
+ title: string;
+ meta: string;
+ status: "verified" | "ready" | "missing";
+ action: ReactNode;
+ last?: boolean;
+}) {
+ const tileBg = status === "ready" ? "#EAF1FB" : "#F1F4F7";
+ const tileFg = status === "ready" ? "#2E5B96" : "#475569";
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {meta}
+
+
+ {status === "verified" && (
+
+
+ Verified
+
+ )}
+ {status === "ready" && (
+
+ Ready
+
+ )}
+ {action}
+
+ );
+}
+
+export function IconSquare({
+ icon,
+ href,
+}: {
+ icon: ReactNode;
+ href?: string | null;
+}) {
+ const style: React.CSSProperties = {
+ flexShrink: 0,
+ width: 34,
+ height: 34,
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ borderRadius: 8,
+ border: "1px solid #E6ECF2",
+ color: "#6B7C8E",
+ };
+ if (href) {
+ return (
+
+ {icon}
+
+ );
+ }
+ return (
+
+ {icon}
+
+ );
+}
+
+export function CountChip({
+ uploaded,
+ total,
+}: {
+ uploaded: number;
+ total: number;
+}) {
+ const done = uploaded === total;
+ return (
+
+ {done && }
+ {uploaded}/{total} uploaded
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Notices.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Notices.tsx
new file mode 100644
index 000000000..77565aed8
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Notices.tsx
@@ -0,0 +1,77 @@
+import { Box, Group, Text } from "@mantine/core";
+import { AlertCircle } from "lucide-react";
+import type { ReactNode } from "react";
+
+export function NoticeBanner({
+ tone,
+ icon,
+ title,
+ children,
+ style,
+}: {
+ tone: "amber" | "red";
+ icon: ReactNode;
+ title?: string;
+ children: ReactNode;
+ style?: React.CSSProperties;
+}) {
+ const palette =
+ tone === "amber"
+ ? { border: "#F6E2BC", bg: "#FDF3E0", color: "#9A5B00" }
+ : { border: "#F3C8C1", bg: "#FBEAE7", color: "#A93226" };
+
+ return (
+
+
+ {icon}
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {children}
+
+
+
+ );
+}
+
+export 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."}
+
+ ))}
+ >
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx
new file mode 100644
index 000000000..f326f86f0
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/PageHeader.tsx
@@ -0,0 +1,125 @@
+import { Box, Button, Group, Stack, Text } from "@mantine/core";
+import { ArrowDownLeft, ArrowUpRight } from "lucide-react";
+import type { ReactNode } from "react";
+
+import type { Freight } from "@edr/types";
+
+import { bookingSubtitle, isDraftLike, isNegative } from "../utils";
+
+export 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}
+
+
+ );
+}
+
+export function HeaderButton({
+ label,
+ icon,
+ onClick,
+ dark,
+ green,
+ disabled,
+}: {
+ label: string;
+ icon: ReactNode;
+ onClick?: () => void;
+ dark?: boolean;
+ green?: boolean;
+ disabled?: boolean;
+}) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx
new file mode 100644
index 000000000..18eae1bf5
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx
@@ -0,0 +1,84 @@
+import { Box, Group, Text } from "@mantine/core";
+import type { ReactNode } from "react";
+
+import type { Freight } from "@edr/types";
+
+import { fmtDate } from "../utils";
+import { CardTitle, SectionCard } from "./layout";
+
+type Row = { label: string; value: ReactNode; muted?: boolean };
+
+export function ScheduleCard({
+ booking,
+ title,
+ consignment,
+}: {
+ booking: Freight.IBooking;
+ title: string;
+ consignment?: boolean;
+}) {
+ const service =
+ booking.serviceType === "RAIL_AND_FORWARDING"
+ ? "Rail + Forwarding"
+ : "Rail only";
+ const equipmentReturn =
+ booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return";
+ const consolidation = booking.allowConsolidation ? "Allowed" : "Not allowed";
+ const assignedTrain: Row = {
+ label: "Assigned train",
+ value: booking.trainId ?? "Not yet assigned",
+ muted: !booking.trainId,
+ };
+
+ const rows: Row[] = consignment
+ ? [
+ { label: "Consignment ID", value: booking.reference },
+ { label: "Service", value: service },
+ { label: "Equipment return", value: equipmentReturn },
+ assignedTrain,
+ { label: "Scheduled", value: fmtDate(booking.scheduledDate) },
+ { label: "Consolidation", value: consolidation },
+ ]
+ : [
+ { label: "Service", value: service },
+ { label: "Equipment return", value: equipmentReturn },
+ { label: "Proposed date", value: fmtDate(booking.scheduledDate) },
+ assignedTrain,
+ { label: "Consolidation", value: consolidation },
+ ];
+
+ return (
+
+
+ {title}
+
+
+ {rows.map((r, i) => (
+
+
+ {r.label}
+
+
+ {r.value}
+
+
+ ))}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx
new file mode 100644
index 000000000..259a5c113
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx
@@ -0,0 +1,106 @@
+import { Box, Group, Text } from "@mantine/core";
+import { FileText } from "lucide-react";
+
+import type { Freight } from "@edr/types";
+
+import { containerSummary, fmtDate, yardLabel } from "../utils";
+import { CardTitle, SectionCard } from "./layout";
+
+export 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}
+
+
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx
new file mode 100644
index 000000000..b85bd81b1
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/StatusHero.tsx
@@ -0,0 +1,213 @@
+import { Box, Group, Text } from "@mantine/core";
+import { AlertTriangle, Check, Clock, FileText, History } from "lucide-react";
+
+import type { Freight } from "@edr/types";
+
+import { PROGRESS_STAGES, STATUS_MAP } from "../constants";
+import { fmtDate, isDraftLike, isNegative } from "../utils";
+import { SectionCard } from "./layout";
+
+export 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;
+
+ const circleStyle: React.CSSProperties =
+ state === "idle"
+ ? { backgroundColor: "#EEF2F6", border: "1px solid #E1E7EE" }
+ : {
+ backgroundColor: state === "active" ? activeFill : "#0EA371",
+ boxShadow:
+ state === "active" ? `0 0 0 4px ${activeRing}` : undefined,
+ };
+
+ return (
+
+
+
+
+ {state === "done" ? (
+
+ ) : state === "active" ? (
+
+ ) : null}
+
+
+
+
+ {stage.label}
+
+
+ {state === "done"
+ ? "Completed"
+ : state === "active"
+ ? negative
+ ? "Stopped"
+ : "In progress"
+ : "Pending"}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Steps.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Steps.tsx
new file mode 100644
index 000000000..cecc9639e
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/Steps.tsx
@@ -0,0 +1,93 @@
+import { Box, Button, Group, Text } from "@mantine/core";
+import { Check } from "lucide-react";
+import type { ReactNode } from "react";
+
+export function StepLine({
+ index,
+ title,
+ desc,
+ action,
+ done,
+ active,
+ highlight,
+}: {
+ index: number;
+ title: string;
+ desc: string;
+ action?: ReactNode;
+ done?: boolean;
+ active?: boolean;
+ highlight?: boolean;
+}) {
+ const badgeStyle: React.CSSProperties = done
+ ? { backgroundColor: "#0EA371", color: "#fff" }
+ : active
+ ? { backgroundColor: "#0C1A2B", color: "#fff" }
+ : {
+ backgroundColor: "#EEF2F6",
+ color: "#9AA8B5",
+ border: "1px solid #E1E7EE",
+ };
+
+ return (
+
+
+ {done ? : index}
+
+
+
+ {title}
+
+
+ {desc}
+
+
+ {action}
+
+ );
+}
+
+export function StepGhostButton({
+ children,
+ onClick,
+}: {
+ children: ReactNode;
+ onClick?: () => void;
+}) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/SupportCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/SupportCard.tsx
new file mode 100644
index 000000000..02fafab17
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/SupportCard.tsx
@@ -0,0 +1,61 @@
+import { Box, Button, Group, Paper, Text } from "@mantine/core";
+import { FileText, MessageSquare, XCircle } from "lucide-react";
+
+export function SupportCard({ onCancel }: { onCancel?: () => void }) {
+ return (
+
+
+
+
+
+
+
+ Need help?
+
+
+ EDR operations team
+
+
+
+
+ Questions about this shipment, documents, or delivery? Our operations
+ team can help.
+
+
+ }
+ styles={{ root: { height: 44 }, label: { fontWeight: 700 } }}
+ >
+ Contact
+
+ :
+ }
+ styles={{
+ root: { height: 44, paddingInline: 16 },
+ label: { fontWeight: 700, color: "#fff" },
+ }}
+ >
+ {onCancel ? "Cancel" : "Report"}
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/layout.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/layout.tsx
new file mode 100644
index 000000000..1e45e2d45
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/layout.tsx
@@ -0,0 +1,55 @@
+import { Box, Flex, Paper, Stack, Text, type PaperProps } from "@mantine/core";
+import type { ReactNode, Ref } from "react";
+
+export function PageShell({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export function BodyGrid({ left, right }: { left: ReactNode; right: ReactNode }) {
+ return (
+
+
+ {left}
+
+
+ {right}
+
+
+ );
+}
+
+interface SectionCardProps extends PaperProps {
+ children: ReactNode;
+ ref?: Ref;
+}
+
+export function SectionCard({ children, ref, ...props }: SectionCardProps) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function CardTitle({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx
new file mode 100644
index 000000000..4017e5b54
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/pricing.tsx
@@ -0,0 +1,162 @@
+import { Box, Button, Group, Stack, Text } from "@mantine/core";
+import { FileText } from "lucide-react";
+
+import type { Freight } from "@edr/types";
+
+import { fmtDate, priceLineItems, priceTotal, type Pricing } from "../utils";
+import { CardTitle, SectionCard } from "./layout";
+
+function LineItems({ pricing }: { pricing: Pricing }) {
+ const items = priceLineItems(pricing);
+ if (items.length === 0) return null;
+ return (
+
+ {items.map((it) => (
+
+
+ {it.label}
+
+
+ {it.value}
+
+
+ ))}
+
+ );
+}
+
+const Divider = () => ;
+
+export function EstimateCard({
+ pricing,
+ title,
+ chip,
+}: {
+ pricing: Pricing;
+ title: string;
+ chip: string;
+}) {
+ const hasItems = priceLineItems(pricing).length > 0;
+ return (
+
+
+ {title}
+
+ {chip}
+
+
+
+
+ {priceTotal(pricing)}
+
+
+ A firm price is confirmed after EDR reviews your booking.
+
+
+ {hasItems && (
+ <>
+
+
+
+
+ Estimated total
+
+
+ {priceTotal(pricing)}
+
+
+ >
+ )}
+
+ );
+}
+
+export function PaymentCard({
+ booking,
+ pricing,
+}: {
+ booking: Freight.IBooking;
+ pricing: Pricing;
+}) {
+ const hasItems = priceLineItems(pricing).length > 0;
+ const paid = booking.paymentStatus === "PAID";
+ const total = priceTotal(pricing);
+
+ return (
+
+
+ Payment
+
+ {paid
+ ? "Paid"
+ : (booking.paymentStatus?.replace(/_/g, " ") ?? "Pending")}
+
+
+
+
+ {total}
+
+ {paid && (
+
+ Paid · {fmtDate(booking.updatedAt)}
+
+ )}
+
+ {hasItems && (
+ <>
+
+
+
+
+ Total
+
+
+ {total}
+
+
+ >
+ )}
+ }
+ styles={{ root: { height: 46 }, label: { fontSize: 13.5, fontWeight: 700, color: "#10202F" } }}
+ >
+ Download invoice
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts
new file mode 100644
index 000000000..80f0b7ab2
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/constants.ts
@@ -0,0 +1,160 @@
+import {
+ ClipboardCheck,
+ FileText,
+ PackageCheck,
+ ShieldCheck,
+ Train,
+} from "lucide-react";
+
+export const PROGRESS_STAGES = [
+ {
+ label: "Request",
+ icon: FileText,
+ statuses: ["DRAFT", "CHANGES_REQUESTED"],
+ },
+ {
+ label: "Submitted",
+ icon: ClipboardCheck,
+ statuses: ["SUBMITTED", "PENDING_APPROVAL"],
+ },
+ {
+ label: "Approved",
+ icon: ShieldCheck,
+ statuses: [
+ "APPROVED_PENDING_SIGNATURE",
+ "APPROVED",
+ "CONTRACT_READY",
+ "SIGNED_CUSTOMER",
+ "FULLY_EXECUTED",
+ ],
+ },
+ {
+ label: "In Transit",
+ icon: Train,
+ statuses: [
+ "PNR_GENERATED",
+ "PAYMENT_VERIFICATION_IN_PROGRESS",
+ "PAID",
+ "IN_TRANSIT",
+ "PENDING_CONSOLIDATION",
+ "CONSOLIDATED",
+ ],
+ },
+ {
+ label: "Complete",
+ icon: PackageCheck,
+ statuses: ["COMPLETED", "DELIVERED"],
+ },
+];
+
+export const STATUS_MAP: Record<
+ string,
+ { title: string; description: string; stage: number }
+> = {
+ 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,
+ },
+};
+
+export const REQUIRED_DOC_FIELDS = [
+ { key: "commercial_invoice", label: "Commercial Invoice" },
+ { key: "packing_list", label: "Packing List" },
+ { key: "certificate_of_origin", label: "Certificate of Origin" },
+ { key: "letter_of_credit", label: "Letter of Credit / LC" },
+];
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/index.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/index.tsx
new file mode 100644
index 000000000..02e0318fd
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/index.tsx
@@ -0,0 +1,86 @@
+import { Box, Center, Loader, Stack, Text } from "@mantine/core";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
+import { AlertTriangle } from "lucide-react";
+import { useParams } from "react-router-dom";
+
+import { api } from "@/services/api";
+
+import { DraftBookingView } from "./DraftBookingView";
+import { PageShell, SectionCard } from "./components/layout";
+import { ReadonlyBookingView } from "./ReadonlyBookingView";
+import { isDraftLike } from "./utils";
+
+export default function BookingDetailPage() {
+ const { id } = useParams<{ id: string }>();
+ const queryClient = useQueryClient();
+
+ const {
+ data: booking,
+ isLoading,
+ isError,
+ error,
+ } = useQuery(
+ api.bookings.get.queryOptions({ input: { id: id! }, enabled: !!id }),
+ );
+
+ const refetchBooking = () => {
+ queryClient.invalidateQueries({
+ queryKey: api.bookings.get.queryKey({ id: id! }),
+ });
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+ Loading booking details…
+
+
+
+ );
+ }
+
+ if (isError || !booking) {
+ return (
+
+
+
+
+
+
+
+ {isError ? "Failed to load booking" : "Booking not found"}
+
+ {isError && (
+
+ {error instanceof Error
+ ? error.message
+ : "An unexpected error occurred."}
+
+ )}
+
+
+
+ );
+ }
+
+ if (isDraftLike(booking.status)) {
+ return (
+
+ );
+ }
+ return ;
+}
diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/utils.ts b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/utils.ts
new file mode 100644
index 000000000..031a2d38c
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/utils.ts
@@ -0,0 +1,50 @@
+import { format } from "date-fns";
+
+import type { Freight } from "@edr/types";
+
+export const isNegative = (s: string) => s === "CANCELLED" || s === "REJECTED";
+export const isDraftLike = (s: string) =>
+ s === "DRAFT" || s === "CHANGES_REQUESTED";
+
+export function fmtDate(value?: string | null) {
+ if (!value) return "—";
+ const d = new Date(value);
+ return Number.isNaN(d.getTime()) ? "—" : format(d, "MMM d, yyyy");
+}
+
+export function yardLabel(y?: Freight.IBooking["originYard"]) {
+ return y?.label ?? y?.code ?? "—";
+}
+
+export 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" : "—";
+}
+
+export 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(" · ");
+}
+
+// ─── Pricing helpers ──────────────────────────────────────────────────────────
+
+export type Pricing = Freight.PricingBreakdown | null | undefined;
+
+export function priceLineItems(pricing: Pricing) {
+ return (pricing?.lineItems ?? []).map((li) => ({
+ label: li.description,
+ value: `${li.amount.toLocaleString()} ${li.currency}`,
+ }));
+}
+
+export function priceTotal(pricing: Pricing) {
+ if (!pricing) return "—";
+ const total = pricing.lineItems.reduce((s, li) => s + li.amount, 0);
+ return `${total.toLocaleString()} ${pricing.currency}`;
+}