/** * 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 = ` Offline - Kayal Aqua
🐟

You're Offline

It looks like you're not connected to the internet. Some features of Kayal Aqua may not be available.

Go to Dashboard
`; return new Response(offlineHTML, { headers: { 'Content-Type': 'text/html' } }); } /** * Check if request is for a static asset */ function isStaticAsset(pathname) { return pathname.includes('/assets/') || pathname.endsWith('.css') || pathname.endsWith('.js') || pathname.endsWith('.png') || pathname.endsWith('.jpg') || pathname.endsWith('.jpeg') || pathname.endsWith('.gif') || pathname.endsWith('.svg') || pathname.endsWith('.ico'); } /** * Check if request is for a page */ function isPageRequest(pathname) { return pathname.endsWith('.php') || pathname === '/' || pathname.startsWith('/pages/'); } // Handle background sync for offline actions self.addEventListener('sync', (event) => { console.log('Service Worker: Background sync triggered', event.tag); if (event.tag === 'background-sync') { event.waitUntil(handleBackgroundSync()); } }); /** * Handle background sync when connection is restored */ async function handleBackgroundSync() { try { // Get pending actions from IndexedDB or localStorage const pendingActions = await getPendingActions(); for (const action of pendingActions) { try { // Retry the failed action await retryAction(action); // Remove from pending actions if successful await removePendingAction(action.id); } catch (error) { console.log('Service Worker: Failed to retry action', action, error); } } console.log('Service Worker: Background sync completed'); } catch (error) { console.log('Service Worker: Background sync failed', error); } } /** * Get pending actions (placeholder - implement with IndexedDB) */ async function getPendingActions() { // TODO: Implement with IndexedDB for persistent storage return []; } /** * Retry a failed action */ async function retryAction(action) { // TODO: Implement retry logic for different action types console.log('Service Worker: Retrying action', action); } /** * Remove pending action after successful retry */ async function removePendingAction(actionId) { // TODO: Implement removal from IndexedDB console.log('Service Worker: Removed pending action', actionId); } // Handle push notifications (if implemented) self.addEventListener('push', (event) => { console.log('Service Worker: Push notification received'); const options = { body: event.data ? event.data.text() : 'New update available', icon: '/assets/images/icons/icon-192x192.png', badge: '/assets/images/icons/icon-192x192.png', tag: 'kayal-aqua-notification', requireInteraction: false, actions: [ { action: 'view', title: 'View', icon: '/assets/images/icons/icon-192x192.png' }, { action: 'close', title: 'Close' } ] }; event.waitUntil( self.registration.showNotification('Kayal Aqua', options) ); }); // Handle notification clicks self.addEventListener('notificationclick', (event) => { console.log('Service Worker: Notification clicked', event); event.notification.close(); if (event.action === 'view') { event.waitUntil( clients.openWindow('/') ); } }); console.log('Service Worker: Registered successfully');