feat: add Steuertags concept to separate workflow from content tags
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s

- New steuertag_ids setting to mark tags as workflow-only (not editable)
- DocumentEditModal shows only content tags (non-Steuertags) as editable chips
- Backend preserves Steuertags when saving document tag changes
- ManuellBearbeitenPage renders content tag chips under document title
- New Steuertags settings tab with multi-select and color preview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Sonnet 4.6 committed 2026-06-09 11:46:39 +02:00
1 parent dad0136365
commit d96e06e86d
8 files changed
+245 -5

No files matched your search

@@ -1,11 +1,13 @@
import { useEffect, useState } from 'react';
import { Table, Popover, Button, Space, message, Tooltip, Typography } from 'antd';
import { Table, Popover, Button, Space, message, Tooltip, Typography, Tag } from 'antd';
const { Title } = Typography;
import { ReloadOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { posteingangApi } from '../api/posteingang';
import type { PosteingangDocument } from '../api/posteingang';
import { paperlessApi } from '../api/paperless';
import type { PaperlessTag } from '../api/paperless';
import DocumentEditModal from '../components/DocumentEditModal';
import { getEnv } from '../utils/env';
import { AuthImage } from '../utils/auth-resource';
@@ -15,6 +17,8 @@ export default function ManuellBearbeitenPage() {
const [loading, setLoading] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<PosteingangDocument | null>(null);
const [allTags, setAllTags] = useState<PaperlessTag[]>([]);
const [steuertagIds, setSteuertagIds] = useState<number[]>([]);
const fetchData = async () => {
setLoading(true);
@@ -28,6 +32,15 @@ export default function ManuellBearbeitenPage() {
}
};
useEffect(() => {
Promise.all([paperlessApi.getTags(), paperlessApi.getSteuertagIds()])
.then(([tags, ids]) => {
setAllTags(tags);
setSteuertagIds(ids);
})
.catch(() => { /* Tags optional; Chips bleiben dann leer */ });
}, []);
useEffect(() => {
fetchData();
// Refresh interval every 30 seconds
@@ -84,6 +97,26 @@ export default function ManuellBearbeitenPage() {
dataIndex: 'title',
key: 'title',
width: '35%',
render: (_: any, record: PosteingangDocument) => {
const contentTags = (record.tags || [])
.filter(id => !steuertagIds.includes(id))
.map(id => allTags.find(t => t.id === id))
.filter((t): t is PaperlessTag => !!t);
return (
<div>
<div>{record.title}</div>
{contentTags.length > 0 && (
<Space size={[4, 4]} wrap style={{ marginTop: 4 }}>
{contentTags.map(t => (
<Tag key={t.id} color={t.color} style={{ color: t.text_color, margin: 0 }}>
{t.name}
</Tag>
))}
</Space>
)}
</div>
);
},
},
{
title: 'Eingangsdatum',
@@ -9,6 +9,7 @@ import {
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';
@@ -2663,6 +2664,86 @@ function AgrarmonitorTab() {
}
// ═══════════════════════════════════════════════════════════════════
// Steuertags Tab
// ═══════════════════════════════════════════════════════════════════
function SteuertagsTab() {
const [tags, setTags] = useState<PaperlessTag[]>([]);
const [selected, setSelected] = useState<number[]>([]);
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 (
<div>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="Steuertags"
description="Als Steuertag markierte Tags dienen der Workflow-Steuerung (z. B. Fertig/Nicht fertig, manuell bearbeiten) und werden NICHT als bearbeitbare Chips im Dokument angezeigt. Alle übrigen Tags gelten als inhaltliche Tags und erscheinen unter dem Titel sowie als Auswahlfeld im Bearbeiten-Dialog."
/>
<Select
mode="multiple"
allowClear
showSearch
optionFilterProp="label"
loading={loading}
style={{ width: '100%', maxWidth: 700 }}
placeholder="Steuertags auswählen"
value={selected}
onChange={setSelected}
options={tags.map((t) => ({ value: t.id, label: t.name }))}
optionRender={(opt) => {
const tag = tags.find((t) => t.id === opt.value);
return tag ? (
<Tag color={tag.color} style={{ color: tag.text_color }}>{tag.name}</Tag>
) : (
opt.label
);
}}
/>
<div style={{ marginTop: 16 }}>
<Button type="primary" loading={saving} onClick={handleSave}>
Speichern
</Button>
</div>
</div>
);
}
// ═══════════════════════════════════════════════════════════════════
// Settings Page
// ═══════════════════════════════════════════════════════════════════
@@ -2699,6 +2780,11 @@ export default function SettingsPage() {
label: <span><UserOutlined /> Korrespondenten</span>,
children: <CorrespondentsTab />,
},
{
key: 'steuertags',
label: <span><TagsOutlined /> Steuertags</span>,
children: <SteuertagsTab />,
},
{
key: 'export-targets',
label: <span><CloudUploadOutlined /> Export-Ziele</span>,