The receive drawer captured one truck per arrival, so a customer whose
containers came on three trucks had to be received three times over.
Stage trucks instead. "Add another truck" validates the form exactly as a
single-truck receive does, files the truck with its own plate, driver and
containers, and clears the form for the next one. The staged trucks are
listed above the form with their boxes, each removable, and the arrival is
received in one operation.
Containers already staged on an earlier truck drop out of the picker, so
the same box cannot be sent twice.
Receiving with a part-filled form and trucks already staged is treated as
the operator still typing rather than a finished arrival: it asks them to
finish or clear the truck instead of silently dropping what they entered.
An untouched form receives just the staged trucks.
With no trucks staged the drawer sends the original truckEntrance and
containerNumbers fields, so a single-truck arrival takes exactly the path
it did before.
Verified: backoffice type-check reports 982 errors both with and without
this change (all pre-existing, none in the touched files); 13 warehouse
API suites pass (73 tests).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
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.
Extends the stat-tile/filter/pagination pattern from the import arrival
queue to every remaining import and export tab (unloaded, eligible,
received, ready-to-load, loaded/dispatch, locate-booking), reusing
useListControls/ListControls/RuleEngineListFooter throughout. Adds a
5-tile stat row to the export tab bar and renames ImportStatCard to
WarehouseStatCard now that it's shared by both directions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds Arrived/Unloaded/Dispatch Ready/Total Bookings stat tiles above the
import queue tabs, wires search+date filters and pagination into the
arrival queue table (reusing useListControls/ListControls/
RuleEngineListFooter already used by Inventory Inquiry), and adds a
copy-to-clipboard action on the truncated schedule ID.
Top Returned Containers table ignored the EDR/Customer tab because
the backend never persisted which truck type performed the return.
Added returned_by column + DTO/entity field, wired create payload to
send it, and filtered the table by the active tab.
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.
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.
Every booking row in the queue tables (export received, ready-to-load,
loaded/dispatch, import unloaded) gets a chevron that expands to the
booking's items: container number, goods, lifecycle stage badge
(PENDING..DELIVERED), carrying truck, and GRN.
Bulk bookings (no container units) show a one-line summary instead
("Bulk cargo — Coffee export cargo, 3,200 t"). The expansion reuses the
['container-items', bookingId] query the container modal already uses, so
data is shared and instant after either has loaded it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- statuses render through the shared InventoryStatusBadge (humanized label,
consistent per-status color) instead of raw enums in ad-hoc badges
- Ready-To-Load drops its selection checkboxes: nothing consumed the
selection since auto-load became train-scoped
- bulk toasts now say WHY items were skipped ("3 skipped — Booking not PAID")
via a shared skippedSummary helper
- one-click irreversible bulk actions (Dispatch All/Selected, Mark Selected
as Inspected x2, Auto Unload train) now ask for confirmation through a
small local ConfirmActionModal - no @mantine/modals dependency added.
Receive All stays unconfirmed: it already funnels through the
truck-entrance modal with an explicit Save.
- table minWidths retuned for the slimmed column sets
(1700->1350, 1600->1200, 2000->1650)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Queue tables showed "Booking ID" / "Customer ID" as dimmed 8-char UUID hashes
next to the human-readable Booking Ref and Customer Name - unusable columns
that forced extra horizontal scrolling. Removed across all six tables
(eligible, received, ready-to-load, loaded/dispatch, import unloaded, import
train detail).
The GRN document button also rendered twice per row (inside the Booking Ref
cell and again in the GRN column) in four tables - kept the GRN column only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Auto Load Ready Items" now opens a train picker (pre-dispatch trains with
these bookings assigned, via the existing loadable-trains flow). No train
available -> no auto-loading, with a clear notice. Loading goes through the
existing per-wagon load path, so items without an allocated wagon are skipped
with a reason.
The train association is stored on the existing warehouse_loadings table
(no new table needed): new train_schedule_id column + a note recording train
number, origin -> destination, and departure time; wagon_id becomes nullable.
The trainless load-passed-export endpoint, its frontend wiring, and the unused
useLoadPassedExport hook are removed.
Migration 2100000000000 (idempotent) also applied to the dev database.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- remove Incoterms, HS codes, and Item code from the truck-entrance form:
nothing in the booking captures them, so they were always hand-typed noise
- remove the hand-typed "Warehouse code and location" input: the backend now
stamps it from the warehouse/yard/zone the operator actually selected
(WH / YARD / ZONE codes), so the GRN and notes always match reality
- Declaration number and Item description widen to full rows
Backend DTO keeps the optional fields for compatibility.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The receive form now arrives filled with everything the booking already knows,
instead of the operator re-typing it:
- container numbers come from the per-unit records (booking_container_units)
entered at booking time, falling back to the line-level aggregate
- customs seal number prefilled from the units' seal numbers
- net weight prefilled from the booking's declared cargo weight (tonnes)
- driver signatory defaults to the arriving driver's name
Existing prefills (owner, TIN, phone, booking ref, cargo, packaging, unit
count, first-mile / customer truck + driver) unchanged; all fields stay
editable where they were editable before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wagon loading happens in the train flow and dispatch at the train level, which
already advances inventory - the per-row buttons duplicated that and confused
operators.
- WarehouseInventoryTable (import dispatch queue + warehouse pages): suppress
the 'load'/'dispatch' next-action buttons and drop the extra per-row Dispatch
on READY_FOR_PICKUP rows (Store stays)
- Export Dispatch Queue: drop the per-row Dispatch button and its Actions
column; the bulk Dispatch All / Dispatch Selected controls remain
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Approve delivery now tracks the handover lifecycle exactly: the button appears
(detail page + portal dashboard) the moment a handover is generated and
disappears when the customer signs. The bookings list attaches the
handoverAwaitingSignature flag via one batched query per page; status
heuristics (COMPLETED / TRUCK_ASSIGNED+arrived) are gone.
- generate the arrival handover for ANY self-haul truck: portal-assigned OR
walk-in registered at the gate (isSelfHaulBooking: assigned_at set, or no
EDR last-mile leg). Same rule now guards the exit paper.
- remind every 5 minutes (in-app + SMS + email) until the handover is signed
(@Cron in HandoverService; one reminder per booking per tick)
- Truck Leaving no longer opens blank: the import queue mapper now carries
inv.notes, so the saved arrival renders read-only with only gate-out time
and gross weight editable
- container bookings get "Weigh truck? Yes/No": No skips tare/gross and the
container weight match (weighingSkipped on ReleaseOrderDto, decision made at
arrival sticks for the exit via the Weighing: SKIPPED note). Bulk always
weighs, unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blob downloads set responseType: 'blob', so axios delivers the JSON error body
as a Blob. extractErrorMessage() reads `.message` off it, finds nothing, and
falls back to "Request failed with status code 400" - hiding every real reason
("Handover must be signed...", "...must be fully paid before terminal release").
Only ContainerItemsModal used the async Blob decoder. Switch the remaining nine
blob-download catches to extractDownloadErrorMessage():
InventoryWorkbench release paper, handover
WarehouseInventoryTable GRN
ReceiveInventoryModal GRN (x2), handover, exit paper
FeePreviewModal release paper
TruckDispatchModal truck exit paper
Mutation-error catches are untouched - their bodies are already parsed JSON.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a per-container/bulk items view for a booking with derived lifecycle
stage (PENDING -> RECEIVED -> GRN -> LOADED -> LEFT -> DELIVERED) and reference
badges (booking, contract, last-mile). Backend containerItems() aggregates
booking_container_units + customer_truck_containers + inventory; exposed at
GET warehouse-inventory/bookings/:id/container-items.
Frontend ContainerItemsModal (opened from the View Details eye): stage tabs
with counts, ref columns, checkbox multiselect of loadable items -> pick truck
-> load (Truck_dispatch), and per-row per-truck Exit Paper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- READY_FOR_PICKUP transitions now → DELIVERED (customer pickup) OR DISPATCHED (dispatch out) OR
STORED (operator-chosen storage) — pickup and dispatch kept separate. No auto-storage.
- GET /warehouse-inventory/import/pickup-ready-queue: READY_FOR_PICKUP import items (shared query
with the unloaded queue, route-derived import filter)
- WarehouseInventoryTable: PICKUP_READY rows now show explicit Store + Dispatch buttons alongside
the customer-pickup (release/deliver) next action; reuses the existing advance() dispatcher
- Import → Dispatch Queue tab renders pickup-ready items via InventoryWorkbench (all existing
actions + modals intact) with Last Mile shown only when door delivery was requested
- Verified: pickup-ready item → Store → STORED; → Dispatch → DISPATCHED
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- eligibleBookings(direction?) now returns all import+export bookings when direction omitted
(DOMESTIC excluded); per-direction filter preserved for backward compatibility
- GET /warehouse-inventory/eligible-bookings (no query) returns the combined set
- Frontend: single shared useEligibleBookings() query; both Receive tabs filter client-side
by route-derived direction, so only one HTTP request fires
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- GET /warehouse-inventory/ready-to-load-export: EXPORT+PASSED+READY_FOR_LOADING items
with booking details (customer, container, cargo type, route, inspection status)
- Direction filtering via deriveTradeDirection (route-based, not stored field)
- Frontend ReadyToLoadTab: full table (checkbox, booking ref/id, customer, container,
cargo type, weight, route, inspection status, current status) with selection
- Auto Load Ready Items button reuses existing POST /load-passed-export endpoint
- Replaces "Ready To Load — coming in the next batch" placeholder in Export sub-tabs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BulkReceiveModal: Receive Selected shows "received at facility" for EXPORT
- Export inspection: POST /warehouse-inventory/bulk-mark-inspected reuses WarehouseInspectionService.create
- EXPORT items advance to READY_FOR_LOADING after inspection PASSED
- InventoryWorkbench: selection state + "Mark Selected as Inspected" bulk button
- WarehouseInventoryTable: optional Checkbox column for bulk selection
- Route-based direction in eligibleBookings, bulkReceive, getBookingDirection
- New BulkInspectDto; service + controller wired
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>