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.
>
Export bookings whose cargo goes straight from customer truck to wagon
never get a warehouse GRN, so the load gate (assertExportReceivedWithGrn)
rejects them forever unless staff first flip exportHandoverMode on the
booking detail page, then come back and retry Load.
Add a Truck to Train button next to Load in the schedule workspace's
on-train list, shown for EXPORT bookings at the boarding yard. One click
sets exportHandoverMode=DIRECT_TO_TRAIN then loads the booking.
- ReportPage drops its own PageHeader (and the back arrow); ReportView
now optionally renders the header itself (pageHeader prop) with
export/refresh as its actions. Embedded ReportSection usage is
unaffected (keeps the inline toolbar next to filters).
- Replace the two xlsx/pdf icon buttons with one Export button opening
a dialog: format as large icon radio cards, fields as checkboxes
(select-all toggle), record count (default all, capped per format).
Export applies the report's current filters and sort.
- Backend: export route accepts fields (whitelisted against the
report's own columns) and limit; ReportExportService takes an
optional column subset instead of always dumping every column.
- Fixed a real bug found while wiring this up: runAll() ignored the
caller's sortBy/sortOrder and always used the report's default sort,
so exports silently didn't match whatever order was on screen.
- Report daterange filters now use DatePickerInput + the shared
getDateRangePresets() (Today/Last 7 days/This month/...) instead of
two bare DateInputs, matching every other date-range filter in the
app.
- Removed the reports hub grid page. /dashboard/reports now redirects
to the first report the caller has access to, or /dashboard if they
have none.
Replaces every remaining split from/to Mantine DateInput/DatePickerInput pair
with a single DatePickerInput type="range", sharing one preset list (Today,
Last 7/30 days, this/last month, YTD) via getDateRangePresets(). Covers
ListControls (18 consumers), FleetResourcePage, ContractRequestsPage,
BookingRequestsPage (created + scheduled ranges), WagonCancellationsPage,
BatchBoardPage, ClearanceDocumentsPage, ShipmentRequestsPage.
Native Mantine range picker, not the shadcn DateRangePicker, to match each
page's existing design system instead of clashing with it.
Reports date-range filter (ReportFilters.tsx) intentionally left untouched.
ReportDefinition gets an optional chart {type: line|bar, x, y[]} field —
plots the same rows the table gets, no separate query. Frontend adds a
table/chart toggle (defaults to table) using the existing recharts
dependency, no new package.
Chart view fetches up to 100 rows (the API's page-size ceiling) instead
of the table's current page, so it doesn't silently plot a fraction of
the filtered set; shows a truncation note past that cap.
Wired onto 5 reports as proof: wagon-fleet-status, locomotive-fleet-
status, booking-status-breakdown, revenue-summary (bar), and
global-logistics-wagons (line). Everything else stays table-only —
charting is opt-in per report, not a default.
Adds a shadcn Popover+Calendar range picker with presets (Today, Last 7/30
days, this/last month, YTD) and Apply/Cancel. Swaps it into every shadcn-family
from-to date filter: activity log, sector reports, incoming records (EN
branch), CombinedFilterBar (13 consumers), user records, audit log — also
fixes an uncontrolled-input bug on the audit log date fields.
Mantine-based ListControls/ReportFilters left untouched (different UI kit).
Import Load List / Marshalling Document only rendered Seq, Wagon,
Booking, Company, Load, Container numbers, Weight T — missing fields
present on the physical marshaling sheet (wagon type, tare, equated
length, departure/arrival station, seal no) and a blank note column
for yard staff. Export marshalling doc already had most of these;
import doc now matches. Existing columns kept in place, unchanged.
Drop reportConfigs.ts (per-report FE config duplicating the backend) and
the chart-drawing ReportPage. Replace with ReportView: one engine that
renders any report the GET /reports catalog describes — filters, KPI
strip, sortable/paginated DataTable, xlsx/pdf export via blob download.
ReportSection embeds a report inline on any page, scoped by idKey, and
renders nothing if the caller lacks that report's permission.
Sidebar Reports submenu is now built from the live catalog
(sidebar-sections.tsx + App.tsx) instead of a hand-listed key — no FE
edit needed to add or hide a report.
Bulk contract templates are now unique per (cargo type, trade direction,
customs option) instead of (cargo type, customs option). Intercity is
domestic and crosses no border, so it carries no customs variant:
with_customs stays null there, enforced by a check constraint.
Contract resolution already passed the trade direction through but the
bulk lookup dropped it, so one template served all three directions.
Container templates are unchanged — cargo_type_id is null on those rows,
which keeps them out of both the new index and the constraint.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Implemented utility to calculate wagon usage metrics for train schedules.
- Created for sending wagons to maintenance with optional notes.
- Added unit tests for train builder maintenance functionalities, including formatting train run labels and building maintenance notes.
- Developed component for merging train schedules with detailed previews and reasons for merging.
- Introduced component for selecting wagons with search functionality and selection limits.
- Created for displaying and filtering audit logs, including detailed views of individual log entries.
- Added for handling API interactions related to audit logs, including fetching logs and entity types.
They hold a TIN but no business licence, so there is no eTrade record to look
their registration up in. A checkbox on the first wizard step marks them, and
everything that assumed a trade licence bends around it:
- The company step replaces the eTrade lookup with typed registration details
— name, region, zone, woreda, kebele, house number — required exactly because
they are now on screen. applyEtradeSourcedFields skips the lookup rather than
failing it, so what the customer sends is what is stored.
- No freight-forwarder role. Forwarding is licensed work, so the option is not
offered, and the API refuses it at start-onboarding and at every later
role-add rather than letting approval fail on a document they cannot produce.
- No per-role business-licence upload, client-side or in the completion gate.
- Their own document set (company_onboarding_documents_cooperative) merges on
top of the nationality one, admin-managed like every other set. Nationality
wins a fileKey collision so no slot renders twice, and the DARS paper is not
injected into it — the set it merges onto already carries one.
- The owner is typed in full; with no eTrade manager on file the licence
comparison reports "nothing to compare against", which backoffice now
explains rather than leaving as a bare dash.
Stored as an attributes flag, not a column: everything it changes is
behavioural, and nothing queries or joins on it.
- Replace the general-manager rows with owner rows across the customer
detail, contract cards, types and service mappings.
- The identity card names its subject ("Verifies for this company") and shows
whether the asserted owner matches the manager on the eTrade licence:
amber when it does not, green when it does, dimmed when eTrade named
nobody. Advisory only, since the comparison is a fuzzy transliteration
match.
- A missing delegation letter now keys on poaDeclared === "yes".
- Keep the three general-manager labels in the change-request label map so
historical requests still render readable field names.
feat(train-builder): update permissions checks for train actions in UI
feat(contracts): enhance contract view and shipment request pages with new features
test(shipment-preview): add tests for customs clearing logic in booking preview
style(contract-sign-bar): create CSS for fixed sign bar layout
End-to-end testing of the direct truck-to-train flow surfaced two defects
in the carriage acceptance sheet's no-wagon fallback. Container numbers
were read from freight.containers, which only gains rows at allocation,
so a direct booking's declared containers never appeared; they are read
from booking_container_units now, seal numbers included. The weight used
bulkTotalWeightTons alone, which is null for CONTAINER and PER_TON bulk
bookings, printing 0.00 — cargoTotalWeightVgm is the real weight there
and only holds an item count for PER_ITEM break-bulk.
Separately, warehouse cargo can be loaded from the Load-to-Train queue or
from the schedule, but loading from the schedule never advanced
warehouse_inventory. The booking went IN_TRANSIT while its cargo still
read as sitting in the warehouse — the same inconsistency that produced
the spurious dispatch block. loadBooking now moves the inventory to
LOADED in the same transaction, and no-ops for direct bookings.