mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 04:15:43 +00:00
322 lines
10 KiB
TypeScript
322 lines
10 KiB
TypeScript
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
import {
|
|
ArrowLeft,
|
|
FolderOpen,
|
|
Layers,
|
|
LayoutGrid,
|
|
Milestone,
|
|
Package,
|
|
ShieldCheck,
|
|
} from "lucide-react";
|
|
import {
|
|
Container,
|
|
Stack,
|
|
Grid,
|
|
Center,
|
|
Loader,
|
|
Tabs,
|
|
Text,
|
|
Paper,
|
|
Button,
|
|
Box,
|
|
} from "@mantine/core";
|
|
|
|
import { PageContainer } from "@/components/page";
|
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
|
import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
|
|
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
|
import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary";
|
|
import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper";
|
|
import { ConsolidationWaitingBanner } from "@/components/bookings/detail/ConsolidationWaitingBanner";
|
|
import {
|
|
detailStyles,
|
|
BookingRequestHero,
|
|
BookingRouteServiceCard,
|
|
BookingMileServicesCard,
|
|
BookingCargoCard,
|
|
BookingCompanyCard,
|
|
BookingContractSummaryCard,
|
|
BookingContainerUnitsCard,
|
|
ClearanceReviewSection,
|
|
BookingDocumentsPanel,
|
|
ContractOrdersPanel,
|
|
} from "@/components/bookings/detail";
|
|
import { WarehouseInfoCard } from "@/components/warehouses";
|
|
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
|
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
|
import type { BookingDetail } from "@/types/booking";
|
|
import {
|
|
useBookingDetail,
|
|
useBookingMutations,
|
|
} from "@/hooks/bookings/useBookings";
|
|
import { useScrollToHash } from "@/hooks/useScrollToHash";
|
|
|
|
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 {
|
|
data: booking,
|
|
isLoading,
|
|
isError,
|
|
refetch,
|
|
isFetching,
|
|
} = useBookingDetail(id);
|
|
const mutations = useBookingMutations(id ?? "");
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<PageContainer>
|
|
<Center mih="60vh">
|
|
<Stack align="center" gap="md">
|
|
<Loader color="gray" />
|
|
<Text size="sm" c="dimmed" fw={500}>
|
|
Loading booking…
|
|
</Text>
|
|
</Stack>
|
|
</Center>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
if (isError || !booking) {
|
|
return (
|
|
<PageContainer>
|
|
<Container size="sm" py="xl">
|
|
<Paper
|
|
radius="md"
|
|
withBorder
|
|
p="xl"
|
|
ta="center"
|
|
style={detailStyles.card}
|
|
>
|
|
<Center>
|
|
<Box
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 16,
|
|
background: "var(--mantine-color-gray-1)",
|
|
color: "var(--mantine-color-gray-6)",
|
|
}}
|
|
>
|
|
<Package size={32} />
|
|
</Box>
|
|
</Center>
|
|
<Text fw={700} size="lg" mt="lg">
|
|
Booking not found
|
|
</Text>
|
|
<Text size="sm" c="dimmed" mt={4}>
|
|
This request may have been removed or the link is invalid.
|
|
</Text>
|
|
<Button
|
|
variant="default"
|
|
mt="lg"
|
|
leftSection={<ArrowLeft size={16} />}
|
|
onClick={() => navigate("/dashboard/booking-requests")}
|
|
>
|
|
Back to booking requests
|
|
</Button>
|
|
</Paper>
|
|
</Container>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const row = toBookingListRow(booking);
|
|
const statusMeta = getStatusMeta(booking.status);
|
|
const showApprovalCard =
|
|
booking.status === "PENDING_APPROVAL" ||
|
|
booking.status === "APPROVED_PENDING_SIGNATURE";
|
|
|
|
// Non-customs clearance is reviewed here by Marketing in its own tab; customs
|
|
// bookings are handled in the Global Logistics clearance queue instead.
|
|
const showClearanceTab =
|
|
!booking.customsClearingEnabled &&
|
|
["AWAITING_DOCUMENTS", "DOCUMENTS_UNDER_REVIEW", "CLEARANCE_READY"].includes(
|
|
booking.status,
|
|
);
|
|
// 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 === "clearance" && showClearanceTab
|
|
? "clearance"
|
|
: requestedTab === "orders" && isGeneralContract
|
|
? "orders"
|
|
: requestedTab === "documents"
|
|
? "documents"
|
|
: "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 });
|
|
};
|
|
|
|
return (
|
|
<PageContainer>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: "Booking requests", href: "/dashboard/booking-requests" },
|
|
{ label: booking.reference },
|
|
]}
|
|
/>
|
|
|
|
<Stack gap="lg">
|
|
<BookingRequestHero
|
|
booking={booking}
|
|
customerLabel={row.customerLabel}
|
|
onBack={() => navigate("/dashboard/booking-requests")}
|
|
onRefresh={() => refetch()}
|
|
isFetching={isFetching}
|
|
/>
|
|
|
|
<BookingWorkflowStepper
|
|
status={booking.status}
|
|
title={statusMeta.title}
|
|
description={statusMeta.description}
|
|
titleColor={statusMeta.color}
|
|
/>
|
|
|
|
{booking.status === "PENDING_CONSOLIDATION" && (
|
|
<ConsolidationWaitingBanner bookingId={booking.id} />
|
|
)}
|
|
|
|
<Grid gap="lg">
|
|
{/* LEFT — primary content, split into tabs to keep each view focused.
|
|
The Documents tab is always present, so the tab bar always renders. */}
|
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
|
<Tabs
|
|
value={activeTab}
|
|
onChange={setActiveTab}
|
|
variant="pills"
|
|
color="edr-blue"
|
|
keepMounted={false}
|
|
>
|
|
<Tabs.List mb="lg">
|
|
<Tabs.Tab
|
|
value="overview"
|
|
leftSection={<LayoutGrid size={16} />}
|
|
>
|
|
Overview
|
|
</Tabs.Tab>
|
|
{isGeneralContract && (
|
|
<Tabs.Tab value="orders" leftSection={<Layers size={16} />}>
|
|
Orders
|
|
</Tabs.Tab>
|
|
)}
|
|
{showClearanceTab && (
|
|
<Tabs.Tab
|
|
value="clearance"
|
|
leftSection={<ShieldCheck size={16} />}
|
|
>
|
|
Customer clearance
|
|
</Tabs.Tab>
|
|
)}
|
|
<Tabs.Tab
|
|
value="documents"
|
|
leftSection={<FolderOpen size={16} />}
|
|
>
|
|
Documents
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="overview">
|
|
<OverviewPanel booking={booking} row={row} />
|
|
</Tabs.Panel>
|
|
{isGeneralContract && (
|
|
<Tabs.Panel value="orders">
|
|
<ContractOrdersPanel
|
|
contractBookingId={booking.id}
|
|
isContainer={booking.freightType === "CONTAINER"}
|
|
/>
|
|
</Tabs.Panel>
|
|
)}
|
|
{showClearanceTab && (
|
|
<Tabs.Panel value="clearance">
|
|
<ClearanceReviewSection
|
|
bookingId={booking.id}
|
|
onChanged={() => refetch()}
|
|
/>
|
|
</Tabs.Panel>
|
|
)}
|
|
<Tabs.Panel value="documents">
|
|
<BookingDocumentsPanel bookingId={booking.id} />
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Grid.Col>
|
|
|
|
{/* RIGHT — sticky action / summary rail */}
|
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
|
<Box style={{ position: "sticky", top: 24 }}>
|
|
<Stack gap="lg">
|
|
<BookingCompanyCard booking={booking} />
|
|
<BookingPricingSummary booking={booking} />
|
|
<Box id="warehouse-payments">
|
|
<WarehouseInfoCard
|
|
bookingId={booking.id}
|
|
bookingReference={booking.reference}
|
|
/>
|
|
</Box>
|
|
<BookingActionsToolbar
|
|
booking={booking}
|
|
mutations={mutations}
|
|
/>
|
|
{booking.customsClearingEnabled && (
|
|
<Button
|
|
fullWidth
|
|
variant="default"
|
|
leftSection={<Milestone size={16} />}
|
|
onClick={() =>
|
|
navigate(`/dashboard/bookings/${booking.id}/clearance`)
|
|
}
|
|
>
|
|
View document clearance
|
|
</Button>
|
|
)}
|
|
{showApprovalCard && (
|
|
<ApprovalStepsCard booking={booking} mutations={mutations} />
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</Stack>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
/** The booking's primary detail cards — route, services, cargo, containers. */
|
|
function OverviewPanel({
|
|
booking,
|
|
row,
|
|
}: {
|
|
booking: BookingDetail;
|
|
row: ReturnType<typeof toBookingListRow>;
|
|
}) {
|
|
return (
|
|
<Stack gap="lg">
|
|
<BookingRouteServiceCard
|
|
booking={booking}
|
|
originLabel={row.originLabel}
|
|
destinationLabel={row.destinationLabel}
|
|
/>
|
|
<BookingMileServicesCard booking={booking} />
|
|
<BookingCargoCard booking={booking} />
|
|
<BookingContainerUnitsCard booking={booking} />
|
|
{booking.contractSummary && (
|
|
<BookingContractSummaryCard summary={booking.contractSummary} />
|
|
)}
|
|
</Stack>
|
|
);
|
|
}
|