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>
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/* 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;
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user