feat: Duplikate im Import als neue Version behandeln #6
No files matched your search
@@ -56,6 +56,8 @@ export interface ActionResult {
|
||||
duplikatKandidaten?: number[];
|
||||
/** Der Benutzer hat das Duplikat verworfen */
|
||||
verworfen?: boolean;
|
||||
/** Aufgelöste interne Belegnummer (bei Duplikaten) */
|
||||
belegnummer?: string;
|
||||
}
|
||||
|
||||
export interface RunForDocumentResult {
|
||||
@@ -69,6 +71,7 @@ interface PaperlessRunResult {
|
||||
duplicateOfDocumentId?: number;
|
||||
versionMoeglich?: boolean;
|
||||
duplikatKandidaten?: number[];
|
||||
belegnummer?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -302,6 +305,7 @@ export class InboxPostprocessorService {
|
||||
duplicateOfDocumentId: res.duplicateOfDocumentId,
|
||||
versionMoeglich: res.versionMoeglich,
|
||||
duplikatKandidaten: res.duplikatKandidaten,
|
||||
belegnummer: res.belegnummer,
|
||||
});
|
||||
if (res.skipped) {
|
||||
abortProcessing = true;
|
||||
@@ -490,6 +494,7 @@ export class InboxPostprocessorService {
|
||||
duplicateOfDocumentId: pruefung.documentId ?? pruefung.kandidaten[0],
|
||||
versionMoeglich: pruefung.status === 'eindeutig',
|
||||
duplikatKandidaten: pruefung.kandidaten,
|
||||
belegnummer: interneBelegnummer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -506,6 +511,7 @@ export class InboxPostprocessorService {
|
||||
message: `Duplikat – Belegnummer ${interneBelegnummer} bereits vorhanden`,
|
||||
duplicateOfDocumentId: existingTask.PaperlessDocumentID ?? undefined,
|
||||
versionMoeglich: false,
|
||||
belegnummer: interneBelegnummer,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -147,6 +147,7 @@ export interface PostprocessActionResult {
|
||||
versionMoeglich?: boolean;
|
||||
duplikatKandidaten?: number[];
|
||||
verworfen?: boolean;
|
||||
belegnummer?: string;
|
||||
}
|
||||
|
||||
export const clientsApi = {
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { useEffect, useState, type ReactNode } from 'react';
|
||||
import { Alert, Button, Modal, Space, Spin, Tag, Typography, message } from 'antd';
|
||||
import { paperlessApi, type BelegnummerPruefung, type VergleichKennzahlen } from '../api/paperless';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
export interface DuplikatVergleichModalProps {
|
||||
open: boolean;
|
||||
belegnummer: string;
|
||||
/** eindeutig → Version möglich; mehrdeutig → nur Verwerfen */
|
||||
pruefung: BelegnummerPruefung | null;
|
||||
/** Vorhandenes Dokument (linke Seite) */
|
||||
paperlessDocumentId: number | null;
|
||||
/** Neuer Beleg (rechte Seite) */
|
||||
ladeNeuesPdf: () => Promise<Blob>;
|
||||
ladeKennzahlen: () => Promise<VergleichKennzahlen>;
|
||||
onVersion: () => void;
|
||||
onVerwerfen: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function KennzahlZeile({ label, vorhanden, neu }: { label: string; vorhanden: number | null; neu: number | null }) {
|
||||
const anzeige = (wert: number | null) => (wert === null ? 'nicht ermittelbar' : String(wert));
|
||||
const vergleichbar = vorhanden !== null && neu !== null;
|
||||
const abweichung = vergleichbar && vorhanden !== neu;
|
||||
return (
|
||||
<div>
|
||||
<Typography.Text strong>{label}:</Typography.Text> {anzeige(vorhanden)} (vorhanden) → {anzeige(neu)} (neu){' '}
|
||||
{abweichung && <Tag color="orange">abweichend</Tag>}
|
||||
{!vergleichbar && <Tag>nicht vergleichbar</Tag>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PdfSpalte({ titel, url, loading }: { titel: string; url: string | null; loading: boolean }) {
|
||||
return (
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
||||
<Typography.Text strong style={{ marginBottom: 4 }}>
|
||||
{titel}
|
||||
</Typography.Text>
|
||||
<div style={{ flex: 1, border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }}>
|
||||
{loading || !url ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<iframe src={url} title={titel} style={{ width: '100%', height: '100%', border: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vergleich eines Belegs mit einem vorhandenen Paperless-Dokument derselben
|
||||
* internen Belegnummer: Auffälligkeiten (Seiten-/Wortzahl) oben, beide PDFs
|
||||
* nebeneinander, Entscheidung „neue Version“ oder „verwerfen“.
|
||||
*/
|
||||
export function DuplikatVergleichModal({
|
||||
open,
|
||||
belegnummer,
|
||||
pruefung,
|
||||
paperlessDocumentId,
|
||||
ladeNeuesPdf,
|
||||
ladeKennzahlen,
|
||||
onVersion,
|
||||
onVerwerfen,
|
||||
onClose,
|
||||
}: DuplikatVergleichModalProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [vorhandenUrl, setVorhandenUrl] = useState<string | null>(null);
|
||||
const [neuUrl, setNeuUrl] = useState<string | null>(null);
|
||||
const [kennzahlen, setKennzahlen] = useState<VergleichKennzahlen | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || paperlessDocumentId === null) return;
|
||||
let cancelled = false;
|
||||
let urlAlt: string | null = null;
|
||||
let urlNeu: string | null = null;
|
||||
setLoading(true);
|
||||
setKennzahlen(null);
|
||||
(async () => {
|
||||
try {
|
||||
const [altBlob, neuBlob] = await Promise.all([paperlessApi.getDocumentPdfBlob(paperlessDocumentId), ladeNeuesPdf()]);
|
||||
if (cancelled) return;
|
||||
urlAlt = URL.createObjectURL(altBlob);
|
||||
urlNeu = URL.createObjectURL(neuBlob);
|
||||
setVorhandenUrl(urlAlt);
|
||||
setNeuUrl(urlNeu);
|
||||
} catch {
|
||||
if (!cancelled) message.error('Vorschau konnte nicht geladen werden');
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
ladeKennzahlen()
|
||||
.then((k) => {
|
||||
if (!cancelled) setKennzahlen(k);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) message.warning('Seiten- und Wortzahl konnten nicht ermittelt werden');
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (urlAlt) URL.revokeObjectURL(urlAlt);
|
||||
if (urlNeu) URL.revokeObjectURL(urlNeu);
|
||||
setVorhandenUrl(null);
|
||||
setNeuUrl(null);
|
||||
};
|
||||
// Lade-Funktionen ändern sich bei jedem Render; neu laden nur bei anderem Dokument
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, paperlessDocumentId]);
|
||||
|
||||
const versionMoeglich = pruefung?.status === 'eindeutig';
|
||||
|
||||
let auffaelligkeiten: ReactNode = <Spin size="small" />;
|
||||
if (kennzahlen) {
|
||||
const { vorhanden, neu } = kennzahlen;
|
||||
const alleBekannt = [vorhanden.seiten, neu.seiten, vorhanden.woerter, neu.woerter].every((w) => w !== null);
|
||||
const gleich = alleBekannt && vorhanden.seiten === neu.seiten && vorhanden.woerter === neu.woerter;
|
||||
auffaelligkeiten = gleich ? (
|
||||
<Alert type="success" showIcon message="Seiten- und Wortzahl gleich" />
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="Auffälligkeiten"
|
||||
description={
|
||||
<Space direction="vertical" size={2}>
|
||||
<KennzahlZeile label="Seiten" vorhanden={vorhanden.seiten} neu={neu.seiten} />
|
||||
<KennzahlZeile label="Wörter" vorhanden={vorhanden.woerter} neu={neu.woerter} />
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={`Belegnummer ${belegnummer} existiert bereits`}
|
||||
onCancel={onClose}
|
||||
width="90vw"
|
||||
style={{ top: 20 }}
|
||||
destroyOnClose
|
||||
footer={[
|
||||
<Button key="verwerfen" danger onClick={onVerwerfen}>
|
||||
Verwerfen
|
||||
</Button>,
|
||||
versionMoeglich && (
|
||||
<Button key="version" type="primary" onClick={onVersion}>
|
||||
Als neue Version hinzufügen
|
||||
</Button>
|
||||
),
|
||||
]}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%', marginBottom: 12 }}>
|
||||
{pruefung?.status === 'mehrdeutig' && (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`Mehrere Dokumente in Paperless mit dieser Belegnummer (IDs: ${pruefung.kandidaten.join(', ')}) – eine neue Version ist nicht möglich.`}
|
||||
/>
|
||||
)}
|
||||
{auffaelligkeiten}
|
||||
</Space>
|
||||
<div style={{ display: 'flex', flexDirection: isMobile ? 'column' : 'row', gap: 12, height: '70vh' }}>
|
||||
<PdfSpalte titel={`Vorhanden (Paperless #${paperlessDocumentId ?? '–'})`} url={vorhandenUrl} loading={loading} />
|
||||
<PdfSpalte titel="Neuer Beleg" url={neuUrl} loading={loading} />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { emailImportApi, type AttachmentImportData } from '../api/email-import';
|
||||
import { emailsApi } from '../api/emails';
|
||||
import { paperlessApi, type PaperlessCorrespondent } from '../api/paperless';
|
||||
import { paperlessApi, type BelegnummerPruefung, 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';
|
||||
import { DuplikatVergleichModal } from './DuplikatVergleichModal';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -43,6 +44,11 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
// Eingangsdatum per item
|
||||
const [eingangsdaten, setEingangsdaten] = useState<Record<string, dayjs.Dayjs>>({});
|
||||
|
||||
// Duplikatvergleich: Entscheidung „neue Version“ je Beleg (gilt nur für diese Belegnummer)
|
||||
const [versionEntscheidung, setVersionEntscheidung] = useState<Record<string, { belegnummer: string; documentId: number }>>({});
|
||||
// Noch zu entscheidende Treffer, werden nacheinander im Vergleichsdialog gezeigt
|
||||
const [vergleichQueue, setVergleichQueue] = useState<{ virtualId: string; belegnummer: string; pruefung: BelegnummerPruefung }[]>([]);
|
||||
|
||||
// Step 3 specific state
|
||||
const [importSuccess, setImportSuccess] = useState(false);
|
||||
const [importStatus, setImportStatus] = useState('');
|
||||
@@ -78,6 +84,8 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
});
|
||||
setEingangsdaten(initialDates);
|
||||
setBarcodes(initialBarcodes);
|
||||
setVersionEntscheidung({});
|
||||
setVergleichQueue([]);
|
||||
|
||||
// Load correspondents and try to find suggestion
|
||||
loadCorrespondents();
|
||||
@@ -349,6 +357,72 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
}
|
||||
};
|
||||
|
||||
// Manuell eingegebene Belegnummer im Format JJJJ-NNNNNN (wie beim Import), sonst null
|
||||
const manuelleBelegnummer = (virtualId: string): string | null => {
|
||||
if ((belegnummerMode[virtualId] || 'neu') !== 'manuell') return null;
|
||||
let num = belegnummern[virtualId] || '';
|
||||
if (num.includes('-')) num = num.split('-')[1];
|
||||
if (!num || /^0+$/.test(num)) return null;
|
||||
const yr = barcodes[virtualId]?.jahr || (eingangsdaten[virtualId] || dayjs(email.Date)).format('YYYY');
|
||||
return `${yr}-${String(num).padStart(6, '0')}`;
|
||||
};
|
||||
|
||||
// Prüft manuelle Belegnummern gegen Paperless; liefert die Treffer ohne gültige Entscheidung
|
||||
const pruefeManuelleBelegnummern = async () => {
|
||||
const kandidaten = importData
|
||||
.filter(i => i.type !== 'IGNORE')
|
||||
.map(i => ({ virtualId: i.virtualId, belegnummer: manuelleBelegnummer(i.virtualId) }))
|
||||
.filter((k): k is { virtualId: string; belegnummer: string } => k.belegnummer !== null);
|
||||
|
||||
// Entscheidungen für inzwischen geänderte Belegnummern verfallen
|
||||
const gueltig: typeof versionEntscheidung = {};
|
||||
for (const k of kandidaten) {
|
||||
const e = versionEntscheidung[k.virtualId];
|
||||
if (e && e.belegnummer === k.belegnummer) gueltig[k.virtualId] = e;
|
||||
}
|
||||
setVersionEntscheidung(gueltig);
|
||||
|
||||
const offen = kandidaten.filter(k => !gueltig[k.virtualId]);
|
||||
const pruefungen = await Promise.all(offen.map(k => paperlessApi.pruefeBelegnummer(k.belegnummer)));
|
||||
return offen
|
||||
.map((k, idx) => ({ ...k, pruefung: pruefungen[idx] }))
|
||||
.filter(k => k.pruefung.status !== 'frei');
|
||||
};
|
||||
|
||||
const weiterZuAbschluss = async (daten: AttachmentImportData[]) => {
|
||||
if (daten.length > 0 && daten.every(i => i.type === 'IGNORE')) {
|
||||
await ignoreEmail();
|
||||
return;
|
||||
}
|
||||
await preFetchBelegnummern();
|
||||
setCurrentStep(2);
|
||||
};
|
||||
|
||||
// Nächsten Treffer zeigen; nach dem letzten geht es zu Schritt 3
|
||||
const naechsterVergleich = (daten: AttachmentImportData[]) => {
|
||||
const rest = vergleichQueue.slice(1);
|
||||
setVergleichQueue(rest);
|
||||
if (rest.length === 0) void weiterZuAbschluss(daten);
|
||||
};
|
||||
|
||||
const handleVergleichVersion = () => {
|
||||
const aktuell = vergleichQueue[0];
|
||||
if (!aktuell?.pruefung.documentId) return;
|
||||
setVersionEntscheidung(prev => ({
|
||||
...prev,
|
||||
[aktuell.virtualId]: { belegnummer: aktuell.belegnummer, documentId: aktuell.pruefung.documentId! },
|
||||
}));
|
||||
naechsterVergleich(importData);
|
||||
};
|
||||
|
||||
const handleVergleichVerwerfen = () => {
|
||||
const aktuell = vergleichQueue[0];
|
||||
if (!aktuell) return;
|
||||
const daten = importData.map(i => (i.virtualId === aktuell.virtualId ? { ...i, type: 'IGNORE' as const } : i));
|
||||
setImportData(daten);
|
||||
naechsterVergleich(daten);
|
||||
};
|
||||
|
||||
const nextStep = async () => {
|
||||
if (currentStep === 0) {
|
||||
if (importData.length > 0 && importData.every(i => i.type === 'IGNORE')) {
|
||||
@@ -372,6 +446,20 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// Manuelle Belegnummern, die es in Paperless schon gibt → erst vergleichen
|
||||
setLoading(true);
|
||||
try {
|
||||
const treffer = await pruefeManuelleBelegnummern();
|
||||
if (treffer.length > 0) {
|
||||
setVergleichQueue(treffer);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
message.error('Belegnummer-Prüfung fehlgeschlagen – bitte erneut versuchen.');
|
||||
return;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
await preFetchBelegnummern();
|
||||
}
|
||||
setCurrentStep(currentStep + 1);
|
||||
@@ -448,9 +536,11 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
}
|
||||
|
||||
const finalBelegnummer = `${yr}-${String(num).padStart(6, '0')}`;
|
||||
const entscheidung = versionEntscheidung[item.virtualId];
|
||||
|
||||
finalData.push({
|
||||
...item,
|
||||
versionZuDokumentId: entscheidung?.belegnummer === finalBelegnummer ? entscheidung.documentId : null,
|
||||
splitRanges: item.pages ? [item.pages] : undefined,
|
||||
barcode: { ...barcodes[item.virtualId], nummer: num, jahr: yr, isNeu: false },
|
||||
belegnummer: finalBelegnummer,
|
||||
@@ -765,6 +855,11 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
<Text type="secondary">
|
||||
Belegnummer: <Tag color="blue" style={{ fontSize: 13 }}>{belegnr}</Tag>
|
||||
</Text>
|
||||
{versionEntscheidung[main.virtualId]?.belegnummer === belegnr && (
|
||||
<Tag color="purple">
|
||||
wird als neue Version zu Dokument {versionEntscheidung[main.virtualId].documentId} hinzugefügt
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
{mainAttachments.length > 0 && (
|
||||
<div style={{ marginTop: 12, paddingLeft: 16 }}>
|
||||
@@ -855,6 +950,26 @@ export default function MailImportWizard({ visible, onClose, onSuccess, email, a
|
||||
{currentStep === 2 && renderStep3()}
|
||||
</div>
|
||||
</Spin>
|
||||
{vergleichQueue.length > 0 && (() => {
|
||||
const aktuell = vergleichQueue[0];
|
||||
const item = importData.find(i => i.virtualId === aktuell.virtualId);
|
||||
const docId = aktuell.pruefung.documentId ?? aktuell.pruefung.kandidaten[0] ?? null;
|
||||
if (!item) return null;
|
||||
return (
|
||||
<DuplikatVergleichModal
|
||||
key={aktuell.virtualId}
|
||||
open
|
||||
belegnummer={aktuell.belegnummer}
|
||||
pruefung={aktuell.pruefung}
|
||||
paperlessDocumentId={docId}
|
||||
ladeNeuesPdf={() => emailImportApi.attachmentPdfBlob(item.attachmentId, item.pages)}
|
||||
ladeKennzahlen={() => emailImportApi.vergleich(item.attachmentId, docId ?? 0, item.pages)}
|
||||
onVersion={handleVergleichVersion}
|
||||
onVerwerfen={handleVergleichVerwerfen}
|
||||
onClose={() => setVergleichQueue([])}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
ZoomOutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { inboxApi, type InboxBarcode, type InboxFile, type PaperlessEntscheidung, type PostprocessActionResult } from '../api/inbox';
|
||||
import { paperlessApi } from '../api/paperless';
|
||||
import type { BelegnummerPruefung } from '../api/paperless';
|
||||
import { DuplikatVergleichModal } from '../components/DuplikatVergleichModal';
|
||||
import { userSettingsApi, type SenderOption } from '../api/userSettings';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
|
||||
@@ -97,110 +98,6 @@ function SourceTag({ source }: { source: InboxFile['source'] }) {
|
||||
);
|
||||
}
|
||||
|
||||
interface CompareModalProps {
|
||||
open: boolean;
|
||||
paperlessDocumentId: number | null;
|
||||
inboxDocumentId: string;
|
||||
onClose: () => void;
|
||||
onCreateNewVersion: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
function CompareModal({
|
||||
open,
|
||||
paperlessDocumentId,
|
||||
inboxDocumentId,
|
||||
onClose,
|
||||
onCreateNewVersion,
|
||||
onSkip,
|
||||
}: CompareModalProps) {
|
||||
const compareIsMobile = useIsMobile();
|
||||
const [paperlessUrl, setPaperlessUrl] = useState<string | null>(null);
|
||||
const [inboxUrl, setInboxUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || paperlessDocumentId === null) return;
|
||||
let cancelled = false;
|
||||
let papUrl: string | null = null;
|
||||
let inbUrl: string | null = null;
|
||||
setLoading(true);
|
||||
(async () => {
|
||||
try {
|
||||
const [pBlob, iBlob] = await Promise.all([
|
||||
paperlessApi.getDocumentPdfBlob(paperlessDocumentId),
|
||||
inboxApi.previewBlob(inboxDocumentId),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
papUrl = URL.createObjectURL(pBlob);
|
||||
inbUrl = URL.createObjectURL(iBlob);
|
||||
setPaperlessUrl(papUrl);
|
||||
setInboxUrl(inbUrl);
|
||||
} catch {
|
||||
if (!cancelled) message.error('Vorschau konnte nicht geladen werden');
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (papUrl) URL.revokeObjectURL(papUrl);
|
||||
if (inbUrl) URL.revokeObjectURL(inbUrl);
|
||||
setPaperlessUrl(null);
|
||||
setInboxUrl(null);
|
||||
};
|
||||
}, [open, paperlessDocumentId, inboxDocumentId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title="Duplikat vergleichen"
|
||||
onCancel={onClose}
|
||||
width="90vw"
|
||||
style={{ top: 20 }}
|
||||
footer={[
|
||||
<Button key="skip" onClick={onSkip}>
|
||||
Überspringen
|
||||
</Button>,
|
||||
<Button key="new" type="primary" onClick={onCreateNewVersion}>
|
||||
Neue Version anlegen
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: compareIsMobile ? 'column' : 'row', gap: 12, height: '75vh' }}>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
||||
<Typography.Text strong style={{ marginBottom: 4 }}>
|
||||
Original (Paperless)
|
||||
</Typography.Text>
|
||||
<div style={{ flex: 1, border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }}>
|
||||
{loading || !paperlessUrl ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<iframe src={paperlessUrl} title="Paperless Original" style={{ width: '100%', height: '100%', border: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 }}>
|
||||
<Typography.Text strong style={{ marginBottom: 4 }}>
|
||||
Aktueller Abschnitt (Inbox)
|
||||
</Typography.Text>
|
||||
<div style={{ flex: 1, border: '1px solid #d9d9d9', borderRadius: 4, overflow: 'hidden' }}>
|
||||
{loading || !inboxUrl ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<Spin />
|
||||
</div>
|
||||
) : (
|
||||
<iframe src={inboxUrl} title="Inbox Abschnitt" style={{ width: '100%', height: '100%', border: 0 }} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
type WizardStepStatus = 'pending' | 'processing' | 'success' | 'duplicate' | 'error' | 'skipped';
|
||||
|
||||
interface WizardStep {
|
||||
@@ -274,6 +171,10 @@ function PostprocessWizardModal({
|
||||
if (skipped) {
|
||||
setStepAt(idx, { status: 'duplicate', message: skipped.message ?? 'Duplikat erkannt', results });
|
||||
setAwaitingDecision(true);
|
||||
// Treffer in Paperless → Vergleich direkt öffnen
|
||||
if (skipped.duplicateOfDocumentId !== undefined && skipped.duplikatKandidaten?.length) {
|
||||
setCompareForSection(idx);
|
||||
}
|
||||
} else if (failed.length > 0) {
|
||||
const msg = failed.map((f) => f.message ?? f.actionType).join(', ');
|
||||
setStepAt(idx, { status: 'error', message: msg, results });
|
||||
@@ -281,6 +182,9 @@ function PostprocessWizardModal({
|
||||
} else if (results.length === 0) {
|
||||
setStepAt(idx, { status: 'error', message: 'Keine aktive Aktion konfiguriert', results });
|
||||
setAwaitingDecision(true);
|
||||
} else if (entscheidung?.aktion === 'verwerfen') {
|
||||
setStepAt(idx, { status: 'skipped', message: 'Verworfen', results });
|
||||
setCurrentSection((s) => s + 1);
|
||||
} else {
|
||||
setStepAt(idx, { status: 'success', results });
|
||||
setCurrentSection((s) => s + 1);
|
||||
@@ -303,13 +207,21 @@ function PostprocessWizardModal({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, currentSection, awaitingDecision, finished, busy, sectionCount]);
|
||||
|
||||
const handleReplace = () => {
|
||||
const handleVersion = () => {
|
||||
const documentId = steps[currentSection]?.results?.find((r) => r.versionMoeglich)?.duplicateOfDocumentId;
|
||||
if (documentId === undefined) return;
|
||||
setCompareForSection(null);
|
||||
setAwaitingDecision(false);
|
||||
void processSection(currentSection, { aktion: 'version', documentId });
|
||||
};
|
||||
|
||||
// Folgende Aktionen (z. B. Export) laufen trotzdem – nur der Paperless-Upload entfällt
|
||||
const handleVerwerfen = () => {
|
||||
setCompareForSection(null);
|
||||
setAwaitingDecision(false);
|
||||
void processSection(currentSection, { aktion: 'verwerfen' });
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
setSteps((prev) =>
|
||||
prev.map((s, i) =>
|
||||
@@ -390,16 +302,13 @@ function PostprocessWizardModal({
|
||||
<span style={{ color: '#faad14' }}>{step.message ?? 'Duplikat erkannt'}</span>
|
||||
{idx === currentSection && awaitingDecision && (
|
||||
<Space wrap>
|
||||
<Button type="primary" size="small" onClick={handleReplace}>
|
||||
Neue Version erstellen
|
||||
</Button>
|
||||
{dupDocId !== null && (
|
||||
<Button size="small" onClick={() => setCompareForSection(idx)}>
|
||||
<Button type="primary" size="small" onClick={() => setCompareForSection(idx)}>
|
||||
Vergleichen
|
||||
</Button>
|
||||
)}
|
||||
<Button size="small" onClick={handleSkip}>
|
||||
Überspringen
|
||||
<Button size="small" danger onClick={handleVerwerfen}>
|
||||
Verwerfen
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
@@ -510,23 +419,32 @@ function PostprocessWizardModal({
|
||||
) : (
|
||||
<Steps direction="vertical" current={stepperCurrent} items={allItems} />
|
||||
)}
|
||||
<CompareModal
|
||||
open={compareForSection !== null}
|
||||
paperlessDocumentId={
|
||||
compareForSection !== null
|
||||
? steps[compareForSection]?.results?.find((r) => r.duplicateOfDocumentId)?.duplicateOfDocumentId ?? null
|
||||
: null
|
||||
{compareForSection !== null && (() => {
|
||||
const dup = steps[compareForSection]?.results?.find((r) => r.skipped);
|
||||
const docId = dup?.duplicateOfDocumentId ?? null;
|
||||
const pages = documents[compareForSection]?.pages ?? [];
|
||||
const kandidaten = dup?.duplikatKandidaten ?? [];
|
||||
const pruefung: BelegnummerPruefung | null = kandidaten.length
|
||||
? {
|
||||
status: dup?.versionMoeglich ? 'eindeutig' : 'mehrdeutig',
|
||||
documentId: dup?.versionMoeglich ? docId : null,
|
||||
kandidaten,
|
||||
}
|
||||
inboxDocumentId={documentId}
|
||||
: null;
|
||||
return (
|
||||
<DuplikatVergleichModal
|
||||
open
|
||||
belegnummer={dup?.belegnummer ?? ''}
|
||||
pruefung={pruefung}
|
||||
paperlessDocumentId={docId}
|
||||
ladeNeuesPdf={() => inboxApi.downloadSegmentBlob(documentId, pages)}
|
||||
ladeKennzahlen={() => inboxApi.vergleich(documentId, docId ?? 0, pages)}
|
||||
onVersion={handleVersion}
|
||||
onVerwerfen={handleVerwerfen}
|
||||
onClose={() => setCompareForSection(null)}
|
||||
onCreateNewVersion={() => {
|
||||
// TODO: wird im nächsten Schritt implementiert
|
||||
}}
|
||||
onSkip={() => {
|
||||
setCompareForSection(null);
|
||||
handleSkip();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user