Commit Graph

4658 Commits

Author SHA1 Message Date
Hagernesh
4a4b1981cb feat(eims): allow private key/cert as inline base64 env vars
EIMS_PRIVATE_KEY_BASE64 / EIMS_CERTIFICATE_BASE64, alternative to the
existing _PATH vars. Wins over the path when set; falls back to the
file otherwise. Neither var required at boot on its own — the
either/or check moved out of the flat REQUIRED_VARS list.

Lets a dockerized deployment receive the key/cert the same way it
already receives every other EIMS_* secret (plain env var into the
container) instead of needing a host bind mount into the container
filesystem.
2026-08-17 09:14:38 +00:00
Hagernesh
28c9dd93e0 feat(eims): seller identity enriched from e-Trade, cached (bootstrap only)
Ten EIMS_SELLER_* env vars were the only source of EDR's own seller identity,
duplicating data the platform already has via the same e-Trade lookup used
for every customer company at onboarding. EimsSellerCacheService now enriches
it — but static config remains the source of truth: MoR validates
SellerDetails against its own taxpayer registry (rule 7017, already cleared
against the current static values), so e-Trade fills a field only when the
static value is blank, never overrides one already confirmed. The static
config is therefore the durable fallback, not the cache; an in-memory
snapshot lost on restart is harmless.

