Commit Graph

5134 Commits

Author SHA1 Message Date
marshal
13ac3144e2 merge conflict resolved 2026-09-02 22:35:18 +00:00
marshal
1d3361d442 feat(freight): GL rebook of cancelled wagons, seal+train required on completion, train/voyage in SMS 2026-09-02 22:28:11 +00:00
marshal
6efa9dab93 feat(freight): GL rebook of cancelled wagons, seal+train required on completion, train/voyage in SMS 2026-09-02 21:23:24 +00:00
marshal
3e274cc2a2 fix(freight): gate loading on booking paymentStatus only; default schedule voyage no. to train's voyage number 2026-09-02 21:15:48 +00:00
Hagernesh Tadesse
78086422ca Merge pull request #1478 from Tria-plc/Emty-container-return
Emty container return
2026-09-02 23:49:17 +03:00
hager
29c1a21b68 feat(bookings): make customer self-haul assignment work end to end
The customer truck card on the booking's Logistics tab is now usable for
every cargo type, with an Excel template and bulk upload that match what
the API enforces.

Excel template and bulk upload
- The template is built per booking. Container bookings get the booking's
  own containers with their sizes on a reference sheet and sample rows
  paired 20ft+20ft; bulk (PER_TON) bookings get a Planned Tons column;
  counted cargo (PER_ITEM: machinery, RoRo vehicles) gets Planned Quantity.
- Parsing validates the whole file before anything is posted: plate,
  driver, truck type, ISO container numbers, containers on the booking,
  duplicates across rows, containers already on a truck, and the capacity
  rule (one 40ft alone, or two 20ft). Errors quote the Excel row number.
- The bulk DTO reuses AddCustomerTruckDto instead of a drifted copy that
  lacked plannedTons/plannedQuantity, so bulk cargo can be uploaded at all.
- A partial failure is reported per row in the modal instead of closing it
  as if every truck had been created.

Capacity rule in the form
- The container picker shows sizes and stops offering a second container
  once a 40ft is picked, or a 40ft once a 20ft is picked.
- PER_ITEM cargo commits by item count; tonnage becomes optional.

Shared vocabulary
- CUSTOMER_TRUCK_TYPES and ISO_CONTAINER_NUMBER move to @edr/types so the
  API validators, the dropdown and the template read one list.

When assignment is open
- The card always renders and states why assignment is closed (EDR
  haulage, unpaid, train not arrived, cargo already loaded) rather than
  vanishing.
- Self-haul is blocked only once EDR has committed to the road leg: an
  approved last-mile request or an existing last-mile leg. A delivery
  address whose request is still awaiting confirmation, submitted or
  rejected no longer blocks the customer from bringing their own truck.
  Collection on exports has no approval step and still blocks as before.
  Both the multi-truck service and the legacy single-truck path read the
  same SQL fragment (edrHaulsThisBooking), and the portal applies the same
  rule with a notice that assigning a truck makes the pending request
  unapprovable. The last-mile side already refuses to approve a booking
  carrying a customer truck, so the two paths stay mutually exclusive.

Verified: EXPLAIN on the new SQL against edr_dev, type-check clean for
freight-api and portal, 12 util specs pass (5 new). Backoffice type-check
fails only in pre-existing user-management files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-02 20:47:18 +00:00
hager
d6f730fa3e fix(empty-return-requests): show the queue to the staff who run returns
The menu item was gated on the brand-new empty_return_requests:view key,
which on the dev DB is held by one position (Operations Chief) — so the
Director and Dispatcher who already work Container Returns two rows above
it never saw the entry at all.

Viewing the queue now accepts that key OR warehouse_inventory:view, the
key the rest of the Imports menu runs on, in the sidebar, the route guard
and the API's read routes alike. Approving and rejecting stay on the
review key on their own, and the buttons come up disabled (with the
reason) for a viewer who lacks it rather than firing into a 403.

