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:
@@ -29,6 +29,10 @@ import { Task } from '../database/entities/task.entity';
|
|||||||
import { Document } from '../database/entities/document.entity';
|
import { Document } from '../database/entities/document.entity';
|
||||||
import { DocumentField } from '../database/entities/document-field.entity';
|
import { DocumentField } from '../database/entities/document-field.entity';
|
||||||
import { DocumentType } from '../database/entities/document-type.entity';
|
import { DocumentType } from '../database/entities/document-type.entity';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
|
|
||||||
|
/** Setting.Tag-Schlüssel für die manuell gepflegte Steuertag-Liste */
|
||||||
|
export const STEUERTAG_SETTING_KEY = 'steuertag_ids';
|
||||||
|
|
||||||
@Controller('api/paperless')
|
@Controller('api/paperless')
|
||||||
export class PaperlessController {
|
export class PaperlessController {
|
||||||
@@ -44,8 +48,26 @@ export class PaperlessController {
|
|||||||
private readonly documentFieldRepo: Repository<DocumentField>,
|
private readonly documentFieldRepo: Repository<DocumentField>,
|
||||||
@InjectRepository(DocumentType)
|
@InjectRepository(DocumentType)
|
||||||
private readonly documentTypeRepo: Repository<DocumentType>,
|
private readonly documentTypeRepo: Repository<DocumentType>,
|
||||||
|
@InjectRepository(Setting)
|
||||||
|
private readonly settingRepo: Repository<Setting>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Liest die als Steuertags markierten Tag-IDs aus den Einstellungen. */
|
||||||
|
private async getSteuertagIds(): Promise<number[]> {
|
||||||
|
const setting = await this.settingRepo.findOneBy({
|
||||||
|
Tag: STEUERTAG_SETTING_KEY,
|
||||||
|
});
|
||||||
|
return (setting?.Wert ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => parseInt(s.trim(), 10))
|
||||||
|
.filter((n) => !isNaN(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('steuertags')
|
||||||
|
async getSteuertags() {
|
||||||
|
return { ids: await this.getSteuertagIds() };
|
||||||
|
}
|
||||||
|
|
||||||
@Post('checksum')
|
@Post('checksum')
|
||||||
async checksumExists(@Body('checksum') checksum: string) {
|
async checksumExists(@Body('checksum') checksum: string) {
|
||||||
const exists = await this.paperlessService.checksumExists(checksum);
|
const exists = await this.paperlessService.checksumExists(checksum);
|
||||||
@@ -390,6 +412,19 @@ export class PaperlessController {
|
|||||||
|
|
||||||
oldDocument.tags = oldDocument.tags || [];
|
oldDocument.tags = oldDocument.tags || [];
|
||||||
|
|
||||||
|
// Im Modal bearbeitbare Inhaltstags übernehmen: Steuertags bleiben
|
||||||
|
// unangetastet, die übrigen (Inhalts-)Tags werden durch die Auswahl ersetzt.
|
||||||
|
if (Array.isArray(body.tags)) {
|
||||||
|
const steuertagIds = new Set(await this.getSteuertagIds());
|
||||||
|
const preserved = oldDocument.tags.filter((t: number) =>
|
||||||
|
steuertagIds.has(t),
|
||||||
|
);
|
||||||
|
const selected = body.tags
|
||||||
|
.map((t: any) => Number(t))
|
||||||
|
.filter((t: number) => !isNaN(t) && !steuertagIds.has(t));
|
||||||
|
oldDocument.tags = Array.from(new Set([...preserved, ...selected]));
|
||||||
|
}
|
||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1);
|
oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1);
|
||||||
if (docType?.TagNotReady)
|
if (docType?.TagNotReady)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DocumentField } from '../database/entities/document-field.entity';
|
|||||||
import { Task } from '../database/entities/task.entity';
|
import { Task } from '../database/entities/task.entity';
|
||||||
import { Document } from '../database/entities/document.entity';
|
import { Document } from '../database/entities/document.entity';
|
||||||
import { Attachment } from '../database/entities/attachment.entity';
|
import { Attachment } from '../database/entities/attachment.entity';
|
||||||
|
import { Setting } from '../database/entities/setting.entity';
|
||||||
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
import { PostprocessingModule } from '../postprocessing/postprocessing.module';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ import { AuthModule } from '../auth/auth.module';
|
|||||||
Task,
|
Task,
|
||||||
Document,
|
Document,
|
||||||
Attachment,
|
Attachment,
|
||||||
|
Setting,
|
||||||
]),
|
]),
|
||||||
forwardRef(() => PostprocessingModule),
|
forwardRef(() => PostprocessingModule),
|
||||||
AuthModule,
|
AuthModule,
|
||||||
|
|||||||
@@ -374,6 +374,36 @@ export class SettingsController {
|
|||||||
return this.settingRepo.findOneByOrFail({ ID: parseInt(id, 10) });
|
return this.settingRepo.findOneByOrFail({ ID: parseInt(id, 10) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Steuertags ===
|
||||||
|
@Get('steuertags')
|
||||||
|
async getSteuertags() {
|
||||||
|
const setting = await this.settingRepo.findOneBy({ Tag: 'steuertag_ids' });
|
||||||
|
const ids = (setting?.Wert ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => parseInt(s.trim(), 10))
|
||||||
|
.filter((n) => !isNaN(n));
|
||||||
|
return { ids };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('steuertags')
|
||||||
|
async updateSteuertags(@Body() body: { ids: number[] }) {
|
||||||
|
const ids = (body.ids ?? [])
|
||||||
|
.map((id) => Number(id))
|
||||||
|
.filter((n) => !isNaN(n));
|
||||||
|
let setting = await this.settingRepo.findOneBy({ Tag: 'steuertag_ids' });
|
||||||
|
if (!setting) {
|
||||||
|
setting = this.settingRepo.create({
|
||||||
|
Typ: 0,
|
||||||
|
Tag: 'steuertag_ids',
|
||||||
|
Wert: ids.join(','),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setting.Wert = ids.join(',');
|
||||||
|
}
|
||||||
|
await this.settingRepo.save(setting);
|
||||||
|
return { ids };
|
||||||
|
}
|
||||||
|
|
||||||
// === Korrespondenten ===
|
// === Korrespondenten ===
|
||||||
@Get('correspondents')
|
@Get('correspondents')
|
||||||
async getCorrespondents(
|
async getCorrespondents(
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export interface PaperlessUser {
|
|||||||
|
|
||||||
export const paperlessApi = {
|
export const paperlessApi = {
|
||||||
getTags: () => api.get<PaperlessTag[]>('/api/paperless/tags').then(r => r.data),
|
getTags: () => api.get<PaperlessTag[]>('/api/paperless/tags').then(r => r.data),
|
||||||
|
getSteuertagIds: () => api.get<{ ids: number[] }>('/api/paperless/steuertags').then(r => r.data.ids),
|
||||||
getDocumentTypes: () => api.get<PaperlessDocType[]>('/api/paperless/document-types').then(r => r.data),
|
getDocumentTypes: () => api.get<PaperlessDocType[]>('/api/paperless/document-types').then(r => r.data),
|
||||||
getCustomFields: () => api.get<PaperlessCustomField[]>('/api/paperless/custom-fields').then(r => r.data),
|
getCustomFields: () => api.get<PaperlessCustomField[]>('/api/paperless/custom-fields').then(r => r.data),
|
||||||
getCorrespondents: (search?: string) => api.get<PaperlessCorrespondent[]>('/api/paperless/correspondents', { params: { search } }).then(r => r.data),
|
getCorrespondents: (search?: string) => api.get<PaperlessCorrespondent[]>('/api/paperless/correspondents', { params: { search } }).then(r => r.data),
|
||||||
|
|||||||
@@ -145,6 +145,11 @@ export const settingsApi = {
|
|||||||
deleteUserClient: (id: number) =>
|
deleteUserClient: (id: number) =>
|
||||||
api.delete(`/api/settings/user-clients/${id}`).then(r => r.data),
|
api.delete(`/api/settings/user-clients/${id}`).then(r => r.data),
|
||||||
|
|
||||||
|
// Steuertags
|
||||||
|
getSteuertagIds: () => api.get<{ ids: number[] }>('/api/settings/steuertags').then(r => r.data.ids),
|
||||||
|
updateSteuertagIds: (ids: number[]) =>
|
||||||
|
api.put<{ ids: number[] }>('/api/settings/steuertags', { ids }).then(r => r.data.ids),
|
||||||
|
|
||||||
// Korrespondenten
|
// Korrespondenten
|
||||||
getCorrespondents: (page = 1, pageSize = 50, search?: string) =>
|
getCorrespondents: (page = 1, pageSize = 50, search?: string) =>
|
||||||
api.get<{ data: any[], total: number }>('/api/settings/correspondents', { params: { page, pageSize, search } }).then(r => r.data),
|
api.get<{ data: any[], total: number }>('/api/settings/correspondents', { params: { page, pageSize, search } }).then(r => r.data),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider } from 'antd';
|
import { Modal, Form, Select, DatePicker, Input, Spin, message, Row, Col, Button, Space, Divider, Tag } from 'antd';
|
||||||
import { PlusOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
|
import { PlusOutlined, EyeOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { posteingangApi } from '../api/posteingang';
|
import { posteingangApi } from '../api/posteingang';
|
||||||
@@ -7,7 +7,7 @@ import type { DocumentRequirement, PosteingangDocument, Kontonummer } from '../a
|
|||||||
import { clientsApi } from '../api/inbox';
|
import { clientsApi } from '../api/inbox';
|
||||||
import type { Client } from '../api/inbox';
|
import type { Client } from '../api/inbox';
|
||||||
import { paperlessApi } from '../api/paperless';
|
import { paperlessApi } from '../api/paperless';
|
||||||
import type { PaperlessDocType, PaperlessCorrespondent } from '../api/paperless';
|
import type { PaperlessDocType, PaperlessCorrespondent, PaperlessTag } from '../api/paperless';
|
||||||
import { getEnv } from '../utils/env';
|
import { getEnv } from '../utils/env';
|
||||||
import { AuthIframe, openAuthUrl } from '../utils/auth-resource';
|
import { AuthIframe, openAuthUrl } from '../utils/auth-resource';
|
||||||
import DocumentSearchModal from './DocumentSearchModal';
|
import DocumentSearchModal from './DocumentSearchModal';
|
||||||
@@ -35,6 +35,11 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
|||||||
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
|
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
|
||||||
const [requirements, setRequirements] = useState<DocumentRequirement[]>([]);
|
const [requirements, setRequirements] = useState<DocumentRequirement[]>([]);
|
||||||
|
|
||||||
|
// Tags: alle Tags + als Steuertags markierte IDs; bearbeitbar sind nur Inhaltstags
|
||||||
|
const [allTags, setAllTags] = useState<PaperlessTag[]>([]);
|
||||||
|
const [steuertagIds, setSteuertagIds] = useState<number[]>([]);
|
||||||
|
const contentTags = allTags.filter(t => !steuertagIds.includes(t.id));
|
||||||
|
|
||||||
const [kontonummerMissing, setKontonummerMissing] = useState<{ correspondentId: number, nummer: string } | null>(null);
|
const [kontonummerMissing, setKontonummerMissing] = useState<{ correspondentId: number, nummer: string } | null>(null);
|
||||||
const [docTitles, setDocTitles] = useState<Record<number, string>>({});
|
const [docTitles, setDocTitles] = useState<Record<number, string>>({});
|
||||||
const [searchModalOpen, setSearchModalOpen] = useState<{ field: string, reqId: number } | null>(null);
|
const [searchModalOpen, setSearchModalOpen] = useState<{ field: string, reqId: number } | null>(null);
|
||||||
@@ -140,6 +145,14 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
|||||||
}
|
}
|
||||||
}, [document, form, open]);
|
}, [document, form, open]);
|
||||||
|
|
||||||
|
// Tags getrennt setzen: nur Inhaltstags (Steuertags werden nicht angezeigt/bearbeitet)
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && document) {
|
||||||
|
const contentTagIds = (document.tags || []).filter(id => !steuertagIds.includes(id));
|
||||||
|
form.setFieldValue('tags', contentTagIds);
|
||||||
|
}
|
||||||
|
}, [document, open, steuertagIds, form]);
|
||||||
|
|
||||||
const ensureCorrespondentInList = async (correspondentId: number | null | undefined) => {
|
const ensureCorrespondentInList = async (correspondentId: number | null | undefined) => {
|
||||||
if (!correspondentId) return;
|
if (!correspondentId) return;
|
||||||
|
|
||||||
@@ -178,14 +191,18 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
|||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const [clientsData, docTypesData, correspondentsData] = await Promise.all([
|
const [clientsData, docTypesData, correspondentsData, tagsData, steuertagData] = await Promise.all([
|
||||||
clientsApi.getMyClients(),
|
clientsApi.getMyClients(),
|
||||||
paperlessApi.getDocumentTypes(),
|
paperlessApi.getDocumentTypes(),
|
||||||
paperlessApi.getCorrespondents()
|
paperlessApi.getCorrespondents(),
|
||||||
|
paperlessApi.getTags(),
|
||||||
|
paperlessApi.getSteuertagIds(),
|
||||||
]);
|
]);
|
||||||
setClients(clientsData);
|
setClients(clientsData);
|
||||||
setDocumentTypes(docTypesData);
|
setDocumentTypes(docTypesData);
|
||||||
setCorrespondents(correspondentsData);
|
setCorrespondents(correspondentsData);
|
||||||
|
setAllTags(tagsData);
|
||||||
|
setSteuertagIds(steuertagData);
|
||||||
|
|
||||||
// If document is already there, ensure its correspondent is in the list
|
// If document is already there, ensure its correspondent is in the list
|
||||||
if (document?.correspondent) {
|
if (document?.correspondent) {
|
||||||
@@ -258,6 +275,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
|||||||
correspondent: values.correspondent,
|
correspondent: values.correspondent,
|
||||||
title: values.title,
|
title: values.title,
|
||||||
date: values.belegdatum ? values.belegdatum.format('YYYY-MM-DD') : null,
|
date: values.belegdatum ? values.belegdatum.format('YYYY-MM-DD') : null,
|
||||||
|
tags: values.tags || [],
|
||||||
customFields: customFieldsObj,
|
customFields: customFieldsObj,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -373,6 +391,36 @@ export default function DocumentEditModal({ documentId, document, open, onClose,
|
|||||||
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="tags" label="Tags">
|
||||||
|
<Select
|
||||||
|
mode="multiple"
|
||||||
|
allowClear
|
||||||
|
showSearch
|
||||||
|
optionFilterProp="label"
|
||||||
|
placeholder="Tags auswählen"
|
||||||
|
options={contentTags.map(t => ({ value: t.id, label: t.name }))}
|
||||||
|
tagRender={({ value, closable, onClose }) => {
|
||||||
|
const tag = allTags.find(t => t.id === value);
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color={tag?.color}
|
||||||
|
style={{ color: tag?.text_color, marginInlineEnd: 4 }}
|
||||||
|
closable={closable}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
{tag?.name ?? value}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
optionRender={(opt) => {
|
||||||
|
const tag = contentTags.find(t => t.id === opt.value);
|
||||||
|
return tag ? (
|
||||||
|
<Tag color={tag.color} style={{ color: tag.text_color }}>{tag.name}</Tag>
|
||||||
|
) : opt.label;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
{/* Rendering dynamic requirements based on DocumentType */}
|
{/* Rendering dynamic requirements based on DocumentType */}
|
||||||
{requirements.map(req => {
|
{requirements.map(req => {
|
||||||
if (req.feldId === '2' || (req.isCustomField && req.customFieldIndex === 9)) {
|
if (req.feldId === '2' || (req.isCustomField && req.customFieldIndex === 9)) {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { useEffect, useState } from 'react';
|
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;
|
const { Title } = Typography;
|
||||||
import { ReloadOutlined } from '@ant-design/icons';
|
import { ReloadOutlined } from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { posteingangApi } from '../api/posteingang';
|
import { posteingangApi } from '../api/posteingang';
|
||||||
import type { PosteingangDocument } 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 DocumentEditModal from '../components/DocumentEditModal';
|
||||||
import { getEnv } from '../utils/env';
|
import { getEnv } from '../utils/env';
|
||||||
import { AuthImage } from '../utils/auth-resource';
|
import { AuthImage } from '../utils/auth-resource';
|
||||||
@@ -15,6 +17,8 @@ export default function ManuellBearbeitenPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||||
const [selectedDoc, setSelectedDoc] = useState<PosteingangDocument | null>(null);
|
const [selectedDoc, setSelectedDoc] = useState<PosteingangDocument | null>(null);
|
||||||
|
const [allTags, setAllTags] = useState<PaperlessTag[]>([]);
|
||||||
|
const [steuertagIds, setSteuertagIds] = useState<number[]>([]);
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
setLoading(true);
|
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(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
// Refresh interval every 30 seconds
|
// Refresh interval every 30 seconds
|
||||||
@@ -84,6 +97,26 @@ export default function ManuellBearbeitenPage() {
|
|||||||
dataIndex: 'title',
|
dataIndex: 'title',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
width: '35%',
|
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',
|
title: 'Eingangsdatum',
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
PlusOutlined, DeleteOutlined, EditOutlined, CloudUploadOutlined,
|
||||||
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
HistoryOutlined, MinusCircleOutlined, CopyOutlined, KeyOutlined,
|
||||||
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, GlobalOutlined,
|
QrcodeOutlined, UnorderedListOutlined, PrinterOutlined, GlobalOutlined,
|
||||||
|
TagsOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { FormInstance } from 'antd';
|
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
|
// Settings Page
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -2699,6 +2780,11 @@ export default function SettingsPage() {
|
|||||||
label: <span><UserOutlined /> Korrespondenten</span>,
|
label: <span><UserOutlined /> Korrespondenten</span>,
|
||||||
children: <CorrespondentsTab />,
|
children: <CorrespondentsTab />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'steuertags',
|
||||||
|
label: <span><TagsOutlined /> Steuertags</span>,
|
||||||
|
children: <SteuertagsTab />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'export-targets',
|
key: 'export-targets',
|
||||||
label: <span><CloudUploadOutlined /> Export-Ziele</span>,
|
label: <span><CloudUploadOutlined /> Export-Ziele</span>,
|
||||||
|
|||||||
Reference in New Issue
Block a user