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';
|
} from 'lucide-react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PrintButton } from '@/components/common/PrintButton';
|
import { PrintButton } from '@/components/common/PrintButton';
|
||||||
|
import { printPdfBlob } from '@/utils/print';
|
||||||
import { useToast } from '@/components/ui/Toast';
|
import { useToast } from '@/components/ui/Toast';
|
||||||
import {
|
import {
|
||||||
useReportTemplates,
|
useReportTemplates,
|
||||||
@@ -163,7 +164,11 @@ export function ReportsPage() {
|
|||||||
output_format: editorFormat,
|
output_format: editorFormat,
|
||||||
});
|
});
|
||||||
const filename = `report_${selectedTemplate.name}.${editorFormat === 'excel' ? 'xlsx' : 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) => [
|
setDownloadHistory((prev) => [
|
||||||
{ id: Date.now().toString(), name: selectedTemplate.name, format: editorFormat, timestamp: new Date().toLocaleString() },
|
{ id: Date.now().toString(), name: selectedTemplate.name, format: editorFormat, timestamp: new Date().toLocaleString() },
|
||||||
...prev,
|
...prev,
|
||||||
@@ -192,7 +197,11 @@ export function ReportsPage() {
|
|||||||
});
|
});
|
||||||
const ext = activePresetFormat === 'excel' ? 'xlsx' : activePresetFormat === 'print' ? 'pdf' : activePresetFormat;
|
const ext = activePresetFormat === 'excel' ? 'xlsx' : activePresetFormat === 'print' ? 'pdf' : activePresetFormat;
|
||||||
const filename = `${preset.key}_report.${ext}`;
|
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) => [
|
setDownloadHistory((prev) => [
|
||||||
{ id: Date.now().toString(), name: preset.name, format: activePresetFormat, timestamp: new Date().toLocaleString() },
|
{ id: Date.now().toString(), name: preset.name, format: activePresetFormat, timestamp: new Date().toLocaleString() },
|
||||||
...prev,
|
...prev,
|
||||||
|
|||||||
+87
-76
@@ -3,6 +3,18 @@
|
|||||||
* Uses the browser's native print-to-PDF capability.
|
* 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.
|
* Print a specific DOM element by cloning it into a new window.
|
||||||
* Copies all stylesheets from the current document so the clone looks identical.
|
* Copies all stylesheets from the current document so the clone looks identical.
|
||||||
@@ -20,53 +32,40 @@ export function printElement(elementId: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clone the target element so we don't disturb the live DOM
|
|
||||||
const clone = element.cloneNode(true) as HTMLElement;
|
const clone = element.cloneNode(true) as HTMLElement;
|
||||||
|
const stylesHtml = collectStylesHtml();
|
||||||
|
|
||||||
// Write the HTML document
|
const printStyles =
|
||||||
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(
|
|
||||||
'<style>' +
|
'<style>' +
|
||||||
'@media print {' +
|
'@media print {' +
|
||||||
'body { margin: 1cm; font-size: 12pt; color: #000; background: #fff; }' +
|
'body { margin: 1cm; font-size: 12pt; color: #000; background: #fff; }' +
|
||||||
'img { max-width: 100%; }' +
|
'img { max-width: 100%; }' +
|
||||||
'table { width: 100%; border-collapse: collapse; }' +
|
'table { width: 100%; border-collapse: collapse; }' +
|
||||||
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; }' +
|
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; }' +
|
||||||
'th { background: #f5f5f5; font-weight: 600; }' +
|
'th { background: #f5f5f5; font-weight: 600; }' +
|
||||||
'}' +
|
'}' +
|
||||||
'@media screen {' +
|
'@media screen {' +
|
||||||
'body { padding: 20px; font-family: sans-serif; }' +
|
'body { padding: 20px; font-family: sans-serif; }' +
|
||||||
'}' +
|
'}' +
|
||||||
'</style>',
|
'</style>';
|
||||||
);
|
|
||||||
|
|
||||||
printWindow.document.write('</head><body>');
|
// Single document.write — no mixing with appendChild
|
||||||
printWindow.document.write(clone.outerHTML);
|
printWindow.document.open();
|
||||||
printWindow.document.write('</body></html>');
|
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();
|
printWindow.document.close();
|
||||||
|
|
||||||
// Wait for stylesheets to load before printing
|
// Wait for stylesheets to load before printing
|
||||||
printWindow.onload = () => {
|
printWindow.onload = () => {
|
||||||
printWindow.focus();
|
printWindow.focus();
|
||||||
printWindow.print();
|
printWindow.print();
|
||||||
// Close the window after print dialog (most browsers do this automatically)
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
printWindow.close();
|
printWindow.close();
|
||||||
}, 500);
|
}, 500);
|
||||||
@@ -102,48 +101,38 @@ export function exportToPDF(elementId: string, filename: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clone = element.cloneNode(true) as HTMLElement;
|
const clone = element.cloneNode(true) as HTMLElement;
|
||||||
|
const stylesHtml = collectStylesHtml();
|
||||||
|
|
||||||
printWindow.document.open();
|
const printStyles =
|
||||||
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(
|
|
||||||
'<style>' +
|
'<style>' +
|
||||||
'@media print {' +
|
'@media print {' +
|
||||||
`@page { margin: 1.5cm; }` +
|
'@page { margin: 1.5cm; }' +
|
||||||
'body { margin: 0; font-size: 12pt; color: #000; background: #fff; line-height: 1.4; }' +
|
'body { margin: 0; font-size: 12pt; color: #000; background: #fff; line-height: 1.4; }' +
|
||||||
'img { max-width: 100%; height: auto; }' +
|
'img { max-width: 100%; height: auto; }' +
|
||||||
'table { width: 100%; border-collapse: collapse; page-break-inside: auto; }' +
|
'table { width: 100%; border-collapse: collapse; page-break-inside: auto; }' +
|
||||||
'tr { page-break-inside: avoid; page-break-after: auto; }' +
|
'tr { page-break-inside: avoid; page-break-after: auto; }' +
|
||||||
'thead { display: table-header-group; }' +
|
'thead { display: table-header-group; }' +
|
||||||
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; text-align: left; }' +
|
'th, td { border: 1px solid #ccc; padding: 4px 8px; font-size: 11pt; text-align: left; }' +
|
||||||
'th { background: #f5f5f5; font-weight: 600; }' +
|
'th { background: #f5f5f5; font-weight: 600; }' +
|
||||||
'h1, h2, h3 { page-break-after: avoid; }' +
|
'h1, h2, h3 { page-break-after: avoid; }' +
|
||||||
'.card, [class*="rounded"] { border: 1px solid #ddd !important; box-shadow: none !important; border-radius: 4px; }' +
|
'.card, [class*="rounded"] { border: 1px solid #ddd !important; box-shadow: none !important; border-radius: 4px; }' +
|
||||||
'}' +
|
'}' +
|
||||||
'@media screen {' +
|
'@media screen {' +
|
||||||
'body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }' +
|
'body { padding: 20px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }' +
|
||||||
'}' +
|
'}' +
|
||||||
'</style>',
|
'</style>';
|
||||||
);
|
|
||||||
|
|
||||||
printWindow.document.write('</head><body>');
|
// Single document.write — no mixing with appendChild
|
||||||
printWindow.document.write(clone.outerHTML);
|
printWindow.document.open();
|
||||||
printWindow.document.write('</body></html>');
|
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.document.close();
|
||||||
|
|
||||||
printWindow.onload = () => {
|
printWindow.onload = () => {
|
||||||
@@ -154,3 +143,25 @@ export function exportToPDF(elementId: string, filename: string): void {
|
|||||||
}, 500);
|
}, 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