mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
feat(bookings): filter by container count, export per-type quantities
Container filters on the booking-requests list:
- "Container type" — bookings carrying that type.
- "Containers" — a count of BOXES (booking_container is one row per line with
a quantity, so this sums quantity rather than counting rows), as an exact
value or a range. It reads the container-type filter when one is set, so the
one control answers both "10 containers in total" and "10 forty-footers".
Export gains a column per container type ("20FT containers", "40FT
containers"), plus the total "Containers" column and the two filters. Container
types are reference rows, not a constant, so `ExportDataset` gains an optional
`dynamicFields` resolver — DB-driven columns appended to the static list and
cached for the process, mirroring the existing `ExportFilterDef.optionsQuery`.
Adding a 45ft container type adds its column with no code change. The type id
is interpolated into raw SQL (ExportField.select has no parameter bag), so the
resolver drops any id that is not a uuid.
Also repoints the export's "Container VGM" column at the per-line sum. It was
projecting bookings.cargo_total_weight_vgm, which the portal wizard leaves at 0
for container freight — the same trap the tonnage fix addressed — so the column
read 0 for every portal-created container booking. Non-zero on dev data goes
from 54 to 170 of 208 container bookings.
This commit is contained in:
@@ -166,6 +166,15 @@ export default function BookingRequestsPage() {
|
||||
// already mounted just works, and every filter — direction included —
|
||||
// auto-pins its own pill the moment it has a value (FilterBar's `secondary`
|
||||
// split), so a deep link can never land behind "More filters" unseen.
|
||||
// Container types, flattened out of the reference data's size groups.
|
||||
const containerTypeOptions = useMemo(
|
||||
() =>
|
||||
(refData?.containers ?? []).flatMap((group) =>
|
||||
group.types.map((t) => ({ value: t.id, label: t.name || t.code })),
|
||||
),
|
||||
[refData],
|
||||
);
|
||||
|
||||
const bookingFilterDefs: FilterDef[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -221,6 +230,27 @@ export default function BookingRequestsPage() {
|
||||
secondary: true,
|
||||
placeholder: "Commodity, description or container type",
|
||||
},
|
||||
{
|
||||
key: "containerTypeId",
|
||||
label: "Container type",
|
||||
type: "enum",
|
||||
multiple: false,
|
||||
options: containerTypeOptions,
|
||||
secondary: true,
|
||||
},
|
||||
{
|
||||
// Counts boxes. Scoped to the container-type filter when one is set, so
|
||||
// this one control answers "10 containers" and "10 forty-footers" both.
|
||||
key: "containers",
|
||||
label: "Containers",
|
||||
type: "number",
|
||||
secondary: true,
|
||||
operators: ["is", "between"],
|
||||
toParams: (v) =>
|
||||
v.op === "between"
|
||||
? { containersMin: v.v[0], containersMax: v.v[1] }
|
||||
: { containersMin: v.v[0], containersMax: v.v[0] },
|
||||
},
|
||||
{
|
||||
key: "serviceTypeId",
|
||||
label: "Service",
|
||||
@@ -282,7 +312,13 @@ export default function BookingRequestsPage() {
|
||||
toParams: dateRangeParams("scheduledFrom", "scheduledTo"),
|
||||
},
|
||||
],
|
||||
[filterOptions, yardOptions, serviceTypeOptions, cargoTypeOptions],
|
||||
[
|
||||
filterOptions,
|
||||
yardOptions,
|
||||
serviceTypeOptions,
|
||||
cargoTypeOptions,
|
||||
containerTypeOptions,
|
||||
],
|
||||
);
|
||||
|
||||
const controls = useFilters(bookingFilterDefs, {
|
||||
|
||||
@@ -77,6 +77,11 @@ export interface BookingListFilter {
|
||||
cargoTypeId?: string;
|
||||
/** Contains-search over content: cargo description, commodity, container types. */
|
||||
cargoText?: string;
|
||||
/** Bookings carrying this container type; also scopes containersMin/Max to it. */
|
||||
containerTypeId?: string;
|
||||
/** Container count bounds — of containerTypeId when set, else of all types. */
|
||||
containersMin?: string;
|
||||
containersMax?: string;
|
||||
/** ONE_TIME | GENERAL_CONTRACT — the booking-kind tab filter. */
|
||||
bookingType?: string;
|
||||
/** 'true' → customs bookings, 'false' → self-clearance (non-customs). */
|
||||
@@ -205,6 +210,9 @@ export const bookingsService = {
|
||||
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
|
||||
if (filter.cargoTypeId) params.cargoTypeId = filter.cargoTypeId;
|
||||
if (filter.cargoText) params.cargoText = filter.cargoText;
|
||||
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
||||
if (filter.containersMin) params.containersMin = filter.containersMin;
|
||||
if (filter.containersMax) params.containersMax = filter.containersMax;
|
||||
if (filter.bookingType) params.bookingType = filter.bookingType;
|
||||
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
||||
if (filter.paymentCurrency)
|
||||
@@ -246,6 +254,9 @@ export const bookingsService = {
|
||||
if (filter.serviceTypeId) params.serviceTypeId = filter.serviceTypeId;
|
||||
if (filter.cargoTypeId) params.cargoTypeId = filter.cargoTypeId;
|
||||
if (filter.cargoText) params.cargoText = filter.cargoText;
|
||||
if (filter.containerTypeId) params.containerTypeId = filter.containerTypeId;
|
||||
if (filter.containersMin) params.containersMin = filter.containersMin;
|
||||
if (filter.containersMax) params.containersMax = filter.containersMax;
|
||||
if (filter.bookingType) params.bookingType = filter.bookingType;
|
||||
if (filter.tradeDirection) params.tradeDirection = filter.tradeDirection;
|
||||
if (filter.paymentCurrency)
|
||||
|
||||
Reference in New Issue
Block a user