// /home/Autopartes/pos/static/pwa/sw.js // Nexus POS — Service Worker v3 // Self-contained vanilla JS. No external imports. const CACHE_NAME = 'nexus-pos-v4'; const APP_SHELL = [ '/pos/login', '/pos/sale', '/pos/catalog', '/pos/inventory', '/pos/customers', '/pos/invoicing', '/pos/accounting', '/pos/dashboard', '/pos/config', '/pos/reports', '/pos/static/css/tokens.css', '/pos/static/css/common.css', '/pos/static/js/app-init.js', '/pos/static/js/sidebar.js', '/pos/static/js/login.js', '/pos/static/js/pos.js', '/pos/static/js/catalog.js', '/pos/static/js/inventory.js', '/pos/static/js/customers.js', '/pos/static/js/invoicing.js', '/pos/static/js/accounting.js', '/pos/static/js/dashboard.js', '/pos/static/js/config.js', '/pos/static/js/reports.js', '/pos/static/js/offline-banner.js', '/pos/static/js/sync-engine.js', '/pos/static/pwa/manifest.json', '/pos/static/pwa/icon-192.png', '/pos/static/pwa/icon-512.png' ]; // ─── IndexedDB helpers (offline queue) ─────────────────────────── const DB_NAME = 'nexus-offline'; const DB_VERSION = 1; const STORE_NAME = 'pendingRequests'; function openDB() { return new Promise(function (resolve, reject) { var request = indexedDB.open(DB_NAME, DB_VERSION); request.onerror = function () { reject(request.error); }; request.onsuccess = function () { resolve(request.result); }; request.onupgradeneeded = function (event) { var db = event.target.result; if (!db.objectStoreNames.contains(STORE_NAME)) { db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true }); } }; }); } function savePendingRequest(entry) { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var tx = db.transaction(STORE_NAME, 'readwrite'); var store = tx.objectStore(STORE_NAME); var request = store.add(entry); request.onsuccess = function () { resolve(request.result); }; request.onerror = function () { reject(request.error); }; }); }); } function getPendingRequests() { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var tx = db.transaction(STORE_NAME, 'readonly'); var store = tx.objectStore(STORE_NAME); var request = store.getAll(); request.onsuccess = function () { resolve(request.result || []); }; request.onerror = function () { reject(request.error); }; }); }); } function clearPendingRequests() { return openDB().then(function (db) { return new Promise(function (resolve, reject) { var tx = db.transaction(STORE_NAME, 'readwrite'); var store = tx.objectStore(STORE_NAME); var request = store.clear(); request.onsuccess = function () { resolve(); }; request.onerror = function () { reject(request.error); }; }); }); } // ─── Install: pre-cache app shell ──────────────────────────────── self.addEventListener('install', function (event) { event.waitUntil( caches.open(CACHE_NAME).then(function (cache) { return cache.addAll(APP_SHELL); }).then(function () { return self.skipWaiting(); }) ); }); // ─── Activate: purge old caches ────────────────────────────────── self.addEventListener('activate', function (event) { event.waitUntil( caches.keys().then(function (names) { return Promise.all( names.filter(function (n) { return n !== CACHE_NAME; }) .map(function (n) { return caches.delete(n); }) ); }).then(function () { return self.clients.claim(); }) ); }); // ─── Fetch strategy ────────────────────────────────────────────── self.addEventListener('fetch', function (event) { var url = new URL(event.request.url); var req = event.request; // Only handle requests within /pos/ scope if (url.pathname.indexOf('/pos/') === -1) { return; } // Never cache auth endpoints — tokens must always come from the server if (url.pathname.indexOf('/pos/api/auth/') !== -1) { return; } // Don't cache login page — always fetch fresh to avoid stale redirects if (url.pathname === '/pos/login' || url.pathname === '/pos/login/') { event.respondWith(networkFirst(req)); return; } // Offline cart queue: POST /pos/api/cart/* if (req.method === 'POST' && url.pathname.indexOf('/pos/api/cart/') !== -1) { event.respondWith( fetch(req.clone()).then(function (response) { return response; }).catch(function () { // Clone request body to store it for later retry return req.clone().text().then(function (bodyText) { var entry = { url: req.url, method: req.method, headers: Array.from(req.headers.entries()), body: bodyText, timestamp: Date.now() }; return savePendingRequest(entry); }).then(function () { return new Response( JSON.stringify({ queued: true, message: 'Added to offline queue' }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); }).catch(function (err) { console.error('[SW] Failed to queue offline cart request:', err); return new Response( JSON.stringify({ queued: false, message: 'Failed to queue request' }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); }) }) ); return; } // API calls → network-first (except WhatsApp which must be real-time) if (url.pathname.indexOf('/pos/api/whatsapp/') !== -1) { // WhatsApp endpoints need fresh server data; skip SW caching return; } if (url.pathname.indexOf('/pos/api/') !== -1) { event.respondWith(networkFirst(req)); return; } // Everything else → cache-first event.respondWith(cacheFirst(req)); }); function cacheFirst(request) { return caches.match(request).then(function (cached) { if (cached) { // Update cache in background fetch(request).then(function (response) { if (response && response.status === 200) { caches.open(CACHE_NAME).then(function (cache) { cache.put(request, response); }); } }).catch(function () { /* offline, ignore */ }); return cached; } return fetch(request).then(function (response) { if (response && response.status === 200) { var clone = response.clone(); caches.open(CACHE_NAME).then(function (cache) { cache.put(request, clone); }); } return response; }); }); } function networkFirst(request) { return fetch(request).then(function (response) { if (response && response.status === 200) { var clone = response.clone(); caches.open(CACHE_NAME).then(function (cache) { cache.put(request, clone); }); } return response; }).catch(function () { return caches.match(request); }); } // ─── Background Sync ───────────────────────────────────────────── // Existing sync handler + new cart-specific sync self.addEventListener('sync', function (event) { if (event.tag === 'nexus-pos-sync') { event.waitUntil( self.clients.matchAll().then(function (clients) { clients.forEach(function (client) { client.postMessage({ type: 'SYNC_REQUESTED' }); }); }) ); } if (event.tag === 'nexus-cart-sync') { event.waitUntil( getPendingRequests().then(function (entries) { if (!entries || entries.length === 0) { console.log('[SW] No pending cart actions to sync.'); return; } console.log('[SW] Syncing', entries.length, 'pending cart action(s)...'); // Replay each pending request to the server var syncPromises = entries.map(function (entry) { return fetch(entry.url, { method: entry.method, headers: entry.headers.reduce(function (obj, h) { obj[h[0]] = h[1]; return obj; }, {}), body: entry.body }).then(function (response) { console.log('[SW] Cart sync success for', entry.url, '- status', response.status); return { ok: true, id: entry.id }; }).catch(function (err) { console.error('[SW] Cart sync failed for', entry.url, '-', err); return { ok: false, id: entry.id }; }); }); return Promise.all(syncPromises).then(function (results) { var allOk = results.every(function (r) { return r.ok; }); if (allOk) { return clearPendingRequests().then(function () { console.log('[SW] All cart actions synced. Pending queue cleared.'); }); } else { // Remove only successfully synced entries; failed ones will retry next time var failedIds = results.filter(function (r) { return !r.ok; }).map(function (r) { return r.id; }); console.warn('[SW] Some cart actions failed. Keeping', failedIds.length, 'entries for retry.'); } }); }).catch(function (err) { console.error('[SW] Error during nexus-cart-sync:', err); }) ); } }); // ─── Push Notifications ────────────────────────────────────────── self.addEventListener('push', function (event) { var data = {}; if (event.data) { try { data = event.data.json(); } catch (e) { data = { title: event.data.text() }; } } var title = data.title || 'Nexus POS'; var options = { body: data.body || 'Tienes una nueva notificación del POS.', icon: '/pos/static/pwa/icon-192.png', badge: '/pos/static/pwa/icon-192.png', tag: data.tag || 'nexus-pos-general', data: data.data || { url: '/pos/sale' }, requireInteraction: false }; event.waitUntil( self.registration.showNotification(title, options) ); }); // ─── Notification Click ────────────────────────────────────────── self.addEventListener('notificationclick', function (event) { event.notification.close(); var targetUrl = event.notification.data && event.notification.data.url ? event.notification.data.url : '/pos/sale'; event.waitUntil( self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (clientList) { // Focus existing tab if it matches the target scope for (var i = 0; i < clientList.length; i++) { var client = clientList[i]; if (client.url.indexOf('/pos/') !== -1 && 'focus' in client) { return client.focus().then(function (focusedClient) { if ('navigate' in focusedClient) { return focusedClient.navigate(targetUrl); } }); } } // Otherwise open a new window if (self.clients.openWindow) { return self.clients.openWindow(targetUrl); } }) ); }); // ─── Periodic Background Sync (stub for future use) ────────────── // This can be used to warm the cache daily or refresh catalog data // in the background. Requires user permission and browser support. // self.addEventListener('periodicsync', function (event) { // if (event.tag === 'nexus-daily-sync') { // event.waitUntil( // // e.g. cache warming, catalog refresh, etc. // caches.open(CACHE_NAME).then(function (cache) { // return cache.add('/pos/api/catalog/refresh'); // }).catch(function (err) { // console.error('[SW] periodicsync failed:', err); // }) // ); // } // }); // ─── Message handler ───────────────────────────────────────────── self.addEventListener('message', function (event) { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'CLEAR_CACHES') { event.waitUntil( caches.keys().then(function (names) { return Promise.all( names.map(function (n) { return caches.delete(n); }) ); }).then(function () { console.log('[SW] All caches cleared by client request.'); return self.clients.matchAll().then(function (clients) { clients.forEach(function (client) { client.postMessage({ type: 'CACHES_CLEARED' }); }); }); }) ); } });