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.
146 lines
6.9 KiB
JavaScript
146 lines
6.9 KiB
JavaScript
// Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static.
|
|
// v30 — step 2 of the catalog search work: pos-search.js gains plural
|
|
// handling ("nuts" also finds "nut", so "bolts" goes from 0 hits to the 884
|
|
// real bolt rows) and glued-token splitting ("M20NUT" finds "M20 Hex Nut").
|
|
// Bumped again even though the shell already contained pos-search.js: that
|
|
// file is served cache-first, so without a new CACHE name tills would keep
|
|
// running the step-1 build of it indefinitely.
|
|
// Measured on the live catalog: "nuts m20" 0 → 12, "washers flat" 0 → 14,
|
|
// with nothing that used to match ceasing to match.
|
|
// v35 — sale refunds. Hold a sale row in history.html to refund the whole
|
|
// sale: the items go back into stock (stock_ledger reason "return", the
|
|
// reason that already existed unused) and a credit slip prints reusing the
|
|
// sale printer path with refund:true. Refunds are whole-sale only, so
|
|
// pos_sales.refunded is a boolean and the server refuses a second one with
|
|
// 409. cashup.html subtracts cash refunds from the expected drawer —
|
|
// without that, every cash-up after a refund shows a false variance.
|
|
// Bumped because history.html, cashup.html, receipt.html and epos-print.js
|
|
// are all served cache-first out of the shell.
|
|
// v34 — escapePbValue now drops backslashes rather than escaping them:
|
|
// PB has no backslash escape in filters (1, 2 or 3 all throw), so a raw one
|
|
// 500s the request. v33 already removed the price field from the edit sheet.
|
|
// v34 — the account-sale customer picker now matches the COMPANY name as
|
|
// well as the person's name/number/email, so typing "Sibanye" returns that
|
|
// mine's staff instead of nothing. /api/v1/pos/customers also returns a
|
|
// real `total` now: the old `slice(0, 8)` silently showed 8 of 52 people
|
|
// at the largest company and read as the whole team, so the till renders
|
|
// "showing N of M" and says so when the list is cut. Name matches rank
|
|
// above company-only matches so an OR'd filter can't bury the person the
|
|
// cashier typed. escapePbValue also escapes backslashes now — a single
|
|
// "\" in the search box used to throw and 500 the request.
|
|
// v33 — the Edit line sheet is quantity-only. The Unit price field is gone
|
|
// on request: the till sells at the catalogue shelf price, and a mis-keyed
|
|
// price is expensive and easy to miss. The per-line price override it fed is
|
|
// removed with it — the server only ever read `unit_price`, and the
|
|
// "price edited" label plus the sale record's `edited` flag are now
|
|
// unreachable. Nothing else could set an override, including a cart reload.
|
|
// v32 — fix the Edit line sheet, which never actually displayed. #editSheet
|
|
// is a sibling of #editBackdrop, but the CSS revealed it with
|
|
// `.edit-backdrop.show .edit-sheet`, a descendant selector that can never
|
|
// match a sibling — so the sheet sat at opacity:0 while the backdrop went
|
|
// opaque. Long-pressing a cart row produced a grey screen with no dialog.
|
|
// The sheet now carries its own z-index above the backdrop and its own
|
|
// .show rule. This is why manual quantity entry looked missing: the control
|
|
// existed and worked, it was just rendering underneath its own backdrop.
|
|
// Bumped because pos.html is served cache-first out of the shell.
|
|
// v31 — cart quantity is now directly editable: the number in each cart line
|
|
// is a real button that opens the existing Edit line sheet (previously only
|
|
// reachable by an undiscoverable 600ms long-press), and holding +/- no
|
|
// longer pops that sheet open mid-tap. The discount trigger moved out of its
|
|
// own 54px footer row to ride inside the Subtotal row, so it costs no
|
|
// vertical height while staying visible.
|
|
// Bumped because pos-app.js and pos.html are both served cache-first out of
|
|
// the shell; without a new CACHE name the tills keep the old ones.
|
|
// v36 — the discount panel and the Discount totals row both carry a `hidden`
|
|
// attribute, but their classes (.discount-body, .totals) set `display:flex`,
|
|
// and an author display rule outranks the UA stylesheet's
|
|
// `[hidden]{display:none}`. So `hidden` did nothing: "Add discount" could not
|
|
// collapse the tier buttons, and a cleared discount left its old amount
|
|
// painted under an unchanged total. Fixed with a global `[hidden]` override
|
|
// plus blanking the figure when the row is hidden. Also carries the dark
|
|
// palette lift.
|
|
const CACHE = "pos-v36";
|
|
const APP_SHELL = [
|
|
'./',
|
|
'./index.html',
|
|
'./pos.html',
|
|
'./receipt.html',
|
|
'./history.html',
|
|
'./cashup.html',
|
|
'./settings.html',
|
|
'./slip-layout.html',
|
|
'./pos-config.js',
|
|
'./pos-search.js',
|
|
'./pos-app.js',
|
|
'./letterhead-config.js',
|
|
'./slip-layout.js',
|
|
'./epos-print.js',
|
|
'./theme.js',
|
|
'./manifest.json',
|
|
// Font Awesome 6 Free (solid) — bundled so the till still renders icons offline.
|
|
'./fa/all.min.css',
|
|
'./fa/webfonts/fa-solid-900.woff2',
|
|
];
|
|
|
|
// Files that must always be fresh (network-first, fallback to cache)
|
|
const NETWORK_FIRST = new Set([
|
|
'theme.js'
|
|
]);
|
|
|
|
self.addEventListener('install', e => {
|
|
e.waitUntil(caches.open(CACHE).then(c => c.addAll(APP_SHELL)).then(()=>self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener('activate', e => {
|
|
e.waitUntil(caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k)))).then(()=>self.clients.claim()));
|
|
});
|
|
|
|
self.addEventListener('fetch', e => {
|
|
const url = new URL(e.request.url);
|
|
// Never cache API responses — only the static app shell. Serving a
|
|
// stale /catalog or, worse, a cached /checkout would be actively wrong.
|
|
if(url.pathname.startsWith('/api/')) return;
|
|
// HTML navigations: always network-first so the user gets the latest page
|
|
if(e.request.mode === 'navigate'){
|
|
e.respondWith(
|
|
fetch(e.request).then(res => {
|
|
if(res && res.ok) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => caches.match(e.request).then(r => r || new Response('<h1>Offline</h1>', {status:503, headers:{'Content-Type':'text/html'}})))
|
|
);
|
|
return;
|
|
}
|
|
if(e.request.method !== 'GET') return;
|
|
|
|
// theme.js (and other network-first assets) always go to network
|
|
const fname = url.pathname.split('/').pop();
|
|
if(NETWORK_FIRST.has(fname)){
|
|
e.respondWith(
|
|
fetch(e.request).then(res => {
|
|
if(res && res.ok){
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => caches.match(e.request).then(r => r || new Response('', {status:504})))
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Default: cache-first for static assets
|
|
e.respondWith(
|
|
caches.match(e.request).then(cached => {
|
|
const fetchP = fetch(e.request).then(res => {
|
|
if(res && res.ok && (e.request.url.startsWith('http'))) {
|
|
const clone = res.clone();
|
|
caches.open(CACHE).then(c => c.put(e.request, clone));
|
|
}
|
|
return res;
|
|
}).catch(() => cached);
|
|
return cached || fetchP;
|
|
})
|
|
);
|
|
});
|