feat(frontend): Vergleichsdialog für Duplikate mit neuer Version
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
21a447e915
commit
fffbf692b2
5 files changed
+344
-131
No files matched your search
@@ -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
|
||||
}
|
||||
inboxDocumentId={documentId}
|
||||
onClose={() => setCompareForSection(null)}
|
||||
onCreateNewVersion={() => {
|
||||
// TODO: wird im nächsten Schritt implementiert
|
||||
}}
|
||||
onSkip={() => {
|
||||
setCompareForSection(null);
|
||||
handleSkip();
|
||||
}}
|
||||
/>
|
||||
{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,
|
||||
}
|
||||
: 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)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user