mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 00:45:41 +00:00
style: inter module integration
This commit is contained in:
@@ -2,43 +2,56 @@ 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,
|
||||
Milestone,
|
||||
MoreHorizontal,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Truck,
|
||||
Wallet,
|
||||
Weight,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Container,
|
||||
Stack,
|
||||
Grid,
|
||||
ActionIcon,
|
||||
Box,
|
||||
Button,
|
||||
Center,
|
||||
Container,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
Menu,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
Paper,
|
||||
Button,
|
||||
Box,
|
||||
SegmentedControl,
|
||||
} from "@mantine/core";
|
||||
|
||||
import { PageContainer } from "@/components/page";
|
||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
||||
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 {
|
||||
detailStyles,
|
||||
BookingRequestHero,
|
||||
BookingRouteServiceCard,
|
||||
BookingMileServicesCard,
|
||||
BookingCargoCard,
|
||||
BookingCompanyCard,
|
||||
BookingContractSummaryCard,
|
||||
BookingContractCard,
|
||||
BookingContainerUnitsCard,
|
||||
BookingSchedulingWindowCard,
|
||||
BookingDocumentsPanel,
|
||||
@@ -48,6 +61,7 @@ import {
|
||||
import { WarehouseInfoCard } from "@/components/warehouses";
|
||||
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
||||
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
||||
import { cargoTonsAndItems } from "@/utils/cargoWeight";
|
||||
import type { BookingDetail } from "@/types/booking";
|
||||
import {
|
||||
useBookingDetail,
|
||||
@@ -133,7 +147,6 @@ export default function BookingRequestDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const row = toBookingListRow(booking);
|
||||
const statusMeta = getStatusMeta(booking.status);
|
||||
// Clearance review + finalize now lives solely on the Operations "Clearance
|
||||
// Documents" hub (/dashboard/contracts/clearance-documents → detail page), so
|
||||
@@ -159,23 +172,176 @@ export default function BookingRequestDetailPage() {
|
||||
setSearchParams(next, { replace: true });
|
||||
};
|
||||
|
||||
const company = booking.company;
|
||||
const customerName = toBookingListRow(booking).customerLabel;
|
||||
|
||||
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: `${booking.paymentCurrency} ${amount.toLocaleString(undefined, { minimumFractionDigits: 2 })}`,
|
||||
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 (
|
||||
<PageContainer>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
<PageHeader
|
||||
breadcrumbs={[
|
||||
{ label: "Booking requests", href: "/dashboard/booking-requests" },
|
||||
{ label: booking.reference },
|
||||
]}
|
||||
backTo="/dashboard/booking-requests"
|
||||
title={booking.reference}
|
||||
meta={
|
||||
<Group gap={6} wrap="wrap">
|
||||
<BookingStatusBadge status={booking.status} />
|
||||
<BookingPriorityBadge score={booking.priorityScore} />
|
||||
{booking.schedulingStatus ? (
|
||||
<SchedulingStatusBadge status={booking.schedulingStatus} />
|
||||
) : null}
|
||||
</Group>
|
||||
}
|
||||
subtitle={
|
||||
<Group gap={6} wrap="wrap">
|
||||
<EntityLink
|
||||
to={company?.id ? `/dashboard/customers/${company.id}` : null}
|
||||
label={customerName ?? "—"}
|
||||
/>
|
||||
<Text size="sm" c="dimmed">
|
||||
· Scheduled {booking.scheduledDate}
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
action={
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
<ActionIcon
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
size="lg"
|
||||
radius="md"
|
||||
loading={isFetching}
|
||||
aria-label="Refresh"
|
||||
onClick={() => refetch()}
|
||||
>
|
||||
<RefreshCw size={16} />
|
||||
</ActionIcon>
|
||||
<Menu position="bottom-end" width={260} withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="md"
|
||||
aria-label="More actions"
|
||||
>
|
||||
<MoreHorizontal size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{hasSignableContract && (
|
||||
<Menu.Item
|
||||
leftSection={<FileSignature size={15} />}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/booking-requests/${booking.id}/contract`)
|
||||
}
|
||||
>
|
||||
View / sign contract
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Item
|
||||
leftSection={<FileText size={15} />}
|
||||
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) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Carriage acceptance sheet is not available yet",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Carriage acceptance sheet
|
||||
</Menu.Item>
|
||||
{booking.customsClearingEnabled && (
|
||||
<Menu.Item
|
||||
leftSection={<Milestone size={15} />}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/bookings/${booking.id}/clearance`)
|
||||
}
|
||||
>
|
||||
View document clearance
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
}
|
||||
/>
|
||||
|
||||
<Stack gap="lg">
|
||||
<BookingRequestHero
|
||||
booking={booking}
|
||||
customerLabel={row.customerLabel}
|
||||
onBack={() => navigate("/dashboard/booking-requests")}
|
||||
onRefresh={() => refetch()}
|
||||
isFetching={isFetching}
|
||||
/>
|
||||
<KpiStrip items={kpis} />
|
||||
|
||||
{booking.holdExpiresAt && booking.schedulingStatus === "HOLDING" ? (
|
||||
<Text size="xs" c="orange.7">
|
||||
Hold expires {new Date(booking.holdExpiresAt).toLocaleString()}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
{booking.nextStep ? (
|
||||
<Paper
|
||||
radius="lg"
|
||||
p={4}
|
||||
style={{
|
||||
background: "var(--mantine-color-gray-0)",
|
||||
border: "1px solid var(--mantine-color-gray-2)",
|
||||
}}
|
||||
>
|
||||
<NextStepBanner nextStep={booking.nextStep} />
|
||||
</Paper>
|
||||
) : null}
|
||||
|
||||
<BookingWorkflowStepper
|
||||
status={booking.status}
|
||||
@@ -223,7 +389,7 @@ export default function BookingRequestDetailPage() {
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="overview">
|
||||
<OverviewPanel booking={booking} row={row} />
|
||||
<OverviewPanel booking={booking} onRefetch={refetch} />
|
||||
</Tabs.Panel>
|
||||
{isGeneralContract && (
|
||||
<Tabs.Panel value="orders">
|
||||
@@ -247,6 +413,7 @@ export default function BookingRequestDetailPage() {
|
||||
<Box style={{ position: "sticky", top: 24 }}>
|
||||
<Stack gap="lg">
|
||||
<BookingCompanyCard booking={booking} />
|
||||
<BookingContractCard booking={booking} />
|
||||
<BookingPricingSummary booking={booking} />
|
||||
<Box id="warehouse-payments">
|
||||
<WarehouseInfoCard
|
||||
@@ -265,97 +432,6 @@ export default function BookingRequestDetailPage() {
|
||||
booking={booking}
|
||||
mutations={mutations}
|
||||
/>
|
||||
{booking.tradeDirection === "EXPORT" && (
|
||||
<Paper withBorder radius="md" p="sm">
|
||||
<Stack gap={6}>
|
||||
<Text size="sm" fw={600}>
|
||||
How the cargo reaches the train
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
size="xs"
|
||||
value={booking.exportHandoverMode ?? "WAREHOUSE"}
|
||||
data={[
|
||||
{ value: "WAREHOUSE", label: "Warehouse then train" },
|
||||
{ value: "DIRECT_TO_TRAIN", label: "Direct truck to train" },
|
||||
]}
|
||||
onChange={async (value) => {
|
||||
try {
|
||||
await bookingsService.setExportHandoverMode(
|
||||
booking.id,
|
||||
value as "DIRECT_TO_TRAIN" | "WAREHOUSE",
|
||||
);
|
||||
await refetch();
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Could not change the handover mode",
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text size="xs" c="dimmed">
|
||||
{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."}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Paper>
|
||||
)}
|
||||
{booking.isGovernment && booking.contractSummary && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="default"
|
||||
leftSection={<FileSignature size={16} />}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/dashboard/booking-requests/${booking.id}/contract`,
|
||||
)
|
||||
}
|
||||
>
|
||||
View / sign contract
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
fullWidth
|
||||
variant="default"
|
||||
leftSection={<FileText size={16} />}
|
||||
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) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "Carriage acceptance sheet is not available yet",
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Carriage acceptance sheet
|
||||
</Button>
|
||||
{booking.customsClearingEnabled && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant="default"
|
||||
leftSection={<Milestone size={16} />}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/bookings/${booking.id}/clearance`)
|
||||
}
|
||||
>
|
||||
View document clearance
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
@@ -368,11 +444,13 @@ export default function BookingRequestDetailPage() {
|
||||
/** The booking's primary detail cards — route, services, cargo, containers. */
|
||||
function OverviewPanel({
|
||||
booking,
|
||||
row,
|
||||
onRefetch,
|
||||
}: {
|
||||
booking: BookingDetail;
|
||||
row: ReturnType<typeof toBookingListRow>;
|
||||
onRefetch: () => void;
|
||||
}) {
|
||||
const row = toBookingListRow(booking);
|
||||
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<BookingRouteServiceCard
|
||||
@@ -380,12 +458,49 @@ function OverviewPanel({
|
||||
originLabel={row.originLabel}
|
||||
destinationLabel={row.destinationLabel}
|
||||
/>
|
||||
<BookingMileServicesCard booking={booking} />
|
||||
<BookingMileServicesCard
|
||||
booking={booking}
|
||||
handoverSection={
|
||||
booking.tradeDirection === "EXPORT" ? (
|
||||
<Stack gap={6}>
|
||||
<Text size="sm" fw={600}>
|
||||
How the cargo reaches the train
|
||||
</Text>
|
||||
<SegmentedControl
|
||||
fullWidth
|
||||
size="xs"
|
||||
value={booking.exportHandoverMode ?? "WAREHOUSE"}
|
||||
data={[
|
||||
{ value: "WAREHOUSE", label: "Warehouse then train" },
|
||||
{ value: "DIRECT_TO_TRAIN", label: "Direct truck to train" },
|
||||
]}
|
||||
onChange={async (value) => {
|
||||
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",
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Text size="xs" c="dimmed">
|
||||
{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."}
|
||||
</Text>
|
||||
</Stack>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<BookingCargoCard booking={booking} />
|
||||
<BookingContainerUnitsCard booking={booking} />
|
||||
{booking.contractSummary && (
|
||||
<BookingContractSummaryCard summary={booking.contractSummary} />
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,11 +10,9 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Progress,
|
||||
RingProgress,
|
||||
Stack,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
AlertCircle,
|
||||
@@ -29,9 +27,13 @@ import {
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { ClearanceOpsTabs } from "@/components/contracts/ClearanceOpsTabs";
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { SectionCard } from "@/components/bookings/detail";
|
||||
import { PageContainer, PageHeader, KpiStrip } from "@/components/page";
|
||||
import type { KpiItem } from "@/components/page";
|
||||
import {
|
||||
SectionCard,
|
||||
BookingCompanyCard,
|
||||
BookingContractCard,
|
||||
} from "@/components/bookings/detail";
|
||||
import { ClearanceReviewSection } from "@/components/bookings/detail/ClearanceReviewSection";
|
||||
import { ClearancePhaseStepper } from "@/components/contracts/ClearancePhaseStepper";
|
||||
import { PhasedClearanceActionPanel } from "@/components/contracts/PhasedClearanceActionPanel";
|
||||
@@ -171,6 +173,18 @@ export default function DocumentClearanceDetailPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const direction = booking?.tradeDirection ?? "—";
|
||||
const origin = booking?.originYard?.label ?? booking?.originYard?.code ?? "Origin";
|
||||
const destination =
|
||||
booking?.destinationYard?.label ?? booking?.destinationYard?.code ?? "Destination";
|
||||
|
||||
const kpis: KpiItem[] = [
|
||||
{ label: "Approved", value: stats.approved, icon: CheckCircle2, color: "edr-green" },
|
||||
{ label: "Queried", value: stats.queried, icon: AlertCircle, color: "red" },
|
||||
{ label: "Pending", value: stats.pending, icon: Clock, color: "gray" },
|
||||
{ label: "Review progress", value: `${stats.pct}%`, icon: PackageCheck, color: "blue" },
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
@@ -182,25 +196,46 @@ export default function DocumentClearanceDetailPage() {
|
||||
{ label: reference },
|
||||
]}
|
||||
meta={
|
||||
clearance.allApproved ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<CheckCircle2 size={13} />}
|
||||
>
|
||||
All approved
|
||||
<Group gap={6} wrap="wrap">
|
||||
<Badge variant="light" color={direction === "IMPORT" ? "edr-green" : "gray"} radius="sm">
|
||||
{direction}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="gray"
|
||||
radius="sm"
|
||||
leftSection={<Clock size={13} />}
|
||||
>
|
||||
Review pending
|
||||
</Badge>
|
||||
)
|
||||
{clearance.includesCustoms ? (
|
||||
<Badge variant="light" color="edr-green" radius="sm" leftSection={<ShieldCheck size={12} />}>
|
||||
Customs
|
||||
</Badge>
|
||||
) : null}
|
||||
{clearance.allApproved ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<CheckCircle2 size={13} />}
|
||||
>
|
||||
All approved
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="gray"
|
||||
radius="sm"
|
||||
leftSection={<Clock size={13} />}
|
||||
>
|
||||
Review pending
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
}
|
||||
subtitle={
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<Text size="sm" c="dimmed" fw={600}>
|
||||
{origin}
|
||||
</Text>
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
<Text size="sm" c="dimmed" fw={600}>
|
||||
{destination}
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
action={
|
||||
canCompleteBooking ? (
|
||||
@@ -233,12 +268,16 @@ export default function DocumentClearanceDetailPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
<ClearanceHero
|
||||
booking={booking}
|
||||
clearance={clearance}
|
||||
stats={stats}
|
||||
requestedLines={requestedLines}
|
||||
/>
|
||||
<KpiStrip items={kpis} />
|
||||
|
||||
{requestedLines ? (
|
||||
<Group gap={10} align="center" wrap="wrap">
|
||||
<Text size="xs" fw={700} tt="uppercase" c="dimmed" lts="0.05em">
|
||||
Requested cargo
|
||||
</Text>
|
||||
<RequestedCargoChips lines={requestedLines} size="sm" />
|
||||
</Group>
|
||||
) : null}
|
||||
|
||||
{isPhasedGeneral ? (
|
||||
<Paper withBorder radius="md" p="lg">
|
||||
@@ -273,67 +312,54 @@ export default function DocumentClearanceDetailPage() {
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: isPhasedGeneral ? 5 : 4 }}>
|
||||
{isPhasedGeneral ? (
|
||||
<PhasedClearanceActionPanel
|
||||
bookingId={id!}
|
||||
clearance={clearance}
|
||||
tradeDirection={booking?.tradeDirection ?? "IMPORT"}
|
||||
workflowFiles={workflowFiles}
|
||||
roleMode="ET"
|
||||
// A bare initiated instance still has no cargo/price — the
|
||||
// stepper's "Create booking" step must read as NOT-yet-created
|
||||
// so it never claims the booking is done before GL completes it.
|
||||
bookingCreated={Number(booking?.totalAmount ?? 0) > 0}
|
||||
bookingMilestones={bookingMilestones ?? []}
|
||||
onChanged={() => void refetch()}
|
||||
onViewFile={view}
|
||||
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
|
||||
/>
|
||||
) : (
|
||||
<Box style={{ position: "sticky", top: 24 }}>
|
||||
<SectionCard
|
||||
icon={PackageCheck}
|
||||
title="Review progress"
|
||||
accent="edr-green"
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<RingProgress
|
||||
size={140}
|
||||
thickness={12}
|
||||
roundCaps
|
||||
sections={[{ value: stats.pct, color: "edr-green" }]}
|
||||
label={
|
||||
<Stack gap={0} align="center">
|
||||
<Text fw={800} fz={26} lh={1}>
|
||||
{stats.pct}%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
approved
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Group gap="lg" justify="center">
|
||||
<ProgressStat
|
||||
color="edr-green"
|
||||
label="Approved"
|
||||
value={stats.approved}
|
||||
<Box style={{ position: "sticky", top: 24 }}>
|
||||
<Stack gap="lg">
|
||||
{booking ? <BookingCompanyCard booking={booking} /> : null}
|
||||
{booking ? <BookingContractCard booking={booking} /> : null}
|
||||
{isPhasedGeneral ? (
|
||||
<PhasedClearanceActionPanel
|
||||
bookingId={id!}
|
||||
clearance={clearance}
|
||||
tradeDirection={booking?.tradeDirection ?? "IMPORT"}
|
||||
workflowFiles={workflowFiles}
|
||||
roleMode="ET"
|
||||
// A bare initiated instance still has no cargo/price — the
|
||||
// stepper's "Create booking" step must read as NOT-yet-created
|
||||
// so it never claims the booking is done before GL completes it.
|
||||
bookingCreated={Number(booking?.totalAmount ?? 0) > 0}
|
||||
bookingMilestones={bookingMilestones ?? []}
|
||||
onChanged={() => void refetch()}
|
||||
onViewFile={view}
|
||||
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
|
||||
/>
|
||||
) : (
|
||||
<SectionCard
|
||||
icon={PackageCheck}
|
||||
title="Review progress"
|
||||
accent="edr-green"
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<RingProgress
|
||||
size={140}
|
||||
thickness={12}
|
||||
roundCaps
|
||||
sections={[{ value: stats.pct, color: "edr-green" }]}
|
||||
label={
|
||||
<Stack gap={0} align="center">
|
||||
<Text fw={800} fz={26} lh={1}>
|
||||
{stats.pct}%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
approved
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="red"
|
||||
label="Queried"
|
||||
value={stats.queried}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="gray"
|
||||
label="Pending"
|
||||
value={stats.pending}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
}
|
||||
@@ -347,125 +373,3 @@ export default function DocumentClearanceDetailPage() {
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ClearanceHero({
|
||||
booking,
|
||||
clearance,
|
||||
stats,
|
||||
requestedLines,
|
||||
}: {
|
||||
booking: ReturnType<typeof useBookingDetail>["data"];
|
||||
clearance: Freight.ClearanceView;
|
||||
stats: { pct: number; approved: number; total: number };
|
||||
requestedLines?: Freight.RequestedShipmentLines | null;
|
||||
}) {
|
||||
const direction = booking?.tradeDirection ?? "—";
|
||||
const origin =
|
||||
booking?.originYard?.label ?? booking?.originYard?.code ?? "Origin";
|
||||
const destination =
|
||||
booking?.destinationYard?.label ??
|
||||
booking?.destinationYard?.code ??
|
||||
"Destination";
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="md" p="lg">
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="lg">
|
||||
<Group gap="md" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<ThemeIcon variant="light" color="edr-green" radius="md" size={52}>
|
||||
<ShieldCheck size={26} />
|
||||
</ThemeIcon>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<Text fw={800} fz={20} c="edr-text" truncate>
|
||||
{booking?.reference ?? "Clearance"}
|
||||
</Text>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color={direction === "IMPORT" ? "edr-green" : "gray"}
|
||||
radius="sm"
|
||||
>
|
||||
{direction}
|
||||
</Badge>
|
||||
{clearance.includesCustoms ? (
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<ShieldCheck size={12} />}
|
||||
>
|
||||
Customs
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
<Group gap={8} mt={6} wrap="nowrap">
|
||||
<Text size="sm" fw={600} truncate maw={160}>
|
||||
{origin}
|
||||
</Text>
|
||||
<ArrowRight size={15} className="shrink-0 text-muted-foreground" />
|
||||
<Text size="sm" fw={600} truncate maw={160}>
|
||||
{destination}
|
||||
</Text>
|
||||
</Group>
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
<Box style={{ minWidth: 200, flex: 1, maxWidth: 320 }}>
|
||||
<Group justify="space-between" mb={6}>
|
||||
<Text size="xs" c="dimmed" fw={600}>
|
||||
Document review
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{stats.approved}/{stats.total}
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress value={stats.pct} color="edr-green" radius="xl" size="md" />
|
||||
</Box>
|
||||
</Group>
|
||||
|
||||
{requestedLines ? (
|
||||
<>
|
||||
<Box my="md" h={1} bg="var(--mantine-color-default-border)" />
|
||||
<Group gap={10} align="center" wrap="wrap">
|
||||
<Text size="xs" fw={700} tt="uppercase" c="dimmed" lts="0.05em">
|
||||
Requested cargo
|
||||
</Text>
|
||||
<RequestedCargoChips lines={requestedLines} size="sm" />
|
||||
</Group>
|
||||
</>
|
||||
) : null}
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function ProgressStat({
|
||||
color,
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
color: string;
|
||||
label: string;
|
||||
value: number;
|
||||
}) {
|
||||
return (
|
||||
<Stack gap={2} align="center">
|
||||
<Text fw={700} fz={18} c="edr-text">
|
||||
{value}
|
||||
</Text>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
<Box
|
||||
style={{
|
||||
width: 7,
|
||||
height: 7,
|
||||
borderRadius: 999,
|
||||
background: `var(--mantine-color-${color}-6)`,
|
||||
}}
|
||||
/>
|
||||
<Text fz="11px" c="dimmed">
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user