Commit Graph

3641 Commits

Author SHA1 Message Date
Nathnael
7c6c894c78 Merge branch 'freight/nati-2' into freight/feat/element-chat 2026-08-17 12:53:39 +00:00
Nathnael
ab734aecc3 feat(chat): join users to rooms on sign-in 2026-08-17 12:53:08 +00:00
Nathnael Wondisha
ab2173e026 Merge pull request #1323 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-17 15:45:27 +03:00
Nathnael
7aa4405423 Merge branch 'dev' into freight/nati-2
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.
2026-08-17 12:43:35 +00:00
Nathnael
7143ba1040 feat(customers): notify marketing on returned changes, name actors in history
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.
2026-08-17 12:38:06 +00:00
Abubeker Yasin
e8e136193d Merge pull request #1322 from Tria-plc/alpha
fix: ( excess-baggage ) pay in the selected method's currency and rec…
2026-08-17 15:36:37 +03:00
Abubeker Yasin
fa16087a4a fix: ( excess-baggage ) pay in the selected method's currency and record settlement 2026-08-17 15:33:43 +03:00
Hagernesh
0999bc0b8b fix(eims): a mapper failure after reservation also orphaned the block
toEimsInvoice/buildEimsContext sat outside the try/catch that calls
settleFailure — reservation happens (TX1), then request-building ran
unguarded, then submit() was the only thing actually wrapped. Any
exception during mapping (a validation error like an unmapped buyer
country, or a bug) skipped settleFailure entirely and left the
reservation permanently held: exactly the live incident just seen —
register 500'd, and every subsequent attempt on any invoice 409'd
'already in flight' until manually resolved.

Fix: the try block now starts right after reserve(), covering
request-building and submit() both. settleFailure's determinism
check is generalized to match — any error that is not an
EimsApiException is pre-wire and safe to release, not just
EimsConfigException (still labeled CONFIG; everything else pre-wire
is now labeled the new LOCAL kind). This is exhaustive by
construction: every error that actually touches the wire is already
normalized to EimsApiException inside EimsClientService.send()'s own
catch, so nothing outside that can be ambiguous.
2026-08-17 12:09:25 +00:00
Hagernesh
0464a44de4 fix(eims): a config error before signing must not block the whole system
settleFailure() treated any non-EimsApiException error as ambiguous
("might have reached MoR") and permanently blocked all further
filing until manually resolved. EimsConfigException (bad/missing
key, unparseable cert) is thrown by EimsSignerService before
EimsClientService.send()'s try/catch is even entered — by
construction it never reached the wire, so there is nothing
ambiguous about it.

This is exactly what happened live: a private-key parse failure
during the key/cert migration work reserved a counter, failed before
any HTTP call, and got treated as an unresolved in-flight submission
— blocking every other invoice from filing until someone manually
POSTs /eims/resolve.

Fix: EimsConfigException is now deterministic in settleFailure, same
treatment as a clean MoR rejection — both counters roll back, no
system-wide block, invoice marked FAILED (not UNKNOWN). Added a
CONFIG failure kind so the invoice's eimsLastError and the staff
alert both say plainly that the request never reached MoR, instead
of implying a MoR rejection.
2026-08-17 12:09:25 +00:00
Hagernesh
c04859d333 fix(warehouses): add container size to standalone container-return form
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.
2026-08-17 11:03:22 +00:00
Hagernesh
13f7bea590 feat(eims): accept private key/cert as raw PEM env vars
EIMS_PRIVATE_KEY / EIMS_CERTIFICATE — the PEM text pasted directly,
no encode/decode step at all. Precedence: raw PEM > base64 > path.

Motivated by the base64 path hitting a DECODER::unsupported error in
a live deployment with no way to tell whether the cause was transport
truncation, double-encoding, or an actually-bad file. Two fixes for
that class of problem together:
  - the raw-PEM var removes the encode/decode step entirely, so
    there's nothing left to corrupt in transit
  - a literal \\n (two chars) is unescaped to a real newline, for
    env stores that can't hold a literal line break
  - getPrivateKey() now checks the decoded bytes look like a PEM
    header before handing them to OpenSSL, so a still-bad value fails
    with byte count + safe preview instead of an opaque decoder error
2026-08-17 11:03:22 +00:00
Hagernesh Tadesse
13b0274da2 Merge pull request #1318 from Tria-plc/eims-integration
feat(eims): allow private key/cert as inline base64 env vars
2026-08-17 12:24:36 +03:00
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
marshal
76a1b98384 Merge pull request #1316 from Tria-plc/freight_feature/usermanagement
Enhance manual payment processing for USD and ETB invoices
2026-08-17 12:13:57 +03:00
Marshal
1ca7776143 Enhance manual payment processing for USD and ETB invoices
- 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.
2026-08-17 09:13:09 +00:00
marshal
8490bb65ab Merge pull request #1314 from Tria-plc/freight_feature/usermanagement
fix issue
2026-08-17 11:02:06 +03:00
Nathnael
00bd1250ee feat: WIP element Chat intergration 2026-08-17 08:02:00 +00:00
Marshal
df488ebfaa chnages 2026-08-17 08:01:29 +00:00
marshal
88dd8894d1 Merge pull request #1312 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-17 10:57:20 +03:00
Marshal
c3c3d08a41 fix issue 2026-08-17 07:55:52 +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
52fb705a3e changes 2026-08-17 06:18:16 +00:00
Marshal
0e228bfd8d changes 2026-08-17 06:14:25 +00: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
9f53114778 feat: enhance booking management with shipping line support and cargo handling improvements 2026-08-15 18:48:33 +00:00
Marshal
156fa9d2e4 fix: improve handling of container sizes in wagon cancellation logic 2026-08-15 11:54:35 +00:00
Marshal
b22f8c838e fix issue 2026-08-15 10:34:05 +00: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
Nathnael
aae0066a5d feat(filters): migrate ClearanceDocumentsPage to the pill filter bar
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.
2026-08-15 08:42:14 +00:00
Nathnael
8ba8376f45 feat(filters): migrate FleetResourcePage to the pill filter bar
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.
2026-08-15 08:39:59 +00: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
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