From b000b08dc63c08932d8a0af7868f0ef94016b7b8 Mon Sep 17 00:00:00 2001 From: Admin Date: Fri, 25 Sep 2026 21:11:25 +0000 Subject: [PATCH] feat: print full pallet production sheet from code lab --- .../src/features/codes/CodeLab.css | 202 +++++++++++++++++- .../src/features/codes/CodeLab.tsx | 179 ++++++++++++++-- 2 files changed, 368 insertions(+), 13 deletions(-) diff --git a/packages/fieldlens-core/src/features/codes/CodeLab.css b/packages/fieldlens-core/src/features/codes/CodeLab.css index c65faa5..c38b200 100644 --- a/packages/fieldlens-core/src/features/codes/CodeLab.css +++ b/packages/fieldlens-core/src/features/codes/CodeLab.css @@ -173,10 +173,17 @@ background: #fff8d7; } -.barcode-print-sheet { +.barcode-print-sheet, +.pallet-print-sheet { display: none; } +.pallet-production-sheet { + color: #111; + background: #fff; + font-family: Arial, Helvetica, sans-serif; +} + @media print { @page { size: auto; @@ -248,6 +255,199 @@ color: #000; font: 11pt/1.3 "Courier New", monospace; } + + .pallet-print-sheet { + display: block !important; + width: 100%; + margin: 0; + padding: 0; + color: #000; + background: #fff; + } + + .pallet-production-sheet { + box-sizing: border-box; + width: 198mm; + min-height: 277mm; + margin: 0 auto; + padding: 5mm; + border: 0; + break-inside: avoid-page; + page-break-inside: avoid; + } + + .pallet-top-strip, + .pallet-bottom-strip { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 2mm; + } + + .pallet-ticket { + min-width: 0; + padding: 1.5mm 1mm; + border: .25mm solid #bbb; + text-align: center; + } + + .pallet-ticket canvas { + display: block; + width: 100%; + height: 11mm; + object-fit: contain; + } + + .pallet-ticket strong { + display: block; + margin-top: .5mm; + font: 700 8pt/1 Arial, Helvetica, sans-serif; + } + + .pallet-sheet-main { + display: grid; + grid-template-columns: 33mm minmax(0, 1fr) 36mm; + gap: 4mm; + align-items: stretch; + margin: 5mm 0; + } + + .pallet-size-panel { + display: grid; + place-items: center; + border: .4mm solid #222; + font: 700 48pt/1 Arial, Helvetica, sans-serif; + } + + .pallet-card { + border: .4mm solid #222; + padding: 3mm; + } + + .pallet-card-title { + margin-bottom: 2mm; + font: 700 14pt/1.1 Arial, Helvetica, sans-serif; + } + + .pallet-card-columns { + display: grid; + grid-template-columns: 1fr 1.05fr; + gap: 3mm; + } + + .pallet-card-left, + .pallet-card-right { + min-width: 0; + } + + .pallet-spec-table, + .pallet-run-table { + width: 100%; + border-collapse: collapse; + table-layout: fixed; + } + + .pallet-spec-table th, + .pallet-spec-table td, + .pallet-run-table th, + .pallet-run-table td { + border: .25mm solid #333; + padding: 1mm .8mm; + text-align: center; + font: 700 7.5pt/1.1 Arial, Helvetica, sans-serif; + } + + .pallet-spec-table th, + .pallet-run-table th { + font-size: 6.5pt; + } + + .pallet-card-left p { + margin: 2mm 0; + font: 9pt/1.2 Arial, Helvetica, sans-serif; + } + + .pallet-time { + margin-top: 4mm !important; + } + + .pallet-export-copy { + margin: 13mm 2mm 7mm; + text-align: center; + font: 8pt/1.25 Arial, Helvetica, sans-serif; + } + + .pallet-signature { + display: flex; + align-items: end; + gap: 2mm; + margin-top: 5mm; + font: 8pt/1 Arial, Helvetica, sans-serif; + } + + .pallet-signature span { + flex: 1; + border-bottom: .25mm dotted #444; + } + + .pallet-main-barcode { + display: block; + width: 72%; + height: 15mm; + margin: 0 auto 2mm; + object-fit: contain; + } + + .pallet-id-row, + .pallet-total-row { + display: flex; + justify-content: space-between; + gap: 3mm; + padding: 1mm 0; + font: 10pt/1.2 "Courier New", monospace; + } + + .pallet-id-row strong { + font-size: 12pt; + } + + .pallet-run-table { + margin-top: 2mm; + } + + .pallet-total-row { + margin-top: 44mm; + border-top: .25mm solid #333; + font-size: 11pt; + } + + .pallet-hero-ticket { + display: grid; + grid-template-rows: 1fr auto; + align-items: center; + border: .4mm solid #222; + padding: 2mm; + } + + .pallet-hero-ticket canvas { + display: block; + width: 100%; + height: 34mm; + object-fit: contain; + } + + .pallet-hero-ticket div { + display: flex; + justify-content: center; + gap: 2mm; + align-items: baseline; + padding: 2mm 0; + background: #f3d923; + font: 700 13pt/1 Arial, Helvetica, sans-serif; + } + + .pallet-hero-ticket strong { + font-size: 20pt; + } } @media (max-width: 760px) { diff --git a/packages/fieldlens-core/src/features/codes/CodeLab.tsx b/packages/fieldlens-core/src/features/codes/CodeLab.tsx index d16aa3e..3308ba7 100644 --- a/packages/fieldlens-core/src/features/codes/CodeLab.tsx +++ b/packages/fieldlens-core/src/features/codes/CodeLab.tsx @@ -31,6 +31,28 @@ type EanLabelFixture = { tempPc: string } +type PalletSheetFixture = { + pallet: string + grower: string + packrun: string + size: string + qty: string + total: string + origin: string + variety: string + brand: string + className: string + base: string + pack: string + pickDate: string + packDate: string + printedTime: string +} + +type PrintTarget = + | { kind: 'barcode'; fixture: CodeFixture } + | { kind: 'pallet'; fixture: PalletSheetFixture } + function makeFixtures(): CodeFixture[] { const gtin14 = appendGtinCheckDigit(randomDigits(13)) const lot = `LOT-${randomDigits(6)}` @@ -39,11 +61,45 @@ function makeFixtures(): CodeFixture[] { { id: crypto.randomUUID(), name: 'EAN-8', bcid: 'ean8', value: appendGtinCheckDigit(randomDigits(7)) }, { id: crypto.randomUUID(), name: 'UPC-A', bcid: 'upca', value: appendGtinCheckDigit(randomDigits(11)) }, { id: crypto.randomUUID(), name: 'ITF-14', bcid: 'itf14', value: gtin14 }, - { id: crypto.randomUUID(), name: 'Code 128', bcid: 'code128', value: `FL-${randomDigits(10)}` }, + { id: crypto.randomUUID(), name: 'Pallet Code 128', bcid: 'code128', value: randomDigits(8) }, { id: crypto.randomUUID(), name: 'GS1 Digital Link QR', bcid: 'qrcode', value: `https://id.gs1.org/01/${gtin14}/10/${lot}` }, ] } +function makePalletSheetFixture(pallet: string): PalletSheetFixture { + const grower = randomDigits(5) + return { + pallet, + grower, + packrun: `${grower}/16/17/26`, + size: '20', + qty: '30', + total: '30', + origin: 'NZ', + variety: 'Hass', + brand: 'NZAG', + className: 'CLASS 3', + base: 'CHEP', + pack: 'B37', + pickDate: '16/09/2026', + packDate: '17/09/2026', + printedTime: '14:46:10', + } +} + +function drawPalletBarcode(canvas: HTMLCanvasElement, value: string, compact = false) { + bwipjs.toCanvas(canvas, { + bcid: 'code128', + text: value, + scale: compact ? 2 : 3, + height: compact ? 9 : 16, + includetext: false, + backgroundcolor: 'FFFFFF', + paddingwidth: compact ? 2 : 4, + paddingheight: compact ? 1 : 2, + }) +} + function makeEanFixtures(): EanLabelFixture[] { const pallet = { ppin: randomDigits(8), @@ -80,7 +136,15 @@ function drawBarcode(canvas: HTMLCanvasElement, fixture: CodeFixture, print = fa }) } -function CodeCard({ fixture, onPrint }: { fixture: CodeFixture; onPrint: (fixture: CodeFixture) => void }) { +function CodeCard({ + fixture, + onPrintBarcode, + onPrintPallet, +}: { + fixture: CodeFixture + onPrintBarcode: (fixture: CodeFixture) => void + onPrintPallet: (fixture: CodeFixture) => void +}) { const canvasRef = useRef(null) useEffect(() => { @@ -98,8 +162,13 @@ function CodeCard({ fixture, onPrint }: { fixture: CodeFixture; onPrint: (fixtur {fixture.name}
VALID SAMPLE -
@@ -132,6 +201,80 @@ function PrintableBarcode({ fixture }: { fixture: CodeFixture }) { ) } +function PrintablePalletSheet({ fixture }: { fixture: PalletSheetFixture }) { + const sheetRef = useRef(null) + + useEffect(() => { + const canvases = sheetRef.current?.querySelectorAll('canvas[data-pallet-barcode]') + canvases?.forEach((canvas) => { + try { + drawPalletBarcode(canvas, fixture.pallet, canvas.dataset.compact === 'true') + } catch { + // Keep the human-readable pallet ID visible if barcode rendering fails. + } + }) + }, [fixture]) + + const tickets = Array.from({ length: 6 }, (_, index) => ( +
+ + JA {fixture.pallet} +
+ )) + + return ( + + ) +} + function EanLabelCard({ fixture }: { fixture: EanLabelFixture }) { return (
@@ -162,7 +305,7 @@ export function CodeLab() { const [tab, setTab] = useState('barcodes') const [fixtures, setFixtures] = useState(makeFixtures) const [eanFixtures, setEanFixtures] = useState(makeEanFixtures) - const [printFixture, setPrintFixture] = useState(null) + const [printTarget, setPrintTarget] = useState(null) const regenerate = () => { if (tab === 'ean') setEanFixtures(makeEanFixtures()) @@ -170,12 +313,12 @@ export function CodeLab() { } useEffect(() => { - if (!printFixture) return + if (!printTarget) return let cancelled = false let frameTwo = 0 - const clearPrintFixture = () => setPrintFixture(null) - window.addEventListener('afterprint', clearPrintFixture, { once: true }) + const clearPrintTarget = () => setPrintTarget(null) + window.addEventListener('afterprint', clearPrintTarget, { once: true }) const frameOne = window.requestAnimationFrame(() => { frameTwo = window.requestAnimationFrame(() => { @@ -187,9 +330,9 @@ export function CodeLab() { cancelled = true window.cancelAnimationFrame(frameOne) if (frameTwo) window.cancelAnimationFrame(frameTwo) - window.removeEventListener('afterprint', clearPrintFixture) + window.removeEventListener('afterprint', clearPrintTarget) } - }, [printFixture]) + }, [printTarget]) return (
@@ -212,11 +355,23 @@ export function CodeLab() { {tab === 'barcodes' ? ( -
{fixtures.map((fixture) => )}
+
{fixtures.map((fixture) => ( + setPrintTarget({ kind: 'barcode', fixture: next })} + onPrintPallet={(next) => setPrintTarget({ kind: 'pallet', fixture: makePalletSheetFixture(next.value) })} + /> + ))}
) : (
{eanFixtures.map((fixture) => )}
)} - {printFixture && createPortal(, document.body)} + {printTarget && createPortal( + printTarget.kind === 'pallet' + ? + : , + document.body, + )}
) } \ No newline at end of file