418 lines
22 KiB
HTML
418 lines
22 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
<meta name="theme-color" content="#ff385c">
|
|
<title>Settings — WFSales POS</title>
|
|
<link rel="manifest" href="manifest.json">
|
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='7' fill='%23ff385c'/><text x='16' y='22' font-family='-apple-system,sans-serif' font-size='17' font-weight='700' text-anchor='middle' fill='white'>W</text></svg>">
|
|
<style>
|
|
:root{
|
|
--bg:#ffffff;--surface:#fff7f8;--fg:#222222;--muted:#dddddd;--border:#ffe3e8;--accent:#ff385c;
|
|
--ok:#0f8a3f;--warn:#b14a00;--danger:#c92a2a;
|
|
--shadow:0 1px 2px rgba(34,34,34,.04), 0 4px 12px rgba(34,34,34,.06);
|
|
--font:'Inter',system-ui,-apple-system,Segoe UI,Helvetica Neue,Arial,sans-serif;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
|
|
html,body{height:100%;background:var(--bg);color:var(--fg);font:16px/1.5 var(--font);-webkit-font-smoothing:antialiased}
|
|
body{display:flex;flex-direction:column;min-height:100dvh;max-width:480px;margin:0 auto;padding:0 20px 32px;background:var(--bg)}
|
|
.topbar{display:flex;align-items:center;gap:10px;padding:14px 0 10px;background:var(--bg);position:sticky;top:0;z-index:10}
|
|
.topbar .back{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg);background:var(--surface);border:1px solid var(--border);flex:0 0 auto}
|
|
.topbar .back:active{background:var(--bg)}
|
|
.topbar h1{font-size:17px;font-weight:600;letter-spacing:-.01em;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.topbar .netpill{font-size:11px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;color:#717171;padding:4px 8px;border:1px solid var(--muted);border-radius:999px;flex:0 0 auto;white-space:nowrap}
|
|
.topbar .netpill.off{color:var(--danger);border-color:var(--danger);background:#fff5f5}
|
|
.topbar .theme-btn{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg);background:transparent;border:0;cursor:pointer;flex:0 0 auto}
|
|
.topbar .theme-btn:active{background:var(--surface)}
|
|
.topbar .theme-btn svg{width:18px;height:18px}
|
|
.topbar .theme-btn .i-moon{display:none}
|
|
:root[data-theme="dark"] .topbar .theme-btn .i-sun{display:none}
|
|
:root[data-theme="dark"] .topbar .theme-btn .i-moon{display:block}
|
|
|
|
.lede{color:#717171;font-size:14px;line-height:1.55;margin:6px 0 16px;max-width:38ch}
|
|
|
|
/* ---- Active letterhead card ---- */
|
|
.active{padding:16px;border:1.5px solid var(--accent);border-radius:14px;background:#fff8fa;display:flex;align-items:center;gap:14px;box-shadow:var(--shadow)}
|
|
.active .logo{width:64px;height:64px;border-radius:10px;background:#fff;border:1px solid var(--border);display:grid;place-items:center;overflow:hidden;flex:0 0 auto}
|
|
.active .logo img{max-width:100%;max-height:100%;object-fit:contain}
|
|
.active .logo .placeholder{font-weight:700;font-size:22px;color:var(--accent)}
|
|
.active .body{flex:1;min-width:0}
|
|
.active .name{font-size:17px;font-weight:700;letter-spacing:-.01em;color:var(--fg);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.active .sub{font-size:12px;color:var(--ink-soft, #717171);margin-top:4px;letter-spacing:.01em;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
|
.active .pill{font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;background:var(--accent);color:#fff;padding:3px 8px;border-radius:999px}
|
|
.active .pill.muted{background:var(--surface);color:var(--fg);border:1px solid var(--border)}
|
|
|
|
/* ---- Letterhead list (for switching) ---- */
|
|
.section-label{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#717171;margin:24px 4px 10px}
|
|
.lh-list{display:flex;flex-direction:column;gap:8px}
|
|
.lh-card{display:flex;align-items:center;gap:12px;padding:12px;border:1.5px solid var(--muted);border-radius:12px;background:#fff;text-align:left;width:100%;cursor:pointer;transition:border-color .12s, background .12s}
|
|
.lh-card:active{background:var(--surface)}
|
|
.lh-card.selected{border-color:var(--accent);background:#fff8fa}
|
|
.lh-card .logo{width:48px;height:48px;border-radius:8px;background:#fff;border:1px solid var(--border);display:grid;place-items:center;overflow:hidden;flex:0 0 auto}
|
|
.lh-card .logo img{max-width:100%;max-height:100%;object-fit:contain}
|
|
.lh-card .logo .placeholder{font-weight:700;font-size:16px;color:var(--accent)}
|
|
.lh-card .body{flex:1;min-width:0}
|
|
.lh-card .name{font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--fg);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.lh-card .meta{font-size:11px;color:#717171;margin-top:2px;letter-spacing:.01em}
|
|
.lh-card .check{width:18px;height:18px;border-radius:50%;background:var(--accent);color:#fff;display:grid;place-items:center;flex:0 0 auto;font-size:11px;font-weight:700;line-height:1}
|
|
.lh-card:not(.selected) .check{background:transparent;border:1.5px solid var(--muted);color:transparent}
|
|
|
|
/* ---- Actions ---- */
|
|
.actions{padding:0;display:flex;flex-direction:column;gap:10px;margin-top:24px}
|
|
.btn{display:flex;align-items:center;justify-content:center;gap:8px;min-height:54px;padding:0 18px;border-radius:12px;font-size:15px;font-weight:600;letter-spacing:-.005em;border:1.5px solid var(--muted);background:#fff;color:var(--fg)}
|
|
.btn:active{background:var(--surface)}
|
|
.btn.primary{background:var(--accent);color:#fff;border-color:var(--accent);box-shadow:0 4px 14px rgba(255,56,92,.25)}
|
|
.btn.primary:active{background:#e5324f;box-shadow:none}
|
|
.btn svg{width:18px;height:18px;stroke-width:1.7}
|
|
.btn:disabled{opacity:.5;pointer-events:none}
|
|
|
|
/* ---- Empty / error states ---- */
|
|
.banner{padding:12px 14px;border-radius:12px;font-size:13px;display:flex;align-items:flex-start;gap:10px;line-height:1.4}
|
|
.banner.warn{background:#fdf2e6;border:1px solid #f5d9b3;color:#8a4a00}
|
|
.banner.err{background:#fff0f0;border:1px solid #f5c2c7;color:#9b1c1c}
|
|
.banner .ic{flex:0 0 auto;width:16px;height:16px;margin-top:1px}
|
|
.banner .t{flex:1;min-width:0}
|
|
.skeleton{padding:24px 16px;text-align:center;color:#999;font-size:13px}
|
|
|
|
/* ---- Dark theme ---- */
|
|
@media (prefers-color-scheme: dark){
|
|
:root:not([data-theme="light"]){
|
|
--bg:#0f0f0f;--surface:#1c1416;--fg:#f0f0f0;--muted:#2a2a2a;--border:#3a2429;
|
|
}
|
|
:root:not([data-theme="light"]) body{background:var(--bg);color:var(--fg)}
|
|
:root:not([data-theme="light"]) .topbar{background:var(--bg)}
|
|
:root:not([data-theme="light"]) .lede{color:#a0a0a0}
|
|
:root:not([data-theme="light"]) .topbar .back{background:var(--surface);border-color:var(--border);color:var(--fg)}
|
|
:root:not([data-theme="light"]) .topbar .netpill{color:#a0a0a0;border-color:var(--muted)}
|
|
:root:not([data-theme="light"]) .topbar .netpill.off{color:#ff6680;border-color:#ff6680;background:#2a1a1a}
|
|
:root:not([data-theme="light"]) .topbar .theme-btn{color:var(--fg)}
|
|
:root:not([data-theme="light"]) .section-label{color:#a0a0a0}
|
|
:root:not([data-theme="light"]) .active{background:#2a1a1a;border-color:#ff385c}
|
|
:root:not([data-theme="light"]) .active .logo{background:#181818;border-color:var(--muted)}
|
|
:root:not([data-theme="light"]) .active .name{color:var(--fg)}
|
|
:root:not([data-theme="light"]) .lh-card{background:#181818;border-color:var(--muted);color:var(--fg)}
|
|
:root:not([data-theme="light"]) .lh-card:active{background:var(--surface)}
|
|
:root:not([data-theme="light"]) .lh-card.selected{background:#2a1a1a;border-color:#ff385c}
|
|
:root:not([data-theme="light"]) .lh-card .logo{background:#181818;border-color:var(--muted)}
|
|
:root:not([data-theme="light"]) .lh-card .meta{color:#a0a0a0}
|
|
:root:not([data-theme="light"]) .lh-card:not(.selected) .check{border-color:var(--muted)}
|
|
:root:not([data-theme="light"]) .btn{background:#181818;border-color:var(--muted);color:var(--fg)}
|
|
:root:not([data-theme="light"]) .btn:active{background:var(--surface)}
|
|
:root:not([data-theme="light"]) .banner.warn{background:#2a1a1a;border-color:#5a3a1a;color:#ffb060}
|
|
:root:not([data-theme="light"]) .banner.err{background:#2a1a1a;border-color:#5a2429;color:#ff6680}
|
|
:root:not([data-theme="light"]) .skeleton{color:#666}
|
|
}
|
|
:root[data-theme="dark"]{
|
|
--bg:#0f0f0f;--surface:#1c1416;--fg:#f0f0f0;--muted:#2a2a2a;--border:#3a2429;
|
|
background:var(--bg);color:var(--fg);
|
|
}
|
|
:root[data-theme="dark"] body{background:var(--bg);color:var(--fg)}
|
|
:root[data-theme="dark"] .topbar{background:var(--bg)}
|
|
:root[data-theme="dark"] .lede{color:#a0a0a0}
|
|
:root[data-theme="dark"] .topbar .back{background:var(--surface);border-color:var(--border);color:var(--fg)}
|
|
:root[data-theme="dark"] .topbar .netpill{color:#a0a0a0;border-color:var(--muted)}
|
|
:root[data-theme="dark"] .topbar .netpill.off{color:#ff6680;border-color:#ff6680;background:#2a1a1a}
|
|
:root[data-theme="dark"] .topbar .theme-btn{color:var(--fg)}
|
|
:root[data-theme="dark"] .section-label{color:#a0a0a0}
|
|
:root[data-theme="dark"] .active{background:#2a1a1a;border-color:#ff385c}
|
|
:root[data-theme="dark"] .active .logo{background:#181818;border-color:var(--muted)}
|
|
:root[data-theme="dark"] .active .name{color:var(--fg)}
|
|
:root[data-theme="dark"] .lh-card{background:#181818;border-color:var(--muted);color:var(--fg)}
|
|
:root[data-theme="dark"] .lh-card:active{background:var(--surface)}
|
|
:root[data-theme="dark"] .lh-card.selected{background:#2a1a1a;border-color:#ff385c}
|
|
:root[data-theme="dark"] .lh-card .logo{background:#181818;border-color:var(--muted)}
|
|
:root[data-theme="dark"] .lh-card .meta{color:#a0a0a0}
|
|
:root[data-theme="dark"] .lh-card:not(.selected) .check{border-color:var(--muted)}
|
|
:root[data-theme="dark"] .btn{background:#181818;border-color:var(--muted);color:var(--fg)}
|
|
:root[data-theme="dark"] .btn:active{background:var(--surface)}
|
|
:root[data-theme="dark"] .banner.warn{background:#2a1a1a;border-color:#5a3a1a;color:#ffb060}
|
|
:root[data-theme="dark"] .banner.err{background:#2a1a1a;border-color:#5a2429;color:#ff6680}
|
|
:root[data-theme="dark"] .skeleton{color:#666}
|
|
:root[data-theme="dark"] .preview-modal{background:#1a1416}
|
|
:root[data-theme="dark"] .preview-modal .x{background:var(--surface)}
|
|
|
|
/* ---- Slip preview tabs ---- */
|
|
.preview-tabs{display:flex;gap:6px;padding:4px 4px 10px}
|
|
.preview-tab{flex:1;padding:8px 10px;border:1px solid var(--muted);border-radius:10px;background:#fff;font-size:13px;font-weight:600;color:var(--fg);transition:background .12s, color .12s, border-color .12s}
|
|
.preview-tab:active{background:var(--surface)}
|
|
.preview-tab.active{background:var(--fg);color:var(--bg);border-color:var(--fg)}
|
|
/* ---- Letterhead card — clickable to open the slip preview modal ---- */
|
|
.active .body{cursor:pointer;-webkit-tap-highlight-color:transparent}
|
|
.active .body:active{opacity:.85}
|
|
.active .body .name{text-decoration:underline;text-decoration-color:rgba(255,56,92,.3);text-underline-offset:3px}
|
|
.preview-note{margin:6px 4px 0;font-size:11px;color:#888;letter-spacing:.01em}
|
|
|
|
/* ---- Slip preview modal ---- */
|
|
.preview-modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:80;opacity:0;pointer-events:none;transition:opacity .15s}
|
|
.preview-modal-backdrop.show{opacity:1;pointer-events:auto}
|
|
.preview-modal{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%) scale(.96);background:var(--bg);border-radius:16px;padding:20px 20px 18px;width:calc(100% - 32px);max-width:440px;max-height:88dvh;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.3);opacity:0;pointer-events:none;transition:opacity .15s, transform .2s cubic-bezier(.2,.8,.2,1);z-index:81}
|
|
.preview-modal-backdrop.show .preview-modal{opacity:1;transform:translate(-50%,-50%) scale(1);pointer-events:auto}
|
|
.preview-modal h2{font-size:16px;font-weight:700;letter-spacing:-.01em;margin:0 0 4px}
|
|
.preview-modal .modal-sub{font-size:12px;color:var(--ink-soft,#717171);margin:0 0 14px}
|
|
.preview-modal .x{position:absolute;top:10px;right:10px;width:32px;height:32px;border-radius:50%;background:var(--surface);display:grid;place-items:center;color:var(--fg);border:0;cursor:pointer}
|
|
.preview-modal .x:active{background:var(--bg)}
|
|
|
|
/* ---- Receipt preview pane ---- */
|
|
.receipt-preview{
|
|
/* `monospace` is the generic CSS family — every browser maps it
|
|
to a real monospace font (Menlo/SF Mono on macOS, Consolas on
|
|
Windows, Roboto Mono / DejaVu on Linux/Android). The previous
|
|
"SF Mono,Menlo,Consolas" stack fell back to a proportional
|
|
font on this system and broke the right-column alignment. */
|
|
font-family:monospace;
|
|
font-size:10px;
|
|
line-height:1.35;
|
|
background:#fdfdfa;
|
|
color:#1a1a1a;
|
|
border:1px solid #999;
|
|
border-radius:4px;
|
|
padding:10px 10px;
|
|
/* 24 chars * ~10px + 20px padding = 260px. Anything wider and we
|
|
scroll horizontally instead of overflowing. */
|
|
width:260px;
|
|
max-width:100%;
|
|
margin:0 auto 4px;
|
|
box-shadow:0 2px 6px rgba(0,0,0,.08);
|
|
white-space:pre;
|
|
overflow-x:auto;
|
|
overflow-wrap:normal;
|
|
word-break:keep-all;
|
|
/* Mimic thermal paper: slightly off-white, ragged-edge look */
|
|
position:relative;
|
|
}
|
|
.receipt-preview::before, .receipt-preview::after{
|
|
content:'';
|
|
position:absolute;
|
|
left:0; right:0;
|
|
height:6px;
|
|
background:radial-gradient(circle at 3px 0, #fff 0 1.5px, transparent 1.5px) repeat-x;
|
|
background-size:6px 6px;
|
|
}
|
|
.receipt-preview::before{ top:-3px; }
|
|
.receipt-preview::after{ bottom:-3px; transform:scaleY(-1); }
|
|
.receipt-preview .center{text-align:center;display:block}
|
|
.receipt-preview .row{display:flex;justify-content:space-between;gap:8px}
|
|
.receipt-preview .grow{flex:1}
|
|
.receipt-preview .right{text-align:right;flex:0 0 auto}
|
|
.receipt-preview .dim{color:#666}
|
|
.receipt-preview .em{font-weight:700;color:#000}
|
|
.receipt-preview .total{font-weight:700}
|
|
.receipt-preview .divider{border-top:1px dashed #999;margin:6px 0;height:0}
|
|
.receipt-preview .signoff{text-align:center;color:#666;margin-top:6px}
|
|
.receipt-preview .empty{text-align:center;color:#999;padding:30px 0;font-family:inherit;white-space:normal}
|
|
|
|
/* The preview pane is intentionally light regardless of the till's
|
|
theme — thermal paper is always light. The wrapper around it still
|
|
follows the theme. */
|
|
|
|
@media (prefers-reduced-motion: reduce){*{transition-duration:0.01ms !important;animation-duration:0.01ms !important}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="topbar">
|
|
<button class="back" data-od-id="back-btn" onclick="location.href='index.html'" aria-label="Back">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 6l-6 6 6 6"/></svg>
|
|
</button>
|
|
<h1>Settings</h1>
|
|
<span class="netpill" id="netpill" data-od-id="netpill">…</span>
|
|
<button class="theme-btn" data-theme-toggle data-od-id="theme-toggle" aria-label="Toggle theme" title="Toggle theme" onclick="(window.WFTheme&&window.WFTheme.cycle())||(window.__wfCycle&&window.__wfCycle())">
|
|
<svg class="i-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
|
|
<svg class="i-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
|
</button>
|
|
</header>
|
|
|
|
<p class="lede">This till runs under the selected letterhead. Slips and receipts will use its name, address, VAT rate, and logo.</p>
|
|
|
|
<div id="activeBlock" class="skeleton">Loading…</div>
|
|
|
|
<div class="section-label">Switch letterhead</div>
|
|
<div class="lh-list" id="lhList"></div>
|
|
|
|
<div class="section-label">Slip layout</div>
|
|
<button class="lh-card" style="width:100%" onclick="location.href='slip-layout.html'" type="button">
|
|
<div class="body">
|
|
<div class="name">Edit slip layout</div>
|
|
<div class="meta">Choose what prints on each slip, in what order</div>
|
|
</div>
|
|
</button>
|
|
|
|
<div id="banner" hidden></div>
|
|
|
|
<div class="actions">
|
|
<button class="btn primary" data-od-id="test-print" onclick="testPrint()">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M6 9V3h12v6M6 18H4v-7h16v7h-2M6 14h12v7H6z"/></svg>
|
|
Test print
|
|
</button>
|
|
<button class="btn" data-od-id="done-btn" onclick="location.href='index.html'">
|
|
Done
|
|
</button>
|
|
</div>
|
|
|
|
<script src="pos-config.js"></script>
|
|
<script src="theme.js"></script>
|
|
<script src="letterhead-config.js"></script>
|
|
<script src="slip-layout.js"></script>
|
|
<script src="epos-print.js"></script>
|
|
<script>
|
|
'use strict';
|
|
|
|
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]);}
|
|
function fmtVat(n){return (Number(n)||0).toString().replace(/\.0$/,'') + '% VAT';}
|
|
function initialsOf(name){
|
|
const s = String(name||'').trim();
|
|
if(!s) return '?';
|
|
return s.split(/\s+/).slice(0,2).map(w => w[0]||'').join('').toUpperCase();
|
|
}
|
|
function logoHtml(item, klass){
|
|
if(item && item.has_logo && item.id){
|
|
// We don't have the full logo URL on the list view (it lives on
|
|
// the full record). The list cards have a placeholder; once the
|
|
// till picks one and we refetch the full record, the active card
|
|
// shows the real logo.
|
|
return '<div class="logo '+klass+'"><span class="placeholder">'+esc(initialsOf(item.display_name))+'</span></div>';
|
|
}
|
|
return '<div class="logo '+klass+'"><span class="placeholder">'+esc(initialsOf(item.display_name))+'</span></div>';
|
|
}
|
|
|
|
function paintNet(){
|
|
const p = document.getElementById('netpill');
|
|
if(!p) return;
|
|
p.textContent = navigator.onLine ? 'Online' : 'Offline';
|
|
p.classList.toggle('off', !navigator.onLine);
|
|
}
|
|
window.addEventListener('online', paintNet);
|
|
window.addEventListener('offline', paintNet);
|
|
paintNet();
|
|
|
|
function showBanner(kind, text){
|
|
const el = document.getElementById('banner');
|
|
if(!text){ el.hidden = true; el.innerHTML = ''; return; }
|
|
el.hidden = false;
|
|
el.innerHTML = '<div class="banner '+kind+'"><span class="ic">'+(kind==='err'
|
|
? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16v.5"/></svg>'
|
|
: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4M12 17v.5M3 18l9-15 9 15z"/></svg>'
|
|
)+'</span><span class="t">'+esc(text)+'</span></div>';
|
|
}
|
|
|
|
function paintActive(item){
|
|
const el = document.getElementById('activeBlock');
|
|
if(!item){
|
|
el.className = 'banner err';
|
|
el.innerHTML = '<span class="ic">'
|
|
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M12 8v5M12 16v.5"/></svg>'
|
|
+ '</span><span class="t">No active letterhead. Pick one below to start ringing up sales.</span>';
|
|
return;
|
|
}
|
|
el.className = 'active';
|
|
const logo = item.logo_url
|
|
? '<img src="'+esc(item.logo_url)+'" alt="">'
|
|
: '<span class="placeholder">'+esc(initialsOf(item.display_name))+'</span>';
|
|
const vatPill = item.vat_percent > 0
|
|
? '<span class="pill">'+esc(fmtVat(item.vat_percent))+'</span>'
|
|
: '<span class="pill muted">No VAT</span>';
|
|
const acc = item.accent_color
|
|
? '<span class="pill muted" style="color:'+esc(item.accent_color)+';border-color:'+esc(item.accent_color)+'">Accent '+esc(item.accent_color)+'</span>'
|
|
: '';
|
|
el.innerHTML = ''
|
|
+ '<div class="logo">'+logo+'</div>'
|
|
+ '<div class="body">'
|
|
+ '<div class="name">'+esc(item.display_name)+'</div>'
|
|
+ '<div class="sub">'
|
|
+ '<span>'+esc(item.company || '')+'</span>'
|
|
+ vatPill
|
|
+ acc
|
|
+ '</div>'
|
|
+ '</div>';
|
|
}
|
|
|
|
function paintList(list, activeId){
|
|
const wrap = document.getElementById('lhList');
|
|
if(!list.length){
|
|
wrap.innerHTML = '<div class="skeleton">No active letterheads configured in PocketBase.</div>';
|
|
return;
|
|
}
|
|
wrap.innerHTML = list.map(item => {
|
|
const isSel = item.id === activeId;
|
|
const meta = [
|
|
item.vat_percent > 0 ? fmtVat(item.vat_percent) : 'No VAT',
|
|
item.accent_color ? 'Accent ' + item.accent_color : null,
|
|
].filter(Boolean).join(' · ');
|
|
return ''
|
|
+ '<button type="button" class="lh-card'+(isSel ? ' selected' : '')+'" data-id="'+esc(item.id)+'" onclick="pickLetterhead(\''+esc(item.id)+'\')">'
|
|
+ logoHtml(item, '')
|
|
+ '<div class="body">'
|
|
+ '<div class="name">'+esc(item.display_name)+'</div>'
|
|
+ '<div class="meta">'+esc(meta)+'</div>'
|
|
+ '</div>'
|
|
+ '<div class="check">✓</div>'
|
|
+ '</button>';
|
|
}).join('');
|
|
}
|
|
|
|
async function pickLetterhead(id){
|
|
showBanner(null);
|
|
try{
|
|
const item = await window.PosLetterhead.fetchById(id);
|
|
paintActive(item);
|
|
// Re-render the list so the new "selected" check shows up.
|
|
const list = window.PosLetterhead.getCachedList();
|
|
paintList(list, item.id);
|
|
}catch(e){
|
|
showBanner('err', e.message || 'Could not pick letterhead');
|
|
}
|
|
}
|
|
|
|
async function testPrint(){
|
|
showBanner(null);
|
|
const btn = document.querySelector('[data-od-id="test-print"]');
|
|
if(btn) btn.disabled = true;
|
|
try{
|
|
const letterhead = await window.PosLetterhead.ensureActive();
|
|
if(!letterhead){
|
|
showBanner('warn', 'Pick a letterhead first.');
|
|
return;
|
|
}
|
|
const where = await window.EposPrint.printTest(letterhead);
|
|
showBanner(null); // success — no banner
|
|
if(btn){
|
|
// Briefly flash the success into the button label.
|
|
const orig = btn.textContent;
|
|
btn.textContent = '✓ ' + where;
|
|
setTimeout(() => { btn.textContent = orig; }, 1800);
|
|
}
|
|
}catch(e){
|
|
showBanner('err', e.message || 'Test print failed');
|
|
}finally{
|
|
if(btn) btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function boot(){
|
|
const wrap = document.getElementById('activeBlock');
|
|
wrap.textContent = 'Loading…';
|
|
let active = null;
|
|
try{
|
|
active = await window.PosLetterhead.ensureActive();
|
|
}catch(e){
|
|
showBanner('err', 'Could not load letterhead: ' + (e.message || e));
|
|
}
|
|
paintActive(active);
|
|
// Try to populate the letterhead picker list.
|
|
let list = window.PosLetterhead.getCachedList();
|
|
if(!list.length){
|
|
try{ list = await window.PosLetterhead.fetchList(); }
|
|
catch(e){
|
|
document.getElementById('lhList').innerHTML = '<div class="skeleton">Letterhead list unavailable offline. Reconnect to set up.</div>';
|
|
return;
|
|
}
|
|
}
|
|
paintList(list, active ? active.id : null);
|
|
}
|
|
|
|
boot();
|
|
</script>
|
|
<script>if("serviceWorker" in navigator){navigator.serviceWorker.register("sw.js").catch(function(){});}</script>
|
|
</body>
|
|
</html>
|