WFDASH-WEBAPP/public/pos/sw.js
Dave 36704bd48b fix(pos): put the stock quantity under the price, not in the sub line
The quantity went into the .sub line, which is nowrap + ellipsis: a
28- or 38-character SKU truncated it, so the number fell off the right
edge and pushed the in-cart count out with it. The sub line is back to
what it was — sku, brand, unit, N in cart — and the quantity now sits
under the price, right-aligned, where nothing truncates it.

The POS tests asserted the file's prose rather than its expressions, so
five mutations of the implementation all passed them: a min_qty cue, a
lost zero boundary, a clamp, a round and the segment deleted. They now
pin the expressions, the interpolation, the class wiring and the dark
rule's colour value. All seven mutations tried are killed.

A non-numeric current_qty rendered "NaN in stock" and was not badged —
NaN <= 0 is false. It now falls back to 0 and badges.

Bumped the service-worker cache: pos-app.js and pos.html are both
served cache-first out of the shell, so without a new CACHE name the
tills keep the old ones.
2026-11-06 04:53:37 +02:00

158 lines
7.8 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.
// v37: pos-app.js accepts a comma as the decimal point (custom item price, cash given).
// v38: the catalogue tile shows the stock quantity. current_qty was already
// coming down the wire — the till just never drew it. The number is shown
// as it is (negatives included) and the tile is tinted when the quantity is
// not positive. Nothing is blocked: the item still sells.
// 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.
// v39: the quantity moved out of the .sub line and under the price —
// the sub line truncates on a long SKU, which pushed the number and the
// in-cart count off the right edge. The sub line is back to what it was.
// 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.
const CACHE = "pos-v39";
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;
})
);
});