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>
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>
Frontend for the per-truck EDR backend:
- Assign modal takes a container LOAD per truck (MultiSelect, max 2) instead of a
single container, so a truck can carry one 40ft or two 20ft. Sends
containerNumbers[]; the API enforces the size rule.
- requiredVehicles is now size-aware: a 40ft fills a truck (1 each), two 20ft
share one. It previously assumed ceil(containers / 2) regardless of size, so a
2x40ft booking under-counted the trucks needed.
- Bulk bookings show the drawdown in the assign modal — remaining vs total
tonnage, what's been hauled, and a "fully hauled" state when it reaches 0.
- New "Truck exit papers" row action opens a per-truck list (plate, containers,
arrival, exit, net weight) with a per-truck exit-paper download.
- last-mile findAll/findById now load each assignment's containers so the UI can
hydrate a truck's load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EDR last-mile is multi-truck but was modelled as one: setVehicles accepted any
number of trucks with no validation, arrival/delivery were stamped once per
last_mile record (N trucks shared one timestamp), EDR trucks got no exit paper,
and the per-truck EDR handover never happened because deliver() resolved the
plate via last_mile_container_allocations — a table nothing writes.
- Migration 2260000000000: per-truck arrived_at/departed_at/gross_weight_tons/
net_weight_tons on last_mile_vehicle_assignments, plus a
last_mile_vehicle_containers child table (a truck holds 1x40ft OR 2x20ft, so
the single container_number scalar could not express a load). Weights are
TONNES and named accordingly — the older gross_weight_kg lies about its unit.
- setVehicles: enforce the size rule (one 40ft, or two 20ft), container
membership, one-container-one-truck, and never more trucks than containers.
Bulk carries no containers and is instead gated on tonnage remaining.
- Bulk drawdown: remainingTonsForBooking = booking VGM minus the net weighed off
every departed truck (both tonnes, no conversion), exposed as
GET /last-mile/booking/:bookingId/remaining-tons.
- release() now stamps the EDR truck's own arrival and exit (matched by plate, so
it works for bulk too) alongside the existing customer-truck stamp. The exit
weighing itself is untouched.
- New GET /warehouse-inventory/edr-truck-exit-paper/:assignmentId — per-truck
exit paper for EDR trucks. Deliberately not signature-gated: EDR handovers are
generated at delivery, after the truck has left.
- deliver(): resolve the handover plate from the truck's own containers instead
of the dead allocations table, so EDR handovers are genuinely per-truck.
- Notify the customer (portal inbox + SMS/email) when a truck leaves — one hook
in release() covers both self-haul and EDR, since it is the single exit path.
Self-haul is intentionally unchanged (one booking-level handover signed once).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add actorLabel(user) — resolves the authenticated user to a readable name
(name → username → email → id) — and use it for the performed_by audit stamp on
every warehouse action, so the activity log shows a person, not a UUID. The
freight DB has no users table to join, so the name is stamped at write time.
approve-delivery keeps the raw user id (it is an id argument, not the audit
label). Existing rows keep their prior value; this applies going forward.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Warehouse mutation endpoints now record the JWT-authenticated user as the actor
(user.id) instead of trusting a client-supplied performedBy string, unlocking
per-operator productivity metrics and a trustworthy audit trail. Covers receive,
receive-bulk, reserve, store, ready-for-loading, ready-for-pickup, load-onto-
train, bulk-dispatch, dispatch, deliver, gate-clearance, approve-delivery, the
Djibouti/import auto-unload actions, and fee-invoice generation. The prior
client value is kept only as a fallback for unauthenticated/internal calls.
move/load do not yet carry an actor (their DTOs have no performedBy) — separate
follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- gateStats(): items cleared through the gate today, average arrival→gate
turnaround (hours, 30d), and clearances per hour over the last 24h → new
GateThroughputCard in the dashboard Performance section.
- Live board: every dashboard query (dashboard, ops, throughput, dwell, cycle,
on-time, zone occupancy, accrual, gate) now auto-refreshes on a 60s interval,
with a "Live" indicator in the header.
New endpoint GET /warehouse-inventory/gate-stats (guarded). Deferred (need
upstream data): capacity forecast, labour productivity, WebSocket push, yard map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the operational-performance layer to the warehouse cockpit:
- Dwell time & aging: dwellStats() (avg + 0-3/4-7/8-14/15+ buckets over
in-warehouse items) → DwellAgingCard histogram.
- Cycle time & on-time: cycleStats() (arrived→ready→loaded→dispatched stage
averages + dock-to-dispatch) and onTimeDispatchStats() (share of items that
left before their storage free-days expired, resolved via the fee engine's
own rule matching) → CycleTimeCard.
- Live tiles: opsStats() now returns receivedYesterday; KpiStrip renders an
optional ▲/▼ delta, and the ops strip shows received-today vs yesterday.
New endpoints: GET /warehouse-inventory/{dwell-stats,cycle-stats} and
/warehouse-fees/on-time-dispatch (all guarded). New "Performance" section on
WarehouseDashboardPage. On-time reads N/A when there is no sample / no active
storage rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- WarehouseDashboardPage now composes the ops KPI strip, lifecycle cards, flow
charts, zone-occupancy heatmap and demurrage/accrual exceptions into one
control-tower view; drop the redundant lifecycle donut.
- New GET /warehouse-inventory/throughput (date_trunc time series) replaces the
client-side buildTrend that downloaded the entire inventory list to bucket it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Removed the field from and related components.
- Updated to reflect the removal of the reopen delay input field.
- Modified to include new train number fields: and .
- Added interface to manage active schedules with trade direction.
- Introduced interface to track wagon shortages in bookings.
- Updated logic to ensure consistent UI state representation.
- Created migrations to drop the column and add and columns to the table.
- Added tests for the new booking window display logic and wagon planning functionality.
Truck loading:
- loadTruck enforces max 2 containers / one 40ft (two 20ft) and auto-marks an
assigned truck arrived on load; container-items payload + modal expose
container size with a client-side selection cap.
- Show "#x containers pending assignment" in the portal truck card and the
backoffice container modal.
Last-mile:
- create() is idempotent — return the existing record for a booking instead of
inserting a duplicate delivery row (fixed the same booking showing twice in
Assign-Mile).
- setVehicles/update reject a truck with no assigned driver; the Assign toast
now surfaces the reason.
- New GET /last-mile/booking/:id/arrival-trucks returns assigned EDR trucks with
driver details; ReleaseOrderModal fetches and auto-fills them so an assigned
EDR truck no longer reads as "not assigned yet".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>