import { useEffect, useState, useCallback } from 'react';
import dayjs from 'dayjs';
import {
Tabs, Typography, Table, Button, Modal, Form, Input, Select,
Switch, Checkbox, Popconfirm, message, Card, Tag, Space, Divider, InputNumber, Badge,
} from 'antd';
import {
UserOutlined, FileTextOutlined, ThunderboltOutlined,
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
QrcodeOutlined, UnorderedListOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import {
settingsApi,
type SettingDocType, type SettingPostprocessing, type SettingUserClient,
type SettingDocField, type SettingPostprocessingAction, type SettingExportTarget,
type SettingPostprocessingLog, type FilterGroup, type FilterCondition,
INBOX_ACTION_LABELS,
type InboxAction, type InboxActionType,
} from '../api/settings';
import { clientsApi, type Client } from '../api/inbox';
import { apiKeysApi, type ApiKey } from '../api/api-keys';
import {
barcodeTemplatesApi,
type BarcodeTemplate,
} from '../api/barcode-templates';
import {
paperlessApi, type PaperlessTag, type PaperlessDocType,
type PaperlessCustomField, type PaperlessCorrespondent,
} from '../api/paperless';
import TaskLogPage from './TaskLogPage';
const { Title } = Typography;
// ═══════════════════════════════════════════════════════════════════
// Filter Builder Component
// ═══════════════════════════════════════════════════════════════════
const FIELD_OPTIONS = [
{ value: 'document_type', label: 'Dokumenttyp' },
{ value: 'correspondent', label: 'Absender' },
{ value: 'owner', label: 'Eigentümer' },
{ value: 'tag', label: 'Tag' },
{ value: 'title', label: 'Titel' },
{ value: 'archive_serial_number', label: 'Ablagenummer' },
];
const OPERATOR_OPTIONS = [
{ value: 'equals', label: '=' },
{ value: 'not_equals', label: '≠' },
{ value: 'contains', label: 'enthält' },
{ value: 'not_contains', label: 'enthält nicht' },
{ value: 'is_set', label: 'ist gesetzt' },
{ value: 'is_not_set', label: 'ist nicht gesetzt' },
{ value: 'gt', label: '>' },
{ value: 'lt', label: '<' },
];
function isFilterGroup(rule: FilterCondition | FilterGroup): rule is FilterGroup {
return 'combinator' in rule;
}
interface FilterBuilderProps {
value: FilterGroup;
onChange: (val: FilterGroup) => void;
tags: PaperlessTag[];
docTypes: PaperlessDocType[];
correspondents: PaperlessCorrespondent[];
customFields: PaperlessCustomField[];
}
function FilterBuilder({ value, onChange, tags, docTypes, correspondents, customFields }: FilterBuilderProps) {
const allFieldOptions = [
...FIELD_OPTIONS,
...customFields.map(cf => ({ value: `custom_field_${cf.id}`, label: `CF: ${cf.name}` })),
];
const updateRule = (index: number, updated: FilterCondition | FilterGroup) => {
const newRules = [...value.rules];
newRules[index] = updated;
onChange({ ...value, rules: newRules });
};
const removeRule = (index: number) => {
const newRules = value.rules.filter((_, i) => i !== index);
onChange({ ...value, rules: newRules });
};
const addCondition = () => {
onChange({
...value,
rules: [...value.rules, { field: 'document_type', operator: 'equals', value: null }],
});
};
const addGroup = () => {
onChange({
...value,
rules: [...value.rules, { combinator: 'AND', rules: [] }],
});
};
const renderValueInput = (cond: FilterCondition, onUpdate: (val: any) => void) => {
if (cond.operator === 'is_set' || cond.operator === 'is_not_set') return null;
if (cond.field === 'document_type') {
return (
{(docTypes || []).map(d => {d.name} )}
);
}
if (cond.field === 'correspondent') {
return (
{(correspondents || []).map(c => {c.name} )}
);
}
if (cond.field === 'tag') {
return (
{(tags || []).map(t => {t.name} )}
);
}
if (cond.field.startsWith('custom_field_')) {
const cfId = parseInt(cond.field.replace('custom_field_', ''), 10);
const cf = (customFields || []).find(c => c.id === cfId);
const selectOptions = cf?.extra_data?.select_options;
if (Array.isArray(selectOptions)) {
return (
{selectOptions.filter(opt => opt && (opt.id || opt.label)).map((opt: any) => (
{opt.label}
))}
);
}
}
return onUpdate(e.target.value)} style={{ minWidth: 160 }} placeholder="Wert" />;
};
return (
onChange({ ...value, combinator: c })}
style={{ width: 80 }}
>
UND
ODER
Alle Bedingungen dieser Gruppe müssen zutreffen
{value.rules.map((rule, idx) => (
{isFilterGroup(rule) ? (
updateRule(idx, updated)}
tags={tags} docTypes={docTypes} correspondents={correspondents} customFields={customFields}
/>
} onClick={() => removeRule(idx)} size="small">
Gruppe entfernen
) : (
updateRule(idx, { ...rule, field: f, value: null })}
style={{ minWidth: 160 }}
showSearch optionFilterProp="children"
>
{allFieldOptions.map(o => {o.label} )}
updateRule(idx, { ...rule, operator: op })}
style={{ width: 140 }}
>
{OPERATOR_OPTIONS.map(o => {o.label} )}
{renderValueInput(rule, val => updateRule(idx, { ...rule, value: val }))}
} onClick={() => removeRule(idx)} />
)}
))}
} onClick={addCondition}>Bedingung
} onClick={addGroup}>Gruppe
);
}
// ═══════════════════════════════════════════════════════════════════
// Benutzer & Betriebe Tab
// ═══════════════════════════════════════════════════════════════════
function UserClientsTab() {
const [data, setData] = useState([]);
const [clients, setClients] = useState([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
const [ucs, cls] = await Promise.all([
settingsApi.getUserClients(),
clientsApi.getMyClients(),
]);
setData(ucs);
setClients(cls);
} finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const handleAdd = async () => {
const values = await form.validateFields();
await settingsApi.createUserClient(values);
message.success('Zuordnung erstellt');
setModalOpen(false);
form.resetFields();
load();
};
const handleDelete = async (id: number) => {
await settingsApi.deleteUserClient(id);
message.success('Gelöscht');
load();
};
const columns: ColumnsType = [
{ title: 'User ID', dataIndex: 'UserId', key: 'userId' },
{
title: 'Betrieb',
key: 'client',
render: (_, r) => clients.find(c => c.Id === r.ClientId)?.Name ?? r.ClientId,
},
{
title: 'Rolle',
dataIndex: 'Role',
key: 'role',
render: (r: string) => {r} ,
},
{
title: '',
key: 'actions',
width: 80,
render: (_, record) => (
handleDelete(record.Id)}>
} size="small" />
),
},
];
return (
<>
} onClick={() => setModalOpen(true)} style={{ marginBottom: 16 }}>
Zuordnung hinzufügen
setModalOpen(false)}>
{clients.map(c => {c.Name} )}
Viewer
Editor
Admin
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Document Fields Tab
// ═══════════════════════════════════════════════════════════════════
const FIELD_TYPE_OPTIONS = [
{ value: 1, label: 'Absender' },
{ value: 2, label: 'Belegdatum' },
{ value: 3, label: 'Ablagenummer' },
{ value: 4, label: 'Custom Field' },
{ value: 5, label: 'Titel' },
];
function DocTypeFieldsTable({ docTypeId }: { docTypeId: number }) {
const [fields, setFields] = useState([]);
const [customFields, setCustomFields] = useState([]);
const [loading, setLoading] = useState(true);
const [editField, setEditField] = useState(null);
const [isNew, setIsNew] = useState(false);
const [form] = Form.useForm();
const [fieldType, setFieldType] = useState(4);
const load = useCallback(async () => {
setLoading(true);
try {
const [f, cf] = await Promise.all([
settingsApi.getDocFields(docTypeId),
paperlessApi.getCustomFields(),
]);
setFields(f);
setCustomFields(cf);
} finally { setLoading(false); }
}, [docTypeId]);
useEffect(() => { load(); }, [load]);
const openNew = () => {
setIsNew(true);
setFieldType(4);
form.resetFields();
form.setFieldsValue({ Type: 4, TypeIndex: null, IsRequired: false, IsRequiredPosteingang: false, VisiblePosteingang: true, Hinweis: '' });
setEditField({} as any);
};
const openEdit = (f: SettingDocField) => {
setIsNew(false);
setFieldType(f.Type);
form.setFieldsValue(f);
setEditField(f);
};
const handleSave = async () => {
const values = await form.validateFields();
// For non-custom-field types, TypeIndex is not relevant
if (values.Type !== 4) values.TypeIndex = null;
if (isNew) {
await settingsApi.createDocField(docTypeId, values);
message.success('Feld erstellt');
} else if (editField) {
await settingsApi.updateDocField(editField.Id, values);
message.success('Feld gespeichert');
}
setEditField(null);
load();
};
const handleDelete = async (id: number) => {
await settingsApi.deleteDocField(id);
message.success('Feld gelöscht');
load();
};
const handleToggle = async (id: number, key: string, val: boolean) => {
await settingsApi.updateDocField(id, { [key]: val } as any);
load();
};
const getFieldLabel = (record: SettingDocField) => {
switch (record.Type) {
case 1: return Absender ;
case 2: return Belegdatum ;
case 3: return Ablagenummer ;
case 4:
const cf = customFields.find(c => c.id === record.TypeIndex);
return {cf ? cf.name : `CF #${record.TypeIndex}`} ;
case 5: return Titel ;
default: return Unbekannt ({record.Type}) ;
}
};
const columns: ColumnsType = [
{ title: 'Feldtyp', key: 'type', render: (_, record) => getFieldLabel(record) },
{
title: 'Pflichtfeld', dataIndex: 'IsRequired', key: 'req', width: 100,
render: (v, r) => handleToggle(r.Id, 'IsRequired', val)} />,
},
{
title: 'Pflicht (Posteingang)', dataIndex: 'IsRequiredPosteingang', key: 'reqPE', width: 140,
render: (v, r) => handleToggle(r.Id, 'IsRequiredPosteingang', val)} />,
},
{
title: 'Sichtbar (Posteingang)', dataIndex: 'VisiblePosteingang', key: 'visPE', width: 150,
render: (v, r) => handleToggle(r.Id, 'VisiblePosteingang', val)} />,
},
{ title: 'Hinweis', dataIndex: 'Hinweis', key: 'hinweis', ellipsis: true },
{
title: '', key: 'actions', width: 100,
render: (_, rec) => (
} size="small" onClick={() => openEdit(rec)} />
handleDelete(rec.Id)}>
} size="small" />
),
},
];
return (
<>
} onClick={openNew} style={{ marginBottom: 8 }}>
Feld hinzufügen
setEditField(null)}
width={500}
>
setFieldType(v)}>
{FIELD_TYPE_OPTIONS.map(o => (
{o.label}
))}
{fieldType === 4 && (
{customFields.map(cf => (
{cf.name}
))}
)}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Dokumenttypen Tab
// ═══════════════════════════════════════════════════════════════════
function DocTypesTab() {
const [data, setData] = useState([]);
const [tags, setTags] = useState([]);
const [docTypes, setDocTypes] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null);
const [form] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
const [docs, fetchedTags, fetchedDocTypes] = await Promise.all([
settingsApi.getDocTypes(),
paperlessApi.getTags(),
paperlessApi.getDocumentTypes()
]);
setData(docs);
setTags(fetchedTags);
setDocTypes(fetchedDocTypes);
}
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const handleSave = async () => {
if (!editing) return;
const values = await form.validateFields();
await settingsApi.updateDocType(editing.Id, values);
message.success('Gespeichert');
setEditing(null);
load();
};
const getTagName = (id: number | null) => {
if (!id) return '—';
const tag = tags.find(t => t.id === id);
return tag ? {tag.name} : id;
};
const getDocTypeName = (id: number) => {
const dt = docTypes.find(d => d.id === id);
return dt ? dt.name : id;
};
const columns: ColumnsType = [
{ title: 'Dokumenttyp', dataIndex: 'DocumentTypeId', key: 'id', render: getDocTypeName },
{ title: 'Titel-Template', dataIndex: 'TitelTemplate', key: 'template', ellipsis: true },
{ title: 'Tag (nicht bereit)', dataIndex: 'TagNotReady', key: 'tagNR', render: getTagName },
{ title: 'Tag (bereit)', dataIndex: 'TagReady', key: 'tagR', render: getTagName },
{
title: '',
key: 'actions',
width: 60,
render: (_, rec) => (
} size="small" onClick={() => { setEditing(rec); form.setFieldsValue(rec); }} />
),
},
];
return (
<>
setEditing(null)}
width={900}
>
{tags.map(t => {t.name} )}
{tags.map(t => {t.name} )}
{editing && (
Dokumentenfelder
)}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Action Editor (Sub-component for Postprocessing)
// ═══════════════════════════════════════════════════════════════════
const ACTION_TYPE_LABELS: Record = {
1: 'Export (FTP/WebDAV)',
2: 'Mail versenden',
3: 'Tags setzen/entfernen',
4: 'Custom Field setzen',
5: 'Webhook',
6: 'Notiz hinzufügen',
};
function ActionEditor({
ppId,
tags,
customFields,
exportTargets,
}: {
ppId: number;
tags: PaperlessTag[];
customFields: PaperlessCustomField[];
exportTargets: SettingExportTarget[];
}) {
const [actions, setActions] = useState([]);
const [loading, setLoading] = useState(true);
const [editAction, setEditAction] = useState(null);
const [isNew, setIsNew] = useState(false);
const [form] = Form.useForm();
const [actionType, setActionType] = useState(1);
const load = useCallback(async () => {
setLoading(true);
try { setActions(await settingsApi.getActions(ppId)); }
finally { setLoading(false); }
}, [ppId]);
useEffect(() => { load(); }, [load]);
const openNew = () => {
setIsNew(true);
setActionType(1);
form.resetFields();
form.setFieldsValue({ ActionType: 1, IsActive: true, Order: actions.length + 1, Content: {} });
setEditAction({} as any);
};
const openEdit = (a: SettingPostprocessingAction) => {
setIsNew(false);
setActionType(a.ActionType);
form.setFieldsValue({ ActionType: a.ActionType, IsActive: a.IsActive, Order: a.Order, ...a.Content });
setEditAction(a);
};
const buildContent = (values: any): Record => {
const { ActionType: _, IsActive: __, Order: ___, ...rest } = values;
return rest;
};
const handleSave = async () => {
const values = await form.validateFields();
const content = buildContent(values);
if (isNew) {
await settingsApi.createAction(ppId, {
ActionType: values.ActionType,
Content: content,
Order: values.Order,
IsActive: values.IsActive,
});
message.success('Aktion erstellt');
} else if (editAction) {
await settingsApi.updateAction(editAction.Id, {
ActionType: values.ActionType,
Content: content,
Order: values.Order,
IsActive: values.IsActive,
});
message.success('Aktion gespeichert');
}
setEditAction(null);
load();
};
const handleDelete = async (id: number) => {
await settingsApi.deleteAction(id);
message.success('Gelöscht');
load();
};
const columns: ColumnsType = [
{ title: '#', dataIndex: 'Order', key: 'order', width: 50 },
{ title: 'Typ', key: 'type', render: (_, r) => {ACTION_TYPE_LABELS[r.ActionType] || r.ActionType} },
{ title: 'Aktiv', key: 'active', width: 60, render: (_, r) => },
{
title: '', key: 'actions', width: 100,
render: (_, r) => (
} size="small" onClick={() => openEdit(r)} />
handleDelete(r.Id)}>
} size="small" />
),
},
];
const renderContentFields = () => {
switch (actionType) {
case 1: // Export
return (
<>
{exportTargets.filter(t => t.IsActive).map(t => (
{t.Name} ({t.Protocol})
))}
Archiv (OCR)
Original
Verfügbare Platzhalter: {'{titel}'} {'{absender_name}'} {'{dokumenttyp_name}'} {'{datum}'} {'{jahr}'} {'{monat}'} {'{tag}'} {'{id}'} {'{ablagenummer}'} {'{besitzer}'} {'{custom_field_}'}
>
);
case 2: // Mail
return (
<>
Archiv (OCR)
Original
>
);
case 3: // Tags
return (
<>
{tags.map(t => {t.name} )}
{tags.map(t => {t.name} )}
>
);
case 4: // Custom Field
return (
<>
{customFields.map(cf => {cf.name} )}
>
);
case 5: // Webhook
return (
<>
GET
POST
PUT
>
);
case 6: // Note
return (
<>
>
);
default: return null;
}
};
return (
Aktionen
} onClick={openNew}>Aktion hinzufügen
setEditAction(null)}
width={600}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Postprocessing Tab
// ═══════════════════════════════════════════════════════════════════
function PostprocessingTab() {
const [data, setData] = useState([]);
const [tags, setTags] = useState([]);
const [docTypes, setDocTypes] = useState([]);
const [correspondents, setCorrespondents] = useState([]);
const [customFields, setCustomFields] = useState([]);
const [exportTargets, setExportTargets] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null);
const [isNew, setIsNew] = useState(false);
const [form] = Form.useForm();
const [filterJson, setFilterJson] = useState({ combinator: 'AND', rules: [] });
const load = useCallback(async () => {
setLoading(true);
try {
const [pp, t, dt, c, cf, et] = await Promise.all([
settingsApi.getPostprocessing(),
paperlessApi.getTags(),
paperlessApi.getDocumentTypes(),
paperlessApi.getCorrespondents(),
paperlessApi.getCustomFields(),
settingsApi.getExportTargets(),
]);
setData(pp);
setTags(t);
setDocTypes(dt);
setCorrespondents(c);
setCustomFields(cf);
setExportTargets(et);
} finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const openNew = () => {
setIsNew(true);
const emptyFilter: FilterGroup = { combinator: 'AND', rules: [] };
setFilterJson(emptyFilter);
form.resetFields();
form.setFieldsValue({ Name: '', IsActive: true, NoFurther: false, Order: data.length + 1 });
setEditing({} as any);
};
const openEdit = (pp: SettingPostprocessing) => {
setIsNew(false);
setFilterJson(pp.FilterJson || { combinator: 'AND', rules: [] });
form.setFieldsValue({ Name: pp.Name, IsActive: pp.IsActive, NoFurther: pp.NoFurther, Order: pp.Order });
setEditing(pp);
};
const handleSave = async () => {
const values = await form.validateFields();
const payload = { ...values, FilterJson: filterJson };
if (isNew) {
await settingsApi.createPostprocessing(payload);
message.success('Regel erstellt');
} else if (editing) {
await settingsApi.updatePostprocessing(editing.Id, payload);
message.success('Gespeichert');
}
setEditing(null);
load();
};
const handleToggle = async (id: number, active: boolean) => {
await settingsApi.updatePostprocessing(id, { IsActive: active });
load();
};
const handleDelete = async (id: number) => {
await settingsApi.deletePostprocessing(id);
message.success('Gelöscht');
load();
};
const handleDuplicate = async (id: number) => {
await settingsApi.duplicatePostprocessing(id);
message.success('Regel dupliziert');
load();
};
const columns: ColumnsType = [
{ title: '#', dataIndex: 'Order', key: 'order', width: 50 },
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{
title: 'Bedingungen',
key: 'conditions',
render: (_, r) => {r.FilterJson?.rules?.length || 0} ,
},
{
title: 'Aktiv',
key: 'active',
width: 70,
render: (_, r) => handleToggle(r.Id, v)} />,
},
{
title: 'Stop',
dataIndex: 'NoFurther',
key: 'stop',
width: 50,
render: (v: boolean) => v ? Stop : null,
},
{
title: '',
key: 'actions',
width: 130,
render: (_, rec) => (
} size="small" onClick={() => openEdit(rec)} />
} size="small" onClick={() => handleDuplicate(rec.Id)} />
handleDelete(rec.Id)}>
} size="small" />
),
},
];
return (
<>
} onClick={openNew} style={{ marginBottom: 16 }}>
Regel hinzufügen
setEditing(null)}
width={900}
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}
>
Filter
{editing && !isNew && (
<>
>
)}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Export-Ziele Tab
// ═══════════════════════════════════════════════════════════════════
function ExportTargetsTab() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null);
const [isNew, setIsNew] = useState(false);
const [form] = Form.useForm();
const [testing, setTesting] = useState(null);
const load = useCallback(async () => {
setLoading(true);
try { setData(await settingsApi.getExportTargets()); }
finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
const openNew = () => {
setIsNew(true);
form.resetFields();
form.setFieldsValue({ Protocol: 'ftp', IsActive: true });
setEditing({} as any);
};
const openEdit = (t: SettingExportTarget) => {
setIsNew(false);
form.setFieldsValue(t);
setEditing(t);
};
const handleSave = async () => {
const values = await form.validateFields();
if (isNew) {
await settingsApi.createExportTarget(values);
message.success('Export-Ziel erstellt');
} else if (editing) {
await settingsApi.updateExportTarget(editing.Id, values);
message.success('Gespeichert');
}
setEditing(null);
load();
};
const handleDelete = async (id: number) => {
await settingsApi.deleteExportTarget(id);
message.success('Gelöscht');
load();
};
const handleTest = async (id: number) => {
setTesting(id);
try {
const result = await settingsApi.testExportTarget(id);
if (result.success) {
message.success(result.message);
} else {
message.error(result.message);
}
} catch {
message.error('Verbindungstest fehlgeschlagen');
} finally {
setTesting(null);
}
};
const columns: ColumnsType = [
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{ title: 'Protokoll', dataIndex: 'Protocol', key: 'protocol', render: v => {v.toUpperCase()} },
{ title: 'Host', dataIndex: 'Host', key: 'host' },
{ title: 'Pfad', dataIndex: 'RemotePath', key: 'path', ellipsis: true },
{ title: 'Aktiv', key: 'active', width: 60, render: (_, r) => },
{
title: '', key: 'actions', width: 160,
render: (_, rec) => (
handleTest(rec.Id)}>
Test
} size="small" onClick={() => openEdit(rec)} />
handleDelete(rec.Id)}>
} size="small" />
),
},
];
return (
<>
} onClick={openNew} style={{ marginBottom: 16 }}>
Export-Ziel hinzufügen
setEditing(null)}>
FTP
WebDAV
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Postprocessing Logs Tab
// ═══════════════════════════════════════════════════════════════════
function PostprocessingLogsTab() {
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const pageSize = 50;
const load = useCallback(async () => {
setLoading(true);
try {
const result = await settingsApi.getPostprocessingLogs(pageSize, (page - 1) * pageSize);
setData(result.data);
setTotal(result.total);
} finally { setLoading(false); }
}, [page]);
useEffect(() => { load(); }, [load]);
const columns: ColumnsType = [
{ title: 'Zeitpunkt', dataIndex: 'CreatedAt', key: 'time', width: 180, render: v => new Date(v).toLocaleString('de-DE') },
{ title: 'Dokument', dataIndex: 'DocumentId', key: 'doc', width: 100 },
{ title: 'Regel', dataIndex: 'PostprocessingId', key: 'rule', width: 80 },
{ title: 'Aktion', dataIndex: 'ActionId', key: 'action', width: 80, render: v => v || '—' },
{
title: 'Status', dataIndex: 'Status', key: 'status', width: 100,
render: (v: string) => (
),
},
{ title: 'Nachricht', dataIndex: 'Message', key: 'msg', ellipsis: true },
];
return (
`${t} Einträge`,
}}
/>
);
}
// ═══════════════════════════════════════════════════════════════════
// API Keys Tab
// ═══════════════════════════════════════════════════════════════════
function ApiKeysTab() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [successModalOpen, setSuccessModalOpen] = useState(false);
const [newKey, setNewKey] = useState(null);
const [form] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
const keys = await apiKeysApi.getApiKeys();
setData(keys);
} catch (err) {
console.error('Error loading API keys:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const handleCreate = async () => {
const values = await form.validateFields();
try {
const result = await apiKeysApi.createApiKey(values.name);
setNewKey(result.plainKey);
setCreateModalOpen(false);
setSuccessModalOpen(true);
form.resetFields();
load();
} catch (err) {
message.error('Fehler beim Erstellen des API-Keys');
}
};
const handleDelete = async (id: string) => {
try {
await apiKeysApi.deleteApiKey(id);
message.success('API-Key gelöscht');
load();
} catch (err) {
message.error('Fehler beim Löschen des API-Keys');
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.success('In die Zwischenablage kopiert');
};
const columns: ColumnsType = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Präfix', dataIndex: 'keyPrefix', key: 'prefix', render: (v) => {v} },
{
title: 'Erstellt am',
dataIndex: 'createdAt',
key: 'createdAt',
render: (v) => dayjs(v).format('DD.MM.YYYY HH:mm')
},
{
title: 'Zuletzt genutzt',
dataIndex: 'lastUsedAt',
key: 'lastUsedAt',
render: (v) => v ? dayjs(v).format('DD.MM.YYYY HH:mm') : 'Nie'
},
{
title: '',
key: 'actions',
width: 80,
render: (_, record) => (
handleDelete(record.id)}>
} size="small" />
),
},
];
return (
<>
API Keys
Hier kannst du API-Keys verwalten, um externe Dienste anzubinden oder Funktionen über spezialisierte URLs aufzurufen.
} onClick={() => setCreateModalOpen(true)}>
Neuen API-Key erstellen
setCreateModalOpen(false)}
>
setSuccessModalOpen(false)}>
Ich habe den Key kopiert
]}
closable={false}
maskClosable={false}
>
Wichtig: Kopiere den API-Key jetzt. Er wird aus Sicherheitsgründen **nie wieder** im Klartext angezeigt!
{newKey}
}
onClick={() => newKey && copyToClipboard(newKey)}
>
Kopieren
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Correspondents Tab
// ═══════════════════════════════════════════════════════════════════
function CorrespondentsTab() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [searchText, setSearchText] = useState('');
const [createModalOpen, setCreateModalOpen] = useState(false);
const [form] = Form.useForm();
const load = useCallback(async (page: number, size: number, search?: string) => {
setLoading(true);
try {
const result = await settingsApi.getCorrespondents(page, size, search);
setData(result.data);
setTotal(result.total);
} catch (err) {
message.error('Fehler beim Laden der Korrespondenten');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load(currentPage, pageSize, searchText);
}, [currentPage, pageSize, searchText, load]);
const handleCreate = async () => {
const values = await form.validateFields();
try {
await settingsApi.createCorrespondent({ name: values.name });
message.success('Korrespondent erstellt');
setCreateModalOpen(false);
form.resetFields();
load(currentPage, pageSize, searchText);
} catch (err) {
message.error('Fehler beim Erstellen des Korrespondenten');
}
};
const updateAgrarmonitorId = async (id: number, val: number | null) => {
try {
await settingsApi.updateCorrespondentSetting(id, val);
message.success('ID aktualisiert');
// No full reload needed to keep focus, but sync might be better.
// We update local state to reflect change immediately without glitchy reload
setData(prev => prev.map(item => item.id === id ? { ...item, agrarmonitorId: val } : item));
} catch (err) {
message.error('Fehler beim Aktualisieren der ID');
}
};
const columns: ColumnsType = [
{ title: 'Name', dataIndex: 'name', key: 'name', sorter: false }, // Sorting is tricky with server-side pagination unless implemented in backend
{ title: 'Paperless ID', dataIndex: 'id', key: 'id', width: 120 },
{
title: 'Agrarmonitor ID',
dataIndex: 'agrarmonitorId',
key: 'agrarmonitorId',
width: 200,
render: (val, record) => (
{
const v = e.target.value ? parseInt(e.target.value.replace(/\D/g, ''), 10) : null;
if (v !== val) updateAgrarmonitorId(record.id, v);
}}
controls={false}
/>
)
},
{
title: 'Dokumente',
dataIndex: 'document_count',
key: 'docCount',
width: 100,
render: (v) =>
},
];
return (
<>
Korrespondenten
Hier kannst du Korrespondenten verwalten und deren Agrarmonitor-ID hinterlegen.
Neue Korrespondenten werden direkt in Paperless erstellt.
{ setSearchText(v); setCurrentPage(1); }}
style={{ width: 300 }}
/>
} onClick={() => setCreateModalOpen(true)}>
Neuen Korrespondenten anlegen
{
setCurrentPage(page);
setPageSize(size);
}
}}
/>
setCreateModalOpen(false)}
>
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Inbox-Aktionen pro Barcode-Vorlage (Sub-Editor)
// ═══════════════════════════════════════════════════════════════════
const VARIABLE_HINT =
'Platzhalter: {datum} {jahr} {monat} {tag} {zeitstempel}, {barcode} (gesamter Barcode-Wert), {barcode.} für Named Capture Groups der Regex (z.B. (?\\d+))';
function InboxActionsForTemplateEditor({ templateId }: { templateId: number }) {
const [actions, setActions] = useState([]);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(null);
const [isNew, setIsNew] = useState(false);
const [actionType, setActionType] = useState('MAIL');
const [tags, setTags] = useState([]);
const [docTypes, setDocTypes] = useState([]);
const [correspondents, setCorrespondents] = useState([]);
const [customFields, setCustomFields] = useState([]);
const [users, setUsers] = useState<{ id: number; username: string }[]>([]);
const [exportTargets, setExportTargets] = useState([]);
const [form] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
setActions(await settingsApi.listInboxActionsForTemplate(templateId));
} finally {
setLoading(false);
}
}, [templateId]);
useEffect(() => { load(); }, [load]);
useEffect(() => {
Promise.all([
paperlessApi.getTags().catch(() => []),
paperlessApi.getDocumentTypes().catch(() => []),
paperlessApi.getCorrespondents().catch(() => []),
paperlessApi.getCustomFields().catch(() => []),
paperlessApi.getUsers().catch(() => []),
settingsApi.getExportTargets().catch(() => []),
]).then(([t, dt, c, cf, u, et]) => {
setTags(t);
setDocTypes(dt);
setCorrespondents(c);
setCustomFields(cf);
setUsers(u as { id: number; username: string }[]);
setExportTargets(et);
});
}, []);
const openNew = () => {
setIsNew(true);
setEditing({} as InboxAction);
setActionType('MAIL');
form.resetFields();
form.setFieldsValue({ ActionType: 'MAIL', IsActive: true, Order: actions.length + 1 });
};
const openEdit = (a: InboxAction) => {
setIsNew(false);
setEditing(a);
setActionType(a.ActionType);
form.setFieldsValue({ ActionType: a.ActionType, IsActive: a.IsActive, Order: a.Order, ...a.Content });
};
const buildContent = (values: any): Record => {
const { ActionType: _, IsActive: __, Order: ___, ...rest } = values;
return rest;
};
const handleDelete = async (id: number) => {
await settingsApi.deleteInboxAction(id);
message.success('Gelöscht');
load();
};
useEffect(() => {
if (!editing) return;
if (isNew) { form.setFieldsValue({ customFieldsList: [] }); return; }
const cf = (editing.Content?.customFields ?? {}) as Record;
form.setFieldsValue({
customFieldsList: Object.entries(cf).map(([fieldId, value]) => ({ fieldId: Number(fieldId), value })),
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editing, isNew]);
const handleSaveWrapped = async () => {
const values = await form.validateFields();
if (values.ActionType === 'PAPERLESS' && Array.isArray(values.customFieldsList)) {
const obj: Record = {};
for (const row of values.customFieldsList) {
if (row?.fieldId !== undefined && row?.fieldId !== null) obj[String(row.fieldId)] = row.value ?? '';
}
values.customFields = obj;
delete values.customFieldsList;
}
const payload = {
ActionType: values.ActionType as InboxActionType,
Content: buildContent(values),
Order: values.Order,
IsActive: values.IsActive,
};
try {
if (isNew) {
await settingsApi.createInboxActionForTemplate(templateId, payload);
message.success('Aktion erstellt');
} else if (editing) {
await settingsApi.updateInboxAction(editing.Id, payload);
message.success('Aktion gespeichert');
}
setEditing(null);
load();
} catch (err: any) {
message.error(err?.response?.data?.message ?? 'Speichern fehlgeschlagen');
}
};
const renderContentFields = () => {
switch (actionType) {
case 'MAIL':
return (
<>
>
);
case 'EXPORT':
return (
<>
{exportTargets.filter((t) => t.IsActive).map((t) => (
{t.Name} ({t.Protocol})
))}
>
);
case 'PAPERLESS':
return (
<>
{docTypes.map((dt) => (
{dt.name}
))}
{correspondents.map((c) => (
{c.name}
))}
{users.map((u) => (
{u.username}
))}
{tags.map((t) => (
{t.name}
))}
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...rest }) => (
{customFields.map((cf) => (
{cf.name}
))}
} onClick={() => remove(name)} />
))}
add()} icon={ }>Custom Field
>
)}
>
);
default:
return null;
}
};
const columns: ColumnsType = [
{ title: '#', dataIndex: 'Order', key: 'order', width: 60 },
{ title: 'Typ', key: 'type', render: (_, r) => {INBOX_ACTION_LABELS[r.ActionType]} },
{ title: 'Aktiv', key: 'active', width: 70, render: (_, r) => },
{
title: '', key: 'edit', width: 110,
render: (_, r) => (
} onClick={() => openEdit(r)} />
handleDelete(r.Id)}>
} />
),
},
];
return (
Weiterverarbeitungs-Aktionen
} onClick={openNew}>Aktion hinzufügen
rowKey="Id" columns={columns} dataSource={actions} loading={loading} pagination={false} size="small" />
setEditing(null)}
okText="Speichern"
cancelText="Abbrechen"
width={680}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Eingangsdokumentarten Tab (ehemals Barcode-Vorlagen)
// ═══════════════════════════════════════════════════════════════════
function BarcodeTemplatesTab() {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [modalOpen, setModalOpen] = useState(false);
const [editing, setEditing] = useState(null);
const [isNew, setIsNew] = useState(false);
const [testValue, setTestValue] = useState('');
const [form] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
setData(await barcodeTemplatesApi.list());
} catch {
message.error('Vorlagen konnten nicht geladen werden');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const openCreate = () => {
setIsNew(true);
setEditing(null);
setTestValue('');
form.resetFields();
form.setFieldsValue({ SplitBefore: false, DateinameTemplate: '' });
setModalOpen(true);
};
const openEdit = (row: BarcodeTemplate) => {
setIsNew(false);
setEditing(row);
setTestValue('');
form.setFieldsValue({ Name: row.Name, Regex: row.Regex, SplitBefore: row.SplitBefore, DateinameTemplate: row.DateinameTemplate ?? '' });
setModalOpen(true);
};
const handleSave = async () => {
const values = await form.validateFields();
try {
if (editing) {
await barcodeTemplatesApi.update(editing.Id, values);
message.success('Eingangsdokumentart aktualisiert');
} else {
await barcodeTemplatesApi.create({ ...values, Actions: [] });
message.success('Eingangsdokumentart angelegt');
}
if (isNew) setModalOpen(false);
load();
} catch (err: any) {
message.error(err?.response?.data?.message ?? 'Speichern fehlgeschlagen');
}
};
const handleDelete = async (id: number) => {
await barcodeTemplatesApi.remove(id);
message.success('Vorlage gelöscht');
load();
};
const columns: ColumnsType = [
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{
title: 'Regex',
dataIndex: 'Regex',
key: 'regex',
render: (v: string) => {v},
},
{
title: 'Trennen',
dataIndex: 'SplitBefore',
key: 'splitBefore',
width: 100,
render: (v: boolean) =>
v ? vor Barcode : — ,
},
{
title: '',
key: 'edit',
width: 140,
render: (_, row) => (
} onClick={() => openEdit(row)} />
handleDelete(row.Id)}>
} />
),
},
];
const currentRegex = Form.useWatch('Regex', form);
let testResult: { valid: boolean; matches: boolean } | null = null;
if (currentRegex && testValue) {
try {
testResult = { valid: true, matches: new RegExp(currentRegex).test(testValue) };
} catch {
testResult = { valid: false, matches: false };
}
}
return (
<>
} onClick={openCreate}>
Neue Eingangsdokumentart
rowKey="Id"
columns={columns}
dataSource={data}
loading={loading}
pagination={false}
/>
{ setModalOpen(false); setEditing(null); }}
okText="Speichern"
cancelText="Abbrechen"
width={720}
>
{
if (!value) return Promise.resolve();
try { new RegExp(value); return Promise.resolve(); }
catch { return Promise.reject(new Error('Ungültige Regex')); }
},
},
]}
extra="JavaScript-Syntax, z. B. ^\d{4}-\d{6}-\d{8}$ oder ^AM-\d{8}$"
>
setTestValue(e.target.value)}
/>
{testResult && (
{!testResult.valid ? (
Regex ungültig
) : testResult.matches ? (
Treffer
) : (
Kein Treffer
)}
)}
Vor diesem Barcode ein neues Dokument starten
{editing && !isNew && (
<>
>
)}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Settings Page
// ═══════════════════════════════════════════════════════════════════
export default function SettingsPage() {
return (
Einstellungen
Benutzer & Betriebe,
children: ,
},
{
key: 'doctypes',
label: Dokumenttypen ,
children: ,
},
{
key: 'postprocessing',
label: Postprocessing ,
children: ,
},
{
key: 'barcode-templates',
label: Eingangsdokumentarten ,
children: ,
},
{
key: 'correspondents',
label: Korrespondenten ,
children: ,
},
{
key: 'export-targets',
label: Export-Ziele ,
children: ,
},
{
key: 'logs',
label: Logs ,
children: ,
},
{
key: 'task-log',
label: Task-Log ,
children: ,
},
{
key: 'api-keys',
label: API-Keys ,
children: ,
},
]}
/>
);
}