fix: Berechtigungen der Tagesübersicht, WebDAV-Ausbau, Tests instand gesetzt
Build and Push Multi-Platform Images / build-and-push (push) Successful in 55s

Die tägliche E-Mail-Zusammenfassung zeigte Nutzern Bereiche, für die sie
keine Berechtigung haben. Zwei unabhängige Ursachen:

- Das Backend prüfte weiterhin die Altgruppe PM_Belege. Die Umbenennung
  zu PM_Buchhaltung (ca1d371) war nur im Frontend angekommen, weshalb
  der Digest "Manuell bearbeiten" und "In Agrarmonitor" anbot, während
  die Oberfläche beide Bereiche sperrte.
- Der Cron-Versand wertet die Gruppen aus user_settings aus. Diese Spalte
  wurde nur beim Aufruf der Benutzereinstellungen gefüllt; entzogene
  Berechtigungen erreichten den Digest daher unter Umständen nie.

Behoben durch Angleichen des Gruppen-Mappings und den neuen
UserIdentitySyncService, der E-Mail, Benutzername und Gruppen bei jedem
authentifizierten Request aus dem Token spiegelt – ohne den Request zu
blockieren und ohne DB-Zugriff, solange sich das Token nicht ändert. Die
doppelte Identitätspflege im UserSettingsService entfällt.

WebDAV wird nicht eingesetzt und ist entfernt; Export-Ziele bieten nur
noch FTP. Damit verschwindet das ESM-Paket webdav, an dem zwei
Jest-Suites bereits beim Parsen scheiterten.

Veraltete Tests instand gesetzt: email.controller und settings.controller
mockten weniger Abhängigkeiten, als die Klassen inzwischen haben;
postprocessing.service.spec beschrieb noch das alte Regelmodell mit
Einzelfeldern statt FilterJson und ist gegen die heutige Filter-Engine
neu geschrieben (AND/OR, verschachtelte Gruppen, Fehlerprotokollierung).

Enthält außerdem den Arbeitsstand der E-Rechnungs-Mandantenzuordnung, da
sich beide Änderungen dieselben Dateien teilen (SettingsPage, package.json).

98 Tests in 12 Suites grün, Backend- und Frontend-Build sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5 committed 2026-09-01 00:06:50 +02:00
1 parent 32ba772e4e
commit b7b65c64ae
41 files changed
+2462 -443

No files matched your search

