193 lines
7.8 KiB
JavaScript
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);
|
|
}
|
|
}
|
|
}));
|
|
});
|