Phase 1: Workflows UI, Dedup/Merge UI, Import/Export UI, Print/PDF

- Workflows UI: full page with definitions/instances tabs, step editor, instance detail with approve/reject
- Dedup/Merge UI: duplicate detection, side-by-side comparison, field-level merge dialog, merge history
- Import/Export UI: import wizard (dry-run preview), export panel (CSV/XLSX), backend export route added
- Print/PDF: PrintButton component, print.css, integrated in Contacts/Calendar/Reports/ContactDetail
- Backend: GET /api/v1/export endpoint, export_companies_csv() service function
- Routes: /workflows, /contacts/dedup, /import-export registered
- Menu items: Workflows, Import/Export, Duplikate added to automation plugin manifest
- IMPLEMENTATION_PLAN.md: audit-corrected plan for all 14 remaining features
This commit is contained in:
Agent Zero
2026-07-26 02:35:44 +02:00
parent 6d484ed747
commit a3a5a10514
27 changed files with 4288 additions and 5 deletions
+156
View File
@@ -0,0 +1,156 @@
/**
* Print / PDF utility functions.
* Uses the browser's native print-to-PDF capability.
*/
/**
* Print a specific DOM element by cloning it into a new window.
* Copies all stylesheets from the current document so the clone looks identical.
*/
export function printElement(elementId: string): void {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`printElement: element with id "${elementId}" not found`);
return;
}
const printWindow = window.open('', '_blank', 'width=900,height=700');
if (!printWindow) {
alert('Bitte erlauben Sie Pop-up-Fenster zum Drucken.');
return;
}
// Clone the target element so we don't disturb the live DOM
const clone = element.cloneNode(true) as HTMLElement;
// 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(
'<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>');
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);
};
}
/**
* Print the current page using window.print().
* Relies on @media print CSS (print.css) to hide non-printable elements.
*/
export function printCurrentPage(): void {
window.print();
}
/**
* Export an element to PDF using the browser's native print-to-PDF.
* Opens the print dialog where the user can choose "Save as PDF".
*
* @param elementId - The id of the DOM element to export
* @param filename - Suggested filename (used in the print window title)
*/
export function exportToPDF(elementId: string, filename: string): void {
const element = document.getElementById(elementId);
if (!element) {
console.warn(`exportToPDF: element with id "${elementId}" not found`);
return;
}
const printWindow = window.open('', '_blank', 'width=900,height=700');
if (!printWindow) {
alert('Bitte erlauben Sie Pop-up-Fenster zum Exportieren als PDF.');
return;
}
const clone = element.cloneNode(true) as HTMLElement;
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(
'<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>');
printWindow.document.close();
printWindow.onload = () => {
printWindow.focus();
printWindow.print();
setTimeout(() => {
printWindow.close();
}, 500);
};
}