Merge branch 'freight_feature/profile' of github.com:Tria-plc/edr-platform into freight_feature/profile

This commit is contained in:
marshal
2026-06-25 05:10:44 +03:00
7 changed files with 168 additions and 54 deletions

View File

@@ -8,6 +8,7 @@ import { useAuth } from "@/auth/useAuth";
import {
getNextPendingApprovalStep,
isAllocateAction,
isClearanceNavAction,
isContractNavAction,
listRowHasActions,
type BookingActionContext,
@@ -38,6 +39,7 @@ export function BookingActionsMenu({
reference: row.reference,
approvalSteps: row.approvalSteps,
schedulingStatus: row.schedulingStatus,
customsClearingEnabled: row.customsClearingEnabled,
};
const flow = useBookingActionDialog(row.id, context);
@@ -46,10 +48,15 @@ export function BookingActionsMenu({
const goToContract = () =>
navigate(`/dashboard/booking-requests/${row.id}/contract`);
const goToClearanceTab = () =>
navigate(`/dashboard/booking-requests/${row.id}?tab=clearance`);
const handleAction = (action: (typeof actions)[number]) => {
onSuppressRowClick?.();
if (isContractNavAction(action.id)) {
goToContract();
} else if (isClearanceNavAction(action.id)) {
goToClearanceTab();
} else if (isAllocateAction(action.id)) {
onAllocateBooking?.();
} else {

View File

@@ -44,9 +44,9 @@ const STATUS_META: Record<
Freight.DocumentReviewStatus,
{ label: string; color: string }
> = {
APPROVED: { label: "Approved", color: "edr-green" },
APPROVED: { label: "Approved", color: "edr-blue" },
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) {
return (
<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>
</Group>
);
@@ -165,15 +165,15 @@ export function ClearanceReviewSection({
<Box>
<Progress
value={stats.pct}
color="edr-green"
color="edr-blue"
radius="xl"
size="sm"
mb={6}
/>
<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="edr-slate" label="Pending" value={stats.pending} />
<StatPill color="edr-accent" label="Pending" value={stats.pending} />
</Group>
</Box>
)}
@@ -219,13 +219,13 @@ export function ClearanceReviewSection({
icon={Upload}
title="Customs output documents"
subtitle="Upload the cleared/customs paperwork to hand back to the customer."
accent="edr-blue"
accent="edr-accent"
>
<Stack gap={10}>
{glDocs.map((doc) => (
<Group key={doc.fileKey} justify="space-between" wrap="nowrap">
<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>
{doc.label}
{doc.required ? " *" : ""}
@@ -239,7 +239,7 @@ export function ClearanceReviewSection({
href={doc.file.url}
target="_blank"
rel="noreferrer"
c="edr-blue"
c="edr-accent"
style={{ display: "flex" }}
>
<Download size={15} />
@@ -261,7 +261,7 @@ export function ClearanceReviewSection({
{...props}
size="compact-xs"
variant="light"
color="edr-green"
color="edr-accent"
leftSection={<Upload size={13} />}
>
{outputFiles[doc.fileKey] ? "Selected" : "Upload"}
@@ -275,7 +275,7 @@ export function ClearanceReviewSection({
<Group justify="flex-end" mt="md">
<Button
variant="light"
color="edr-green"
color="edr-accent"
radius="md"
leftSection={<Upload size={15} />}
disabled={Object.keys(outputFiles).length === 0}
@@ -301,7 +301,7 @@ export function ClearanceReviewSection({
<Group gap={8} wrap="nowrap" style={{ minWidth: 0 }}>
<ThemeIcon
variant="light"
color={clearance.allApproved ? "edr-green" : "gray"}
color={clearance.allApproved ? "edr-blue" : "gray"}
radius="md"
size={28}
>
@@ -314,7 +314,7 @@ export function ClearanceReviewSection({
</Text>
</Group>
<Button
color="edr-green"
color="edr-blue"
radius="md"
leftSection={<CheckCircle2 size={16} />}
disabled={!clearance.allApproved}
@@ -391,7 +391,7 @@ function DocReviewCard({
status === "QUERIED"
? "var(--mantine-color-red-2)"
: status === "APPROVED"
? "var(--mantine-color-edr-green-2)"
? "var(--mantine-color-edr-blue-2)"
: "var(--mantine-color-edr-border-6)",
}}
>
@@ -471,7 +471,7 @@ function DocReviewCard({
</Button>
<Button
size="compact-sm"
color="edr-green"
color="edr-blue"
radius="md"
leftSection={<CheckCircle2 size={14} />}
disabled={busy}

View File

@@ -32,6 +32,7 @@ export type BookingActionId =
| "rejectApproval"
| "viewContract"
| "signContractStaff"
| "reviewClearance"
| "allocateBooking"
| "startTransit"
| "complete"
@@ -69,6 +70,7 @@ export type BookingActionContext = Pick<
| "approvalSteps"
| "reference"
| "schedulingStatus"
| "customsClearingEnabled"
>;
const ALLOCATABLE_SCHEDULING_STATUSES = new Set([
@@ -250,6 +252,20 @@ const SIGN_CONTRACT_STAFF_ACTION: BookingActionDef = {
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[] {
return [...actions, CANCEL_ACTION];
}
@@ -261,6 +277,7 @@ const ACTION_PERMISSION: Partial<Record<BookingActionId, string>> = {
rejectApproval: FREIGHT_PERMS.bookings.rejectApproval,
viewContract: FREIGHT_PERMS.bookings.view,
signContractStaff: FREIGHT_PERMS.bookings.signStaff,
reviewClearance: FREIGHT_PERMS.bookings.reviewDocuments,
startTransit: FREIGHT_PERMS.bookings.operations,
complete: FREIGHT_PERMS.bookings.operations,
operationAccept: FREIGHT_PERMS.bookings.operations,
@@ -359,6 +376,14 @@ export function getBookingActions(
},
];
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":
actions = withCancel(OPERATION_REVIEW_ACTIONS);
break;
@@ -446,11 +471,17 @@ export function isAllocateAction(id: BookingActionId): boolean {
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(
row: {
status: BookingStatus;
paymentCurrency: string;
approvalSteps?: BookingApprovalStep[] | null;
customsClearingEnabled?: boolean;
},
user?: AuthUser | null,
): boolean {
@@ -461,6 +492,7 @@ export function listRowHasActions(
reference: "",
approvalSteps: row.approvalSteps ?? undefined,
schedulingStatus: row.status,
customsClearingEnabled: row.customsClearingEnabled,
},
user,
);

View File

@@ -44,6 +44,7 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
governmentInstitution: booking.governmentInstitution ?? null,
consolidationPartnerId: booking.consolidationPartnerId ?? null,
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
customsClearingEnabled: booking.customsClearingEnabled ?? false,
createdAt: booking.createdAt,
};
}

View File

@@ -1,11 +1,18 @@
import { useNavigate, useParams } from "react-router-dom";
import { ArrowLeft, FileSignature, Package } from "lucide-react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import {
ArrowLeft,
FileSignature,
LayoutGrid,
Package,
ShieldCheck,
} from "lucide-react";
import {
Container,
Stack,
Grid,
Center,
Loader,
Tabs,
Text,
Paper,
Button,
@@ -34,6 +41,7 @@ import {
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,
@@ -53,6 +61,7 @@ const SIGNATURE_FILE_CODES = new Set([
export default function BookingRequestDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams();
const {
data: booking,
isLoading,
@@ -143,6 +152,23 @@ export default function BookingRequestDetailPage() {
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,
);
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 (
<PageContainer>
<Breadcrumbs
@@ -173,37 +199,52 @@ export default function BookingRequestDetailPage() {
)}
<Grid gap="lg">
{/* LEFT — primary content */}
{/* LEFT — primary content, split into tabs to keep each view focused */}
<Grid.Col span={{ base: 12, lg: 8 }}>
<Stack gap="lg">
{/* Non-customs clearance is reviewed here by Marketing; customs
bookings are handled in the Global Logistics clearance queue. */}
{!booking.customsClearingEnabled &&
["AWAITING_DOCUMENTS", "DOCUMENTS_UNDER_REVIEW"].includes(
booking.status,
) ? (
<ClearanceReviewSection
bookingId={booking.id}
onChanged={() => refetch()}
/>
) : null}
<BookingRouteServiceCard
{showClearanceTab ? (
<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>
<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}
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 ?? ""),
)}
row={row}
onDownload={handleDownloadFile}
/>
</Stack>
)}
</Grid.Col>
{/* RIGHT — sticky action / summary rail */}
@@ -245,3 +286,35 @@ export default function BookingRequestDetailPage() {
</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>
);
}

View File

@@ -64,7 +64,7 @@ export default function DocumentClearanceDetailPage() {
return (
<PageContainer>
<Group justify="center" py={80} gap={10}>
<Loader color="edr-green" />
<Loader color="edr-blue" />
<Text c="dimmed">Loading clearance</Text>
</Group>
</PageContainer>
@@ -103,7 +103,7 @@ export default function DocumentClearanceDetailPage() {
clearance.allApproved ? (
<Badge
variant="light"
color="edr-green"
color="edr-blue"
radius="sm"
leftSection={<CheckCircle2 size={13} />}
>
@@ -112,7 +112,7 @@ export default function DocumentClearanceDetailPage() {
) : (
<Badge
variant="light"
color="edr-blue"
color="edr-accent"
radius="sm"
leftSection={<Clock size={13} />}
>
@@ -133,13 +133,13 @@ export default function DocumentClearanceDetailPage() {
{/* RIGHT — sticky progress gauge */}
<Grid.Col span={{ base: 12, lg: 4 }}>
<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">
<RingProgress
size={140}
thickness={12}
roundCaps
sections={[{ value: stats.pct, color: "edr-green" }]}
sections={[{ value: stats.pct, color: "edr-blue" }]}
label={
<Stack gap={0} align="center">
<Text fw={800} fz={26} lh={1}>
@@ -153,7 +153,7 @@ export default function DocumentClearanceDetailPage() {
/>
<Group gap="lg" justify="center">
<ProgressStat
color="edr-green"
color="edr-blue"
label="Approved"
value={stats.approved}
/>
@@ -163,7 +163,7 @@ export default function DocumentClearanceDetailPage() {
value={stats.queried}
/>
<ProgressStat
color="edr-slate"
color="edr-accent"
label="Pending"
value={stats.pending}
/>
@@ -199,7 +199,7 @@ function ClearanceHero({
<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}>
<ThemeIcon variant="light" color="edr-blue" radius="md" size={52}>
<ShieldCheck size={26} />
</ThemeIcon>
<Box style={{ minWidth: 0 }}>
@@ -219,7 +219,7 @@ function ClearanceHero({
<Badge
size="sm"
variant="light"
color="edr-green"
color="edr-accent"
radius="sm"
leftSection={<ShieldCheck size={12} />}
>
@@ -248,7 +248,7 @@ function ClearanceHero({
{stats.approved}/{stats.total}
</Text>
</Group>
<Progress value={stats.pct} color="edr-green" radius="xl" size="md" />
<Progress value={stats.pct} color="edr-blue" radius="xl" size="md" />
</Box>
</Group>
</Paper>

View File

@@ -221,5 +221,6 @@ export interface BookingListRow {
governmentInstitution?: string | null;
consolidationPartnerId?: string | null;
consolidationPartnerReference?: string | null;
customsClearingEnabled?: boolean;
createdAt: string;
}