- Implemented method in to allow customers to request an extension for expired contracts.
- Added component in for users to initiate extension requests.
- Updated to include logic for handling extension requests for expired contracts.
- Enhanced to display extension request options and status.
- Created migration to add and columns to the contracts table.
- Added unit tests for contract extension request and handling in .
- Defined DTOs for request and extension in .
- Updated types in to include new fields related to contract extensions.
- Added TrainCrewAssignment module with controller and service for managing crew assignments.
- Integrated TrainCrewAssignmentService into TrainSchedulingService to ensure crew readiness before train dispatch.
- Updated TrainScheduling module to include TrainCrewModule for dependency injection.
- Introduced new permissions for assigning train crew in freight permissions registry.
- Enhanced front-end ScheduleCrewPage to allow assignment of crew members to train schedules, including validation and UI for adding/removing drivers and support crew.
- Created trainCrewAssignment.service to handle API interactions for crew assignments.
- Added DTOs for creating, querying, and updating train crew members.
- Created entity for train crew members with relevant fields and enums for role, nationality, and status.
- Developed service for handling CRUD operations and ensuring no duplicate crew members.
- Implemented controller to manage API endpoints for train crew operations.
- Integrated permissions for viewing, creating, updating, and deleting train crew members.
- Added frontend components for displaying, adding, editing, and deleting train crew members.
- Established API service for interacting with the train crew backend.
- Updated routing and sidebar to include train crew management section.
Currency dropdowns/pickers (AdditionalPaymentsTab, ClearanceChargesTab,
PhasedClearanceActionPanel, AdviseDutyCard, ContractRequestsPage,
ruleEngine/resources, WarehouseRulesPage, VehicleDetailPage,
FeePreviewModal) offer DJF alongside ETB/USD; GlCreateBookingForm's
currency selector gets allowDjf next to allowUsd. Narrow 'ETB'|'USD'
type unions widened to include 'DJF' across the warehouse
billingCurrency plumbing (useWarehouses, warehouse.service, api.ts)
and the customer/invoice types.
Ad-hoc money() formatters (BookingTrucksPanel, AccrualDashboard,
ImportTrucksPage, EmptyReturnRequestsPage) and formatMoney call sites
that hardcoded 2 decimals (wagon-cancellation cards, BookingRequestDetailPage,
WagonCancellationsPage, PaymentsPage, WarehouseInvoicesPage) now use
currencyDecimals() from @edr/ui-common so DJF renders with 0 decimals
instead of forced cents. The 3 duplicate overview formatCurrency/
formatAmount helpers (typed 'ETB'|'USD') widen to accept any currency.
Two correctness fixes: OverviewRecentBookingsTable's currency==='USD'
? 'USD' : 'ETB' was mislabeling every non-USD currency as ETB; and
WarehouseInvoicesPage's gateway-method default now routes any
non-ETB currency (not just USD) to WAAFI, so DJF invoices get a
working default instead of TELEBIRR (ETB-only).
Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
exchangeSettings.service/hook/card follow the backend's new shape: a
single ExchangeSettings object becomes a list, get() becomes list(),
and setFallbackRate(rate) becomes setFallbackRate(currency, rate).
ExchangeRateSettingsCard now renders one row per currency instead of
one hardcoded USD->ETB form.
manualPaymentSettings.service/hook/card add djfEnabled alongside
etb/usdEnabled, matching the backend's new column.
InvoicesPage's collected-summary KPI strip adds a DJF tile and folds
its ETB-equivalent into the existing 'Total collected' conversion,
reading each currency's rate from the now-list-shaped settings query.
Claude-Session: https://claude.ai/code/session_01CZy77vCWhka3pnmVF9NDkL
- Implemented for Excel download of wagon performance report sections, including column width adjustment and timestamped filenames.
- Created to compute derived wagon performance figures based on movements and status logs, ensuring consistency with API data.
- Enhanced with new fields for tracking last movement and statistics window days for improved reporting capabilities.
- Implemented BookingWagonsPanel to show allocated wagons, their containers, and export functionality.
- Integrated the new panel into BookingRequestDetailPage and BookingRequestsPage.
- Enhanced wagon cancellation modal to support rebooking of wagon cancellations with partner units.
- Updated API service to include a method for downloading wagons workbook.
- Modified types and constants to accommodate new features related to wagons.
- Adjusted various components and pages to ensure compatibility with the new wagon-related functionality.
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.
A booking sold WITHOUT the return service had no way to send its empties
back: the containers were the customer's problem and nothing in the
system priced, billed or planned the movement.
Adds the request flow end to end. The customer opens the booking, says
how many containers are coming back and types each number; the request
lands in a new backoffice queue (Empty Return Requests). Approval prices
it off the same live WITH_RETURN route rate the rule engine bills when
the service IS bought up front — per container, converted to birr, and
overridable by the reviewer — and issues the invoice there and then.
Payment settles through the normal invoice path, whose
`empty_return_request.invoice.paid` event moves the request to PAID; the
customer then books the return date and the truck.
Scheduled requests surface on Container Returns as Planned Empty
Returns, where confirming the arrival records the containers through the
existing empty-container-return flow — which in turn closes the request
once its last container is in.
Container freight only, never a booking that already ships WITH_RETURN,
and only from IN_TRANSIT onward: the empty comes back after delivery, so
the option has to outlive ARRIVED.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Container Returns only ever offered the last-mile path: a booking reached
the list once it had a truck assigned and warehouse inventory flagged as
returning. Bookings that ship WITH equipment return had no way in, so the
empties they owe were invisible until that path happened to fire.
Adds GET /import-operations/empty-return-bookings — the containers a
booking flagged is_return, carrying whichever of them already has an
empty return recorded, grouped one row per booking and dropped from the
list once nothing is pending. Covers both spellings of the booking's
equipment_return (WITH_RETURN and the older RETURN) and skips bookings
that never ship.
Backoffice grows a "Bookings With Empty Container Return" card above the
existing sections: pick the booking, tick the containers coming back,
say where they landed, and each tick becomes an empty container return
on that booking — which is what the Returned Containers table then
advances. The existing last-mile, standalone and bulk flows are
untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
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
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.
Backend now generates a separate marshalling document per corridor stop
where the consist actually coupled/uncoupled/switched something
(marshallingStops/marshallingDocumentAt), instead of one 'current
position' doc. Wires that into the backoffice:
- TrainScheduleV2DetailPage: the single Intercity Marshalling menu item
becomes one item per stop with a logged change, falling back to the
old single item when nothing has happened yet.
- TrainScheduleTrackPage: same fallback/menu treatment on its own
marshalling button.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Empties already sitting in an EDR yard but never entered in the system had
to be typed one at a time. Adds a bulk path: parse the sheet in the browser
(all-or-nothing, row-numbered errors), preview it, then POST one batch.
The server rejects the batch if any container already has a non-COMPLETED
return, so re-uploading the same sheet cannot duplicate boxes. No interchange
notification fires — these are historical rows, not a live handover.
Company is an Autocomplete over registered customers that also accepts a
typed name, since a backfilled box may belong to a company that is not a
customer yet. Exact name match sets customer_id; the name always lands in the
new empty_container_returns.company_name.
Also fixes the single Record Return modal, which collected Yard and Zone and
then dropped them before the API call, and did not invalidate the returns
list after a standalone return.
A GENERAL + customs contract does not let the customer book directly: they
submit a shipment request, and initiateForShipmentRequest opens a BARE booking
from it — "the request itself carries the quantities; the instance carries
none". Between initiation and completeUnderContract the booking legitimately
holds no cargo, so the export reported 0 containers for a customer who had
declared, say, 2 x 20FT. 23 bookings on dev data are in that state.
Adds two columns and one filter reading booking_requests.requested_lines:
- "Requested cargo" — the declared lines as text ("2 x 20FT"), handling the
bulk shape too (tons / item count), not only containers.
- "Requested containers" — the declared box count, with a matching min/max
filter on the list and the export.
Deliberately a separate column rather than a fallback inside the real container
count: a declared 2 x 20FT is a request, not two boxes on a booking, and
merging them would overstate operational totals. The two compose instead —
Containers = 0 AND Requested containers >= 1 is exactly the set awaiting
completion after clearance.
requested_lines is free-form jsonb, so the container array is guarded by
jsonb_typeof before jsonb_array_elements; one malformed row would otherwise
500 the whole list.
Container filters on the booking-requests list:
- "Container type" — bookings carrying that type.
- "Containers" — a count of BOXES (booking_container is one row per line with
a quantity, so this sums quantity rather than counting rows), as an exact
value or a range. It reads the container-type filter when one is set, so the
one control answers both "10 containers in total" and "10 forty-footers".
Export gains a column per container type ("20FT containers", "40FT
containers"), plus the total "Containers" column and the two filters. Container
types are reference rows, not a constant, so `ExportDataset` gains an optional
`dynamicFields` resolver — DB-driven columns appended to the static list and
cached for the process, mirroring the existing `ExportFilterDef.optionsQuery`.
Adding a 45ft container type adds its column with no code change. The type id
is interpolated into raw SQL (ExportField.select has no parameter bag), so the
resolver drops any id that is not a uuid.
Also repoints the export's "Container VGM" column at the per-line sum. It was
projecting bookings.cargo_total_weight_vgm, which the portal wizard leaves at 0
for container freight — the same trap the tonnage fix addressed — so the column
read 0 for every portal-created container booking. Non-zero on dev data goes
from 54 to 170 of 208 container bookings.
The booking-requests list could filter by freight type but not by what is
actually in the booking, and the export's only cargo column showed the
commodity name — blank for every container booking, which stores no
commodity at all.
Adds one resolver, `bookingContentSql`, that answers "what did the customer
say is in this booking" per freight type: the container lines they entered
("2 × 40FT, 1 × 20FT") for container freight, since the wizard asks them for
no description; the commodity they picked from the cargo tree for bulk,
falling back to their free-text description.
List filters:
- "Content" — a single select flattening the cargo tree the same way the
booking wizard presents it (group, then each commodity as "Bulk → Wheat").
Picking a GROUP matches its whole subtree via a recursive walk, so "Bulk"
returns all 44 bulk bookings rather than the 0 that carry the group id
itself. This makes the existing, previously unexposed `cargoTypeId` param
group-aware.
- "Content contains" — a contains-search over the description, the commodity
name and the container types, so container bookings are reachable by "40FT"
even though they carry no words of the customer's own.
Both apply through `applyListFilters`, so the list, its summary tiles and its
facets agree, and both are declared on the bookings export dataset — the
export button already forwards the page's filters verbatim.
Export fields: "Content" (default), plus "Cargo description" as its own
column. The old `cargo` column is unchanged and still selectable, relabelled
"Cargo (commodity)"; it loses only its default tick, so saved presets that
name it keep working.
- Added a reason field to train history entries for detach/maintenance actions.
- Updated TrainHistoryPanel to display the reason for wagon detachments.
- Introduced per-wagon load/unload functionality in ScheduleWorkspacePanel with a modal for managing individual wagons.
- Implemented API endpoints for loading and unloading specific wagons, including the ability to cancel remaining wagons with a reason.
- Refactored detach request handling in TrainBuilderDetailPage to streamline the process and remove the approval flow, requiring a reason for detachments.
- Updated types and services to support new wagon loading/unloading features and booking wagon retrieval.
The detail page showed the company's business contact details but not the
credentials anyone actually signs in with, and the two drift apart
routinely — so "the customer says they can't log in" was unanswerable
from this screen.
Adds `GET /backoffice/customers/:companyId/accounts`, joining each
external profile to its IAM account, primary contact first. Deliberately
not filtered to active accounts: a suspended or never-activated login is
exactly the case being looked into. The user query selects columns
explicitly — the entity's relations include credentials and sessions, and
this response reaches a browser.
Rendered as cards rather than a table: it is a handful of rows of
mostly-optional fields, which a table renders as a field of dashes.
"Password never set" is called out on its own, being the usual answer to
"they never got in", and a profile whose IAM user is gone reads as a red
fault rather than an inactive status.
- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage.
- Implemented API endpoints for recording station work and managing wagon detach requests.
- Updated contract templates to include Ethiopian customs handling options.
- Enhanced shipment forms to collect customs clearing agent details for without-customs bookings.
- Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts.
- Improved validation for customs clearing agent information in shipment forms.
- Updated various components and services to accommodate new features and ensure data integrity.
- Implemented read-only locking for customer-requested container sizes and billing currency in the GlCreateBookingForm component.
- Added functionality to lock partner quantities based on shipment requests in the ConsolidationPartnerPanel.
- Introduced a new Leave action in the LogPassYardWorkModal to unassign bookings from trains.
- Enhanced the AuditLogsPage to support filtering by action and added a Go button for direct navigation to entity detail pages.
- Updated WagonCancellationsPage to handle odd-20ft credits requiring partner selection during rebooking.
- Improved TrainScheduleV2DetailPage to allow manual loading of cargo and display warnings for unassigned bookings.
- Added a new reference field to the audit logs for better searchability and tracking of actions.
- Created a migration to add the reference column to the audit logs table and established an index for efficient querying.
- Defined a registry for audit reference sources to streamline the retrieval of human identifiers for various entities.