+131 -3
View File
@@ -23,6 +23,7 @@ import {
agrarmonitorApi, type AgrarmonitorStatusData,
type SettingClient, type AgrarmonitorPollingConfig, type AgrarmonitorPollingResult,
type SyncConflict,
type SettingClientIdentifier, type ClientIdentifierTyp, CLIENT_IDENTIFIER_LABELS,
} from '../api/settings';
import { clientsApi, type Client } from '../api/inbox';
import { apiKeysApi, type ApiKey } from '../api/api-keys';
@@ -223,6 +224,12 @@ function UserClientsTab() {
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
// E-Rechnungs-Kennungen der Mandanten
const [identifiers, setIdentifiers] = useState<SettingClientIdentifier[]>([]);
const [identLoading, setIdentLoading] = useState(false);
const [identModalOpen, setIdentModalOpen] = useState(false);
const [identForm] = Form.useForm();
const load = useCallback(async () => {
setLoading(true);
try {
@@ -235,6 +242,13 @@ function UserClientsTab() {
} finally { setLoading(false); }
}, []);
const loadIdentifiers = useCallback(async () => {
setIdentLoading(true);
try {
setIdentifiers(await settingsApi.getClientIdentifiers());
} finally { setIdentLoading(false); }
}, []);
const loadAllClients = useCallback(async () => {
setClientsLoading(true);
try {
@@ -243,7 +257,7 @@ function UserClientsTab() {
} finally { setClientsLoading(false); }
}, []);
useEffect(() => { load(); loadAllClients(); }, [load, loadAllClients]);
useEffect(() => { load(); loadAllClients(); loadIdentifiers(); }, [load, loadAllClients, loadIdentifiers]);
const handleAdd = async () => {
const values = await form.validateFields();
@@ -269,6 +283,52 @@ function UserClientsTab() {
}
};
const handleAddIdentifier = async () => {
const values = await identForm.validateFields();
try {
await settingsApi.createClientIdentifier(values);
message.success('Kennung hinzugefügt');
setIdentModalOpen(false);
identForm.resetFields();
loadIdentifiers();
} catch (e) {
const meldung = (e as { response?: { data?: { message?: string } } })?.response?.data?.message;
message.error(meldung ?? 'Kennung konnte nicht angelegt werden');
}
};
const handleDeleteIdentifier = async (id: number) => {
await settingsApi.deleteClientIdentifier(id);
message.success('Gelöscht');
loadIdentifiers();
};
const identifierColumns: ColumnsType<SettingClientIdentifier> = [
{
title: 'Betrieb',
key: 'client',
render: (_, r) => allClients.find(c => c.Id === r.ClientId)?.Name ?? r.ClientId,
},
{
title: 'Art',
dataIndex: 'Typ',
key: 'typ',
width: 160,
render: (t: ClientIdentifierTyp) => <Tag>{CLIENT_IDENTIFIER_LABELS[t] ?? t}</Tag>,
},
{ title: 'Kennung', dataIndex: 'Wert', key: 'wert' },
{
title: '',
key: 'actions',
width: 80,
render: (_, record) => (
<Popconfirm title="Kennung wirklich löschen?" onConfirm={() => handleDeleteIdentifier(record.Id)}>
<Button danger icon={<DeleteOutlined />} size="small" />
</Popconfirm>
),
},
];
const allClientColumns: ColumnsType<SettingClient> = [
{ title: 'Name', dataIndex: 'Name', key: 'name' },
{
@@ -338,6 +398,62 @@ function UserClientsTab() {
scroll={{ x: 'max-content' }}
/>
<Divider />
<Typography.Title level={5} style={{ marginBottom: 8 }}>Betriebe — E-Rechnungs-Kennungen</Typography.Title>
<Typography.Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
Über diese Käufer-Kennungen wird der Mandant aus einer E-Rechnung erkannt. Neue Kennungen
werden beim Speichern eines Belegs automatisch gelernt — hier lassen sie sich prüfen,
ergänzen und korrigieren. Jede Kennung darf nur einem Betrieb gehören.
</Typography.Text>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => setIdentModalOpen(true)}
style={{ marginBottom: 12 }}
>
Kennung hinzufügen
</Button>
<Table
dataSource={identifiers}
columns={identifierColumns}
loading={identLoading}
rowKey="Id"
size="small"
pagination={false}
scroll={{ x: 'max-content' }}
locale={{ emptyText: 'Noch keine Kennungen — die erste entsteht, sobald ein Beleg mit E-Rechnung gespeichert wird.' }}
/>
<Modal
title="Neue E-Rechnungs-Kennung"
open={identModalOpen}
onOk={handleAddIdentifier}
onCancel={() => { setIdentModalOpen(false); identForm.resetFields(); }}
>
<Form form={identForm} layout="vertical">
<Form.Item name="ClientId" label="Betrieb" rules={[{ required: true, message: 'Betrieb wählen' }]}>
<Select showSearch optionFilterProp="children">
{allClients.map(c => <Select.Option key={c.Id} value={c.Id}>{c.Name}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item name="Typ" label="Art der Kennung" rules={[{ required: true, message: 'Art wählen' }]}>
<Select>
{(Object.keys(CLIENT_IDENTIFIER_LABELS) as ClientIdentifierTyp[]).map(t => (
<Select.Option key={t} value={t}>{CLIENT_IDENTIFIER_LABELS[t]}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name="Wert"
label="Kennung"
rules={[{ required: true, message: 'Kennung eingeben' }]}
tooltip="Groß-/Kleinschreibung sowie Leer- und Trennzeichen sind egal — sie werden beim Speichern vereinheitlicht."
>
<Input placeholder="z.B. DE123456789" />
</Form.Item>
</Form>
</Modal>
<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 }]}>
@@ -588,6 +704,12 @@ function DocTypesTab() {
key: 'freigabe',
render: (v: boolean | null) => v ? <Tag color="blue">Ja</Tag> : '—',
},
{
title: 'E-Rechnungs-Codes',
dataIndex: 'ERechnungTypeCodes',
key: 'erechnung',
render: (v: string | null) => v ? <Tag>{v}</Tag> : '—',
},
{
title: '',
key: 'actions',
@@ -637,6 +759,13 @@ function DocTypesTab() {
<Form.Item name="FreigabeErforderlich" valuePropName="checked" label="Freigabe erforderlich">
<Checkbox />
</Form.Item>
<Form.Item
name="ERechnungTypeCodes"
label="E-Rechnungs-Codes (BT-3)"
tooltip="Rechnungstypen, die auf diese Belegart abgebildet werden – kommagetrennt. Üblich: 380 = Rechnung, 384 = korrigierte Rechnung, 381 = Gutschrift, 389 = Gutschriftverfahren. Leer lassen, wenn diese Belegart nie vorgeschlagen werden soll."
>
<Input placeholder="z.B. 380,384" />
</Form.Item>
</Form>
{editing && (
@@ -655,7 +784,7 @@ function DocTypesTab() {
// ═══════════════════════════════════════════════════════════════════
const ACTION_TYPE_LABELS: Record<number, string> = {
1: 'Export (FTP/WebDAV)',
1: 'Export (FTP)',
2: 'Mail versenden',
3: 'Tags setzen/entfernen',
4: 'Custom Field setzen',
@@ -1191,7 +1320,6 @@ function ExportTargetsTab() {
<Form.Item name="Protocol" label="Protokoll" rules={[{ required: true }]}>
<Select>
<Select.Option value="ftp">FTP</Select.Option>
<Select.Option value="webdav">WebDAV</Select.Option>
</Select>
</Form.Item>
<Form.Item name="Host" label="Host" rules={[{ required: true }]}><Input /></Form.Item>