import { Controller, Get, Param, Post, Put, Delete, UseGuards, UseInterceptors, UploadedFile, Body, Logger, HttpException, HttpStatus, Res, Query } from '@nestjs/common'; import type { Response } from 'express'; import { Public } from '../auth/public.decorator'; import { RequirePermissions } from '../auth/permissions.decorator'; import { Permission } from '../auth/permissions.enum'; import { FileInterceptor } from '@nestjs/platform-express'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { PaperlessService } from './paperless.service'; import { ApiKeyGuard } from '../auth/api-key.guard'; import { UploadExternalDto } from './dto/upload-external.dto'; 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'; @Controller('api/paperless') export class PaperlessController { private readonly logger = new Logger(PaperlessController.name); constructor( private readonly paperlessService: PaperlessService, @InjectRepository(Task) private readonly taskRepo: Repository, @InjectRepository(Document) private readonly documentRepo: Repository, @InjectRepository(DocumentField) private readonly documentFieldRepo: Repository, @InjectRepository(DocumentType) private readonly documentTypeRepo: Repository, ) {} @Post('checksum') async checksumExists(@Body('checksum') checksum: string) { const exists = await this.paperlessService.checksumExists(checksum); return { exists }; } @Get('documents') async getDocuments( @Query('search') search?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, ) { const params: any = { page: page || '1', page_size: pageSize || '20', }; if (search) { params.query = search; // Global search in Paperless } return this.paperlessService.getDocuments(params); } @Get('documents/:id') async getDocument(@Param('id') id: string) { return this.paperlessService.getDocument(parseInt(id, 10)); } @Get('tags') async getTags() { return this.paperlessService.getTags(); } @Get('tags/:id') async getTag(@Param('id') id: string) { // If the service doesn't have getTag(id), I should add it or just fetch all and find const tags = await this.paperlessService.getTags(); return tags.find(t => t.id === parseInt(id, 10)); } @Get('document-types') async getDocumentTypes() { return this.paperlessService.getDocumentTypes(); } @Get('custom-fields') async getCustomFields() { return this.paperlessService.getCustomFields(); } @Get('users') async getUsers() { return this.paperlessService.getUsers(); } @Get('correspondents') async getCorrespondents(@Query('search') search?: string) { const params: any = { page_size: 100 }; if (search) { params.name__icontains = search; } const response = await this.paperlessService.getCorrespondents(params); return response.results; } @Get('correspondents/:id') async getCorrespondent(@Param('id') id: string) { return this.paperlessService.getCorrespondent(parseInt(id, 10)); } @Get('inbox/list') @RequirePermissions(Permission.VIEW_INBOX) async getInboxList() { const documents = await this.paperlessService.getInboxDocuments(); // In old C# logic: only return docs where archive_serial_number is not null return documents .filter((doc: any) => doc.archive_serial_number !== null && doc.archive_serial_number !== undefined) .map((doc: any) => ({ id: doc.id, title: doc.title, asn: doc.archive_serial_number, documentType: doc.document_type, correspondent: doc.correspondent, created: doc.created_date, added: doc.added, tags: doc.tags, customFields: doc.custom_fields, owner: doc.owner, })); } @Get('manuell/list') @RequirePermissions(Permission.PROCESS_MANUALLY) async getManuellList() { const documents = await this.paperlessService.getManuellDocuments(); return documents .filter((doc: any) => doc.archive_serial_number !== null && doc.archive_serial_number !== undefined) .map((doc: any) => ({ id: doc.id, title: doc.title, asn: doc.archive_serial_number, documentType: doc.document_type, correspondent: doc.correspondent, created: doc.created_date, added: doc.added, tags: doc.tags, customFields: doc.custom_fields, owner: doc.owner, })); } @Public() @Get('inbox/preview/:id') async getInboxPreview(@Param('id') id: string, @Res() res: Response) { try { const stream = await this.paperlessService.getDocumentPreviewStream(parseInt(id, 10)); res.set({ 'Content-Type': 'image/png', 'Content-Disposition': `inline; filename="${id}preview.png"`, }); stream.pipe(res); } catch (error) { if (!res.headersSent) { res.status(HttpStatus.INTERNAL_SERVER_ERROR).send('Error fetching preview'); } } } @Public() @Get('inbox/pdf/:id') async getInboxPdf(@Param('id') id: string, @Res() res: Response) { try { const stream = await this.paperlessService.getDocumentPdfStream(parseInt(id, 10)); res.set({ 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="${id}.pdf"`, }); stream.pipe(res); } catch (error) { if (!res.headersSent) { res.status(HttpStatus.INTERNAL_SERVER_ERROR).send('Error fetching PDF'); } } } @Get('requirements/:id') async getRequirements(@Param('id') id: string, @Query('Posteingang') posteingang: string) { const documentTypeId = parseInt(id, 10); const isPosteingang = posteingang === '1'; const requirements = await this.documentFieldRepo.find({ where: { DocumentType: documentTypeId }, }); // Custom fields fetching inside here could be slow, but this is the simplest translation of the old API // Actually, getting all CFs doesn't take too long in Paperless API. const customFields = await this.paperlessService.getCustomFields(); const retVal: any[] = []; for (const req of requirements) { if (isPosteingang && !req.VisiblePosteingang) { continue; } const tmp: any = { id: req.Id, feldId: req.Type + (req.TypeIndex !== null ? '-' + req.TypeIndex : ''), hinweis: req.Hinweis || '', required: isPosteingang ? req.IsRequiredPosteingang : req.IsRequired, }; if (req.Type === 4) { // Custom Field const cfId = Number(req.TypeIndex); const cf = customFields.find((c: any) => c.id === cfId); tmp.isCustomField = true; tmp.customFieldIndex = cfId; if (cf) { tmp.feldName = cf.name; tmp.feldTyp = cf.id === 12 ? 'int' : cf.data_type; if (cf.extra_data && cf.extra_data.select_options) { tmp.fieldOptions = cf.extra_data.select_options .filter((o: any) => o !== null) .map((o: any) => ({ id: o.id, label: o.label })); } } else { tmp.feldName = `Feld ${req.TypeIndex}`; tmp.feldTyp = 'string'; } } else if (req.Type === 1) { tmp.feldName = 'Absender'; tmp.feldTyp = 'select'; const response = await this.paperlessService.getCorrespondents({ page_size: 9999 }); const correspondents = response.results; tmp.fieldOptions = correspondents.map((c: any) => ({ id: c.id.toString(), label: c.name })); } else if (req.Type === 2) { tmp.feldName = 'Belegdatum'; tmp.feldTyp = 'date'; } else if (req.Type === 3) { tmp.feldName = 'Ablagenummer'; tmp.feldTyp = 'string'; } else if (req.Type === 5) { tmp.feldName = 'Titel'; tmp.feldTyp = 'string'; } else { tmp.feldName = tmp.feldId; tmp.feldTyp = 'string'; } retVal.push(tmp); } return retVal; } @Put('inbox/:id') async putInboxDocument(@Param('id') id: string, @Body() body: any) { const documentId = parseInt(id, 10); // Fetch from paperless const oldDocument = await this.paperlessService.getDocument(documentId); oldDocument.document_type = body.documentType ?? oldDocument.document_type; oldDocument.owner = body.mandant ?? oldDocument.owner; oldDocument.correspondent = body.correspondent ?? oldDocument.correspondent; oldDocument.title = body.title ?? oldDocument.title; if (body.date) { let docDate = new Date(body.date); if (docDate.getHours() > 22) { docDate = new Date(docDate.getTime() + 24 * 60 * 60 * 1000 - docDate.getHours() * 60 * 60 * 1000); } oldDocument.created_date = docDate.toISOString().split('T')[0]; } const cfDefinitions = await this.paperlessService.getCustomFields(); // update custom fields if (body.customFields) { for (const [key, value] of Object.entries(body.customFields)) { const fieldId = parseInt(key, 10); const cfDef = cfDefinitions.find((c: any) => c.id === fieldId); let processedValue = value; if (cfDef?.data_type === 'documentlink' && value !== null && value !== '' && !Array.isArray(value)) { processedValue = [value]; } const existingFieldIndex = oldDocument.custom_fields.findIndex((f: any) => f.field === fieldId); if (existingFieldIndex !== -1) { if (processedValue === null || processedValue === '') { oldDocument.custom_fields.splice(existingFieldIndex, 1); } else { oldDocument.custom_fields[existingFieldIndex].value = processedValue; } } else if (processedValue !== null && processedValue !== '') { oldDocument.custom_fields.push({ field: fieldId, value: processedValue }); } } } // Requirements check const reqs = await this.documentFieldRepo.find({ where: { DocumentType: oldDocument.document_type }, }); let isReady = true; let isReadyPosteingang = true; for (const req of reqs) { let isFieldValid = false; if (req.Type === 1) isFieldValid = oldDocument.correspondent !== null; if (req.Type === 2) isFieldValid = oldDocument.created_date !== null; if (req.Type === 3) isFieldValid = oldDocument.archive_serial_number !== null; if (req.Type === 4) isFieldValid = !!oldDocument.custom_fields.find((cf: any) => cf.field === req.TypeIndex && cf.value !== null && cf.value !== ''); if (req.Type === 5) isFieldValid = oldDocument.title !== null && oldDocument.title !== ''; if (req.IsRequired && !isFieldValid) isReady = false; if (req.IsRequiredPosteingang && !isFieldValid) isReadyPosteingang = false; } const docType = await this.documentTypeRepo.findOne({ where: { DocumentTypeId: oldDocument.document_type }, }); oldDocument.tags = oldDocument.tags || []; if (isReady) { oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1); if (docType?.TagNotReady) oldDocument.tags = oldDocument.tags.filter((t: number) => t !== docType.TagNotReady); if (docType?.TagReady && !oldDocument.tags.includes(docType.TagReady)) { oldDocument.tags.push(docType.TagReady); } let titleTemplate = docType?.TitelTemplate || ''; if (titleTemplate) { for (const cf of oldDocument.custom_fields) { const placeholder = `{{CUSTOM[${cf.field}]}}`; if (titleTemplate.includes(placeholder)) { titleTemplate = titleTemplate.replace(placeholder, cf.value?.toString() ?? ''); } } titleTemplate = titleTemplate.replace('{{DATE}}', oldDocument.created_date); oldDocument.title = titleTemplate; } } else { if (docType?.TagNotReady) { if (isReadyPosteingang) { oldDocument.tags = oldDocument.tags.filter((t: number) => t !== 1); if (!oldDocument.tags.includes(docType.TagNotReady)) oldDocument.tags.push(docType.TagNotReady); } else { if (!oldDocument.tags.includes(1)) oldDocument.tags.push(1); } } else { if (!oldDocument.tags.includes(1)) oldDocument.tags.push(1); } if (docType?.TagReady) { oldDocument.tags = oldDocument.tags.filter((t: number) => t !== docType.TagReady); } } await this.paperlessService.updateDocument(documentId, oldDocument); return { success: true }; } @Get('tasks') @RequirePermissions(Permission.MANAGE_SETTINGS) async getAllTasks() { return this.taskRepo.find({ order: { Fertig: 'ASC' } }); } @Delete('tasks/fertig') @RequirePermissions(Permission.MANAGE_SETTINGS) async deleteFertigeTasks() { const result = await this.taskRepo.delete({ Fertig: 1 }); return { deleted: result.affected ?? 0 }; } @Delete('tasks/:taskId') @RequirePermissions(Permission.MANAGE_SETTINGS) async deleteTask(@Param('taskId') taskId: string) { const result = await this.taskRepo.delete({ TaskId: taskId }); if (!result.affected) { throw new HttpException('Task nicht gefunden', HttpStatus.NOT_FOUND); } return { deleted: result.affected }; } @Post('external-upload') @UseGuards(ApiKeyGuard) @UseInterceptors(FileInterceptor('document')) async externalUpload( @UploadedFile() file: Express.Multer.File, @Body() dto: UploadExternalDto, ) { this.logger.log(`Externer Upload gestartet: ${dto.interneBelegnummer} (${file?.originalname})`); try { // 0. Check if ASN already exists await this.paperlessService.validateAsnNotExists(dto.interneBelegnummer); // 1. Forward to Paperless const paperlessTaskId = await this.paperlessService.uploadDocument(file.path, { title: `Beleg ${dto.interneBelegnummer}`, }); // 2. Create local Task const task = this.taskRepo.create({ TaskId: paperlessTaskId.replace(/"/g, ''), InterneBelegnummer: dto.interneBelegnummer, DocumentType: dto.dokumentType, Eingangsdatum: dto.Eingangsdatum ? new Date(dto.Eingangsdatum) : null, Tags: dto.tag ? String(dto.tag) : null, BetriebID: dto.betriebId, Lieferant: dto.lieferant, EinkaufID: dto.einkaufId, externeBelegnummer: dto.externeBelegnummer, Belegdatum: dto.belegdatum ? new Date(dto.belegdatum) : null, TaskReferenceID: dto.parentId || '', BarcodeJson: dto.barcodeJson || '', DuplikatZU: dto.duplikatZu, Fertig: 0, }); await this.taskRepo.save(task); this.logger.log(`Externer Upload erfolgreich: ${task.TaskId} für Beleg ${dto.interneBelegnummer}`); return task.TaskId; } catch (err) { this.logger.error(`Fehler beim externen Upload für Beleg ${dto.interneBelegnummer}: ${err.message}`, err.stack); throw new HttpException( `Fehler beim Verarbeiten des Dokumenten-Uploads: ${err.message}`, HttpStatus.INTERNAL_SERVER_ERROR, ); } } }