fix: Bug fixed für Rollen
Backend Image / build-and-push (push) Failing after 1m12s
Frontend Image / build-and-push (push) Successful in 1m14s

This commit is contained in:
Nick Szaunig committed 2026-09-01 16:54:42 +02:00
1 parent 8432313fa9
commit 5f4e1a7ba9
9 files changed
+311 -341

No files matched your search

@@ -6,25 +6,54 @@ import { AuditLogService } from '../audit/audit-log.service';
import { LabelTemplate } from '../../generated/prisma';
import { CreateLabelTemplateDto } from './dto/create-label-template.dto';
import { UpdateLabelTemplateDto } from './dto/update-label-template.dto';
import {
LabelElement,
LabelFontFamily,
PlaceholderKey,
} from './label-element.types';
const MM_TO_PT = 72 / 25.4;
const PAGE_MARGIN_MM = 5;
const LABEL_GAP_MM = 2;
const A4_WIDTH_MM = 210;
const A4_HEIGHT_MM = 297;
const QR_CODE_PLACEHOLDER = '{{qrCode}}';
const QR_SOURCE_SIZE_PX = 400;
function formatDate(date: Date): string {
return date.toLocaleDateString('de-DE');
}
function fillPlaceholders(
content: string,
values: Record<string, string>,
// Prisma stores `elements` as a plain Json column; the shape is only ever
// written through CreateLabelTemplateDto/UpdateLabelTemplateDto, so casting
// back to the app-level type here is safe.
function elementsOf(template: LabelTemplate): LabelElement[] {
return template.elements as unknown as LabelElement[];
}
function pdfFontName(
family: LabelFontFamily,
bold: boolean,
italic: boolean,
): string {
return content.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
key in values ? values[key] : match,
);
const base =
family === 'serif' ? 'Times' : family === 'mono' ? 'Courier' : 'Helvetica';
if (base === 'Times') {
if (bold && italic) return 'Times-BoldItalic';
if (bold) return 'Times-Bold';
if (italic) return 'Times-Italic';
return 'Times-Roman';
}
const suffix =
bold && italic ? '-BoldOblique' : bold ? '-Bold' : italic ? '-Oblique' : '';
return `${base}${suffix}`;
}
function dataUrlToBuffer(dataUrl: string): Buffer | null {
const match = /^data:image\/(png|jpe?g);base64,(.+)$/.exec(dataUrl);
if (!match) {
return null;
}
return Buffer.from(match[2], 'base64');
}
@Injectable()
@@ -59,7 +88,9 @@ export class LabelTemplatesService {
data: { isDefault: false },
});
}
return tx.labelTemplate.create({ data: { ...dto } });
return tx.labelTemplate.create({
data: { ...dto, elements: dto.elements as never },
});
});
await this.auditLog.record({
entityType: 'LabelTemplate',
@@ -84,7 +115,10 @@ export class LabelTemplatesService {
data: { isDefault: false },
});
}
return tx.labelTemplate.update({ where: { id }, data: { ...dto } });
return tx.labelTemplate.update({
where: { id },
data: { ...dto, elements: dto.elements as never },
});
});
await this.auditLog.record({
entityType: 'LabelTemplate',
@@ -127,7 +161,7 @@ export class LabelTemplatesService {
throw new NotFoundException('Bottling batch not found');
}
const values: Record<string, string> = {
const values: Record<PlaceholderKey, string> = {
batchNumber: bottlingBatch.honeyBatch.batchNumber,
honeyType: bottlingBatch.honeyBatch.honeyType ?? '',
lotNumber: bottlingBatch.lotNumber,
@@ -136,25 +170,29 @@ export class LabelTemplatesService {
jarSize: `${bottlingBatch.jarSize} ml`,
productName: bottlingBatch.product?.name ?? '',
};
const labelText = fillPlaceholders(template.content, values);
// The QR code is generated once (not per copy) - it always points at the
// public Herkunft/traceability page for this batch, which already shows
// honey type, dates, and origin, so the label itself only needs to
// reference it rather than duplicate that information.
const elements = elementsOf(template).map((element) =>
element.type === 'text' && element.placeholderKey
? { ...element, text: values[element.placeholderKey] }
: element,
);
// The QR code always points at the public Herkunft/traceability page for
// this batch (honey type, dates, origin already shown there), generated
// once and reused across every copy and every qrCode element.
let qrCodeBuffer: Buffer | null = null;
if (labelText.includes(QR_CODE_PLACEHOLDER)) {
if (elements.some((element) => element.type === 'qrCode')) {
const frontendOrigin =
process.env.FRONTEND_ORIGIN ?? 'http://localhost:3000';
const herkunftUrl = `${frontendOrigin}/herkunft/${encodeURIComponent(bottlingBatch.lotNumber)}`;
qrCodeBuffer = await QRCode.toBuffer(herkunftUrl, {
margin: 1,
width: 300,
width: QR_SOURCE_SIZE_PX,
});
}
return this.renderPdf(
labelText,
elements,
template.widthMm,
template.heightMm,
bottlingBatch.jarCount,
@@ -163,7 +201,7 @@ export class LabelTemplatesService {
}
private renderPdf(
labelText: string,
elements: LabelElement[],
widthMm: number,
heightMm: number,
copies: number,
@@ -197,40 +235,85 @@ export class LabelTemplatesService {
const row = Math.floor(onPage / columns);
const xMm = PAGE_MARGIN_MM + column * (widthMm + LABEL_GAP_MM);
const yMm = PAGE_MARGIN_MM + row * (heightMm + LABEL_GAP_MM);
const x = xMm * MM_TO_PT;
const y = yMm * MM_TO_PT;
const originX = xMm * MM_TO_PT;
const originY = yMm * MM_TO_PT;
const w = widthMm * MM_TO_PT;
const h = heightMm * MM_TO_PT;
doc.rect(x, y, w, h).dash(2, { space: 2 }).stroke('#999999').undash();
doc
.rect(originX, originY, w, h)
.dash(2, { space: 2 })
.stroke('#999999')
.undash();
if (qrCodeBuffer) {
const qrSide = Math.min(w, h) - 8;
const qrX = x + w - qrSide - 4;
const qrY = y + (h - qrSide) / 2;
doc.image(qrCodeBuffer, qrX, qrY, { width: qrSide, height: qrSide });
const textWidth = qrX - 4 - (x + 4);
doc
.fontSize(9)
.fillColor('#000000')
.text(
labelText.replace(QR_CODE_PLACEHOLDER, '').trim(),
x + 4,
y + 4,
{
width: Math.max(textWidth, 0),
height: h - 8,
},
);
} else {
doc
.fontSize(9)
.fillColor('#000000')
.text(labelText, x + 4, y + 4, { width: w - 8, height: h - 8 });
for (const element of elements) {
this.drawElement(doc, element, originX, originY, qrCodeBuffer);
}
}
doc.end();
});
}
private drawElement(
doc: PDFKit.PDFDocument,
element: LabelElement,
originX: number,
originY: number,
qrCodeBuffer: Buffer | null,
): void {
const x = originX + element.x * MM_TO_PT;
const y = originY + element.y * MM_TO_PT;
switch (element.type) {
case 'text': {
doc
.font(pdfFontName(element.fontFamily, element.bold, element.italic))
.fontSize(element.fontSize)
.fillColor(element.color)
.text(element.text, x, y, {
width: element.width * MM_TO_PT,
height: element.height * MM_TO_PT,
align: element.align,
});
return;
}
case 'qrCode': {
if (!qrCodeBuffer) {
return;
}
const side = element.size * MM_TO_PT;
doc.image(qrCodeBuffer, x, y, { width: side, height: side });
return;
}
case 'image': {
const buffer = dataUrlToBuffer(element.src);
if (!buffer) {
return;
}
doc.image(buffer, x, y, {
width: element.width * MM_TO_PT,
height: element.height * MM_TO_PT,
});
return;
}
case 'line': {
const endX =
element.orientation === 'horizontal'
? x + element.length * MM_TO_PT
: x;
const endY =
element.orientation === 'vertical'
? y + element.length * MM_TO_PT
: y;
doc
.moveTo(x, y)
.lineTo(endX, endY)
.lineWidth(element.thickness)
.strokeColor(element.color)
.stroke();
return;
}
}
}
}