Commit Graph

991 Commits

Author SHA1 Message Date
Nathnael
17c2dca3cc feat(reports): record loading and unloading time per stop
The OCC report publishes total loading and unloading time and the other
activity left over from a station stay, but nothing recorded when handling
started or ended — the July 2026 seed had to write the figure into a
checkpoint note. Four nullable stamps now ride the stop's arrival row, which
is the row the staying-time report builds a stay from (a turnaround's
departure belongs to a different schedule).

Handling is unloading start to loading end, so a container stop reads as one
window and a bulk station that only loads or only unloads still reports its
half; other activity is the rest of the stay. Both stay NULL where nothing
was logged rather than collapsing to zero.

- station-staying-time: + loading/unloading and other activity per stop
- turnaround-cycle: + the same, summed over the cycle's stops
- loading-unloading (new): per train per station per period, so a week or
  month view is that train's average over its stops
- the stop/stay query moves to operations-classification, shared by both
2026-08-24 12:09:04 +00:00
Nathnael
ee2caa332c Merge branch 'dev' into freight/nati-2 2026-08-24 08:38:37 +00:00
Nathnael
c3894462e7 feat(billing): search invoices by PNR and transaction ref
Both are numbers a customer or a provider support desk quotes back, so they
belong in the free-text box rather than behind a filter pill.

The transaction id and merchant order id are plain ORs — the payment alias is
already joined by every caller of applyInvoiceFilters. The PNR folds into the
existing booking EXISTS block instead of adding a second subquery, so it
inherits that block's correlation and also matches warehouse-, first-mile-
and last-mile-sourced invoices, not just booking-sourced ones.

bk is promoted to alwaysJoin now that the export's scope() references it.
2026-08-24 07:03:08 +00:00
Nathnael
3da00e1f06 feat(billing): show the booking PNR on an invoice
The PNR is the CBE_BILL reference the customer actually pays against, but it
is stamped onto the booking at payment-initiation time — it is a column on
neither the invoice nor the payment. Both surfaces read it back by source id,
the same lookup the sealed invoice PDF already did, so screen, export and
document now agree.

The export's join casts bk.id::text rather than i.source_id::uuid: source_id
is a bare varchar pointer that is not always a UUID (EIMS self-test rows
carry a slug), and casting that direction throws on those rows.
2026-08-24 07:02:46 +00:00
Nathnael
018505d6fa feat(billing): filter, show and export invoice payment method
The settled method is split across two stores: a gateway settlement records
the real provider on the linked freight.payments row (cbe-bill, telebirr)
while the invoice's own payments ledger only writes a flat "GATEWAY"; a
manual settlement has no payments row at all and the ledger is the only
source (BANK_TRANSFER, OFFLINE, or whatever PayInvoiceDto.method carried).

invoicePaymentMethodExpr folds both into one UPPER_SNAKE vocabulary —
provider first, newest ledger entry as the fallback — and the list filter,
the export field and the export filter all use that same expression, so the
screen and the file can never disagree.

The paymentMethods param is deliberately not validated against a fixed list:
the manual pay endpoint takes a free-form method, so an IsIn would silently
drop real values.
2026-08-24 07:02:27 +00:00
Hagernesh
c937290019 feat(eims): resolve buyer geography from the MoR location master
BuyerDetails Country/Region/City/Wereda now resolve from the Ministry's own
EIMS_COUNTRY_REGION_VW master instead of the EIMS_BUYER_*_CODES env maps and
the ethiopia-geo-codes table. Both invented their codes and looked names up
globally, so KERSA/GORO/BABILE/BURE — each present in several zones with
different LOCALITY_NOs — could be filed against the wrong jurisdiction.

Resolution is hierarchical and refuses to guess: an unknown or ambiguous
address raises a local validation error naming the level that failed, and
never selects the first matching row. Spelling differences between EDR and
MoR live in a reviewed, parent-scoped alias layer; the dataset itself stays
verbatim so it remains traceable to the Ministry sheet.

Resolution now runs before the counter reservation in both the single and
bulk paths, so a bad company address no longer burns an EIMS sequence number.

