diff --git a/frontend/src/pages/Reports.tsx b/frontend/src/pages/Reports.tsx index 1c24fe5..e391e22 100644 --- a/frontend/src/pages/Reports.tsx +++ b/frontend/src/pages/Reports.tsx @@ -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, diff --git a/frontend/src/utils/print.ts b/frontend/src/utils/print.ts index 54554cd..8e8e344 100644 --- a/frontend/src/utils/print.ts +++ b/frontend/src/utils/print.ts @@ -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(''); - printWindow.document.write(''); - printWindow.document.write('Druckansicht'); - - // 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 = '', - ); + '@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; }' + + '}' + + ''; - printWindow.document.write(''); - printWindow.document.write(clone.outerHTML); - printWindow.document.write(''); + // Single document.write — no mixing with appendChild + printWindow.document.open(); + printWindow.document.write( + '' + + 'Druckansicht' + + stylesHtml + + printStyles + + '' + + clone.outerHTML + + '', + ); 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(''); - printWindow.document.write(''); - printWindow.document.write(`${filename}`); - - // 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 = '', - ); + '@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; }' + + '}' + + ''; - printWindow.document.write(''); - printWindow.document.write(clone.outerHTML); - printWindow.document.write(''); + // Single document.write — no mixing with appendChild + printWindow.document.open(); + printWindow.document.write( + '' + + `${filename}` + + stylesHtml + + printStyles + + '' + + clone.outerHTML + + '', + ); 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); +}