The dedicated keys are still there to hand out per position whenever
that call gets made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 20:47:18 +00:00
Nathnael Wondisha
42f7a5a7b4 Merge pull request #1477 from Tria-plc/freight/hot-fixes
fix: export colmn
2026-09-02 16:51:12 +03:00
nathnael
a06faa4254 fix: export colmn 2026-09-02 13:47:23 +00:00
Hagernesh Tadesse
0f979862cd Merge pull request #1476 from Tria-plc/Emty-container-return
Emty container return
2026-09-02 14:42:51 +03:00
hager
de55d809c7 fix(empty-return-requests): pick the containers off the booking
Typing container numbers let a customer request a return for a box that
never came in on that booking — and gave them a blank field to guess at
when they did not have the numbers to hand.

The portal now lists the booking's own containers, minus any already
spoken for by a live request, as a tick list; the count follows the
ticks. The API backs that up: every submitted number must be one of the
booking's containers, and a booking with no container numbers recorded
says so instead of offering an empty form.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:24:43 +00:00
hager
3f7b744987 feat(empty-return-requests): customer-requested empty return, priced and paid
A booking sold WITHOUT the return service had no way to send its empties
back: the containers were the customer's problem and nothing in the
system priced, billed or planned the movement.

Adds the request flow end to end. The customer opens the booking, says
how many containers are coming back and types each number; the request
lands in a new backoffice queue (Empty Return Requests). Approval prices
it off the same live WITH_RETURN route rate the rule engine bills when
the service IS bought up front — per container, converted to birr, and
overridable by the reviewer — and issues the invoice there and then.
Payment settles through the normal invoice path, whose
`empty_return_request.invoice.paid` event moves the request to PAID; the
customer then books the return date and the truck.

Scheduled requests surface on Container Returns as Planned Empty
Returns, where confirming the arrival records the containers through the
existing empty-container-return flow — which in turn closes the request
once its last container is in.

Container freight only, never a booking that already ships WITH_RETURN,
and only from IN_TRANSIT onward: the empty comes back after delivery, so
the option has to outlive ARRIVED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 11:05:03 +00:00
Hagernesh Tadesse
bc2e005bbc Merge pull request #1475 from Tria-plc/Emty-container-return
feat(import-operations): record empty container return per booking
2026-09-02 13:20:09 +03:00
hager
2e51342d1e feat(import-operations): record empty container return per booking
Container Returns only ever offered the last-mile path: a booking reached
the list once it had a truck assigned and warehouse inventory flagged as
returning. Bookings that ship WITH equipment return had no way in, so the
empties they owe were invisible until that path happened to fire.

Adds GET /import-operations/empty-return-bookings — the containers a
booking flagged is_return, carrying whichever of them already has an
empty return recorded, grouped one row per booking and dropped from the
list once nothing is pending. Covers both spellings of the booking's
equipment_return (WITH_RETURN and the older RETURN) and skips bookings
that never ship.

Backoffice grows a "Bookings With Empty Container Return" card above the
existing sections: pick the booking, tick the containers coming back,
say where they landed, and each tick becomes an empty container return
on that booking — which is what the Returned Containers table then
advances. The existing last-mile, standalone and bulk flows are
untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 10:17:19 +00:00
Abubeker Yasin
2e33e9aeb3 Merge pull request #1474 from Tria-plc/alpha
Alpha
2026-09-02 11:59:02 +03:00
Abubeker Yasin
db97c3682f feat: ( reports ) add intercity/international trip-type filter to finance summary 2026-09-02 11:57:48 +03:00
Hagernesh Tadesse
75bbdbf84c Merge pull request #1473 from Tria-plc/Warehousechanges
Customer Truck Assignmnet
2026-09-02 10:24:26 +03:00
hagiye
031efce92b Truck assiggnment and per truc 2026-09-02 07:17:19 +03:00
hagiye
6d6b32feae api key fix 2026-09-02 07:17:01 +03:00
marshal
ec5df4b328 Merge pull request #1472 from Tria-plc/freight_feature/usermanagement
feat: Implement handling for partially loaded bookings in train sched…
2026-09-01 23:56:07 +03:00
marshalyordanos
d51bed5630 feat: Implement handling for partially loaded bookings in train scheduling
- Added  to manage decisions on partially loaded bookings during log-pass and dispatch actions.
- Enhanced  to track and manage bookings left behind when a train departs a yard.
- Updated  and  to pass necessary station data for handling partially loaded bookings.
- Modified  and  to account for customer-fault fees and ensure proper handling of credits.
- Introduced fault tracking in  interface to differentiate between customer and EDR faults.
2026-09-01 23:53:58 +03:00
Nathnael Wondisha
6411dbce9b Merge pull request #1471 from Tria-plc/freight/fix/quick-fixes
fix: matrix
2026-09-01 10:08:20 +03:00
Nathnael
7fd5616188 fix(backoffice): stop sharing the position cookie with Smart Office
Both apps wrote a cookie named `current-position-id` but stored
different ids in it — freight the `employeePositionId`, Smart Office the
`position.id`. On a shared domain each login overwrote the other's desk
selection, and the loser silently fell back to the first position.

