- New reports backend (registry, service, controller, outputs, types) with catalog endpoint + slug/CSV/XLSX/PDF/print outputs. - /reportes catalog + /reportes/[slug] runner; ReportRunner + ContextReports components wire pre-filtered links from domain pages. - Fix: /reportes/[slug] now reads searchParams and forwards initialParams to ReportRunner so /reportes/edo-cuenta-datos?customerId=... auto-runs instead of dropping the id and forcing a manual customer search. - /inicio landing page; root + login redirect to /inicio. - Company header env vars + logo asset for PDF/print rendering. - exceljs + pdfkit deps.
131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
import {
|
||
Body,
|
||
Controller,
|
||
Get,
|
||
Header,
|
||
Param,
|
||
Post,
|
||
Query,
|
||
Res,
|
||
UseGuards,
|
||
} from "@nestjs/common";
|
||
import type { Response } from "express";
|
||
import { AuthenticatedGuard } from "../auth/authenticated.guard";
|
||
import { ReportsService } from "./reports.service";
|
||
import {
|
||
renderCsv,
|
||
renderPdf,
|
||
renderPrintHtml,
|
||
renderXlsx,
|
||
} from "./outputs";
|
||
import { findReport } from "./reports.registry";
|
||
|
||
/**
|
||
* Reports routes. Every report is dispatched by slug; outputs are
|
||
* differentiated by `?format=...` (default `json`). Reads only — gated
|
||
* by AuthenticatedGuard alone, like every other read in the app.
|
||
*/
|
||
@UseGuards(AuthenticatedGuard)
|
||
@Controller("reports")
|
||
export class ReportsController {
|
||
constructor(private readonly reports: ReportsService) {}
|
||
|
||
/** Catalog of all registered reports (the /reportes index). */
|
||
@Get()
|
||
catalog() {
|
||
return { items: this.reports.catalog() };
|
||
}
|
||
|
||
/** Run a report and return the JSON result (rows + totals + the def's columns). */
|
||
@Get(":slug")
|
||
async runJson(
|
||
@Param("slug") slug: string,
|
||
@Query() query: Record<string, string | undefined>,
|
||
) {
|
||
const def = findReport(slug);
|
||
const result = await this.reports.run(slug, query);
|
||
return { ...result, columns: def?.columns ?? [] };
|
||
}
|
||
|
||
/** CSV download. */
|
||
@Get(":slug/csv")
|
||
@Header("Content-Type", "text/csv; charset=utf-8")
|
||
async runCsv(
|
||
@Param("slug") slug: string,
|
||
@Query() query: Record<string, string | undefined>,
|
||
@Res() res: Response,
|
||
) {
|
||
const def = findReport(slug);
|
||
const result = await this.reports.run(slug, query);
|
||
const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.csv`;
|
||
res.setHeader(
|
||
"Content-Disposition",
|
||
`attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`,
|
||
);
|
||
res.send(renderCsv(def?.columns ?? [], result));
|
||
}
|
||
|
||
/** XLSX download. */
|
||
@Get(":slug/xlsx")
|
||
async runXlsx(
|
||
@Param("slug") slug: string,
|
||
@Query() query: Record<string, string | undefined>,
|
||
@Res() res: Response,
|
||
) {
|
||
const def = findReport(slug);
|
||
const result = await this.reports.run(slug, query);
|
||
const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.xlsx`;
|
||
const buf = await renderXlsx(def?.columns ?? [], result);
|
||
res.setHeader(
|
||
"Content-Type",
|
||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
);
|
||
res.setHeader(
|
||
"Content-Disposition",
|
||
`attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`,
|
||
);
|
||
res.send(buf);
|
||
}
|
||
|
||
/** PDF download. */
|
||
@Get(":slug/pdf")
|
||
async runPdf(
|
||
@Param("slug") slug: string,
|
||
@Query() query: Record<string, string | undefined>,
|
||
@Res() res: Response,
|
||
) {
|
||
const def = findReport(slug);
|
||
const result = await this.reports.run(slug, query);
|
||
const buf = await renderPdf(def?.columns ?? [], result, def?.title ?? slug);
|
||
const filename = `${def?.title ?? slug}-${new Date().toISOString().slice(0, 10)}.pdf`;
|
||
res.setHeader("Content-Type", "application/pdf");
|
||
res.setHeader(
|
||
"Content-Disposition",
|
||
`attachment; filename="${filename.replace(/[^\wÀ-ſ .-]/g, "_")}"`,
|
||
);
|
||
res.send(buf);
|
||
}
|
||
|
||
/** Browser-printable HTML view (the user hits Print → Save as PDF). */
|
||
@Get(":slug/print")
|
||
@Header("Content-Type", "text/html; charset=utf-8")
|
||
async runPrint(
|
||
@Param("slug") slug: string,
|
||
@Query() query: Record<string, string | undefined>,
|
||
) {
|
||
const def = findReport(slug);
|
||
const result = await this.reports.run(slug, query);
|
||
return renderPrintHtml(def?.columns ?? [], result, def?.title ?? slug);
|
||
}
|
||
|
||
/** POST a customer-picker-driven report (statement). Mirrors GET to keep
|
||
* the param contract simple: same body shape, same response. */
|
||
@Post(":slug")
|
||
async runPost(
|
||
@Param("slug") slug: string,
|
||
@Body() body: Record<string, string | undefined>,
|
||
) {
|
||
return this.reports.run(slug, body);
|
||
}
|
||
}
|