diff --git a/paperless-backend/src/paperless/paperless.controller.ts b/paperless-backend/src/paperless/paperless.controller.ts index 2cc83c5..0b719cc 100644 --- a/paperless-backend/src/paperless/paperless.controller.ts +++ b/paperless-backend/src/paperless/paperless.controller.ts @@ -29,6 +29,10 @@ import { Task } from '../database/entities/task.entity'; import { Document } from '../database/entities/document.entity'; import { DocumentField } from '../database/entities/document-field.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') export class PaperlessController { @@ -44,8 +48,26 @@ export class PaperlessController { private readonly documentFieldRepo: Repository, @InjectRepository(DocumentType) private readonly documentTypeRepo: Repository, + @InjectRepository(Setting) + private readonly settingRepo: Repository, ) {} + /** Liest die als Steuertags markierten Tag-IDs aus den Einstellungen. */ + private async getSteuertagIds(): Promise { + 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') async checksumExists(@Body('checksum') checksum: string) { const exists = await this.paperlessService.checksumExists(checksum); @@ -390,6 +412,19 @@ export class PaperlessController { 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) { oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1); if (docType?.TagNotReady) diff --git a/paperless-backend/src/paperless/paperless.module.ts b/paperless-backend/src/paperless/paperless.module.ts index 7b655cd..eb5db29 100644 --- a/paperless-backend/src/paperless/paperless.module.ts +++ b/paperless-backend/src/paperless/paperless.module.ts @@ -9,6 +9,7 @@ import { DocumentField } from '../database/entities/document-field.entity'; import { Task } from '../database/entities/task.entity'; import { Document } from '../database/entities/document.entity'; import { Attachment } from '../database/entities/attachment.entity'; +import { Setting } from '../database/entities/setting.entity'; import { PostprocessingModule } from '../postprocessing/postprocessing.module'; import { AuthModule } from '../auth/auth.module'; @@ -20,6 +21,7 @@ import { AuthModule } from '../auth/auth.module'; Task, Document, Attachment, + Setting, ]), forwardRef(() => PostprocessingModule), AuthModule, diff --git a/paperless-backend/src/settings/settings.controller.ts b/paperless-backend/src/settings/settings.controller.ts index 01d6115..67fc92b 100644 --- a/paperless-backend/src/settings/settings.controller.ts +++ b/paperless-backend/src/settings/settings.controller.ts @@ -374,6 +374,36 @@ export class SettingsController { 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 === @Get('correspondents') async getCorrespondents( diff --git a/paperless-frontend/src/api/paperless.ts b/paperless-frontend/src/api/paperless.ts index 09cfb6d..62cd1a9 100644 --- a/paperless-frontend/src/api/paperless.ts +++ b/paperless-frontend/src/api/paperless.ts @@ -49,6 +49,7 @@ export interface PaperlessUser { export const paperlessApi = { getTags: () => api.get('/api/paperless/tags').then(r => r.data), + getSteuertagIds: () => api.get<{ ids: number[] }>('/api/paperless/steuertags').then(r => r.data.ids), getDocumentTypes: () => api.get('/api/paperless/document-types').then(r => r.data), getCustomFields: () => api.get('/api/paperless/custom-fields').then(r => r.data), getCorrespondents: (search?: string) => api.get('/api/paperless/correspondents', { params: { search } }).then(r => r.data), diff --git a/paperless-frontend/src/api/settings.ts b/paperless-frontend/src/api/settings.ts index ec392e5..89a12d5 100644 --- a/paperless-frontend/src/api/settings.ts +++ b/paperless-frontend/src/api/settings.ts @@ -145,6 +145,11 @@ export const settingsApi = { deleteUserClient: (id: number) => 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 getCorrespondents: (page = 1, pageSize = 50, search?: string) => api.get<{ data: any[], total: number }>('/api/settings/correspondents', { params: { page, pageSize, search } }).then(r => r.data), diff --git a/paperless-frontend/src/components/DocumentEditModal.tsx b/paperless-frontend/src/components/DocumentEditModal.tsx index 7943695..433c24d 100644 --- a/paperless-frontend/src/components/DocumentEditModal.tsx +++ b/paperless-frontend/src/components/DocumentEditModal.tsx @@ -1,5 +1,5 @@ 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 dayjs from 'dayjs'; import { posteingangApi } from '../api/posteingang'; @@ -7,7 +7,7 @@ import type { DocumentRequirement, PosteingangDocument, Kontonummer } from '../a import { clientsApi } from '../api/inbox'; import type { Client } from '../api/inbox'; 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 { AuthIframe, openAuthUrl } from '../utils/auth-resource'; import DocumentSearchModal from './DocumentSearchModal'; @@ -35,6 +35,11 @@ export default function DocumentEditModal({ documentId, document, open, onClose, const [correspondents, setCorrespondents] = useState([]); const [requirements, setRequirements] = useState([]); + // Tags: alle Tags + als Steuertags markierte IDs; bearbeitbar sind nur Inhaltstags + const [allTags, setAllTags] = useState([]); + const [steuertagIds, setSteuertagIds] = useState([]); + const contentTags = allTags.filter(t => !steuertagIds.includes(t.id)); + const [kontonummerMissing, setKontonummerMissing] = useState<{ correspondentId: number, nummer: string } | null>(null); const [docTitles, setDocTitles] = useState>({}); 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]); + // 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) => { if (!correspondentId) return; @@ -178,14 +191,18 @@ export default function DocumentEditModal({ documentId, document, open, onClose, const loadInitialData = async () => { setLoading(true); try { - const [clientsData, docTypesData, correspondentsData] = await Promise.all([ + const [clientsData, docTypesData, correspondentsData, tagsData, steuertagData] = await Promise.all([ clientsApi.getMyClients(), paperlessApi.getDocumentTypes(), - paperlessApi.getCorrespondents() + paperlessApi.getCorrespondents(), + paperlessApi.getTags(), + paperlessApi.getSteuertagIds(), ]); setClients(clientsData); setDocumentTypes(docTypesData); setCorrespondents(correspondentsData); + setAllTags(tagsData); + setSteuertagIds(steuertagData); // If document is already there, ensure its correspondent is in the list if (document?.correspondent) { @@ -258,6 +275,7 @@ export default function DocumentEditModal({ documentId, document, open, onClose, correspondent: values.correspondent, title: values.title, date: values.belegdatum ? values.belegdatum.format('YYYY-MM-DD') : null, + tags: values.tags || [], customFields: customFieldsObj, }; @@ -373,6 +391,36 @@ export default function DocumentEditModal({ documentId, document, open, onClose, + + ({ value: t.id, label: t.name }))} + optionRender={(opt) => { + const tag = tags.find((t) => t.id === opt.value); + return tag ? ( + {tag.name} + ) : ( + opt.label + ); + }} + /> +
+ +
+ + ); +} + // ═══════════════════════════════════════════════════════════════════ // Settings Page // ═══════════════════════════════════════════════════════════════════ @@ -2699,6 +2780,11 @@ export default function SettingsPage() { label: Korrespondenten, children: , }, + { + key: 'steuertags', + label: Steuertags, + children: , + }, { key: 'export-targets', label: Export-Ziele,