The account-sale picker filtered on three customer columns — name,
number, email. `company` on a customer row is a RELATION id, not a
name, so it could never be text-matched; the company name was only ever
resolved afterwards by `expand` for display. Typing "Sibanye" returned
nothing even though 52 people work there.
- the filter now traverses the relation: `company.company ~ q`.
Verified live: "Sibanye - Rustenburg" 0 → 52, "Adal" 0 → 2,
"Alano" 0 → 1, while "Botha" and "Smit" are unchanged at 6 and 4.
- each row carries `viaCompany`, and name matches sort above
company-only matches. PB's own sort is by name across the whole
result set, which would interleave the two bands.
- /customers returns { items, total, shown } and the till renders a
count header: "52 matches", or "Showing 100 of 122 — keep typing to
narrow". The old `slice(0, 8)` showed 8 of 52 people with nothing
to suggest 44 were missing; 15 companies have more staff than that.
The picker click handler bound to every `div` in the results box, so
adding a count header would have made it selectable and picked a
customer with an empty id. Now scoped to `.cust-row`.
Two bugs the tests caught rather than the code:
- the display cap was first set to 50 while its own comment claimed
it sat above the largest company — which has 52 staff. That
silently re-created the truncation this change removes. Now 100,
with a test asserting a 52-person company comes back whole.
- PocketBase has no escape for a literal backslash: `\'` is
accepted, but one, two or three backslashes all fail to parse and
500 the request, so typing `\` in the search box errored. No
escaping survives, so backslashes are stripped before the filter
(`M20\Nut` searches `M20Nut`) and a backslash-only query returns
nothing rather than matching every customer.
13 new tests cover the company clause, the 52-staff case, the cap,
ranking order, backslash stripping and the empty-after-strip case.
Verified live against PocketBase across 10 queries — the ranking
guarantee held on every one.
sw.js v33 -> v34: pos-app.js and pos.html are cache-first in the shell.
129 lines
5.8 KiB
JavaScript
129 lines
5.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.
|
|
// 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.
|
|
const CACHE = "pos-v34";
|
|
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;
|
|
})
|
|
);
|
|
});
|