mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 12:41:04 +00:00
feat(filters): migrate ClearanceDocumentsPage to the pill filter bar
Mechanical Family-A port — same server contract BookingRequestsPage already uses (tradeDirection/freightType/isGovernment/createdFrom/ createdTo/search/page/pageSize all already accepted). One wrinkle: this hub has no true "unfiltered" state — it always scopes to a fixed 4-status baseline (customsClearingEnabled=false self-clearance bookings), with the old status Select's "All statuses" row just being that baseline spelled out as an option. Modeled as a normal optional Status enum pill (4 individual statuses, no synthetic "All" entry) whose absence falls back to the baseline in the query build, not in the filter defs themselves — `customsClearingEnabled` stays a fixed, non-user-facing param the same way.
This commit is contained in:
@@ -3,34 +3,28 @@ import {
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Group,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
TextInput,
|
||||
ThemeIcon,
|
||||
} from "@mantine/core";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { getDateRangePresets } from "@/components/common/dateRangePresets";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { FileText, Inbox, RefreshCw, Search, User, X } from "lucide-react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { FileText, Inbox, RefreshCw, 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 } from "@/services/bookings.service";
|
||||
import { bookingsService, type BookingListFilter } from "@/services/bookings.service";
|
||||
import type { BookingDetail } from "@/types/booking";
|
||||
import {
|
||||
Badge,
|
||||
DataTable,
|
||||
DataTableFooter,
|
||||
usePagination,
|
||||
type ColumnDef,
|
||||
} from "@edr/ui-common";
|
||||
import { dateRangeParams, FilterBar, useFilters, type FilterDef } from "@/components/filters";
|
||||
|
||||
/**
|
||||
* Operations "Clearance Documents" hub — the worklist for self-clearance
|
||||
@@ -43,16 +37,14 @@ import {
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
/**
|
||||
* Status filter options (values = `statuses` param). FULLY_EXECUTED is the
|
||||
* post-approval status of intercity (domestic) bookings — kept in the list as
|
||||
* history, otherwise an approved intercity row vanishes from the hub.
|
||||
* 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 BOOKING_STATUS_OPTIONS = [
|
||||
{
|
||||
value:
|
||||
"AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY,FULLY_EXECUTED",
|
||||
label: "All statuses",
|
||||
},
|
||||
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" },
|
||||
@@ -75,72 +67,58 @@ const OWNERSHIP_OPTIONS = [
|
||||
{ value: "false", label: "Private" },
|
||||
];
|
||||
|
||||
function startOfDayIso(d: Date): string {
|
||||
const x = new Date(d);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x.toISOString();
|
||||
}
|
||||
|
||||
function endOfDayIso(d: Date): string {
|
||||
const x = new Date(d);
|
||||
x.setHours(23, 59, 59, 999);
|
||||
return x.toISOString();
|
||||
}
|
||||
|
||||
export default function ClearanceDocumentsPage() {
|
||||
const navigate = useNavigate();
|
||||
const [query, setQuery] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(query, 300);
|
||||
const [bookingStatuses, setBookingStatuses] = useState(
|
||||
BOOKING_STATUS_OPTIONS[0].value,
|
||||
);
|
||||
const { filterOptions } = useMyTradeAccess();
|
||||
const [directionFilter, setDirectionFilter] = useState<string | null>(null);
|
||||
const [freightTypeFilter, setFreightTypeFilter] = useState<string | null>(null);
|
||||
const [ownershipFilter, setOwnershipFilter] = useState<string | null>(null);
|
||||
const [createdFrom, setCreatedFrom] = useState<Date | null>(null);
|
||||
const [createdTo, setCreatedTo] = useState<Date | null>(null);
|
||||
const { pagination, setPagination } = usePagination({ pageSize: PAGE_SIZE });
|
||||
|
||||
const search = debouncedQuery.trim() || undefined;
|
||||
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: "isGovernment", label: "Ownership", type: "enum", multiple: false, options: OWNERSHIP_OPTIONS },
|
||||
{
|
||||
key: "created",
|
||||
label: "Created",
|
||||
type: "date",
|
||||
toParams: dateRangeParams("createdFrom", "createdTo"),
|
||||
},
|
||||
],
|
||||
[filterOptions],
|
||||
);
|
||||
|
||||
const resetPage = useCallback(() => {
|
||||
setPagination({ pageIndex: 0, pageSize: PAGE_SIZE });
|
||||
}, [setPagination]);
|
||||
const controls = useFilters(filterDefs, { pageSize: PAGE_SIZE });
|
||||
|
||||
const page = pagination.pageIndex + 1;
|
||||
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",
|
||||
bookingStatuses,
|
||||
directionFilter,
|
||||
freightTypeFilter,
|
||||
ownershipFilter,
|
||||
createdFrom,
|
||||
createdTo,
|
||||
page,
|
||||
search,
|
||||
],
|
||||
queryFn: () =>
|
||||
// Self-clearance instances carry bookingType=ONE_TIME whatever their
|
||||
// contract kind, so customsClearingEnabled=false + the three per-booking
|
||||
// clearance statuses are what isolate exactly this worklist.
|
||||
bookingsService.list({
|
||||
statuses: bookingStatuses,
|
||||
customsClearingEnabled: "false",
|
||||
page,
|
||||
pageSize: PAGE_SIZE,
|
||||
search,
|
||||
...(directionFilter ? { tradeDirection: directionFilter } : {}),
|
||||
...(freightTypeFilter ? { freightType: freightTypeFilter } : {}),
|
||||
...(ownershipFilter
|
||||
? { isGovernment: ownershipFilter as "true" | "false" }
|
||||
: {}),
|
||||
...(createdFrom ? { createdFrom: startOfDayIso(createdFrom) } : {}),
|
||||
...(createdTo ? { createdTo: endOfDayIso(createdTo) } : {}),
|
||||
}),
|
||||
queryKey: ["clearance-documents", "bookings", filter],
|
||||
queryFn: () => bookingsService.list(filter),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
|
||||
@@ -233,7 +211,6 @@ export default function ClearanceDocumentsPage() {
|
||||
);
|
||||
|
||||
const total = bookingsQuery.data?.total ?? 0;
|
||||
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
const showEmpty =
|
||||
!bookingsQuery.isLoading && !bookingsQuery.isError && bookingRows.length === 0;
|
||||
const tableStatus = bookingsQuery.isLoading
|
||||
@@ -265,103 +242,12 @@ export default function ClearanceDocumentsPage() {
|
||||
<Card p={0}>
|
||||
<Stack gap={0}>
|
||||
<Box px="md" pt="md" pb="sm" w="100%">
|
||||
<Group justify="space-between" gap="md" wrap="wrap">
|
||||
<TextInput
|
||||
placeholder="Search booking, contract or customer…"
|
||||
leftSection={<Search size={18} />}
|
||||
value={query}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
resetPage();
|
||||
}}
|
||||
rightSection={
|
||||
query && (
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
color="gray"
|
||||
radius="md"
|
||||
variant="transparent"
|
||||
onClick={() => {
|
||||
setQuery("");
|
||||
resetPage();
|
||||
}}
|
||||
>
|
||||
<X size={16} />
|
||||
</ActionIcon>
|
||||
)
|
||||
}
|
||||
style={{ flex: 1, minWidth: "200px" }}
|
||||
radius="lg"
|
||||
/>
|
||||
<Select
|
||||
data={BOOKING_STATUS_OPTIONS}
|
||||
value={bookingStatuses}
|
||||
onChange={(v) => {
|
||||
setBookingStatuses(v ?? BOOKING_STATUS_OPTIONS[0].value);
|
||||
resetPage();
|
||||
}}
|
||||
allowDeselect={false}
|
||||
radius="lg"
|
||||
w={220}
|
||||
aria-label="Filter by status"
|
||||
/>
|
||||
</Group>
|
||||
<Group gap="sm" mt="sm" wrap="wrap">
|
||||
<Select
|
||||
placeholder="Direction"
|
||||
data={filterOptions(TRADE_DIRECTION_OPTIONS)}
|
||||
value={directionFilter}
|
||||
onChange={(v) => {
|
||||
setDirectionFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 130 }}
|
||||
aria-label="Filter by direction"
|
||||
/>
|
||||
<Select
|
||||
placeholder="Freight type"
|
||||
data={FREIGHT_TYPE_OPTIONS}
|
||||
value={freightTypeFilter}
|
||||
onChange={(v) => {
|
||||
setFreightTypeFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Filter by freight type"
|
||||
/>
|
||||
<Select
|
||||
placeholder="Gov / Private"
|
||||
data={OWNERSHIP_OPTIONS}
|
||||
value={ownershipFilter}
|
||||
onChange={(v) => {
|
||||
setOwnershipFilter(v);
|
||||
resetPage();
|
||||
}}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 140 }}
|
||||
aria-label="Filter by ownership"
|
||||
/>
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
placeholder="Created date range"
|
||||
value={[createdFrom, createdTo]}
|
||||
onChange={([from, to]) => {
|
||||
setCreatedFrom(from ? new Date(from) : null);
|
||||
setCreatedTo(to ? new Date(to) : null);
|
||||
resetPage();
|
||||
}}
|
||||
presets={getDateRangePresets()}
|
||||
clearable
|
||||
radius="lg"
|
||||
style={{ minWidth: 220 }}
|
||||
aria-label="Created date range"
|
||||
/>
|
||||
</Group>
|
||||
<FilterBar
|
||||
defs={filterDefs}
|
||||
controls={controls}
|
||||
searchPlaceholder="Search booking, contract or customer…"
|
||||
viewId="clearance-documents"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{showEmpty ? (
|
||||
@@ -384,18 +270,7 @@ export default function ClearanceDocumentsPage() {
|
||||
state: { from: "/dashboard/contracts/clearance-documents" },
|
||||
})
|
||||
}
|
||||
pagination={{
|
||||
pageIndex: pagination.pageIndex,
|
||||
pageSize: pagination.pageSize,
|
||||
pageCount,
|
||||
totalCount: total,
|
||||
}}
|
||||
tableOptions={{
|
||||
state: { pagination },
|
||||
onPaginationChange: setPagination,
|
||||
manualPagination: true,
|
||||
pageCount,
|
||||
}}
|
||||
{...controls.tableProps(total)}
|
||||
containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words"
|
||||
footer={DataTableFooter}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user