Freigabe #4

Merged
bjoernpoettker merged 23 commits from Freigabe into main 2026-06-16 14:49:23 +00:00
13 changed files with 438 additions and 0 deletions
Showing only changes of commit 37ffc6c13b - Show all commits
+2
View File
@@ -19,6 +19,7 @@ import { InboxPostprocessorModule } from './inbox-postprocessor/inbox-postproces
import { UserSettingsModule } from './user-settings/user-settings.module';
import { LabelPrintAgentModule } from './label-print-agent/label-print-agent.module';
import { AgrarmonitorModule } from './agrarmonitor/agrarmonitor.module';
import { FreigabeModule } from './freigabe/freigabe.module';
import * as path from 'path';
@Module({
@@ -49,6 +50,7 @@ import * as path from 'path';
UserSettingsModule,
LabelPrintAgentModule,
AgrarmonitorModule,
FreigabeModule,
],
})
export class AppModule {}
@@ -5,6 +5,7 @@ export const Permission = {
VIEW_INBOX: 'VIEW_INBOX',
VIEW_SCANNER: 'VIEW_SCANNER',
MANAGE_SETTINGS: 'MANAGE_SETTINGS',
VIEW_FREIGABE: 'VIEW_FREIGABE',
} as const;
export type Permission = typeof Permission[keyof typeof Permission];
@@ -23,6 +24,7 @@ export function mapGroupsToPermissions(groups: string[] | undefined | null): Per
permissions.add(Permission.VIEW_INBOX);
permissions.add(Permission.VIEW_SCANNER);
permissions.add(Permission.MANAGE_SETTINGS);
permissions.add(Permission.VIEW_FREIGABE);
return Array.from(permissions);
}
@@ -30,6 +32,7 @@ export function mapGroupsToPermissions(groups: string[] | undefined | null): Per
if (groups.includes('PM_Maileingang')) permissions.add(Permission.VIEW_MAIL);
if (groups.includes('PM_Posteingang')) permissions.add(Permission.VIEW_INBOX);
if (groups.includes('PM_Scanner')) permissions.add(Permission.VIEW_SCANNER);
if (groups.includes('PM_Freigabe')) permissions.add(Permission.VIEW_FREIGABE);
return Array.from(permissions);
}
@@ -16,4 +16,7 @@ export class DocumentType {
@Column({ type: 'int', nullable: true })
TagReady!: number | null;
@Column({ type: 'tinyint', width: 1, nullable: true, default: null })
FreigabeErforderlich!: boolean | null;
}
@@ -0,0 +1,36 @@
import { Controller, Get, Put, Param, Body, Query } from '@nestjs/common';
import { RequirePermissions } from '../auth/permissions.decorator';
import { Permission } from '../auth/permissions.enum';
import { FreigabeService } from './freigabe.service';
@Controller('api/freigabe')
@RequirePermissions(Permission.VIEW_FREIGABE)
export class FreigabeController {
constructor(private readonly freigabeService: FreigabeService) {}
@Get('documents')
getDocuments(
@Query('page') page = '1',
@Query('pageSize') pageSize = '25',
@Query('nurNichtFreigegeben') nurNichtFreigegeben = 'true',
) {
return this.freigabeService.getFreigabeDocuments(
parseInt(page, 10),
Math.min(parseInt(pageSize, 10), 100),
nurNichtFreigegeben !== 'false',
);
}
@Put('documents/:id/freigabe')
setFreigabe(
@Param('id') id: string,
@Body('value') value: string | null,
) {
return this.freigabeService.setFreigabe(parseInt(id, 10), value ?? null);
}
@Get('options')
getOptions() {
return this.freigabeService.getFreigabeOptions();
}
}
@@ -0,0 +1,16 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DocumentType } from '../database/entities/document-type.entity';
import { PaperlessModule } from '../paperless/paperless.module';
import { FreigabeController } from './freigabe.controller';
import { FreigabeService } from './freigabe.service';
@Module({
imports: [
TypeOrmModule.forFeature([DocumentType]),
PaperlessModule,
],
controllers: [FreigabeController],
providers: [FreigabeService],
})
export class FreigabeModule {}
@@ -0,0 +1,106 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { DocumentType } from '../database/entities/document-type.entity';
import { PaperlessService } from '../paperless/paperless.service';
const FREIGABE_FIELD_ID = 15;
@Injectable()
export class FreigabeService {
private readonly logger = new Logger(FreigabeService.name);
constructor(
@InjectRepository(DocumentType)
private readonly documentTypeRepo: Repository<DocumentType>,
private readonly paperlessService: PaperlessService,
) {}
async getFreigabeDocuments(page: number, pageSize: number, nurNichtFreigegeben: boolean) {
const docTypes = await this.documentTypeRepo.find({
where: { FreigabeErforderlich: true as any },
});
if (docTypes.length === 0) {
return { count: 0, results: [] };
}
const docTypeIds = docTypes.map((dt) => dt.DocumentTypeId).join(',');
const params: Record<string, any> = {
page,
page_size: pageSize,
document_type__id__in: docTypeIds,
ordering: '-created',
truncate_content: true,
};
if (nurNichtFreigegeben) {
// Filter für Belege, bei denen Custom Field 15 nicht gesetzt ist
params[`custom_fields__field_id`] = FREIGABE_FIELD_ID;
params[`custom_fields__value__isnull`] = true;
}
try {
const result = await this.paperlessService.getDocuments(params);
return result;
} catch (err: any) {
// Fallback: Paperless unterstützt den custom_fields-Filter möglicherweise nicht
// In diesem Fall alle Belege laden und client-seitig filtern
this.logger.warn('custom_fields Filter nicht unterstützt, lade alle Belege und filtere lokal');
const fallbackParams: Record<string, any> = {
page: 1,
page_size: 9999,
document_type__id__in: docTypeIds,
ordering: '-created',
truncate_content: true,
};
const allDocs = await this.paperlessService.getDocuments(fallbackParams);
const results: any[] = allDocs.results ?? [];
if (nurNichtFreigegeben) {
const filtered = results.filter((doc: any) => {
const cf = (doc.custom_fields ?? []).find((f: any) => f.field === FREIGABE_FIELD_ID);
return !cf || cf.value === null || cf.value === '' || cf.value === undefined;
});
const start = (page - 1) * pageSize;
return {
count: filtered.length,
results: filtered.slice(start, start + pageSize),
};
}
const start = (page - 1) * pageSize;
return {
count: results.length,
results: results.slice(start, start + pageSize),
};
}
}
async setFreigabe(documentId: number, value: string | null) {
const doc = await this.paperlessService.getDocument(documentId);
const customFields: any[] = [...(doc.custom_fields ?? [])];
const existing = customFields.find((f: any) => f.field === FREIGABE_FIELD_ID);
if (existing) {
existing.value = value;
} else if (value !== null && value !== '') {
customFields.push({ field: FREIGABE_FIELD_ID, value });
}
await this.paperlessService.updateDocument(documentId, { custom_fields: customFields });
return { success: true };
}
async getFreigabeOptions(): Promise<{ id: string; label: string }[]> {
const fields = await this.paperlessService.getCustomFields();
const field = (fields as any[]).find((f: any) => f.id === FREIGABE_FIELD_ID);
if (!field) return [];
const options: string[] = field.extra_data?.select_options ?? [];
return options
.filter((o) => o !== null && o !== undefined && o !== '')
.map((o) => ({ id: o, label: o }));
}
}
+2
View File
@@ -20,6 +20,7 @@ const SettingsPage = lazy(() => import('./pages/SettingsPage'));
const UserSettingsPage = lazy(() => import('./pages/UserSettingsPage'));
const LoginPage = lazy(() => import('./pages/LoginPage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const FreigabePage = lazy(() => import('./pages/FreigabePage'));
import { Permission } from './auth/permissions';
function UnauthorizedPage() {
@@ -131,6 +132,7 @@ function ThemedApp() {
<Route path="/mailpostfach" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailpostfachPage /></PermissionRoute>} />
<Route path="/mailpostfach/:id" element={<PermissionRoute permission={Permission.VIEW_MAIL}><MailDetailPage /></PermissionRoute>} />
<Route path="/settings" element={<PermissionRoute permission={Permission.MANAGE_SETTINGS}><SettingsPage /></PermissionRoute>} />
<Route path="/freigabe" element={<PermissionRoute permission={Permission.VIEW_FREIGABE}><FreigabePage /></PermissionRoute>} />
<Route path="/user-settings" element={<UserSettingsPage />} />
</Route>
</Routes>
+40
View File
@@ -0,0 +1,40 @@
import api from './client';
export interface FreigabeDocument {
id: number;
title: string;
created: string;
created_date: string;
correspondent: number | null;
document_type: number | null;
archive_serial_number: number | null;
tags: number[];
custom_fields: { field: number; value: any }[];
}
export interface FreigabeOption {
id: string;
label: string;
}
export interface FreigabeResult {
count: number;
results: FreigabeDocument[];
}
export const freigabeApi = {
getDocuments: (page = 1, pageSize = 25, nurNichtFreigegeben = true) =>
api
.get<FreigabeResult>('/api/freigabe/documents', {
params: { page, pageSize, nurNichtFreigegeben },
})
.then((r) => r.data),
setFreigabe: (docId: number, value: string | null) =>
api
.put<{ success: boolean }>(`/api/freigabe/documents/${docId}/freigabe`, { value })
.then((r) => r.data),
getOptions: () =>
api.get<FreigabeOption[]>('/api/freigabe/options').then((r) => r.data),
};
+1
View File
@@ -6,6 +6,7 @@ export interface SettingDocType {
TitelTemplate: string;
TagNotReady: number | null;
TagReady: number | null;
FreigabeErforderlich?: boolean | null;
}
export interface SettingDocField {
@@ -5,6 +5,7 @@ export const Permission = {
VIEW_INBOX: 'VIEW_INBOX',
VIEW_SCANNER: 'VIEW_SCANNER',
MANAGE_SETTINGS: 'MANAGE_SETTINGS',
VIEW_FREIGABE: 'VIEW_FREIGABE',
} as const;
export type Permission = typeof Permission[keyof typeof Permission];
@@ -24,6 +25,7 @@ export function mapGroupsToPermissions(groups: string[] | undefined | null): Per
permissions.add(Permission.VIEW_INBOX);
permissions.add(Permission.VIEW_SCANNER);
permissions.add(Permission.MANAGE_SETTINGS);
permissions.add(Permission.VIEW_FREIGABE);
return Array.from(permissions);
}
@@ -39,6 +41,9 @@ export function mapGroupsToPermissions(groups: string[] | undefined | null): Per
if (groups.includes('PM_Scanner')) {
permissions.add(Permission.VIEW_SCANNER);
}
if (groups.includes('PM_Freigabe')) {
permissions.add(Permission.VIEW_FREIGABE);
}
return Array.from(permissions);
}
@@ -13,6 +13,7 @@ import {
MoonOutlined,
AppstoreOutlined,
GlobalOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { useAuth } from '../auth/AuthContext';
import { useTheme } from '../theme/ThemeContext';
@@ -38,6 +39,7 @@ const allMenuItems: MenuItemDef[] = [
{ key: '/manuell', icon: <EditOutlined />, label: 'Manuell bearbeiten', permission: Permission.PROCESS_MANUALLY, countKey: 'manuell' },
{ key: '/mailpostfach', icon: <MailOutlined />, label: 'Mailpostfach', permission: Permission.VIEW_MAIL, countKey: 'mailpostfach' },
{ key: 'agrarmonitor', icon: <GlobalOutlined />, label: 'In Agrarmonitor', permission: Permission.PROCESS_MANUALLY, countKey: 'agrarmonitor', externalUrl: 'https://admin7.agrarmonitor.de/dateien/eingang#dateien' },
{ key: '/freigabe', icon: <CheckCircleOutlined />, label: 'Freigabe', permission: Permission.VIEW_FREIGABE },
{ key: '/settings', icon: <SettingOutlined />, label: 'Einstellungen', permission: Permission.MANAGE_SETTINGS },
];
@@ -0,0 +1,213 @@
import { useEffect, useState, useCallback } from 'react';
import {
Table, Typography, Tag, Button, Modal, Select, message, Space, Radio,
} from 'antd';
import { CheckCircleOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import dayjs from 'dayjs';
import { freigabeApi, type FreigabeDocument, type FreigabeOption } from '../api/freigabe';
import { paperlessApi, type PaperlessDocType, type PaperlessCorrespondent } from '../api/paperless';
const { Title } = Typography;
const FREIGABE_FIELD_ID = 15;
export default function FreigabePage() {
const [data, setData] = useState<FreigabeDocument[]>([]);
const [total, setTotal] = useState(0);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(25);
const [nurNichtFreigegeben, setNurNichtFreigegeben] = useState(true);
const [docTypes, setDocTypes] = useState<PaperlessDocType[]>([]);
const [correspondents, setCorrespondents] = useState<PaperlessCorrespondent[]>([]);
const [freigabeOptions, setFreigabeOptions] = useState<FreigabeOption[]>([]);
const [selectedDoc, setSelectedDoc] = useState<FreigabeDocument | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
useEffect(() => {
Promise.all([
paperlessApi.getDocumentTypes(),
paperlessApi.getCorrespondents(),
freigabeApi.getOptions(),
]).then(([dts, corrs, opts]) => {
setDocTypes(dts);
setCorrespondents(corrs);
setFreigabeOptions(opts);
});
}, []);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const result = await freigabeApi.getDocuments(page, pageSize, nurNichtFreigegeben);
setData(result.results ?? []);
setTotal(result.count ?? 0);
} catch {
message.error('Fehler beim Laden der Belege');
} finally {
setLoading(false);
}
}, [page, pageSize, nurNichtFreigegeben]);
useEffect(() => {
fetchData();
}, [fetchData]);
const getDocTypeName = (id: number | null) => {
if (!id) return '—';
return docTypes.find((d) => d.id === id)?.name ?? String(id);
};
const getCorrespondentName = (id: number | null) => {
if (!id) return '—';
return correspondents.find((c) => c.id === id)?.name ?? String(id);
};
const getFreigabeValue = (doc: FreigabeDocument) => {
const cf = doc.custom_fields?.find((f) => f.field === FREIGABE_FIELD_ID);
if (!cf || cf.value === null || cf.value === '' || cf.value === undefined) {
return <Tag color="warning">Nicht gesetzt</Tag>;
}
const opt = freigabeOptions.find((o) => String(o.id) === String(cf.value));
return <Tag color="success">{opt?.label ?? String(cf.value)}</Tag>;
};
const openModal = (doc: FreigabeDocument) => {
const cf = doc.custom_fields?.find((f) => f.field === FREIGABE_FIELD_ID);
setSelectedDoc(doc);
setSelectedValue(cf?.value ?? null);
setModalOpen(true);
};
const handleFreigabe = async () => {
if (!selectedDoc) return;
setSaving(true);
try {
await freigabeApi.setFreigabe(selectedDoc.id, selectedValue);
message.success('Freigabe gesetzt');
setModalOpen(false);
setSelectedDoc(null);
fetchData();
} catch {
message.error('Fehler beim Speichern der Freigabe');
} finally {
setSaving(false);
}
};
const columns: ColumnsType<FreigabeDocument> = [
{
title: 'Dokumenttyp',
dataIndex: 'document_type',
key: 'doctype',
render: getDocTypeName,
},
{
title: 'Titel',
dataIndex: 'title',
key: 'title',
ellipsis: true,
},
{
title: 'Erstellt',
dataIndex: 'created_date',
key: 'created',
width: 110,
render: (v: string) => v ? dayjs(v).format('DD.MM.YYYY') : '—',
},
{
title: 'Absender',
dataIndex: 'correspondent',
key: 'correspondent',
render: getCorrespondentName,
},
{
title: 'Freigabe',
key: 'freigabe',
width: 140,
render: (_, doc) => getFreigabeValue(doc),
},
{
title: '',
key: 'action',
width: 130,
render: (_, doc) => (
<Button
icon={<CheckCircleOutlined />}
size="small"
type="primary"
onClick={() => openModal(doc)}
>
Freigabe setzen
</Button>
),
},
];
return (
<>
<Title level={4} style={{ marginTop: 0, marginBottom: 16 }}>Freigabe</Title>
<Space style={{ marginBottom: 16 }}>
<Radio.Group
value={nurNichtFreigegeben}
onChange={(e) => {
setPage(1);
setNurNichtFreigegeben(e.target.value);
}}
optionType="button"
buttonStyle="solid"
>
<Radio.Button value={true}>Nicht freigegeben</Radio.Button>
<Radio.Button value={false}>Alle</Radio.Button>
</Radio.Group>
</Space>
<Table<FreigabeDocument>
dataSource={data}
columns={columns}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: page,
pageSize,
total,
showSizeChanger: true,
pageSizeOptions: ['25', '50', '100'],
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
showTotal: (t) => `${t} Belege`,
}}
/>
<Modal
title="Freigabe setzen"
open={modalOpen}
onOk={handleFreigabe}
onCancel={() => { setModalOpen(false); setSelectedDoc(null); }}
okText="Speichern"
cancelText="Abbrechen"
confirmLoading={saving}
>
<p style={{ marginBottom: 12 }}>
<strong>{selectedDoc?.title}</strong>
</p>
<Select
style={{ width: '100%' }}
placeholder="Freigabe-Status wählen"
allowClear
value={selectedValue ?? undefined}
onChange={(v) => setSelectedValue(v ?? null)}
options={freigabeOptions.map((o) => ({ value: o.id, label: o.label }))}
/>
</Modal>
</>
);
}
@@ -579,6 +579,12 @@ function DocTypesTab() {
{ title: 'Titel-Template', dataIndex: 'TitelTemplate', key: 'template', ellipsis: true },
{ title: 'Tag (nicht bereit)', dataIndex: 'TagNotReady', key: 'tagNR', render: getTagName },
{ title: 'Tag (bereit)', dataIndex: 'TagReady', key: 'tagR', render: getTagName },
{
title: 'Freigabe erf.',
dataIndex: 'FreigabeErforderlich',
key: 'freigabe',
render: (v: boolean | null) => v ? <Tag color="blue">Ja</Tag> : '—',
},
{
title: '',
key: 'actions',
@@ -624,6 +630,9 @@ function DocTypesTab() {
{tags.map(t => <Select.Option key={t.id} value={t.id}>{t.name}</Select.Option>)}
</Select>
</Form.Item>
<Form.Item name="FreigabeErforderlich" valuePropName="checked" label="Freigabe erforderlich">
<Checkbox />
</Form.Item>
</Form>
{editing && (