// Service Worker for LittleShop Admin PWA const CACHE_NAME = 'littleshop-admin-v2'; const urlsToCache = [ '/Admin/Dashboard', '/manifest.json', '/lib/bootstrap/css/bootstrap.min.css', '/lib/fontawesome/css/all.min.css', '/css/modern-admin.css', '/css/mobile-admin.css', '/lib/jquery/jquery.min.js', '/lib/bootstrap/js/bootstrap.bundle.min.js', '/js/pwa.js', '/js/notifications.js', '/icons/icon-192x192.png', '/icons/icon-512x512.png' ]; // Install event - cache essential files self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME) .then(cache => { console.log('Service Worker: Caching essential files'); return cache.addAll(urlsToCache); }) .then(() => self.skipWaiting()) ); }); // Activate event - clean up old caches self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames .filter(cacheName => cacheName !== CACHE_NAME) .map(cacheName => caches.delete(cacheName)) ); }).then(() => self.clients.claim()) ); }); // Fetch event - network first, fallback to cache self.addEventListener('fetch', event => { // Skip non-GET requests if (event.request.method !== 'GET') { return; } // Skip API requests from cache if (event.request.url.includes('/api/')) { event.respondWith( fetch(event.request) .catch(() => { // If offline and it's an API request, return offline message return new Response( JSON.stringify({ error: 'Offline - Please check your connection' }), { headers: { 'Content-Type': 'application/json' } } ); }) ); return; } // For everything else, try network first, then cache event.respondWith( fetch(event.request) .then(response => { // Don't cache if not a successful response if (!response || response.status !== 200 || response.type === 'error') { return response; } // Clone the response as it can only be consumed once const responseToCache = response.clone(); caches.open(CACHE_NAME) .then(cache => { cache.put(event.request, responseToCache); }); return response; }) .catch(() => { // If network fails, try cache return caches.match(event.request) .then(response => { if (response) { return response; } // If not in cache, return offline page for navigation requests if (event.request.mode === 'navigate') { return caches.match('/offline.html'); } }); }) ); }); // Push notification event self.addEventListener('push', event => { console.log('Push notification received:', event); let notificationData = { title: 'LittleShop Admin', body: 'You have a new notification', icon: '/icons/icon-192x192.png', badge: '/icons/icon-96x96.png', vibrate: [200, 100, 200], data: { dateOfArrival: Date.now(), primaryKey: 1 } }; if (event.data) { try { const data = event.data.json(); notificationData = { title: data.title || notificationData.title, body: data.body || notificationData.body, icon: data.icon || notificationData.icon, badge: data.badge || notificationData.badge, vibrate: data.vibrate || notificationData.vibrate, data: data.data || notificationData.data, tag: data.tag, requireInteraction: data.requireInteraction || false, actions: data.actions || [] }; } catch (e) { console.error('Error parsing push data:', e); } } event.waitUntil( self.registration.showNotification(notificationData.title, notificationData) ); }); // Notification click event self.addEventListener('notificationclick', event => { console.log('Notification clicked:', event); event.notification.close(); // Handle action clicks if (event.action) { switch (event.action) { case 'view': clients.openWindow(event.notification.data.url || '/Admin/Dashboard'); break; case 'dismiss': // Just close the notification break; default: clients.openWindow('/Admin/Dashboard'); } return; } // Default click - open the app or focus if already open event.waitUntil( clients.matchAll({ type: 'window', includeUncontrolled: true }) .then(clientList => { // Check if there's already a window open for (let client of clientList) { if (client.url.includes('/Admin/') && 'focus' in client) { return client.focus(); } } // If no window is open, open a new one if (clients.openWindow) { return clients.openWindow(event.notification.data?.url || '/Admin/Dashboard'); } }) ); }); // Background sync for offline actions self.addEventListener('sync', event => { console.log('Background sync triggered:', event.tag); if (event.tag === 'sync-orders') { event.waitUntil(syncOrders()); } else if (event.tag === 'sync-products') { event.waitUntil(syncProducts()); } }); // Sync orders when back online async function syncOrders() { try { // Get any pending orders from IndexedDB const db = await openDB(); const tx = db.transaction('pending-orders', 'readonly'); const store = tx.objectStore('pending-orders'); const pendingOrders = await store.getAll(); for (const order of pendingOrders) { await fetch('/api/orders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(order) }); // Remove from pending after successful sync const deleteTx = db.transaction('pending-orders', 'readwrite'); await deleteTx.objectStore('pending-orders').delete(order.id); } // Show notification of successful sync self.registration.showNotification('Orders Synced', { body: `${pendingOrders.length} orders synchronized successfully`, icon: '/icons/icon-192x192.png' }); } catch (error) { console.error('Error syncing orders:', error); } } // Sync products when back online async function syncProducts() { try { const response = await fetch('/api/catalog/products'); if (response.ok) { const products = await response.json(); // Update cached products const cache = await caches.open(CACHE_NAME); await cache.put('/api/catalog/products', new Response(JSON.stringify(products))); console.log('Products synced successfully'); } } catch (error) { console.error('Error syncing products:', error); } } // Helper function to open IndexedDB function openDB() { return new Promise((resolve, reject) => { const request = indexedDB.open('LittleShopDB', 1); request.onerror = () => reject(request.error); request.onsuccess = () => resolve(request.result); request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('pending-orders')) { db.createObjectStore('pending-orders', { keyPath: 'id', autoIncrement: true }); } if (!db.objectStoreNames.contains('pending-products')) { db.createObjectStore('pending-products', { keyPath: 'id', autoIncrement: true }); } }; }); } // Message event for communication with the app self.addEventListener('message', event => { console.log('Service Worker received message:', event.data); if (event.data.action === 'skipWaiting') { self.skipWaiting(); } if (event.data.action === 'clearCache') { event.waitUntil( caches.keys().then(cacheNames => { return Promise.all( cacheNames.map(cacheName => caches.delete(cacheName)) ); }).then(() => { event.ports[0].postMessage({ success: true }); }) ); } }); console.log('Service Worker loaded successfully');