mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: enhance contract clearance functionality and UI, update routing and status handling
This commit is contained in:
@@ -479,13 +479,19 @@ export class ContractClearanceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* GL ET queue: customs (Path B) contracts awaiting pre-booking document review.
|
||||
* GL ET clearance hub: every customs (Path B) contract that still needs
|
||||
* customs clearance — awaiting the customer's documents, under GL review, or
|
||||
* finalized and waiting for the customer to create the booking in the portal.
|
||||
*/
|
||||
async queue(filter: FilterContractDto): Promise<PaginatedContracts> {
|
||||
return this.contractsRepository.findAllPaginated({
|
||||
page: filter.page ?? 1,
|
||||
pageSize: filter.pageSize ?? 100,
|
||||
statuses: ['CLEARANCE_UNDER_REVIEW'],
|
||||
statuses: [
|
||||
'AWAITING_CLEARANCE_DOCUMENTS',
|
||||
'CLEARANCE_UNDER_REVIEW',
|
||||
'CLEARANCE_READY_FOR_BOOKING',
|
||||
],
|
||||
customsClearingEnabled: true,
|
||||
sortBy: filter.sortBy,
|
||||
sortOrder: filter.sortOrder,
|
||||
|
||||
@@ -29,12 +29,12 @@ import LoginPage from "./pages/auth/LoginPage";
|
||||
import BookingContractPage from "./pages/bookings/BookingContractPage";
|
||||
import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage";
|
||||
import BookingRequestsPage from "./pages/bookings/BookingRequestsPage";
|
||||
import DocumentClearanceDetailPage from "./pages/bookings/DocumentClearanceDetailPage";
|
||||
import DocumentClearanceListPage from "./pages/bookings/DocumentClearanceListPage";
|
||||
import NewBookingPage from "./pages/bookings/NewBookingPage";
|
||||
import ContractRequestsPage from "./pages/contracts/ContractRequestsPage";
|
||||
import ContractRequestDetailPage from "./pages/contracts/ContractRequestDetailPage";
|
||||
import ContractViewPage from "./pages/contracts/ContractViewPage";
|
||||
import ContractClearanceListPage from "./pages/contracts/ContractClearanceListPage";
|
||||
import ContractClearanceDetailPage from "./pages/contracts/ContractClearanceDetailPage";
|
||||
import GlCreateBookingForm from "./components/contracts/GlCreateBookingForm";
|
||||
import BookingMilestonesPage from "./pages/contracts/BookingMilestonesPage";
|
||||
import CustomerDetailPage from "./pages/customers/CustomerDetailPage";
|
||||
@@ -128,9 +128,9 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
items: [
|
||||
{
|
||||
label: "Document Clearance",
|
||||
href: "/dashboard/clearance",
|
||||
href: "/dashboard/contracts/clearance",
|
||||
icon: <ShieldCheck />,
|
||||
permission: FREIGHT_PERMS.bookings.reviewDocuments,
|
||||
permission: FREIGHT_PERMS.contracts.clearanceReview,
|
||||
},
|
||||
{
|
||||
label: "Train Schedules",
|
||||
@@ -409,21 +409,14 @@ const App = () => {
|
||||
path="booking-requests/:id/contract"
|
||||
element={<BookingContractPage />}
|
||||
/>
|
||||
{/* Legacy booking-based clearance URLs → the contract clearance hub. */}
|
||||
<Route
|
||||
path="clearance"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.reviewDocuments}>
|
||||
<DocumentClearanceListPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
element={<Navigate to="/dashboard/contracts/clearance" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="clearance/:id"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.bookings.reviewDocuments}>
|
||||
<DocumentClearanceDetailPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
element={<Navigate to="/dashboard/contracts/clearance" replace />}
|
||||
/>
|
||||
|
||||
{/* Contracts (Path A/B) */}
|
||||
@@ -451,17 +444,27 @@ const App = () => {
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/* GL (Path B) contract clearance review hub */}
|
||||
<Route
|
||||
path="contracts/clearance"
|
||||
element={<Navigate to="/dashboard/clearance" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="contracts/ops-clearance"
|
||||
element={<Navigate to="/dashboard/clearance?mode=ops" replace />}
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.contracts.clearanceReview}>
|
||||
<ContractClearanceListPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="contracts/clearance/:id"
|
||||
element={<Navigate to="/dashboard/clearance" replace />}
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.contracts.clearanceReview}>
|
||||
<ContractClearanceDetailPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/* Path A ops queue out of scope for now → fold into the GL hub. */}
|
||||
<Route
|
||||
path="contracts/ops-clearance"
|
||||
element={<Navigate to="/dashboard/contracts/clearance" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="contracts/:id/create-booking"
|
||||
|
||||
@@ -112,7 +112,9 @@ export function ContractClearanceReviewSection({
|
||||
);
|
||||
const glDocs = useMemo(
|
||||
() =>
|
||||
(clearance?.documents ?? []).filter((d) => d.uploadedBy === "gl"),
|
||||
(clearance?.documents ?? []).filter(
|
||||
(d) => d.uploadedBy === "gl_et" || d.uploadedBy === "gl_dj",
|
||||
),
|
||||
[clearance],
|
||||
);
|
||||
|
||||
|
||||
@@ -90,6 +90,20 @@ export default function GlCreateBookingForm() {
|
||||
return [...sizes];
|
||||
}, [contract?.cargoScope]);
|
||||
|
||||
// Bulk cargo types declared on the contract scope (prefill, no free-text).
|
||||
const bulkCargoOptions = useMemo(() => {
|
||||
const seen = new Map<string, string>();
|
||||
(contract?.cargoScope ?? []).forEach((s) => {
|
||||
if (s.containerSize || !s.cargoTypeId) return;
|
||||
if (!seen.has(s.cargoTypeId)) {
|
||||
seen.set(s.cargoTypeId, s.cargoFreeText?.trim() || s.cargoTypeId);
|
||||
}
|
||||
});
|
||||
return [...seen.entries()].map(([value, label]) => ({ value, label }));
|
||||
}, [contract?.cargoScope]);
|
||||
|
||||
const defaultBulkCargoTypeId = bulkCargoOptions[0]?.value ?? "";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
@@ -105,7 +119,7 @@ export default function GlCreateBookingForm() {
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Contract not found"
|
||||
backTo="/dashboard/contracts/clearance"
|
||||
backTo="/dashboard/clearance"
|
||||
/>
|
||||
</PageContainer>
|
||||
);
|
||||
@@ -151,7 +165,12 @@ export default function GlCreateBookingForm() {
|
||||
const addBulkLine = () =>
|
||||
setBulkLines((prev) => [
|
||||
...prev,
|
||||
{ cargoTypeId: "", cargoWeightTons: "", itemCount: "", hazardousQuantity: "" },
|
||||
{
|
||||
cargoTypeId: defaultBulkCargoTypeId,
|
||||
cargoWeightTons: "",
|
||||
itemCount: "",
|
||||
hazardousQuantity: "",
|
||||
},
|
||||
]);
|
||||
const removeBulkLine = (idx: number) =>
|
||||
setBulkLines((prev) => prev.filter((_, i) => i !== idx));
|
||||
@@ -214,12 +233,12 @@ export default function GlCreateBookingForm() {
|
||||
<PageHeader
|
||||
title="Create booking (GL)"
|
||||
subtitle={`Enter the shipment details on behalf of the customer for contract ${contract.reference}.`}
|
||||
backTo={`/dashboard/contracts/clearance/${contract.id}`}
|
||||
backTo={`/dashboard/clearance/${contract.id}`}
|
||||
breadcrumbs={[
|
||||
{ label: "Contract Clearance", href: "/dashboard/contracts/clearance" },
|
||||
{ label: "Document Clearance", href: "/dashboard/clearance" },
|
||||
{
|
||||
label: contract.reference,
|
||||
href: `/dashboard/contracts/clearance/${contract.id}`,
|
||||
href: `/dashboard/clearance/${contract.id}`,
|
||||
},
|
||||
{ label: "Create booking" },
|
||||
]}
|
||||
@@ -486,14 +505,28 @@ export default function GlCreateBookingForm() {
|
||||
</Group>
|
||||
<Grid gap="sm">
|
||||
<Grid.Col span={{ base: 12, sm: 6 }}>
|
||||
<TextInput
|
||||
label="Cargo type id"
|
||||
placeholder="Optional"
|
||||
value={line.cargoTypeId}
|
||||
onChange={(e) =>
|
||||
patchBulk(idx, { cargoTypeId: e.currentTarget.value })
|
||||
}
|
||||
/>
|
||||
{bulkCargoOptions.length > 0 ? (
|
||||
<Select
|
||||
label="Cargo type"
|
||||
placeholder="Select cargo type"
|
||||
value={line.cargoTypeId || null}
|
||||
onChange={(v) =>
|
||||
patchBulk(idx, { cargoTypeId: v ?? "" })
|
||||
}
|
||||
data={bulkCargoOptions}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
label="Cargo type id"
|
||||
placeholder="Optional"
|
||||
value={line.cargoTypeId}
|
||||
onChange={(e) =>
|
||||
patchBulk(idx, {
|
||||
cargoTypeId: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 6, sm: 6 }}>
|
||||
<NumberInput
|
||||
@@ -545,9 +578,7 @@ export default function GlCreateBookingForm() {
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/contracts/clearance/${contract.id}`)
|
||||
}
|
||||
onClick={() => navigate(`/dashboard/clearance/${contract.id}`)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
@@ -54,7 +54,6 @@ import {
|
||||
|
||||
type ViewMode = "table" | "cards";
|
||||
type PageTab = "queue" | "history";
|
||||
type ClearanceMode = "gl" | "ops";
|
||||
|
||||
const CLEARANCE_REVIEW_STATUS = "DOCUMENTS_UNDER_REVIEW";
|
||||
const CLEARANCE_HISTORY_STATUS = "CLEARANCE_READY";
|
||||
@@ -131,12 +130,6 @@ function DirectionIcon({ direction }: { direction: string }) {
|
||||
|
||||
export default function DocumentClearanceListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// "mode=ops" query param lets the old ops-clearance redirect land on the right tab
|
||||
const initialMode: ClearanceMode =
|
||||
searchParams.get("mode") === "ops" ? "ops" : "gl";
|
||||
const [clearanceMode, setClearanceMode] = useState<ClearanceMode>(initialMode);
|
||||
const [pageTab, setPageTab] = useState<PageTab>("queue");
|
||||
const [activeTab, setActiveTab] = useState<ClearanceTabKey>("all");
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -146,7 +139,7 @@ export default function DocumentClearanceListPage() {
|
||||
const isHistory = pageTab === "history";
|
||||
|
||||
const { data, isLoading, isError, isFetching, refetch } = useQuery({
|
||||
queryKey: ["clearance", "list", clearanceMode, isHistory],
|
||||
queryKey: ["clearance", "list", isHistory],
|
||||
queryFn: () =>
|
||||
bookingsService.list({
|
||||
status: isHistory ? CLEARANCE_HISTORY_STATUS : CLEARANCE_REVIEW_STATUS,
|
||||
@@ -155,22 +148,18 @@ export default function DocumentClearanceListPage() {
|
||||
});
|
||||
|
||||
const allRows = useMemo(() => {
|
||||
const rows = (data?.items ?? []).map(toClearanceRow);
|
||||
const filtered =
|
||||
clearanceMode === "gl"
|
||||
? rows.filter((r) => r.hasCustoms)
|
||||
: rows.filter((r) => !r.hasCustoms);
|
||||
// GL clearance queue: customs bookings only
|
||||
const rows = (data?.items ?? []).map(toClearanceRow).filter((r) => r.hasCustoms);
|
||||
|
||||
if (isHistory) {
|
||||
// Latest cleared first — fall back to updatedAt
|
||||
return [...filtered].sort((a, b) => {
|
||||
return [...rows].sort((a, b) => {
|
||||
const ta = new Date(a.updatedAt || 0).getTime();
|
||||
const tb = new Date(b.updatedAt || 0).getTime();
|
||||
return tb - ta;
|
||||
});
|
||||
}
|
||||
return filtered;
|
||||
}, [data?.items, clearanceMode, isHistory]);
|
||||
return rows;
|
||||
}, [data?.items, isHistory]);
|
||||
|
||||
const tabCounts = useMemo(
|
||||
() => ({
|
||||
@@ -209,15 +198,6 @@ export default function DocumentClearanceListPage() {
|
||||
[navigate],
|
||||
);
|
||||
|
||||
const handleModeChange = (mode: ClearanceMode) => {
|
||||
setClearanceMode(mode);
|
||||
setPageTab("queue");
|
||||
setActiveTab("all");
|
||||
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
// clear the ?mode= param after first use
|
||||
setSearchParams({});
|
||||
};
|
||||
|
||||
const statusBadge = isHistory ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
@@ -302,7 +282,7 @@ export default function DocumentClearanceListPage() {
|
||||
{
|
||||
id: "status",
|
||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||
cell: ({ row }) =>
|
||||
cell: () =>
|
||||
isHistory ? (
|
||||
<Badge size="sm" variant="light" color="edr-green" radius="sm">
|
||||
Cleared
|
||||
@@ -347,18 +327,7 @@ export default function DocumentClearanceListPage() {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Mode toggle: GL Customs vs Self-Clearance (Operations) */}
|
||||
<Group>
|
||||
<SegmentedControl
|
||||
value={clearanceMode}
|
||||
onChange={(v) => handleModeChange(v as ClearanceMode)}
|
||||
data={[
|
||||
{ value: "gl", label: "GL Clearance" },
|
||||
{ value: "ops", label: "Self-Clearance" },
|
||||
]}
|
||||
radius="md"
|
||||
color="edr-green"
|
||||
/>
|
||||
<SegmentedControl
|
||||
value={pageTab}
|
||||
onChange={(v) => {
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
PackageCheck,
|
||||
PackagePlus,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
|
||||
@@ -32,15 +31,9 @@ import { ContractClearanceReviewSection } from "@/components/contracts/ContractC
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { useContractDetail } from "@/hooks/contracts/useContracts";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { canCreateContractBooking } from "@/lib/permissions";
|
||||
import { Button } from "@mantine/core";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function ContractClearanceDetailPage() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: contract } = useContractDetail(id);
|
||||
const {
|
||||
@@ -66,13 +59,9 @@ export default function ContractClearanceDetailPage() {
|
||||
}, [clearance]);
|
||||
|
||||
const reference = contract?.reference ?? "Clearance";
|
||||
// Path A (no customs): Operations reviews; the customer books in the portal —
|
||||
// there is no "Create booking" action here.
|
||||
const selfClear = clearance?.includesCustoms === false;
|
||||
const canBook =
|
||||
!selfClear &&
|
||||
clearance?.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING" &&
|
||||
canCreateContractBooking(user);
|
||||
// Customs (Path B) hub. The customer always creates the booking in the portal
|
||||
// after GL finalizes clearance — there is no GL "Create booking" action here.
|
||||
const ready = clearance?.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING";
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -90,11 +79,11 @@ export default function ContractClearanceDetailPage() {
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Clearance not found"
|
||||
backTo="/dashboard/contracts/clearance"
|
||||
backTo="/dashboard/clearance"
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: "Contract Clearance",
|
||||
href: "/dashboard/contracts/clearance",
|
||||
label: "Document Clearance",
|
||||
href: "/dashboard/clearance",
|
||||
},
|
||||
{ label: "Not found" },
|
||||
]}
|
||||
@@ -111,16 +100,25 @@ export default function ContractClearanceDetailPage() {
|
||||
<Stack gap="lg">
|
||||
<PageHeader
|
||||
title={reference}
|
||||
backTo="/dashboard/contracts/clearance"
|
||||
backTo="/dashboard/clearance"
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: "Contract Clearance",
|
||||
href: "/dashboard/contracts/clearance",
|
||||
label: "Document Clearance",
|
||||
href: "/dashboard/clearance",
|
||||
},
|
||||
{ label: reference },
|
||||
]}
|
||||
meta={
|
||||
clearance.allApproved ? (
|
||||
ready ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<PackageCheck size={13} />}
|
||||
>
|
||||
Ready — customer books
|
||||
</Badge>
|
||||
) : clearance.allApproved ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
@@ -140,29 +138,28 @@ export default function ContractClearanceDetailPage() {
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
action={
|
||||
canBook ? (
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<PackagePlus size={16} />}
|
||||
onClick={() =>
|
||||
navigate(`/dashboard/contracts/${id}/create-booking`)
|
||||
}
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<ClearanceHero contract={contract} stats={stats} />
|
||||
|
||||
{ready ? (
|
||||
<Alert
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
icon={<PackageCheck size={16} />}
|
||||
title="Clearance finalized"
|
||||
>
|
||||
Customs clearance is complete. The customer can now create the
|
||||
shipment booking from the portal — no further action is needed here.
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Grid gap="lg">
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<ContractClearanceReviewSection
|
||||
contractId={id!}
|
||||
hideSummary
|
||||
selfClear={selfClear}
|
||||
selfClear={false}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
|
||||
@@ -15,12 +15,11 @@ import {
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
ArrowRight,
|
||||
CheckCircle,
|
||||
ChevronRight,
|
||||
FileText,
|
||||
History,
|
||||
Inbox,
|
||||
LayoutGrid,
|
||||
PackageCheck,
|
||||
RefreshCw,
|
||||
Search,
|
||||
ShieldCheck,
|
||||
@@ -38,19 +37,11 @@ import {
|
||||
} from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { ContractStatusBadge } from "@/components/contracts/ContractStatusBadge";
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { KpiStrip } from "@/components/page/KpiStrip";
|
||||
import { bookingTable } from "@/components/bookings/booking-ui.styles";
|
||||
import {
|
||||
useContractClearanceHistory,
|
||||
useContractClearanceQueue,
|
||||
useOpsClearanceHistory,
|
||||
useOpsClearanceQueue,
|
||||
} from "@/hooks/contracts/useContracts";
|
||||
|
||||
type PageTab = "queue" | "history";
|
||||
import { useContractClearanceQueue } from "@/hooks/contracts/useContracts";
|
||||
|
||||
type ViewMode = "table" | "cards";
|
||||
|
||||
@@ -64,6 +55,8 @@ interface ClearanceRow {
|
||||
destinationLabel: string;
|
||||
contractKind: string;
|
||||
status: string;
|
||||
/** true once GL has finalized clearance — customer now books in the portal. */
|
||||
ready: boolean;
|
||||
}
|
||||
|
||||
function yardLabel(
|
||||
@@ -92,6 +85,7 @@ function toClearanceRow(contract: Freight.IContract): ClearanceRow {
|
||||
destinationLabel: yardLabel(last?.destinationYard),
|
||||
contractKind: contract.contractKind,
|
||||
status: contract.status,
|
||||
ready: contract.status === "CLEARANCE_READY_FOR_BOOKING",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -114,33 +108,46 @@ function DirectionIcon({ direction }: { direction: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ row }: { row: ClearanceRow }) {
|
||||
if (row.ready) {
|
||||
return (
|
||||
<Tooltip
|
||||
label="Clearance finalized — the customer creates the booking in the portal"
|
||||
withArrow
|
||||
>
|
||||
<Badge
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<PackageCheck size={12} />}
|
||||
>
|
||||
Ready — customer books
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Badge size="sm" variant="light" color="yellow" radius="sm">
|
||||
Under review
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-booking clearance queue. In `opsMode` it lists Path A self-clearance
|
||||
* contracts for the Operations team (non-customs); otherwise the GL ET/DJ
|
||||
* customs queue (Path B). Both route to the same detail page, which detects the
|
||||
* path from the contract.
|
||||
* Document Clearance hub. Lists every customs (Path B) contract that still needs
|
||||
* customs clearance — awaiting documents, under GL review, or finalized and
|
||||
* waiting for the customer to create the booking in the portal. A single list,
|
||||
* no queue/history/direction tabs.
|
||||
*/
|
||||
export default function ContractClearanceListPage({
|
||||
opsMode = false,
|
||||
}: {
|
||||
opsMode?: boolean;
|
||||
} = {}) {
|
||||
export default function ContractClearanceListPage() {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState("");
|
||||
const [view, setView] = useState<ViewMode>("table");
|
||||
const [pageTab, setPageTab] = useState<PageTab>("queue");
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
|
||||
const isHistory = pageTab === "history";
|
||||
|
||||
const glQueue = useContractClearanceQueue(!opsMode && !isHistory);
|
||||
const opsQueue = useOpsClearanceQueue(opsMode && !isHistory);
|
||||
const glHistory = useContractClearanceHistory(!opsMode && isHistory);
|
||||
const opsHistory = useOpsClearanceHistory(opsMode && isHistory);
|
||||
|
||||
const { data, isLoading, isError, isFetching, refetch } = opsMode
|
||||
? (isHistory ? opsHistory : opsQueue)
|
||||
: (isHistory ? glHistory : glQueue);
|
||||
const { data, isLoading, isError, isFetching, refetch } =
|
||||
useContractClearanceQueue(true);
|
||||
|
||||
const allRows = useMemo(
|
||||
() => (data?.items ?? []).map(toClearanceRow),
|
||||
@@ -150,8 +157,8 @@ export default function ContractClearanceListPage({
|
||||
const counts = useMemo(
|
||||
() => ({
|
||||
all: allRows.length,
|
||||
import: allRows.filter((r) => r.tradeDirection === "IMPORT").length,
|
||||
export: allRows.filter((r) => r.tradeDirection === "EXPORT").length,
|
||||
ready: allRows.filter((r) => r.ready).length,
|
||||
review: allRows.filter((r) => !r.ready).length,
|
||||
}),
|
||||
[allRows],
|
||||
);
|
||||
@@ -177,7 +184,7 @@ export default function ContractClearanceListPage({
|
||||
}, [rows, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
const openDetail = useCallback(
|
||||
(id: string) => navigate(`/dashboard/contracts/clearance/${id}`),
|
||||
(id: string) => navigate(`/dashboard/clearance/${id}`),
|
||||
[navigate],
|
||||
);
|
||||
|
||||
@@ -244,14 +251,7 @@ export default function ContractClearanceListPage({
|
||||
{
|
||||
id: "status",
|
||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||
cell: ({ row }) =>
|
||||
isHistory ? (
|
||||
<ContractStatusBadge status={row.original.status} />
|
||||
) : (
|
||||
<Badge size="sm" variant="light" color="edr-green" radius="sm">
|
||||
Under review
|
||||
</Badge>
|
||||
),
|
||||
cell: ({ row }) => <StatusBadge row={row.original} />,
|
||||
},
|
||||
{
|
||||
id: "go",
|
||||
@@ -270,20 +270,16 @@ export default function ContractClearanceListPage({
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
<PageHeader
|
||||
title={opsMode ? "Self-Clearance Review" : "Contract Clearance"}
|
||||
subtitle={
|
||||
opsMode
|
||||
? "Review the customer's own clearance documents (no EDR customs service) before they create a shipment booking."
|
||||
: "Review pre-booking clearance documents on contracts before Global Logistics creates the shipment booking."
|
||||
}
|
||||
title="Document Clearance"
|
||||
subtitle="Review pre-booking customs documents on contracts and finalize clearance. Once finalized, the customer creates the shipment booking in the portal."
|
||||
meta={
|
||||
<Badge
|
||||
variant="light"
|
||||
color={isHistory ? "gray" : "edr-green"}
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={isHistory ? <History size={13} /> : <ShieldCheck size={13} />}
|
||||
leftSection={<ShieldCheck size={13} />}
|
||||
>
|
||||
{isHistory ? `${counts.all} completed` : `${counts.all} awaiting review`}
|
||||
{counts.all} need clearance
|
||||
</Badge>
|
||||
}
|
||||
action={
|
||||
@@ -300,53 +296,28 @@ export default function ContractClearanceListPage({
|
||||
}
|
||||
/>
|
||||
|
||||
<Group>
|
||||
<SegmentedControl
|
||||
size="sm"
|
||||
radius="md"
|
||||
value={pageTab}
|
||||
onChange={(v) => {
|
||||
setPageTab(v as PageTab);
|
||||
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
}}
|
||||
data={[
|
||||
{
|
||||
value: "queue",
|
||||
label: (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Inbox size={15} />
|
||||
<Box visibleFrom="xs">Queue</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "history",
|
||||
label: (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<History size={15} />
|
||||
<Box visibleFrom="xs">History</Box>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Group>
|
||||
|
||||
<KpiStrip
|
||||
loading={isLoading}
|
||||
items={
|
||||
isHistory
|
||||
? [
|
||||
{ label: "Total cleared", value: counts.all, icon: CheckCircle, color: "edr-green" },
|
||||
{ label: "Import", value: counts.import, icon: Truck, color: "edr-green" },
|
||||
{ label: "Export", value: counts.export, icon: ShipWheel, color: "gray" },
|
||||
]
|
||||
: [
|
||||
{ label: "Awaiting review", value: counts.all, icon: Inbox, color: "edr-green" },
|
||||
{ label: "Import", value: counts.import, icon: Truck, color: "edr-green" },
|
||||
{ label: "Export", value: counts.export, icon: ShipWheel, color: "gray" },
|
||||
]
|
||||
}
|
||||
items={[
|
||||
{
|
||||
label: "Need clearance",
|
||||
value: counts.all,
|
||||
icon: Inbox,
|
||||
color: "edr-green",
|
||||
},
|
||||
{
|
||||
label: "Awaiting review",
|
||||
value: counts.review,
|
||||
icon: ShieldCheck,
|
||||
color: "yellow",
|
||||
},
|
||||
{
|
||||
label: "Ready — customer books",
|
||||
value: counts.ready,
|
||||
icon: PackageCheck,
|
||||
color: "edr-green",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card p={0} withBorder shadow="sm" radius="lg">
|
||||
@@ -444,7 +415,6 @@ export default function ContractClearanceListPage({
|
||||
rows={pagedRows}
|
||||
loading={isLoading}
|
||||
onOpen={openDetail}
|
||||
isHistory={isHistory}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -458,12 +428,10 @@ function ClearanceCardGrid({
|
||||
rows,
|
||||
loading,
|
||||
onOpen,
|
||||
isHistory = false,
|
||||
}: {
|
||||
rows: ClearanceRow[];
|
||||
loading: boolean;
|
||||
onOpen: (id: string) => void;
|
||||
isHistory?: boolean;
|
||||
}) {
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -480,7 +448,7 @@ function ClearanceCardGrid({
|
||||
<ThemeIcon variant="light" color="gray" radius="xl" size={48}>
|
||||
<Inbox size={22} />
|
||||
</ThemeIcon>
|
||||
<Text c="dimmed">No contracts awaiting review.</Text>
|
||||
<Text c="dimmed">No contracts need customs clearance.</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -495,7 +463,7 @@ function ClearanceCardGrid({
|
||||
}}
|
||||
>
|
||||
{rows.map((r) => (
|
||||
<ClearanceCard key={r.id} row={r} onOpen={() => onOpen(r.id)} isHistory={isHistory} />
|
||||
<ClearanceCard key={r.id} row={r} onOpen={() => onOpen(r.id)} />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
@@ -504,11 +472,9 @@ function ClearanceCardGrid({
|
||||
function ClearanceCard({
|
||||
row,
|
||||
onOpen,
|
||||
isHistory = false,
|
||||
}: {
|
||||
row: ClearanceRow;
|
||||
onOpen: () => void;
|
||||
isHistory?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card
|
||||
@@ -545,9 +511,7 @@ function ClearanceCard({
|
||||
</Group>
|
||||
</Box>
|
||||
</Group>
|
||||
<Badge size="sm" variant="light" color={isHistory ? "gray" : "edr-green"} radius="sm">
|
||||
{isHistory ? "Cleared" : "Under review"}
|
||||
</Badge>
|
||||
<StatusBadge row={row} />
|
||||
</Group>
|
||||
|
||||
<Box
|
||||
|
||||
Reference in New Issue
Block a user