mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
- Updated ContractRouteLineView to clarify that routes carry no quantity and are pure origin-destination lanes. - Modified GeneralContractService to reflect changes in route handling, removing quantity-related logic. - Adjusted BookingsService to persist contracted routes without quantities, aligning with the new contract structure. - Revised CreateBookingDto and CreateContractRouteDto to remove quantity fields, emphasizing shared pool usage. - Added ContractOrdersPanel component to display drawdown orders and their associated pool. - Implemented useContractOrders and useContractPool hooks for fetching order and pool data. - Created booking-orders.service.ts to manage API interactions for drawdown orders and pool data. - Updated BookingRequestDetailPage and PlaceOrderDialog to accommodate new order handling logic.
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
|
import {
|
|
ArrowLeft,
|
|
FileSignature,
|
|
Layers,
|
|
LayoutGrid,
|
|
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,
|
|
BookingDocumentsCard,
|
|
ClearanceReviewSection,
|
|
ContractOrdersPanel,
|
|
type BookingFileView,
|
|
} 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 { downloadBookingFile } from "@/services/files.service";
|
|
import {
|
|
useBookingDetail,
|
|
useBookingMutations,
|
|
} from "@/hooks/bookings/useBookings";
|
|
import toast from "react-hot-toast";
|
|
|
|
// Signature / generated-contract files are surfaced on the contract page, not
|
|
// in the booking's Documents list.
|
|
const SIGNATURE_FILE_CODES = new Set([
|
|
"signature",
|
|
"signature_customer",
|
|
"signature_staff",
|
|
"contract",
|
|
]);
|
|
|
|
export default function BookingRequestDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const navigate = useNavigate();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const {
|
|
data: booking,
|
|
isLoading,
|
|
isError,
|
|
refetch,
|
|
isFetching,
|
|
} = useBookingDetail(id);
|
|
const mutations = useBookingMutations(id ?? "");
|
|
|
|
const handleDownloadFile = async (file: BookingFileView) => {
|
|
try {
|
|
await downloadBookingFile(file.id, file.name);
|
|
} catch {
|
|
toast.error("Could not download file.");
|
|
}
|
|
};
|
|
|
|
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 showContractButton = [
|
|
"CONTRACT_READY",
|
|
"SIGNED_CUSTOMER",
|
|
"FULLY_EXECUTED",
|
|
].includes(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";
|
|
const showTabs = showClearanceTab || isGeneralContract;
|
|
const requestedTab = searchParams.get("tab");
|
|
const activeTab =
|
|
requestedTab === "clearance" && showClearanceTab
|
|
? "clearance"
|
|
: requestedTab === "orders" && isGeneralContract
|
|
? "orders"
|
|
: "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 */}
|
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
|
{showTabs ? (
|
|
<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.List>
|
|
|
|
<Tabs.Panel value="overview">
|
|
<OverviewPanel
|
|
booking={booking}
|
|
row={row}
|
|
onDownload={handleDownloadFile}
|
|
/>
|
|
</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>
|
|
) : (
|
|
<OverviewPanel
|
|
booking={booking}
|
|
row={row}
|
|
onDownload={handleDownloadFile}
|
|
/>
|
|
)}
|
|
</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} />
|
|
<WarehouseInfoCard
|
|
bookingId={booking.id}
|
|
bookingReference={booking.reference}
|
|
/>
|
|
<BookingActionsToolbar
|
|
booking={booking}
|
|
mutations={mutations}
|
|
/>
|
|
{showContractButton && (
|
|
<Button
|
|
fullWidth
|
|
color="edr-green"
|
|
leftSection={<FileSignature size={16} />}
|
|
onClick={() =>
|
|
navigate(
|
|
`/dashboard/booking-requests/${booking.id}/contract`,
|
|
)
|
|
}
|
|
>
|
|
View & sign contract
|
|
</Button>
|
|
)}
|
|
{showApprovalCard && (
|
|
<ApprovalStepsCard booking={booking} mutations={mutations} />
|
|
)}
|
|
</Stack>
|
|
</Box>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</Stack>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
/** The booking's primary detail cards — route, services, cargo, contract, docs. */
|
|
function OverviewPanel({
|
|
booking,
|
|
row,
|
|
onDownload,
|
|
}: {
|
|
booking: BookingDetail;
|
|
row: ReturnType<typeof toBookingListRow>;
|
|
onDownload: (file: BookingFileView) => void;
|
|
}) {
|
|
return (
|
|
<Stack gap="lg">
|
|
<BookingRouteServiceCard
|
|
booking={booking}
|
|
originLabel={row.originLabel}
|
|
destinationLabel={row.destinationLabel}
|
|
/>
|
|
<BookingMileServicesCard booking={booking} />
|
|
<BookingCargoCard booking={booking} />
|
|
{booking.contractSummary && (
|
|
<BookingContractSummaryCard summary={booking.contractSummary} />
|
|
)}
|
|
<BookingDocumentsCard
|
|
files={(booking.files ?? []).filter(
|
|
(f) => !SIGNATURE_FILE_CODES.has(f.code ?? ""),
|
|
)}
|
|
onDownload={onDownload}
|
|
/>
|
|
</Stack>
|
|
);
|
|
}
|