Commit Graph

3755 Commits

Author SHA1 Message Date
marshal
8923c2416c Merge pull request #1390 from Tria-plc/freight_feature/usermanagement
fix issue
2026-08-22 04:31:27 +03:00
Marshal
3455c1b3b4 fix issue 2026-08-22 01:29:44 +00:00
marshal
f4115f658d Merge pull request #1389 from Tria-plc/freight_feature/usermanagement
fix issue
2026-08-22 04:24:35 +03:00
Marshal
1ab2cfdb3b fix issue 2026-08-22 01:23:50 +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
b6a01e9635 Merge pull request #1385 from Tria-plc/freight_feature/usermanagement
changes
2026-08-22 03:10:29 +03:00
Marshal
bad418d79e changes 2026-08-22 00:09:00 +00:00
marshal
1816fe41f3 Merge pull request #1383 from Tria-plc/freight_feature/usermanagement
Freight feature/usermanagement
2026-08-22 02:33:08 +03:00
Marshal
c48e9b8905 fix issue and add consolidation 2026-08-21 23:28:59 +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
Nathnael
47a0ba5ee6 fix: pos seeder 2026-08-21 19:26:35 +00:00
Stephanos A.
6de2cc17cc Merge pull request #1380 from Tria-plc/alpha
Alpha
2026-08-21 18:28:05 +03:00
Stephanos A
e84c2edae9 Merge branch 'alpha' of github.com:Tria-plc/edr-platform into alpha 2026-08-21 18:26:54 +03:00
Stephanos A
d2a942ffbf feat: add contact phone overrides for payment links in excess baggage and supplementary charges 2026-08-21 18:26:38 +03:00
ghost2023
53e4b93ac9 test(reports): pin the receivable/payable ledger sides
Guards the two facts the report exists to get right: the cancellation
fee is never a payable, and exactly one wagon-cancellation status
(CREDIT_AVAILABLE) is a live liability. Adding a status to
WAGON_CANCELLATION_STATUSES now fails here until someone decides which
side of the ledger it lands on.

Also pins the sort expressions to the union wrapper alias — a branch
alias would resolve at build time and 42P01 at runtime, since the runner
appends ORDER BY outside the subquery.
2026-08-21 17:49:02 +03:00
ghost2023
6d113ca00c fix(reports): classify receivables and payables by real money flow
The receivable/payable split contradicted how money actually moves, in
three ways that each changed a headline number:

- The wagon-cancellation FEE was booked as a payable. It is money the
  customer owes EDR (raised ISSUED and unpaid at request time), so it
  belongs on the receivable side while open. The sign was inverted.
- A whole-booking wagon cancellation was booked at the source invoice's
  full paid_amount, and never cleared: the booking stays CANCELLED and
  the invoice stays PAID even after the credit is rebooked. The real
  liability is the ledger row's credit_amount, and only while it sits in
  CREDIT_AVAILABLE — cancellation refunds no cash, it hands back
  bookable credit redeemed by creating another booking.
- Shipping-line debt in UNBILLED has no invoice row at all, so an
  invoice-only fact table could not see it. That is the un-batched half
  of the debt, in the report whose stated purpose is shipping-line
  credit.

The report is now a UNION of the three tables that hold the answer:
invoices with a balance (plus prepayments against dead bookings),
UNBILLED shipping_line_credits, and CREDIT_AVAILABLE
booking_wagon_cancellations. A booking already carried by the
cancellation ledger is excluded from the invoice branch so its money is
counted once. Fully settled invoices are dropped — zero exposure is
neither a receivable nor a payable.

Branches are re-projected through an explicit column list before being
unioned: UNION matches by position and TypeORM does not preserve
addSelect order, which silently reordered one branch into
"gross, exposure, side_key, ..." and failed with "UNION types text and
numeric cannot be matched".

