feat(print): label print (artist/title/price/SKU/QR, Dymo 54x25mm) + optional RFID chip program (Chafon H102 via local :7790 daemon)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-06-22 22:55:31 +10:00
parent a817b9d163
commit 38c6714184

View File

@ -204,6 +204,7 @@ function invEdit(sku){
<div class="bar" style="margin-bottom:6px"><span class=muted style="width:90px">In stock</span><input id="eStock" type=checkbox ${r.in_stock?'checked':''}></div>
${fld('Notes','eNotes',r.notes)}
<div class="bar" style="margin-top:8px"><button onclick="invSave('${sku}')" style="flex:1">Save</button>
<button class="ghost" onclick="invLabel('${sku}')">🏷 Label</button>
<button class="ghost" onclick="invDelete('${sku}')" style="color:#e66">Delete</button>
<button class="ghost" onclick="invModalClose()">Cancel</button></div>`);
}
@ -240,6 +241,65 @@ async function invDoAdd(){
await fetch('/admin/inventory/add',{method:'POST',headers:hdr(),body:JSON.stringify(body)});
invModalClose(); loadInv();
}
// ---------- Label print + RFID (drives the local rfid-daemon :7790, Chafon H102) ----------
const RFID_DAEMON = 'http://127.0.0.1:7790'; // localhost is a secure context — HTTPS page may call it
function invLabel(sku){
const r = invItems[sku] || { sku };
const qr = 'https://api.qrserver.com/v1/create-qr-code/?size=120x120&margin=0&data=' + encodeURIComponent(r.sku);
invModalBox(`<h3 style="margin:0 0 10px">🏷 Label — ${escapeH(r.title||sku)}</h3>
<div style="display:flex;gap:10px;background:#fff;color:#111;border-radius:8px;padding:8px;align-items:center">
<div style="flex:1;min-width:0">
<div style="font-weight:700;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeH(r.artist||'')}</div>
<div style="font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escapeH(r.title||'')}</div>
<div style="font-size:22px;font-weight:800;margin-top:2px">${money(r.price)}</div>
<div style="font-size:9px;color:#555">${escapeH(r.sku)}${r.condition?' · '+escapeH(r.condition):''}</div>
</div>
<img src="${qr}" style="width:64px;height:64px">
</div>
<label class="bar" style="margin:10px 0 4px;cursor:pointer"><input type="checkbox" id="lblRfid"> <span>⚡ Program RFID chip at the same time (UHF H102)</span></label>
<div id="lblDaemon" class="muted" style="font-size:11px">checking tag reader…</div>
<div class="bar" style="margin-top:10px"><button onclick='invDoPrint(${JSON.stringify(r).replace(/'/g,"&#39;")})' style="flex:1">🖨 Print label</button>
<button class="ghost" onclick="invEdit('${sku}')"> back</button></div>
<div id="lblMsg" class="muted" style="margin-top:8px"></div>`);
daemonPing();
}
async function daemonPing(){
const el = $('#lblDaemon'); if(!el) return;
try { const r = await fetch(RFID_DAEMON+'/status', {signal:AbortSignal.timeout(2500)}).then(x=>x.json());
el.innerHTML = '<span style="color:#6ee7b7">● tag reader connected</span>'; }
catch(e){ el.innerHTML = '<span style="color:#e6a046">● rfid-daemon not running on :7790 — RFID disabled (label still prints)</span>'; }
}
function invDoPrint(r){
const qr = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&margin=0&data=' + encodeURIComponent(r.sku);
const html = `<html><head><meta charset=utf-8><style>
@page{size:54mm 25mm;margin:0}
*{box-sizing:border-box}body{margin:0;font-family:system-ui,sans-serif}
.lbl{width:54mm;height:25mm;padding:2mm;display:flex;gap:2mm;align-items:center}
.info{flex:1;min-width:0}
.a{font-weight:700;font-size:10pt;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.t{font-size:7.5pt;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.p{font-size:17pt;font-weight:800;line-height:1.1;margin-top:0.5mm}
.s{font-size:5.5pt;color:#444}.qr{width:18mm;height:18mm}
</style></head><body onload="window.print();setTimeout(()=>window.close(),300)">
<div class="lbl"><div class="info"><div class="a">${escapeH(r.artist||'')}</div><div class="t">${escapeH(r.title||'')}</div>
<div class="p">${money(r.price)}</div><div class="s">${escapeH(r.sku)}</div></div>
<img class="qr" src="${qr}"></div></body></html>`;
const w = window.open('', '_blank', 'width=420,height=240');
if(w){ w.document.open(); w.document.write(html); w.document.close(); }
if($('#lblRfid') && $('#lblRfid').checked) rfidWrite(r.sku, r.release_id);
}
async function rfidWrite(sku, releaseId){
const msg = $('#lblMsg'); if(msg) msg.innerHTML = '⚡ writing tag (present the chip to the reader)…';
try {
await fetch(RFID_DAEMON+'/clear-mask', {method:'POST', signal:AbortSignal.timeout(3000)}).catch(()=>{});
const body = { sku }; if(releaseId) body.releaseId = String(releaseId);
const d = await fetch(RFID_DAEMON+'/write-tag', {method:'POST', headers:{'Content-Type':'application/json'},
body:JSON.stringify(body), signal:AbortSignal.timeout(120000)}).then(x=>x.json());
if(msg) msg.innerHTML = d.ok ? `<span style="color:#6ee7b7">✓ RFID tag programmed (${escapeH(sku)})</span>`
: `<span style="color:#e66">RFID write failed: ${escapeH(d.error||'?')}</span>`;
} catch(e){ if(msg) msg.innerHTML = '<span style="color:#e6a046">RFID daemon unreachable (:7790)</span>'; }
}
function invImport(){
invModalBox(`<h3 style="margin:0 0 6px">Import list (bulk stock-in)</h3>
<div class="muted" style="margin-bottom:8px">One per line — Release ID / barcode / catalogue no. Resolved against the catalog, each becomes an in-stock item.</div>