chore: more reporting

This commit is contained in:
Nathnael
2026-08-19 10:14:46 +00:00
parent 54f52f077c
commit e964a9b8f4
20 changed files with 1707 additions and 25 deletions

View File

@@ -1,5 +1,7 @@
import { Controller, Get, NotFoundException, Param, Query, Res } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { CurrentUser } from '@edr/api-common';
import type { Response } from 'express';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
@@ -12,13 +14,44 @@ import { ReportExportService } from './report-export.service';
import { resolveExportCap, resolveExportColumns, resolveExportFormat } from './report-export-request.util';
import { RawReportQuery, ReportRunnerService } from './report-runner.service';
import { REPORTS, getReport } from './report.registry';
import { ReportCatalogEntry, ReportDefinition } from './report.types';
import { ReportCatalogEntry, ReportDefinition, ReportFilterOption } from './report.types';
const toCatalogEntry = (def: ReportDefinition): ReportCatalogEntry => {
const { query: _query, summary, ...meta } = def;
return { ...meta, hasSummary: Boolean(summary) };
};
/**
* Filters whose choices are reference data resolve their options here rather
* than declaring them inline, so the catalog the frontend receives looks the
* same either way. Cached for the process lifetime — these are small, rarely
* changing lists (23 stations, 18 cargo types), and the catalog is hit on
* every page load.
*/
const optionsCache = new Map<string, ReportFilterOption[]>();
async function resolveFilterOptions(
def: ReportCatalogEntry,
ds: DataSource,
): Promise<ReportCatalogEntry> {
if (!def.filters.some((f) => f.optionsQuery)) return def;
const filters = await Promise.all(
def.filters.map(async (filter) => {
if (!filter.optionsQuery) return filter;
let options = optionsCache.get(filter.key);
if (!options) {
options = await filter.optionsQuery(ds);
optionsCache.set(filter.key, options);
}
// Drop the resolver itself — it is a function and would not serialise.
const { optionsQuery: _resolver, ...rest } = filter;
return { ...rest, options };
}),
);
return { ...def, filters };
}
@ApiTags('Reports')
@ApiBearerAuth()
@Controller('reports')
@@ -28,14 +61,16 @@ export class ReportsController {
private readonly runner: ReportRunnerService,
private readonly exportService: ReportExportService,
private readonly userTradeAccessService: UserTradeAccessService,
@InjectDataSource() private readonly dataSource: DataSource,
) {}
@Get()
@ApiOperation({ summary: 'List reports the caller has permission to run' })
async catalog(@CurrentUser() user: TCurrentUser): Promise<ReportCatalogEntry[]> {
return REPORTS.filter((def) => hasFreightPermission(user, reportPermissionKey(def.key))).map(
toCatalogEntry,
);
const allowed = REPORTS.filter((def) =>
hasFreightPermission(user, reportPermissionKey(def.key)),
).map(toCatalogEntry);
return Promise.all(allowed.map((def) => resolveFilterOptions(def, this.dataSource)));
}
@Get(':key')