WFDASH-WEBAPP/public/pos/settings.html

334 lines
18 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}
@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 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="epos-print.js"></script>
<script>
'use strict';
function esc(s){return String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[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);
// Always show the list so the operator can switch.
let list = window.PosLetterhead.getCachedList();
if(!list.length){
try{ list = await window.PosLetterhead.fetchList(); }
catch(e){
// Cached list unavailable (offline + first run). Render a hint
// rather than nothing.
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>