Files
paperlessmanager/paperless-frontend/src/components/DuplikatVergleichModal.tsx
T

199 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
/** Länger wird auf Seiten-/Wortzahl nicht gewartet – der Vergleich bleibt trotzdem möglich. */
const KENNZAHLEN_TIMEOUT_MS = 30000;
function mitTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('Zeitüberschreitung')), ms);
promise.then(
(wert) => {
clearTimeout(timer);
resolve(wert);
},
(err: unknown) => {
clearTimeout(timer);
reject(err instanceof Error ? err : new Error(String(err)));
},
);
});
}
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 [kennzahlenFehler, setKennzahlenFehler] = useState(false);
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);
setKennzahlenFehler(false);
(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);
}
})();
mitTimeout(ladeKennzahlen(), KENNZAHLEN_TIMEOUT_MS)
.then((k) => {
if (!cancelled) setKennzahlen(k);
})
.catch(() => {
if (!cancelled) setKennzahlenFehler(true);
});
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 (kennzahlenFehler) {
auffaelligkeiten = (
<Alert type="warning" showIcon message="Seiten- und Wortzahl konnten nicht ermittelt werden – bitte die PDFs direkt vergleichen." />
);
} else 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>
);
}