feat: add service type fitler

This commit is contained in:
Nathnael
2026-08-18 11:38:07 +00:00
parent 2f293e4ae4
commit 03b16b4ee8
9 changed files with 103 additions and 4 deletions

View File

@@ -130,6 +130,15 @@ export default function BookingRequestsPage() {
[yardRefs], [yardRefs],
); );
// Service-type options — same reference-data payload the booking form uses.
const { data: refData } = useQuery(
api.bookings.referenceData.queryOptions({ staleTime: 5 * 60_000 }),
);
const serviceTypeOptions = useMemo(
() => (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name })),
[refData],
);
// Deep links land here pre-filtered (?statuses=A,B&tradeDirection=IMPORT) — // Deep links land here pre-filtered (?statuses=A,B&tradeDirection=IMPORT) —
// the header's document-review alarm opens exactly the undecided requests // the header's document-review alarm opens exactly the undecided requests
// it is counting down for. No sync effect needed any more: controls.values // it is counting down for. No sync effect needed any more: controls.values
@@ -147,6 +156,7 @@ export default function BookingRequestsPage() {
options: filterOptions(TRADE_DIRECTION_OPTIONS), options: filterOptions(TRADE_DIRECTION_OPTIONS),
}, },
{ key: "freightType", label: "Freight", type: "enum", multiple: false, options: FREIGHT_TYPE_OPTIONS }, { key: "freightType", label: "Freight", type: "enum", multiple: false, options: FREIGHT_TYPE_OPTIONS },
{ key: "serviceTypeId", label: "Service", type: "enum", multiple: false, options: serviceTypeOptions },
{ key: "paymentStatus", label: "Payment", type: "enum", multiple: false, options: PAYMENT_STATUS_OPTIONS, secondary: true }, { key: "paymentStatus", label: "Payment", type: "enum", multiple: false, options: PAYMENT_STATUS_OPTIONS, secondary: true },
{ {
// Wins over the `paymentStatus` filter above — the queue is by // Wins over the `paymentStatus` filter above — the queue is by
@@ -172,7 +182,7 @@ export default function BookingRequestsPage() {
toParams: dateRangeParams("scheduledFrom", "scheduledTo"), toParams: dateRangeParams("scheduledFrom", "scheduledTo"),
}, },
], ],
[filterOptions, yardOptions], [filterOptions, yardOptions, serviceTypeOptions],
); );
const controls = useFilters(bookingFilterDefs, { defaultSort: "createdAt:DESC", pageSize: 10 }); const controls = useFilters(bookingFilterDefs, { defaultSort: "createdAt:DESC", pageSize: 10 });

View File

@@ -115,6 +115,15 @@ export default function ContractRequestsPage() {
[yardRefs], [yardRefs],
); );
// Service-type options — same reference-data payload the booking form uses.
const { data: refData } = useQuery(
api.bookings.referenceData.queryOptions({ staleTime: 5 * 60_000 }),
);
const serviceTypeOptions = useMemo(
() => (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name })),
[refData],
);
// Static shape only (no facet counts) — this is what useFilters needs to // Static shape only (no facet counts) — this is what useFilters needs to
// parse the URL and build API params. Counts are attached separately below, // parse the URL and build API params. Counts are attached separately below,
// for rendering only, once the summary query (which itself depends on // for rendering only, once the summary query (which itself depends on
@@ -143,6 +152,13 @@ export default function ContractRequestsPage() {
multiple: false, multiple: false,
options: FREIGHT_TYPE_OPTIONS, options: FREIGHT_TYPE_OPTIONS,
}, },
{
key: "serviceTypeId",
label: "Service",
type: "enum",
multiple: false,
options: serviceTypeOptions,
},
{ {
key: "paymentCurrency", key: "paymentCurrency",
label: "Currency", label: "Currency",
@@ -170,7 +186,7 @@ export default function ContractRequestsPage() {
toParams: ({ v }) => ({ originYardId: v[0], destinationYardId: v[1] }), toParams: ({ v }) => ({ originYardId: v[0], destinationYardId: v[1] }),
}, },
], ],
[filterOptions, yardOptions], [filterOptions, yardOptions, serviceTypeOptions],
); );
const controls = useFilters(filterDefs, { const controls = useFilters(filterDefs, {

View File

@@ -2741,6 +2741,13 @@ export const api = {
({ id }) => QUERY_KEYS.BOOKINGS.byId(id), ({ id }) => QUERY_KEYS.BOOKINGS.byId(id),
), ),
referenceData: endpoint<void, Freight.BookingReferenceData>(
"bookings",
"referenceData",
() => bookingsService.getReferenceData() as Promise<Freight.BookingReferenceData>,
() => ["bookings", "reference-data"],
),
remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) => remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) =>
bookingsService.remove(id), bookingsService.remove(id),
), ),

View File

