import { formatINR } from '@sims/domain' import { ean13CheckDigit } from '@sims/scanning' /** * Shelf/MRP label printing (S2). `ean13Svg` renders a standards-correct EAN-13 * symbol (L/G/R element patterns, first-digit parity) so a scanner reads the * printed label; `renderLabelSheetHtml` lays labels out as a printable grid for * a laser sheet or label roll. Pure — no DOM — so both are unit-testable. */ // Left-hand odd-parity (L) element patterns, digit 0–9. 7 modules each. const L: string[] = [ '0001101', '0011001', '0010011', '0111101', '0100011', '0110001', '0101111', '0111011', '0110111', '0001011', ] // Right-hand (R) patterns are the ones-complement of L. const R: string[] = L.map((s) => [...s].map((b) => (b === '0' ? '1' : '0')).join('')) // Left-hand even-parity (G) patterns are R read right-to-left. const G: string[] = R.map((s) => [...s].reverse().join('')) // First digit selects the parity mix of the six left-hand digits. const PARITY: string[] = [ 'LLLLLL', 'LLGLGG', 'LLGGLG', 'LLGGGL', 'LGLLGG', 'LGGLLG', 'LGGGLL', 'LGLGLG', 'LGLGGL', 'LGGLGL', ] /** * Strict validation: a 12-digit body gets its check digit computed (via the * scanning package's `ean13CheckDigit`); a 13-digit code is verified and throws * on a checksum mismatch. Use for validating catalog input. */ export function normalizeEan13(code: string): string { const digits = code.replace(/\D/g, '') if (digits.length === 12) return digits + String(ean13CheckDigit(digits)) if (digits.length === 13) { const cd = ean13CheckDigit(digits.slice(0, 12)) if (cd !== Number(digits[12])) throw new Error(`EAN-13 checksum mismatch: expected ${cd}, got ${digits[12]}`) return digits } throw new Error('EAN-13 needs 12 or 13 digits') } /** * Lenient 13-digit form for rendering: a 12-digit body gains its computed check * digit; a 13-digit code is encoded exactly as stored (so the label round-trips * to the catalog's barcode even when legacy data has an off check digit — the * label must always print, R18). Only the length is enforced. */ function toEan13(code: string): string { const digits = code.replace(/\D/g, '') if (digits.length === 12) return digits + String(ean13CheckDigit(digits)) if (digits.length === 13) return digits throw new Error('EAN-13 needs 12 or 13 digits') } /** The 95-module bar/space string (1 = bar): start · 6 left · centre · 6 right · end. */ export function ean13Modules(code: string): string { const c = toEan13(code) const first = Number(c[0]) const pattern = PARITY[first]! let out = '101' // start guard for (let i = 0; i < 6; i++) { const d = Number(c[1 + i]) out += pattern[i] === 'L' ? L[d]! : G[d]! } out += '01010' // centre guard for (let i = 0; i < 6; i++) { out += R[Number(c[7 + i])]! } out += '101' // end guard return out } const GUARD = new Set() for (const i of [0, 1, 2, 45, 46, 47, 48, 49, 92, 93, 94]) GUARD.add(i) /** A self-contained SVG EAN-13 symbol with the human-readable digits beneath. */ export function ean13Svg(code13: string, opts: { module?: number; height?: number } = {}): string { const c = toEan13(code13) const modules = ean13Modules(c) const m = opts.module ?? 2 const barH = opts.height ?? 56 const guardH = barH + 7 const quietL = 11 * m const quietR = 7 * m const width = quietL + 95 * m + quietR const totalH = guardH + 14 const bars: string[] = [] for (let i = 0; i < modules.length; i++) { if (modules[i] !== '1') continue const h = GUARD.has(i) ? guardH : barH bars.push(``) } // Human-readable: first digit in the left quiet zone, then two groups of six. const ty = guardH + 11 const leftMid = quietL + (3 + 21) * m const rightMid = quietL + (50 + 21) * m const text = `${c[0]}` + `${c.slice(1, 7)}` + `${c.slice(7)}` return `` + `` + `${bars.join('')}` + `${text}` + `` } export interface LabelInput { name: string mrpPaise: number /** Barcode to print; omitted labels show text only. */ code13?: string /** Selling price when it differs from MRP. */ pricePaise?: number } export interface LabelSheetOptions { /** Columns in the grid. */ cols: number /** Label cell size in millimetres. */ labelWmm: number labelHmm: number title?: string printButton?: boolean } const esc = (s: string): string => s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') /** A printable A4 sheet of MRP/barcode labels laid out in a grid. */ export function renderLabelSheetHtml(labels: LabelInput[], opts: LabelSheetOptions): string { const title = opts.title ?? 'Barcode / MRP Labels' const cells = labels.map((lab) => { let bar = '' if (lab.code13 !== undefined && lab.code13 !== '') { try { bar = `
${ean13Svg(lab.code13, { module: 2, height: 40 })}
` } catch { bar = `
no barcode
` } } const price = lab.pricePaise !== undefined && lab.pricePaise !== lab.mrpPaise ? `
Rate ${formatINR(lab.pricePaise)}
` : '' return `
${esc(lab.name)}
${bar}
MRP ${formatINR(lab.mrpPaise)}
${price}
` }).join('') return ` ${esc(title)} ${opts.printButton === false ? '' : '
'}
${cells}
` }