mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
add customs clearing filter and enhance clearance documents page for non-customs contracts
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Badge,
|
||||
Group,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
Tabs,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { Search } from "lucide-react";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableFooter,
|
||||
usePagination,
|
||||
type ColumnDef,
|
||||
} from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import type { BookingDetail } from "@/types/booking";
|
||||
|
||||
/**
|
||||
* Operations "Clearance Documents" hub — the worklist for clearance-document
|
||||
* review on contracts WITHOUT customs clearing (self-clearance / Path A):
|
||||
*
|
||||
* - Contracts tab: contracts whose clearance runs at contract level; rows open
|
||||
* the contract clearance detail where Operations approves + finalizes.
|
||||
* - General tab: booking instances under GENERAL non-customs contracts (those
|
||||
* clear per booking); rows open the booking clearance review page.
|
||||
*
|
||||
* The hub only lists — all review/approve/finalize actions live on the
|
||||
* existing detail pages it links to.
|
||||
*/
|
||||
|
||||
type HubTab = "contracts" | "general";
|
||||
type QueueTab = "queue" | "history";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
/** Booking statuses that mean "docs awaiting review" / "review finished". */
|
||||
const BOOKING_QUEUE_STATUS = "DOCUMENTS_UNDER_REVIEW";
|
||||
const BOOKING_HISTORY_STATUS = "CLEARANCE_READY";
|
||||
|
||||
function formatDate(iso?: string | null): string {
|
||||
if (!iso) return "—";
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function statusLabel(status?: string | null): string {
|
||||
return (status ?? "—").replaceAll("_", " ");
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status?: string | null }) {
|
||||
const done =
|
||||
status === "CLEARANCE_READY" ||
|
||||
status === "CLEARANCE_READY_FOR_BOOKING" ||
|
||||
status === "ACTIVE" ||
|
||||
status === "CONTRACT_ACTIVE" ||
|
||||
status === "FULLY_EXECUTED";
|
||||
return (
|
||||
<Badge variant="light" color={done ? "edr-green" : "yellow"} radius="sm">
|
||||
{statusLabel(status)}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ClearanceDocumentsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [hubTab, setHubTab] = useState<HubTab>("contracts");
|
||||
const [queueTab, setQueueTab] = useState<QueueTab>("queue");
|
||||
const [query, setQuery] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(query, 300);
|
||||
const search = debouncedQuery.trim() || undefined;
|
||||
|
||||
const contractsPager = usePagination({ pageSize: PAGE_SIZE });
|
||||
const generalPager = usePagination({ pageSize: PAGE_SIZE });
|
||||
|
||||
// Any search / queue-history / tab switch restarts both lists from page 1.
|
||||
useEffect(() => {
|
||||
contractsPager.setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
generalPager.setPagination((p) => ({ ...p, pageIndex: 0 }));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedQuery, queueTab, hubTab]);
|
||||
|
||||
const isHistory = queueTab === "history";
|
||||
|
||||
const contractsQuery = useQuery({
|
||||
queryKey: [
|
||||
"clearance-documents",
|
||||
"contracts",
|
||||
queueTab,
|
||||
contractsPager.pagination.pageIndex,
|
||||
search,
|
||||
],
|
||||
queryFn: () => {
|
||||
const filter = {
|
||||
page: contractsPager.pagination.pageIndex + 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
search,
|
||||
};
|
||||
return isHistory
|
||||
? contractsService.getOpsClearanceHistory(filter)
|
||||
: contractsService.getOpsClearanceQueue(filter);
|
||||
},
|
||||
enabled: hubTab === "contracts",
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const generalQuery = useQuery({
|
||||
queryKey: [
|
||||
"clearance-documents",
|
||||
"general",
|
||||
queueTab,
|
||||
generalPager.pagination.pageIndex,
|
||||
search,
|
||||
],
|
||||
queryFn: () =>
|
||||
bookingsService.list({
|
||||
status: isHistory ? BOOKING_HISTORY_STATUS : BOOKING_QUEUE_STATUS,
|
||||
bookingType: "GENERAL_CONTRACT",
|
||||
customsClearingEnabled: "false",
|
||||
page: generalPager.pagination.pageIndex + 1,
|
||||
pageSize: PAGE_SIZE,
|
||||
search,
|
||||
}),
|
||||
enabled: hubTab === "general",
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
const contractColumns = useMemo(
|
||||
(): ColumnDef<Freight.IContract, unknown>[] => [
|
||||
{
|
||||
header: "Reference",
|
||||
accessorKey: "reference",
|
||||
},
|
||||
{
|
||||
header: "Customer",
|
||||
cell: ({ row }) => row.original.company?.name ?? "—",
|
||||
},
|
||||
{
|
||||
header: "Kind",
|
||||
cell: ({ row }) => statusLabel(row.original.contractKind),
|
||||
},
|
||||
{
|
||||
header: "Direction",
|
||||
cell: ({ row }) => statusLabel(row.original.tradeDirection),
|
||||
},
|
||||
{
|
||||
header: "Freight",
|
||||
cell: ({ row }) => statusLabel(row.original.freightType),
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||
},
|
||||
{
|
||||
header: "Created",
|
||||
cell: ({ row }) => formatDate(row.original.createdAt),
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const bookingColumns = useMemo(
|
||||
(): ColumnDef<BookingDetail, unknown>[] => [
|
||||
{
|
||||
header: "Reference",
|
||||
accessorKey: "reference",
|
||||
},
|
||||
{
|
||||
header: "Customer",
|
||||
cell: ({ row }) =>
|
||||
row.original.isGovernment
|
||||
? (row.original.governmentInstitution ?? "Government")
|
||||
: (row.original.company?.name ?? "—"),
|
||||
},
|
||||
{
|
||||
header: "Contract",
|
||||
cell: ({ row }) => row.original.contractReference ?? "—",
|
||||
},
|
||||
{
|
||||
header: "Direction",
|
||||
cell: ({ row }) => statusLabel(row.original.tradeDirection),
|
||||
},
|
||||
{
|
||||
header: "Freight",
|
||||
cell: ({ row }) => statusLabel(row.original.freightType),
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const activeQuery = hubTab === "contracts" ? contractsQuery : generalQuery;
|
||||
const total = activeQuery.data?.total ?? 0;
|
||||
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
|
||||
const tableStatus = activeQuery.isLoading
|
||||
? "loading"
|
||||
: activeQuery.isError
|
||||
? "error"
|
||||
: "success";
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="Clearance Documents"
|
||||
subtitle="Operations review of customer clearance documents for contracts without customs clearing — contract-level (one-time) and per-booking (general)."
|
||||
/>
|
||||
|
||||
<Paper withBorder radius="md" p="md">
|
||||
<Group justify="space-between" mb="md" wrap="wrap" gap="sm">
|
||||
<Tabs
|
||||
value={hubTab}
|
||||
onChange={(v) => setHubTab((v as HubTab) ?? "contracts")}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value="contracts">Contracts</Tabs.Tab>
|
||||
<Tabs.Tab value="general">General</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
</Tabs>
|
||||
<Group gap="sm">
|
||||
<SegmentedControl
|
||||
value={queueTab}
|
||||
onChange={(v) => setQueueTab(v as QueueTab)}
|
||||
data={[
|
||||
{ value: "queue", label: "Queue" },
|
||||
{ value: "history", label: "History" },
|
||||
]}
|
||||
size="xs"
|
||||
/>
|
||||
<TextInput
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.currentTarget.value)}
|
||||
placeholder={
|
||||
hubTab === "contracts"
|
||||
? "Search reference or customer…"
|
||||
: "Search booking, customer or contract…"
|
||||
}
|
||||
leftSection={<Search size={14} />}
|
||||
w={260}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
{hubTab === "contracts" ? (
|
||||
<DataTable<Freight.IContract, unknown>
|
||||
columns={contractColumns}
|
||||
data={contractsQuery.data?.items ?? []}
|
||||
status={tableStatus}
|
||||
onRowClick={(row) =>
|
||||
navigate(`/dashboard/contracts/clearance/${row.id}`)
|
||||
}
|
||||
pagination={{
|
||||
pageIndex: contractsPager.pagination.pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
pageCount,
|
||||
totalCount: total,
|
||||
}}
|
||||
tableOptions={{
|
||||
state: { pagination: contractsPager.pagination },
|
||||
onPaginationChange: contractsPager.setPagination,
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
) : (
|
||||
<DataTable<BookingDetail, unknown>
|
||||
columns={bookingColumns}
|
||||
data={generalQuery.data?.items ?? []}
|
||||
status={tableStatus}
|
||||
onRowClick={(row) => navigate(`/dashboard/clearance/${row.id}`)}
|
||||
pagination={{
|
||||
pageIndex: generalPager.pagination.pageIndex,
|
||||
pageSize: PAGE_SIZE,
|
||||
pageCount,
|
||||
totalCount: total,
|
||||
}}
|
||||
tableOptions={{
|
||||
state: { pagination: generalPager.pagination },
|
||||
onPaginationChange: generalPager.setPagination,
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
}}
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user