Files
Autoparts-DB/pos/static/pwa/sw.js
consultoria-as ea29cc31c0 feat(catalog): supplier catalog cleanup, fuzzy matching, and navigation fixes
- Cleaned 137+ fake engine-displacement models from supplier imports
  (v3/v4 scripts: Chevrolet, Ford, Chrysler, Dodge, Jeep, Nissan, etc.)
- Removed 1,251+ corrupted models (INT. prefixes, year-suffix, torque specs,
  empty names, trailing-year variants)
- Migrated supplier tables to master DB (supplier_catalog,
  supplier_catalog_compat, supplier_catalog_interchange)
- Fixed _get_mye_ids_with_parts() to query supplier_catalog_compat from
  master DB so supplier-only vehicles appear for all tenants
- Added fuzzy model matcher with parenthesis stripping, noise suffix removal,
  compact matching, prefix/substring fallback, model aliases, and ±3 year
  proximity
- Matched compat rows: KEEP GREEN +14,152, KNADIAN +3,021, VAZLO +127,500,
  LUK +477, RAYBESTOS +1,743
- Added KNADIAN catalog importer with year-range expansion and future-year
  filtering
- Added VAZLO catalog importer with position parsing and SKU-in-model cleanup
- Added Keep Green, LUK, Yokomitsu, Raybestos catalog importers
- Cache clearing after cleanups (_classify_cache_*, nexus:mye_ids:*,
  nexus:brand_mye_counts:*)

Final match rates:
- KEEP GREEN: 90.3%
- VAZLO: 93.6%
- YOKOMITSU: 100.0%
- KNADIAN: 57.4%
- LUK: 51.0%
- RAYBESTOS: 55.9%
2026-06-09 07:47:42 +00:00

355 lines
12 KiB
JavaScript

// /home/Autopartes/pos/static/pwa/sw.js
// Nexus POS — Service Worker v17
// Self-contained vanilla JS. No external imports.
//
// Bump CACHE_NAME whenever static assets change significantly.
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
// so templates can use cache-busting query params freely.
const CACHE_NAME = 'nexus-pos-v17';
const APP_SHELL = [
'/pos/static/css/tokens.css',
'/pos/static/css/common.css',
'/pos/static/css/pos-ui.css',
'/pos/static/js/app-init.js',
'/pos/static/js/sidebar.js',
'/pos/static/js/offline-banner.js',
'/pos/static/js/sync-engine.js',
'/pos/static/js/i18n.js',
'/pos/static/js/kiosk.js',
'/pos/static/js/splash-loader.js',
'/pos/static/js/pos-utils.js',
'/pos/static/js/pwa-install.js',
'/pos/static/js/chat.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();
}).then(function () {
return self.clients.matchAll({ type: 'window' }).then(function (clients) {
clients.forEach(function (client) {
client.postMessage({ type: 'SW_UPDATED', cacheName: CACHE_NAME });
});
});
})
);
});
// ─── 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;
}
// Normalize cache key for static assets (strip query strings) so
// catalog.js?v=5 and catalog.js?v=6 share the same cache entry.
var cacheKey = req;
if (/\.(js|css|png|jpg|jpeg|webp|svg|gif|ico|woff|woff2|ttf|eot|json)$/.test(url.pathname)) {
cacheKey = new Request(url.pathname);
}
// Never cache auth endpoints
if (url.pathname.indexOf('/pos/api/auth/') !== -1) {
return;
}
// HTML pages -> network-first (always fresh)
if (req.headers.get('accept') && req.headers.get('accept').indexOf('text/html') !== -1) {
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 (JS, CSS, images) -> cache-first
event.respondWith(cacheFirst(req, cacheKey));
});
function cacheFirst(request, cacheKey) {
cacheKey = cacheKey || request;
return caches.match(cacheKey).then(function (cached) {
if (cached) {
fetch(request).then(function (response) {
if (response && response.status === 200 && request.method === 'GET') {
caches.open(CACHE_NAME).then(function (cache) {
cache.put(cacheKey, response);
});
}
}).catch(function () {});
return cached;
}
return fetch(request).then(function (response) {
if (response && response.status === 200 && request.method === 'GET') {
var clone = response.clone();
caches.open(CACHE_NAME).then(function (cache) {
cache.put(cacheKey, clone);
});
}
return response;
});
});
}
function networkFirst(request) {
return fetch(request).then(function (response) {
if (response && response.status === 200 && request.method === 'GET') {
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' });
});
});
})
);
}
});