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);
+}