feat: extend SettingsPage with Agrarmonitor polling UI

Benutzer & Betriebe tab:
- Add Betriebe table with inline-editable AgrarmonitorBetriebId column

Agrarmonitor tab:
- Add Polling-Konfiguration card (tag-IDs, auto-loaded, save button)
- Add Polling ausführen card (run button, result display with error list)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 15:04:17 +02:00
parent 5ca202a59e
commit 6e1f995fe5
+150 -1
View File
@@ -20,6 +20,7 @@ import {
INBOX_ACTION_LABELS,
type InboxAction, type InboxActionType,
agrarmonitorApi, type AgrarmonitorStatusData,
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
} from '../api/settings';
import { clientsApi, type Client } from '../api/inbox';
import { apiKeysApi, type ApiKey } from '../api/api-keys';
@@ -213,7 +214,9 @@ function FilterBuilder({ value, onChange, tags, docTypes, correspondents, custom
function UserClientsTab() {
const [data, setData] = useState<SettingUserClient[]>([]);
const [clients, setClients] = useState<Client[]>([]);
const [allClients, setAllClients] = useState<SettingClient[]>([]);
const [loading, setLoading] = useState(true);
const [clientsLoading, setClientsLoading] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
@@ -229,7 +232,15 @@ function UserClientsTab() {
} finally { setLoading(false); }
}, []);
useEffect(() => { load(); }, [load]);
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();
@@ -246,6 +257,37 @@ function UserClientsTab() {
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<SettingClient> = [
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{
title: 'Agrarmonitor-BetriebId',
dataIndex: 'AgrarmonitorBetriebId',
key: 'betriebId',
render: (val: number | null, record) => (
<InputNumber
value={val ?? undefined}
placeholder=""
min={1}
style={{ width: 120 }}
onBlur={(e) => {
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<SettingUserClient> = [
{ title: 'User ID', dataIndex: 'UserId', key: 'userId' },
{
@@ -277,6 +319,21 @@ function UserClientsTab() {
Zuordnung hinzufügen
</Button>
<Table dataSource={data} columns={columns} loading={loading} rowKey="Id" size="small" pagination={false} />
<Divider />
<Typography.Title level={5} style={{ marginBottom: 8 }}>Betriebe Agrarmonitor-Zuordnung</Typography.Title>
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
Ordne jedem Betrieb die zugehörige Agrarmonitor-BetriebId zu. Wird beim Polling verwendet.
</Typography.Text>
<Table
dataSource={allClients}
columns={allClientColumns}
loading={clientsLoading}
rowKey="Id"
size="small"
pagination={false}
/>
<Modal title="Neue Zuordnung" open={modalOpen} onOk={handleAdd} onCancel={() => setModalOpen(false)}>
<Form form={form} layout="vertical">
<Form.Item name="UserId" label="User ID (Authentik)" rules={[{ required: true }]}>
@@ -2228,10 +2285,15 @@ function BarcodeTemplatesTab() {
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 [status, setStatus] = useState<AgrarmonitorStatusData | null>(null);
const [registerResult, setRegisterResult] = useState<{ success: boolean; message: string } | null>(null);
const [pollingResult, setPollingResult] = useState<AgrarmonitorPollingResult | null>(null);
const handleLoadStatus = async () => {
setLoading(true);
@@ -2267,6 +2329,46 @@ function AgrarmonitorTab() {
}
};
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(); }, [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 renderStatusTag = (value: boolean | null, labelTrue: string, labelFalse: string) => {
if (value === null) return <Tag></Tag>;
return value
@@ -2343,6 +2445,53 @@ function AgrarmonitorTab() {
)}
</Card>
)}
<Card size="small" title="Polling-Konfiguration" loading={pollingConfigLoading}>
<Form form={pollingForm} layout="vertical">
<Form.Item
name="tagFertig"
label="Tag-ID: Fertig in Agrarmonitor"
rules={[{ required: true, message: 'Pflichtfeld' }]}
>
<Input placeholder="4" style={{ width: 120 }} />
</Form.Item>
<Form.Item
name="tagVerbucht"
label="Tag-ID: Verbucht"
rules={[{ required: true, message: 'Pflichtfeld' }]}
>
<Input placeholder="9" style={{ width: 120 }} />
</Form.Item>
<Button type="primary" loading={pollingSaving} onClick={handleSavePollingConfig}>
Speichern
</Button>
</Form>
</Card>
<Card size="small" title="Polling ausführen">
<Space direction="vertical" style={{ width: '100%' }}>
<Button loading={pollingRunning} onClick={handleRunPolling}>
Jetzt ausführen
</Button>
{pollingResult && (
<div>
<Tag color="blue">{pollingResult.processed} verarbeitet</Tag>
<Tag color="success">{pollingResult.updated} aktualisiert</Tag>
<Tag>{pollingResult.skipped} übersprungen</Tag>
{pollingResult.errors.length > 0 && (
<Tag color="error">{pollingResult.errors.length} Fehler</Tag>
)}
{pollingResult.errors.length > 0 && (
<ul style={{ marginTop: 8, paddingLeft: 20 }}>
{pollingResult.errors.map((e, i) => (
<li key={i} style={{ color: '#ff4d4f' }}>{e}</li>
))}
</ul>
)}
</div>
)}
</Space>
</Card>
</Space>
</div>
);