// Service Worker สำหรับระบบจัดการนัดหมาย // Version 1.0.0 const CACHE_NAME = 'appointment-chat-v1.0.0'; const STATIC_CACHE = 'appointment-static-v1.0.0'; const DYNAMIC_CACHE = 'appointment-dynamic-v1.0.0'; // ไฟล์ที่ต้องการ cache ไว้สำหรับการทำงานแบบออฟไลน์ const STATIC_FILES = [ '/', '/index.html', '/manifest.json', // Font Awesome 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css', // Google Fonts 'https://fonts.googleapis.com/css2?family=Prompt:wght@400;500;600;700&display=swap', 'https://fonts.gstatic.com/s/prompt/v10/jizfREFUsnUct9P6cDfd4OmnLD0Z4zM.woff2', 'https://fonts.gstatic.com/s/prompt/v10/jizfREFUsnUct9P6cDfd4OmnLD0Z5DM.woff2', // เพิ่มไฟล์อื่นๆ ตามต้องการ ]; // URLs ที่ไม่ต้องการ cache const EXCLUDED_URLS = [ '/api.php', 'chrome-extension://', 'moz-extension://', ]; // ✅ Install Event - Cache static files self.addEventListener('install', (event) => { console.log('🔧 Service Worker: Installing...'); event.waitUntil( caches.open(STATIC_CACHE) .then((cache) => { console.log('📦 Service Worker: Caching static files'); return cache.addAll(STATIC_FILES); }) .catch((error) => { console.error('❌ Service Worker: Cache installation failed', error); }) ); // บังคับให้ service worker ใหม่เข้ามาแทนที่ทันที self.skipWaiting(); }); // ✅ Activate Event - Clean up old caches self.addEventListener('activate', (event) => { console.log('🚀 Service Worker: Activating...'); event.waitUntil( caches.keys() .then((cacheNames) => { return Promise.all( cacheNames.map((cacheName) => { // ลบ cache เก่าที่ไม่ใช้แล้ว if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE) { console.log('🗑️ Service Worker: Deleting old cache:', cacheName); return caches.delete(cacheName); } }) ); }) .then(() => { console.log('✅ Service Worker: Cleanup complete'); // เข้าควบคุม client ทั้งหมดทันที return self.clients.claim(); }) ); }); // ✅ Fetch Event - Network First with Cache Fallback self.addEventListener('fetch', (event) => { const {request} = event; const url = request.url; // ข้าม URLs ที่ไม่ต้องการ cache if (EXCLUDED_URLS.some(excludedUrl => url.includes(excludedUrl))) { return; } // ข้าม non-GET requests if (request.method !== 'GET') { return; } event.respondWith( networkFirstWithCacheFallback(request) ); }); // 🌐 Network First Strategy with Cache Fallback async function networkFirstWithCacheFallback(request) { const url = request.url; try { // พยายามเรียกจาก network ก่อน const networkResponse = await fetch(request); if (networkResponse.ok) { // หาก response ดี ให้ cache ไว้ใน dynamic cache const cache = await caches.open(DYNAMIC_CACHE); // Cache เฉพาะ static files และ font files if (isStaticResource(url) || isFontResource(url)) { cache.put(request, networkResponse.clone()); } return networkResponse; } // หาก network response ไม่ดี ให้ fallback ไป cache return getCachedResponse(request); } catch (error) { // หาก network fail ให้ fallback ไป cache console.log('🔌 Service Worker: Network failed, trying cache for:', url); return getCachedResponse(request); } } // 📦 Get response from cache async function getCachedResponse(request) { const url = request.url; // ลองหาใน static cache ก่อน let cachedResponse = await caches.match(request, {cacheName: STATIC_CACHE}); if (cachedResponse) { console.log('📦 Service Worker: Serving from static cache:', url); return cachedResponse; } // ถ้าไม่มีใน static cache ให้ลองหาใน dynamic cache cachedResponse = await caches.match(request, {cacheName: DYNAMIC_CACHE}); if (cachedResponse) { console.log('📦 Service Worker: Serving from dynamic cache:', url); return cachedResponse; } // หาก request เป็น navigation (เช่น หน้าเว็บ) ให้ return index.html if (request.mode === 'navigate') { console.log('🏠 Service Worker: Serving index.html for navigation'); return caches.match('/index.html'); } // ถ้าไม่มีใน cache เลย ให้ return offline page หรือ error response console.log('❌ Service Worker: No cache found for:', url); return new Response('Offline - Content not available', { status: 503, statusText: 'Service Unavailable', headers: {'Content-Type': 'text/plain'} }); } // 🔍 Helper Functions function isStaticResource(url) { return url.includes('.css') || url.includes('.js') || url.includes('.png') || url.includes('.jpg') || url.includes('.jpeg') || url.includes('.gif') || url.includes('.svg') || url.includes('.ico') || url.includes('.webp'); } function isFontResource(url) { return url.includes('.woff') || url.includes('.woff2') || url.includes('.ttf') || url.includes('.eot') || url.includes('fonts.googleapis.com') || url.includes('fonts.gstatic.com'); } // 🔄 Background Sync for offline data self.addEventListener('sync', (event) => { console.log('🔄 Service Worker: Background sync triggered'); if (event.tag === 'appointment-sync') { event.waitUntil(syncAppointments()); } }); // 📤 Sync appointments when back online async function syncAppointments() { console.log('📤 Service Worker: Syncing appointments...'); try { // ส่งข้อความไปยัง client ให้ sync ข้อมูล const clients = await self.clients.matchAll(); clients.forEach(client => { client.postMessage({ type: 'SYNC_APPOINTMENTS' }); }); console.log('✅ Service Worker: Sync message sent to clients'); } catch (error) { console.error('❌ Service Worker: Sync failed', error); } } // 🔔 Push Notification Handler self.addEventListener('push', (event) => { console.log('🔔 Service Worker: Push notification received'); const options = { body: 'คุณมีนัดหมายใกล้เข้ามาแล้ว', icon: '/icon-192.png', badge: '/icon-192.png', vibrate: [100, 50, 100], data: { dateOfArrival: Date.now(), primaryKey: 1 }, actions: [ { action: 'view', title: 'ดูนัดหมาย', icon: '/icon-192.png' }, { action: 'close', title: 'ปิด' } ] }; event.waitUntil( self.registration.showNotification('เตือนนัดหมาย', options) ); }); // 🔔 Notification Click Handler self.addEventListener('notificationclick', (event) => { console.log('🔔 Service Worker: Notification clicked'); event.notification.close(); if (event.action === 'view') { // เปิดแอปหรือ focus ไปที่แอป event.waitUntil( clients.matchAll().then((clientList) => { for (const client of clientList) { if (client.url === '/' && 'focus' in client) { return client.focus(); } } if (clients.openWindow) { return clients.openWindow('/'); } }) ); } }); // 📨 Message Handler - รับข้อความจาก client self.addEventListener('message', (event) => { console.log('📨 Service Worker: Message received:', event.data); if (event.data && event.data.type === 'SKIP_WAITING') { self.skipWaiting(); } if (event.data && event.data.type === 'GET_VERSION') { event.ports[0].postMessage({version: CACHE_NAME}); } }); // 🧹 Periodic cleanup of old dynamic cache entries setInterval(() => { cleanupDynamicCache(); }, 24 * 60 * 60 * 1000); // ทุก 24 ชั่วโมง async function cleanupDynamicCache() { try { const cache = await caches.open(DYNAMIC_CACHE); const requests = await cache.keys(); // ลบ cache entries ที่เก่าเกิน 7 วัน const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000); for (const request of requests) { const response = await cache.match(request); const dateHeader = response.headers.get('date'); if (dateHeader) { const responseDate = new Date(dateHeader).getTime(); if (responseDate < oneWeekAgo) { await cache.delete(request); console.log('🧹 Service Worker: Cleaned old cache entry:', request.url); } } } } catch (error) { console.error('❌ Service Worker: Cache cleanup failed', error); } } console.log('🎉 Service Worker: Script loaded successfully');