Update service worker cache name to nexus-pos-v5 to force cache invalidation. Add brand-catalog.js to APP_SHELL precache list. This should resolve stale cached JS causing parse errors.
351 lines
12 KiB
JavaScript
351 lines
12 KiB
JavaScript
// /home/Autopartes/pos/static/pwa/sw.js
|
|
// Nexus POS — Service Worker v5
|
|
// Self-contained vanilla JS. No external imports.
|
|
|
|
const CACHE_NAME = 'nexus-pos-v5';
|
|
|
|
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/js/brand-catalog.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
|
|
if (url.pathname.indexOf('/pos/api/auth/') !== -1) {
|
|
return;
|
|
}
|
|
|
|
// Don't cache login page
|
|
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 () {
|
|
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;
|
|
}
|
|
|
|
// WhatsApp endpoints need fresh data
|
|
if (url.pathname.indexOf('/pos/api/whatsapp/') !== -1) {
|
|
return;
|
|
}
|
|
|
|
// API calls -> network-first
|
|
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) {
|
|
fetch(request).then(function (response) {
|
|
if (response && response.status === 200) {
|
|
caches.open(CACHE_NAME).then(function (cache) {
|
|
cache.put(request, response);
|
|
});
|
|
}
|
|
}).catch(function () {});
|
|
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 ─────────────────────────────────────────────
|
|
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)...');
|
|
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 {
|
|
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) {
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (self.clients.openWindow) {
|
|
return self.clients.openWindow(targetUrl);
|
|
}
|
|
})
|
|
);
|
|
});
|
|
|
|
// ─── 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' });
|
|
});
|
|
});
|
|
})
|
|
);
|
|
}
|
|
});
|