WFDASH-WEBAPP/public/pos/index.html
Dave 600b3ee3bf feat(pos): rewire index.html product count to /api/v1/pos/catalog
Also surfaces a rejected API key explicitly instead of letting it read
as an offline blip.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 01:44:55 +02:00

173 lines
10 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>WFSales — Register</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), 0 16px 32px rgba(34,34,34,.08);
--font:'Inter',system-ui,-apple-system,Segoe UI,Helvetica Neue,Arial,sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
a:focus-visible,button:focus-visible{outline:0;box-shadow:0 0 0 3px rgba(255,56,92,.30);border-radius:14px}
.theme-btn:focus-visible{border-radius:50%}
.card:focus-visible{border-color:var(--accent)}
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:24px 20px 32px;background:var(--bg)}
header{display:flex;align-items:center;gap:12px;padding:12px 0 24px}
.logo{width:42px;height:42px;border-radius:10px;background:var(--accent);color:#fff;display:grid;place-items:center;font-weight:700;font-size:20px;letter-spacing:-.01em;flex:0 0 auto}
.brand{display:flex;flex-direction:column;line-height:1.15}
.brand .name{font-weight:700;font-size:17px;letter-spacing:-.01em}
.brand .sub{font-size:12px;color:#717171;letter-spacing:.02em}
.pill{margin-left:auto;display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid var(--border);border-radius:999px;background:var(--surface);font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--fg)}
.pill .dot{width:6px;height:6px;border-radius:50%;background:var(--ok)}
.pill.offline .dot{background:var(--danger)}
h1{font-size:28px;font-weight:700;letter-spacing:-.02em;line-height:1.15;margin:8px 0 4px}
.lede{color:#717171;font-size:14px;line-height:1.55;margin-bottom:20px;max-width:38ch}
.cards{display:grid;gap:12px;margin-top:auto}
.card{display:flex;align-items:center;gap:14px;padding:18px 16px;background:#fff;border:1px solid var(--muted);border-radius:14px;text-decoration:none;color:var(--fg);box-shadow:var(--shadow);transition:transform .12s ease,box-shadow .12s ease;min-height:64px}
.card:active{transform:translateY(1px);box-shadow:0 1px 2px rgba(34,34,34,.04),0 2px 6px rgba(34,34,34,.06)}
.card .ic{width:44px;height:44px;border-radius:50%;background:var(--surface);border:1px solid var(--border);display:grid;place-items:center;color:var(--accent);flex:0 0 auto}
.card .ic svg{width:22px;height:22px;stroke-width:1.7}
.card .body{flex:1;min-width:0}
.card .title{font-size:16px;font-weight:600;letter-spacing:-.01em;line-height:1.2}
.card .desc{font-size:12px;color:#717171;margin-top:2px;line-height:1.4}
.card .go{color:var(--accent);font-size:12px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
.section-label{font-size:11px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:#717171;margin:18px 4px 8px}
.foot{margin-top:18px;text-align:center;font-size:11px;color:#999;letter-spacing:.04em}
.foot b{color:var(--fg);font-weight:600}
.statusbar{margin-top:14px;padding:10px 12px;border:1px solid var(--border);background:var(--surface);border-radius:10px;font-size:12px;color:#717171;display:flex;align-items:center;gap:8px}
.statusbar .ic{width:14px;height:14px;flex:0 0 auto}
.theme-btn{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;color:var(--fg);flex:0 0 auto;background:transparent;border:0;cursor:pointer;position:relative}
.theme-btn:active{background:var(--surface)}
.theme-btn svg{width:18px;height:18px}
.theme-btn .i-moon{display:none}
:root[data-theme="dark"] .theme-btn .i-sun{display:none}
:root[data-theme="dark"] .theme-btn .i-moon{display:block}
@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"]) .lede,:root:not([data-theme="light"]) .brand .sub,:root:not([data-theme="light"]) .card .desc,:root:not([data-theme="light"]) .foot{color:#a0a0a0}
:root:not([data-theme="light"]) .card{background:#181818}
:root:not([data-theme="light"]) .statusbar{background:#181818;border-color:#3a2429;color:#a0a0a0}
:root:not([data-theme="light"]) .pill{background:#181818;border-color:#3a2429;color:#f0f0f0}
:root:not([data-theme="light"]) .pill .dot{box-shadow:0 0 0 3px rgba(15,138,63,.2)}
}
: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"] .lede,:root[data-theme="dark"] .brand .sub,:root[data-theme="dark"] .card .desc,:root[data-theme="dark"] .foot{color:#a0a0a0}
:root[data-theme="dark"] .card{background:#181818}
:root[data-theme="dark"] .statusbar{background:#181818;border-color:#3a2429;color:#a0a0a0}
:root[data-theme="dark"] .pill{background:#181818;border-color:#3a2429;color:#f0f0f0}
@media (prefers-reduced-motion: reduce){*{transition-duration:0.01ms !important;animation-duration:0.01ms !important}}
</style>
</head>
<body>
<header>
<div class="logo" aria-hidden="true">J</div>
<div class="brand">
<span class="name" id="brandName">WFSALES POS</span>
<span class="sub" id="brandSub">Point of sale</span>
</div>
<span class="pill" id="netPill" data-state="online"><span class="dot"></span><span id="netLabel">Online</span></span>
<button class="theme-btn" data-theme-toggle data-od-id="theme-toggle" aria-label="Toggle theme" title="Toggle theme" style="margin-left:6px" 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>
<h1>Register</h1>
<p class="lede" id="lede">Start a sale, reprint a slip, or close out the till. Works offline — your sales sync when you reconnect.</p>
<div class="cards">
<a class="card" data-od-id="launcher-card-pos" href="pos.html">
<span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M3 7h18l-2 12H5L3 7z"/><path d="M8 7V5a4 4 0 0 1 8 0v2"/></svg></span>
<span class="body">
<span class="title">New sale</span>
<span class="desc">Browse catalog, build cart, take payment</span>
</span>
<span class="go">Open ›</span>
</a>
<a class="card" data-od-id="launcher-card-history" href="history.html">
<span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg></span>
<span class="body">
<span class="title">Sales history</span>
<span class="desc">Reprint slip, view today's sales</span>
</span>
<span class="go">Open ›</span>
</a>
<a class="card" data-od-id="launcher-card-cashup" href="cashup.html">
<span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="M4 19V8M9 19V8M14 19V8M19 19V8"/><path d="M3 5h18M3 21h18"/></svg></span>
<span class="body">
<span class="title">Cash-up</span>
<span class="desc">End-of-day takings report</span>
</span>
<span class="go">Open ›</span>
</a>
</div>
<div class="section-label">Till</div>
<div class="statusbar" id="statusBar">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
<span id="statusMsg">Connecting to register…</span>
</div>
<p class="foot"><b id="prodCount">—</b> products · cache <span id="cacheState">pending</span></p>
<script src="pos-config.js"></script>
<script src="theme.js"></script>
<script>
(async function(){
// Product count via the same catalog endpoint pos.html uses.
// A 401 here means the till's API key is wrong — say so, rather than
// letting it read as an offline blip.
let authFailed = false;
try{
const r = await window.posFetch('/catalog');
if(r.status === 401){ authFailed = true; }
else if(r.ok){
const j = await r.json();
document.getElementById('prodCount').textContent = (j.totalItems||0).toLocaleString();
}
}catch{}
// Cache state
try{
const c = await caches.open('pos-v1');
const keys = await c.keys();
document.getElementById('cacheState').textContent = keys.length ? 'ready' : 'cold';
}catch{ document.getElementById('cacheState').textContent = '—'; }
// Network pill
function paintNet(){
const on = navigator.onLine;
const p = document.getElementById('netPill');
const l = document.getElementById('netLabel');
p.classList.toggle('offline', !on);
l.textContent = on ? 'Online' : 'Offline';
const s = document.getElementById('statusMsg');
if(authFailed){
s.textContent = 'This till’s API key was rejected — tap to re-enter it';
s.style.cursor = 'pointer';
s.onclick = () => window.posClearApiKey();
return;
}
s.textContent = on ? 'Connected' : 'Offline — sales will sync when you reconnect';
}
window.addEventListener('online', paintNet);
window.addEventListener('offline', paintNet);
paintNet();
if('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js').catch(()=>{});
})();
</script>
</body>
</html>