feat: botón Generar PDF en los reportes HTML (Chrome sin pantalla, papel y márgenes de la plantilla fijos)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-20 16:53:24 -05:00
1 parent 2d358f0cf1
commit f71577e792
13 files changed
+287 -19

No files matched your search

@@ -7,6 +7,7 @@ import { PermissionsService } from '../permissions/permissions.service.js';
import { SapDbService } from '../sapdb/sapdb.service.js';
import { HTML_REPORTS } from './html-reports.js';
import { createRenderer } from './html-render.js';
import { HtmlPdfService } from './html-pdf.service.js';
import type { HtmlReportDef, HtmlReportInfo, HtmlReportValues } from './html-reports.types.js';
/** Ruta de menú que permite ejecutar los reportes HTML. */
@@ -55,6 +56,7 @@ export class HtmlReportsService {
private readonly permissions: PermissionsService,
private readonly audit: AuditService,
private readonly sapdb: SapDbService,
private readonly pdfService: HtmlPdfService,
) {}
private async assertCanRun(user: AuthUser): Promise<void> {
@@ -70,21 +72,30 @@ export class HtmlReportsService {
/** El HTML del reporte con los valores indicados. */
async render(user: AuthUser, code: string, raw: Readonly<Record<string, unknown>>, ip: string): Promise<string> {
return this.run(user, code, raw, ip, 'html', (html) => html);
}
/** El PDF del reporte: el mismo HTML pasado por Chrome sin pantalla, con el papel y los márgenes de la plantilla. */
async pdf(user: AuthUser, code: string, raw: Readonly<Record<string, unknown>>, ip: string): Promise<Buffer> {
return this.run(user, code, raw, ip, 'pdf', (html) => this.pdfService.toPdf(html));
}
private async run<T>(user: AuthUser, code: string, raw: Readonly<Record<string, unknown>>, ip: string, format: 'html' | 'pdf', finish: (html: string) => T | Promise<T>): Promise<T> {
await this.assertCanRun(user);
const def = HTML_REPORTS.get(code);
if (!def) throw new NotFoundException(`No existe el reporte ${code}`);
const values = parseHtmlValues(def, raw);
// solo los nombres de los parámetros: los valores pueden ser datos de clientes
const detail = { params: def.params.map((p) => p.name) };
const detail = { format, params: def.params.map((p) => p.name) };
try {
const data = await def.data(values, { query: async (name, params) => (await this.sapdb.run(user, name, params ?? {}, ip)).rows });
this.renderer ??= createRenderer(this.config.get('HTML_REPORTS_DIR'));
const html = this.renderer.render(def.template, { ...data, params: values });
const out = await finish(this.renderer.render(def.template, { ...data, params: values }));
await this.audit.record({ action: 'html_report', webUser: user.webUser, ip, resource: code, detail });
return html;
return out;
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
this.logger.error(`Reporte HTML ${code} falló: ${message}`);
this.logger.error(`Reporte HTML ${code} (${format}) falló: ${message}`);
await this.audit.record({ action: 'html_report', webUser: user.webUser, ip, resource: code, outcome: 'error', detail: { ...detail, error: message.slice(0, 300) } });
throw e;
}