Files
2026-09-16 23:20:08 +07:00

193 lines
7.8 KiB
JavaScript

/**
* 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);
}
}
}));
});