„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>
718 lines
31 KiB
TypeScript
718 lines
31 KiB
TypeScript
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}
|
||
/>
|
||
</>
|
||
);
|
||
}
|