mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Conflict in ClearanceDocumentsPage: this branch migrated the page to the pill FilterBar, dev added filters to the Select stack it replaced. Kept the FilterBar and carried dev's additions across as a "Booked by" (customerKind) FilterDef plus the shipping-line search placeholder; dev's startOfDayIso/endOfDayIso went away because dateRangeParams already does that. The Ship icon import is needed by dev's shipping-line customer cell, which merged cleanly on its own.
304 lines
10 KiB
TypeScript
304 lines
10 KiB
TypeScript
import { useMyTradeAccess } from "@/hooks/useMyTradeAccess";
|
|
import {
|
|
ActionIcon,
|
|
Box,
|
|
Card,
|
|
Stack,
|
|
Text,
|
|
ThemeIcon,
|
|
} from "@mantine/core";
|
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
|
import { FileText, Inbox, RefreshCw, Ship, User } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
import { useNavigate } from "react-router-dom";
|
|
|
|
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
|
import { ContractReferenceLink } from "@/components/bookings/ContractReferenceLink";
|
|
import { bookingTable } from "@/components/bookings/booking-ui.styles";
|
|
import { PageContainer, PageHeader } from "@/components/page";
|
|
import { bookingsService, type BookingListFilter } from "@/services/bookings.service";
|
|
import type { BookingDetail } from "@/types/booking";
|
|
import {
|
|
Badge,
|
|
DataTable,
|
|
DataTableFooter,
|
|
type ColumnDef,
|
|
} from "@edr/ui-common";
|
|
import { dateRangeParams, FilterBar, useFilters, type FilterDef } from "@/components/filters";
|
|
|
|
/**
|
|
* Operations "Clearance Documents" hub — the worklist for self-clearance
|
|
* (non-customs) document review. Clearance is always per SHIPMENT: the customer
|
|
* uploads his documents on the booking he initiated, whatever kind of contract
|
|
* it draws on, so this hub lists bookings only. Rows deep-link to the booking
|
|
* clearance review page; search / status filter / pagination are server-side.
|
|
*/
|
|
|
|
const PAGE_SIZE = 10;
|
|
|
|
/**
|
|
* The hub's baseline scope — FULLY_EXECUTED is the post-approval status of
|
|
* intercity (domestic) bookings, kept in as history so an approved intercity
|
|
* row doesn't just vanish. Sent whenever the Status pill has no narrower pick.
|
|
*/
|
|
const DEFAULT_STATUSES =
|
|
"AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY,FULLY_EXECUTED";
|
|
|
|
const STATUS_OPTIONS = [
|
|
{ value: "AWAITING_DOCUMENTS", label: "Awaiting documents" },
|
|
{ value: "DOCUMENTS_UNDER_REVIEW", label: "Under review" },
|
|
{ value: "CLEARANCE_READY", label: "Clearance ready" },
|
|
{ value: "FULLY_EXECUTED", label: "Approved (history)" },
|
|
];
|
|
|
|
const TRADE_DIRECTION_OPTIONS = [
|
|
{ value: "IMPORT", label: "Import" },
|
|
{ value: "EXPORT", label: "Export" },
|
|
{ value: "DOMESTIC", label: "Domestic" },
|
|
];
|
|
|
|
const FREIGHT_TYPE_OPTIONS = [
|
|
{ value: "CONTAINER", label: "Container" },
|
|
{ value: "BULK", label: "Bulk" },
|
|
];
|
|
|
|
const OWNERSHIP_OPTIONS = [
|
|
{ value: "true", label: "Government" },
|
|
{ value: "false", label: "Private" },
|
|
];
|
|
|
|
/** Who booked: shipping lines own bookings via `shippingLineCompanyId`, not a customer company. */
|
|
const CUSTOMER_KIND_OPTIONS = [
|
|
{ value: "SHIPPING_LINE", label: "Shipping line" },
|
|
{ value: "CUSTOMER", label: "Customer" },
|
|
];
|
|
|
|
export default function ClearanceDocumentsPage() {
|
|
const navigate = useNavigate();
|
|
const { filterOptions } = useMyTradeAccess();
|
|
|
|
const filterDefs: FilterDef[] = useMemo(
|
|
() => [
|
|
{
|
|
key: "status",
|
|
label: "Status",
|
|
type: "enum",
|
|
multiple: false,
|
|
options: STATUS_OPTIONS,
|
|
// No pick ⇒ no `statuses` param at all; the query fills in
|
|
// DEFAULT_STATUSES itself, same as the old Select's "All statuses" row.
|
|
toParams: ({ v }) => ({ statuses: v[0] }),
|
|
},
|
|
{
|
|
key: "tradeDirection",
|
|
label: "Direction",
|
|
type: "enum",
|
|
multiple: false,
|
|
options: filterOptions(TRADE_DIRECTION_OPTIONS),
|
|
},
|
|
{ key: "freightType", label: "Freight", type: "enum", multiple: false, options: FREIGHT_TYPE_OPTIONS },
|
|
{ key: "customerKind", label: "Booked by", type: "enum", multiple: false, options: CUSTOMER_KIND_OPTIONS },
|
|
{ key: "isGovernment", label: "Ownership", type: "enum", multiple: false, options: OWNERSHIP_OPTIONS },
|
|
{
|
|
key: "created",
|
|
label: "Created",
|
|
type: "date",
|
|
toParams: dateRangeParams("createdFrom", "createdTo"),
|
|
},
|
|
],
|
|
[filterOptions],
|
|
);
|
|
|
|
const controls = useFilters(filterDefs, { pageSize: PAGE_SIZE });
|
|
|
|
const filter: BookingListFilter = useMemo(
|
|
() => ({
|
|
...(controls.params as unknown as BookingListFilter),
|
|
// Self-clearance instances carry bookingType=ONE_TIME whatever their
|
|
// contract kind, so customsClearingEnabled=false + the status scope
|
|
// above are what isolate exactly this worklist.
|
|
customsClearingEnabled: "false",
|
|
statuses: (controls.params.statuses as string | undefined) ?? DEFAULT_STATUSES,
|
|
}),
|
|
[controls.params],
|
|
);
|
|
|
|
const bookingsQuery = useQuery({
|
|
queryKey: ["clearance-documents", "bookings", filter],
|
|
queryFn: () => bookingsService.list(filter),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
|
|
const bookingRows = bookingsQuery.data?.items ?? [];
|
|
|
|
const bookingColumns: ColumnDef<BookingDetail>[] = useMemo(
|
|
() => [
|
|
{
|
|
id: "booking",
|
|
header: () => <span className={bookingTable.headerCell}>Customer</span>,
|
|
cell: ({ row }) => {
|
|
const b = row.original;
|
|
const isShippingLine = Boolean(b.shippingLineCompany ?? b.shippingLineCompanyId);
|
|
const customer = isShippingLine
|
|
? (b.shippingLineCompany?.name ?? "Shipping line")
|
|
: b.isGovernment
|
|
? (b.governmentInstitution ?? "Government")
|
|
: (b.company?.name ?? "—");
|
|
return (
|
|
<div className="flex items-center gap-3 py-1.5">
|
|
<div className={bookingTable.rowIcon}>
|
|
{isShippingLine ? (
|
|
<Ship className="size-4" strokeWidth={1.75} />
|
|
) : (
|
|
<User className="size-4" strokeWidth={1.75} />
|
|
)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p className="flex items-center gap-1.5 font-medium text-foreground">
|
|
{customer}
|
|
{isShippingLine ? (
|
|
<Badge variant="secondary" className="h-4 shrink-0 px-1 text-[9px] font-medium">
|
|
Shipping line
|
|
</Badge>
|
|
) : null}
|
|
</p>
|
|
<p className="mt-0.5 flex items-center gap-1 text-xs text-muted-foreground">
|
|
<FileText className="size-3 shrink-0 opacity-70" />
|
|
{b.reference}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "contractRef",
|
|
header: () => <span className={bookingTable.headerCell}>Contract</span>,
|
|
cell: ({ row }) => {
|
|
const b = row.original;
|
|
return b.contractId && b.contractReference ? (
|
|
<ContractReferenceLink
|
|
contractId={b.contractId}
|
|
contractReference={b.contractReference}
|
|
className="block text-sm text-foreground underline underline-offset-2 hover:text-muted-foreground"
|
|
/>
|
|
) : (
|
|
<Text size="sm">—</Text>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "shipment",
|
|
header: () => <span className={bookingTable.headerCell}>Shipment</span>,
|
|
cell: ({ row }) => {
|
|
const b = row.original;
|
|
return (
|
|
<div className="flex gap-1.5 py-1">
|
|
<Badge
|
|
variant="outline"
|
|
className="h-5 border-border/50 bg-background/50 px-1.5 text-[10px] font-medium uppercase backdrop-blur-sm"
|
|
>
|
|
{b.tradeDirection ?? "—"}
|
|
</Badge>
|
|
<Badge
|
|
variant="secondary"
|
|
className="h-5 bg-muted/40 px-1.5 text-[10px] font-medium"
|
|
>
|
|
{b.freightType ?? "—"}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "status",
|
|
size: 200,
|
|
minSize: 180,
|
|
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
|
cell: ({ row }) => (
|
|
<div className="py-1">
|
|
<BookingStatusBadge status={row.original.status} />
|
|
</div>
|
|
),
|
|
meta: {
|
|
headerClassName: "min-w-[11rem]",
|
|
cellClassName: "min-w-[11rem]",
|
|
},
|
|
},
|
|
],
|
|
[],
|
|
);
|
|
|
|
const total = bookingsQuery.data?.total ?? 0;
|
|
const showEmpty =
|
|
!bookingsQuery.isLoading && !bookingsQuery.isError && bookingRows.length === 0;
|
|
const tableStatus = bookingsQuery.isLoading
|
|
? "loading"
|
|
: bookingsQuery.isError
|
|
? "error"
|
|
: "success";
|
|
|
|
return (
|
|
<PageContainer>
|
|
<Stack gap="lg">
|
|
<PageHeader
|
|
title="Clearance Documents"
|
|
subtitle="Operations review of the clearance documents customers upload on their shipments (services without customs clearing)."
|
|
action={
|
|
<ActionIcon
|
|
variant="default"
|
|
size="lg"
|
|
radius="md"
|
|
loading={bookingsQuery.isFetching}
|
|
onClick={() => void bookingsQuery.refetch()}
|
|
aria-label="Refresh"
|
|
>
|
|
<RefreshCw size={16} />
|
|
</ActionIcon>
|
|
}
|
|
/>
|
|
|
|
<Card p={0}>
|
|
<Stack gap={0}>
|
|
<Box px="md" pt="md" pb="sm" w="100%">
|
|
<FilterBar
|
|
defs={filterDefs}
|
|
controls={controls}
|
|
searchPlaceholder="Search booking, contract, customer or shipping line…"
|
|
viewId="clearance-documents"
|
|
/>
|
|
</Box>
|
|
|
|
{showEmpty ? (
|
|
<Stack align="center" gap={8} py={48}>
|
|
<ThemeIcon variant="light" color="gray" radius="xl" size={48}>
|
|
<Inbox size={22} />
|
|
</ThemeIcon>
|
|
<Text c="dimmed">No shipments match this view.</Text>
|
|
</Stack>
|
|
) : (
|
|
<Box style={{ overflowX: "auto" }} w="100%">
|
|
<DataTable
|
|
columns={bookingColumns}
|
|
data={bookingRows}
|
|
status={tableStatus}
|
|
// `from` so the detail page's Back returns to THIS hub, not
|
|
// to whichever worklist the fallback would guess.
|
|
onRowClick={(row) =>
|
|
navigate(`/dashboard/clearance/${row.id}`, {
|
|
state: { from: "/dashboard/contracts/clearance-documents" },
|
|
})
|
|
}
|
|
{...controls.tableProps(total)}
|
|
containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words"
|
|
footer={DataTableFooter}
|
|
/>
|
|
</Box>
|
|
)}
|
|
</Stack>
|
|
</Card>
|
|
</Stack>
|
|
</PageContainer>
|
|
);
|
|
}
|