433 lines
15 KiB
TypeScript
433 lines
15 KiB
TypeScript
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<Task>,
|
|
@InjectRepository(Document)
|
|
private readonly documentRepo: Repository<Document>,
|
|
@InjectRepository(DocumentField)
|
|
private readonly documentFieldRepo: Repository<DocumentField>,
|
|
@InjectRepository(DocumentType)
|
|
private readonly documentTypeRepo: Repository<DocumentType>,
|
|
) {}
|
|
|
|
@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,
|
|
);
|
|
}
|
|
}
|
|
}
|