/** * Sales Bot - Service Worker for PWA Offline Support */ const CACHE_NAME = 'salesbot-v1'; const RUNTIME_CACHE = 'salesbot-runtime-v1'; // Assets to cache on install const PRECACHE_ASSETS = [ '/dashboard', '/dashboard/analytics', '/dashboard/executive', '/static/css/main.css', '/static/js/app.js', '/static/js/pwa.js', '/static/js/camera.js', '/static/js/charts.js', '/static/manifest.json' ]; // API endpoints to cache with network-first strategy const API_ROUTES = [ '/api/dashboard/resumen', '/api/dashboard/ranking', '/api/dashboard/ventas-recientes', '/api/analytics/trends', '/api/analytics/predictions' ]; // Install event - cache core assets self.addEventListener('install', (event) => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('[SW] Precaching assets'); return cache.addAll(PRECACHE_ASSETS); }) .then(() => self.skipWaiting()) .catch(err => console.error('[SW] Precache failed:', err)) ); }); // Activate event - clean old caches self.addEventListener('activate', (event) => { event.waitUntil( caches.keys() .then(cacheNames => { return Promise.all( cacheNames .filter(name => name !== CACHE_NAME && name !== RUNTIME_CACHE) .map(name => { console.log('[SW] Deleting old cache:', name); return caches.delete(name); }) ); }) .then(() => self.clients.claim()) ); }); // Fetch event - handle requests self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); // Skip non-GET requests if (request.method !== 'GET') { return; } // Skip external requests if (url.origin !== location.origin) { return; } // API requests - Network first, fall back to cache if (url.pathname.startsWith('/api/')) { event.respondWith(networkFirst(request)); return; } // Static assets - Cache first, fall back to network if (url.pathname.startsWith('/static/')) { event.respondWith(cacheFirst(request)); return; } // HTML pages - Network first with cache fallback if (request.headers.get('Accept')?.includes('text/html')) { event.respondWith(networkFirst(request)); return; } // Default - Network first event.respondWith(networkFirst(request)); }); // Cache first strategy async function cacheFirst(request) { const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } try { const networkResponse = await fetch(request); if (networkResponse.ok) { const cache = await caches.open(CACHE_NAME); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { console.error('[SW] Cache first failed:', error); return new Response('Offline', { status: 503 }); } } // Network first strategy async function networkFirst(request) { try { const networkResponse = await fetch(request); // Cache successful responses if (networkResponse.ok) { const cache = await caches.open(RUNTIME_CACHE); cache.put(request, networkResponse.clone()); } return networkResponse; } catch (error) { // Network failed, try cache const cachedResponse = await caches.match(request); if (cachedResponse) { console.log('[SW] Serving from cache:', request.url); return cachedResponse; } // No cache available if (request.headers.get('Accept')?.includes('text/html')) { return caches.match('/dashboard'); // Fallback to main page } if (request.headers.get('Accept')?.includes('application/json')) { return new Response(JSON.stringify({ error: 'offline', message: 'Sin conexion. Mostrando datos en cache.' }), { status: 503, headers: { 'Content-Type': 'application/json' } }); } return new Response('Offline', { status: 503 }); } } // Handle messages from clients self.addEventListener('message', (event) => { if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } }); // Background sync for offline actions (if supported) self.addEventListener('sync', (event) => { if (event.tag === 'sync-sales') { event.waitUntil(syncSales()); } }); async function syncSales() { // Sync any pending sales when back online console.log('[SW] Syncing pending sales...'); // Implementation would go here } // Push notifications (if implemented) self.addEventListener('push', (event) => { if (!event.data) return; const data = event.data.json(); const options = { body: data.body || 'Nueva notificacion de Sales Bot', icon: '/static/icons/icon-192x192.png', badge: '/static/icons/icon-72x72.png', vibrate: [100, 50, 100], data: { url: data.url || '/dashboard' } }; event.waitUntil( self.registration.showNotification(data.title || 'Sales Bot', options) ); }); // Handle notification click self.addEventListener('notificationclick', (event) => { event.notification.close(); event.waitUntil( clients.matchAll({ type: 'window' }).then(clientList => { // Focus existing window if available for (const client of clientList) { if (client.url.includes('/dashboard') && 'focus' in client) { return client.focus(); } } // Open new window if (clients.openWindow) { return clients.openWindow(event.notification.data?.url || '/dashboard'); } }) ); });