mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(reports): header actions, single export dialog, date-range presets
- 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.
This commit is contained in:
@@ -36,6 +36,7 @@ export class ReportExportService {
|
||||
def: ReportDefinition,
|
||||
rows: Record<string, unknown>[],
|
||||
kpis: ReportKpi[],
|
||||
columns: ReportColumn[] = def.columns,
|
||||
): Promise<Buffer> {
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
const sheet = workbook.addWorksheet(def.title.slice(0, 31));
|
||||
@@ -45,14 +46,14 @@ export class ReportExportService {
|
||||
sheet.addRow([]);
|
||||
}
|
||||
|
||||
const headerRow = sheet.addRow(def.columns.map((c) => c.label));
|
||||
const headerRow = sheet.addRow(columns.map((c) => c.label));
|
||||
headerRow.font = { bold: true };
|
||||
|
||||
for (const row of rows) {
|
||||
sheet.addRow(def.columns.map((c) => row[c.key] ?? null));
|
||||
sheet.addRow(columns.map((c) => row[c.key] ?? null));
|
||||
}
|
||||
|
||||
def.columns.forEach((col, i) => {
|
||||
columns.forEach((col, i) => {
|
||||
const format = NUMBER_FORMAT[col.type];
|
||||
const excelCol = sheet.getColumn(i + 1);
|
||||
excelCol.width = Math.max(col.label.length + 2, 12);
|
||||
@@ -67,8 +68,9 @@ export class ReportExportService {
|
||||
def: ReportDefinition,
|
||||
rows: Record<string, unknown>[],
|
||||
kpis: ReportKpi[],
|
||||
columns: ReportColumn[] = def.columns,
|
||||
): Promise<Buffer> {
|
||||
const html = this.buildHtml(def, rows, kpis);
|
||||
const html = this.buildHtml(def, rows, kpis, columns);
|
||||
return this.pdfRender.htmlToPdfBuffer(html, { label: `report:${def.key}`, landscape: true });
|
||||
}
|
||||
|
||||
@@ -76,6 +78,7 @@ export class ReportExportService {
|
||||
def: ReportDefinition,
|
||||
rows: Record<string, unknown>[],
|
||||
kpis: ReportKpi[],
|
||||
columns: ReportColumn[],
|
||||
): string {
|
||||
const esc = (v: unknown) =>
|
||||
String(v ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
@@ -89,11 +92,11 @@ export class ReportExportService {
|
||||
.join('')}</div>`
|
||||
: '';
|
||||
|
||||
const head = def.columns.map((c) => `<th>${esc(c.label)}</th>`).join('');
|
||||
const head = columns.map((c) => `<th>${esc(c.label)}</th>`).join('');
|
||||
const body = rows
|
||||
.map(
|
||||
(row) =>
|
||||
`<tr>${def.columns.map((c) => `<td>${esc(formatCell(row[c.key], c.type))}</td>`).join('')}</tr>`,
|
||||
`<tr>${columns.map((c) => `<td>${esc(formatCell(row[c.key], c.type))}</td>`).join('')}</tr>`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
|
||||
@@ -132,7 +132,9 @@ export class ReportRunnerService {
|
||||
const params = coerceParams(def, raw);
|
||||
const ctx = { ds: this.ds, params, directions };
|
||||
const qb = def.query(ctx);
|
||||
const sort = resolveSort(def, undefined, undefined);
|
||||
// Same sort the on-screen table is using, not always the default — an
|
||||
// export is supposed to match what the user is looking at.
|
||||
const sort = resolveSort(def, raw.sortBy, raw.sortOrder);
|
||||
if (sort) qb.orderBy(sort.expr, sort.dir);
|
||||
const items = await qb.limit(limit).getRawMany();
|
||||
if (items.length >= limit) {
|
||||
|
||||
@@ -53,20 +53,30 @@ export class ReportsController {
|
||||
@ApiOperation({ summary: 'Export a report to xlsx or pdf' })
|
||||
async export(
|
||||
@Param('key') key: string,
|
||||
@Query() query: RawReportQuery & { format?: string },
|
||||
@Query() query: RawReportQuery & { format?: string; fields?: string; limit?: string },
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
@Res() res: Response,
|
||||
): Promise<void> {
|
||||
const def = this.resolve(key, user);
|
||||
const directions = await this.userTradeAccessService.resolveAllowedDirections(user);
|
||||
const format = query.format === 'pdf' ? 'pdf' : 'xlsx';
|
||||
const cap = format === 'pdf' ? PDF_ROW_CAP : XLSX_ROW_CAP;
|
||||
const formatCap = format === 'pdf' ? PDF_ROW_CAP : XLSX_ROW_CAP;
|
||||
const requestedLimit = Number(query.limit);
|
||||
const cap = requestedLimit > 0 ? Math.min(requestedLimit, formatCap) : formatCap;
|
||||
|
||||
// Whitelist against the report's own columns — an unknown/empty `fields`
|
||||
// value falls back to every column rather than shipping a blank sheet.
|
||||
const requestedFields = query.fields?.split(',').filter(Boolean);
|
||||
const columns = requestedFields?.length
|
||||
? def.columns.filter((c) => requestedFields.includes(c.key))
|
||||
: def.columns;
|
||||
const exportColumns = columns.length ? columns : def.columns;
|
||||
|
||||
const { items, kpis } = await this.runner.runAll(def, query, directions, cap);
|
||||
const buffer =
|
||||
format === 'pdf'
|
||||
? await this.exportService.toPdf(def, items, kpis)
|
||||
: await this.exportService.toXlsx(def, items, kpis);
|
||||
? await this.exportService.toPdf(def, items, kpis, exportColumns)
|
||||
: await this.exportService.toXlsx(def, items, kpis, exportColumns);
|
||||
|
||||
const filename = `${def.key}.${format === 'pdf' ? 'pdf' : 'xlsx'}`;
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
|
||||
Reference in New Issue
Block a user