mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
feat(bookings): add clearance review functionality and update UI components
This commit is contained in:
@@ -8,6 +8,7 @@ import { useAuth } from "@/auth/useAuth";
|
|||||||
import {
|
import {
|
||||||
getNextPendingApprovalStep,
|
getNextPendingApprovalStep,
|
||||||
isAllocateAction,
|
isAllocateAction,
|
||||||
|
isClearanceNavAction,
|
||||||
isContractNavAction,
|
isContractNavAction,
|
||||||
listRowHasActions,
|
listRowHasActions,
|
||||||
type BookingActionContext,
|
type BookingActionContext,
|
||||||
@@ -38,6 +39,7 @@ export function BookingActionsMenu({
|
|||||||
reference: row.reference,
|
reference: row.reference,
|
||||||
approvalSteps: row.approvalSteps,
|
approvalSteps: row.approvalSteps,
|
||||||
schedulingStatus: row.schedulingStatus,
|
schedulingStatus: row.schedulingStatus,
|
||||||
|
customsClearingEnabled: row.customsClearingEnabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
const flow = useBookingActionDialog(row.id, context);
|
const flow = useBookingActionDialog(row.id, context);
|
||||||
@@ -46,10 +48,15 @@ export function BookingActionsMenu({
|
|||||||
const goToContract = () =>
|
const goToContract = () =>
|
||||||
navigate(`/dashboard/booking-requests/${row.id}/contract`);
|
navigate(`/dashboard/booking-requests/${row.id}/contract`);
|
||||||
|
|
||||||
|
const goToClearanceTab = () =>
|
||||||
|
navigate(`/dashboard/booking-requests/${row.id}?tab=clearance`);
|
||||||
|
|
||||||
const handleAction = (action: (typeof actions)[number]) => {
|
const handleAction = (action: (typeof actions)[number]) => {
|
||||||
onSuppressRowClick?.();
|
onSuppressRowClick?.();
|
||||||
if (isContractNavAction(action.id)) {
|
if (isContractNavAction(action.id)) {
|
||||||
goToContract();
|
goToContract();
|
||||||
|
} else if (isClearanceNavAction(action.id)) {
|
||||||
|
goToClearanceTab();
|
||||||
} else if (isAllocateAction(action.id)) {
|
} else if (isAllocateAction(action.id)) {
|
||||||
onAllocateBooking?.();
|
onAllocateBooking?.();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ const STATUS_META: Record<
|
|||||||
Freight.DocumentReviewStatus,
|
Freight.DocumentReviewStatus,
|
||||||
{ label: string; color: string }
|
{ label: string; color: string }
|
||||||
> = {
|
> = {
|
||||||
APPROVED: { label: "Approved", color: "edr-green" },
|
APPROVED: { label: "Approved", color: "edr-blue" },
|
||||||
QUERIED: { label: "Queried", color: "red" },
|
QUERIED: { label: "Queried", color: "red" },
|
||||||
PENDING: { label: "Pending", color: "edr-slate" },
|
PENDING: { label: "Pending", color: "edr-accent" },
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,7 +142,7 @@ export function ClearanceReviewSection({
|
|||||||
if (isLoading || !clearance) {
|
if (isLoading || !clearance) {
|
||||||
return (
|
return (
|
||||||
<Group justify="center" py="xl" gap={10}>
|
<Group justify="center" py="xl" gap={10}>
|
||||||
<Loader size="sm" color="edr-green" />
|
<Loader size="sm" color="edr-blue" />
|
||||||
<Text c="dimmed">Loading clearance…</Text>
|
<Text c="dimmed">Loading clearance…</Text>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
@@ -165,15 +165,15 @@ export function ClearanceReviewSection({
|
|||||||
<Box>
|
<Box>
|
||||||
<Progress
|
<Progress
|
||||||
value={stats.pct}
|
value={stats.pct}
|
||||||
color="edr-green"
|
color="edr-blue"
|
||||||
radius="xl"
|
radius="xl"
|
||||||
size="sm"
|
size="sm"
|
||||||
mb={6}
|
mb={6}
|
||||||
/>
|
/>
|
||||||
<Group gap="lg">
|
<Group gap="lg">
|
||||||
<StatPill color="edr-green" label="Approved" value={stats.approved} />
|
<StatPill color="edr-blue" label="Approved" value={stats.approved} />
|
||||||
<StatPill color="red" label="Queried" value={stats.queried} />
|
<StatPill color="red" label="Queried" value={stats.queried} />
|
||||||
<StatPill color="edr-slate" label="Pending" value={stats.pending} />
|
<StatPill color="edr-accent" label="Pending" value={stats.pending} />
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -219,13 +219,13 @@ export function ClearanceReviewSection({
|
|||||||
icon={Upload}
|
icon={Upload}
|
||||||
title="Customs output documents"
|
title="Customs output documents"
|
||||||
subtitle="Upload the cleared/customs paperwork to hand back to the customer."
|
subtitle="Upload the cleared/customs paperwork to hand back to the customer."
|
||||||
accent="edr-blue"
|
accent="edr-accent"
|
||||||
>
|
>
|
||||||
<Stack gap={10}>
|
<Stack gap={10}>
|
||||||
{glDocs.map((doc) => (
|
{glDocs.map((doc) => (
|
||||||
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
|
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
|
||||||
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<FileText size={16} color="var(--mantine-color-edr-blue-6)" />
|
<FileText size={16} color="var(--mantine-color-edr-accent-6)" />
|
||||||
<Text fz="13px" c="edr-text" truncate>
|
<Text fz="13px" c="edr-text" truncate>
|
||||||
{doc.label}
|
{doc.label}
|
||||||
{doc.required ? " *" : ""}
|
{doc.required ? " *" : ""}
|
||||||
@@ -239,7 +239,7 @@ export function ClearanceReviewSection({
|
|||||||
href={doc.file.url}
|
href={doc.file.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
c="edr-blue"
|
c="edr-accent"
|
||||||
style={{ display: "flex" }}
|
style={{ display: "flex" }}
|
||||||
>
|
>
|
||||||
<Download size={15} />
|
<Download size={15} />
|
||||||
@@ -261,7 +261,7 @@ export function ClearanceReviewSection({
|
|||||||
{...props}
|
{...props}
|
||||||
size="compact-xs"
|
size="compact-xs"
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-green"
|
color="edr-accent"
|
||||||
leftSection={<Upload size={13} />}
|
leftSection={<Upload size={13} />}
|
||||||
>
|
>
|
||||||
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
|
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
|
||||||
@@ -275,7 +275,7 @@ export function ClearanceReviewSection({
|
|||||||
<Group justify="flex-end" mt="md">
|
<Group justify="flex-end" mt="md">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-green"
|
color="edr-accent"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<Upload size={15} />}
|
leftSection={<Upload size={15} />}
|
||||||
disabled={Object.keys(outputFiles).length === 0}
|
disabled={Object.keys(outputFiles).length === 0}
|
||||||
@@ -301,7 +301,7 @@ export function ClearanceReviewSection({
|
|||||||
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<ThemeIcon
|
<ThemeIcon
|
||||||
variant="light"
|
variant="light"
|
||||||
color={clearance.allApproved ? "edr-green" : "gray"}
|
color={clearance.allApproved ? "edr-blue" : "gray"}
|
||||||
radius="md"
|
radius="md"
|
||||||
size={28}
|
size={28}
|
||||||
>
|
>
|
||||||
@@ -314,7 +314,7 @@ export function ClearanceReviewSection({
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Button
|
<Button
|
||||||
color="edr-green"
|
color="edr-blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<CheckCircle2 size={16} />}
|
leftSection={<CheckCircle2 size={16} />}
|
||||||
disabled={!clearance.allApproved}
|
disabled={!clearance.allApproved}
|
||||||
@@ -391,7 +391,7 @@ function DocReviewCard({
|
|||||||
status === "QUERIED"
|
status === "QUERIED"
|
||||||
? "var(--mantine-color-red-2)"
|
? "var(--mantine-color-red-2)"
|
||||||
: status === "APPROVED"
|
: status === "APPROVED"
|
||||||
? "var(--mantine-color-edr-green-2)"
|
? "var(--mantine-color-edr-blue-2)"
|
||||||
: "var(--mantine-color-edr-border-6)",
|
: "var(--mantine-color-edr-border-6)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -471,7 +471,7 @@ function DocReviewCard({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
color="edr-green"
|
color="edr-blue"
|
||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<CheckCircle2 size={14} />}
|
leftSection={<CheckCircle2 size={14} />}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type BookingActionId =
|
|||||||
| "rejectApproval"
|
| "rejectApproval"
|
||||||
| "viewContract"
|
| "viewContract"
|
||||||
| "signContractStaff"
|
| "signContractStaff"
|
||||||
|
| "reviewClearance"
|
||||||
| "allocateBooking"
|
| "allocateBooking"
|
||||||
| "startTransit"
|
| "startTransit"
|
||||||
| "complete"
|
| "complete"
|
||||||
@@ -69,6 +70,7 @@ export type BookingActionContext = Pick<
|
|||||||
| "approvalSteps"
|
| "approvalSteps"
|
||||||
| "reference"
|
| "reference"
|
||||||
| "schedulingStatus"
|
| "schedulingStatus"
|
||||||
|
| "customsClearingEnabled"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const ALLOCATABLE_SCHEDULING_STATUSES = new Set([
|
const ALLOCATABLE_SCHEDULING_STATUSES = new Set([
|
||||||
@@ -250,6 +252,20 @@ const SIGN_CONTRACT_STAFF_ACTION: BookingActionDef = {
|
|||||||
primary: true,
|
primary: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Opens the booking detail straight on the Clearance tab so Marketing can
|
||||||
|
// review the customer's clearance documents (non-customs bookings only).
|
||||||
|
const REVIEW_CLEARANCE_ACTION: BookingActionDef = {
|
||||||
|
id: "reviewClearance",
|
||||||
|
label: "Review clearance",
|
||||||
|
shortLabel: "Clearance",
|
||||||
|
description: "Approve or query the customer's clearance documents",
|
||||||
|
confirmTitle: "",
|
||||||
|
confirmDescription: "",
|
||||||
|
variant: "default",
|
||||||
|
icon: ShieldCheck,
|
||||||
|
primary: true,
|
||||||
|
};
|
||||||
|
|
||||||
function withCancel(actions: BookingActionDef[]): BookingActionDef[] {
|
function withCancel(actions: BookingActionDef[]): BookingActionDef[] {
|
||||||
return [...actions, CANCEL_ACTION];
|
return [...actions, CANCEL_ACTION];
|
||||||
}
|
}
|
||||||
@@ -261,6 +277,7 @@ const ACTION_PERMISSION: Partial<Record<BookingActionId, string>> = {
|
|||||||
rejectApproval: FREIGHT_PERMS.bookings.rejectApproval,
|
rejectApproval: FREIGHT_PERMS.bookings.rejectApproval,
|
||||||
viewContract: FREIGHT_PERMS.bookings.view,
|
viewContract: FREIGHT_PERMS.bookings.view,
|
||||||
signContractStaff: FREIGHT_PERMS.bookings.signStaff,
|
signContractStaff: FREIGHT_PERMS.bookings.signStaff,
|
||||||
|
reviewClearance: FREIGHT_PERMS.bookings.reviewDocuments,
|
||||||
startTransit: FREIGHT_PERMS.bookings.operations,
|
startTransit: FREIGHT_PERMS.bookings.operations,
|
||||||
complete: FREIGHT_PERMS.bookings.operations,
|
complete: FREIGHT_PERMS.bookings.operations,
|
||||||
operationAccept: FREIGHT_PERMS.bookings.operations,
|
operationAccept: FREIGHT_PERMS.bookings.operations,
|
||||||
@@ -359,6 +376,14 @@ export function getBookingActions(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
case "AWAITING_DOCUMENTS":
|
||||||
|
case "DOCUMENTS_UNDER_REVIEW":
|
||||||
|
// Marketing reviews non-customs clearance here; customs bookings are
|
||||||
|
// handled in the Global Logistics clearance queue, not the booking list.
|
||||||
|
actions = ctx.customsClearingEnabled
|
||||||
|
? [CANCEL_ACTION]
|
||||||
|
: withCancel([REVIEW_CLEARANCE_ACTION]);
|
||||||
|
break;
|
||||||
case "OPERATION_REQUEST_PENDING":
|
case "OPERATION_REQUEST_PENDING":
|
||||||
actions = withCancel(OPERATION_REVIEW_ACTIONS);
|
actions = withCancel(OPERATION_REVIEW_ACTIONS);
|
||||||
break;
|
break;
|
||||||
@@ -446,11 +471,17 @@ export function isAllocateAction(id: BookingActionId): boolean {
|
|||||||
return id === "allocateBooking";
|
return id === "allocateBooking";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the booking detail on the Clearance tab without a confirm dialog. */
|
||||||
|
export function isClearanceNavAction(id: BookingActionId): boolean {
|
||||||
|
return id === "reviewClearance";
|
||||||
|
}
|
||||||
|
|
||||||
export function listRowHasActions(
|
export function listRowHasActions(
|
||||||
row: {
|
row: {
|
||||||
status: BookingStatus;
|
status: BookingStatus;
|
||||||
paymentCurrency: string;
|
paymentCurrency: string;
|
||||||
approvalSteps?: BookingApprovalStep[] | null;
|
approvalSteps?: BookingApprovalStep[] | null;
|
||||||
|
customsClearingEnabled?: boolean;
|
||||||
},
|
},
|
||||||
user?: AuthUser | null,
|
user?: AuthUser | null,
|
||||||
): boolean {
|
): boolean {
|
||||||
@@ -461,6 +492,7 @@ export function listRowHasActions(
|
|||||||
reference: "",
|
reference: "",
|
||||||
approvalSteps: row.approvalSteps ?? undefined,
|
approvalSteps: row.approvalSteps ?? undefined,
|
||||||
schedulingStatus: row.status,
|
schedulingStatus: row.status,
|
||||||
|
customsClearingEnabled: row.customsClearingEnabled,
|
||||||
},
|
},
|
||||||
user,
|
user,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
|
|||||||
governmentInstitution: booking.governmentInstitution ?? null,
|
governmentInstitution: booking.governmentInstitution ?? null,
|
||||||
consolidationPartnerId: booking.consolidationPartnerId ?? null,
|
consolidationPartnerId: booking.consolidationPartnerId ?? null,
|
||||||
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
|
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
|
||||||
|
customsClearingEnabled: booking.customsClearingEnabled ?? false,
|
||||||
createdAt: booking.createdAt,
|
createdAt: booking.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { ArrowLeft, FileSignature, Package } from "lucide-react";
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
FileSignature,
|
||||||
|
LayoutGrid,
|
||||||
|
Package,
|
||||||
|
ShieldCheck,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
Stack,
|
Stack,
|
||||||
Grid,
|
Grid,
|
||||||
Center,
|
Center,
|
||||||
Loader,
|
Loader,
|
||||||
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
Paper,
|
Paper,
|
||||||
Button,
|
Button,
|
||||||
@@ -34,6 +41,7 @@ import {
|
|||||||
import { WarehouseInfoCard } from "@/components/warehouses";
|
import { WarehouseInfoCard } from "@/components/warehouses";
|
||||||
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
import { getStatusMeta } from "@/features/bookings/booking-status.config";
|
||||||
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
import { toBookingListRow } from "@/features/bookings/mapBookingListRow";
|
||||||
|
import type { BookingDetail } from "@/types/booking";
|
||||||
import { downloadBookingFile } from "@/services/files.service";
|
import { downloadBookingFile } from "@/services/files.service";
|
||||||
import {
|
import {
|
||||||
useBookingDetail,
|
useBookingDetail,
|
||||||
@@ -53,6 +61,7 @@ const SIGNATURE_FILE_CODES = new Set([
|
|||||||
export default function BookingRequestDetailPage() {
|
export default function BookingRequestDetailPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const {
|
const {
|
||||||
data: booking,
|
data: booking,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -143,6 +152,23 @@ export default function BookingRequestDetailPage() {
|
|||||||
booking.status === "PENDING_APPROVAL" ||
|
booking.status === "PENDING_APPROVAL" ||
|
||||||
booking.status === "APPROVED_PENDING_SIGNATURE";
|
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,
|
||||||
|
);
|
||||||
|
const requestedTab = searchParams.get("tab");
|
||||||
|
const activeTab =
|
||||||
|
requestedTab === "clearance" && showClearanceTab ? "clearance" : "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 (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Breadcrumbs
|
<Breadcrumbs
|
||||||
@@ -173,37 +199,52 @@ export default function BookingRequestDetailPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<Grid gap="lg">
|
<Grid gap="lg">
|
||||||
{/* LEFT — primary content */}
|
{/* LEFT — primary content, split into tabs to keep each view focused */}
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
<Stack gap="lg">
|
{showClearanceTab ? (
|
||||||
{/* Non-customs clearance is reviewed here by Marketing; customs
|
<Tabs
|
||||||
bookings are handled in the Global Logistics clearance queue. */}
|
value={activeTab}
|
||||||
{!booking.customsClearingEnabled &&
|
onChange={setActiveTab}
|
||||||
["AWAITING_DOCUMENTS", "DOCUMENTS_UNDER_REVIEW"].includes(
|
variant="pills"
|
||||||
booking.status,
|
color="edr-blue"
|
||||||
) ? (
|
keepMounted={false}
|
||||||
<ClearanceReviewSection
|
>
|
||||||
bookingId={booking.id}
|
<Tabs.List mb="lg">
|
||||||
onChanged={() => refetch()}
|
<Tabs.Tab
|
||||||
/>
|
value="overview"
|
||||||
) : null}
|
leftSection={<LayoutGrid size={16} />}
|
||||||
<BookingRouteServiceCard
|
>
|
||||||
|
Overview
|
||||||
|
</Tabs.Tab>
|
||||||
|
<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>
|
||||||
|
<Tabs.Panel value="clearance">
|
||||||
|
<ClearanceReviewSection
|
||||||
|
bookingId={booking.id}
|
||||||
|
onChanged={() => refetch()}
|
||||||
|
/>
|
||||||
|
</Tabs.Panel>
|
||||||
|
</Tabs>
|
||||||
|
) : (
|
||||||
|
<OverviewPanel
|
||||||
booking={booking}
|
booking={booking}
|
||||||
originLabel={row.originLabel}
|
row={row}
|
||||||
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={handleDownloadFile}
|
onDownload={handleDownloadFile}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
)}
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
|
|
||||||
{/* RIGHT — sticky action / summary rail */}
|
{/* RIGHT — sticky action / summary rail */}
|
||||||
@@ -245,3 +286,35 @@ export default function BookingRequestDetailPage() {
|
|||||||
</PageContainer>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Group justify="center" py={80} gap={10}>
|
<Group justify="center" py={80} gap={10}>
|
||||||
<Loader color="edr-green" />
|
<Loader color="edr-blue" />
|
||||||
<Text c="dimmed">Loading clearance…</Text>
|
<Text c="dimmed">Loading clearance…</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
@@ -103,7 +103,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
clearance.allApproved ? (
|
clearance.allApproved ? (
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-green"
|
color="edr-blue"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
leftSection={<CheckCircle2 size={13} />}
|
leftSection={<CheckCircle2 size={13} />}
|
||||||
>
|
>
|
||||||
@@ -112,7 +112,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
) : (
|
) : (
|
||||||
<Badge
|
<Badge
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-blue"
|
color="edr-accent"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
leftSection={<Clock size={13} />}
|
leftSection={<Clock size={13} />}
|
||||||
>
|
>
|
||||||
@@ -133,13 +133,13 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
{/* RIGHT — sticky progress gauge */}
|
{/* RIGHT — sticky progress gauge */}
|
||||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||||
<Box style={{ position: "sticky", top: 24 }}>
|
<Box style={{ position: "sticky", top: 24 }}>
|
||||||
<SectionCard icon={PackageCheck} title="Review progress">
|
<SectionCard icon={PackageCheck} title="Review progress" accent="blue">
|
||||||
<Stack align="center" gap="sm">
|
<Stack align="center" gap="sm">
|
||||||
<RingProgress
|
<RingProgress
|
||||||
size={140}
|
size={140}
|
||||||
thickness={12}
|
thickness={12}
|
||||||
roundCaps
|
roundCaps
|
||||||
sections={[{ value: stats.pct, color: "edr-green" }]}
|
sections={[{ value: stats.pct, color: "edr-blue" }]}
|
||||||
label={
|
label={
|
||||||
<Stack gap={0} align="center">
|
<Stack gap={0} align="center">
|
||||||
<Text fw={800} fz={26} lh={1}>
|
<Text fw={800} fz={26} lh={1}>
|
||||||
@@ -153,7 +153,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
/>
|
/>
|
||||||
<Group gap="lg" justify="center">
|
<Group gap="lg" justify="center">
|
||||||
<ProgressStat
|
<ProgressStat
|
||||||
color="edr-green"
|
color="edr-blue"
|
||||||
label="Approved"
|
label="Approved"
|
||||||
value={stats.approved}
|
value={stats.approved}
|
||||||
/>
|
/>
|
||||||
@@ -163,7 +163,7 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
value={stats.queried}
|
value={stats.queried}
|
||||||
/>
|
/>
|
||||||
<ProgressStat
|
<ProgressStat
|
||||||
color="edr-slate"
|
color="edr-accent"
|
||||||
label="Pending"
|
label="Pending"
|
||||||
value={stats.pending}
|
value={stats.pending}
|
||||||
/>
|
/>
|
||||||
@@ -199,7 +199,7 @@ function ClearanceHero({
|
|||||||
<Paper withBorder radius="md" p="lg">
|
<Paper withBorder radius="md" p="lg">
|
||||||
<Group justify="space-between" align="flex-start" wrap="wrap" gap="lg">
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="lg">
|
||||||
<Group gap="md" wrap="nowrap" style={{ minWidth: 0 }}>
|
<Group gap="md" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||||
<ThemeIcon variant="light" color="edr-green" radius="md" size={52}>
|
<ThemeIcon variant="light" color="edr-blue" radius="md" size={52}>
|
||||||
<ShieldCheck size={26} />
|
<ShieldCheck size={26} />
|
||||||
</ThemeIcon>
|
</ThemeIcon>
|
||||||
<Box style={{ minWidth: 0 }}>
|
<Box style={{ minWidth: 0 }}>
|
||||||
@@ -219,7 +219,7 @@ function ClearanceHero({
|
|||||||
<Badge
|
<Badge
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
color="edr-green"
|
color="edr-accent"
|
||||||
radius="sm"
|
radius="sm"
|
||||||
leftSection={<ShieldCheck size={12} />}
|
leftSection={<ShieldCheck size={12} />}
|
||||||
>
|
>
|
||||||
@@ -248,7 +248,7 @@ function ClearanceHero({
|
|||||||
{stats.approved}/{stats.total}
|
{stats.approved}/{stats.total}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<Progress value={stats.pct} color="edr-green" radius="xl" size="md" />
|
<Progress value={stats.pct} color="edr-blue" radius="xl" size="md" />
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|||||||
@@ -221,5 +221,6 @@ export interface BookingListRow {
|
|||||||
governmentInstitution?: string | null;
|
governmentInstitution?: string | null;
|
||||||
consolidationPartnerId?: string | null;
|
consolidationPartnerId?: string | null;
|
||||||
consolidationPartnerReference?: string | null;
|
consolidationPartnerReference?: string | null;
|
||||||
|
customsClearingEnabled?: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user