Freight now uses `freight-current-position-id` through a small helper
that reads the old name once, so a session live across the deploy keeps
its desk, and clears it on every write.

Also mounts the position switcher in the freight dashboard header. It
only existed under /performance-management, so on every other page a
two-desk user had no way to switch and was stuck on whatever
`useAuthUser` defaulted to. It now hides below two positions rather than
showing a one-option dropdown to the single-desk majority.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:06:05 +00:00
Nathnael
db9d6e49c7 fix(auth): load every position a user holds
Staff given a post in Smart Office and another in freight only ever
loaded one of them. Two causes, both in how the IAM guard collapses the
login snapshot:

- `x-current-position-id` is read two ways inside one function: the
  employee row is matched on `position.id`, the position on
  `employeePositionId`. Freight sends the latter, Smart Office the
  former, so whichever value arrives one lookup matches nothing and
  falls back to `positions[0]`. FreightJwtGuard now matches both fields.

- IAM keeps one employee row per organization, and EDR and EDR Freight
  are separate organizations, so a user holding a post in each owns two
  rows. Only the active row reached `collectPermissionKeys`, so the
  freight post's permissions disappeared whenever the other row won the
  active slot. `employee.positions` now unions every row, which is what
  the util already does for the array shape.

`delegatedPositions` stays scoped to the active row on purpose: yard
scope widens on it, and someone standing in on another organization's
row is not this desk's stand-in.

/auth/me now returns every employee row, active row first, so the
position picker can offer a desk that is not on the active row.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:03:58 +00:00
Nathnael
3f2bcf1a1a fix(customers): drop fake before-state in history diff rows
DiffRow rendered onboarding's initial field values as '— → value',
implying a prior state that never existed. Show the value alone when
there is no real before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-01 07:01:27 +00:00
Nathnael
2dfea96cb8 fix(chat): retry rate limits, lock instead of deactivate, guard reconcile
Provisioning created nothing on dev. Synapse answers a burst of writes with
429 + retry_after_ms, and a reconcile is nothing but a burst of writes, so the
first throttled m.space.child PUT threw and took the whole run with it. On the
sign-in path ChatSsoService catches that by design, so every employee got a
working sign-in into an Element with no rooms in it. fetchWithRetry now sits
behind all three request wrappers and honours the delay Synapse asks for,
capped at 5 attempts so a wedged homeserver still fails rather than hangs.

Two ways the reconcile could destroy state, both now blocked:

- Zero position holders meant "remove everyone": every member kicked from
  every room, then every account deactivated. It never means that - it means
  the IAM query failed, the org/unit keys drifted, or a migration is
  mid-flight. reconcile() aborts, and syncMembership() refuses to empty a
  populated room, as a per-room backstop.
- Deactivation could not be undone here. Reactivation wants a password and
  password_config.enabled is false, and room memberships do not come back.
  Departed accounts are locked instead - same access block, one PUT to
  reverse - and ensureUser lifts the lock when someone returns in IAM.