Verified against Postgres with a rollback-only fixture covering every
side, plus EXPLAIN over each filter combination and every sortable
column.
2026-08-21 17:47:41 +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
260d5a1590 feat(reports): cascade an unmet plan onto the periods that remain
A target is a quota, not a flat allowance. The Plan column spread it evenly
and kept asking for the same twelfth of a yearly figure no matter how far
behind the year had fallen, so the one number operations actually needs —
what must move per month for the rest of the year — was nowhere on the
page.

Plan now keeps its meaning and a Required column sits beside it. Plan is
the committed spread and never moves, which is the whole reason it stays:
Implement Rate is measured against it, so a month that missed still reads
as a month that missed. Required is the same target read as a quota — at
each bucket, whatever is still outstanding spread across the time still
left. A 1,200 t year 20% met by June asks 140 t of June and 960 t of
December, which is 1,200 less the 240 delivered. Over-delivery clamps to
zero rather than going negative.

Attainment is deliberately measured with the user's date bounds stripped
(attainmentCtx) and every other filter left in place. Reusing the report's
own filtered aggregate would make a July-only view read year-to-date as
nothing delivered and demand the entire year's tonnage from one month —
the failure would look like a plausible number, not an error.

Granularity gains half-year, nine-month and 90-day. Postgres has no
date_trunc for any of them, so PERIOD_UNITS entries became builders rather
than fragments to interpolate, and all eight blocks anchor to the calendar
year. Nine does not divide twelve and 90 does not divide 365: a nine-month
year is Jan-Sep plus a short Oct-Dec, and the fourth 90-day block absorbs
the remainder at 95 days. That last one is a choice — uncapped floor
division opens a five-day stub bucket every December, which is noise
rather than a period.

Two consequences of the shared unit table, both handled here:

- plannedRowsSql now generates a day at a time and groups, instead of
  stepping by the bucket width. The ragged blocks restart each January, so
  stepping 90 days from January 1st walks off the anchor in the second
  year. Day grain also gets partial-bucket overlap for free, at the same
  sub-day precision the old clipping had.
- nextPeriodOrdinalExpr asks the unit for its next block start rather than
  adding its own step. revenue-by-period evaluates a regression there, and
  a ragged unit's final block is shorter than its nominal width, so + step
  would land past the next block and forecast at the wrong x.

Verified against Postgres 16 with the entities synchronised into it: all
272 report/granularity combinations in the registry EXPLAIN clean, and the
1,200 t drill-down sums back to 1,200 at every one of the eight grains.
2026-08-21 17:40:35 +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
ad5caddf60 feat(wagons): filter list by last-maintenance date range
Adds maintenanceFrom/maintenanceTo to ListWagonsQueryDto and applies them
in WagonsService.buildListQuery as a correlated subquery against
wagon_status_logs (last flip to MAINTENANCE), mirroring the existing
createdFrom/createdTo range filter. Both ends inclusive, whole days.
2026-08-21 15:54:53 +03:00
ghost2023
91c9c3e513 feat(reports): list unbilled categories at zero in revenue-by-category
A category with no invoice lines in a period simply had no row, so a
category going quiet was indistinguishable from one that never existed,
and filtering to a category that was never billed returned an empty table.

The query is now three levels. The aggregate groups as before. A grid
crosses every period that saw revenue with every category the filter
allows, and LEFT JOINs the aggregate onto it so a missing combination
lands at zero. The wrapper does the display rounding and the labelling.

Two things had to move for that to be correct:

- The lag() window is now in the wrapper. A window function only sees the
  rows its own query level produces, so left on the aggregate it would
  skip a category's silent periods — billed in January and March, it
  would read March's prior as January and report flat growth.
- The category filter is off the aggregate and enforced by the grid's
  category list. Filtering the aggregate too would make the period axis
  depend on the selection, which is what left the table empty when the
  selected category had never been billed.

Periods come from the data, not generate_series over the date filter: a
twelve-month range over one billed month would otherwise publish eleven
months of pure zeros, and daily granularity would multiply that by thirty.

The Categories KPI is now "Categories with revenue" — a bare count of live
categories reads as a contradiction next to a table listing all fourteen.

