WFDASH-WEBAPP/public/pos/sw.js
Dave 076e80ecb4 feat(pos): add FA icons to catalog, matched by category
Each stock item now renders a Font Awesome icon in the catalog grid
in place of the generic '?' initials. The icon comes from the item's
category, set on the categories collection's new 'icon' field (e.g.
'fa-solid fa-bolt' for Fasteners). All 10 active categories seeded
with sensible defaults; can be edited per-row in PB admin.

Match logic lives in pos-service.listActiveStockCatalog: it pulls the
categories matcher table, then assigns each item by:
  1. category.match_brands (against item text or supplier's supply_tags)
  2. category.match_supplier_tags
  3. category.match_keywords (longest first so 'hydraulic hose' beats 'hose')
  4. the 'Unknown / Other' catch-all is excluded from matching — anything
     with no match simply gets the initials fallback

The matcher table is in PB (the existing match_keywords/match_supplier_tags
/match_brands fields on categories), so admins can tune coverage without
a deploy. Two keyword additions to Fasteners (allen cap, set screw) and
Cutting Tools (tap, drill, chuck) lifted the live match rate from 67% to
71% (298/418 items across 8 categories).

FA 6.5.2 Free, solid only, bundled under public/pos/fa/ (256KB total:
100KB CSS + 156KB solid woff2). Both the CSS and the woff2 are in the
service worker APP_SHELL so icons render offline. No CDN dependency.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-31 16:40:02 +02:00

80 lines
2.6 KiB
JavaScript

// Service worker — cache app shell, network-first for HTML + theme.js, cache-first for other static.
const CACHE = "pos-v14";
const APP_SHELL = [
'./',
'./index.html',
'./pos.html',
'./receipt.html',
'./history.html',
'./cashup.html',
'./pos-config.js',
'./pos-app.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;
})
);
});