fix: repair print/PDF in Reports — fix document.write/appendChild mix, add printPdfBlob for print format
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user