Title and subtitle grew unbounded, relying only on flex-squeeze from action
buttons to ever truncate. Caps the container at 640px and gives the
subtitle the same truncate treatment the title already had, so a long
page title or description (report descriptions in particular) always
ellipses instead of stretching the header.
Swaps ReportView's bespoke ReportFilters for the same FilterBar/useFilters
combo BookingRequestsPage uses — pills, saved-view-ready state, URL sync.
toFilterDefs() maps the report catalog's own filter vocabulary
(daterange/date/select/multiselect/text) onto FilterDef, matched against
every report definition's backend param handling. The search box only
shows for reports that actually implement `search` server-side, so it's
not a dead control on the rest. ReportFilters.tsx is now dead, removed.
DateBody always initialized its op state to DEFAULT_OP.date ("between"),
ignoring a def's `operators` restriction. A single-operator exact-date
filter (e.g. `operators: ["before"]`) opened on the range UI with no way
off it, since OperatorSelect hides itself when there's only one choice.
Default to the def's first allowed operator instead.
Conflict in ClearanceDocumentsPage: this branch migrated the page to the
pill FilterBar, dev added filters to the Select stack it replaced. Kept
the FilterBar and carried dev's additions across as a "Booked by"
(customerKind) FilterDef plus the shipping-line search placeholder; dev's
startOfDayIso/endOfDayIso went away because dateRangeParams already does
that. The Ship icon import is needed by dev's shipping-line customer cell,
which merged cleanly on its own.
Three gaps on the backoffice customer detail page:
- Rejecting a change request or sending it back for correction notified
nobody. Adds CompanyNotifierService.changeRequestReturned, which pings
the customer desk with the reviewer, the outcome and the note. Marketing
joins that desk via customers:view + customers:get_notification in the
role preset — grants still come from the IAM UI, the preset only sets
the default for new environments.
- submitted_by / reviewed_by / actor_id were stored but never resolved, so
the History tab could say what changed but never who asked or who sent
it back. Resolves them through a shared iam-user-name util (deduped from
the private copy in contract-document-history.service) and renders
"Requested by" / "Sent back to marketing by" lines. The
changes_requested badge is relabelled to match the workflow.
- "View" opened an in-page modal one document at a time. Adds
openFileInNewTab, which opens the tab inside the click gesture and fills
it once the authenticated fetch resolves, and an "Open all" button that
loops over the documents table so every file lands in its own tab.
Backend already accepted containerSize ('20'|'40') on
CreateEmptyContainerReturnDto and enforces one-40ft-or-two-20ft-per-wagon
via assertWagonLoad — the Standalone Return modal just never collected it.
Add a Container Type select and wire it into the submit payload.
- Updated API documentation and summaries to reflect support for both USD and ETB invoices.
- Modified data structures to include trade direction for invoices.
- Adjusted UI components to accommodate manual payment confirmations and display relevant information.
- Implemented filtering options for currency in the manual payments worklist.
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.
The real live fleet page — FleetCrudPages.tsx (previous commit) turned
out to be dead code; every fleet route (locomotives/trains/wagons/
containers/cargoes/vehicles/drivers) actually renders this one,
config-driven off resources.ts. Highest-leverage single file in the
remaining inventory: 7 routes at once.
- filterDefs are built per slug from `config.listFilters` (status/yard/
wagon type/train…, wired to real server params via each def's
default `{key: value}` toParams) plus a "Registered" date range. The
3 slugs with no server list filters (trains/containers/cargoes) fall
back to a plain client-only Status filter off a fixed enum, not
"whatever status exists in the currently-loaded rows" — the latter
would be circular (filterDefs feeds useFilters, which feeds the
query that produces those rows).
- serverListFilters/pagedFilters derive straight from `controls.params`
instead of hand-picking each field off local state — the def keys
already match the API's param names, so this is mostly free.
- filteredRows keeps the original's exact "usesServerListFilters ⇒
trust the server, don't re-check client-side" short-circuit — some
server list filters (a wagon's `trainNumber` matches either of two
different columns) aren't expressible as a plain client-side field
equality, and re-applying them would silently break.
- Pagination moved from a local `usePagination()` (component state) to
useFilters' URL-backed page/pageSize — the whole point of this pass.
DataTable takes `controls.tableProps(total)` directly; FleetCardGrid
(not a DataTable) is fed the same pieces by hand.
- The two manual "reset on slug/filter change" effects are dropped —
a same-app nav Link to a bare path already clears the query string,
and useFilters already deletes `page` on every filter/search change.
- FleetToolbar's view-mode SegmentedControl moves into FilterBar's
`children` slot; its search/filters props are no longer used here.
GET billing/invoices/:id/document?format=thermal renders a dedicated 80mm
receipt template (72mm printable, 4mm margins each side), not a CSS variant
of the A4 layout — the A4 CSS is absolutely-positioned/fixed-px, tuned for a
210mm page, and doesn't reflow at thermal width. No seal (not a thermal
convention, renders badly on 1-bit thermal heads); line items stack
(description, then qty x rate = amount) instead of a table, since a real
table leaves ~10-14 chars for description at this width.
PdfRenderService gains a thermal render path: full 80mm-width viewport,
content height measured via page.evaluate after settle (continuous-roll
receipts have no fixed page length), and a noFallback option — a Chromium
failure throws a clear error instead of silently degrading to the generic
A4/no-QR fallback, which would hand back a different document than what was
asked for. The frontend surfaces that as a toast pointing at the existing A4
download.
format is strictly validated (a4|thermal only, BadRequestException
otherwise), not silently coerced.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds cancel, sales/withholding receipt filing + listing + PDF download, and
credit/debit memo issuance to EimsFilingCard, each gated on its own
permission.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Covers 6 pages at once: the shared FleetCrudPage<T> factory
(TrainMasterDataPage, WagonsCrudPage, ContainersCrudPage,
CargoesCrudPage, LocomotivesCrudPage) plus the standalone
WagonTypesCrudPage.
- FleetCrudPage gains an optional `statusOptions` prop; when passed it
builds a Status enum FilterDef and swaps the old plain search Input
for FilterBar + useFilters + applyClientFilters (client-bridge —
these endpoints return bare arrays). Status option lists sourced
from the actual entity/enum definitions, not guessed, and reused in
each page's create/edit form Select instead of duplicating them.
Column-header click-to-sort is untouched (separate mechanism).
- WagonTypesCrudPage (hand-rolled, not on the factory) gets a boolean
Active/Inactive filter the same way, replacing its search-text hack
that string-matched "active"/"inactive" against the query.
- Radio-based filters (single-select enum, boolean) apply the instant
an option is picked and close — no Apply click needed. Checkbox
(multi-select) keeps the explicit Apply, since picking several is a
multi-step gesture.
- Route filter's Select dropdowns portal separately by default, so a
click landed as "outside" the outer pill popover and closed it
before a pick registered — same class of bug DateBody had.
`comboboxProps={{ withinPortal: false }}` fixes it.
- Route filter now pinned by default on both pages instead of behind
More filters.
- Date filter: widened to before/between/after (repository already
applies each bound independently) via a new `dateRangeParams`
helper — the old positional `{v[0]:from, v[1]:to}` mapping silently
put a "before" pick in the from param. Added a calendar icon + sm
sizing, and a wider popover so the range presets sidebar has room.
- Pill's clear (X) button enlarged; search box border/text opacity
strengthened to match the pill trigger restyle.
- New "route" FilterType: RouteBody popover (searchable origin +
destination selects, apply once both are picked), wired into
ContractRequestsPage and BookingRequestsPage. Bookings already had
server-side originYardId/destinationYardId; contracts gets both new
(contract_routes is one-to-many, so origin/destination are separate
EXISTS subqueries, not a join).
- Inactive FilterPill trigger restyled to read like a closed Mantine
Select (opaque solid border, trailing chevron) instead of a dashed
"+" pill — trigger only, popover body/position unchanged.
- Search box text set to regular weight.
- A couple more filters (Direction, Freight) pinned by default per
page on top of the existing always-pinned ones; the rest stay behind
More filters.
- ruleEngineFooterProps.ts: toRuleEngineFooterProps() — the
useFilters-to-RuleEngineListFooter pagination adapter had already been
hand-written twice (TrucksOnSitePage, CompliancePage) with the same
pageSize-drop risk useFilters itself just had fixed; extracted before a
third copy could drift.
- CompliancePage, FuelPurchasePage, IncidentsPage: ListControls ->
FilterBar + applyClientFilters, same mechanical pattern as the
warehouse pages (search + one date range, endpoints take no params at
all per the inventory sweep — confirmed correct bucket, not assumed).
13 useState fields -> 11 FilterDef entries + useFilters. Notable pieces:
- the deep-link-follow useEffect is gone entirely — controls.values reads
live off the URL every render, so a link opened while the page is
already mounted (the header document-review alarm) just works, and
every filter auto-pins its own pill once it has a value (FilterBar's
secondary/pinned split), so a deep link can never land behind "More
filters" hidden. Confirmed the encoding stays back-compatible with the
existing ?statuses=A,B&tradeDirection=IMPORT links via url.test.ts.
- "Paid, not allocated" (a checkbox that overrides the payment-status
filter) became a boolean FilterDef with a custom toParams — ordered
after paymentStatus in the defs array so its toParams wins on merge,
matching the original override semantics exactly.
- booking kind and status stay pinned pills (always visible, matching the
old primary row); direction/freight/payment/ownership/yards/dates are
secondary (behind "More filters", matching the old advanced collapse).
fix(useFilters): pageSize was a hardcoded constant, never read from the
URL, with no setter — the DataTable/RuleEngineListFooter page-size
dropdown silently did nothing on every page using the filter bar,
including the already-shipped ContractRequestsPage pilot. Added a `size`
URL param (mirrors `page`), `setPageSize`, and wired
`tableProps().onPaginationChange` to route page-size vs page-index
changes to the right setter instead of only ever calling setPage().
feat(clientFilter): applyClientFilters — the Family-B bridge the plan
called for. Generalizes ListControls'/useListControls' one hardcoded
search box + one date range to every FilterDef, matched against
`row[def.key]`. Reuses matchesDayRange/toDayString from
hooks/useListControls.ts (imported, not duplicated) so the inclusive-
range/timezone-safe semantics stay defined in exactly one place. Lets a
page ship the full pill-bar UI immediately and flip to server-side
filtering later by deleting one function call — no endpoint changes
required up front.
Migrated to the filter bar (mechanical, pattern established by
ContractRequestsPage):
- WarehouseInvoicesPage, LoadedInventoryPage, TrucksOnSitePage — Family B
(ListControls/useListControls → FilterBar + applyClientFilters)
- CustomersPage — Family A (useState bag → useFilters), no filter pills
needed here (search + sort only), the SegmentedControl "view" stays
page-level tab state (like ContractStatusTabs), not a filter pill —
it now resets the page via controls.setPage(1) on change, the same
hazard useFilters already guards its own filters against
The two-zone layout (left: filters, right: sort+save) hardcoded
flexWrap: "nowrap" on the outer container so the right zone would never
get pushed below the left on desktop. On a phone that's the same
two-column squeeze with no room for either side: the search box and
right-hand controls got clipped/overlapped instead of stacking.
- outer container: flex-col below the `sm` breakpoint (stacks to two full-
width rows), flex-row + nowrap at `sm` and up (the original two-zone,
right-pinned behavior)
- right zone swapped from <Group wrap="nowrap"> to a plain div with
Tailwind classes — Group's `wrap` prop sets an inline flex-wrap style,
which always wins over a `sm:flex-nowrap` class regardless of
breakpoint, so the old responsive attempt could never have worked
- search input's hardcoded minWidth: 220 (wider than a lot of phones)
dropped to a shrinkable 160
- saved-view card grid: base 2 columns forced text-clipping card overflow
on a phone; base is now 1, growing to 2+ only once there's room
Replace the modal "type a name to save" flow with a one-click Save that
auto-generates the label from the active filters (describeQuery, reusing
the same formatter FilterPill uses for pill text — one source of truth,
nothing to type, nothing to fall out of sync). Saved views render as a
grid of cards at the top of the filter bar instead of being buried in a
dropdown.
- useSavedViews: localStorage state, extracted so FilterBar owns one
instance instead of two components each subscribing separately
- SaveViewButton: filled (not outline) since it's the one action-y
control among a row of filter pills; flashes to a disabled "Saved" +
checkmark state for 1.5s on click, in addition to the corner toast —
the toast alone wasn't registering as feedback
- SavedViewCards: click to apply, per-card delete, active one highlighted
- moved from the wrapping filter-pill row into the pinned right-hand zone
(with sort) behind its own divider, so it can't shove pills around
- Save icon: BookmarkPlus -> Save (floppy disk reads as "save" faster)
- More filters button: outline/gray -> default variant — same low-
contrast-outline problem the inactive filter pill had
- Clear button font bumped xs -> sm to match the rest of the bar
Replace the ad-hoc filter controls with a URL-linkable pill filter bar:
each filter is a pill that opens a type-aware popover (text/enum/date/
number/boolean, each with the right operator set), overflow filters live
behind a searchable "More filters" menu, sorting is a separate control,
and filter state round-trips through the URL query string (shareable,
back/forward-safe, backward compatible with existing ?statuses=A,B links).
Frontend (apps/edr-freight-web/backoffice/src/components/filters/):
- FilterDef schema + a pure url.ts codec (parse/serialize/toApiParams),
with a 24-case round-trip + malformed-input test suite
- useFilters hook driving react-query params straight from useSearchParams,
debounced search, saved views in localStorage (@mantine/hooks
useLocalStorage), page-reset-on-filter-change baked into one
setSearchParams call instead of a separate effect
- FilterBar/FilterPill/OperatorSelect/MoreFiltersMenu/SortControl +
per-type popover bodies (Mantine)
- ContractRequestsPage migrated end to end as the pilot
Backend (apps/edr-freight-api):
- pagination.util: applySort() — whitelisted sortBy resolved against a
per-module column map (never interpolated), with a mandatory `id ASC`
tiebreaker so paginating a non-unique sort can't drop/duplicate rows
- facets.util: computeFacets() — one GROUP BY per enum column, each
omitting its own predicate, so picking a value doesn't hide its siblings
- contracts/bookings: list-summary now returns real filter-scoped facet
counts (contracts' getStatusCounts was unfiltered/global; superseded)
- deleted drivers/vehicles findAllWithFilters — dead code that
interpolated an unwhitelisted sortBy straight into orderBy()
- migration: missing bookings(status)/wagons(status) indexes +
(created_at DESC, id ASC) partials on the hot list tables
UI polish pass: inactive pill uses the opaque "default" variant instead
of a faint tinted outline, active pill uses "light" not "filled", larger
X hit target, applied filters sort first, sort control separated behind
a divider on the right and wraps independently from the filter row,
popover option rows are fully clickable (count moved inside the native
label) with bigger hit area and font, fixed a real date-filter bug where
the calendar's own portal falsely registered as an "outside click" and
closed the popover, and fixed a timezone bug where bare YYYY-MM-DD
strings were parsed as UTC instead of local time (shifts a day for EAT).
Not in this commit: rollout to the other ~59 list pages, the Ethiopian-
calendar DateBody branch, and the Family-B (client-side) bridge mode —
tracked in the filter-bar plan.
Empties had no way onto a departure: the return record could name a train
but nothing seated it on a wagon. Export schedules now expose a loading
action that packs selected returns onto free wagons at one 40ft or two 20ft
each, enforced both in the picker and in the API (existing empties on the
schedule count against their wagon).
Adds container_size, train_schedule_id and wagon_sequence_no to
freight.empty_container_returns.
Advancing an empty container return to WAGON_ALLOCATED opened no dialog,
so the wagon_allocation_reference column stayed null and nobody knew which
departure carried the empty. Now the action opens a picker of scheduled
EXPORT trains (DRAFT/SCHEDULED) and records the selection.
- Added ShippingLineBookingCompletionController and associated service to handle the completion of shipping line bookings.
- Introduced a new module for booking completion to maintain module separation and avoid cyclic dependencies.
- Updated the train scheduling global rules to set default desk hours to 24 hours.
- Modified existing services and entities to accommodate the new booking completion logic.
- Enhanced the front-end components to support the new booking completion flow, including updates to the booking detail and bookings pages.
- Implemented validation and error handling for booking completion, ensuring that only approved bookings can be completed.
- Added migration to set default desk hours in the database.
- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
New logo-settings module (mirrors stamp-settings): single uploaded logo,
stored via FilesService/MinIO, injected as a data URL into invoice/receipt,
contract, warehouse, train-scheduling, and payment-receipt PDFs. Adds a
matching backoffice settings page and settings:logo:view/manage permissions.
>