diff --git a/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts b/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts index 11fbb3c..098528a 100644 --- a/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts +++ b/ApiCore-Backend/src/label-templates/label-templates.service.spec.ts @@ -247,6 +247,33 @@ describe('LabelTemplatesService', () => { expect(countPages(pdf)).toBe(3); }); + it('clamps an explicit columns/rows override that would run labels off the A4 page', async () => { + // widthMm:90, marginLeftMm:5, gapXMm:2 only fit 2 columns on A4 - an + // explicit 99 would place columns 3+ past the page's right edge, + // where a printer just clips them instead of printing anything. + prisma.labelTemplate.findUnique.mockResolvedValue({ + ...template, + columns: 99, + rows: 1, + }); + prisma.bottlingBatch.findUnique.mockResolvedValue({ + id: 'batch-1', + jarSize: 500, + jarCount: 3, + lotNumber: 'L-001', + date: new Date('2026-06-01T00:00:00.000Z'), + bestBeforeDate: new Date('2028-06-01T00:00:00.000Z'), + honeyBatch: { batchNumber: 'HB-001', honeyType: 'Bluetenhonig' }, + product: { name: 'Glas 500g' }, + }); + + const pdf = await service.renderLabelsPdf('template-1', 'batch-1'); + + // Clamped to the real max of 2 columns x 1 row = 2 per page, so 3 + // copies still need 2 pages - same as the explicit columns:2 test. + expect(countPages(pdf)).toBe(2); + }); + it('skips already-used slots on the first sheet via startPosition, but not later sheets', async () => { prisma.labelTemplate.findUnique.mockResolvedValue({ ...template, diff --git a/ApiCore-Backend/src/label-templates/label-templates.service.ts b/ApiCore-Backend/src/label-templates/label-templates.service.ts index 1773dbd..d11bcec 100644 --- a/ApiCore-Backend/src/label-templates/label-templates.service.ts +++ b/ApiCore-Backend/src/label-templates/label-templates.service.ts @@ -237,15 +237,21 @@ export class LabelTemplatesService { 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 maxColumns = Math.max( + 1, + Math.floor((usableWidthMm + gapXMm) / (widthMm + gapXMm)), + ); + const maxRows = Math.max( + 1, + Math.floor((usableHeightMm + gapYMm) / (heightMm + gapYMm)), + ); + // An explicit columns/rows value from the sheet-layout designer is + // clamped to what actually fits the A4 page - without this, a value + // typed in by the user (or already saved from before this fix) would + // place labels past the page's right/bottom edge, where printers just + // clip them instead of printing anything. + const columns = Math.min(layout.columns ?? maxColumns, maxColumns); + const rows = Math.min(layout.rows ?? maxRows, maxRows); 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 diff --git a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.tsx b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.tsx index a89cb43..5556efe 100644 --- a/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.tsx +++ b/ApiCore-Frontend/src/app/(app)/settings/label-templates/designer/label-designer.tsx @@ -35,6 +35,8 @@ import { LabelCanvas } from "./label-canvas"; import { ElementPropertiesPanel } from "./element-properties-panel"; const ELEMENT_TYPES = ["text", "qrCode", "image", "line"] as const; +const A4_WIDTH_MM = 210; +const A4_HEIGHT_MM = 297; const ZOOM_STEPS: number[] = [0.5, 0.75, 1, 1.25, 1.5, 2]; const ZOOM_MIN = ZOOM_STEPS[0]; const ZOOM_MAX = ZOOM_STEPS[ZOOM_STEPS.length - 1]; @@ -152,6 +154,21 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP const selectedElement = elements.find((element) => element.id === selectedId) ?? null; + // Mirrors the backend's clamp in label-templates.service.ts exactly, so + // the hint shown here always matches what actually prints - a value typed + // in above this is silently capped server-side rather than running labels + // off the physical A4 page. + const maxColumns = Math.max( + 1, + Math.floor((A4_WIDTH_MM - 2 * marginLeftMm + gapXMm) / (widthMm + gapXMm)), + ); + const maxRows = Math.max( + 1, + Math.floor((A4_HEIGHT_MM - 2 * marginTopMm + gapYMm) / (heightMm + gapYMm)), + ); + const columnsExceedsMax = columns !== null && columns > maxColumns; + const rowsExceedsMax = rows !== null && rows > maxRows; + function addElement(element: LabelElement) { setElements((current) => [...current, element]); setSelectedId(element.id); @@ -365,9 +382,11 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP

- Spalten/Zeilen leer lassen, um automatisch so viele einzuplanen wie passen. Feste - Werte eintragen, um exakt zum vorgedruckten Raster eines Etikettenbogens (z.B. - Avery-Vorlage) zu passen. + Spalten/Zeilen leer lassen, um automatisch so viele einzuplanen wie passen (aktuell{" "} + {maxColumns} × {maxRows}). Feste Werte eintragen, um exakt zum vorgedruckten Raster + eines Etikettenbogens (z.B. Avery-Vorlage) zu passen - mehr als {maxColumns} Spalten + bzw. {maxRows} Zeilen passen bei dieser Etikettengröße und diesen Rändern nicht auf + ein A4-Blatt und werden beim Drucken abgeschnitten.

@@ -382,6 +401,11 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP setColumns(event.target.value === "" ? null : Number(event.target.value)) } /> + {columnsExceedsMax ? ( +

+ Passt nicht auf A4 - maximal {maxColumns} Spalte{maxColumns === 1 ? "" : "n"}. +

+ ) : null}
@@ -395,6 +419,11 @@ export function LabelDesigner({ initial, onSubmit, submitLabel }: LabelDesignerP setRows(event.target.value === "" ? null : Number(event.target.value)) } /> + {rowsExceedsMax ? ( +

+ Passt nicht auf A4 - maximal {maxRows} Zeile{maxRows === 1 ? "" : "n"}. +

+ ) : null}