feat: implement Freigabesystem for payment approval workflow
Adds a dedicated approval view for PM_Freigabe users to release documents for payment by setting Paperless custom field 15 to a predefined value. - Backend: VIEW_FREIGABE permission mapped to PM_Freigabe OIDC group - Backend: FreigabeErforderlich flag on DocumentType entity (auto-migrated) - Backend: FreigabeModule with endpoints to list documents, fetch field options dynamically from Paperless, and set the approval custom field - Frontend: /freigabe route with filter (default: nicht freigegeben), paginated table, and modal to select approval value - Frontend: Settings checkbox to mark document types as requiring approval - Frontend: Freigabe menu item visible only to PM_Freigabe/PM_Admin users Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
72d199fb3a
commit
37ffc6c13b
13 files changed
+438
No files matched your search
@@ -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 }));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user