/** * Thai Traditional Massage Queue Management System (TTMQMS Enterprise) * Core PWA & Alpine.js Application State Management */ document.addEventListener('alpine:init', () => { Alpine.data('globalApp', () => ({ theme: localStorage.getItem('ttmqms_theme') || 'dark', user: JSON.parse(localStorage.getItem('ttmqms_user') || 'null'), token: localStorage.getItem('ttmqms_token') || null, isOnline: navigator.onLine, sidebarOpen: false, notifications: [], unreadCount: 0, usbDevice: null, init() { // 1. Apply initial theme this.applyTheme(this.theme); // 2. Listen to network connectivity window.addEventListener('online', () => { this.isOnline = true; this.toast('เชื่อมต่ออินเทอร์เน็ตแล้ว (Online Mode)', 'success'); }); window.addEventListener('offline', () => { this.isOnline = false; this.toast('ขาดการเชื่อมต่ออินเทอร์เน็ต (Offline Mode)', 'warning'); }); // 3. Register PWA Service Worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js').then((reg) => { console.log('[PWA] Service Worker registered:', reg.scope); }).catch((err) => { console.warn('[PWA] SW registration failed:', err); }); } // 4. Check auth token expiration or mock default admin for demo if (!this.token && window.location.pathname !== '/login') { // ในโหมด Demo ถ้าไม่มี token อนุญาตให้จำลอง Admin ทันที this.mockDemoLogin(); } }, toggleTheme() { this.theme = this.theme === 'dark' ? 'light' : 'dark'; this.applyTheme(this.theme); localStorage.setItem('ttmqms_theme', this.theme); }, applyTheme(themeName) { document.body.classList.remove('theme-light', 'theme-dark'); document.body.classList.add(`theme-${themeName}`); }, toast(title, icon = 'info') { if (typeof Swal !== 'undefined') { Swal.fire({ toast: true, position: 'top-end', icon: icon, title: title, showConfirmButton: false, timer: 3000, timerProgressBar: true, background: this.theme === 'dark' ? '#1e293b' : '#ffffff', color: this.theme === 'dark' ? '#f8fafc' : '#0f172a' }); } else { alert(`${title}`); } }, async apiFetch(endpoint, options = {}) { options.headers = options.headers || {}; options.headers['Accept'] = 'application/json'; if (options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) { options.headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(options.body); } if (this.token) { options.headers['Authorization'] = `Bearer ${this.token}`; } try { const baseUrl = window.BASE_URL || ''; const parts = endpoint.split('?'); const path = parts[0]; const query = parts.length > 1 ? '&' + parts[1] : ''; const cleanPath = path.startsWith('/api/v1') ? path : '/api/v1' + (path.startsWith('/') ? path : '/' + path); const apiUrl = `${baseUrl}/index.php?api=${cleanPath}${query}`; const response = await fetch(apiUrl, options); const text = await response.text(); if (!text || !text.trim()) { throw new Error('เซิร์ฟเวอร์ไม่ได้ตอบกลับข้อมูลใดๆ (Empty Response)'); } let data; try { data = JSON.parse(text); } catch (parseErr) { console.error('[API Parse Error] Raw response:', text); const cleanMsg = text.replace(/<[^>]*>?/gm, '').trim().substring(0, 150); throw new Error(cleanMsg ? `ข้อผิดพลาดจากเซิร์ฟเวอร์: ${cleanMsg}` : 'รูปแบบข้อมูลที่เซิร์ฟเวอร์ตอบกลับไม่ถูกต้อง'); } if (response.status === 401 && !data.require_2fa) { this.logout('เซสชันหมดอายุ กรุณาเข้าสู่ระบบอีกครั้ง'); return null; } if (!response.ok) { throw new Error(data.error || 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์'); } return data; } catch (err) { console.error('[API Error]', err); if (!options.silent) { this.toast(err.message || 'ไม่สามารถติดต่อเซิร์ฟเวอร์ได้', 'error'); } throw err; } }, mockDemoLogin() { this.user = { id: 1, national_id: '1100000000001', full_name: 'นพ.สมชาย เชี่ยวชาญ (Admin Demo)', role: 'Admin', branch_id: 1 }; this.token = 'demo_jwt_access_token_mock'; localStorage.setItem('ttmqms_user', JSON.stringify(this.user)); localStorage.setItem('ttmqms_token', this.token); }, logout(reason = null) { this.token = null; this.user = null; localStorage.removeItem('ttmqms_token'); localStorage.removeItem('ttmqms_user'); if (reason) this.toast(reason, 'warning'); setTimeout(() => { window.location.href = '/login'; }, 1000); }, // WebUSB / Smart Card Reader & Thermal Printer Agent async connectUsbPrinter() { if (!('usb' in navigator)) { this.toast('เบราว์เซอร์ของท่านไม่รองรับ WebUSB API กรุณาใช้ Google Chrome หรือ Edge', 'error'); return; } try { // ขอสิทธิ์เชื่อมต่อเครื่องพิมพ์ ESC/POS (Vendor ID ทั่วไปสำหรับ Epson / Xprinter / Citizen) const device = await navigator.usb.requestDevice({ filters: [{ classCode: 7 }] }); // Class 7 = Printer await device.open(); if (device.configuration === null) await device.selectConfiguration(1); await device.claimInterface(0); this.usbDevice = device; this.toast(`เชื่อมต่อเครื่องพิมพ์ความร้อน ${device.productName || 'USB Thermal Printer'} สำเร็จ`, 'success'); } catch (err) { console.log('[WebUSB] Connection cancelled or failed:', err); this.toast('ยังไม่ได้เชื่อมต่อ USB Printer (ใช้งานโหมดจำลองการพิมพ์ผ่านหน้าจอ)', 'info'); } }, // Web Audio API Synthesizer - 4-Tone Chime for TV Queue Calling playCallSound() { try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const tones = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 tones.forEach((freq, idx) => { const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.type = 'sine'; osc.frequency.value = freq; gain.gain.setValueAtTime(0, ctx.currentTime + (idx * 0.2)); gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + (idx * 0.2) + 0.05); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + (idx * 0.2) + 0.35); osc.connect(gain); gain.connect(ctx.destination); osc.start(ctx.currentTime + (idx * 0.2)); osc.stop(ctx.currentTime + (idx * 0.2) + 0.4); }); } catch (e) { console.warn('[Web Audio] Sound play blocked by browser autoplay policy', e); } } })); });