- Added functionality to move containers between wagons in the train scheduling system.
- Introduced API endpoint and service method to handle container movement.
- Updated component to support drag-and-drop for rearranging containers.
- Enhanced to allow moving containers to other wagons via a context menu.
- Implemented UI feedback for container movement actions, including loading states and success/error notifications.
- Updated relevant types and constants to accommodate new container movement logic.
- Added tests for the rule engine to ensure proper handling of hazardous bookings.
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>
The IsNotBackdated validator was covered in isolation, but not on the DTO
that actually carries it. Asserts a backdated occurredAt is rejected, that
"now" passes, and that omitting the field still validates so the service can
stamp it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Containers need a reach stacker or gantry, so only Indode, Modjo and Dire Dawa
take them. Bulk needs far less and is handled at all five facilities. Having a
facility was previously enough to load anything, so a container booking through
Sebeta or Adama would have been accepted and then had nothing to lift it.
- yard_facilities gains handles_container / handles_bulk, both defaulting true so
a facility handles everything unless told otherwise; the seeder states the real
capability.
- The intercity gate now refuses cargo a facility cannot lift, saying which type,
not just "no facility". canHandleFreight keeps that rule in the resolver so
callers cannot get it subtly wrong.
- The intercity list resolves each end against the booking's own freight type, so
the view flags a container booking routed through a bulk-only yard while the
train is still coming rather than when the load is refused.
Import/export untouched — the gate is still DOMESTIC-only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Introduced DEACTIVATED status for trains, allowing staff to park trains indefinitely.
- Implemented methods to deactivate and reactivate trains in the TrainBuilderService.
- Added UI components for train deactivation and reactivation in TrainBuilderDetailPage.
- Created a dropdown setting for admin-managed import train numbers, with corresponding migrations.
- Updated yard code length to accommodate soft-delete suffix.
- Enhanced train status handling to include DEACTIVATED state.
- Introduced new boolean fields (isHazardous, isReefer, isReturn) in UnitDraft and related interfaces to allow individual container handling options.
- Updated emptyUnit function to initialize these new fields.
- Modified GlCreateBookingForm to handle and display these options for each container.
- Adjusted calculations for hazardous, reefer, and return quantities based on the new handling options.
- Updated the schema for container units and booking container lines to include handling options.
- Added migration to support the new return flag in the database.
- Enhanced various components to reflect gross weight calculations, ensuring consistency across the application.
Intercity bookings never get their own train — they ride whichever import/export
train passes through their corridor — so the work is scattered across other
people's schedules and there was nowhere to see it as a whole. The per-schedule
ride-along panel answers "what can THIS train carry"; this answers "what is
happening to intercity cargo".
Purely additive: the existing ride-along panel and the schedule detail page are
untouched, and loading/unloading still happens there, where the train's position
is confirmed. This is a read-only view that points back to it.
Each row carries both ends' facility status, because a booking whose origin or
destination has no equipment can never be worked there — the operator should see
that while the train is still coming, not when the load is refused. Those
bookings are counted and called out.
New GET /train-scheduling/intercity/bookings; the type is IntercityRideAlongRow,
not IntercityBookingRow, which already means the per-schedule candidate row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Intercity cargo rides a passing train and is handled at the booking's own yards,
so those yards need the equipment to do it — a train stopping somewhere is not
the same as somewhere being able to load it. Loading at an origin or unloading at
a destination without has_facility is now refused, naming the yard.
The check sits inside loadBooking/unloadBooking rather than the intercity
controller wrapper, because recording a checkpoint auto-unloads every booking
destined at that yard (autoUnloadAtYard) and would otherwise route around it.
Import/export are untouched: their cargo is handled at the route's terminal
ports, not at an arbitrary mid-corridor yard, so the gate returns early for
anything that isn't DOMESTIC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The guard fired on every dispatch. Loading out of an origin warehouse is an
export concept — import cargo isn't loaded from a warehouse, so its warehouse
inventory says nothing about what's aboard and the check would have blocked
legitimate import dispatches. Derive the route direction (reusing
deriveTradeDirection, as the warehouse loading queue does) and return early for
anything that isn't EXPORT. Import and domestic behave exactly as before.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
- 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.
- Added parameter to and for server-side free-text search on contract reference, company name, and booking details.
- Introduced new validation errors in for container clashes and space issues when creating bookings.
- Implemented paginated dropdown settings retrieval in .
- Updated to fetch active yards using a new method that handles pagination.
- Enhanced with a method to fetch all records by walking through pages.
- Refactored to support filtering and pagination in schedule listings.
- Improved to return a paginated list of facilities.
- Updated UI components in and to utilize debounced search inputs for better performance.
- Added alerts in to inform users about booking constraints related to splits and capacity.
- Enhanced to display notifications for split bookings and capacity usage.