import db from './db.js'; import { clean, localStamp } from './util.js'; export function listSites({ activeOnly = false } = {}) { const sql = `SELECT * FROM sites ${activeOnly ? 'WHERE active = 1' : ''} ORDER BY name COLLATE NOCASE`; return db.prepare(sql).all(); } export function getSite(idOrSlug) { if (idOrSlug === undefined || idOrSlug === null || idOrSlug === '') return null; const asNumber = Number(idOrSlug); if (Number.isInteger(asNumber) && String(asNumber) === String(idOrSlug)) { return db.prepare('SELECT * FROM sites WHERE id = ?').get(asNumber) || null; } return db.prepare('SELECT * FROM sites WHERE slug = ?').get(String(idOrSlug).toLowerCase()) || null; } /** Falls back to the only active site, which keeps single-site installs simple. */ export function resolveSite(idOrSlug) { const found = getSite(idOrSlug); if (found && found.active) return found; const active = listSites({ activeOnly: true }); return active.length === 1 ? active[0] : found || null; } export function slugify(value) { return clean(value, 60) .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') .slice(0, 40); } export function uniqueSlug(base, excludeId = null) { let slug = slugify(base) || 'site'; let n = 2; while (true) { const clash = db.prepare('SELECT id FROM sites WHERE slug = ?').get(slug); if (!clash || clash.id === excludeId) return slug; slug = `${slugify(base)}-${n}`; n += 1; } } export function shapeSite(site) { return { id: site.id, name: site.name, slug: site.slug, active: Boolean(site.active), badge: { enabled: Boolean(site.badge_enabled), widthMm: site.badge_width_mm, heightMm: site.badge_height_mm, showPhoto: Boolean(site.badge_show_photo), accent: Boolean(site.badge_accent), note: site.badge_note, }, }; } /* --------------------------------------------------------------- badge */ const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c] ); /** * A self-contained print page sized to the site's label stock. It calls print() * on load so a kiosk can drop it into a hidden iframe and get one badge out. */ export function badgeHtml(visit, site, { autoPrint = true, photoUrl = null } = {}) { const width = Number(site.badge_width_mm) || 62; const height = Number(site.badge_height_mm) || 100; const showPhoto = Boolean(site.badge_show_photo) && Boolean(photoUrl); // A label noticeably taller than it is wide gets a stacked layout. That is the // normal case on a 62mm roll printer like the Brother QL-820NWB, where the roll // fixes the width and the length runs down the badge. const portrait = height >= width * 1.2; // Type scales with the dimension that constrains it: the width on a portrait // badge, the shorter side on a wide one. Keeps small stock legible. const unit = portrait ? width : Math.min(width, height); const pad = unit * 0.07; const nameSize = Math.max(3.2, unit * (portrait ? 0.105 : 0.115)); const bodySize = Math.max(2.0, unit * (portrait ? 0.055 : 0.062)); // Square, matching the crop taken at the kiosk. Slightly smaller on a portrait // badge than the old 4:3 frame, so a square photo still leaves room for the name. const photoWidth = portrait ? unit * 0.46 : unit * 0.42; // Red only appears on a two-colour roll (DK-22251 on the QL-820NWB). Anywhere // else it prints as grey, so it is off unless the site opts in. const accent = site.badge_accent ? '#d00019' : '#000'; const timeIn = new Date(visit.signed_in_at); const noCheck = visit.check_type === 'NONE'; const photo = showPhoto ? `` : ''; const details = `
Visiting ${esc(visit.host_name)}
In at ${esc( timeIn.toLocaleTimeString('en-AU', { hour: '2-digit', minute: '2-digit', hour12: false }) )} on ${esc(timeIn.toLocaleDateString('en-AU', { day: '2-digit', month: 'short', year: '2-digit' }))}
${ noCheck ? 'No WWCC / VIT' : `${esc(visit.check_type)} ${esc(visit.check_number || '')}` }
${site.badge_note ? `
${esc(site.badge_note)}
` : ''}
`; return ` Badge — ${esc(visit.first_name)} ${esc(visit.last_name)}
${photo}
${esc(site.name)} · Visitor
${esc(visit.first_name)} ${esc(visit.last_name)}
${details}
${autoPrint ? '' : ''} `; } export { esc as escapeHtml, localStamp };