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.
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.
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
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
Nullable freight_type on the warehouse, null meaning it takes both.
No backfill: every existing warehouse is unrestricted today and
writing a value would narrow allocation behind the operator's back.
Reuses FREIGHT_TYPES from the booking entity rather than a third copy
of the same two values.
Yard and zone soft-delete, refused with 409 while a yard still has
zones or a zone still holds inventory. warehouse_zones:delete was
missing from the catalog — the role presets spread every zone key, so
its absence crashes FreightPositionsSeeder at boot; a migration seeds
it everywhere.
Clicking a zone opens its contents as a datatable. Container identity
comes from booking_container_units for booked cargo and from
containers for backlog registrations; bulk cargo keeps its row with no
container number rather than disappearing from the zone.
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.
An invoice is billed to one company profile, and that profile's eTrade
licence is usually a different business from the one the company
registered under — so the buyer's name alone does not say which business
was billed.
Both document paths gain a row: the shared invoice/receipt model reads it
off the already-loaded `companyProfile` relation, and the warehouse fee
invoice joins `company_profiles` through the booking.
`sameCompanyName` suppresses the row when it merely repeats the buyer
name, which is the common case. It compares loosely because eTrade spells
one legal suffix three ways (PLC / P L C / PRIVATE LIMITED COMPANY) and
pads names with double spaces; it decides whether a row is worth printing
and nothing else.
The EIMS buyer `LegalName` is deliberately untouched — a MoR filing
carries the registered entity, same rule as the seller side.
Backs the previously-dead header date-picker and Filters button with
an actual scoped query. getDashboard() takes optional dateFrom/dateTo/
warehouseId: received (renamed from receivedToday) is the only
activity counter and respects the date range, defaulting to today when
omitted; the 10 status-backlog counters respect warehouseId only;
totalWarehouses/emptyContainers/importTrains/exportTrains stay global
since nothing ties them to a specific warehouse in the schema. Adds a
warehouse Select + date-range picker to the dashboard header, with a
caption spelling out what is and isn't scoped.
Extends the warehouse dashboard with 3 metrics the screenshot target
needed but the backend didn't expose: emptyContainers (AVAILABLE
containers — closest proxy, no literal EMPTY status exists),
importTrains (reuses the import arrival queue definition), and
exportTrains (reuses the Djibouti export arrival queue definition) via
SchedulingReadFacade. Reorders the frontend metric grid to match and
fixes the Empty Containers card linking to a route that doesn't exist.
- Add ShippingLineBookingsPage for listing and managing shipping line bookings.
- Create ShippingLineDocumentsModal for document uploads related to bookings.
- Introduce ShippingLineInitiateModal for initiating new shipping line bookings.
- Implement booking document state management with booking-doc-state utility.
- Add shipping line bookings service for API interactions.
- Update index to export new components and services.
- Enhance types for freight to include shipping line credits.
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().
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.
>
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.
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.
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
Nest runs class and method guards together, so a class gate naming only
the view key silently required view AND action. Staff granted just an
action were denied before their key was checked. Each class gate now
names every key its routes use, and FleetView accepts an array so the
fleet controllers keep their coarse fallback.
Drops the one-off grant mapping SQL with it: already applied to dev, and
this fix removes the companion-view rule that was its recurring part.
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.
Gates the previously open support-agent, procurement, compliance,
facilities, list-users and trade-access controllers, separates customer
from staff routes across bookings, contracts, companies, billing,
warehouses, files and train scheduling, and moves billing, overview,
reports and the settings controllers onto their own keys instead of the
blanket admin key. Drops the demo-permissions module and the untested
notification test route.
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.
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.
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.
Join truck_types via vehicles.truck_type_id (normalized legacy
vehicle_type only as fallback) so type renames can't unmatch detention
rules and FK-less vehicles keep billing.
Store the signer's saved-signature URL on booking_handovers (new
signature_image_url column) when a handover is signed — per-truck or
booking-level — so signed handover documents can render the actual
signature, matching the contract-signing flow.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Yard create/update now rejects capacities that overflow the parent warehouse,
and zone create/update rejects capacities that overflow the parent yard, via
BadRequestException. Dispatcher permission preset expanded to full CRUD on
warehouse and fleet management; allocation and fee rules remain view-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 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
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.
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>