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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user