Adds eims:import-locations to regenerate the dataset from a future workbook,
reporting duplicate rows and same-hierarchy code conflicts.
2026-08-24 06:59:50 +00:00
Marshal
58f88b74f8 add contract link 2026-08-23 05:00:43 +00:00
Marshal
716444e60a feat: add pagination to schedule history and consolidation approvals
- Implemented pagination in ScheduleHistoryPanel to manage large history entries.
- Updated API to support pagination parameters for schedule history.
- Enhanced ConsolidationApprovalsPage with tabbed navigation and pagination for approval rows.
- Introduced new types for paginated responses in bookings and train scheduling services.
- Added a database migration to create an index on wagon_booking_allocations for performance improvements.
2026-08-23 04:51:34 +00:00
Marshal
e2189040fa feat: add pagination to schedule history and consolidation approvals
- Implemented pagination in ScheduleHistoryPanel to manage large history entries.
- Updated API to support pagination parameters for schedule history.
- Enhanced ConsolidationApprovalsPage with tabbed navigation and pagination for approval rows.
- Introduced new types for paginated responses in bookings and train scheduling services.
- Added a database migration to create an index on wagon_booking_allocations for performance improvements.
2026-08-23 04:49:58 +00:00
marshal
105879b531 Merge pull request #1399 from Tria-plc/freight_feature/usermanagement
feat(train-scheduling): mid-route consist changes, audit history, saf…
2026-08-23 06:59:27 +03:00
Marshal
8e6fc09aac feat(train-scheduling): mid-route consist changes, audit history, safer workspace
- planned couples: loose wagons join the train at a route stop, added
  from the schedule yards tab; capacity credits them per corridor edge
  and coupling validates locomotive weight/length caps per leg
- real-cut toggle: a cut wagon permanently leaves the train build at
  its cut yard (soft cut still sits out one trip only)
- fix heaviest-leg display counting a shared slot's full cargo on
  every spanned edge (phantom pull-weight overload on S-2026-00045)
- confirmation dialogs for workspace add/load/unload/remove actions
- train-builder History and Detached-wagons tabs, backed by paginated
  endpoints; builder detaches now always write adjustment-log rows

Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and
3670 (adjustment log train_schedule_id nullable) — both applied to the
dev DB by hand; watch mode does not run migrations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-23 03:55:54 +00:00
Hagernesh
7f5e8349da Intercity andd import export gating 2026-08-22 09:17:28 +00:00
marshal
e2dd761203 Merge pull request #1387 from Tria-plc/freight_feature/usermanagement
fix issue
2026-08-22 03:50:34 +03:00
Marshal
b6c1efa043 fix issue 2026-08-22 00:49:53 +00:00
marshal
ed57490575 Merge pull request #1382 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-22 02:17:34 +03:00
Marshal
09deecd04c fix issue and add consolidation 2026-08-21 23:16:06 +00:00
Nathnael Wondisha
a0822f9d4f Merge pull request #1381 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-21 22:48:14 +03:00
ghost2023
23b3f265e6 feat(operations-targets): plan at any of the eight report grains
Targets could only be committed weekly, monthly, quarterly or yearly, so a
figure the business quotes per half-year or per 90 days had to be split by
hand into buckets it was never expressed in. The reports already re-gather
a target into whatever grain the viewer asks for; this just lets the plan
be entered at the grain it was agreed in.

Adds day, half-year, nine-month and 90-day, matching the report units added
alongside. normalisePeriodStart snaps each to its block start with the same
calendar-year anchoring the SQL uses — Jan/Jul for half-years, Jan/Oct for
nine-months, days 1/91/181/271 for 90-day blocks, including the same cap on
the fourth block so late December does not snap into a stub of its own.

That agreement is the load-bearing part. The unique index is keyed on
period_start, and a target snapped to a boundary the report does not bucket
on is a plan measured against a period that does not exist. The two halves
live in different files and different languages, so the spec pins the
boundaries rather than trusting them to stay in step.

