fix: repair print/PDF in Reports — fix document.write/appendChild mix, add printPdfBlob for print format

This commit is contained in:
Agent Zero
2026-07-26 09:40:43 +02:00
parent 0571cc8193
commit 4dbbc422ce
2 changed files with 98 additions and 78 deletions
+11 -2
View File
@@ -17,6 +17,7 @@ import {
} from 'lucide-react';
import clsx from 'clsx';
import { PrintButton } from '@/components/common/PrintButton';
import { printPdfBlob } from '@/utils/print';
import { useToast } from '@/components/ui/Toast';
import {
useReportTemplates,
@@ -163,7 +164,11 @@ export function ReportsPage() {
output_format: editorFormat,
});
const filename = `report_${selectedTemplate.name}.${editorFormat === 'excel' ? 'xlsx' : editorFormat}`;
downloadBlob(response.data as Blob, filename);
if (editorFormat === 'print') {
printPdfBlob(response.data as Blob);
} else {
downloadBlob(response.data as Blob, filename);
}
setDownloadHistory((prev) => [
{ id: Date.now().toString(), name: selectedTemplate.name, format: editorFormat, timestamp: new Date().toLocaleString() },
...prev,
@@ -192,7 +197,11 @@ export function ReportsPage() {
});
const ext = activePresetFormat === 'excel' ? 'xlsx' : activePresetFormat === 'print' ? 'pdf' : activePresetFormat;
const filename = `${preset.key}_report.${ext}`;
downloadBlob(response.data as Blob, filename);
if (activePresetFormat === 'print') {
printPdfBlob(response.data as Blob);
} else {
downloadBlob(response.data as Blob, filename);
}
setDownloadHistory((prev) => [
{ id: Date.now().toString(), name: preset.name, format: activePresetFormat, timestamp: new Date().toLocaleString() },
...prev,
+87 -76
View File
@@ -3,6 +3,18 @@
* Uses the browser's native print-to-PDF capability.
*/
/**
* Collect all stylesheet HTML (style tags + link tags) from the current document
* as a single string, so it can be written in one document.write() call.
*/
function collectStylesHtml(): string {
let html = '';
document.querySelectorAll('style, link[rel="stylesheet"]').forEach((sheet) => {
html += sheet.outerHTML;
});
return html;
}
/**
* Print a specific DOM element by cloning it into a new window.
* Copies all stylesheets from the current document so the clone looks identical.
@@ -20,53 +32,40 @@ export function printElement(elementId: string): void {
return;
}
// Clone the target element so we don't disturb the live DOM
const clone = element.cloneNode(true) as HTMLElement;
const stylesHtml = collectStylesHtml();
// Write the HTML document
printWindow.document.open();
printWindow.document.write('<!DOCTYPE html>');
printWindow.document.write('<html lang="de"><head><meta charset="UTF-8">');
printWindow.document.write('<title>Druckansicht</title>');
// Copy all stylesheets from the current document
const styleSheets = document.querySelectorAll('style, link[rel="stylesheet"]');
styleSheets.forEach((sheet) => {
if (sheet.tagName === 'STYLE') {
const styleEl = sheet.cloneNode(true) as HTMLElement;
printWindow.document.head.appendChild(styleEl);
} else if (sheet.tagName === 'LINK') {
const linkEl = sheet.cloneNode(true) as HTMLElement;
printWindow.document.head.appendChild(linkEl);
}
});
// Add print-specific CSS
printWindow.document.write(
const printStyles =
'<style>' +
'@media print {' +
'body { margin: 1cm; font-size: 12pt; color: #000; background: #fff; }' +
'img { max-width: 100%; }' +
'table { width: 100%; border-collapse: collapse; }' +
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; }' +
'th { background: #f5f5f5; font-weight: 600; }' +
'}' +
'@media screen {' +
'body { padding: 20px; font-family: sans-serif; }' +
'}' +
'</style>',
);
'@media print {' +
'body { margin: 1cm; font-size: 12pt; color: #000; background: #fff; }' +
'img { max-width: 100%; }' +
'table { width: 100%; border-collapse: collapse; }' +
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; }' +
'th { background: #f5f5f5; font-weight: 600; }' +
'}' +
'@media screen {' +
'body { padding: 20px; font-family: sans-serif; }' +
'}' +
'</style>';
printWindow.document.write('</head><body>');
printWindow.document.write(clone.outerHTML);
printWindow.document.write('</body></html>');
// Single document.write — no mixing with appendChild
printWindow.document.open();
printWindow.document.write(
'<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">' +
'<title>Druckansicht</title>' +
stylesHtml +
printStyles +
'</head><body>' +
clone.outerHTML +
'</body></html>',
);
printWindow.document.close();
// Wait for stylesheets to load before printing
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
// Close the window after print dialog (most browsers do this automatically)
setTimeout(() => {
printWindow.close();
}, 500);
@@ -102,48 +101,38 @@ export function exportToPDF(elementId: string, filename: string): void {
}
const clone = element.cloneNode(true) as HTMLElement;
const stylesHtml = collectStylesHtml();
printWindow.document.open();
printWindow.document.write('<!DOCTYPE html>');
printWindow.document.write('<html lang="de"><head><meta charset="UTF-8">');
printWindow.document.write(`<title>${filename}</title>`);
// Copy all stylesheets
const styleSheets = document.querySelectorAll('style, link[rel="stylesheet"]');
styleSheets.forEach((sheet) => {
if (sheet.tagName === 'STYLE') {
const styleEl = sheet.cloneNode(true) as HTMLElement;
printWindow.document.head.appendChild(styleEl);
} else if (sheet.tagName === 'LINK') {
const linkEl = sheet.cloneNode(true) as HTMLElement;
printWindow.document.head.appendChild(linkEl);
}
});
// Print-specific styles for PDF export
printWindow.document.write(
const printStyles =
'<style>' +
'@media print {' +
`@page { margin: 1.5cm; }` +
'body { margin: 0; font-size: 12pt; color: #000; background: #fff; line-height: 1.4; }' +
'img { max-width: 100%; height: auto; }' +
'table { width: 100%; border-collapse: collapse; page-break-inside: auto; }' +
'tr { page-break-inside: avoid; page-break-after: auto; }' +
'thead { display: table-header-group; }' +
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; text-align: left; }' +
'th { background: #f5f5f5; font-weight: 600; }' +
'h1, h2, h3 { page-break-after: avoid; }' +
'.card, [class*="rounded"] { border: 1px solid #ddd !important; box-shadow: none !important; border-radius: 4px; }' +
'}' +
'@media screen {' +
'body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }' +
'}' +
'</style>',
);
'@media print {' +
'@page { margin: 1.5cm; }' +
'body { margin: 0; font-size: 12pt; color: #000; background: #fff; line-height: 1.4; }' +
'img { max-width: 100%; height: auto; }' +
'table { width: 100%; border-collapse: collapse; page-break-inside: auto; }' +
'tr { page-break-inside: avoid; page-break-after: auto; }' +
'thead { display: table-header-group; }' +
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; text-align: left; }' +
'th { background: #f5f5f5; font-weight: 600; }' +
'h1, h2, h3 { page-break-after: avoid; }' +
'.card, [class*="rounded"] { border: 1px solid #ddd !important; box-shadow: none !important; border-radius: 4px; }' +
'}' +
'@media screen {' +
'body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }' +
'}' +
'</style>';
printWindow.document.write('</head><body>');
printWindow.document.write(clone.outerHTML);
printWindow.document.write('</body></html>');
// Single document.write — no mixing with appendChild
printWindow.document.open();
printWindow.document.write(
'<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">' +
`<title>${filename}</title>` +
stylesHtml +
printStyles +
'</head><body>' +
clone.outerHTML +
'</body></html>',
);
printWindow.document.close();
printWindow.onload = () => {
@@ -154,3 +143,25 @@ export function exportToPDF(elementId: string, filename: string): void {
}, 500);
};
}
/**
* Open a PDF blob in a new browser window and trigger the print dialog.
* Used for backend-generated PDF reports that should be printed, not downloaded.
*/
export function printPdfBlob(blob: Blob): void {
const url = URL.createObjectURL(blob);
const printWindow = window.open(url, '_blank', 'width=900,height=700');
if (!printWindow) {
// Fallback: download the file
const link = document.createElement('a');
link.href = url;
link.download = 'report.pdf';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setTimeout(() => URL.revokeObjectURL(url), 1000);
return;
}
// Revoke after the window has had time to load
setTimeout(() => URL.revokeObjectURL(url), 5000);
}