Files
ERP/public/sw.js
denis_ireri 472573a1a8 Make the terminal work offline end to end, and add stock screens
Terminal (PRD 5.3/5.5):
- Opens with no network: a service worker caches the app shell and build
  assets, and the last confirmed session and product list are kept on the
  phone. Only a definitive 401/403 sends the cashier to the login page.
- The offline queue is keyed by cashier and branch, sends only the
  signed-in cashier's sales, chunks to the server's batch limit, and acts
  on the per-sale result: synced, or rejected with the reason shown and
  never retried. Rows from the old queue are adopted on first sync.
- Several products per sale (tap to add), optional customer details, and
  a printable receipt. The banner shows sales waiting to sync and any
  that need attention; sign-out warns about unsynced sales.
- M-Pesa only appears when FEATURE_MPESA is on. A timed-out prompt now
  tells the cashier not to take cash until the payment is checked,
  instead of reporting it as failed.

Admin:
- New Stock page: stock per branch with low/out flags, physical counts
  and adjustments, inter-branch transfers, morning dispatch and evening
  count entry, and recent movements.
- Dashboard shows stock on hand per branch and links to Stock.
- Staff page can deactivate and reactivate accounts.
- Products page is read-only for branch managers and edits the low-stock
  alert level.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 18:46:23 +03:00

87 lines
2.9 KiB
JavaScript

/* AquaFlow service worker: lets the cashier terminal open and run with no network.
*
* - Page loads (navigations): network first, falling back to the cached copy,
* and to the cached /terminal page for anything else, so a cashier who opens
* the app offline still lands on the till.
* - Next.js build assets (/_next/static/*): cache first. They are content-hashed
* and immutable, so a cached copy is always correct.
* - API calls (/api/*) and every non-GET request: never cached. Sales go through
* the IndexedDB queue in the page, not through this worker.
*
* Bump CACHE_VERSION to drop every cached page and asset on the next visit.
*/
const CACHE_VERSION = "aquaflow-v1";
const APP_SHELL = ["/terminal", "/login"];
async function cacheShell() {
const cache = await caches.open(CACHE_VERSION);
for (const path of APP_SHELL) {
try {
const res = await fetch(path, { credentials: "same-origin" });
if (!res.ok) continue;
await cache.put(path, res.clone());
// Also cache the scripts and styles the page needs to boot.
const html = await res.text();
const assets = Array.from(html.matchAll(/(?:src|href)="(\/_next\/static\/[^"]+)"/g), (m) => m[1]);
await Promise.all(
Array.from(new Set(assets)).map((url) =>
cache.match(url).then((hit) => hit || fetch(url).then((r) => r.ok && cache.put(url, r)))
)
);
} catch (e) {
// Offline during install: the runtime handlers below fill the cache later.
}
}
}
self.addEventListener("install", (event) => {
event.waitUntil(cacheShell().then(() => self.skipWaiting()));
});
self.addEventListener("activate", (event) => {
event.waitUntil(
caches
.keys()
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});
self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
const url = new URL(req.url);
if (url.origin !== self.location.origin) return;
if (url.pathname.startsWith("/api/")) return;
if (req.mode === "navigate") {
event.respondWith(
fetch(req)
.then((res) => {
if (res.ok) {
const copy = res.clone();
caches.open(CACHE_VERSION).then((c) => c.put(url.pathname, copy));
}
return res;
})
.catch(async () => {
const cache = await caches.open(CACHE_VERSION);
return (await cache.match(url.pathname)) || (await cache.match("/terminal")) || Response.error();
})
);
return;
}
if (url.pathname.startsWith("/_next/static/")) {
event.respondWith(
caches.open(CACHE_VERSION).then(async (cache) => {
const hit = await cache.match(req);
if (hit) return hit;
const res = await fetch(req);
if (res.ok) cache.put(req, res.clone());
return res;
})
);
}
});