mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +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,
|
ActionIcon,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Group,
|
|
||||||
Select,
|
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
|
||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
} from "@mantine/core";
|
} 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 { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||||
import { FileText, Inbox, RefreshCw, Search, User, X } from "lucide-react";
|
import { FileText, Inbox, RefreshCw, User } from "lucide-react";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge";
|
||||||
import { ContractReferenceLink } from "@/components/bookings/ContractReferenceLink";
|
import { ContractReferenceLink } from "@/components/bookings/ContractReferenceLink";
|
||||||
import { bookingTable } from "@/components/bookings/booking-ui.styles";
|
import { bookingTable } from "@/components/bookings/booking-ui.styles";
|
||||||
import { PageContainer, PageHeader } from "@/components/page";
|
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 type { BookingDetail } from "@/types/booking";
|
||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
DataTable,
|
DataTable,
|
||||||
DataTableFooter,
|
DataTableFooter,
|
||||||
usePagination,
|
|
||||||
type ColumnDef,
|
type ColumnDef,
|
||||||
} from "@edr/ui-common";
|
} from "@edr/ui-common";
|
||||||
|
import { dateRangeParams, FilterBar, useFilters, type FilterDef } from "@/components/filters";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Operations "Clearance Documents" hub — the worklist for self-clearance
|
* Operations "Clearance Documents" hub — the worklist for self-clearance
|
||||||
@@ -43,16 +37,14 @@ import {
|
|||||||
const PAGE_SIZE = 10;
|
const PAGE_SIZE = 10;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status filter options (values = `statuses` param). FULLY_EXECUTED is the
|
* The hub's baseline scope — FULLY_EXECUTED is the post-approval status of
|
||||||
* post-approval status of intercity (domestic) bookings — kept in the list as
|
* intercity (domestic) bookings, kept in as history so an approved intercity
|
||||||
* history, otherwise an approved intercity row vanishes from the hub.
|
* row doesn't just vanish. Sent whenever the Status pill has no narrower pick.
|
||||||
*/
|
*/
|
||||||
const BOOKING_STATUS_OPTIONS = [
|
const DEFAULT_STATUSES =
|
||||||
{
|
"AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY,FULLY_EXECUTED";
|
||||||
value:
|
|
||||||
"AWAITING_DOCUMENTS,DOCUMENTS_UNDER_REVIEW,CLEARANCE_READY,FULLY_EXECUTED",
|
const STATUS_OPTIONS = [
|
||||||
label: "All statuses",
|
|
||||||
},
|
|
||||||
{ value: "AWAITING_DOCUMENTS", label: "Awaiting documents" },
|
{ value: "AWAITING_DOCUMENTS", label: "Awaiting documents" },
|
||||||
{ value: "DOCUMENTS_UNDER_REVIEW", label: "Under review" },
|
{ value: "DOCUMENTS_UNDER_REVIEW", label: "Under review" },
|
||||||
{ value: "CLEARANCE_READY", label: "Clearance ready" },
|
{ value: "CLEARANCE_READY", label: "Clearance ready" },
|
||||||
@@ -75,72 +67,58 @@ const OWNERSHIP_OPTIONS = [
|
|||||||
{ value: "false", label: "Private" },
|
{ 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() {
|
export default function ClearanceDocumentsPage() {
|
||||||
const navigate = useNavigate();
|
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 { 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(() => {
|
const controls = useFilters(filterDefs, { pageSize: PAGE_SIZE });
|
||||||
setPagination({ pageIndex: 0, pageSize: PAGE_SIZE });
|
|
||||||
}, [setPagination]);
|
|
||||||
|
|
||||||
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({
|
const bookingsQuery = useQuery({
|
||||||
queryKey: [
|
queryKey: ["clearance-documents", "bookings", filter],
|
||||||
"clearance-documents",
|
queryFn: () => bookingsService.list(filter),
|
||||||
"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) } : {}),
|
|
||||||
}),
|
|
||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -233,7 +211,6 @@ export default function ClearanceDocumentsPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const total = bookingsQuery.data?.total ?? 0;
|
const total = bookingsQuery.data?.total ?? 0;
|
||||||
const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
||||||
const showEmpty =
|
const showEmpty =
|
||||||
!bookingsQuery.isLoading && !bookingsQuery.isError && bookingRows.length === 0;
|
!bookingsQuery.isLoading && !bookingsQuery.isError && bookingRows.length === 0;
|
||||||
const tableStatus = bookingsQuery.isLoading
|
const tableStatus = bookingsQuery.isLoading
|
||||||
@@ -265,103 +242,12 @@ export default function ClearanceDocumentsPage() {
|
|||||||
<Card p={0}>
|
<Card p={0}>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Box px="md" pt="md" pb="sm" w="100%">
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
<Group justify="space-between" gap="md" wrap="wrap">
|
<FilterBar
|
||||||
<TextInput
|
defs={filterDefs}
|
||||||
placeholder="Search booking, contract or customer…"
|
controls={controls}
|
||||||
leftSection={<Search size={18} />}
|
searchPlaceholder="Search booking, contract or customer…"
|
||||||
value={query}
|
viewId="clearance-documents"
|
||||||
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>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{showEmpty ? (
|
{showEmpty ? (
|
||||||
@@ -384,18 +270,7 @@ export default function ClearanceDocumentsPage() {
|
|||||||
state: { from: "/dashboard/contracts/clearance-documents" },
|
state: { from: "/dashboard/contracts/clearance-documents" },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
pagination={{
|
{...controls.tableProps(total)}
|
||||||
pageIndex: pagination.pageIndex,
|
|
||||||
pageSize: pagination.pageSize,
|
|
||||||
pageCount,
|
|
||||||
totalCount: total,
|
|
||||||
}}
|
|
||||||
tableOptions={{
|
|
||||||
state: { pagination },
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
manualPagination: true,
|
|
||||||
pageCount,
|
|
||||||
}}
|
|
||||||
containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words"
|
containerClassName="border-0 shadow-none bg-transparent [&_th]:max-w-[100px] [&_td]:max-w-[100px] [&_td]:break-words"
|
||||||
footer={DataTableFooter}
|
footer={DataTableFooter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user