From 03b16b4ee8f8f458d0de1d9a53a9f59d6fbde67e Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 18 Aug 2026 11:38:07 +0000 Subject: [PATCH] feat: add service type fitler --- .../pages/bookings/BookingRequestsPage.tsx | 12 +++++++- .../pages/contracts/ContractRequestsPage.tsx | 18 ++++++++++- .../backoffice/src/services/api.ts | 7 +++++ .../src/services/bookings.service.ts | 4 +++ .../src/services/contracts.service.ts | 3 ++ .../src/pages/bookings/BookingsListPage.tsx | 29 +++++++++++++++++- .../src/pages/contracts/ContractsList.tsx | 30 ++++++++++++++++++- .../portal/src/services/bookings.service.ts | 2 ++ .../portal/src/services/contracts.service.ts | 2 ++ 9 files changed, 103 insertions(+), 4 deletions(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx index 8c2a3a912..d6819d940 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingRequestsPage.tsx @@ -130,6 +130,15 @@ export default function BookingRequestsPage() { [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) — // the header's document-review alarm opens exactly the undecided requests // 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), }, { 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 }, { // Wins over the `paymentStatus` filter above — the queue is by @@ -172,7 +182,7 @@ export default function BookingRequestsPage() { toParams: dateRangeParams("scheduledFrom", "scheduledTo"), }, ], - [filterOptions, yardOptions], + [filterOptions, yardOptions, serviceTypeOptions], ); const controls = useFilters(bookingFilterDefs, { defaultSort: "createdAt:DESC", pageSize: 10 }); diff --git a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx index cf87a1e1b..aa8b5ed8f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/contracts/ContractRequestsPage.tsx @@ -115,6 +115,15 @@ export default function ContractRequestsPage() { [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 // parse the URL and build API params. Counts are attached separately below, // for rendering only, once the summary query (which itself depends on @@ -143,6 +152,13 @@ export default function ContractRequestsPage() { multiple: false, options: FREIGHT_TYPE_OPTIONS, }, + { + key: "serviceTypeId", + label: "Service", + type: "enum", + multiple: false, + options: serviceTypeOptions, + }, { key: "paymentCurrency", label: "Currency", @@ -170,7 +186,7 @@ export default function ContractRequestsPage() { toParams: ({ v }) => ({ originYardId: v[0], destinationYardId: v[1] }), }, ], - [filterOptions, yardOptions], + [filterOptions, yardOptions, serviceTypeOptions], ); const controls = useFilters(filterDefs, { diff --git a/apps/edr-freight-web/backoffice/src/services/api.ts b/apps/edr-freight-web/backoffice/src/services/api.ts index 8d61698d1..b68589e34 100644 --- a/apps/edr-freight-web/backoffice/src/services/api.ts +++ b/apps/edr-freight-web/backoffice/src/services/api.ts @@ -2741,6 +2741,13 @@ export const api = { ({ id }) => QUERY_KEYS.BOOKINGS.byId(id), ), + referenceData: endpoint( + "bookings", + "referenceData", + () => bookingsService.getReferenceData() as Promise, + () => ["bookings", "reference-data"], + ), + remove: endpoint<{ id: string }, void>("bookings", "remove", ({ id }) => bookingsService.remove(id), ), diff --git a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts index dead9c520..c6ba796d2 100644 --- a/apps/edr-freight-web/backoffice/src/services/bookings.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/bookings.service.ts @@ -19,6 +19,8 @@ export interface BookingListFilter { /** Bookings drawn down under this contract (contract detail's Shipments tab). */ contractId?: string; freightType?: string; + /** Service type (rule-engine service_types.id). */ + serviceTypeId?: string; /** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */ bookingType?: string; /** '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.companyId) params.companyId = filter.companyId; if (filter.freightType) params.freightType = filter.freightType; + if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId; if (filter.bookingType) params.bookingType = filter.bookingType; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; @@ -176,6 +179,7 @@ export const bookingsService = { if (filter.companyId) params.companyId = filter.companyId; if (filter.contractId) params.contractId = filter.contractId; if (filter.freightType) params.freightType = filter.freightType; + if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId; if (filter.bookingType) params.bookingType = filter.bookingType; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; diff --git a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts index 01dbadac7..8c11af23d 100644 --- a/apps/edr-freight-web/backoffice/src/services/contracts.service.ts +++ b/apps/edr-freight-web/backoffice/src/services/contracts.service.ts @@ -13,6 +13,8 @@ export interface ContractListFilter { tab?: string; companyId?: string; freightType?: string; + /** Service type (rule-engine service_types.id). */ + serviceTypeId?: string; tradeDirection?: string; contractKind?: string; paymentCurrency?: string; @@ -171,6 +173,7 @@ function buildListParams(filter?: ContractListFilter) { if (filter.sortOrder) params.sortOrder = filter.sortOrder; if (filter.companyId) params.companyId = filter.companyId; if (filter.freightType) params.freightType = filter.freightType; + if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId; if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection; if (filter.contractKind) params.contractKind = filter.contractKind; if (filter.paymentCurrency) params.paymentCurrency = filter.paymentCurrency; diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingsListPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingsListPage.tsx index 175aec08f..925569be1 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingsListPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingsListPage.tsx @@ -374,6 +374,16 @@ export default function BookingsListPage() { const [debouncedQuery] = useDebouncedValue(query, 300); const [typeFilter, setTypeFilter] = useState(null); const [freightFilter, setFreightFilter] = useState(null); + const [serviceFilter, setServiceFilter] = useState(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("createdAt:DESC"); const [createdFrom, setCreatedFrom] = useState(""); const [createdTo, setCreatedTo] = useState(""); @@ -395,10 +405,11 @@ export default function BookingsListPage() { }; const hasExtraFilters = - !!typeFilter || !!freightFilter || !!createdFrom || !!createdTo; + !!typeFilter || !!freightFilter || !!serviceFilter || !!createdFrom || !!createdTo; const clearExtraFilters = () => { setTypeFilter(null); setFreightFilter(null); + setServiceFilter(null); setCreatedFrom(""); setCreatedTo(""); resetPage(); @@ -410,6 +421,7 @@ export default function BookingsListPage() { assignedToSchedule, bookingType: typeFilter ?? undefined, freightType: freightFilter ?? undefined, + serviceTypeId: serviceFilter ?? undefined, createdFrom: createdFrom || undefined, // include the whole selected end day createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, @@ -425,6 +437,7 @@ export default function BookingsListPage() { assignedToSchedule, typeFilter, freightFilter, + serviceFilter, createdFrom, createdTo, debouncedQuery, @@ -852,6 +865,20 @@ export default function BookingsListPage() { style={{ width: 150 }} aria-label="Filter by cargo type" /> + ({ value: o.value, diff --git a/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx b/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx index f465ca849..f32890a14 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/ContractsList.tsx @@ -71,6 +71,16 @@ export default function ContractsList() { const [disclaimerOpen, setDisclaimerOpen] = useState(false); const [freightFilter, setFreightFilter] = useState(null); const [kindFilter, setKindFilter] = useState(null); + const [serviceFilter, setServiceFilter] = useState(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(""); const [createdTo, setCreatedTo] = useState(""); const [expanded, setExpanded] = useState>(new Set()); @@ -96,10 +106,11 @@ export default function ContractsList() { const resetPage = () => setPagination({ pageIndex: 0, pageSize: pagination.pageSize }); const hasExtraFilters = - !!freightFilter || !!kindFilter || !!createdFrom || !!createdTo; + !!freightFilter || !!kindFilter || !!serviceFilter || !!createdFrom || !!createdTo; const clearExtraFilters = () => { setFreightFilter(null); setKindFilter(null); + setServiceFilter(null); setCreatedFrom(""); setCreatedTo(""); resetPage(); @@ -109,6 +120,7 @@ export default function ContractsList() { () => ({ contractKind: kindFilter ?? undefined, freightType: freightFilter ?? undefined, + serviceTypeId: serviceFilter ?? undefined, createdFrom: createdFrom || undefined, createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined, // Server-side free-text search (contract reference, company name). @@ -121,6 +133,7 @@ export default function ContractsList() { [ kindFilter, freightFilter, + serviceFilter, createdFrom, createdTo, debouncedQuery, @@ -322,6 +335,21 @@ export default function ContractsList() { styles={{ input: { height: 42 } }} aria-label="Filter by cargo type" /> +