Also: join the space itself, not only the rooms inside it, or Element leaves
every dept room loose in Home and never shows the space at all. And drop the
bridge's per-type routing - it pointed at a hardcoded dept-operation alias
while the reconcile derives dept-${positionKey} from IAM, so bridged
notifications went to a room the bridge created and nobody was in.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 06:28:55 +00:00
Nathnael
0d1720a99b fix: matrix 2026-09-01 05:51:48 +00:00
Hagernesh Tadesse
e598f03104 Merge pull request #1470 from Tria-plc/Wrehousechanges
feat(billing): render EIMS documents in the MoR tax-document layout
2026-09-01 07:33:14 +03:00
Hagernesh
ed839ebb92 feat(billing): render EIMS documents in the MoR tax-document layout 2026-09-01 04:28:48 +00:00
Hagernesh Tadesse
7c5531c0c8 Merge pull request #1469 from Tria-plc/Wrehousechanges
Mor Verify fix
fix(eims): sign the /v1/verify request body
2026-09-01 06:45:50 +03:00
Hagernesh
fe3d398757 fix(eims): sign the /v1/verify request body 2026-09-01 03:43:10 +00:00
Hagernesh
dd9f597e01 fix(eims): resolve Addis Ababa addresses against MoR's sub-city tier
MoR's location master has no zone level in a chartered city: CITY_NAME is the
sub-city and LOCALITY_DESC is the numbered woreda. e-Trade fills the gap by
repeating the region in ZONE, pushing the sub-city into CITY_NAME and the
woreda number into LOCALITY_DESC, so every Addis Ababa buyer failed CITY_NAME
lookup and could not be filed at all.

Retry one level down when ZONE repeats the region, accepting it only when
both shifted levels resolve exactly — an unknown zone still fails with its own
message. Match a zero-padded 07 against MoR's own WOREDA 7 spelling, and
add the reviewed AMAHARA / KOLFIE KERANIYO / MISRAK SHOA aliases.

Verified against the dev database: 2 of 40 companies resolved before, 31 after.
2026-09-01 03:43:09 +00:00
Hagernesh
3721ac1ef0 feat(warehouses): gate loading queues on the train's station loading window
Warehouse loading queues and the train schedule page now share one truth:
the schedule's per-yard stationWorkLogs. A booking's items aren't loadable
until "Start loading" has been clicked for their boarding yard, mirroring
the same assertStationWorkStarted check the train schedule page's own Load
button already enforces.

- LoadableTrainRow/LoadableTrain carry originStationId + stationWorkLogs.
- TrainLoadableItem carries originYardId/originYardLabel/loadingWindowStarted,
  computed from station_work_logs in the same query.
- New YardLoadingWindows component surfaces the per-yard windows on the
  loading panel; ReceiveInventoryModal and LoadToTrainPanel wire it in.
- Invalidate loadable-trains/train-loadable-items/warehouse-inventory
  queries alongside train-scheduling ones, since they render off the same
  data.

Fixes the StationWorkLogJson typo that broke the freight-api build.
2026-09-01 03:43:09 +00:00
Hagernesh Tadesse
c7133ba1da Merge pull request #1467 from Tria-plc/Wrehousechanges
Wrehousechanges
2026-08-31 15:08:00 +03:00
Nathnael Wondisha
790d17f846 Merge pull request #1466 from Tria-plc/freight/fix/quick-fixes
fix
2026-08-31 14:46:54 +03:00
Nathnael
0ac85ebc1f fix 2026-08-31 11:45:55 +00:00
Hagernesh
cc50a6687a feat(bookings): per-row stations + totals footer on carriage acceptance sheet
The sheet printed Departure/Arrival Station as booking-level constants and
squeezed the footer figures into unrelated table columns (gross weight under
Cargo Name, full/empty under Wagon No.).

Stations now resolve per row from the slot's own board/alight yard, falling
back to the schedule's origin/destination and then the booking yards —
mirroring buildExportLoadListHtml's leg-slot handling.

