- Add QWEN (qwen3.6) as primary AI backend with short system prompt - Hermes remains as fallback with 45s timeout - Increase QWEN timeout to 35s, max_tokens to 4000 - Add conversation history loading from whatsapp_messages (last 4 msgs) - Persist detected vehicle in whatsapp_sessions table - Add 'limpiar chat' / 'nuevo chat' / 'reset' commands to clear history - Fix CSS conflict: rename whatsapp chat-panel classes to wa-chat-panel - Fix JS ID conflicts with chat.js widget (waChatPanel, waChatMessages, etc.) - Improve no-stock response: conversational with alternatives - Split search_query by | for multi-part lookups - Add DEMO_PROMPTS.md and DEMO_PROMPTS_V2.md
385 lines
13 KiB
JavaScript
385 lines
13 KiB
JavaScript
// /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' });
|
|
});
|
|
});
|
|
})
|
|
);
|
|
}
|
|
});
|