Adds operations-targets.service.spec.ts, which the module had none of:
every period type, idempotency, the leap year, and the block-four cap.
2026-08-21 17:40:51 +03:00
ghost2023
c8b44d4d04 fix(operations-targets): derive cargo category from its dimension
A station target's plan is per station AND per cargo type; the other two
dimensions already carry the category inside dimension_key. update() kept
whatever was stored whenever the payload omitted the key, and the admin
form builds its payload from visible fields only — so switching "Plan by"
away from Station left the old category behind on a row that no longer has
any use for one.

That row is not merely untidy. It survives the COALESCE(cargo_category,'')
unique index alongside the legitimate null-category row for the same key,
plannedRowsSql groups by the column, and the two plan rows then join the
same operated row through the FULL OUTER JOIN: the category lists twice,
each line carrying the full operated tonnage, while the summary tiles are
computed separately and stay correct — so the table disagrees with its own
totals and nothing says why.

create() and update() now resolve the slot through one place, which is the
point: the two paths cannot drift again. cargoCategory is derived from the
effective dimension rather than carried over, and a station target without
one is rejected instead of stored as a plan the report can never match.

dimensionKey is checked against the vocabulary the reports actually emit —
CARGO_CATEGORIES / CONTAINER_CLASSES, or live yards.code for stations. An
unknown key used to store fine, list fine and fall back to showing the raw
key, while being a plan no report would ever find.

Also drops @Global from the module. Nothing outside it injects either
service; the reports read both tables in raw SQL, so the docstring's stated
reason for being global was not true.
2026-08-21 17:37:21 +03:00
Nathnael Wondisha
4a295a73a4 Merge pull request #1379 from Tria-plc/freight/nati-2
Freight/nati 2
2026-08-21 16:09:24 +03:00
Hagernesh Tadesse
4eb840c090 Merge branch 'dev' into eims-bulk-register 2026-08-21 16:03:03 +03:00
ghost2023
2317684db9 feat(fleet): add last-maintenance date filter to wagons list
Adds maintenanceFrom/maintenanceTo to WagonListFilters and wires a
wagons-only "Last maintenance" date-range filter into FleetResourcePage,
alongside the existing Registered filter. Server-paged, so the range is
resolved by the API (see wagons.service.ts).
2026-08-21 15:55:02 +03:00
ghost2023
503ea30f58 feat(overview): resolve dashboard layout from permissions, not role keys
role-dashboards.config.ts's ROLE_LAYOUTS was a hand-typed table of ~40
raw role/position keys (including edr_freight_app/xxx ad-hoc
sub-positions), matched against the user's held keys client-side.
Replaced with resolveOverviewLayout(), which just picks the
highest-priority key out of whatever GET /overview/layouts returns —
the same 'server filters by permission, frontend renders what comes
back' shape Reports already uses, so the frontend no longer needs to
know any individual permission key.

