The customers:* keys were seeded and present in the backoffice constants but
enforced nowhere except reset-password. Customer CRUD sat behind the coarse
edr_freight_app:admin umbrella, and every company read endpoint was unguarded.
Two routes could not be gated on the route alone, because the authority they
need depends on the request BODY, not the path:
- PATCH /companies/:id carries `status` (UpdateCompanyDto extends
PartialType(CreateCompanyDto)), so it both edits fields and blacklists.
- PATCH /company-profiles/:profileId/status is approve, reject, suspend and
blacklist on one route.
Both now take a one-of route guard and assert per-status against a shared
STATUS_PERM map: approving/reactivating needs customers:verify, suspending or
blacklisting needs customers:deactivate. PATCH /companies/:id additionally
requires customers:update when any non-status field is present, so a caller
holding only deactivate cannot rename a company. The backoffice mirrors the
same map so no button is offered that the server would reject.
GET /companies/:companyId/documents is left authenticated-only with the split
in the handler: it is dual-audience. The portal reads its own documents during
onboarding, and the contract-request detail page (gated on contracts:view)
reads the applicant's. Gating it on customers:view alone would have 403'd
customers on their own documents and blanked the contract reviewer's panel.
The two by-company customer-view reads take a one-of guard for the same reason
— otherwise a staffer granted only customers:view gets a detail page whose tabs
403 individually.
Frontend: the customers routes were sidebar-filtered but not wrapped in
RequirePermission, so direct URL navigation rendered them for anyone.
Verified: freight-api type-check clean; backoffice type-check unchanged from
HEAD (pre-existing errors only); 25 tests pass across the companies and
freight-permission suites. Not exercised against a running API.
Co-Authored-By: Claude Opus 4.8 <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>
- 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
- 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.
Plate, power-plate and trailer accepted any free text — a vehicle could be
saved with a plate of "assadasd". They must be letters, a hyphen, then digits,
like ET-9875 or AA-8642.
The server now enforces it on CreateVehicleDto (and UpdateVehicleDto via
PartialType): each plate is trimmed and upper-cased, then matched against
^[A-Z]{2,3}-\d{2,6}$, so "et-9875" is accepted and stored as ET-9875 while an
empty optional trailer/power plate still passes.
The fleet form gains the same check inline: FleetFormFieldDef takes an optional
pattern, the dialog tests it on submit against the upper-cased value, and the
vehicle config points plate and trailer at a regex that mirrors the server's.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The four headline cards — total vehicles, drivers, fuel spend, maintenance —
were static numbers with no way through to the list behind them. Each now takes
an optional href and, when set, wraps in a link to its detail page (vehicles,
drivers, fuel purchases, maintenance). A card without an href stays exactly as
before. The Card is wrapped rather than turned into a link so Mantine's Card
typing stays clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marketing asked for approval requests to surface in order instead of the
alphabetical default. New sortBy=review tiers the list by what needs
action - submitted applications awaiting first approval, then approved
customers with a pending change request, then everyone else (drafts
included) - newest first within each tier. Exposed as the backoffice
"Needs review first" sort option and made the default on both ends.
EDRFREIGHT-232
- Introduced new yard distances resource with CRUD operations.
- Created migration for yard distances table with necessary constraints.
- Implemented service and repository for yard distances handling.
- Added controller for API endpoints to manage yard distances.
- Updated rule engine configuration to include yard distances.
- Enhanced rule engine resource page to support yard distance selection.
- Updated contracts and train builder pages to handle new yard distance logic.
- Added error handling utility for better error message extraction.
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>
Two review-workflow gaps for freight customer onboarding:
Request for change per document. Backoffice can now flag a single uploaded
document (company document, profile licence, or POA delegation letter) with a
note the customer sees, instead of rejecting the whole role over it. Adds
review_status/review_note/reviewed_by/reviewed_at to freight.files (migration
AddFileReviewStatus, partial index for the gate), a POST
documents/:fileId/request-change endpoint, the backoffice action + modal, and a
portal banner/badge so the customer knows what to re-upload. Re-uploading clears
the flag. Approving a role is blocked while any of its documents has an open
correction; the gate check and the status write share a pessimistic write lock
on the company row (as does the change-request write) so a correction can never
slip in between the check and the profile going Active.
Resubmission is visible to reviewers. When a customer resubmits a rejected role
or amends a change request, backoffice staff are notified (allBackoffice inbox
item, deep-linked to the customer) and the resubmission surfaces in a new
"Pending changes" list view + KPI, since such companies are status = active and
never matched the pending-approval filter.
Multi-truck self-haul had no list anywhere on the warehouse side. The ops
dashboard counted trucks on site and offered no way to open the list, and
the inventory table showed a blank plate on exactly the bookings that have
several trucks: it read booking.customer_truck_plate_number, which
multi-truck self-haul leaves null because plates live in
customer_truck_assignments. Booking BK-2026-000033 has a truck and a driver
on file and displayed neither.
Adds a Trucks on Site page listing every truck that has arrived and not yet
departed, across bookings, with plate, driver, booking, customer, containers
and dwell time. It covers both haulage paths because the gate does — a
customer's own truck and an EDR last-mile truck reach the same barrier — and
flags anything sitting over four hours. It lives under Warehouse Management
rather than Imports or Exports, since the yard is not per-direction.
The inventory queries now read plates and drivers from the assignments and
keep the booking columns as the fallback for single-truck bookings written
before that table existed.
Both new statements were EXPLAIN-validated against the live schema; the
plate fix returns the data that was previously null.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>