feat(reports): reports module + /inicio + edo-cuenta-datos prefill
- 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.
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user