feat: print full pallet production sheet from code lab
Some checks failed
CI / verify (push) Has been cancelled
CI / print-rust (push) Has been cancelled

This commit is contained in:
Admin 2026-09-25 21:11:25 +00:00
parent 435c64cd7f
commit b000b08dc6
2 changed files with 368 additions and 13 deletions

View File

@ -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) {

View File

@ -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<HTMLCanvasElement>(null)
useEffect(() => {
@ -98,8 +162,13 @@ function CodeCard({ fixture, onPrint }: { fixture: CodeFixture; onPrint: (fixtur
<strong>{fixture.name}</strong>
<div className="code-card-heading-actions">
<span>VALID SAMPLE</span>
<button className="code-print-button" type="button" onClick={() => onPrint(fixture)} aria-label={`Print one ${fixture.name} barcode`}>
<Printer size={14} /> Print one
<button
className="code-print-button"
type="button"
onClick={() => fixture.bcid === 'code128' ? onPrintPallet(fixture) : onPrintBarcode(fixture)}
aria-label={fixture.bcid === 'code128' ? 'Print pallet production sheet' : `Print one ${fixture.name} barcode`}
>
<Printer size={14} /> {fixture.bcid === 'code128' ? 'Print pallet sheet' : 'Print one'}
</button>
</div>
</div>
@ -132,6 +201,80 @@ function PrintableBarcode({ fixture }: { fixture: CodeFixture }) {
)
}
function PrintablePalletSheet({ fixture }: { fixture: PalletSheetFixture }) {
const sheetRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const canvases = sheetRef.current?.querySelectorAll<HTMLCanvasElement>('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) => (
<div className="pallet-ticket" key={index}>
<canvas data-pallet-barcode data-compact="true" />
<strong>JA {fixture.pallet}</strong>
</div>
))
return (
<div className="pallet-print-sheet" aria-hidden="true" ref={sheetRef}>
<div className="pallet-production-sheet">
<div className="pallet-top-strip">{tickets}</div>
<div className="pallet-sheet-main">
<div className="pallet-size-panel">20</div>
<div className="pallet-card">
<div className="pallet-card-title">Avocados - Hass</div>
<div className="pallet-card-columns">
<div className="pallet-card-left">
<table className="pallet-spec-table">
<thead><tr><th>ORIGIN</th><th>VAR</th><th>BRAND</th><th>CLASS</th></tr></thead>
<tbody>
<tr><td>{fixture.origin}</td><td>{fixture.variety}</td><td>{fixture.brand}</td><td>{fixture.className}.</td></tr>
<tr><td></td><th>BASE</th><th>PACK</th><td></td></tr>
<tr><td></td><td>{fixture.base}</td><td>{fixture.pack}</td><td></td></tr>
</tbody>
</table>
<p><b>Pick Date:</b> {fixture.pickDate}</p>
<p><b>Pack Date:</b> {fixture.packDate}</p>
<p className="pallet-time"><b>Time:</b> {fixture.printedTime}</p>
<div className="pallet-export-copy">
This product meets export quality specifications and has been inspected and passed for export to
</div>
<div className="pallet-signature">Signature: <span></span></div>
</div>
<div className="pallet-card-right">
<canvas className="pallet-main-barcode" data-pallet-barcode />
<div className="pallet-id-row"><b>Pallet:</b><strong>{fixture.pallet}</strong></div>
<table className="pallet-run-table">
<thead><tr><th>Grower</th><th>Packrun</th><th>Size</th><th>Qty</th></tr></thead>
<tbody><tr><td>{fixture.grower}</td><td>{fixture.packrun}</td><td>{fixture.size}</td><td>{fixture.qty}</td></tr></tbody>
</table>
<div className="pallet-total-row"><b>Total</b><strong>{fixture.total}</strong></div>
</div>
</div>
</div>
<div className="pallet-hero-ticket">
<canvas data-pallet-barcode />
<div><span>JA</span><strong>{fixture.pallet}</strong></div>
</div>
</div>
<div className="pallet-bottom-strip">{tickets}</div>
</div>
</div>
)
}
function EanLabelCard({ fixture }: { fixture: EanLabelFixture }) {
return (
<article className="ean-lab-carton">
@ -162,7 +305,7 @@ export function CodeLab() {
const [tab, setTab] = useState<LabTab>('barcodes')
const [fixtures, setFixtures] = useState(makeFixtures)
const [eanFixtures, setEanFixtures] = useState(makeEanFixtures)
const [printFixture, setPrintFixture] = useState<CodeFixture | null>(null)
const [printTarget, setPrintTarget] = useState<PrintTarget | null>(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 (
<section className="workspace-view code-lab" aria-labelledby="lab-title">
@ -212,11 +355,23 @@ export function CodeLab() {
</div>
{tab === 'barcodes' ? (
<div className="code-grid">{fixtures.map((fixture) => <CodeCard fixture={fixture} key={fixture.id} onPrint={setPrintFixture} />)}</div>
<div className="code-grid">{fixtures.map((fixture) => (
<CodeCard
fixture={fixture}
key={fixture.id}
onPrintBarcode={(next) => setPrintTarget({ kind: 'barcode', fixture: next })}
onPrintPallet={(next) => setPrintTarget({ kind: 'pallet', fixture: makePalletSheetFixture(next.value) })}
/>
))}</div>
) : (
<div className="ean-lab-grid">{eanFixtures.map((fixture) => <EanLabelCard fixture={fixture} key={fixture.id} />)}</div>
)}
{printFixture && createPortal(<PrintableBarcode fixture={printFixture} />, document.body)}
{printTarget && createPortal(
printTarget.kind === 'pallet'
? <PrintablePalletSheet fixture={printTarget.fixture} />
: <PrintableBarcode fixture={printTarget.fixture} />,
document.body,
)}
</section>
)
}