handover pdf

This commit is contained in:
Bastian Wagner
2026-03-12 11:41:29 +01:00
parent 93053e0101
commit b72e2d6784
20 changed files with 2891 additions and 33 deletions

2363
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -20,6 +20,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1007.0",
"@nestjs-modules/mailer": "2.0.2",
"@nestjs/axios": "4.0.1",
"@nestjs/cache-manager": "3.1.0",
@@ -35,6 +36,7 @@
"class-transformer": "^0.5.1",
"class-validator": "0.14.1",
"mysql2": "3.18.2",
"puppeteer": "^24.39.0",
"reflect-metadata": "^0.2.2",
"rxjs": "7.8.2",
"typeorm": "0.3.28"

View File

@@ -16,6 +16,7 @@ import { APP_INTERCEPTOR } from '@nestjs/core';
import { MailModule } from './modules/mail/mail.module';
import { LogModule } from './modules/log/log.module';
import { SseModule } from './modules/realtime/sse/sse.module';
import { PdfModule } from './shared/pdf/pdf.module';
@Module({
imports: [
@@ -34,7 +35,8 @@ import { SseModule } from './modules/realtime/sse/sse.module';
SystemModule,
MailModule,
LogModule,
SseModule
SseModule,
PdfModule
],
controllers: [AppController],
providers: [

View File

@@ -0,0 +1,17 @@
export class KeyHandoverDto {
handoverId!: string;
handoverDate!: Date;
place!: string;
giverName!: string;
giverAddress?: string;
receiverName!: string;
receiverAddress?: string;
keyType!: string;
keyNumber?: string;
quantity!: number;
objectDescription?: string;
notes?: string;
}

View File

@@ -33,6 +33,10 @@ export class KeyHandout {
@ManyToOne(() => User)
user: User;
@Column({ nullable: true })
pdfFormKey: string;
@BeforeInsert()
insertTimestamp() {
if (this.timestamp == null) {

View File

@@ -7,19 +7,23 @@ import {
Post,
Put,
Req,
Res,
Sse,
UseGuards,
} from '@nestjs/common';
import { Response } from 'express';
import { KeyService } from './key.service';
import { AuthenticatedRequest } from 'src/model/interface/authenticated-request.interface';
import { AuthGuard } from 'src/core/guards/auth.guard';
import { Key } from 'src/model/entitites';
import { interval, map, Observable } from 'rxjs';
import { KeyHandoverPdfService } from 'src/shared/pdf/key-handover-pdf/key-handover-pdf.service';
import { KeyHandoverDto } from 'src/model/dto/key-handover.dto';
@Controller('key')
export class KeyController {
constructor(private service: KeyService) {}
constructor(private service: KeyService, private pdfService: KeyHandoverPdfService) {}
@UseGuards(AuthGuard)
@Get()
@@ -78,4 +82,15 @@ export class KeyController {
getArchive(@Req() req: AuthenticatedRequest) {
return this.service.getDeletedKeys(req.user);
}
@Post('pdf')
async generatePdf(@Body() dto: KeyHandoverDto, @Res() res: Response): Promise<void> {
const pdfBuffer = await this.pdfService.generatePdf(dto);
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="schluesseluebergabe.pdf"');
res.setHeader('Content-Length', pdfBuffer.length);
res.end(pdfBuffer);
}
}

View File

@@ -7,10 +7,11 @@ import { SharedServiceModule } from 'src/shared/service/shared.service.module';
import { ConfigService } from '@nestjs/config';
import { MailModule } from '../mail/mail.module';
import { SseModule } from '../realtime/sse/sse.module';
import { PdfModule } from 'src/shared/pdf/pdf.module';
@Module({
controllers: [KeyController],
providers: [KeyService, ConfigService],
imports: [DatabaseModule, AuthModule, SharedServiceModule, MailModule, SseModule],
imports: [DatabaseModule, AuthModule, SharedServiceModule, MailModule, SseModule, PdfModule],
})
export class KeyModule {}

View File

@@ -0,0 +1,248 @@
import { Injectable } from '@nestjs/common';
import { KeyHandoverDto } from 'src/model/dto/key-handover.dto';
import * as puppeteer from 'puppeteer';
import { MinioService } from '../minio/minio.service';
import { KeyHandoutRepository } from 'src/model/repositories/key-handout.repository';
import { KeyHandout } from 'src/model/entitites';
@Injectable()
export class KeyHandoverPdfService {
constructor(private readonly minioService: MinioService, private handoverRepo: KeyHandoutRepository) {}
async generatePdf(dto: KeyHandoverDto): Promise<Buffer> {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
try {
const page = await browser.newPage();
const html = this.buildHtml(dto);
await page.setContent(html, {
waitUntil: 'networkidle0',
});
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '20mm',
right: '15mm',
bottom: '20mm',
left: '15mm',
},
displayHeaderFooter: true,
headerTemplate: `<div></div>`,
footerTemplate: `
<div style="
width: 100%;
font-size: 10px;
padding: 0 20px;
color: #666;
display: flex;
justify-content: space-between;
">
<span>Übergabeprotokoll</span>
<span>${new Date().toLocaleString()}</span>
</div>
`,
});
const buffer = Buffer.from(pdf);
const bucket = 'keyvault-pro-develop';
const key = `handover-forms/${Date.now()}-schluesseluebergabe.pdf`;
await this.minioService.uploadPdf(bucket, key, buffer);
await this.saveKeyToHandover(dto.handoverId, key)
return buffer;
} finally {
await browser.close();
}
}
private async saveKeyToHandover(handoverId: string, fileKey: string) {
const handover: KeyHandout = await this.handoverRepo.findOneBy({ id: handoverId });
if (!handover) { return; }
handover.pdfFormKey = fileKey;
this.handoverRepo.save(handover);
}
private buildHtml(dto: KeyHandoverDto): string {
return `
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<title>Übergabeprotokoll</title>
<style>
body {
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
color: #222;
margin: 0;
padding: 0;
}
h1 {
text-align: center;
margin: 0 0 24px 0;
font-size: 22px;
}
.section {
margin-bottom: 18px;
}
.section-title {
font-weight: bold;
font-size: 14px;
margin-bottom: 8px;
border-bottom: 1px solid #ccc;
padding-bottom: 4px;
}
.row {
margin-bottom: 6px;
}
.label {
display: inline-block;
width: 170px;
font-weight: bold;
vertical-align: top;
}
.value {
display: inline-block;
max-width: 360px;
word-break: break-word;
}
.note-box {
min-height: 60px;
border: 1px solid #ccc;
padding: 8px;
margin-top: 6px;
white-space: pre-wrap;
}
.declaration {
margin-top: 24px;
line-height: 1.5;
}
.signature-wrapper {
margin-top: 60px;
display: flex;
justify-content: space-between;
gap: 40px;
}
.signature-block {
width: 45%;
}
.signature-line {
border-top: 1px solid #000;
margin-top: 50px;
padding-top: 6px;
text-align: center;
}
</style>
</head>
<body>
<h1>Übergabeprotokoll</h1>
<div class="section">
<div class="section-title">Übergabedaten</div>
<div class="row">
<span class="label">Datum der Übergabe:</span>
<span class="value">${this.escapeHtml(new Date(dto.handoverDate).toLocaleDateString())}</span>
</div>
<div class="row" style="display: none">
<span class="label">Ort:</span>
<span class="value">${this.escapeHtml(dto.place)}</span>
</div>
</div>
<div class="section">
<div class="section-title">Übergeber</div>
<div class="row">
<span class="label">Name:</span>
<span class="value">${this.escapeHtml(dto.giverName)}</span>
</div>
<div class="row">
<span class="label">Adresse:</span>
<span class="value">${this.escapeHtml(dto.giverAddress ?? '-')}</span>
</div>
</div>
<div class="section">
<div class="section-title">Empfänger</div>
<div class="row">
<span class="label">Name:</span>
<span class="value">${this.escapeHtml(dto.receiverName)}</span>
</div>
<div class="row">
<span class="label">Adresse:</span>
<span class="value">${this.escapeHtml(dto.receiverAddress ?? '-')}</span>
</div>
</div>
<div class="section">
<div class="section-title">Schlüsselangaben</div>
<div class="row" style="display: none;">
<span class="label">Schlüsselart:</span>
<span class="value">${this.escapeHtml(dto.keyType)}</span>
</div>
<div class="row">
<span class="label">Schlüsselnummer:</span>
<span class="value">${this.escapeHtml(dto.keyNumber ?? '-')}</span>
</div>
<div class="row" style="display: none;">
<span class="label">Anzahl:</span>
<span class="value">${dto.quantity}</span>
</div>
<div class="row">
<span class="label">Objekt / Raum:</span>
<span class="value">${this.escapeHtml(dto.objectDescription ?? '-')}</span>
</div>
<div class="row">
<span class="label">Bemerkungen:</span>
</div>
<div class="note-box">${this.escapeHtml(dto.notes ?? '-')}</div>
</div>
<div class="declaration">
Hiermit bestätigt der Empfänger den Erhalt der oben aufgeführten Schlüssel.
<br />
Mit ihrer Unterschrift bestätigen beide Parteien die ordnungsgemäße Übergabe.
</div>
<div class="signature-wrapper">
<div class="signature-block">
<div class="signature-line">Unterschrift Übergeber</div>
</div>
<div class="signature-block">
<div class="signature-line">Unterschrift Empfänger</div>
</div>
</div>
</body>
</html>
`;
}
private escapeHtml(value: string): string {
if (!value) { return ""; }
return value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@nestjs/common';
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MinioService {
constructor(private configService: ConfigService) {}
private readonly client = new S3Client({
region: 'us-east-1',
endpoint: this.configService.get('MINIOHOST'),
credentials: {
accessKeyId: this.configService.get('MINIOUSER'),
secretAccessKey: this.configService.get('MINIOACCESSKEY'),
},
forcePathStyle: true,
});
async uploadPdf(bucket: string, key: string, pdfBuffer: Buffer): Promise<void> {
await this.client.send(
new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: pdfBuffer,
ContentType: 'application/pdf',
}),
);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { KeyHandoverPdfService } from './key-handover-pdf/key-handover-pdf.service';
import { MinioService } from './minio/minio.service';
import { ConfigModule } from '@nestjs/config';
import { DatabaseModule } from '../database/database.module';
@Module({
providers: [KeyHandoverPdfService, MinioService],
imports: [ConfigModule, DatabaseModule],
exports: [KeyHandoverPdfService]
})
export class PdfModule {}