import { redirect } from "next/navigation"; import { currentUser } from "@/lib/session"; import { prisma } from "@/lib/db"; import { buildSnapshot } from "@/lib/snapshot"; import { bcBound, itemMap, key, ledger, onhand } from "@/lib/compute"; import AutoPrint from "@/components/AutoPrint"; import { barcodeKind, barcodeSvg } from "@/lib/barcode"; export const dynamic = "force-dynamic"; type SP = Record; /* Garment labels, 6 to an A4 sheet. * * Two ways in. `?code=` prints one bound code a chosen number of times — the reprint, for a label * that has worn off. `?item=` prints a whole garment: every size that carries a code, once per * garment on the shelf, which is what labelling a rack of stock that arrived unlabelled actually * needs. Six size-14s in the cupboard means six size-14 labels, because each of those six shirts is * going to have one stuck on it. * * Only ever the code that is really bound to the size. ThreadCount's internal 93XXXXXXX fallback * appears on no garment, so printing it would put a barcode into the linen room that no scanner has * ever seen. */ export default async function LabelSheet({ searchParams }: { searchParams: Promise }) { const user = await currentUser(); if (!user) redirect("/auth"); const q = await searchParams; const code = String(q.code || "").trim().slice(0, 64); const itemId = String(q.item || "").trim().slice(0, 64); const copies = Math.min(24, Math.max(1, parseInt(q.copies || "6", 10) || 6)); const reason = String(q.reason || "").slice(0, 60); if (!code && !itemId) redirect("/app/stock"); const fac = await prisma.facility.findUniqueOrThrow({ where: { id: user.facilityId } }); const label = (it: { item: string; gender: string; sku: string }) => it.item + (it.gender !== "Unisex" ? ` — ${it.gender}` : ""); /** One entry per label to be printed, in size order. */ type Row = { code: string; name: string; size: string; sku: string }; const rows: Row[] = []; let heading = ""; if (code) { const bc = await prisma.barcode.findUnique({ where: { facilityId_code: { facilityId: user.facilityId, code } }, include: { item: { select: { item: true, gender: true, sizes: true, sku: true } } }, }); if (!bc) redirect("/app/stock"); const size = String(bc.item.sizes[bc.sizeIndex] ?? bc.sizeIndex); for (let i = 0; i < copies; i++) rows.push({ code, name: label(bc.item), size, sku: bc.item.sku }); heading = `${label(bc.item)} · size ${size}`; } else { const it = await prisma.catalogItem.findFirst({ where: { id: itemId, facilityId: user.facilityId }, select: { id: true, item: true, gender: true, sizes: true, sku: true, barcodes: { select: { code: true, sizeIndex: true } } }, }); if (!it) redirect("/app/stock"); /* How many of each. Read through the same ledger the rest of the product uses rather than summing stock rows here: a second on-hand expression is how a screen ends up disagreeing with the shelf, and this one decides how much paper comes out of the printer. */ const snap = await buildSnapshot(user); const L = ledger(snap); /* Go size by size, not code by code. A size can end up carrying more than one Barcode row — the supplier re-labels a range, the new code is scanned onto a size that already carries one and the old row survives — so walking the rows printed that size twice over, half of it in the code the linen room had just replaced. One garment on the shelf gets one label. The code that prints is the one bcBound() picks, which is the code the size's own screens show, so the sheet and the screen can never name different numbers for the same size. */ const snapItem = itemMap(snap)[it.id]; const bySize = new Map(); for (const b of it.barcodes) if (!bySize.has(b.sizeIndex)) bySize.set(b.sizeIndex, b.code); for (const si of [...bySize.keys()].sort((a, z) => a - z)) { const code = (snapItem ? bcBound(snap, snapItem, si) : "") || bySize.get(si)!; const n = Math.max(0, onhand(snap, L, key(it.id, si))); const size = String(it.sizes[si] ?? si); for (let i = 0; i < n; i++) rows.push({ code, name: label(it), size, sku: it.sku }); } heading = label(it); // Nothing on the shelf, or no size labelled yet: say which, rather than printing a blank sheet. if (!rows.length) { return (

{heading}

{it.barcodes.length === 0 ? "No size on this garment has a barcode yet, so there is nothing to print. Generate barcodes for it first." : "There is nothing on the shelf to label — every size with a barcode is showing zero on hand. Count some in, or print a single size from its own page."}

); } } const X_NOMINAL = 0.33; // EAN-13 at 100% magnification const X_MIN = 0.25; // below this a hand scanner starts refusing the symbol const LABEL_MM = 62; // printable width inside one cell, see below /* barcodeSvg sizes itself in CSS pixels at whatever module width it was handed, which says nothing about how wide the symbol lands on paper. A 12-digit UPC-A or an alphanumeric supplier asset tag is not a valid EAN-13, so it falls through to Code 128 — half as wide again as the EAN-13 this sheet was laid out around — and at a fixed pixel width it printed past the edge of its label and pushed the second column off the page. So the sheet works the other way round: read back how many modules the symbol actually has, then pick the widest X-dimension that still fits the label, capped at the 0.33mm EAN-13 nominal so a short code isn't blown up to fill the cell. LABEL_MM is the printable width inside one cell: A4 210mm, less the 12mm @page margins, less the sheet's 10mm side padding, less the 6mm gutter, halved, less the 8mm padding inside the label — rounded down to leave the border somewhere to sit. */ const drawn = new Map(); for (const r of rows) { if (drawn.has(r.code)) continue; const svg = barcodeSvg(r.code, { module: 2, height: 58 }); const modules = Math.round(parseFloat(/width="([\d.]+)"/.exec(svg)?.[1] || "0") / 2); const xDim = modules > 0 ? Math.min(X_NOMINAL, LABEL_MM / modules) : X_NOMINAL; drawn.set(r.code, { svg, printMm: modules * xDim, xDim }); } const tooSmall = [...drawn.entries()].filter(([, d]) => d.xDim < X_MIN).length; return (
{rows.length} label{rows.length === 1 ? "" : "s"} {heading} {code && {code} · {barcodeKind(code)}} {itemId && one per garment on hand, {drawn.size} size{drawn.size === 1 ? "" : "s"}} {reason && Reason: {reason}} {/* A symbol squeezed below about 0.25mm per module is printed but not reliably readable, and finding that out at the shelf with a scanner in your hand is the wrong place to find out. */} {tooSmall > 0 && {tooSmall === 1 ? "One code is" : `${tooSmall} codes are`} too long to print at a scannable size on this label — bind a shorter code, or print on a wider label from your own stationery.}
{rows.map((r, i) => { const d = drawn.get(r.code)!; return (
{r.name}
Size {r.size}{r.sku ? ` · ${r.sku}` : ""}
{fac.slipOrg || fac.name}
); })}
); }