Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,170 @@
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Sarabun:wght@300;400;500;600;700&display=swap');
:root {
--color-primary: #10b981; /* Emerald 500 */
--color-primary-dark: #059669; /* Emerald 600 */
--color-secondary: #06b6d4; /* Cyan 500 */
--color-accent: #f59e0b; /* Amber 500 */
--color-danger: #ef4444; /* Red 500 */
--color-success: #10b981; /* Emerald 500 */
--bg-gradient-light: linear-gradient(135deg, #ecfdf5 0%, #f0fdf4 50%, #e0f2fe 100%);
--bg-gradient-dark: linear-gradient(135deg, #0f172a 0%, #064e3b 50%, #0c4a6e 100%);
--glass-bg-light: rgba(255, 255, 255, 0.75);
--glass-border-light: rgba(255, 255, 255, 0.6);
--glass-shadow-light: 0 10px 30px -10px rgba(16, 185, 129, 0.15);
--glass-bg-dark: rgba(15, 23, 42, 0.75);
--glass-border-dark: rgba(255, 255, 255, 0.12);
--glass-shadow-dark: 0 10px 30px -10px rgba(0, 0, 0, 0.5);
--font-main: 'Sarabun', 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif;
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
[x-cloak] {
display: none !important;
}
body {
font-family: var(--font-main);
margin: 0;
padding: 0;
min-height: 100vh;
transition: background 0.5s ease, color 0.5s ease;
overflow-x: hidden;
}
/* Light Theme */
body.theme-light {
background: var(--bg-gradient-light);
color: #1e293b;
}
body.theme-light .glass-card,
body.theme-light .glass-nav,
body.theme-light .glass-modal {
background: var(--glass-bg-light);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid var(--glass-border-light);
box-shadow: var(--glass-shadow-light);
}
body.theme-light .text-subtle {
color: #64748b;
}
/* Dark Theme */
body.theme-dark {
background: var(--bg-gradient-dark);
color: #f8fafc;
}
body.theme-dark .glass-card,
body.theme-dark .glass-nav,
body.theme-dark .glass-modal {
background: var(--glass-bg-dark);
backdrop-filter: blur(16px) saturate(180%);
-webkit-backdrop-filter: blur(16px) saturate(180%);
border: 1px solid var(--glass-border-dark);
box-shadow: var(--glass-shadow-dark);
}
body.theme-dark .text-subtle {
color: #94a3b8;
}
/* Glassmorphism Interactive Elements */
.glass-btn-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
color: white;
border: none;
font-weight: 600;
padding: 0.625rem 1.25rem;
border-radius: 0.75rem;
box-shadow: 0 4px 14px 0 rgba(16, 185, 129, 0.39);
transition: var(--transition-smooth);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.glass-btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.55);
}
.glass-btn-primary:active {
transform: translateY(0);
}
.glass-input {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
transition: var(--transition-smooth);
font-size: 0.95rem;
}
body.theme-light .glass-input {
background: rgba(255, 255, 255, 0.8);
border: 1px solid #cbd5e1;
color: #0f172a;
}
body.theme-dark .glass-input {
background: rgba(30, 41, 59, 0.7);
border: 1px solid #334155;
color: #f8fafc;
}
.glass-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.25);
}
/* Animations & Micro-Interactions */
@keyframes pulse-glow {
0%, 100% { box-shadow: 0 0 15px rgba(16, 185, 129, 0.4); }
50% { box-shadow: 0 0 25px rgba(6, 182, 212, 0.7); }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
.animate-slide-up {
animation: slide-up 0.4s ease-out forwards;
}
.animate-float {
animation: float 4s ease-in-out infinite;
}
.animate-pulse-glow {
animation: pulse-glow 3s infinite;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.4);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--color-primary);
}
/* Accessibility WCAG 2.1 Focus Rings */
a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible {
outline: 2px solid var(--color-primary) !important;
outline-offset: 2px !important;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,192 @@
/**
* 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);
}
}
}));
});
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long