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:
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* PrintButton — reusable button with dropdown for Print and PDF export.
|
||||
* Uses lucide-react icons and clsx for styling.
|
||||
*/
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { Printer, FileDown, ChevronDown } from 'lucide-react';
|
||||
import { printElement, printCurrentPage, exportToPDF } from '@/utils/print';
|
||||
|
||||
export interface PrintButtonProps {
|
||||
/** Element id to print. If omitted, prints the whole page. */
|
||||
targetId?: string;
|
||||
/** Filename suggestion for PDF export. */
|
||||
filename?: string;
|
||||
/** Additional Tailwind classes for the trigger button. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function PrintButton({
|
||||
targetId,
|
||||
filename = 'export',
|
||||
className,
|
||||
}: PrintButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') setOpen(false);
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
const handlePrint = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (targetId) {
|
||||
printElement(targetId);
|
||||
} else {
|
||||
printCurrentPage();
|
||||
}
|
||||
}, [targetId]);
|
||||
|
||||
const handleExportPDF = useCallback(() => {
|
||||
setOpen(false);
|
||||
if (targetId) {
|
||||
exportToPDF(targetId, filename);
|
||||
} else {
|
||||
// No specific element — use whole-page print which user can save as PDF
|
||||
printCurrentPage();
|
||||
}
|
||||
}, [targetId, filename]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative inline-block" data-no-print>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={clsx(
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-1.5 rounded-md',
|
||||
'text-sm font-medium text-secondary-700',
|
||||
'border border-secondary-300 bg-white',
|
||||
'hover:bg-secondary-50 transition-colors',
|
||||
'min-h-touch',
|
||||
open && 'ring-2 ring-primary-500 ring-offset-1',
|
||||
className,
|
||||
)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={open}
|
||||
aria-label="Drucken oder als PDF exportieren"
|
||||
title="Drucken / PDF"
|
||||
data-testid="print-button-trigger"
|
||||
>
|
||||
<Printer className="w-4 h-4" aria-hidden="true" strokeWidth={2} />
|
||||
<span className="hidden sm:inline">Drucken</span>
|
||||
<ChevronDown
|
||||
className={clsx('w-3.5 h-3.5 transition-transform', open && 'rotate-180')}
|
||||
aria-hidden="true"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div
|
||||
role="menu"
|
||||
className={clsx(
|
||||
'absolute right-0 mt-1 z-50',
|
||||
'min-w-[180px] rounded-md border border-secondary-200',
|
||||
'bg-white shadow-lg py-1',
|
||||
)}
|
||||
data-testid="print-button-dropdown"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePrint}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 w-full px-3 py-2',
|
||||
'text-sm text-secondary-700 hover:bg-secondary-50',
|
||||
'transition-colors text-left',
|
||||
)}
|
||||
role="menuitem"
|
||||
data-testid="print-button-print"
|
||||
>
|
||||
<Printer className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
<span>Drucken</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExportPDF}
|
||||
className={clsx(
|
||||
'flex items-center gap-2 w-full px-3 py-2',
|
||||
'text-sm text-secondary-700 hover:bg-secondary-50',
|
||||
'transition-colors text-left',
|
||||
)}
|
||||
role="menuitem"
|
||||
data-testid="print-button-pdf"
|
||||
>
|
||||
<FileDown className="w-4 h-4 text-secondary-500" aria-hidden="true" strokeWidth={2} />
|
||||
<span>Als PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user