Freigabe #4
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user