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.
158 lines
7.8 KiB
JavaScript
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;
|
|
})
|
|
);
|
|
});
|