199 lines
7.1 KiB
TypeScript
199 lines
7.1 KiB
TypeScript
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>
|
||
);
|
||
}
|