import { useNavigate, useParams, useSearchParams } from "react-router-dom"; import toast from "react-hot-toast"; import { ArrowLeft, Container as ContainerIcon, FileSignature, FileText, Flame, FolderOpen, Layers, LayoutGrid, Link2, Milestone, MoreHorizontal, Package, Receipt, RefreshCw, Ship, Train, Truck, Wallet, Weight, } from "lucide-react"; import { ActionIcon, Alert, Badge, Box, Button, Center, Container, Grid, Group, Loader, Menu, Paper, SegmentedControl, Stack, Tabs, Text, } from "@mantine/core"; import { PageContainer, PageHeader, KpiStrip } from "@/components/page"; import { extractDownloadErrorMessage } from "@/components/warehouses/options"; import type { KpiItem } from "@/components/page"; import { EntityLink } from "@/components/detail"; import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar"; import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary"; import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { BookingPriorityBadge } from "@/components/bookings/BookingPriorityBadge"; import { NextStepBanner } from "@/components/bookings/NextStepBanner"; import { SchedulingStatusBadge } from "@/components/trainScheduling/ScheduleStatusBadge"; import { ConsolidationWaitingBanner } from "@/components/bookings/detail/ConsolidationWaitingBanner"; import { ConsolidationApprovalCard } from "@/components/bookings/detail/ConsolidationApprovalCard"; import { detailStyles, BookingRouteServiceCard, BookingMileServicesCard, BookingCargoCard, BookingCompanyCard, BookingContractCard, BookingContainerUnitsCard, BookingSchedulingWindowCard, BookingDocumentsPanel, BookingTrucksPanel, BookingWagonsPanel, ContractOrdersPanel, } from "@/components/bookings/detail"; import { WarehouseInfoCard } from "@/components/warehouses"; import { AdditionalPaymentsTab } from "@/components/bookings/AdditionalPaymentsTab"; import { getStatusMeta } from "@/features/bookings/booking-status.config"; import { toBookingListRow } from "@/features/bookings/mapBookingListRow"; import { formatDateTime, formatMoney } from "@/lib/format"; import { currencyDecimals } from "@edr/ui-common"; import { cargoTonsAndItems } from "@/utils/cargoWeight"; import type { BookingDetail } from "@/types/booking"; import { useBookingDetail, useBookingMutations, } from "@/hooks/bookings/useBookings"; import { useScrollToHash } from "@/hooks/useScrollToHash"; import { useFileViewer } from "@/hooks/useFileViewer"; import { bookingsService } from "@/services/bookings.service"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import { WagonCancellationCreditCard } from "@/components/bookings/wagon-cancellation"; export default function BookingRequestDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); // Deep-link from a warehouse fee invoice → this booking's warehouse section. useScrollToHash(); const { view, viewer } = useFileViewer(); const { user } = useAuth(); const canSeeAdditionalCharges = hasFreightPermission( user, FREIGHT_PERMS.additionalCharges.view, ); // Consolidated pair: `?booking=` swaps the WHOLE page over to the // other half of the shared wagon. Everything below — KPIs, stepper, the // overview/orders/documents/trucks sub-tabs, the action toolbar — then reads // from the selected booking, so each half gets its own complete detail page // under a top-level tab. The URL id stays put so Back still works. const selectedId = searchParams.get("booking") || id; const { data: booking, isLoading, isError, refetch, isFetching, } = useBookingDetail(selectedId); const mutations = useBookingMutations(selectedId ?? ""); // The pair is discovered from whichever half is on screen: each booking // carries a reference to the other. const routeBookingId = id ?? ""; const partnerId = booking?.consolidationPartnerId ?? null; const isPaired = Boolean(partnerId); const viewingPartner = selectedId !== routeBookingId; // Tab identities: the booking named by the URL is always the first tab, the // other half the second — regardless of which one is currently displayed. const firstTabId = routeBookingId; const secondTabId = viewingPartner ? selectedId : partnerId; // Only for the tab label (reference + customer) — the displayed half is // loaded above. Skipped entirely when the booking is not part of a pair. const { data: otherBooking } = useBookingDetail( secondTabId && secondTabId !== selectedId ? secondTabId : undefined, ); const firstTabBooking = viewingPartner ? otherBooking : booking; const secondTabBooking = viewingPartner ? booking : otherBooking; const selectBooking = (bookingId: string) => { const next = new URLSearchParams(searchParams); if (bookingId === routeBookingId) next.delete("booking"); else next.set("booking", bookingId); // Switching booking resets the sub-tab: the other half has its own content // and may not even have the tab that was open (e.g. Orders). next.delete("tab"); setSearchParams(next, { replace: true }); }; if (isLoading) { return (
Loading booking…
); } if (isError || !booking) { return (
Booking not found This request may have been removed or the link is invalid.
); } const statusMeta = getStatusMeta(booking.status); // Clearance review + finalize now lives solely on the Operations "Clearance // Documents" hub (/dashboard/contracts/clearance-documents → detail page), so // no clearance tab is embedded here anymore. // A general contract drives an "Orders" tab: each drawdown order spawns a // child booking that staff manage (clearance/approval) independently. const isGeneralContract = booking.bookingType === "GENERAL_CONTRACT"; // The Documents tab is always available — every booking can accrue clearance, // customs-workflow, invoice or notice files — so the tab bar always renders. const requestedTab = searchParams.get("tab"); const activeTab = requestedTab === "orders" && isGeneralContract ? "orders" : requestedTab === "documents" ? "documents" : requestedTab === "trucks" ? "trucks" : requestedTab === "wagons" ? "wagons" : requestedTab === "additional-charges" ? "additional-charges" : "overview"; const setActiveTab = (tab: string | null) => { const next = new URLSearchParams(searchParams); if (tab && tab !== "overview") next.set("tab", tab); else next.delete("tab"); setSearchParams(next, { replace: true }); }; const company = booking.company; const shippingLine = booking.shippingLineCompany ?? null; const customerName = toBookingListRow(booking).customerLabel; // What is being shipped, in words: bulk → the commodity (Wheat, Steel…); // containers → the shipper's own description when given. const cargoLabel = booking.freightType === "BULK" ? (booking.cargoType?.label ?? booking.cargoType?.name ?? null) : (booking.cargoFreeText?.trim() || null); const amount = Number(booking.totalAmount); const containers = booking.bookingContainers ?? []; const containerCount = containers.reduce( (sum, c) => sum + Number(c.quantity ?? 0), 0, ); const { tons: weight, items: itemCount } = cargoTonsAndItems(booking); const kpis: KpiItem[] = [ { label: "Total value", value: formatMoney(amount, booking.paymentCurrency, currencyDecimals(booking.paymentCurrency)), hint: booking.paymentStatus, icon: Wallet, color: "edr-green", }, { label: "Cargo weight", value: `${weight} T`, hint: itemCount != null ? `${itemCount} items` : "VGM total", icon: Weight, color: "blue", }, { label: "Containers", value: containerCount || "—", hint: `${containers.length} line${containers.length === 1 ? "" : "s"}`, icon: ContainerIcon, color: "teal", }, { label: "Priority score", value: booking.priorityScore ?? 0, hint: booking.tradeDirection, icon: Flame, color: "orange", }, ]; const hasSignableContract = booking.isGovernment && booking.contractSummary; return ( {booking.schedulingStatus ? ( ) : null} } subtitle={ {shippingLine ? ( {shippingLine.name} Shipping line ) : ( )} {cargoLabel ? ( · {cargoLabel} ) : null} · Scheduled {booking.scheduledDate} } action={ refetch()} > {hasSignableContract && ( } onClick={() => navigate(`/dashboard/booking-requests/${booking.id}/contract`) } > View / sign contract )} } onClick={async () => { try { const blob = await bookingsService.downloadCarriageAcceptanceSheet( booking.id, ); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `carriage-acceptance-${booking.reference}.pdf`; a.click(); URL.revokeObjectURL(url); } catch (error) { // Blob response: the JSON reason is inside the Blob, so // the sync path would show only "status code 400". toast.error(await extractDownloadErrorMessage(error)); } }} > Carriage acceptance sheet {booking.customsClearingEnabled && ( } onClick={() => navigate(`/dashboard/bookings/${booking.id}/clearance`) } > View document clearance )} } /> {/* Consolidated pair: one tab per booking, switching the ENTIRE page below. The overview/orders/documents/trucks tabs further down are sub-tabs of whichever booking is selected here. */} {isPaired && secondTabId ? ( value && selectBooking(value)} variant="pills" radius="md" > }> {firstTabBooking?.reference ?? "Booking"} {firstTabBooking?.company?.name ?? "—"} }> {secondTabBooking?.reference ?? "Partner booking"} {secondTabBooking?.company?.name ?? "—"} ) : null} {isPaired ? ( These two bookings share one wagon. Accepting or cancelling applies to both; each is invoiced and paid separately. ) : null} {booking.holdExpiresAt && booking.schedulingStatus === "HOLDING" ? ( Hold expires {formatDateTime(booking.holdExpiresAt)} ) : null} {booking.nextStep ? ( ) : null} {booking.status === "PENDING_CONSOLIDATION" && ( )} {booking.status === "CONSOLIDATION_APPROVAL_PENDING" && ( } title="Waiting for shared-wagon approval" > This booking shares a wagon with another customer's booking. Both are held here until the pairing is approved — neither reaches Operations before then. )} {/* LEFT — primary content, split into tabs to keep each view focused. The Documents tab is always present, so the tab bar always renders. */} } > Overview {isGeneralContract && ( }> Orders )} } > Documents }> Trucks }> Wagons {canSeeAdditionalCharges && ( } > Additional payments )} {isGeneralContract && ( )} {canSeeAdditionalCharges && ( )} {/* RIGHT — sticky action / summary rail */} {/* Sits directly above the staff actions: reviewing an operation request means approving the booking onto a specific train, so that train and its clock must be readable before the approve button. */} {/* Renders itself only when this booking has a shared wagon. */} {viewer} ); } /** The booking's primary detail cards — route, services, cargo, containers. */ function OverviewPanel({ booking, onRefetch, }: { booking: BookingDetail; onRefetch: () => void; }) { const row = toBookingListRow(booking); return ( How the cargo reaches the train { try { await bookingsService.setExportHandoverMode( booking.id, value as "DIRECT_TO_TRAIN" | "WAREHOUSE", ); onRefetch(); } catch (error) { toast.error( error instanceof Error ? error.message : "Could not change the handover mode", ); } }} /> {booking.exportHandoverMode === "DIRECT_TO_TRAIN" ? "No warehouse receipt and no GRN — the carriage acceptance sheet is the handover document." : "Cargo is received at the warehouse and issued a GRN before loading."} ) : null } /> ); }