Files
paperlessmanager/paperless-frontend/src/components/DocumentEditModal.tsx
T
bjoernpoettkerandClaude Opus 5.5 e0a6598b35 refactor: Paperless-Dokumentrouten unter /api/paperless/documents/:id
„inbox“ war doppelt belegt (/api/inbox = Scanner, /api/paperless/inbox =
Paperless-Posteingang) und passte nicht zur Manuell-Seite, die dieselben
Routen nutzt. Umbenannt (ohne Alias, nur das eigene Frontend nutzt sie):

  PUT  paperless/inbox/:id            → PUT  paperless/documents/:id
  GET  paperless/inbox/pdf/:id        → GET  paperless/documents/:id/pdf
  GET  paperless/inbox/preview/:id    → GET  paperless/documents/:id/preview
  GET  paperless/inbox/:id/erechnung  → GET  paperless/documents/:id/erechnung

Dabei:
- Dokumentsuche (GET documents) wieder für den Posteingang (VIEW_INBOX)
  statt nur Admin – die Verknüpfungssuche im Bearbeiten-Dialog nutzt sie;
  die Einschränkung aus dem vorigen Commit hatte sie gebrochen.
- Suche und GET documents/:id liefern nur noch id, Titel, ASN, Datum
  (kein OCR-Volltext, keine Statusfelder).
