Commit Graph

128 Commits

Author SHA1 Message Date
hager
22250ebdb9 feat(warehouses): keep failed cargo off the train until it is re-passed
An inspection report mirrored its outcome onto the inventory item but
never touched the item's status, and the loading paths gated on status
alone. Cargo that passed, reached READY_FOR_LOADING and was then
re-inspected as FAILED kept that status and loaded anyway.

Gate loading on the inspection outcome. load() is the single choke point
every loading path runs through, including loadItemsOntoTrain, so the
check sits there: nothing but PASSED travels, and the message names the
outcome so the operator knows what to fix.

A failed or under-review re-inspection also pulls the cargo back out of
the ready queue. load() refuses it either way, but leaving it READY_FOR_*
would keep it on the loading and pickup lists as though nothing had
happened.

Reversing a held inspection now needs a reason. Passing cargo whose
current inspection is FAILED or NEEDS_REVIEW is rejected without remarks,
so the record says why cargo that was deliberately held may now travel.
A first-time pass is unaffected.

Mark Selected as Inspected skips failed and under-review items instead of
clearing them. Overturning a failure is a deliberate, reasoned act, never
a side effect of ticking a row in a list; those items are reported back
with a reason telling the operator to re-inspect them individually.

Verified: freight-api type-check passes; 15 warehouse suites pass
(90 tests, 12 of them new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:06:56 +00:00
hager
cb150e46b7 feat(warehouses): pass a whole booking when its cargo is inspected
Marking a received item inspected advanced only the rows the operator
ticked. A booking's cargo spans one inventory row per container, and a
multi-truck arrival files a GRN batch per truck, so a six-container
booking stayed half-inspected and never reached Ready To Load.

Widen the selection to every still-inspectable row of the same booking
before inspecting. Each row is then passed and advanced exactly as
before — EXPORT to READY_FOR_LOADING, IMPORT to READY_FOR_PICKUP — so
the whole booking moves together.

The rows the operator actually ticked lead the response, and are kept
even when ineligible so their skip reason still surfaces rather than
being dropped silently. Rows already PASSED, rows in a status that
cannot be inspected, and rows belonging to another booking are left
alone. Ad-hoc inventory with no booking expands to itself.

Also show yards by name in the receive and received queues. They
selected yards.code, so the route column read KALITY, the internal code
for the yard everyone calls GMP / Gelan Multipurpose Port.

Verified: freight-api type-check passes; both new SQL statements
EXPLAIN-validated against the dev database; 14 warehouse suites pass
(78 tests, 5 of them new).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 20:02:10 +00:00
hager
076884623e feat(warehouses): load trains from the warehouse, mirroring the schedule
Ready to Load listed inventory and offered an auto-load picker, but the
warehouse floor works train by train: a train stands at the yard, its
bookings board wagon by wagon, it rolls. That view existed only on the
train schedule workspace, which the warehouse staff do not run.

Ready to Load now has an Items / By train switch. By train is a mirror
of the schedule's own column, placed where the loading actually happens.

Nothing in it has its own rules. Train position, the per-yard loading and
unloading windows, and the per-booking Load / Wagons / Unload actions all
come from the train-scheduling endpoints the schedule workspace already
calls, so a Load that would be refused there is disabled here with the
same reason and the two surfaces cannot disagree. No train-scheduling
code is touched.

What the warehouse adds is what the schedule cannot see: which of the
train's bookings are physically in the shed, with their GRN and
inspection state, laid out along the flow the staff follow -- receive and
GRN, inspect, ready, open the loading window, train at yard, load per
wagon, dispatch, unload at port.

Wagons
- The wagon modal calls the same per-wagon journey endpoints, so every
  server gate (train at the yard, window started, PAID, GRN) is the
  schedule's own.
- Wagons go one at a time in order: the server flips the booking to
  IN_TRANSIT or ARRIVED on whichever call clears the last wagon, so
  sequential is required, not merely tidy. A failure stops the run, the
  wagons already sent stay done, and the toast says how many, so a retry
  only resends the rest.
- Deliberately not mirrored: cancelling wagons that will not ride, and
  the direct truck-to-train handover. Both are commercial decisions
  (fees, credits, GRN waiver) that belong to the schedule workspace.

loadable-trains takes includeDispatched. Loading follows the train after
it rolls, since a mid-corridor warehouse boards its cargo when the train
stands at its yard, and the train-centric view needs the same set the
schedule offers Load on. The default stays pre-dispatch only, so the
existing auto-load picker is unchanged.

Also fixes the backoffice build: ReceiveInventoryModal used MultiSelect
without importing it, left behind by the self-haul assignment work.
2026-09-03 04:38:46 +00:00
marshal
13ac3144e2 merge conflict resolved 2026-09-02 22:35:18 +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
hagiye
031efce92b Truck assiggnment and per truc 2026-09-02 07:17:19 +03: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
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
Hagernesh
fd6f0d03b6 feat(loading): notify loaded and left-behind containers, surface the CAS
A booking is routinely loaded in parts, and nothing told the customer which
containers boarded and which stayed behind. The carriage acceptance sheet was
the only record, and it both totalled up cargo still sitting in the yard and
lived inside a Warehouse documents bundle that direct truck-to-train cargo
has no business in.

The sheet now marks each wagon Loaded or Not loaded and totals only the loaded
ones. On load, the customer gets an in-app, SMS and email notice carrying the
train number, route, departure time and both container lists — capped to a
summary on SMS and email, complete in the inbox. Anything left behind also
raises a warehouse-desk notice so somebody owns finding it space.

That desk is addressed by a new warehouse_inventory:get_notification
permission: a recipient selector, not a route guard, so ops can assign who
gets pinged without granting access to anything.

The GRN notice went out over SMS alone, to whatever phone number the gate
clerk typed. Where the receive carries a booking it now resolves the company
and delivers in-app, SMS and email, skipping the typed phone so the customer
is not texted twice; manual and backlog receives keep the old path.
2026-08-29 08:47:39 +00:00
Hagernesh
312c1f1da3 feat(warehouses): backoffice UI for container stack positions
Surfaces the physical stack/slot model in the staff app.

- ZoneLayoutModal: stacks drawn level by level with occupancy colours,
  configured-vs-built-vs-occupied counts, and stack create/delete plus
  block/reserve/free on empty levels
- SlotPicker in the store and move modals, offering only the next
  fillable level of each stack so the form cannot suggest a position
  the API will refuse
- move modal warns when a container is buried, lists the blockers, and
  disables the action instead of firing a 409
- fix: move() now asserts accessibility server-side, matching release —
  both are exits from a stack
2026-08-29 06:39:29 +00:00
Hagernesh
a6ea1a48ac feat(warehouses): track physical container stack and slot positions
Extends the warehouse hierarchy below zone with ground stacks and vertical
slots, so a container's exact position is recorded rather than only its zone.

- freight.warehouse_zone_stacks / warehouse_zone_slots, plus nullable
  stack_id / slot_id on warehouse_inventory (existing rows stay valid)
- slot occupancy is derived from inventory status, guarded by a partial
  unique index, so no exit path has to remember to free a slot
- placement service: hierarchy validation, bottom-up stacking rules,
  accessibility/blocking-container reads, capacity vs slot summaries
- stack CRUD with auto-generated slots; reuses warehouse-zone permissions
- slot support folded into the existing move()/store() paths
- fix: validateLocation now rejects a mismatched warehouse/yard/zone triple
- seed:warehouse-layout builds the layout from a JSON config
2026-08-28 16:09:41 +00:00
Hagernesh
8ef50f9aff feat(warehouses): allow deleting a warehouse
Soft-delete route guarded by warehouses:delete, refused with 409 while
yards remain — zones and inventory hang off a yard, so cascading would
orphan stock. Backoffice list gets a delete action in both views,
omitted when the user lacks the permission.
2026-08-28 14:40:18 +00:00
Nathnael
6823a32fee feat: yard scoping to position 2026-08-18 12:55:11 +00:00
Hagernesh
6c96b7ad94 fix(warehouses): free warehouse/yard/zone capacity on export dispatch
dispatch() (LOADED -> DISPATCHED) routed through the shared transition()
helper, which only flipped status/timestamp and never called
applyCapacityDelta. deliver() (import pickup) already decrements
correctly, so export cargo leaving by train inflated currentWeight/
currentVolume/currentContainers forever instead of freeing capacity.

Add optional freeCapacity hook to transition(), wire dispatch() to it,
mirroring the negative-delta pattern already used in deliver().
2026-08-13 11:27:56 +00:00
Hagernesh
c4bf3c9479 feat(freight): add company logo setting applied to every generated document
New logo-settings module (mirrors stamp-settings): single uploaded logo,
stored via FilesService/MinIO, injected as a data URL into invoice/receipt,
contract, warehouse, train-scheduling, and payment-receipt PDFs. Adds a
matching backoffice settings page and settings:logo:view/manage permissions.

>
2026-08-13 10:54:23 +00:00
Hagernesh
89dba01cd7 feat(train-scheduling): notify customer of CAS on direct-to-train load
Direct truck-to-train export cargo skips the warehouse, so the existing
carriage acceptance sheet ready notice (fired on warehouse receive)
never reached these bookings. Their handover moment is the load itself.

Extract notifyCarriageAcceptanceReady into a shared notifications util
(was private to WarehouseInventoryService) and call it from
BookingJourneyService.loadBooking for EXPORT + DIRECT_TO_TRAIN bookings,
right after the GRN gate, before the load transaction proceeds.
2026-08-13 09:58:04 +00:00
ghost2023
a603807e8e feat(freight-api): seal EDR side of contracts with the one global stamp 2026-08-12 06:26:46 +03:00
Hagernesh
337ff6cfe3 feat(freight): support direct truck-to-train export handover
Export cargo reaches a train two ways, but the platform only modelled
one. Direct truck-to-train cargo loads straight onto the wagon, never
enters a warehouse and so never has a GRN — yet assertExportReceivedWithGrn
required one before the carriage acceptance sheet could be issued or the
booking loaded from inside its schedule.

Adds export_handover_mode to freight.bookings (null = WAREHOUSE, so
existing bookings are unaffected) and teaches the shared gate to skip
DIRECT_TO_TRAIN. Both call sites are fixed by that single early return.

For direct bookings the carriage acceptance sheet builds its lines from
the booking's own containers, falling back to the declared bulk tonnage,
and is issuable as soon as the mode is chosen. Direct bookings are also
removed from the warehouse receive queue, since that cargo is never
coming to the shed.

Staff choose the mode from the booking detail page via a new endpoint
reusing bookings:operations. Switching to direct is refused once
warehouse inventory exists, so the two flows cannot cross.

Warehouse-then-train keeps every gate it had.
2026-08-08 15:16:31 +00:00
Hagernesh
80e1648672 fix 2026-08-08 13:17:43 +00:00
Hagernesh
c20f395f5f fix: stamp export self-haul truck arrival on receive to warehouse
Import's customer_truck_assignments.arrived_at is set by a separate later
gate action (release()'s arrival branch — the truck returning to collect
already-warehoused goods). Export has no equivalent second step: the truck
delivering cargo to the warehouse arrives and is received in the same act,
so its arrival was never recorded anywhere.

Add markCustomerTruckArrived, mirroring release()'s existing self-haul
departure UPDATE (plate-matched, COALESCE(arrived_at, NOW())), and call it
from receive()/bulkReceive() for EXPORT bookings
2026-08-08 09:28:14 +00:00
Hagernesh
d229808fd8 Fix 2 — lastmile should only be yes if EDR last mile. 2026-07-31 13:07:18 +00:00
Hagernesh
2759960d7e generate and send notifcation for Carriage Acceptance sheet 2026-07-31 11:44:02 +00:00
Hagernesh
1b1b0e7ade feat(booking-trucks-tab): show truck-level detention, gate times, inspection status, and cargo costs
Booking detail now has a Trucks tab displaying every EDR or customer truck
assigned to a booking's last mile. Each row shows warehouse-gate times
(arrival/departure) and destination-detention times (arrival/return), with
detention costs and inspection status. Detention rows only for EDR trucks;
customer self-haul shows —. Reuses existing TruckDetentionModal and
FeePreviewModal for edit/view actions.

Backend: arrivalTrucksForBooking() adds lastMileId per truck (direct chain to
detention preview), containerItems() adds inspection_status column to responses.
No new SQL, no migrations, no new endpoints.

Frontend: BookingTrucksPanel.tsx self-fetches all data via existing warehouse +
last-mile services, renders adaptive table with cargo-cost strip above.
Wired into BookingRequestDetailPage.tsx tab bar.
2026-07-29 11:13:20 +00:00
Hagernesh
101bf69271 feat(warehouses): map GRN numbers to the goods owner
GRN-<DIR>-<DATE>-<REF8> carried no owner, so a note couldn't be
identified by who owns the cargo. Add an owner segment sourced from the
booking's company at every generation point (import, export, facility,
manual receive), keep REF8 for uniqueness, and label the GRN document
row Owner's Name.
2026-07-27 09:57:59 +00:00
Hagernesh
6983645347 feat(warehouses): gate double-handling fee on a per-booking Yes/No
Double handling billed every import with a matching rule. Add
bookings.double_handling (+ set_at/by), charge only when Yes, expose a
PATCH endpoint (import-only, locked after invoicing, audited) and Yes/No
items in the inventory row menu.
2026-07-25 10:45:34 +00:00
Hagernesh
a544bebc51 fix(operations): last-mile assign until load fully trucked + 40ft cap
Assign was gated on any truck assigned, blocking multi-truck
deliveries. Gate on remaining containers (or bulk tonnage) instead, show
covered/total in the modal, cap a 40ft container to one truck with no
size mixing (mirrors assertTruckLoad), and lock arrived/departed rows.
2026-07-25 10:29:44 +00:00
Hagernesh
73f36fe46e Export interchange document — a generation error after Djibouti unloading failed the whole request and left the document missing until a manual rerun. Generation is now best-effort (unload never fails on paperwork) and reruns backfill the document. 2026-07-23 11:31:44 +00:00
Hagernesh
f2f6ac616f KM-Based Maintenance Scheduling 2026-07-23 07:53:11 +00:00
Hagernesh
8dc4dd585e feat(procurement): validate acquisition lease fields and enforce bulk-receive capacity
Reject lease start/end and monthly payment on PURCHASE acquisitions (create and
update, validated against the resulting record). Add asset_acquisitions.item_name
column + migration. Enforce warehouse/yard/zone capacity on bulk receive and apply
capacity-counter deltas on save. Adds acquisition-guard spec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:15:47 +00:00
Hagernesh
19906b273e fix(operations): prefill booking context on single assign; enforce per-truck container loads
- single-row Assign vehicle uses the full single-record flow (details + containers)
- release() rejects exit containers not assigned to the departing truck
- weighing modal offers only the selected truck's assigned containers
2026-07-22 07:24:14 +00:00
Hagernesh
fd3212f326 fix(warehouses): exit-paper truck weight from summed container VGM
Per-truck weight now sums the departing truck's container vgm_tons
(recorded net for bulk); weighed gross only as fallback.
2026-07-22 07:05:26 +00:00
Hagernesh
b54cb2483e Card called GET /warehouse-inventory (staff perm) → 403 swallowed as
empty state. New customer-safe bookings/:id/location endpoint returns
location fields only; card now uses it.
2026-07-21 14:15:56 +00:00
Hagernesh
9da48bd61a fix 2026-07-21 12:57:37 +00:00
Hagernesh
7566c57ed9 feat(warehouses): per-truck EDR handover signing with auto-delivery on completion
EDR truck gate-out now generates a per-truck handover (new
edr_assignment_id link) and notifies the customer to sign from the
portal. New sign endpoint delivers the signed truck's containers; last
signature auto-delivers remaining inventory, frees trucks and completes
the booking via import.handover.completed. Adds gate-in truck-arrival
notification and per-truck handover PDFs.
2026-07-21 11:55:18 +00:00
Hagernesh
86569f7490 Customer portal card displays warehouse location data (warehouse/yard/zone + arrival time) once cargo arrives at warehouse. Shows loading state during fetch, placeholder text if not yet received. Type-check passes. 2026-07-21 11:55:18 +00:00
Hagernesh
1fae0752a0 Ticket: Warehouse dashboard Needs Attention cards — fix trucks-on-site aging counters (EDR trucks, UNLOADED status) and make each card open the exact list behind its count via new inventory drill-down filters. 2026-07-21 11:03:40 +00:00
Hagernesh
2397c6bca4 fix(warehouse): show assigned trucks on the Trucks on Site page, not only arrived ones
The page filtered on arrived_at IS NOT NULL, so a truck appeared only once the
warehouse receive flow stamped its arrival. Assigned trucks that had not yet
reached the yard were invisible, which left the page empty whenever nothing had
been received — every assigned truck was missing.