@@ -19,6 +19,8 @@ export interface BookingListFilter {
/** Bookings drawn down under this contract (contract detail's Shipments tab). */ /** Bookings drawn down under this contract (contract detail's Shipments tab). */
contractId?: string; contractId?: string;
freightType?: string; freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */ /** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
bookingType?: string; bookingType?: string;
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */ /** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
@@ -142,6 +144,7 @@ export const bookingsService = {
if (filter.pageSize != null) params.pageSize = filter.pageSize; if (filter.pageSize != null) params.pageSize = filter.pageSize;
if (filter.companyId) params.companyId = filter.companyId; if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType; if (filter.freightType) params.freightType = filter.freightType;
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
if (filter.bookingType) params.bookingType = filter.bookingType; if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;
@@ -176,6 +179,7 @@ export const bookingsService = {
if (filter.companyId) params.companyId = filter.companyId; if (filter.companyId) params.companyId = filter.companyId;
if (filter.contractId) params.contractId = filter.contractId; if (filter.contractId) params.contractId = filter.contractId;
if (filter.freightType) params.freightType = filter.freightType; if (filter.freightType) params.freightType = filter.freightType;
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
if (filter.bookingType) params.bookingType = filter.bookingType; if (filter.bookingType) params.bookingType = filter.bookingType;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;

View File

@@ -13,6 +13,8 @@ export interface ContractListFilter {
tab?: string; tab?: string;
companyId?: string; companyId?: string;
freightType?: string; freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
tradeDirection?: string; tradeDirection?: string;
contractKind?: string; contractKind?: string;
paymentCurrency?: string; paymentCurrency?: string;
@@ -171,6 +173,7 @@ function buildListParams(filter?: ContractListFilter) {
if (filter.sortOrder) params.sortOrder = filter.sortOrder; if (filter.sortOrder) params.sortOrder = filter.sortOrder;
if (filter.companyId) params.companyId = filter.companyId; if (filter.companyId) params.companyId = filter.companyId;
if (filter.freightType) params.freightType = filter.freightType; if (filter.freightType) params.freightType = filter.freightType;
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
if (filter.contractKind) params.contractKind = filter.contractKind; if (filter.contractKind) params.contractKind = filter.contractKind;
if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency;

View File

@@ -374,6 +374,16 @@ export default function BookingsListPage() {
const [debouncedQuery] = useDebouncedValue(query, 300); const [debouncedQuery] = useDebouncedValue(query, 300);
const [typeFilter, setTypeFilter] = useState<string | null>(null); const [typeFilter, setTypeFilter] = useState<string | null>(null);
const [freightFilter, setFreightFilter] = useState<string | null>(null); const [freightFilter, setFreightFilter] = useState<string | null>(null);
const [serviceFilter, setServiceFilter] = useState<string | null>(null);
// Service-type options — same reference-data payload the booking form uses.
const { data: refData } = useQuery(
api.bookings.referenceData.queryOptions({ staleTime: 5 * 60_000 }),
);
const serviceTypeOptions = useMemo(
() => (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name })),
[refData],
);
const [sort, setSort] = useState<string>("createdAt:DESC"); const [sort, setSort] = useState<string>("createdAt:DESC");
const [createdFrom, setCreatedFrom] = useState<string>(""); const [createdFrom, setCreatedFrom] = useState<string>("");
const [createdTo, setCreatedTo] = useState<string>(""); const [createdTo, setCreatedTo] = useState<string>("");
@@ -395,10 +405,11 @@ export default function BookingsListPage() {
}; };
const hasExtraFilters = const hasExtraFilters =
!!typeFilter || !!freightFilter || !!createdFrom || !!createdTo; !!typeFilter || !!freightFilter || !!serviceFilter || !!createdFrom || !!createdTo;
const clearExtraFilters = () => { const clearExtraFilters = () => {
setTypeFilter(null); setTypeFilter(null);
setFreightFilter(null); setFreightFilter(null);
setServiceFilter(null);
setCreatedFrom(""); setCreatedFrom("");
setCreatedTo(""); setCreatedTo("");
resetPage(); resetPage();
@@ -410,6 +421,7 @@ export default function BookingsListPage() {
assignedToSchedule, assignedToSchedule,
bookingType: typeFilter ?? undefined, bookingType: typeFilter ?? undefined,
freightType: freightFilter ?? undefined, freightType: freightFilter ?? undefined,
serviceTypeId: serviceFilter ?? undefined,
createdFrom: createdFrom || undefined, createdFrom: createdFrom || undefined,
// include the whole selected end day // include the whole selected end day
createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined,
@@ -425,6 +437,7 @@ export default function BookingsListPage() {
assignedToSchedule, assignedToSchedule,
typeFilter, typeFilter,
freightFilter, freightFilter,
serviceFilter,
createdFrom, createdFrom,
createdTo, createdTo,
debouncedQuery, debouncedQuery,
@@ -852,6 +865,20 @@ export default function BookingsListPage() {
style={{ width: 150 }} style={{ width: 150 }}
aria-label="Filter by cargo type" aria-label="Filter by cargo type"
/> />
<Select
placeholder="Any service"
data={serviceTypeOptions}
value={serviceFilter}
onChange={(v) => {
setServiceFilter(v);
resetPage();
}}
clearable
radius="md"
comboboxProps={{ withinPortal: true }}
style={{ width: 190 }}
aria-label="Filter by service type"
/>
<Select <Select
data={SORT_OPTIONS.map((o) => ({ data={SORT_OPTIONS.map((o) => ({
value: o.value, value: o.value,

View File

@@ -71,6 +71,16 @@ export default function ContractsList() {
const [disclaimerOpen, setDisclaimerOpen] = useState(false); const [disclaimerOpen, setDisclaimerOpen] = useState(false);
const [freightFilter, setFreightFilter] = useState<string | null>(null); const [freightFilter, setFreightFilter] = useState<string | null>(null);
const [kindFilter, setKindFilter] = useState<string | null>(null); const [kindFilter, setKindFilter] = useState<string | null>(null);
const [serviceFilter, setServiceFilter] = useState<string | null>(null);
// Service-type options — same reference-data payload the contract form uses.
const { data: refData } = useQuery(
api.bookings.referenceData.queryOptions({ staleTime: 5 * 60_000 }),
);
const serviceTypeOptions = useMemo(
() => (refData?.service ?? []).map((s) => ({ value: s.id, label: s.name })),
[refData],
);
const [createdFrom, setCreatedFrom] = useState<string>(""); const [createdFrom, setCreatedFrom] = useState<string>("");
const [createdTo, setCreatedTo] = useState<string>(""); const [createdTo, setCreatedTo] = useState<string>("");
const [expanded, setExpanded] = useState<Set<string>>(new Set()); const [expanded, setExpanded] = useState<Set<string>>(new Set());
@@ -96,10 +106,11 @@ export default function ContractsList() {
const resetPage = () => const resetPage = () =>
setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
const hasExtraFilters = const hasExtraFilters =
!!freightFilter || !!kindFilter || !!createdFrom || !!createdTo; !!freightFilter || !!kindFilter || !!serviceFilter || !!createdFrom || !!createdTo;
const clearExtraFilters = () => { const clearExtraFilters = () => {
setFreightFilter(null); setFreightFilter(null);
setKindFilter(null); setKindFilter(null);
setServiceFilter(null);
setCreatedFrom(""); setCreatedFrom("");
setCreatedTo(""); setCreatedTo("");
resetPage(); resetPage();
@@ -109,6 +120,7 @@ export default function ContractsList() {
() => ({ () => ({
contractKind: kindFilter ?? undefined, contractKind: kindFilter ?? undefined,
freightType: freightFilter ?? undefined, freightType: freightFilter ?? undefined,
serviceTypeId: serviceFilter ?? undefined,
createdFrom: createdFrom || undefined, createdFrom: createdFrom || undefined,
createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined,
// Server-side free-text search (contract reference, company name). // Server-side free-text search (contract reference, company name).
@@ -121,6 +133,7 @@ export default function ContractsList() {
[ [
kindFilter, kindFilter,
freightFilter, freightFilter,
serviceFilter,
createdFrom, createdFrom,
createdTo, createdTo,
debouncedQuery, debouncedQuery,
@@ -322,6 +335,21 @@ export default function ContractsList() {
styles={{ input: { height: 42 } }} styles={{ input: { height: 42 } }}
aria-label="Filter by cargo type" aria-label="Filter by cargo type"
/> />
<Select
placeholder="Any service"
data={serviceTypeOptions}
value={serviceFilter}
onChange={(v) => {
setServiceFilter(v);
resetPage();
}}
clearable
radius="md"
comboboxProps={{ withinPortal: true }}
style={{ width: 180 }}
styles={{ input: { height: 42 } }}
aria-label="Filter by service type"
/>
<TextInput <TextInput
type="date" type="date"
value={createdFrom} value={createdFrom}

View File

@@ -182,6 +182,8 @@ export interface BookingListFilter {
bookingType?: string; bookingType?: string;
/** CONTAINER or BULK. */ /** CONTAINER or BULK. */
freightType?: string; freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
/** IMPORT / EXPORT / DOMESTIC. */ /** IMPORT / EXPORT / DOMESTIC. */
tradeDirection?: string; tradeDirection?: string;
/** Narrow to a single operational profile (importer/exporter/freight_forwarder). */ /** Narrow to a single operational profile (importer/exporter/freight_forwarder). */

View File

@@ -88,6 +88,8 @@ export interface ContractListFilter {
contractKind?: string; contractKind?: string;
/** CONTAINER or BULK. */ /** CONTAINER or BULK. */
freightType?: string; freightType?: string;
/** Service type (rule-engine service_types.id). */
serviceTypeId?: string;
/** IMPORT / EXPORT / DOMESTIC. */ /** IMPORT / EXPORT / DOMESTIC. */
tradeDirection?: string; tradeDirection?: string;
companyProfileId?: string; companyProfileId?: string;