// 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('

Offline

', {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; }) ); });