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, ) { 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, @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, @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, @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, ) { 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, ) { return this.reports.run(slug, body); } }