Hard cutover: no fallback to the old key table. Positions not covered
by the previous commit's grants (the ad-hoc department sub-positions)
will render the executive layout until granted a layout permission.
2026-08-21 15:43:44 +03:00
Nathnael
25d13baa6f chore: reporting and filtering 2026-08-21 09:25:21 +00:00
Marshal
a339ea620e feat(train-scheduling): add showWagonStat prop to control wagon stats visibility 2026-08-21 08:59:44 +00:00
Marshal
a9c6b85c08 feat(clearance): preview charge documents before and after upload 2026-08-21 08:59:06 +00:00
Marshal
2f894ba51b changes 2026-08-21 08:58:27 +00:00
Marshal
76bfbe1b17 feat: enhance GlDjiboutiClearanceListPage with tab navigation and improved filtering
- Added tab navigation for All, Import, Export, and On hold shipments.
- Implemented a new LivePill component to show real-time updates.
- Refactored shipment filtering logic to accommodate new tab functionality.
- Improved UI elements including buttons and text inputs for better user experience.
- Updated styles for table cells to allow text wrapping for long company names.
- Adjusted theme to use Space Grotesk font for headings.
2026-08-21 08:58:27 +00:00
Marshal
c5909b8ec7 [200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries 2026-08-21 08:47:47 +00:00
Marshal
4c549029fe feat(clearance): preview charge documents before and after upload 2026-08-21 07:04:22 +00:00
Marshal
d99b294c21 fix issue 2026-08-20 18:10:29 +00:00
Marshal
b72cbab535 changes 2026-08-20 18:06:13 +00:00
Marshal
934ed43ccb feat: enhance GlDjiboutiClearanceListPage with tab navigation and improved filtering
- Added tab navigation for All, Import, Export, and On hold shipments.
- Implemented a new LivePill component to show real-time updates.
- Refactored shipment filtering logic to accommodate new tab functionality.
- Improved UI elements including buttons and text inputs for better user experience.
- Updated styles for table cells to allow text wrapping for long company names.
- Adjusted theme to use Space Grotesk font for headings.
2026-08-20 17:44:52 +00:00
Marshal
9e1d5ee9f2 [200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries 2026-08-20 15:45:42 +00:00
Hagernesh
2e28b10610 dashboard UI 2026-08-20 13:48:02 +00:00
Hagernesh
7f772efcb3 feat(warehouses): make dashboard date range + warehouse filter real
Backs the previously-dead header date-picker and Filters button with
an actual scoped query. getDashboard() takes optional dateFrom/dateTo/
warehouseId: received (renamed from receivedToday) is the only
activity counter and respects the date range, defaulting to today when
omitted; the 10 status-backlog counters respect warehouseId only;
totalWarehouses/emptyContainers/importTrains/exportTrains stay global
since nothing ties them to a specific warehouse in the schema. Adds a
warehouse Select + date-range picker to the dashboard header, with a
caption spelling out what is and isn't scoped.
2026-08-20 13:30:28 +00:00
Hagernesh
6e6d6329b6 feat(bookings): add Additional Payments tab, add-charge modal, portal pay
Backoffice: new tab on the booking detail page (?tab=additional-charges,
already linked from the row menu) with an Add Charge modal — Save draft or
Send to customer.

Portal: charges panel on the booking detail page, Pay now per charge via
the existing OTP/CBE-bill payment flow.

Also fixes: GET /bookings/:id/additional-charges was returning DRAFT
charges to the customer (hidden client-side only) — now filtered
server-side per caller.
2026-08-20 12:59:33 +00:00
marshal
3604e78fbe Merge pull request #1364 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-20 14:49:27 +03:00
Nathnael
1b08666527 refactor(train-scheduling): move the schedule list onto the shared FilterBar
The list was the last major board still on ad-hoc filtering: usePagination
plus FleetToolbar's hand-rolled Selects, a debounced search and a manual
sort Select, assembled into a filters object by hand. It now uses
useFilters/FilterBar like every other list, so pagination, search, sort
and filters all travel as URL params and a link reproduces the view.

Origin and destination ride the shared Route filter, which no longer
requires both ends — filtering by origin alone stays possible, and each
end now takes several stations.

The card/table view toggle moves into FilterBar's children slot, and the
row tinting for shipping-line and direction is untouched.
2026-08-20 11:37:02 +00:00
Nathnael
f0d66ce8f9 feat(invoices): show and search what an invoice was raised against
`source` named the subsystem and `sourceId` was a raw UUID, so the list
could not say which record an invoice belonged to, and search matched
only the invoice number and that UUID — nobody types a UUID.

Every source except a shipping-line credit hangs off a booking, directly
or through the warehouse/first-mile/last-mile record, so the list read
now resolves each row to a booking reference, GRN or shipping line and
sends it as `sourceRef`. Search spans the same ground plus the customer
name, with the raw sourceId still matchable so a pasted UUID keeps
working.
2026-08-20 11:36:50 +00:00
Nathnael
f28b6faf29 feat(operations-reporting): plan station targets per cargo category
The OCC report plans a station lane per cargo type — Nagad–Mojo container
and Nagad–Mojo fertilizer are separate numbers — but a target's identity
was period + metric + dimension + dimensionKey, so the two collided on
one slot. `cargo_category` is now part of the row and of the uniqueness
check; it stays null for cargo_category and container_class targets,
whose dimensionKey already carries the category.

The config grid showed raw codes (VOLUME_TONS, cargo_category, a yard
code). The list read now sends readable twins alongside the stored codes,
which stay exactly as they are because the reports join on them — the
same shape YardDistancesService uses. Labels resolve per dimension rather
than from one merged map: CONTAINER_EXPORT exists in both vocabularies
and reads differently in each, and merging them gave every cargo-category
row the container-class wording.
2026-08-20 11:36:32 +00:00
Nathnael
2ec818e590 Merge branch 'dev' into freight/nati-2
# Conflicts:
#	apps/edr-freight-api/src/app.module.ts
#	apps/edr-freight-api/src/seed/freight-permissions.registry.ts
#	apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx
#	apps/edr-freight-web/backoffice/src/constants/URLS.ts
#	apps/edr-freight-web/backoffice/src/lib/permissions.ts
2026-08-20 11:33:19 +00:00
Nathnael
684c3173f4 feat(filters): filter by either end of a route, and by several stations per end
The route body paired two single Selects behind an Apply gated on
`origin && destination`, so the only question it could ask was "A to B".
"Everything leaving Nagad" and "everything arriving at Gelan" are both
real operator questions, and neither was expressible.

Origin and destination are now independent multi-selects, either of which
may be left empty: OR inside a side, AND across the two. A hint line says
so in words, because two stacked pickers do not communicate that on their
own, and a swap button flips the ends for a return leg.

The value is a tagged flat list — ["o:<id>", "d:<id>", ...] — because
url.ts knows exactly one encoding, comma-split inside one query param;
the tags are what buy back the two sides. decodeRouteValue still reads
the old untagged pair, so existing deep links and saved views keep
working. routeParams(originKey, destinationKey) maps each side onto its
own API param and omits an empty side entirely rather than sending a
blank one.

Each side's dropdown stays shut until something is typed. That needs two
levers, not one: openOnFocus={false} covers the focus, but MultiSelect's
PillsInput root also calls openDropdown() on every click with no prop to
gate it, so dropdownOpened is driven off the search text instead.
2026-08-20 11:24:33 +00:00
Hagernesh
0066a87972 feat(warehouses): add empty-containers/import-trains/export-trains to dashboard
Extends the warehouse dashboard with 3 metrics the screenshot target
needed but the backend didn't expose: emptyContainers (AVAILABLE
containers — closest proxy, no literal EMPTY status exists),
importTrains (reuses the import arrival queue definition), and
exportTrains (reuses the Djibouti export arrival queue definition) via
SchedulingReadFacade. Reorders the frontend metric grid to match and
fixes the Empty Containers card linking to a route that doesn't exist.
2026-08-20 10:46:42 +00:00
Marshal
bcf684814d fix(clearance): flag bookings with documents still awaiting GL approval 2026-08-20 10:29:59 +00:00
Marshal
26d864a55e View manual (offline) payment channel settings
Confirm offline (bank transfer) invoice payment
2026-08-20 10:29:59 +00:00
Marshal
c889797870 feat(bookings): two-level clearance charges (port + misc) billed to customer with invoices 2026-08-20 10:29:59 +00:00
Marshal
a809215e74 feat(bookings): two-level clearance charges (port + misc) billed to customer with invoices 2026-08-20 10:29:58 +00:00
Marshal
d7752f4386 fix(portal): show declaration, T1 and Djibouti clearance documents to the customer 2026-08-20 08:36:02 +00:00
Nathnael
6e95c5b8b8 fix(backoffice): map overview layouts to the org's real position keys
The overview layout table matched invented keys (`edr_operations_officer`,
`edr_marketing`, …) that only ever existed as IAM roles. The positions
actually configured under the unit use their own keys — `edr_freight_app/opn`,
`ethiopian_gl`, `edr_freight_app/finance` — so most staff fell through to the
executive fallback regardless of desk.

Map every position key in the current org tree, roots and sub-positions
alike, and keep the legacy role-form keys so accounts that model the desks
as roles still resolve. Finance was previously unmapped entirely.

Also drop a stray console.log from resolveOverviewLayout.

Safety (`edr_freight_app/sf_146`) stays unmapped — no such layout exists yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 08:20:00 +00:00