Nest runs class and method guards together, so a class-level view key
ANDs with every action key below it. Staff granted only an action were
denied before their key was ever checked: OCC could not fulfil wagon
transfers, dispatchers could not create a yard, and track staff could not
assign first/last-mile vehicles. Reads now carry the view key themselves,
and the warehouses baseline lists every key its routes use.
The rate matrix currency cell hardcoded USD, so a last-mile rate
priced in ETB still displayed as dollars. formatCell now takes the
row and reads its currency code, falling back to USD.
Last-mile currency select gets defaultValue ETB (new generic
FormFieldDef.defaultValue for create-time pre-selection) and lists
ETB (Birr) first; USD stays selectable. Backend already persisted
and validated the chosen currency.
Enable per-truck editing of warehouse gate arrival/departure times
(arrivedAt/departedAt) via modal on Import Trucks page. Accessible via
row action menu for EDR-haulage trucks. Includes backend endpoint
POST /last-mile/:id/warehouse-gate-times and frontend modal with
DateTimePicker inputs.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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.
Create tests, debug failures, and improve quality faster than ever. Get AI-powered guidance at every stage of testing, with full code ownership, on a platform that connects your team.
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>
The rule that a customer's own truck and an EDR road leg are alternatives
existed on the truck side only (CustomerTruckService.assertSelfHaulPaid).
LastMileService had no counterpart: create checked payment and nothing else,
so any paid booking could be accepted into the queue. A booking took a
customer truck at 06:42 and an EDR last-mile leg with a real EDR truck at
06:47, neither side aware of the other, on a contract that had chosen no
road legs at all.
The road legs are chosen on the contract and copied onto the booking, and
the pickup/delivery address is the only per-booking record of that choice.
service_types cannot serve: every type ships with includes_first_mile and
includes_last_mile set to true, so reading them would mean no booking could
ever self-haul. That same always-true flag had already killed the first-mile
guard, whose `address || serviceType.includesFirstMile` admitted every paid
export booking.
One shared rule now answers it for both sides, so the two halves cannot
drift apart again: last-mile create rejects a booking that chose no road
legs and one already carrying a customer truck; first-mile no longer honours
the service-type flag; the customer-truck guard reads the same helper.
Existing legs are untouched — the guards are on creation, so the one booking
already carrying both needs a human to reconcile it.
Co-Authored-By: Claude Opus 4.8 <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>
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>