EXPLAIN-validated against the dev database across seven filter shapes,
including the empty-array case (hence unnest(ARRAY[...]) over VALUES,
which is a syntax error when empty).

Claude-Session: https://claude.ai/code/session_01LoY3hNWqcaAC1pYmGPN7jr
2026-08-21 15:51:18 +03:00
ghost2023
3ce58d4c57 refactor(reports): label revenue categories from a key column
CATEGORY_LABEL_EXPR wraps the classifying CASE, so it only works where the
classification happens in the same SELECT. A report that classifies in a
subquery and labels in the wrapper has a plain key column to label instead.

CATEGORY_LABEL_OF takes that key expression; CATEGORY_LABEL_EXPR is now
defined through it, so its three existing callers are unchanged. Mirrors
CATEGORY_LABEL_OF in operations-classification.ts.

Claude-Session: https://claude.ai/code/session_01LoY3hNWqcaAC1pYmGPN7jr
2026-08-21 15:51:05 +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
ghost2023
17e41aa767 feat(overview): serve GET /overview/layouts filtered by permission
New catalog endpoint, same shape as GET /reports: returns the overview
layouts (key + label) the caller holds the matching
edr_freight_app:overview:<layout>:view permission for, in priority
order. Backend enforcement to go with the permission-based frontend
resolver (next commit) — a caller can no longer land on a layout their
JWT doesn't actually carry the permission for.
2026-08-21 15:43:26 +03:00
ghost2023
b60e361483 feat(iam): add per-layout overview permissions
Adds edr_freight_app:overview:<layout>:view for each of the 6 overview
dashboard layouts (clearance, occ, operation, marketer, finance,
executive), seeded via OVERVIEW_LAYOUT_PERMISSIONS alongside the
existing report permissions.

Granted 1:1 to match today's role-dashboards.config.ts ROLE_LAYOUTS
key table, appended only at the terminal EDR_FREIGHT_ROLES /
EDR_FREIGHT_POSITIONS assembly points (never inside the reusable
ROLE_PERMISSION_PRESETS/POSITION_PERMISSION_PRESETS builders) so
composite positions like chief don't leak multiple layout grants from
the role arrays they spread.

The ~20 ad-hoc edr_freight_app/xxx department sub-positions are not
backfilled here and will fall back to the executive layout until
granted manually via the IAM positions admin screen.

Part of switching overview layout resolution from role/position-key
matching to permission checks, mirroring how reports already work.
2026-08-21 15:42:58 +03:00
Nathnael
7a2383f02c fix: revenue by customer 2026-08-21 10:57:42 +00: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
Hagernesh
8181834037 Ticket
Empty container returns now generate a downloadable Equipment Interchange Receipt: container number & size, exact return timestamp, depot/location, condition, and the carrier/booking reference. Customers can download it from their booking's Documents tab (own-booking returns only); staff can pull any via the same endpoint.
2026-08-21 08:59:06 +00:00
Marshal
fbb169469f feat(clearance): preview charge documents before and after upload 2026-08-21 08:59:06 +00:00
Marshal
a9c6b85c08 feat(clearance): preview charge documents before and after upload 2026-08-21 08:59:06 +00:00
Marshal
b17a06052b fix issue 2026-08-21 08:59:06 +00:00
Marshal
b3823a4f32 fix issue 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
3a4eeee61b feat: enhance ClearanceChargesSection with file viewing and downloading capabilities 2026-08-21 08:47:47 +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
24f5d94709 feat(clearance): preview charge documents before and after upload 2026-08-21 07:10:50 +00:00
Marshal
4c549029fe feat(clearance): preview charge documents before and after upload 2026-08-21 07:04:22 +00:00
Marshal
ce3fde676e fix issue 2026-08-20 18:30:28 +00:00
Marshal
0e8e6acb04 fix issue 2026-08-20 18:28:40 +00:00
Marshal
a5e6cab248 fix issue 2026-08-20 18:16:03 +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