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, Row, Col, Radio, Alert,
} from 'antd';
import {
UserOutlined, FileTextOutlined, ThunderboltOutlined,
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, GlobalOutlined,
TagsOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { FormInstance } from 'antd';
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,
agrarmonitorApi, type AgrarmonitorStatusData,
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
type SyncConflict,
} from '../api/settings';
import { clientsApi, type Client } from '../api/inbox';
import { apiKeysApi, type ApiKey } from '../api/api-keys';
import {
barcodeTemplatesApi,
type BarcodeTemplate,
type LabelInputField,
} from '../api/barcode-templates';
import { labelPrintAgentApi } from '../api/labelPrintAgent';
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 [allClients, setAllClients] = useState([]);
const [loading, setLoading] = useState(true);
const [clientsLoading, setClientsLoading] = useState(false);
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); }
}, []);
const loadAllClients = useCallback(async () => {
setClientsLoading(true);
try {
const cls = await settingsApi.getClients();
setAllClients(cls);
} finally { setClientsLoading(false); }
}, []);
useEffect(() => { load(); loadAllClients(); }, [load, loadAllClients]);
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 handleUpdateBetriebId = async (id: number, val: number | null) => {
try {
const updated = await settingsApi.updateClient(id, val);
setAllClients(prev => prev.map(c => c.Id === id ? updated : c));
} catch {
message.error('Speichern fehlgeschlagen');
}
};
const allClientColumns: ColumnsType = [
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{
title: 'Agrarmonitor-BetriebId',
dataIndex: 'AgrarmonitorBetriebId',
key: 'betriebId',
render: (val: number | null, record) => (
{
const parsed = e.target.value ? parseInt(e.target.value, 10) : null;
const current = val ?? null;
if (parsed !== current) handleUpdateBetriebId(record.Id, isNaN(parsed as number) ? null : parsed);
}}
/>
),
},
];
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
Betriebe — Agrarmonitor-Zuordnung
Ordne jedem Betrieb die zugehörige Agrarmonitor-BetriebId zu. Wird beim Polling verwendet.
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: 'Freigabe erf.',
dataIndex: 'FreigabeErforderlich',
key: 'freigabe',
render: (v: boolean | null) => v ? Ja : '—',
},
{
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 [syncLoading, setSyncLoading] = useState(false);
const [conflicts, setConflicts] = useState([]);
const [conflictsModalOpen, setConflictsModalOpen] = useState(false);
const [conflictSelections, setConflictSelections] = useState>({});
const [mergeLoading, setMergeLoading] = 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 handleSync = async () => {
setSyncLoading(true);
try {
const result = await agrarmonitorApi.syncCorrespondents();
const parts: string[] = [
`${result.matched} zugeordnet`,
`${result.unmatched} ohne Treffer`,
];
if (result.autoMerged > 0) parts.push(`${result.autoMerged} automatisch bereinigt`);
message.success(`Abgleich abgeschlossen: ${parts.join(', ')} (${result.total} gesamt)`);
load(currentPage, pageSize, searchText);
if (result.conflicts.length > 0) {
setConflicts(result.conflicts);
setConflictSelections(
Object.fromEntries(result.conflicts.map(c => [c.agrarmonitorId, c.correspondents[0].id]))
);
setConflictsModalOpen(true);
}
} catch (err) {
message.error('Fehler beim Agrarmonitor-Abgleich');
} finally {
setSyncLoading(false);
}
};
const handleMergeConflicts = async () => {
setMergeLoading(true);
try {
for (const conflict of conflicts) {
const keepId = conflictSelections[conflict.agrarmonitorId];
if (keepId === undefined) continue;
const deleteIds = conflict.correspondents.map(c => c.id).filter(id => id !== keepId);
for (const deleteId of deleteIds) {
await agrarmonitorApi.mergeCorrespondents(keepId, deleteId);
}
}
message.success(`${conflicts.length} Konflikt(e) aufgelöst`);
setConflictsModalOpen(false);
setConflicts([]);
load(currentPage, pageSize, searchText);
} catch (err) {
message.error('Fehler beim Zusammenführen');
} finally {
setMergeLoading(false);
}
};
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 }}
/>
} loading={syncLoading} onClick={handleSync}>
Agrarmonitor-Abgleich
} onClick={() => setCreateModalOpen(true)}>
Neuen Korrespondenten anlegen
{
setCurrentPage(page);
setPageSize(size);
}
}}
/>
setCreateModalOpen(false)}
>
setConflictsModalOpen(false)}
okText="Zusammenführen"
cancelText="Abbrechen"
confirmLoading={mergeLoading}
width={560}
>
{conflicts.map((conflict, idx) => (
Agrarmonitor-ID: {conflict.agrarmonitorId}
setConflictSelections(prev => ({ ...prev, [conflict.agrarmonitorId]: e.target.value as number }))}
>
{conflict.correspondents.map(c => (
{c.name}
({c.documentCount} Dokumente)
))}
))}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// 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 LabelElementRow({ form, listName, remove }: { form: FormInstance; listName: number; remove: (n: number) => void }) {
const type = Form.useWatch(['LabelLayout', listName, 'type'], form);
return (
} onClick={() => remove(listName)} />}
>
{type === 'text' && (
)}
{type === 'qr' && (
)}
{type === 'line' && (
)}
);
}
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 [testPrinting, setTestPrinting] = useState(false);
const [previewUrl, setPreviewUrl] = useState(null);
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: '',
LabelEnabled: false,
LabelWidthMm: 57,
LabelHeightMm: 32,
LabelInputFields: [],
LabelLayout: [],
});
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 ?? '',
LabelEnabled: row.LabelEnabled ?? false,
LabelWidthMm: row.LabelWidthMm ?? 57,
LabelHeightMm: row.LabelHeightMm ?? 32,
LabelInputFields: row.LabelInputFields ?? [],
LabelGetUrl: row.LabelGetUrl ?? '',
LabelPrintedUrl: row.LabelPrintedUrl ?? '',
LabelReleaseUrl: row.LabelReleaseUrl ?? '',
LabelLayout: row.LabelLayout ?? [],
});
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 handleTestLabel = async () => {
if (!editing) return;
setTestPrinting(true);
try {
const values = await form.validateFields();
await barcodeTemplatesApi.update(editing.Id, values);
const inputFields: LabelInputField[] = values.LabelInputFields ?? [];
const testFieldValues: Record = {};
const today = new Date().toISOString().slice(0, 10);
for (const f of inputFields) {
if (f.type === 'date') testFieldValues[f.name] = today;
else if (f.type === 'number') testFieldValues[f.name] = '1';
else testFieldValues[f.name] = 'Test';
}
const url = await labelPrintAgentApi.previewLabel(editing.Id, testFieldValues);
if (previewUrl) URL.revokeObjectURL(previewUrl);
setPreviewUrl(url);
load();
} catch (err: any) {
message.error(err?.response?.data?.message ?? 'Vorschau fehlgeschlagen');
} finally {
setTestPrinting(false);
}
};
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); }}
width={720}
footer={[
...(editing && !isNew ? [
} loading={testPrinting} onClick={handleTestLabel}>
Testetikett erstellen
,
] : []),
{ setModalOpen(false); setEditing(null); }}>Abbrechen ,
Speichern ,
]}
>
{
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
Etikett
prev.LabelEnabled !== curr.LabelEnabled}>
{({ getFieldValue }) =>
getFieldValue('LabelEnabled') ? (
<>
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...rest }) => (
remove(name)} />
))}
add({ type: 'text' })} icon={ } size="small">
Feld hinzufügen
>
)}
{({ getFieldValue: gfv }) => {
const inputFields: LabelInputField[] = gfv('LabelInputFields') ?? [];
const chips: string[] = [];
for (const f of inputFields) {
if (!f?.name) continue;
chips.push(`{${f.name}}`);
if (f.type === 'date') {
chips.push(`{${f.name}.year}`, `{${f.name}.month}`, `{${f.name}.day}`);
}
}
const chipsWithNumber = [...chips, '{number}'];
return (
<>
0 ? `Platzhalter: ${chips.join(' ')}` : undefined}>
0 ? `Platzhalter: ${chipsWithNumber.join(' ')}` : undefined}>
0 ? `Platzhalter: ${chipsWithNumber.join(' ')}` : undefined}>
0 ? `Platzhalter für Inhalt: ${chipsWithNumber.join(' ')}` : undefined}>
{(layoutFields, { add: addEl, remove: removeEl }) => (
<>
{layoutFields.map(({ key, name: elName }) => (
))}
}
onClick={() => addEl({ type: 'text', x: 0, y: 0, fontSize: 3, content: '', bold: false })}>
Text
}
onClick={() => addEl({ type: 'qr', x: 0, y: 0, sizeMm: 20, content: '' })}>
QR-Code
}
onClick={() => addEl({ type: 'line', x1: 0, y1: 0, x2: 50, y2: 0 })}>
Linie
>
)}
>
);
}}
>
) : null
}
{editing && !isNew && (
<>
>
)}
{ URL.revokeObjectURL(previewUrl!); setPreviewUrl(null); }}
footer={ { URL.revokeObjectURL(previewUrl!); setPreviewUrl(null); }}>Schließen }
width={520}
>
{previewUrl && (
)}
>
);
}
// ═══════════════════════════════════════════════════════════════════
// Agrarmonitor Tab
// ═══════════════════════════════════════════════════════════════════
function AgrarmonitorTab() {
const [form] = Form.useForm();
const [pollingForm] = Form.useForm();
const [loading, setLoading] = useState(false);
const [registering, setRegistering] = useState(false);
const [pollingConfigLoading, setPollingConfigLoading] = useState(false);
const [pollingSaving, setPollingSaving] = useState(false);
const [pollingRunning, setPollingRunning] = useState(false);
const [uploadCheckRunning, setUploadCheckRunning] = useState(false);
const [status, setStatus] = useState(null);
const [registerResult, setRegisterResult] = useState<{ success: boolean; message: string } | null>(null);
const [pollingResult, setPollingResult] = useState(null);
const [uploadCheckResult, setUploadCheckResult] = useState(null);
const [customFields, setCustomFields] = useState([]);
const handleLoadStatus = async () => {
setLoading(true);
setRegisterResult(null);
try {
const data = await agrarmonitorApi.getStatus();
setStatus(data);
} catch (err: any) {
const msg = err?.code === 'ECONNABORTED'
? 'Timeout – Backend antwortet nicht rechtzeitig'
: (err?.response?.data?.message ?? err?.message ?? 'Netzwerkfehler');
setStatus({ connected: false, registriert: null, freigeschaltet: null, error: msg });
} finally {
setLoading(false);
}
};
const handleRegister = async () => {
const values = await form.validateFields();
setRegistering(true);
setRegisterResult(null);
try {
const result = await agrarmonitorApi.registerDevice(values.pcName, values.agrarmonitorId);
setRegisterResult(result);
if (result.success) {
message.success('Gerät erfolgreich registriert');
await handleLoadStatus();
}
} catch {
setRegisterResult({ success: false, message: 'Registrierung fehlgeschlagen' });
} finally {
setRegistering(false);
}
};
const handleLoadPollingConfig = useCallback(async () => {
setPollingConfigLoading(true);
try {
const cfg = await agrarmonitorApi.getPollingConfig();
pollingForm.setFieldsValue(cfg);
} catch {
message.error('Polling-Konfiguration konnte nicht geladen werden');
} finally {
setPollingConfigLoading(false);
}
}, [pollingForm]);
useEffect(() => {
handleLoadPollingConfig();
paperlessApi.getCustomFields().then(setCustomFields).catch(() => {});
}, [handleLoadPollingConfig]);
const handleSavePollingConfig = async () => {
const values = await pollingForm.validateFields() as AgrarmonitorPollingConfig;
setPollingSaving(true);
try {
await agrarmonitorApi.updatePollingConfig(values);
message.success('Konfiguration gespeichert');
} catch {
message.error('Speichern fehlgeschlagen');
} finally {
setPollingSaving(false);
}
};
const handleRunPolling = async () => {
setPollingRunning(true);
setPollingResult(null);
try {
const result = await agrarmonitorApi.runPolling();
setPollingResult(result);
} catch {
message.error('Polling fehlgeschlagen');
} finally {
setPollingRunning(false);
}
};
const handleProcessUploads = async () => {
setUploadCheckRunning(true);
setUploadCheckResult(null);
try {
const result = await agrarmonitorApi.processUploads();
setUploadCheckResult(result);
} catch {
message.error('Upload-Check fehlgeschlagen');
} finally {
setUploadCheckRunning(false);
}
};
const renderStatusTag = (value: boolean | null, labelTrue: string, labelFalse: string) => {
if (value === null) return – ;
return value
? {labelTrue}
: {labelFalse} ;
};
return (
);
}
// ═══════════════════════════════════════════════════════════════════
// Steuertags Tab
// ═══════════════════════════════════════════════════════════════════
function SteuertagsTab() {
const [tags, setTags] = useState([]);
const [selected, setSelected] = useState([]);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const [fetchedTags, steuertagIds] = await Promise.all([
paperlessApi.getTags(),
settingsApi.getSteuertagIds(),
]);
setTags(fetchedTags);
setSelected(steuertagIds);
} catch {
message.error('Tags konnten nicht geladen werden.');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const handleSave = async () => {
setSaving(true);
try {
await settingsApi.updateSteuertagIds(selected);
message.success('Steuertags gespeichert.');
} catch {
message.error('Steuertags konnten nicht gespeichert werden.');
} finally {
setSaving(false);
}
};
return (
({ value: t.id, label: t.name }))}
optionRender={(opt) => {
const tag = tags.find((t) => t.id === opt.value);
return tag ? (
{tag.name}
) : (
opt.label
);
}}
/>
Speichern
);
}
// ═══════════════════════════════════════════════════════════════════
// 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: 'steuertags',
label: Steuertags ,
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: ,
},
{
key: 'agrarmonitor',
label: Agrarmonitor ,
children: ,
},
]}
/>
);
}