WFDASH-WEBAPP/public/pos/index.html

199 lines
12 KiB
HTML
Raw Normal View History

<!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>
<a class="card" data-od-id="launcher-card-settings" href="settings.html">
<span class="ic" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></span>
<span class="body">
<span class="title">Settings</span>
<span class="desc">Pick letterhead, configure printer</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> · <span id="activeLetterhead">letterhead —</span></p>
<script src="pos-config.js"></script>
<script src="theme.js"></script>
<script src="letterhead-config.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(()=>{});
// Active letterhead — show a one-word status in the footer so the
// operator notices if the till somehow has no letterhead picked
// (the settings page will gate sales in that case).
try{
const active = await window.PosLetterhead.ensureActive();
const lh = document.getElementById('activeLetterhead');
if(lh){
lh.textContent = active
? ('letterhead ' + active.display_name.split(' ')[0].toLowerCase())
: 'letterhead none';
if(!active) lh.style.color = '#c92a2a';
}
}catch(e){
const lh = document.getElementById('activeLetterhead');
if(lh){ lh.textContent = 'letterhead error'; lh.style.color = '#c92a2a'; }
}
})();
</script>
</body>
</html>