- documents/:id/pdf auch für VIEW_SCANNER (Duplikatvergleich im Scanner).
- E-Rechnungs-Vorschlag prüft wie das Speichern pro Dokument (Tag 1 bzw.
  Manuell-Tag); Vorschau/PDF bewusst nicht (Suchergebnisse, Duplikate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:46:57 +01:00

718 lines
31 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, useCallback } from 'react';
import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider, Tag, Alert } from 'antd';
import { PlusOutlined, EyeOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { posteingangApi } from '../api/posteingang';
import type { DocumentRequirement, PosteingangDocument, Kontonummer, ERechnungVorschlag } from '../api/posteingang';
import { clientsApi } from '../api/inbox';
import type { Client } from '../api/inbox';
import { paperlessApi } from '../api/paperless';
import type { PaperlessDocType, PaperlessCorrespondent, PaperlessTag } from '../api/paperless';
import { getEnv } from '../utils/env';
import { AuthIframe, openAuthUrl } from '../utils/auth-resource';
import DocumentSearchModal from './DocumentSearchModal';
import { useIsMobile } from '../hooks/useIsMobile';
const { Option } = Select;
interface Props {
documentId: number | null;
document: PosteingangDocument | null;
open: boolean;
onClose: (next?: boolean) => void;
onSave: () => void;
isPosteingang?: boolean;
hasNextDocument?: boolean;
}
export default function DocumentEditModal({ documentId, document, open, onClose, onSave, isPosteingang = true, hasNextDocument = true }: Props) {
const [form] = Form.useForm();
const isMobile = useIsMobile();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [clients, setClients] = useState<Client[]>([]);
const [documentTypes, setDocumentTypes] = useState<PaperlessDocType[]>([]);
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
const [requirements, setRequirements] = useState<DocumentRequirement[]>([]);
// Tags: alle Tags + als Steuertags markierte IDs; bearbeitbar sind nur Inhaltstags
const [allTags, setAllTags] = useState<PaperlessTag[]>([]);
const [steuertagIds, setSteuertagIds] = useState<number[]>([]);
const contentTags = allTags.filter(t => !steuertagIds.includes(t.id));
// E-Rechnung: ausgelesener Vorschlag und die Felder, die daraus befüllt wurden
const [erechnung, setErechnung] = useState<ERechnungVorschlag | null>(null);
const [ausERechnung, setAusERechnung] = useState<{ mandant: boolean, documentType: boolean }>({ mandant: false, documentType: false });
const [kontonummerMissing, setKontonummerMissing] = useState<{ correspondentId: number, nummer: string } | null>(null);
const [docTitles, setDocTitles] = useState<Record<number, string>>({});
const [searchModalOpen, setSearchModalOpen] = useState<{ field: string, reqId: number } | null>(null);
// Kontonummer state
const [kontonummern, setKontonummern] = useState<Kontonummer[]>([]);
const [kontonummernLoading, setKontonummernLoading] = useState(false);
const [newKontonummer, setNewKontonummer] = useState('');
const selectedCorrespondent = Form.useWatch('correspondent', form);
const loadKontonummern = useCallback(async (correspondentId: number) => {
setKontonummernLoading(true);
try {
const data = await posteingangApi.getKontonummern(correspondentId);
setKontonummern(data);
} catch {
setKontonummern([]);
} finally {
setKontonummernLoading(false);
}
}, []);
useEffect(() => {
if (selectedCorrespondent) {
loadKontonummern(selectedCorrespondent);
} else {
setKontonummern([]);
}
}, [selectedCorrespondent, loadKontonummern]);
const handleAddKontonummer = async () => {
const trimmed = newKontonummer.trim();
if (!trimmed || !selectedCorrespondent) return;
try {
await posteingangApi.createKontonummer({ correspondentId: selectedCorrespondent, nummer: trimmed });
message.success(`Kontonummer "${trimmed}" hinzugefügt.`);
setNewKontonummer('');
await loadKontonummern(selectedCorrespondent);
form.setFieldValue('cf_5', trimmed);
} catch {
message.error('Kontonummer konnte nicht angelegt werden.');
}
};
useEffect(() => {
const resolveLinkTitles = async () => {
const linkFields = requirements.filter(r => r.feldTyp === 'documentlink');
for (const req of linkFields) {
const fieldName = req.isCustomField ? `cf_${req.customFieldIndex}` : req.feldId;
const val = form.getFieldValue(fieldName);
if (val && !docTitles[val]) {
try {
const d = await paperlessApi.getDocument(val);
setDocTitles(prev => ({ ...prev, [val]: d.title }));
} catch (e) {
// Handle or ignore
}
}
}
};
if (open && requirements.length > 0) {
resolveLinkTitles();
}
}, [requirements, open]); // Removed docTitles to avoid loop, we check val existence anyway
useEffect(() => {
if (open) {
loadInitialData();
} else {
form.resetFields();
setRequirements([]);
setKontonummerMissing(null);
setKontonummern([]);
setNewKontonummer('');
setErechnung(null);
setAusERechnung({ mandant: false, documentType: false });
}
}, [open]);
useEffect(() => {
if (open && document) {
let customFieldsObj: any = {};
if (document.customFields) {
document.customFields.forEach(cf => {
customFieldsObj[`cf_${cf.field}`] = cf.value;
});
}
form.setFieldsValue({
mandant: document.owner,
documentType: document.documentType,
correspondent: document.correspondent,
title: document.title,
asn: document.asn,
belegdatum: document.created ? dayjs(document.created) : null,
eingangsdatum: customFieldsObj['cf_9'] ? dayjs(customFieldsObj['cf_9']) : (document.added ? dayjs(document.added) : null),
...customFieldsObj,
});
if (document.documentType) {
fetchRequirements(document.documentType);
}
}
}, [document, form, open]);
// Tags getrennt setzen: nur Inhaltstags (Steuertags werden nicht angezeigt/bearbeitet)
useEffect(() => {
if (open && document) {
const contentTagIds = (document.tags || []).filter(id => !steuertagIds.includes(id));
form.setFieldValue('tags', contentTagIds);
}
}, [document, open, steuertagIds, form]);
// E-Rechnung auswerten und Mandant/Belegart vorschlagen. Es werden nur leere
// Felder befüllt – ein bereits gesetzter Wert stammt vom Benutzer oder einer
// Regel und wird nicht überschrieben.
useEffect(() => {
setErechnung(null);
setAusERechnung({ mandant: false, documentType: false });
if (!open || !isPosteingang || !documentId) return;
let abgebrochen = false;
(async () => {
try {
const vorschlag = await posteingangApi.getErechnung(documentId);
if (abgebrochen || !vorschlag.erechnung) return;
setErechnung(vorschlag);
const uebernommen = { mandant: false, documentType: false };
if (vorschlag.mandantId && !form.getFieldValue('mandant')) {
form.setFieldValue('mandant', vorschlag.mandantId);
uebernommen.mandant = true;
}
if (vorschlag.belegartId && !form.getFieldValue('documentType')) {
form.setFieldValue('documentType', vorschlag.belegartId);
uebernommen.documentType = true;
fetchRequirements(vorschlag.belegartId);
}
setAusERechnung(uebernommen);
} catch {
// Die Auswertung ist eine Hilfestellung – ein Fehler darf das
// Bearbeiten nicht stören und wird deshalb nicht gemeldet.
}
})();
return () => { abgebrochen = true; };
}, [open, isPosteingang, documentId]);
const ensureCorrespondentInList = async (correspondentId: number | null | undefined) => {
if (!correspondentId) return;
setCorrespondents(prev => {
const exists = prev.some(c => Number(c.id) === Number(correspondentId));
if (!exists) {
// We need to fetch it. But we can't easily do it inside setCorrespondents.
// So we'll do it outside.
return prev;
}
return prev;
});
const exists = correspondents.some(c => Number(c.id) === Number(correspondentId));
if (!exists) {
try {
const currentCorr = await paperlessApi.getCorrespondent(correspondentId);
if (currentCorr) {
setCorrespondents(prev => {
if (prev.some(c => Number(c.id) === Number(currentCorr.id))) return prev;
return [...prev, currentCorr];
});
}
} catch (e) {
console.error("Fehler beim Laden des zugewiesenen Absenders:", e);
}
}
};
useEffect(() => {
if (open && document?.correspondent) {
ensureCorrespondentInList(document.correspondent);
}
}, [document?.correspondent, open]);
const loadInitialData = async () => {
setLoading(true);
try {
const [clientsData, docTypesData, correspondentsData, tagsData, steuertagData] = await Promise.all([
clientsApi.getMyClients(),
paperlessApi.getDocumentTypes(),
paperlessApi.getCorrespondents(),
paperlessApi.getTags(),
paperlessApi.getSteuertagIds(),
]);
setClients(clientsData);
setDocumentTypes(docTypesData);
setCorrespondents(correspondentsData);
setAllTags(tagsData);
setSteuertagIds(steuertagData);
// If document is already there, ensure its correspondent is in the list
if (document?.correspondent) {
const exists = correspondentsData.some(c => Number(c.id) === Number(document.correspondent));
if (!exists) {
const currentCorr = await paperlessApi.getCorrespondent(document.correspondent);
if (currentCorr) {
setCorrespondents([...correspondentsData, currentCorr]);
}
}
}
} catch (e) {
message.error("Fehler beim Laden der Stammdaten.");
} finally {
setLoading(false);
}
};
const fetchRequirements = async (docType: number) => {
try {
const reqs = await posteingangApi.getRequirements(docType, isPosteingang);
setRequirements(reqs);
} catch (e) {
message.error("Fehler beim Laden der Pflichtfelder.");
}
};
const [fetchingCorrespondents, setFetchingCorrespondents] = useState(false);
const [searchTimeout, setSearchTimeout] = useState<any>(null);
const handleCorrespondentSearch = async (value: string) => {
if (searchTimeout) clearTimeout(searchTimeout);
setSearchTimeout(setTimeout(async () => {
setFetchingCorrespondents(true);
try {
const data = await paperlessApi.getCorrespondents(value);
setCorrespondents(data);
} catch (e) {
message.error("Absender konnten nicht geladen werden.");
} finally {
setFetchingCorrespondents(false);
}
}, 500));
};
const handleDocumentTypeChange = (value: number) => {
setAusERechnung(prev => ({ ...prev, documentType: false }));
fetchRequirements(value);
};
/** Kurzbeschreibung der erkannten Rechnung zur Sichtkontrolle. */
const beschreibeRechnung = (v: ERechnungVorschlag) => {
const r = v.rechnung;
const teile: string[] = [];
if (r?.verkaeufer) teile.push(r.verkaeufer);
if (r?.rechnungsnummer) teile.push(`Nr. ${r.rechnungsnummer}`);
if (r?.rechnungsdatum) teile.push(dayjs(r.rechnungsdatum).format('DD.MM.YYYY'));
if (r?.gesamtbetrag !== null && r?.gesamtbetrag !== undefined) {
const waehrung = /^[A-Z]{3}$/.test(r.waehrung || '') ? r.waehrung! : 'EUR';
teile.push(new Intl.NumberFormat('de-DE', { style: 'currency', currency: waehrung }).format(r.gesamtbetrag));
}
return teile.length ? teile.join(' \u00b7 ') : 'Keine weiteren Angaben im Rechnungs-XML.';
};
/** Label mit Chip, solange der Wert aus der E-Rechnung stammt. */
const labelMitHerkunft = (text: string, ausRechnung: boolean) =>
ausRechnung ? (
<Space size={4}>
{text}
<Tag color="blue" style={{ marginInlineEnd: 0 }}>aus E-Rechnung</Tag>
</Space>
) : text;
const handleSaveDocument = async (values: any, isNext: boolean = false) => {
// Collect custom fields into an array
const customFieldsObj: any = {};
Object.keys(values).forEach(key => {
if (key.startsWith('cf_')) {
const fieldId = parseInt(key.replace('cf_', ''), 10);
customFieldsObj[fieldId] = values[key];
}
});
if (values.eingangsdatum) {
customFieldsObj[9] = values.eingangsdatum.format('YYYY-MM-DD');
} else {
customFieldsObj[9] = null;
}
const payload = {
mandant: values.mandant,
documentType: values.documentType,
correspondent: values.correspondent,
title: values.title,
date: values.belegdatum ? values.belegdatum.format('YYYY-MM-DD') : null,
tags: values.tags || [],
customFields: customFieldsObj,
// Signal fürs Backend, die Käufer-Kennungen auf den gespeicherten
// Mandanten fortzuschreiben.
erechnung: erechnung?.erechnung === true,
};
setSaving(true);
try {
await posteingangApi.updateDocument(documentId!, payload);
message.success("Dokument erfolgreich aktualisiert.");
onSave();
onClose(isNext);
} catch (e) {
message.error("Fehler beim Speichern des Dokuments.");
} finally {
setSaving(false);
}
};
const runSave = async (values: any, isNext: boolean) => {
// Kontonummer-Logik (unverändert): neue Kontonummer ggf. bestätigen lassen.
const kontonummerReq = requirements.find(r => r.customFieldIndex === 5);
if (kontonummerReq && values['cf_5'] && values.correspondent) {
const kNummer = values['cf_5'];
const knData = await posteingangApi.getKontonummern(values.correspondent);
const exists = knData.some(k => k.Nummer === kNummer);
if (!exists) {
// Prompt user to save kontonummer
setKontonummerMissing({ correspondentId: values.correspondent, nummer: kNummer });
return; // Stop saving, wait for confirmation
}
}
await handleSaveDocument(values, isNext);
};
const handleSubmit = async (isNext: boolean = false) => {
try {
const values = await form.validateFields();
await runSave(values, isNext);
} catch (e) {
const errorFields =
(e as { errorFields?: { name: unknown; errors: string[] }[] })?.errorFields ?? [];
// Kein Validierungsfehler, oder Posteingang → bisheriges Verhalten (stiller Abbruch).
if (errorFields.length === 0 || isPosteingang) {
console.error("Form validation failed:", e);
return;
}
// "Manuell bearbeiten": Speichern trotz fehlender Pflichtfelder mit Hinweis anbieten.
const missing = Array.from(new Set(errorFields.flatMap((f) => f.errors)));
Modal.confirm({
title: 'Pflichtfelder unvollständig',
okText: 'Trotzdem speichern',
cancelText: 'Abbrechen',
content: (
<div>
<p>Nicht alle Pflichtfelder sind ausgefüllt:</p>
<ul>{missing.map((m, i) => <li key={i}>{m}</li>)}</ul>
<p>Möchtest du trotzdem speichern?</p>
</div>
),
onOk: () => runSave(form.getFieldsValue(), isNext),
});
}
};
const confirmKontonummerSave = async (shouldSave: boolean, isNext: boolean = false) => {
if (shouldSave && kontonummerMissing) {
try {
await posteingangApi.createKontonummer(kontonummerMissing);
message.success("Kontonummer hinterlegt.");
} catch (e) {
message.error("Fehler beim Speichern der Kontonummer.");
}
}
setKontonummerMissing(null);
const values = form.getFieldsValue();
await handleSaveDocument(values, isNext);
};
const handleSelectLink = (doc: any) => {
if (searchModalOpen) {
form.setFieldValue(searchModalOpen.field, doc.id);
setDocTitles(prev => ({ ...prev, [doc.id]: doc.title }));
setSearchModalOpen(null);
form.validateFields([searchModalOpen.field]);
}
};
if (documentId === null) return null;
return (
<>
<Modal
title={`Dokument bearbeiten (${document?.title || ''})`}
open={open && !kontonummerMissing}
onCancel={() => onClose(false)}
width={{ xs: '100vw', md: 900, xl: 1400 }}
style={isMobile ? {} : { top: 20 }}
footer={
hasNextDocument ? [
<Button key="cancel" onClick={() => onClose(false)}>Abbrechen</Button>,
<Button key="save" type="primary" loading={saving} onClick={() => handleSubmit(false)}>Speichern</Button>,
<Button key="saveNext" type="primary" ghost loading={saving} onClick={() => handleSubmit(true)}>Speichern & Nächstes</Button>
] : [
<Button key="cancel" onClick={() => onClose(false)}>Abbrechen</Button>,
<Button key="save" type="primary" loading={saving} onClick={() => handleSubmit(false)}>Speichern</Button>
]
}
>
<Spin spinning={loading}>
<Row gutter={[16, 16]} style={isMobile ? {} : { height: '75vh', overflow: 'hidden' }}>
<Col
xs={24}
lg={10}
style={isMobile ? {} : { overflowY: 'auto', paddingRight: '1rem', borderRight: '1px solid #f0f0f0' }}
>
{erechnung?.erechnung && (
<Alert
type={erechnung.hinweis ? 'warning' : 'info'}
showIcon
style={{ marginBottom: 16 }}
message="E-Rechnung erkannt"
description={
<>
<div>{beschreibeRechnung(erechnung)}</div>
{erechnung.hinweis && <div style={{ marginTop: 4 }}>{erechnung.hinweis}</div>}
</>
}
/>
)}
<Form form={form} layout="vertical" disabled={saving}>
<Form.Item name="mandant" label={labelMitHerkunft('Mandant', ausERechnung.mandant)} rules={[{ required: true, message: 'Wähle einen Mandanten' }]}>
<Select
showSearch
optionFilterProp="children"
allowClear
onChange={() => setAusERechnung(prev => ({ ...prev, mandant: false }))}
>
{clients.map(c => (
<Option key={c.Id} value={c.PaperlessUserId}>{c.Name}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="documentType" label={labelMitHerkunft('Dokumentart', ausERechnung.documentType)} rules={[{ required: true, message: 'Wähle eine Dokumentart' }]}>
<Select showSearch optionFilterProp="children" onChange={handleDocumentTypeChange}>
{documentTypes.map(d => (
<Option key={d.id} value={d.id}>{d.name}</Option>
))}
</Select>
</Form.Item>
<Form.Item name="belegdatum" label="Belegdatum">
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
</Form.Item>
<Form.Item name="eingangsdatum" label="Eingangsdatum">
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
</Form.Item>
<Form.Item name="tags" label="Tags">
<Select
mode="multiple"
allowClear
showSearch
optionFilterProp="label"
placeholder="Tags auswählen"
options={contentTags.map(t => ({ value: t.id, label: t.name }))}
tagRender={({ value, closable, onClose }) => {
const tag = allTags.find(t => t.id === value);
return (
<Tag
color={tag?.color}
style={{ color: tag?.text_color, marginInlineEnd: 4 }}
closable={closable}
onClose={onClose}
>
{tag?.name ?? value}
</Tag>
);
}}
optionRender={(opt) => {
const tag = contentTags.find(t => t.id === opt.value);
return tag ? (
<Tag color={tag.color} style={{ color: tag.text_color }}>{tag.name}</Tag>
) : opt.label;
}}
/>
</Form.Item>
{/* Rendering dynamic requirements based on DocumentType */}
{requirements.map(req => {
if (req.feldId === '2' || (req.isCustomField && req.customFieldIndex === 9)) {
return null;
}
let inputElement = <Input />;
if (req.isCustomField && req.customFieldIndex === 5) {
// Kontonummer — Select mit Möglichkeit neue anzulegen
inputElement = (
<Select
showSearch
allowClear
optionFilterProp="children"
placeholder={!selectedCorrespondent ? 'Bitte zuerst einen Absender wählen' : 'Kontonummer wählen'}
disabled={!selectedCorrespondent}
loading={kontonummernLoading}
notFoundContent={kontonummernLoading ? <Spin size="small" /> : 'Keine Kontonummern vorhanden'}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: '8px 0' }} />
<div style={{ display: 'flex', gap: 8, padding: '0 8px 8px' }}>
<Input
placeholder="Neue Kontonummer"
value={newKontonummer}
onChange={(e) => setNewKontonummer(e.target.value)}
onKeyDown={(e) => e.stopPropagation()}
/>
<Button
type="text"
icon={<PlusOutlined />}
onClick={handleAddKontonummer}
disabled={!newKontonummer.trim()}
>
Anlegen
</Button>
</div>
</>
)}
>
{kontonummern.map((k) => (
<Option key={k.KontonummerId ?? k.Nummer} value={k.Nummer}>
{k.Nummer}
</Option>
))}
</Select>
);
} else if (req.feldId === '1') {
inputElement = (
<Select
showSearch
filterOption={false}
onSearch={handleCorrespondentSearch}
notFoundContent={fetchingCorrespondents ? <Spin size="small" /> : null}
>
{correspondents.map(c => (
<Option key={c.id} value={c.id}>{c.name}</Option>
))}
</Select>
);
} else if (req.feldTyp === 'date') {
inputElement = <DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />;
} else if (req.feldTyp === 'select' && req.fieldOptions) {
inputElement = (
<Select showSearch optionFilterProp="children">
{req.fieldOptions.map(opt => (
<Option key={opt.id} value={opt.id}>{opt.label}</Option>
))}
</Select>
);
} else if (req.feldTyp === 'documentlink') {
const fieldName = req.isCustomField ? `cf_${req.customFieldIndex}` :
req.feldId === '1' ? 'correspondent' :
req.feldId === '2' ? 'belegdatum' :
req.feldId === '3' ? 'asn' :
req.feldId === '5' ? 'title' : req.feldId;
const currentId = form.getFieldValue(fieldName);
inputElement = (
<Input
readOnly
placeholder="Klicken zum Suchen..."
value={currentId ? (docTitles[currentId] || `Dokument #${currentId}`) : ''}
onClick={() => setSearchModalOpen({ field: fieldName, reqId: req.id })}
suffix={
<Space>
{currentId && (
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
openAuthUrl(`${getEnv('VITE_API_URL')}/api/paperless/documents/${currentId}/pdf`);
}}
/>
)}
<Button
type="text"
size="small"
icon={<SearchOutlined />}
onClick={(e) => {
e.stopPropagation();
setSearchModalOpen({ field: fieldName, reqId: req.id });
}}
/>
</Space>
}
/>
);
}
const name = req.isCustomField ? `cf_${req.customFieldIndex}` :
req.feldId === '1' ? 'correspondent' :
req.feldId === '2' ? 'belegdatum' :
req.feldId === '3' ? 'asn' :
req.feldId === '5' ? 'title' : req.feldId;
return (
<Form.Item
key={req.id}
name={name}
label={req.feldName}
rules={[{ required: req.required, message: `${req.feldName} ist ein Pflichtfeld` }]}
tooltip={req.hinweis}
>
{inputElement}
</Form.Item>
);
})}
</Form>
</Col>
<Col xs={24} lg={14} style={isMobile ? {} : { height: '100%' }}>
{isMobile && (
<Button
icon={<ExportOutlined />}
block
style={{ marginBottom: 8 }}
onClick={() => openAuthUrl(`${getEnv('VITE_API_URL')}/api/paperless/documents/${documentId}/pdf`)}
>
PDF in neuem Tab öffnen
</Button>
)}
<AuthIframe
src={`${getEnv('VITE_API_URL')}/api/paperless/documents/${documentId}/pdf#toolbar=0`}
style={{ width: '100%', height: isMobile ? '50vh' : '100%' }}
title="PDF Preview"
/>
</Col>
</Row>
</Spin>
</Modal>
{/* Kontonummer Dialog */}
<Modal
title="Neue Kontonummer hinterlegen?"
open={!!kontonummerMissing}
onCancel={() => confirmKontonummerSave(false)}
onOk={() => confirmKontonummerSave(true)}
okText="Hinterlegen"
cancelText="Nein, nur Speichern"
>
<p>Die Kontonummer <b>{kontonummerMissing?.nummer}</b> ist bei diesem Absender noch nicht hinterlegt.</p>
<p>Möchtest du sie dem Absender fest zuordnen?</p>
</Modal>
<DocumentSearchModal
open={!!searchModalOpen}
onCancel={() => setSearchModalOpen(null)}
onSelect={handleSelectLink}
/>
</>
);
}