feat: Etiketten-Seitenlayout auf dem A4-Bogen frei waehlbar
Backend Image / build-and-push (push) Successful in 2m16s
Frontend Image / build-and-push (push) Successful in 2m0s

- LabelTemplate bekommt columns/rows (optional, leer = automatisch
  passend), plus konfigurierbare Raender/Abstaende - damit ein Etikett
  exakt auf das vorgedruckte Raster eines physischen Etikettenbogens
  (z.B. Avery) gelegt werden kann statt nur automatisch berechnet
- Startposition beim Drucken waehlbar, fuer bereits angebrochene Boegen
  (erste Seite startet ab der gewaehlten Position, Folgeseiten wieder
  von vorne)
This commit is contained in:
Nick Szaunig committed 2026-09-01 19:43:01 +02:00
1 parent 9659b8d266
commit c6941a797f
11 files changed
+357 -27

No files matched your search

@@ -13,12 +13,21 @@ import {
} 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_SOURCE_SIZE_PX = 400;
interface SheetLayout {
widthMm: number;
heightMm: number;
columns: number | null;
rows: number | null;
marginTopMm: number;
marginLeftMm: number;
gapXMm: number;
gapYMm: number;
}
function formatDate(date: Date): string {
return date.toLocaleDateString('de-DE');
}
@@ -151,6 +160,7 @@ export class LabelTemplatesService {
async renderLabelsPdf(
templateId: string,
bottlingBatchId: string,
startPosition = 1,
): Promise<Buffer> {
const template = await this.findById(templateId);
const bottlingBatch = await this.prisma.bottlingBatch.findUnique({
@@ -193,19 +203,28 @@ export class LabelTemplatesService {
return this.renderPdf(
elements,
template.widthMm,
template.heightMm,
{
widthMm: template.widthMm,
heightMm: template.heightMm,
columns: template.columns,
rows: template.rows,
marginTopMm: template.marginTopMm,
marginLeftMm: template.marginLeftMm,
gapXMm: template.gapXMm,
gapYMm: template.gapYMm,
},
bottlingBatch.jarCount,
qrCodeBuffer,
startPosition,
);
}
private renderPdf(
elements: LabelElement[],
widthMm: number,
heightMm: number,
layout: SheetLayout,
copies: number,
qrCodeBuffer: Buffer | null,
startPosition: number,
): Promise<Buffer> {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({ size: 'A4', margin: 0 });
@@ -214,27 +233,34 @@ export class LabelTemplatesService {
doc.on('end', () => resolve(Buffer.concat(chunks)));
doc.on('error', (error: Error) => reject(error));
const usableWidthMm = A4_WIDTH_MM - 2 * PAGE_MARGIN_MM;
const usableHeightMm = A4_HEIGHT_MM - 2 * PAGE_MARGIN_MM;
const columns = Math.max(
1,
Math.floor((usableWidthMm + LABEL_GAP_MM) / (widthMm + LABEL_GAP_MM)),
);
const rows = Math.max(
1,
Math.floor((usableHeightMm + LABEL_GAP_MM) / (heightMm + LABEL_GAP_MM)),
);
const { widthMm, heightMm, marginTopMm, marginLeftMm, gapXMm, gapYMm } =
layout;
const usableWidthMm = A4_WIDTH_MM - 2 * marginLeftMm;
const usableHeightMm = A4_HEIGHT_MM - 2 * marginTopMm;
const columns =
layout.columns ??
Math.max(1, Math.floor((usableWidthMm + gapXMm) / (widthMm + gapXMm)));
const rows =
layout.rows ??
Math.max(
1,
Math.floor((usableHeightMm + gapYMm) / (heightMm + gapYMm)),
);
const perPage = columns * rows;
// startPosition only skips ahead on the first sheet (partially-used
// physical sheet, e.g. a few labels already peeled off) - every sheet
// after that always starts fresh at the top-left slot.
const skip = Math.min(Math.max(0, startPosition - 1), perPage - 1);
for (let index = 0; index < copies; index += 1) {
const onPage = index % perPage;
if (index > 0 && onPage === 0) {
const slotOnPage = (skip + index) % perPage;
if (index > 0 && slotOnPage === 0) {
doc.addPage({ size: 'A4', margin: 0 });
}
const column = onPage % columns;
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 column = slotOnPage % columns;
const row = Math.floor(slotOnPage / columns);
const xMm = marginLeftMm + column * (widthMm + gapXMm);
const yMm = marginTopMm + row * (heightMm + gapYMm);
const originX = xMm * MM_TO_PT;
const originY = yMm * MM_TO_PT;
const w = widthMm * MM_TO_PT;