import { useState, useEffect } from 'react'; import { Modal, Steps, Button, Table, Radio, Select, Input, DatePicker, Space, Row, Col, Typography, message, Spin, Alert, Tag } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { emailImportApi, type AttachmentImportData } from '../api/email-import'; import { paperlessApi, type PaperlessCorrespondent } from '../api/paperless'; import dayjs from 'dayjs'; import { PrinterOutlined, PaperClipOutlined, ArrowRightOutlined, WarningOutlined } from '@ant-design/icons'; import PdfSplitViewer from './PdfSplitViewer'; import BarcodePositioner from './BarcodePositioner'; const { Text } = Typography; interface MailImportWizardProps { visible: boolean; onClose: () => void; onSuccess?: () => void; email: any; attachments: any[]; } export default function MailImportWizard({ visible, onClose, onSuccess, email, attachments }: MailImportWizardProps) { const [currentStep, setCurrentStep] = useState(0); const [importData, setImportData] = useState([]); const [loading, setLoading] = useState(false); const [correspondents, setCorrespondents] = useState([]); const [suggestedCorrespondentId, setSuggestedCorrespondentId] = useState(null); // Step 2 specific state const [belegnummern, setBelegnummern] = useState>({}); const [barcodes, setBarcodes] = useState>({}); // Step 1 expand/collapse state const [expandedRows, setExpandedRows] = useState([]); // Belegnummer mode per item: 'neu' (auto from API) or 'manuell' const [belegnummerMode, setBelegnummerMode] = useState>({}); // Eingangsdatum per item const [eingangsdaten, setEingangsdaten] = useState>({}); // Step 3 specific state const [importSuccess, setImportSuccess] = useState(false); const [importStatus, setImportStatus] = useState(''); useEffect(() => { if (visible && attachments.length > 0) { // Initialize import data const initialData = attachments.map(att => ({ attachmentId: att.Id, virtualId: `${att.Id}_full`, type: 'MAIN' as 'MAIN' | 'ATTACHMENT' | 'IGNORE', fileName: att.FileName, pages: undefined, // full document initially })); setImportData(initialData); setExpandedRows(initialData.length > 0 ? [initialData[0].virtualId] : []); // Initialize eingangsdaten and barcodes with email date const mailDate = dayjs(email.Date); const initialDates: Record = {}; const initialBarcodes: Record = {}; initialData.forEach(d => { initialDates[d.virtualId] = mailDate; initialBarcodes[d.virtualId] = { x: 7, y: 7, datum: mailDate.format('YYYY-MM-DD'), jahr: mailDate.format('YYYY'), isNeu: true, nummer: '000000' }; }); setEingangsdaten(initialDates); setBarcodes(initialBarcodes); // Load correspondents and try to find suggestion loadCorrespondents(); // Check for duplicates in Paperless checkDuplicates(initialData); } }, [visible, attachments]); const checkDuplicates = async (initialData: AttachmentImportData[]) => { setLoading(true); try { const updatedData = [...initialData]; for (let i = 0; i < updatedData.length; i++) { const item = updatedData[i]; const attachment = attachments.find(a => a.Id === item.attachmentId); if (attachment && attachment.Checksum) { const exists = await paperlessApi.checksumExists(attachment.Checksum); if (exists) { updatedData[i] = { ...updatedData[i], type: 'IGNORE', isDuplicate: true }; } } } setImportData(updatedData); } catch (e) { console.error('Error checking duplicates', e); } finally { setLoading(false); } }; const loadCorrespondents = async () => { try { const data = await paperlessApi.getCorrespondents(); setCorrespondents(data || []); // Try to find matching correspondent by email // We parse the from address, which might be "Name " const match = email.From.match(/<([^>]+)>/); const emailAddress = match ? match[1] : email.From; const mapping = await emailImportApi.getCorrespondentByEmail(emailAddress); if (mapping && mapping.paperlessCorrespondentId) { setSuggestedCorrespondentId(mapping.paperlessCorrespondentId); setImportData(prev => prev.map(item => ({ ...item, paperlessCorrespondentId: mapping.paperlessCorrespondentId }))); } } catch (e) { // silently fail } }; const updateImportData = (virtualId: string, key: string, value: any) => { setImportData(prev => prev.map(item => item.virtualId === virtualId ? { ...item, [key]: value } : item)); }; const handleSplit = async (virtualId: string, splitPage: number) => { const idx = importData.findIndex(i => i.virtualId === virtualId); if (idx === -1) return; const itemToSplit = importData[idx]; const start = itemToSplit.pages?.start || 1; const end = itemToSplit.pages?.end || 999; const part1Pages = { start, end: splitPage }; const part2Pages = { start: splitPage + 1, end }; const part1 = { ...itemToSplit, virtualId: `${itemToSplit.attachmentId}_${start}_${splitPage}`, pages: part1Pages, fileName: `${itemToSplit.fileName} (Teil 1)` }; const part2 = { ...itemToSplit, virtualId: `${itemToSplit.attachmentId}_${splitPage+1}_${end}`, pages: part2Pages, fileName: `${itemToSplit.fileName} (Teil 2)` }; const parentDate = eingangsdaten[virtualId] || dayjs(email.Date); const parentBarcode = barcodes[virtualId]; setEingangsdaten(prev => ({ ...prev, [part1.virtualId]: parentDate, [part2.virtualId]: parentDate, })); if (parentBarcode) { setBarcodes(prev => ({ ...prev, [part1.virtualId]: { ...parentBarcode }, [part2.virtualId]: { ...parentBarcode }, })); } setImportData(prev => { const newArray = [...prev]; newArray.splice(idx, 1, part1, part2); return newArray; }); // Checksumme der geteilten Teile prüfen try { const [dup1, dup2] = await Promise.all([ emailImportApi.checkSplitChecksum(itemToSplit.attachmentId, part1Pages), emailImportApi.checkSplitChecksum(itemToSplit.attachmentId, part2Pages), ]); if (dup1 || dup2) { setImportData(prev => prev.map(item => { if (item.virtualId === part1.virtualId && dup1) return { ...item, isDuplicate: true, type: 'IGNORE' as const }; if (item.virtualId === part2.virtualId && dup2) return { ...item, isDuplicate: true, type: 'IGNORE' as const }; return item; })); } } catch (e) { console.error('Fehler bei Checksummen-Prüfung nach Split', e); } }; const loadBelegnummern = async () => { // No longer fetching from API here, just initializing state for Step 2 for (const item of importData) { if (item.type === 'IGNORE') continue; const vid = item.virtualId; const mode = belegnummerMode[vid] || 'neu'; const itemDate = eingangsdaten[vid] || dayjs(email.Date); const dateStr = itemDate.format('YYYY-MM-DD'); const jahr = itemDate.format('YYYY'); if (mode === 'neu') { setBarcodes(prev => ({ ...prev, [vid]: { ...prev[vid], x: prev[vid]?.x ?? 6, y: prev[vid]?.y ?? 6, datum: dateStr, jahr, isNeu: true, nummer: '000000' } })); } else if (mode === 'manuell') { const manJahr = barcodes[vid]?.jahr || jahr; const manNummer = belegnummern[vid] || '000000'; setBelegnummern(prev => ({ ...prev, [vid]: manNummer })); setBarcodes(prev => ({ ...prev, [vid]: { ...prev[vid], x: prev[vid]?.x ?? 6, y: prev[vid]?.y ?? 6, datum: dateStr, jahr: manJahr, nummer: manNummer, isNeu: false } })); } if (!eingangsdaten[vid]) { setEingangsdaten(prev => ({ ...prev, [vid]: itemDate })); } if (!belegnummerMode[vid]) setBelegnummerMode(prev => ({ ...prev, [vid]: 'neu' })); } }; const preFetchBelegnummern = async () => { setLoading(true); try { const newBelegnummern = { ...belegnummern }; const newBarcodes = { ...barcodes }; for (const item of importData) { if (item.type === 'IGNORE') continue; const vid = item.virtualId; const mode = belegnummerMode[vid] || 'neu'; if (mode === 'neu' && (!newBelegnummern[vid] || newBelegnummern[vid] === '000000')) { const itemDate = eingangsdaten[vid] || dayjs(email.Date); const dateStr = itemDate.format('YYYY-MM-DD'); try { const res = await emailImportApi.getBelegnummer(dateStr); let num = res.nummer; let yr = itemDate.format('YYYY'); if (num.includes('-')) { const parts = num.split('-'); if (parts.length === 2 && parts[0].length === 4) { yr = parts[0]; num = parts[1]; } } newBelegnummern[vid] = num; newBarcodes[vid] = { ...newBarcodes[vid], nummer: num, jahr: yr, datum: dateStr, isNeu: false // We show it as "fixed" in summary }; } catch (e) { message.error(`Belegnummer für ${item.fileName} konnte nicht geladen werden.`); } } } setBelegnummern(newBelegnummern); setBarcodes(newBarcodes); } finally { setLoading(false); } }; const handleBack = async () => { if (currentStep === 2) { setLoading(true); try { for (const item of importData) { const vid = item.virtualId; const mode = belegnummerMode[vid] || 'neu'; const num = belegnummern[vid]; if (mode === 'neu' && num && num !== '000000') { const dateStr = (eingangsdaten[vid] || dayjs(email.Date)).format('YYYY-MM-DD'); await emailImportApi.releaseBelegnummer(dateStr, num); } } // Clear numbers for "neu" mode so they get re-fetched const clearedBelegnummern = { ...belegnummern }; const clearedBarcodes = { ...barcodes }; for (const vid in belegnummerMode) { if (belegnummerMode[vid] === 'neu') { clearedBelegnummern[vid] = '000000'; if (clearedBarcodes[vid]) { clearedBarcodes[vid] = { ...clearedBarcodes[vid], nummer: '000000', isNeu: true }; } } } setBelegnummern(clearedBelegnummern); setBarcodes(clearedBarcodes); } catch (e) { console.error('Failed to release numbers', e); } finally { setLoading(false); } } setCurrentStep(currentStep - 1); }; const nextStep = async () => { if (currentStep === 0) { await loadBelegnummern(); } else if (currentStep === 1) { await preFetchBelegnummern(); } setCurrentStep(currentStep + 1); }; const executeImport = async () => { setLoading(true); setImportStatus('Import wird gestartet...'); const jobId = crypto.randomUUID ? crypto.randomUUID() : `job-${Date.now()}`; const statusPoll = setInterval(async () => { try { const status = await emailImportApi.getJobStatus(jobId); if (status?.message) setImportStatus(status.message); } catch {} }, 1500); try { const finalData = []; for (const item of importData) { if (item.type === 'IGNORE') continue; let num = belegnummern[item.virtualId] || '000000'; let yr = barcodes[item.virtualId]?.jahr || eingangsdaten[item.virtualId]?.format('YYYY') || dayjs(email.Date).format('YYYY'); const mode = belegnummerMode[item.virtualId] || 'neu'; if (mode === 'neu' && (!num || num === '000000')) { // Fallback in case pre-fetch failed or was skipped const dateStr = (eingangsdaten[item.virtualId] || dayjs(email.Date)).format('YYYY-MM-DD'); try { const res = await emailImportApi.getBelegnummer(dateStr); num = res.nummer; if (num.includes('-')) { const parts = num.split('-'); if (parts.length === 2 && parts[0].length === 4) { yr = parts[0]; num = parts[1]; } } } catch (e) { throw new Error(`Konnte keine neue Belegnummer für ${item.fileName} abrufen.`); } } else { // Manuell or already fetched: Ensure num is just the 6-digit part if it contains a dash if (num.includes('-')) { num = num.split('-')[1]; } } const finalBelegnummer = `${yr}-${String(num).padStart(6, '0')}`; finalData.push({ ...item, splitRanges: item.pages ? [item.pages] : undefined, barcode: { ...barcodes[item.virtualId], nummer: num, jahr: yr, isNeu: false }, belegnummer: finalBelegnummer, }); } await emailImportApi.executeImport(email.Date, finalData, jobId); setImportSuccess(true); setCurrentStep(2); } catch (e: any) { message.error(`Fehler beim Import: ${e.message}`); } finally { clearInterval(statusPoll); setImportStatus(''); setLoading(false); } }; const printDocument = async (virtualId: string, attachmentId: number) => { // Open a new tab immediately to satisfy pop-up blockers const printWindow = window.open('', '_blank'); if (!printWindow) { message.warning('Bitte Pop-ups erlauben, um direkt zu drucken.'); return; } printWindow.document.write('Druckvorschau
Lade Dokument...
'); try { const barcode = barcodes[virtualId]; if (!barcode) { printWindow.close(); return; } const item = importData.find(i => i.virtualId === virtualId); const pages = item?.pages; const blob = await emailImportApi.printPreview(attachmentId, barcode, pages); const url = window.URL.createObjectURL(blob); // Navigate the already open window to the PDF printWindow.location.href = url; // Some browsers allow triggering print() on the new window // but it's inconsistent for PDFs. Most PDF viewers have their own print button. // So we just leave it open for the user. } catch (e) { message.error('Fehler beim Generieren der Druckvorschau'); printWindow.close(); } }; // --- Step 1: Zuordnung Render --- const renderStep1 = () => { const columns: ColumnsType = [ { title: 'Dateiname', dataIndex: 'fileName', key: 'fileName', render: (text, record) => ( {text} {attachments.find(a => a.Id === record.attachmentId)?.Erechnung && ( eRechnung )} {record.isDuplicate && ( }>Bereits vorhanden )} ) }, { title: 'Aktion', key: 'action', render: (_, record) => { const hasOtherMain = importData.some(item => item.type === 'MAIN' && item.virtualId !== record.virtualId); const showAttachmentOption = importData.length > 1 && hasOtherMain; return ( updateImportData(record.virtualId, 'type', e.target.value)} > Importieren Ignorieren {showAttachmentOption && Als Anlage} ); } }, { title: 'Hauptdokument', key: 'parent', render: (_, record) => { if (record.type === 'ATTACHMENT') { const mainItems = importData.filter(i => i.type === 'MAIN' && i.virtualId !== record.virtualId); return ( ); } return null; } } ]; return (
Absender: {email.From}
Korrespondent (Paperless):
{ setExpandedRows(expanded ? [record.virtualId] : []); }, expandedRowRender: record => { const originalAtt = attachments.find(a => a.Id === record.attachmentId); if (!originalAtt) return null; return ( handleSplit(record.virtualId, page)} disabled={originalAtt.Erechnung} /> ); } }} /> ); }; // --- Step 2: Bearbeitung Render --- const renderStep2 = () => { const toProcess = importData.filter(i => i.type !== 'IGNORE'); if (toProcess.length === 0) return Keine Dokumente zum Importieren ausgewählt.; return (
{toProcess.map(item => (
{item.fileName}
{/* Eingangsdatum */}
Eingangsdatum: { if (date) { setEingangsdaten(prev => ({ ...prev, [item.virtualId]: date })); setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], datum: date.format('YYYY-MM-DD'), jahr: date.format('YYYY') } })); } }} style={{ width: '100%' }} format="DD.MM.YYYY" />
{/* Belegnummer */} Belegnummer { const mode = e.target.value; setBelegnummerMode(prev => ({ ...prev, [item.virtualId]: mode })); if (mode === 'manuell') { const jahr = (eingangsdaten[item.virtualId] || dayjs(email.Date)).format('YYYY'); setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], jahr, isNeu: false } })); if (!belegnummern[item.virtualId] || belegnummerMode[item.virtualId] === 'neu') { setBelegnummern(prev => ({ ...prev, [item.virtualId]: '000000' })); } } else { setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], isNeu: true } })); } }} style={{ marginBottom: 12 }} > Neu Manuell {(belegnummerMode[item.virtualId] || 'neu') === 'manuell' && (
Jahr (4-stellig): { const val = e.target.value.replace(/\D/g, '').slice(0, 4); setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], jahr: val } })); }} style={{ width: '100%' }} placeholder="2026" />
Nummer (6-stellig): { const val = e.target.value.replace(/\D/g, '').slice(0, 6); setBelegnummern(prev => ({ ...prev, [item.virtualId]: val })); setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], nummer: val } })); }} style={{ width: '100%' }} placeholder="000000" />
)} {(belegnummerMode[item.virtualId] || 'neu') === 'neu' && belegnummern[item.virtualId] && (
Reserviert: {belegnummern[item.virtualId]}
)}
Position: {barcodes[item.virtualId]?.x || 0} × {barcodes[item.virtualId]?.y || 0} mm
setBarcodes(prev => ({ ...prev, [item.virtualId]: { ...prev[item.virtualId], x: pos.x, y: pos.y } }))} /> ))} ); }; // --- Step 3: Abschluss Render --- const renderDocumentList = (showPrint: boolean) => { const mainDocs = importData.filter(i => i.type === 'MAIN'); const attachmentsToImport = importData.filter(i => i.type === 'ATTACHMENT'); return ( <> {mainDocs.length === 0 && ( )} {mainDocs.map(main => { const mainAttachments = attachmentsToImport.filter(a => a.parentVirtualId === main.virtualId); const num = belegnummern[main.virtualId] || '000000'; const yr = barcodes[main.virtualId]?.jahr || dayjs(email.Date).format('YYYY'); const datum = eingangsdaten[main.virtualId]; const belegnr = `${yr}-${String(num).padStart(6, '0')}`; return (
{main.fileName}
Eingangsdatum: {datum?.format('DD.MM.YYYY') ?? '—'} Belegnummer: {belegnr} {mainAttachments.length > 0 && (
{mainAttachments.map(att => (
{att.fileName} Anlage
))}
)} {showPrint && ( )} ); })} {attachmentsToImport.filter(a => !mainDocs.find(m => m.virtualId === a.parentVirtualId)).map(orphan => (
))} ); }; const renderStep3 = () => { if (importSuccess) { return (
{renderDocumentList(true)}
); } return renderDocumentList(false); }; return ( Schließen ) : ( {currentStep > 0 && } {currentStep < 2 && } {currentStep === 2 && } ) } >
{currentStep === 0 && renderStep1()} {currentStep === 1 && renderStep2()} {currentStep === 2 && renderStep3()}
); }