mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(reports): add optional chart view to the report engine
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.
This commit is contained in:
@@ -77,6 +77,7 @@ export const bookingStatusBreakdownReport: ReportDefinition = {
|
||||
{ key: 'amount', label: 'Amount', type: 'money', sortable: true },
|
||||
],
|
||||
defaultSort: { key: 'bookings', dir: 'DESC' },
|
||||
chart: { type: 'bar', x: 'status', y: ['bookings'] },
|
||||
query(ctx) {
|
||||
return baseQuery(ctx)
|
||||
.select('b.status', 'status')
|
||||
|
||||
@@ -46,6 +46,7 @@ export const globalLogisticsWagonsReport: ReportDefinition = {
|
||||
{ key: 'cancelled', label: 'Cancelled', type: 'number', sortable: true },
|
||||
],
|
||||
defaultSort: { key: 'date', dir: 'DESC' },
|
||||
chart: { type: 'line', x: 'date', y: ['allocated', 'cancelled'] },
|
||||
query(ctx) {
|
||||
return baseQuery(ctx)
|
||||
.select(`to_char(date_trunc('day', l.occurred_at), 'YYYY-MM-DD')`, 'date')
|
||||
|
||||
@@ -32,6 +32,7 @@ export const locomotiveFleetStatusReport: ReportDefinition = {
|
||||
{ key: 'count', label: 'Count', type: 'number', sortable: true },
|
||||
],
|
||||
defaultSort: { key: 'count', dir: 'DESC' },
|
||||
chart: { type: 'bar', x: 'status', y: ['count'] },
|
||||
query(ctx) {
|
||||
return baseQuery(ctx)
|
||||
.select('l.locomotive_type', 'locomotiveType')
|
||||
|
||||
@@ -37,6 +37,7 @@ export const revenueSummaryReport: ReportDefinition = {
|
||||
{ key: 'revenue', label: 'Revenue', type: 'money', sortable: true },
|
||||
],
|
||||
defaultSort: { key: 'revenue', dir: 'DESC' },
|
||||
chart: { type: 'bar', x: 'direction', y: ['revenue'] },
|
||||
query(ctx) {
|
||||
return baseQuery(ctx)
|
||||
.select('b.trade_direction', 'direction')
|
||||
|
||||
@@ -38,6 +38,7 @@ export const wagonFleetStatusReport: ReportDefinition = {
|
||||
{ key: 'count', label: 'Count', type: 'number', sortable: true },
|
||||
],
|
||||
defaultSort: { key: 'count', dir: 'DESC' },
|
||||
chart: { type: 'bar', x: 'status', y: ['count'] },
|
||||
query(ctx) {
|
||||
return baseQuery(ctx)
|
||||
.select('COALESCE(wt.name, \'Unknown\')', 'wagonType')
|
||||
|
||||
@@ -38,4 +38,15 @@ describe('REPORTS', () => {
|
||||
expect(def.filters.some((f) => f.key === def.idKey!.key)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('chart.x and chart.y, when declared, point at real column keys', () => {
|
||||
for (const def of REPORTS) {
|
||||
if (!def.chart) continue;
|
||||
const columnKeys = new Set(def.columns.map((c) => c.key));
|
||||
expect(columnKeys.has(def.chart.x)).toBe(true);
|
||||
for (const y of def.chart.y) {
|
||||
expect(columnKeys.has(y)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,21 @@ export interface ReportKpi {
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export type ReportChartType = 'line' | 'bar';
|
||||
|
||||
/**
|
||||
* Plots the SAME rows the table gets — no separate query. `x` and `y` are
|
||||
* column keys from `columns`. A report whose group-by has dimensions beyond
|
||||
* `x` will render one mark per row (e.g. two rows sharing a date because they
|
||||
* differ by direction), which is a busier chart, not a wrong one. Pivoting
|
||||
* rows into one-per-x series is a later add if a report actually needs it.
|
||||
*/
|
||||
export interface ReportChartDef {
|
||||
type: ReportChartType;
|
||||
x: string;
|
||||
y: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Optional entity scope a report can be embedded against — e.g. a
|
||||
* contract-utilization report shown on a single contract's detail page.
|
||||
@@ -73,6 +88,8 @@ export interface ReportDefinition {
|
||||
query(ctx: ReportContext): SelectQueryBuilder<ObjectLiteral>;
|
||||
/** KPIs over the same filtered set; shown above the table and in exports. */
|
||||
summary?(ctx: ReportContext): Promise<ReportKpi[]>;
|
||||
/** Optional chart view of the same rows. Table remains the default view. */
|
||||
chart?: ReportChartDef;
|
||||
}
|
||||
|
||||
/** Catalog shape served by GET /reports — metadata only, no rows. */
|
||||
|
||||
Reference in New Issue
Block a user