/** * Service Worker for Kayal Aqua PWA * Handles offline functionality and caching */ const CACHE_NAME = 'kayal-aqua-v1.0.0'; const API_CACHE_NAME = 'kayal-aqua-api-v1.0.0'; // Files to cache for offline functionality const STATIC_ASSETS = [ '/', '/index.php', '/login.php', '/assets/css/style.css', '/assets/css/mobile.css', '/assets/css/components.css', '/assets/js/main.js', '/assets/js/mobile.js', '/assets/js/charts.js', '/pages/dashboard.php', '/pages/sales.php', '/pages/expenses.php', '/pages/investments.php', '/pages/base.php', // Add manifest and icons '/manifest.json', '/assets/images/icons/icon-192x192.png', '/assets/images/icons/icon-512x512.png' ]; // API endpoints to cache const API_ENDPOINTS = [ '/api/dashboard.php' ]; // Install event - cache static assets self.addEventListener('install', (event) => { console.log('Service Worker: Installing...'); event.waitUntil( Promise.all([ // Cache static assets caches.open(CACHE_NAME).then((cache) => { console.log('Service Worker: Caching static assets'); return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { credentials: 'same-origin' }))); }), // Cache API endpoints caches.open(API_CACHE_NAME).then((cache) => { console.log('Service Worker: Caching API endpoints'); return Promise.all( API_ENDPOINTS.map(url => { return fetch(new Request(url, { credentials: 'same-origin' })) .then(response => { if (response.ok) { return cache.put(url, response.clone()); } }) .catch(error => { console.log('Service Worker: Failed to cache API endpoint', url, error); }); }) ); }) ]).then(() => { console.log('Service Worker: Installation complete'); // Take control immediately self.skipWaiting(); }) ); }); // Activate event - clean up old caches self.addEventListener('activate', (event) => { console.log('Service Worker: Activating...'); event.waitUntil( Promise.all([ // Clean up old caches caches.keys().then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) { console.log('Service Worker: Deleting old cache', cacheName); return caches.delete(cacheName); } }) ); }), // Take control of all clients self.clients.claim() ]).then(() => { console.log('Service Worker: Activation complete'); }) ); }); // Fetch event - serve from cache when offline self.addEventListener('fetch', (event) => { const request = event.request; const url = new URL(request.url); // Only handle GET requests if (request.method !== 'GET') { return; } // Handle different types of requests if (url.pathname.startsWith('/api/')) { // API requests - network first, then cache event.respondWith(handleAPIRequest(request)); } else if (isStaticAsset(url.pathname)) { // Static assets - cache first, then network event.respondWith(handleStaticAsset(request)); } else if (isPageRequest(url.pathname)) { // Page requests - network first, then cache, then offline page event.respondWith(handlePageRequest(request)); } }); /** * Handle API requests with network-first strategy */ async function handleAPIRequest(request) { try { // Try network first const networkResponse = await fetch(request); if (networkResponse.ok) { // Update cache with fresh data const cache = await caches.open(API_CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: API network request failed, trying cache', error); // Try cache if network fails const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline response for API requests return new Response( JSON.stringify({ error: 'Offline', message: 'This feature requires an internet connection' }), { status: 503, headers: { 'Content-Type': 'application/json' } } ); } } /** * Handle static assets with cache-first strategy */ async function handleStaticAsset(request) { // Try cache first const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Try network if not in cache try { const networkResponse = await fetch(request); if (networkResponse.ok) { // Add to cache for future use const cache = await caches.open(CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: Static asset request failed', error); // Return a fallback for failed static assets if (request.url.includes('.css')) { return new Response('/* Offline */', { headers: { 'Content-Type': 'text/css' } }); } if (request.url.includes('.js')) { return new Response('// Offline', { headers: { 'Content-Type': 'application/javascript' } }); } // For other assets, throw the error throw error; } } /** * Handle page requests with network-first strategy */ async function handlePageRequest(request) { try { // Try network first const networkResponse = await fetch(request); if (networkResponse.ok) { // Update cache with fresh content const cache = await caches.open(CACHE_NAME); cache.put(request.url, networkResponse.clone()); return networkResponse; } throw new Error('Network response not ok'); } catch (error) { console.log('Service Worker: Page network request failed, trying cache', error); // Try cache if network fails const cachedResponse = await caches.match(request); if (cachedResponse) { return cachedResponse; } // Return offline page if nothing else works return getOfflinePage(); } } /** * Generate offline page */ function getOfflinePage() { const offlineHTML = `