Freigabe #4

Merged
bjoernpoettker merged 23 commits from Freigabe into main 2026-06-16 14:49:23 +00:00
8 changed files with 245 additions and 5 deletions
Showing only changes of commit d96e06e86d - Show all commits
@@ -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<DocumentField>,
@InjectRepository(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')
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)
@@ -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,
@@ -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(
+1
View File
@@ -49,6 +49,7 @@ export interface PaperlessUser {
export const paperlessApi = {
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),
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),
+5
View File
@@ -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),
@@ -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<PaperlessCorrespondent[]>([]);
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 [docTitles, setDocTitles] = useState<Record<number, string>>({});
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,
<DatePicker style={{ width: '100%' }} format="DD.MM.YYYY" />
</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 */}
{requirements.map(req => {
if (req.feldId === '2' || (req.isCustomField && req.customFieldIndex === 9)) {
@@ -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>,