feat: add Steuertags concept to separate workflow from content tags
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s
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:
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>,
|
||||
|
||||
Reference in new issue
Block a user