Extracted the export route's format/cap/column-whitelist branching out
of the controller into pure functions (resolveExportFormat,
resolveExportCap, resolveExportColumns) and added a spec: unknown
format falls back to xlsx, limit clamps to the format cap and ignores
non-positive/NaN input, unknown field keys are dropped and an
all-unknown fields list falls back to every column instead of
shipping a blank sheet. Was untested branching logic before this.
- 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.
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.
customer-status (company-profile roles, not Company — importer/
exporter/forwarder lives there), contract-lifecycle, customs-documents
(clearance milestones), invoicing-pipeline, first-last-mile-bookings
(one resolver, UNION ALL over first_mile/last_mile — verified the
raw-string .from() subquery against the live query builder, not just
hand-written SQL, after the join-alias bug earlier this branch),
invoices-by-status, payments-by-status, revenue-summary, cargo-summary.
payments carries no deleted_at column despite extending BaseEntity —
caught by column-checking against the live DB before shipping, dropped
the soft-delete filter for that one query.
Completes the ITLMS dashboard spec's 20-resolver dedup list (19 built,
freight-weight-variance dropped — no charged-vs-actual weight
distinction in the schema).
booking-status-breakdown (dedupes the same 'status per port/train/
cargo/contract' ask across 4 dashboards), train-schedule-status,
train-turnaround, wagon-teu-utilization, loaded-capacity,
global-logistics-wagons.
Dropped freight-weight-variance from this batch: the schema has no
'charged weight' distinct from VGM/actual, so a charged-vs-actual
variance report isn't buildable without a product decision on what
'charged' means here.
wagon-fleet-status, wagon-status-duration, wagon-requests,
locomotive-fleet-status. First batch off the ITLMS dashboard spec —
fleet data (wagons/locomotives/transfer-requests) needed no schema
work, just resolvers. No frontend changes: catalog is server-driven.
Sorting by a column with no explicit sortExpr fell back to the bare
select alias unquoted. Postgres folds unquoted identifiers to lowercase,
so any camelCase alias (utilizationPct, bookedTons) 42703'd. Quote the
fallback to match the case TypeORM's addSelect actually emitted.
Nuke the 17 hand-written raw-SQL reports (no pagination, hard LIMITs) and
the reports module built around them. Replace with a resolver contract:
a report declares columns/filters/permission and a TypeORM QueryBuilder;
ReportRunnerService applies filtering, a whitelisted sort, offset/limit
paging, and a COUNT(*) FROM (query) wrapper for the total (getCount() is
wrong for GROUP BY). ReportExportService re-runs the same resolver
unpaginated for xlsx (exceljs) and pdf (existing PdfRenderService, now
landscape-capable) exports.
Ships with 4 reports: bookings-list, revenue-by-customer,
aging-receivables, contract-utilization. Catalog + per-report permission
checks live in the controller; adding a report is one new definitions/
file plus a REPORT_KEYS entry, no frontend change.
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.