ETradeService has no request timeout of its own and no AbortController, so
the cache enforces one locally (stops waiting, doesn't cancel the request)
and de-duplicates concurrent refresh() calls into the same in-flight promise.
getSellerDetails() is fully synchronous — zero I/O — so live registration
never depends on e-Trade being reachable, at boot or per invoice.

VatNumber and Email stay on static config permanently — confirmed by reading
e-Trade's actual response shapes, neither field exists anywhere in what it
returns. Region/Wereda/City reuse the existing EIMS_BUYER_*_CODES maps rather
than adding seller-specific ones — the geography is objective, not
buyer-specific.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:04 +00:00
Hagernesh
09bc7c74d7 feat(eims): derive buyer City/Country from company profile, not global config
City: EimsMapperContext.buyerCity was declared but never wired anywhere —
always null, silently, for every buyer. No dedicated city column on Company;
derives from Zone via a new EIMS_BUYER_CITY_CODES map, same lookup mechanism
as Region/Wereda but optional (an unmapped zone resolves to null rather than
throwing) — MoR has already accepted a live filing with City null.

Country: previously a single flat EIMS_BUYER_COUNTRY_CODE applied to every
buyer regardless of Company.country. Now reads company.country, resolved via
a new EIMS_BUYER_COUNTRY_CODES name-to-code map; the flat env var becomes a
domestic-only fallback (applies only when country is empty/Ethiopia), so an
unmapped foreign buyer fails locally instead of silently filing as Ethiopia.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:04 +00:00
Hagernesh
562ebf485c fix(eims): thermal page-length used viewport height, not content height
scrollHeight is defined as the larger of an element's content height and its
own (viewport) height — for a receipt shorter than the placeholder 1123px
viewport, it silently returned the viewport height back, producing a
correctly-formatted but page-length-tall PDF with a huge trailing blank
strip below the real content. Found by actually rendering one and looking
at it, not caught by unit tests (buildThermalHtml is pure string output,
never exercises page.pdf() sizing).

Fix: use a deliberately tiny (100px) viewport height for the thermal
measurement pass, forcing content to overflow it so scrollHeight always
reflects the receipt's real height. Also round the computed mm value before
templating it into the CSS length string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-17 07:43:03 +00:00
Abubeker Yasin
f038bd5b48 Merge pull request #1309 from Tria-plc/alpha
feat: ( backoffice ) gate Master Data pages and sidebar on view permi…
2026-08-17 10:24:28 +03:00
Abubeker Yasin
41080c1650 feat: ( backoffice ) gate Master Data pages and sidebar on view permissions 2026-08-17 10:22:52 +03:00
Abubeker Yasin
1626bc2a2c Merge pull request #1308 from Tria-plc/alpha
feat: ( backoffice ) hide Generate Ticket action without tickets:gene…
2026-08-17 10:05:06 +03:00
Abubeker Yasin
b453b81ff5 feat: ( backoffice ) hide Generate Ticket action without tickets:generate 2026-08-17 09:55:02 +03:00
Abubeker Yasin
70663f91da Merge pull request #1307 from Tria-plc/alpha
fix: ( payments ) restrict force-confirm to tickets:generate permission
2026-08-17 09:48:40 +03:00
Abubeker Yasin
b9e000729d fix: ( payments ) restrict force-confirm to tickets:generate permission 2026-08-17 09:47:36 +03:00
marshal
a2c30a3c96 Merge pull request #1303 from Tria-plc/freight_feature/usermanagement
feat: enhance booking and scheduling features with shipping line supp…
2026-08-16 11:20:09 +03:00
Marshal
5ce5cbe29d feat: enhance booking and scheduling features with shipping line support and improved search functionality 2026-08-16 08:18:40 +00:00
marshal
c331df3eba Merge pull request #1300 from Tria-plc/freight_feature/usermanagement
feat: enhance booking management with shipping line support and cargo…
2026-08-15 21:54:07 +03:00
Marshal
9f53114778 feat: enhance booking management with shipping line support and cargo handling improvements 2026-08-15 18:48:33 +00:00
marshal
9c770df509 Merge pull request #1299 from Tria-plc/freight_feature/usermanagement
fix: improve handling of container sizes in wagon cancellation logic
2026-08-15 14:55:57 +03:00
Marshal
156fa9d2e4 fix: improve handling of container sizes in wagon cancellation logic 2026-08-15 11:54:35 +00:00
marshal
9e21934983 Merge pull request #1298 from Tria-plc/freight_feature/usermanagement
fix issue
2026-08-15 13:34:35 +03:00
Marshal
b22f8c838e fix issue 2026-08-15 10:34:05 +00:00
marshal
fa7f6e507a Merge pull request #1297 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-15 13:17:37 +03:00
Marshal
ca9f631f6f Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement 2026-08-15 10:12:59 +00:00
Marshal
dc38e843a6 feat: full wagon cancel, leg board, wagon dates
feat(freight): editable train leg times, SL invoice payer
2026-08-15 10:12:37 +00:00
Marshal
c9c2d4dcb3 booking cancellation 2026-08-15 08:53:24 +00:00
Hagernesh Tadesse
a01a574bd3 Merge pull request #1296 from Tria-plc/eims-integration
Eims integration
2026-08-15 11:44:17 +03:00
Hagernesh
d40c340e1c feat(billing): 80mm thermal invoice layout (ADD-P001)
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>
2026-08-15 08:38:35 +00:00
Hagernesh
47c2e82839 feat(freight-backoffice): EIMS cancel/receipt/memo actions in filing card
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>
2026-08-15 08:38:35 +00:00
Hagernesh
8e70352407 feat(eims): printable receipt PDF with RRN and QR
eims-receipt-document.mapper.ts maps an EimsReceipt onto the shared
InvoiceDocumentModel layout, reading amounts back out of the stored request
body. Refuses to render anything not REGISTERED. GET
invoices/:id/eims/receipts/:receiptId/document, scoped to the invoice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 08:38:35 +00:00
Hagernesh
d66cfe2328 feat(billing): issue credit/debit memos against registered invoices
POST billing/invoices/:id/memo files a MoR DEB/CRE memo by reusing
createInvoice unchanged. sourceId is the original invoice's own id, not its
source's — this structurally keeps memos out of findPayable/expirePayable/
billQuery's sourceId-keyed lookups regardless of status. Credit notes are
created settled; debit notes are created open/unpaid as a genuine new
receivable, not force-settled. memoIssue is granted to the chief position,
not the general finance role.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 08:38:35 +00:00
Hagernesh
87c51d7676 fix(eims): drain pre-reservation rejections in auto-submit sweep
A BadRequestException thrown before reserve() (config assertion, DEB/CRE
validation) left the invoice NOT_SUBMITTED with nothing persisted, so the
same row was retried every tick forever — a permanent head-of-line block on
every invoice behind it. Now marked FAILED, guarded by a fresh status
re-read so a reservation's own SUBMITTING/UNKNOWN/blocked state is never
clobbered.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-15 08:38:35 +00:00
Hagernesh
83d9265e85 Credit and Debt plus reason attribute inide register 2026-08-15 08:38:35 +00:00
Nathnael Wondisha
0adbc096a1 Merge pull request #1295 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-15 11:15:39 +03:00
Nathnael
69805315d9 feat: dev and staging bypass 2026-08-15 08:10:47 +00:00
Nathnael
6c4969f3fd feat(filters): migrate FleetCrudPages to the pill filter bar
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.
2026-08-15 07:51:31 +00:00
Nathnael
4b4ab21ea1 fix(filters): auto-apply radios, route popover bug, date operators, polish
- 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.
2026-08-15 07:31:19 +00:00
Nathnael
839dfdbae3 feat(filters): route filter, select-styled trigger, tune default pin set
- 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.
2026-08-15 07:23:43 +00:00
Nathnael
e5e75b4166 feat(filter-bar): migrate 3 more fleet pages, extract footer adapter
- 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).
2026-08-14 14:13:11 +00:00
Nathnael
fa32fa6a96 feat(filter-bar): migrate BookingRequestsPage (richest Family A page)
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).
2026-08-14 14:08:08 +00:00
Nathnael
d94c444a51 feat(filter-bar): fix pageSize bug, add client bridge, migrate 4 more pages
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
2026-08-14 14:03:33 +00:00
Nathnael
a15d4d922e Merge branch 'dev' into freight/nati-2 2026-08-14 13:52:23 +00:00
Nathnael
524f9aab56 fix(filter-bar): responsive layout broken on narrow viewports
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
2026-08-14 13:51:48 +00:00
Nathnael
bec9575311 refactor(filter-bar): saved views as auto-named cards, not a name-prompt menu
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
2026-08-14 13:48:54 +00:00
Abubeker Yasin
086c4fe8c9 Merge pull request #1293 from Tria-plc/alpha
fix: ( portal ) opt out of browser translation to stop booking crash
2026-08-14 16:41:35 +03:00
Abubeker Yasin
d952792e15 fix: ( portal ) opt out of browser translation to stop booking crash 2026-08-14 16:37:51 +03:00
Nathnael
4a4d3077d7 feat: Stripe-style filter bar for freight backoffice (pilot: contracts)
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.
2026-08-14 13:18:46 +00:00
Hagernesh Tadesse
60f0824fd3 Merge pull request #1291 from Tria-plc/eims-integration
feat(train-scheduling): show loaded empties on the marshalling document
2026-08-14 14:39:22 +03:00
Hagernesh
1adc19baef feat(train-scheduling): show loaded empties on the marshalling document
Empty containers loaded onto an export departure carry no booking and no
wagon allocation, so their wagons printed as EMPTY — no cargo allocated —
staff checking the paper against the consist found boxes the list denied.
Those wagons now print the container numbers with cargo type EMPTY
CONTAINER, count toward the 40ft/20ft tallies, and get their own summary
tile; only genuinely bare wagons keep the empty wording.
2026-08-14 11:30:25 +00:00
Hagernesh Tadesse
4a836428c0 Merge pull request #1290 from Tria-plc/eims-integration
feat(train-scheduling): load returned empties onto an export train
2026-08-14 14:18:31 +03:00
Hagernesh
b4f2ec1c75 feat(train-scheduling): load returned empties onto an export train
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.
2026-08-14 09:57:50 +00:00
Hagernesh Tadesse
30c3567048 Merge pull request #1289 from Tria-plc/eims-integration
feat(warehouses): pick export train when allocating empty return to w…
2026-08-14 11:25:39 +03:00
Hagernesh
daee3eac0e feat(warehouses): pick export train when allocating empty return to wagon
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.
2026-08-14 08:17:35 +00:00
Hagernesh Tadesse
48762dccd1 Merge pull request #1288 from Tria-plc/eims-integration
feat(billing): add PNR row to freight invoice from booking payment re…
2026-08-14 10:28:06 +03:00