The paper form's footer becomes its own tile block: In Total Wagon No., Tare
Weight, Load Capacity, Gross Weight, Equated Length, Full Wagon, Empty Wagon,
Total Amount. Rendered as .tile so buildTabularFallbackPdf still prints every
figure where Chromium is absent.
2026-08-31 11:28:36 +00:00
marshal
313e0053c3 Merge pull request #1464 from Tria-plc/freight_feature/usermanagement
fix train build issue
2026-08-29 23:28:35 +03:00
Marshal
678c5d7d49 fix train build issue 2026-08-29 20:26:16 +00:00
marshal
87ef817b1f Merge pull request #1463 from Tria-plc/freight_feature/usermanagement
fix train build issue
2026-08-29 22:26:48 +03:00
Marshal
ef1d310413 fix train build issue 2026-08-29 19:26:09 +00:00
marshal
522a65f0e7 Merge pull request #1461 from Tria-plc/freight_feature/usermanagement
feat(contracts): implement staff cancellation of contracts with reaso…
2026-08-29 14:36:08 +03:00
Marshal
6ac41cf078 feat(contracts): implement staff cancellation of contracts with reason and notification 2026-08-29 11:31:13 +00:00
Hagernesh
f23500c273 fix(train-scheduling): drop wagon slots with no physical wagon pinned from marshalling docs
A slot can hold a LOADED allocation with no physical wagon backing it
— a fleet shortfall can leave a booking's allocation unpinned, and a
REAL cut nulls physical_wagon_id without ever touching the slot's own
status. Neither buildImportLoadListHtml, buildExportLoadListHtml, nor
intercityOnBoardView checked for this: they rendered a ghost row (dash
wagon number, but cargo/container info still listed) and counted it
toward the Wagons tile.

Reproduced live on S-2026-00073: 5 slots (seq 60-64) with no physical
wagon, from a booking currently held out for lack of fleet, rendered
as phantom rows on the origin doc and every numbered marshalling doc
— visible as the Seq column running up to 64 despite only 54 real
wagons.

All three now additionally require physicalWagonId (or a resolved
wagonNumber, for the import doc's already-flattened shape) before a
slot gets a row. Added coverage for all three call sites.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 10:43:34 +00:00
Hagernesh Tadesse
3346e82d9d Merge pull request #1460 from Tria-plc/Wrehousechanges
feat(train-scheduling): per-row Departure/Arrival Station on marshall…
2026-08-29 13:06:31 +03:00
Hagernesh
7df96d3609 feat(train-scheduling): per-row Departure/Arrival Station on marshalling docs
buildExportLoadListHtml's wagon table (origin export doc, Marshalling
2/3, and the intercity fallback) had no station columns — only the
import doc did. Adds Departure Station / Arrival Station per row,
mirroring the import doc's existing pattern: a whole-route wagon reads
the schedule's own origin/destination, a leg-slot wagon reads its own
boardYardId/alightYardId instead (resolved via a new yardLabelById
opt, computed once per document from board+alight yard ids across the
trainSet).

Also fixes two pre-existing off-by-one colspans on the EMPTY row and
the 'No wagons on this train set' placeholder, now that the real
column count changed with the two new ones.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 10:02:46 +00:00
Hagernesh Tadesse
b6d27379b1 Merge pull request #1459 from Tria-plc/Wrehousechanges
fix(train-scheduling): scope numbered marshalling docs to their own s…
2026-08-29 12:45:10 +03:00
Hagernesh
122b6d447a fix(train-scheduling): scope numbered marshalling docs to their own stop's coupling
Marshalling 2's wagon list was computed from intercityOnBoardView's
CURRENT state (boardYardId == null || hasLoaded), which can't tell
'coupled at THIS stop' from 'coupled at a LATER stop that has, by
generation time, also already happened' — both read LOADED once the
trip has moved on. Reprinting Marshalling 2 (Dire Dawa) after
Marshalling 3's stop (Adama) had already run leaked Adama's 5 coupled
wagons into Dire Dawa's document (54 wagons became 59), reproduced
live on S-2026-00073.

marshallingDocumentAt now corrects the on-board list against the
adjustment log: a leg-slot wagon belongs on a stop's document only if
it has a logged ADD at or before that stop's own timestamp. Extracted
as wagonsAsOfStop so it's unit-testable without mocking the DB.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 09:38:53 +00:00
Abubeker Yasin
beb98edbf7 Merge pull request #1458 from Tria-plc/reschedule
feat: ( bookings ) add authenticated My Bookings history to the passe…
2026-08-29 12:29:33 +03:00
Hagernesh Tadesse
b2c64c1435 Merge pull request #1457 from Tria-plc/Wrehousechanges
fix(train-scheduling): drop not-yet-boarded wagons from every marshal…
2026-08-29 12:28:56 +03:00