2026-08-30 23:41:54 +00:00
|
|
|
|
<!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){
|
fix(pos): dead discount toggle + stale discount figure, lift dark palette
Two reported symptoms that turned out to be one bug, plus the dark
mode the user described as "too black".
The `hidden` attribute does nothing on the discount controls
`hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`.
Any author rule that sets `display` outranks it — and `.totals` and
`.discount-body` both set `display:flex`. So `el.hidden = true` was a
no-op on both:
- the discount tier panel could not be collapsed, making "Add
discount" look like a dead control;
- the Discount totals row stayed on screen after the discount was
cleared, still showing the last figure (R 98.37) under a total
that no longer contained it. The JS only ever set `hidden` and
never cleared the text, so the operator had no way to tell stale
text from a live one.
`.cat-menu` and `.cat-backdrop` already carried per-element
`[hidden]{display:none}` overrides for exactly this reason; the two
discount elements were missed. Replaced with one global rule so the
whole class is covered rather than relying on each element to
remember. The clear path now also blanks the amount.
Verified statically rather than in a browser: jsdom does not model
the UA-vs-author cascade and reported the broken and fixed cases
identically, so the cascade was checked by resolving which rules
apply. Regression test fails without the fix.
Dark palette
The POS canvas was #0f0f0f (L* 4.3 — effectively black) with
surfaces only 3-4 L* above it, mixing a warm #1c1416 against a
neutral #181818 in the same UI. That near-black base plus two
competing surface tones is what read as muddy. Replaced with a
single cool-neutral ramp with four real steps, and menus/modals now
sit on a `--raised` layer instead of reusing the surface colour.
`#717171` was the secondary text colour in 25 places and measured
3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1.
Both the text greys and the surfaces were hardcoded per component
across seven POS pages; they are now tokens, with the dark block
rebinding them.
The shared `.dark` block in themes.css (drives all 24 themes) sat at
oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its
shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still
above AA. Cursor/Supabase, which override it darker, were at L* 1.3
and lifted too.
Service worker cache bumped to pos-v36 — pos.html and pos-app.js are
served cache-first, so without the bump the tills keep serving the
broken files.
3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
|
|
|
|
:root:not([data-theme="light"]){--bg:#161619;--surface:#1e1e23;--fg:#f2f2f5;--muted:#303039;--border:#3b3b46}
|
2026-08-30 23:41:54 +00:00
|
|
|
|
: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}
|
fix(pos): dead discount toggle + stale discount figure, lift dark palette
Two reported symptoms that turned out to be one bug, plus the dark
mode the user described as "too black".
The `hidden` attribute does nothing on the discount controls
`hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`.
Any author rule that sets `display` outranks it — and `.totals` and
`.discount-body` both set `display:flex`. So `el.hidden = true` was a
no-op on both:
- the discount tier panel could not be collapsed, making "Add
discount" look like a dead control;
- the Discount totals row stayed on screen after the discount was
cleared, still showing the last figure (R 98.37) under a total
that no longer contained it. The JS only ever set `hidden` and
never cleared the text, so the operator had no way to tell stale
text from a live one.
`.cat-menu` and `.cat-backdrop` already carried per-element
`[hidden]{display:none}` overrides for exactly this reason; the two
discount elements were missed. Replaced with one global rule so the
whole class is covered rather than relying on each element to
remember. The clear path now also blanks the amount.
Verified statically rather than in a browser: jsdom does not model
the UA-vs-author cascade and reported the broken and fixed cases
identically, so the cascade was checked by resolving which rules
apply. Regression test fails without the fix.
Dark palette
The POS canvas was #0f0f0f (L* 4.3 — effectively black) with
surfaces only 3-4 L* above it, mixing a warm #1c1416 against a
neutral #181818 in the same UI. That near-black base plus two
competing surface tones is what read as muddy. Replaced with a
single cool-neutral ramp with four real steps, and menus/modals now
sit on a `--raised` layer instead of reusing the surface colour.
`#717171` was the secondary text colour in 25 places and measured
3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1.
Both the text greys and the surfaces were hardcoded per component
across seven POS pages; they are now tokens, with the dark block
rebinding them.
The shared `.dark` block in themes.css (drives all 24 themes) sat at
oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its
shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still
above AA. Cursor/Supabase, which override it darker, were at L* 1.3
and lifted too.
Service worker cache bumped to pos-v36 — pos.html and pos-app.js are
served cache-first, so without the bump the tills keep serving the
broken files.
3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
|
|
|
|
:root:not([data-theme="light"]) .card{background:var(--surface)}
|
|
|
|
|
|
:root:not([data-theme="light"]) .statusbar{background:var(--surface);border-color:var(--border);color:#a0a0a0}
|
|
|
|
|
|
:root:not([data-theme="light"]) .pill{background:var(--surface);border-color:var(--border);color:#f0f0f0}
|
2026-08-30 23:41:54 +00:00
|
|
|
|
:root:not([data-theme="light"]) .pill .dot{box-shadow:0 0 0 3px rgba(15,138,63,.2)}
|
|
|
|
|
|
}
|
|
|
|
|
|
:root[data-theme="dark"]{
|
fix(pos): dead discount toggle + stale discount figure, lift dark palette
Two reported symptoms that turned out to be one bug, plus the dark
mode the user described as "too black".
The `hidden` attribute does nothing on the discount controls
`hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`.
Any author rule that sets `display` outranks it — and `.totals` and
`.discount-body` both set `display:flex`. So `el.hidden = true` was a
no-op on both:
- the discount tier panel could not be collapsed, making "Add
discount" look like a dead control;
- the Discount totals row stayed on screen after the discount was
cleared, still showing the last figure (R 98.37) under a total
that no longer contained it. The JS only ever set `hidden` and
never cleared the text, so the operator had no way to tell stale
text from a live one.
`.cat-menu` and `.cat-backdrop` already carried per-element
`[hidden]{display:none}` overrides for exactly this reason; the two
discount elements were missed. Replaced with one global rule so the
whole class is covered rather than relying on each element to
remember. The clear path now also blanks the amount.
Verified statically rather than in a browser: jsdom does not model
the UA-vs-author cascade and reported the broken and fixed cases
identically, so the cascade was checked by resolving which rules
apply. Regression test fails without the fix.
Dark palette
The POS canvas was #0f0f0f (L* 4.3 — effectively black) with
surfaces only 3-4 L* above it, mixing a warm #1c1416 against a
neutral #181818 in the same UI. That near-black base plus two
competing surface tones is what read as muddy. Replaced with a
single cool-neutral ramp with four real steps, and menus/modals now
sit on a `--raised` layer instead of reusing the surface colour.
`#717171` was the secondary text colour in 25 places and measured
3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1.
Both the text greys and the surfaces were hardcoded per component
across seven POS pages; they are now tokens, with the dark block
rebinding them.
The shared `.dark` block in themes.css (drives all 24 themes) sat at
oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its
shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still
above AA. Cursor/Supabase, which override it darker, were at L* 1.3
and lifted too.
Service worker cache bumped to pos-v36 — pos.html and pos-app.js are
served cache-first, so without the bump the tills keep serving the
broken files.
3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
|
|
|
|
--bg:#161619;--surface:#1e1e23;--fg:#f2f2f5;--muted:#303039;--border:#3b3b46;
|
2026-08-30 23:41:54 +00:00
|
|
|
|
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}
|
fix(pos): dead discount toggle + stale discount figure, lift dark palette
Two reported symptoms that turned out to be one bug, plus the dark
mode the user described as "too black".
The `hidden` attribute does nothing on the discount controls
`hidden` is implemented by the UA stylesheet as `[hidden]{display:none}`.
Any author rule that sets `display` outranks it — and `.totals` and
`.discount-body` both set `display:flex`. So `el.hidden = true` was a
no-op on both:
- the discount tier panel could not be collapsed, making "Add
discount" look like a dead control;
- the Discount totals row stayed on screen after the discount was
cleared, still showing the last figure (R 98.37) under a total
that no longer contained it. The JS only ever set `hidden` and
never cleared the text, so the operator had no way to tell stale
text from a live one.
`.cat-menu` and `.cat-backdrop` already carried per-element
`[hidden]{display:none}` overrides for exactly this reason; the two
discount elements were missed. Replaced with one global rule so the
whole class is covered rather than relying on each element to
remember. The clear path now also blanks the amount.
Verified statically rather than in a browser: jsdom does not model
the UA-vs-author cascade and reported the broken and fixed cases
identically, so the cascade was checked by resolving which rules
apply. Regression test fails without the fix.
Dark palette
The POS canvas was #0f0f0f (L* 4.3 — effectively black) with
surfaces only 3-4 L* above it, mixing a warm #1c1416 against a
neutral #181818 in the same UI. That near-black base plus two
competing surface tones is what read as muddy. Replaced with a
single cool-neutral ramp with four real steps, and menus/modals now
sit on a `--raised` layer instead of reusing the surface colour.
`#717171` was the secondary text colour in 25 places and measured
3.93:1 on the old canvas — below WCAG AA. It now measures 6.49:1.
Both the text greys and the surfaces were hardcoded per component
across seven POS pages; they are now tokens, with the dark block
rebinding them.
The shared `.dark` block in themes.css (drives all 24 themes) sat at
oklch(0.15) ≈ L* 3.1. Lifted +0.04 across the ramp, preserving its
shape and separation; muted-foreground goes 6.32:1 → 5.95:1, still
above AA. Cursor/Supabase, which override it darker, were at L* 1.3
and lifted too.
Service worker cache bumped to pos-v36 — pos.html and pos-app.js are
served cache-first, so without the bump the tills keep serving the
broken files.
3863 tests pass (3 new); build clean.
2026-10-12 21:17:57 +00:00
|
|
|
|
:root[data-theme="dark"] .card{background:var(--surface)}
|
|
|
|
|
|
:root[data-theme="dark"] .statusbar{background:var(--surface);border-color:var(--border);color:#a0a0a0}
|
|
|
|
|
|
:root[data-theme="dark"] .pill{background:var(--surface);border-color:var(--border);color:#f0f0f0}
|
2026-08-30 23:41:54 +00:00
|
|
|
|
@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>
|
2026-09-04 07:13:53 +00:00
|
|
|
|
<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>
|
2026-08-30 23:41:54 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
|
2026-09-04 07:13:53 +00:00
|
|
|
|
<p class="foot"><b id="prodCount">—</b> products · cache <span id="cacheState">pending</span> · <span id="activeLetterhead">letterhead —</span></p>
|
2026-08-30 23:41:54 +00:00
|
|
|
|
|
2026-08-30 23:42:24 +00:00
|
|
|
|
<script src="pos-config.js"></script>
|
2026-08-30 23:41:54 +00:00
|
|
|
|
<script src="theme.js"></script>
|
2026-09-04 07:13:53 +00:00
|
|
|
|
<script src="letterhead-config.js"></script>
|
2026-08-30 23:41:54 +00:00
|
|
|
|
<script>
|
|
|
|
|
|
(async function(){
|
2026-08-30 23:44:55 +00:00
|
|
|
|
// 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;
|
2026-08-30 23:41:54 +00:00
|
|
|
|
try{
|
2026-08-30 23:44:55 +00:00
|
|
|
|
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();
|
2026-08-30 23:41:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
}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');
|
2026-08-30 23:44:55 +00:00
|
|
|
|
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';
|
2026-08-30 23:41:54 +00:00
|
|
|
|
}
|
|
|
|
|
|
window.addEventListener('online', paintNet);
|
|
|
|
|
|
window.addEventListener('offline', paintNet);
|
|
|
|
|
|
paintNet();
|
|
|
|
|
|
if('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js').catch(()=>{});
|
2026-09-04 07:13:53 +00:00
|
|
|
|
// 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'; }
|
|
|
|
|
|
}
|
2026-08-30 23:41:54 +00:00
|
|
|
|
})();
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|