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:
1 parent
2d358f0cf1
commit
f71577e792
13 files changed
+287
-19
No files matched your search
@@ -13,6 +13,11 @@ export class HtmlReportsApi {
|
||||
return firstValueFrom(this.http.get<GatewayReport[]>(`${API_URL}/html-reports`));
|
||||
}
|
||||
|
||||
/** El PDF del reporte (lo genera el servidor con el papel y los márgenes de la plantilla). */
|
||||
pdf(code: string, values: GatewayValues): Promise<Blob> {
|
||||
return firstValueFrom(this.http.post(`${API_URL}/html-reports/${encodeURIComponent(code)}/pdf`, { values }, { responseType: 'blob' }));
|
||||
}
|
||||
|
||||
/** El HTML del reporte con los valores escritos. */
|
||||
async render(code: string, values: GatewayValues): Promise<string> {
|
||||
try {
|
||||
|
||||
@@ -60,11 +60,11 @@ describe('Reportes HTML', () => {
|
||||
await tick(fixture);
|
||||
const el = fixture.nativeElement as HTMLElement;
|
||||
expect(el.textContent).toContain('Cantidad de etiquetas');
|
||||
expect(el.querySelector('button.main')?.textContent).toContain('Generar');
|
||||
expect(el.querySelector('button.main')?.textContent).not.toContain('PDF');
|
||||
expect(el.querySelector('button.main')?.textContent).toContain('Generar PDF');
|
||||
const htmlButton = () => [...el.querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Ver como HTML') as HTMLButtonElement;
|
||||
|
||||
// falta un obligatorio: no se genera
|
||||
(el.querySelector('button.main') as HTMLButtonElement).click();
|
||||
htmlButton().click();
|
||||
await tick(fixture);
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
expect(el.querySelector('.status')?.textContent).toContain('Falta indicar: Cantidad de etiquetas');
|
||||
@@ -73,7 +73,7 @@ describe('Reportes HTML', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fixture.componentInstance as any).set(params[0], '6');
|
||||
await tick(fixture);
|
||||
(el.querySelector('button.main') as HTMLButtonElement).click();
|
||||
htmlButton().click();
|
||||
await tick(fixture);
|
||||
expect(render).toHaveBeenCalledWith('etiquetas-bultos', { cantidad: '6', papel: 'Carta' });
|
||||
expect(gateway.export).not.toHaveBeenCalled();
|
||||
@@ -89,13 +89,34 @@ describe('Reportes HTML', () => {
|
||||
expect(print).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('«Generar PDF» pide el PDF al servidor y lo muestra con su descarga', async () => {
|
||||
const pdf = vi.fn(async () => new Blob(['%PDF'], { type: 'application/pdf' }));
|
||||
const render = vi.fn();
|
||||
URL.createObjectURL = vi.fn(() => 'blob:x');
|
||||
URL.revokeObjectURL = vi.fn();
|
||||
setup({ reports: vi.fn(async () => [REPORT]), pdf, render }, 'etiquetas-bultos');
|
||||
const fixture = TestBed.createComponent(ReportRunnerPage);
|
||||
await tick(fixture);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fixture.componentInstance as any).set(params[0], '6');
|
||||
const el = fixture.nativeElement as HTMLElement;
|
||||
(el.querySelector('button.main') as HTMLButtonElement).click();
|
||||
await tick(fixture);
|
||||
expect(pdf).toHaveBeenCalledWith('etiquetas-bultos', { cantidad: '6', papel: 'Carta' });
|
||||
expect(render).not.toHaveBeenCalled();
|
||||
expect(el.querySelector('iframe[title="Vista del PDF"]')).not.toBeNull();
|
||||
expect(el.querySelector('a[download]')?.getAttribute('download')).toBe('etiquetas-bultos.pdf');
|
||||
// la vista HTML y el PDF no conviven: al pedir uno desaparece el otro
|
||||
expect(el.querySelector('iframe[title="Vista del reporte"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('un error del servidor se muestra y no deja un marco vacío', async () => {
|
||||
setup({ reports: vi.fn(async () => [REPORT]), render: vi.fn(async () => { throw new Error('boom'); }) }, 'etiquetas-bultos');
|
||||
const fixture = TestBed.createComponent(ReportRunnerPage);
|
||||
await tick(fixture);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(fixture.componentInstance as any).set(params[0], '2');
|
||||
(fixture.nativeElement as HTMLElement).querySelector<HTMLButtonElement>('button.main')!.click();
|
||||
[...(fixture.nativeElement as HTMLElement).querySelectorAll('button')].find((b) => b.textContent?.trim() === 'Ver como HTML')!.click();
|
||||
await tick(fixture);
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('iframe')).toBeNull();
|
||||
expect((fixture.nativeElement as HTMLElement).querySelector('.status')?.textContent).toMatch(/error|boom/i);
|
||||
|
||||
@@ -48,7 +48,10 @@ type Value = string | readonly string[];
|
||||
<iframe #htmlFrame class="pdf" title="Vista del reporte" sandbox="allow-same-origin allow-modals" [srcdoc]="doc"></iframe>
|
||||
}
|
||||
}
|
||||
<button sboActions class="btn main" type="button" [disabled]="running() || !report()" (click)="run()">{{ running() ? 'Generando…' : action }}</button>
|
||||
<button sboActions class="btn main" type="button" [disabled]="running() || !report()" (click)="run('pdf')">{{ running() ? 'Generando…' : action }}</button>
|
||||
@if (isHtml) {
|
||||
<button sboActions class="btn" type="button" [disabled]="running() || !report()" (click)="run('html')">Ver como HTML</button>
|
||||
}
|
||||
@if (htmlDoc()) {
|
||||
<button sboActions class="btn" type="button" (click)="print()">Imprimir</button>
|
||||
}
|
||||
@@ -69,8 +72,8 @@ export class ReportRunnerPage {
|
||||
private readonly api = inject(GatewayApi);
|
||||
private readonly htmlApi = inject(HtmlReportsApi);
|
||||
/** Reportes HTML (se imprimen desde el navegador) o del Gateway (PDF), según la ruta (`data.source`). */
|
||||
private readonly isHtml = this.route.snapshot.data['source'] === 'html';
|
||||
protected readonly action = this.isHtml ? 'Generar' : 'Generar PDF';
|
||||
protected readonly isHtml = this.route.snapshot.data['source'] === 'html';
|
||||
protected readonly action = 'Generar PDF';
|
||||
private readonly htmlFrame = viewChild<ElementRef<HTMLIFrameElement>>('htmlFrame');
|
||||
private readonly sanitizer = inject(DomSanitizer);
|
||||
|
||||
@@ -124,7 +127,8 @@ export class ReportRunnerPage {
|
||||
this.htmlFrame()?.nativeElement.contentWindow?.print();
|
||||
}
|
||||
|
||||
protected async run(): Promise<void> {
|
||||
/** `pdf`: el PDF (del Gateway o, en los reportes HTML, generado por el servidor); `html`: la vista HTML para imprimir desde el navegador. */
|
||||
protected async run(mode: 'pdf' | 'html'): Promise<void> {
|
||||
const r = this.report();
|
||||
if (!r || this.running()) return;
|
||||
const values: Record<string, Value> = {};
|
||||
@@ -137,13 +141,15 @@ export class ReportRunnerPage {
|
||||
this.running.set(true);
|
||||
this.message.set({ kind: 'info', text: 'Generando el reporte…' });
|
||||
try {
|
||||
if (this.isHtml) {
|
||||
if (this.isHtml && mode === 'html') {
|
||||
this.pdfUrl.set(null);
|
||||
// el HTML lo arma nuestro servidor a partir de plantillas del proyecto (los valores ya van escapados)
|
||||
this.htmlDoc.set(this.sanitizer.bypassSecurityTrustHtml(await this.htmlApi.render(r.code, values as GatewayValues)));
|
||||
this.message.set({ kind: 'ok', text: 'Reporte generado. Pulse «Imprimir».' });
|
||||
return;
|
||||
}
|
||||
const blob = await this.api.export(r.code, values as GatewayValues);
|
||||
this.htmlDoc.set(null);
|
||||
const blob = this.isHtml ? await this.htmlApi.pdf(r.code, values as GatewayValues) : await this.api.export(r.code, values as GatewayValues);
|
||||
this.revoke();
|
||||
const url = URL.createObjectURL(blob);
|
||||
this.download.set({ url, name: `${r.code}.pdf` });
|
||||
|
||||
Reference in new issue
Block a user