// scripts/generate-qr.ts import QRCode from 'qrcode' import { mkdirSync } from 'fs' import path from 'path' import { pathToFileURL } from 'url' export const DEFAULT_BASE_URL = 'http://localhost:3000' // Matches seeded zone qr_code_token values in 20260709000008_seed.sql export const ZONES = [ { token: 'scw1-dock-a-qr-2026', name: 'SCW1 — Dock A' }, { token: 'scw1-cold-storage-qr-2026', name: 'SCW1 — Cold Storage' }, { token: 'scw1-loading-bay-qr-2026', name: 'SCW1 — Loading Bay' }, ] export interface Zone { token: string name: string } /** Pure helper: builds the URL encoded in each QR code. */ export function buildQrUrl(baseUrl: string, token: string): string { return `${baseUrl}/report?zone=${token}` } export interface GenerateDeps { toFile: (outputPath: string, url: string, opts: object) => Promise mkdirSync: (dir: string, opts: { recursive: boolean }) => void log: (...args: unknown[]) => void } /** Core logic, with injectable deps so tests never touch real I/O. */ export async function generateZoneQrs( zones: Zone[], outDir: string, baseUrl: string, deps: GenerateDeps, ): Promise { deps.mkdirSync(outDir, { recursive: true }) for (const zone of zones) { const url = buildQrUrl(baseUrl, zone.token) const outputPath = path.join(outDir, `${zone.token}.png`) await deps.toFile(outputPath, url, { width: 400, margin: 2, color: { dark: '#000000', light: '#FFFFFF' }, }) deps.log(`✓ ${zone.name}`) deps.log(` → ${url}`) deps.log(` → ${outputPath}`) } } async function main() { const baseUrl = process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_BASE_URL const outDir = path.join(process.cwd(), 'public', 'qr') await generateZoneQrs(ZONES, outDir, baseUrl, { toFile: (outputPath, url, opts) => QRCode.toFile(outputPath, url, opts as Parameters[2]), mkdirSync, log: console.log, }) } // Only run when executed directly (npm run generate-qr) — importing this // module (e.g. from tests) must not touch the filesystem. if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch((err) => { console.error(err) process.exit(1) }) }