It now lists every truck assigned to a booking that has not departed, from both
haulage paths, tagged INBOUND (assigned, not yet arrived) or ON_SITE (arrived).
A scope toggle filters between them, dwell time shows only once a truck has
actually arrived, and the KPI count on the dashboard stays strict (arrived only).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:24:39 +00:00
Hagernesh
1fe8c2fd8e feat(export): gate receive on payment and loading on received + GRN
Three export rules that the flow left open.

An unpaid export booking could be received at the warehouse. Receiving is
what starts storage and mints a GRN, so it must not happen against cargo the
customer has not settled. receive() now rejects an unpaid EXPORT booking.
Import is untouched — it arrives OFF a train and its receive is the unload,
so gating that on payment would strand cargo already at the yard.

An allocated export booking could be marked loaded onto its train without
ever reaching the warehouse. An allocation is a plan; the GRN is the proof
the goods are in hand. Two loading paths skipped that check — the per-yard
loadBooking and the workspace confirmScheduleLoading — and both now require
every export booking to be received with a GRN first, however it arrived
(first-mile or the customer's own truck) and whatever it is allocated to.
The rule lives in one shared guard (assertExportReceivedWithGrn) so the two
paths cannot drift.

Export self-haul without a first-mile leg already worked and is unchanged:
assertSelfHaulPaid allows a customer truck when there is no EDR mile leg and
the booking is paid, and addTruck applies the same one-40ft-or-two-20ft rule
to containers and the tonnage drawdown to bulk, exactly as import does.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:01:32 +00:00
Hagernesh
36f0045b5d auto generate grn on import arrival unloading 2026-07-21 07:25:45 +00:00
Hagernesh
0c0de284b0 feat(warehouse): show the trucks in the yard, not just a count of them
Multi-truck self-haul had no list anywhere on the warehouse side. The ops
dashboard counted trucks on site and offered no way to open the list, and
the inventory table showed a blank plate on exactly the bookings that have
several trucks: it read booking.customer_truck_plate_number, which
multi-truck self-haul leaves null because plates live in
customer_truck_assignments. Booking BK-2026-000033 has a truck and a driver
on file and displayed neither.

Adds a Trucks on Site page listing every truck that has arrived and not yet
departed, across bookings, with plate, driver, booking, customer, containers
and dwell time. It covers both haulage paths because the gate does — a
customer's own truck and an EDR last-mile truck reach the same barrier — and
flags anything sitting over four hours. It lives under Warehouse Management
rather than Imports or Exports, since the yard is not per-direction.

The inventory queries now read plates and drivers from the assignments and
keep the booking columns as the fallback for single-truck bookings written
before that table existed.

Both new statements were EXPLAIN-validated against the live schema; the
plate fix returns the data that was previously null.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:54:10 +00:00
Hagernesh
c8a70593cf fix(mile): apply one truck-load rule to customer and EDR haulage alike
A truck holds one 40ft or two 20ft, a container booking takes no more trucks
than it has containers, and a bulk booking takes trucks until its tonnage is
hauled away. The same physics whoever drives, but the rule was written out
four times — addTruck, updateTruck, departTruck and LastMileService — beside
a byte-identical container-size query. Copies drift: that is how the
self-haul guard ended up enforced on one side only.

The bulk cap was the real gap. EDR summed net_weight_tons of departed trucks
and refused another once the booking was drawn down. The customer side had
no cap at all: for bulk it skipped straight past every check, so a self-haul
bulk booking could take unlimited trucks.

It could not simply reuse the EDR sum. customer_truck_assignments had no net
and no tare, only a gross_weight_kg that holds tonnes despite its name and
that nothing in the live flow ever wrote — release() recorded exit weights
against the EDR table alone, which is why all five customer trucks on dev
have neither weight nor departure. Any drawdown keyed on it would have
summed zero forever and never fired.

So the customer table now carries tare_weight_tons and net_weight_tons to
match the EDR one, release() records the customer truck's exit as it already
did for EDR, and the drawdown counts both sources — a booking hauls by one
path or the other and "until no tonnage is left" means the same either way.

Also locks a load once its truck has arrived on the EDR side, which the
customer side has always done, and fills the arrival form from the customer
truck on file: the prefill read booking.customer_truck_*, which multi-truck
self-haul leaves null, so a booking with a truck assigned opened blank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 11:41:57 +00:00
marshal
f672d66a92 Merge pull request #826 from Tria-plc/freight_feature/usermanagement
changes
2026-07-20 06:06:06 +03:00
Marshal
4969f62896 changes 2026-07-20 03:05:28 +00:00
Hagernesh
a73faecbe3 feat(intercity): raise a GRN when a facility loads or unloads cargo
Every facility raises a GRN — the goods changed hands, whether or not anyone
stores them. What differs is what happens next: Indode has a warehouse, so cargo
left there goes through the existing warehouse flow and accrues storage and
demurrage; Sebeta, Modjo, Adama and Dire Dawa only move cargo between train and
truck, so the handling event and its GRN are the whole record.

facility_handling_events carries that record because warehouse_inventory cannot:
its warehouse/yard/zone are NOT NULL, so a facility with equipment but no
warehouse could never have a row there. inventory_id links the storage record
when the facility does keep the cargo, which is what ties an Indode handover to
its demurrage.

generateGrnNumber moves to common/grn.util.ts so a GRN raised at a facility is
indistinguishable from one raised in a warehouse — the two live in different
tables, and a second generator would let the formats drift.

Recording is best-effort: the cargo moved regardless, so paperwork must never
fail the journey.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 10:40:16 +00:00
Hagernesh
fc636fbe4f fix(warehouse): scope the arrival GRN stamp to EXPORT only
The previous commit made autoUnloadArrived and unloadBooking raise a GRN for any
direction, which changed import behaviour. Import keeps its own GRN handling
(autoUnloadArrivedBookings) and is left exactly as it was. Both paths now stamp a
GRN only when the booking is EXPORT — the direction whose cargo needs one to be
loaded onto a train. Import and domestic behave as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:01:58 +00:00
Hagernesh
a95667fde4 fix(warehouse): raise a GRN on every warehouse receipt
Requiring a GRN before loading only works if every path into the warehouse
issues one. Two did not: autoUnloadArrived and unloadBooking created RECEIVED
inventory with a null grn_number, so cargo that genuinely arrived — by first
mile or self haul — would have been stuck un-loadable behind the new gate.

Both now stamp a GRN, derived from the booking's trade direction, matching
receive/bulkReceive/autoUnloadArrivedBookings. unloadBooking keeps an
already-issued GRN when it re-unloads an existing row rather than reissuing one.

Every path that creates warehouse inventory now issues a GRN, so the chain is
seamless: booking arrives (first mile or self haul) -> received -> GRN -> loadable
onto its allocated wagon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:01:58 +00:00
Hagernesh
a26fa61d66 feat(warehouse): require a GRN before export cargo can be loaded onto a train
The export chain is: paid -> received into the warehouse -> GRN raised on arrival
-> loaded onto the allocated wagon. Receipt was already structural (the inventory
row only exists once receive() runs) and the wagon was already required, but the
GRN was merely displayed, never enforced — so cargo could be loaded and
dispatched without one.

- loadable now also requires a GRN, so the queue won't offer un-GRN'd cargo.
- loadItemsOntoTrain skips items with no GRN, so the rule holds server-side and
  a hand-made API call can't bypass it.
- Read the GRN from inv.grn_number (what receive() stamps) and fall back to the
  note only for legacy/seeded rows; it previously read the note alone, which the
  real receive path merely mirrors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:01:57 +00:00
Hagernesh
2798fed6d8 fix(train-scheduling): block dispatch when allocated cargo is not loaded
dispatchSchedule guarded status, Djibouti departure rules, locomotives and
wagons — but never checked the cargo. A train could be dispatched while the
bookings allocated to it sat received in the warehouse, silently leaving them
behind. Dispatch now refuses when an allocated booking has warehouse inventory
in RECEIVED/STORED/READY_FOR_LOADING, naming the bookings and pointing at the
two ways out: load them, or drop the wagon allocation so they ride a later
train. Bookings with no inventory at all are not blocked — allocating a wagon
before the goods arrive is normal planning.

Also drops RESERVED from the Load-to-Train filters: reserved stock is not
awaiting loading.

The sched_bookings CTE moves to common/schedule-bookings.sql so the warehouse
loading queue and this dispatch guard resolve a train's bookings identically —
if they drift, a train departs leaving cargo the warehouse still expects to load.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:01:57 +00:00
Hagernesh
e115b9b753 fix(warehouse): resolve Load-to-Train bookings from wagon allocation
The Load-to-Train queue was permanently empty for real traffic. loadableTrains()
and trainLoadableItems() gated on freight.train_schedule_bookings, but nothing in
the application writes that table — only the demo seeders do. Real wagon
allocation writes wagon_booking_allocations, reached via
train_schedules -> train_sets -> train_set_wagons, so an allocated export booking
never satisfied the EXISTS gate and no train ever appeared.

Both queries now resolve a schedule's bookings through a shared sched_bookings
CTE that unions the wagon-allocation chain with train_schedule_bookings, so real
allocations show up and the seeded demo scenarios keep working. The panel already
groups the returned rows by booking with their containers, so the queue now lists
the train, its bookings and their containers for selection.

Export flow this serves: booked -> paid -> received at the warehouse (first-mile
or self-haul) -> GRN -> loaded onto the wagons allocated to the booking.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 08:01:57 +00:00
Nathnael
e626e1db63 Merge branch 'dev' into freight/feat/fixes-v1 2026-07-16 13:23:40 +00:00