463 lines
26 KiB
PHP
463 lines
26 KiB
PHP
<?php
|
|
/**
|
|
* Reception Queue Board & Walk-in Booking View (Glassmorphism)
|
|
*/
|
|
?>
|
|
<div x-data="queueApp" class="space-y-6">
|
|
|
|
<!-- Header & Actions -->
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4 glass-card p-6 rounded-2xl border">
|
|
<div>
|
|
<h1 class="text-2xl font-bold tracking-tight bg-gradient-to-r from-emerald-400 to-cyan-400 bg-clip-text text-transparent">
|
|
จัดการคิวนวด & ระบบจัดสรรอัจฉริยะ (AI Queue Board)
|
|
</h1>
|
|
<p class="text-sm text-subtle mt-1">ออกคิว Walk-in, อ่านบัตรประชาชน Smart Card และควบคุมคิวแบบ Realtime</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-3">
|
|
<!-- Read Smart Card Button -->
|
|
<button @click="readSmartCard()" class="glass-btn-primary bg-gradient-to-r from-cyan-500 to-blue-600 shadow-cyan-500/30 text-sm">
|
|
<i class="bi bi-person-vcard-fill text-lg"></i>
|
|
<span>อ่านบัตรประชาชน (Smart Card)</span>
|
|
</button>
|
|
|
|
<!-- Open Walk-in Modal Button -->
|
|
<button @click="openModal()" class="glass-btn-primary text-sm">
|
|
<i class="bi bi-plus-circle-fill text-lg"></i>
|
|
<span>ออกคิวนวด Walk-in</span>
|
|
</button>
|
|
|
|
<button @click="fetchQueues()" class="p-2.5 rounded-xl bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 hover:bg-emerald-500/20 transition-all" title="รีเฟรชตารางคิว">
|
|
<i class="bi bi-arrow-clockwise text-lg" :class="loading ? 'animate-spin' : ''"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Queue Kanban Tabs / Columns -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
<!-- Column 1: Waiting (รอเรียกคิว / รอ AI จัดสรร) -->
|
|
<div class="glass-card p-5 rounded-2xl border flex flex-col h-[650px]">
|
|
<div class="flex items-center justify-between pb-3 mb-4 border-b border-emerald-500/20">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-amber-400 animate-ping"></span>
|
|
<h3 class="font-bold text-base">รอเรียกคิว (Waiting)</h3>
|
|
</div>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-amber-500/20 text-amber-300 text-xs font-bold font-mono" x-text="waitingQueues.length">0</span>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto space-y-3.5 pr-1">
|
|
<template x-for="q in waitingQueues" :key="q.id">
|
|
<div class="p-4 rounded-xl bg-slate-800/60 border border-slate-700/60 hover:border-amber-500/50 transition-all relative group">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-xl font-bold font-mono text-amber-400" x-text="q.queue_no">A001</span>
|
|
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase"
|
|
:class="q.priority === 'VIP' ? 'bg-purple-500/20 text-purple-300 border border-purple-500/30' : (q.priority === 'Emergency' ? 'bg-red-500/20 text-red-300 border border-red-500/30 animate-pulse' : 'bg-slate-700 text-slate-300')"
|
|
x-text="q.priority">Normal</span>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-xs my-2">
|
|
<p class="font-semibold text-sm truncate" x-text="q.patient_name || 'ผู้รับบริการทั่วไป'"></p>
|
|
<p class="text-subtle truncate" x-text="`บริการ: ${q.service_name || '-'}`"></p>
|
|
<p class="text-cyan-400 font-mono text-[11px]"><i class="bi bi-clock-history mr-1"></i> เวลาประเมินเริ่ม: <strong x-text="q.est_start_time ? q.est_start_time.split(' ')[1] : 'กำลังคำนวณ...'"></strong></p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="pt-2.5 mt-2.5 border-t border-slate-700/60 flex items-center justify-between">
|
|
<button @click="printTicket(q)" class="text-xs text-subtle hover:text-emerald-400 flex items-center gap-1">
|
|
<i class="bi bi-printer"></i> ใบคิว
|
|
</button>
|
|
<button @click="updateStatus(q.id, 'Assigned')" class="px-3 py-1 rounded-lg bg-emerald-500/20 text-emerald-400 hover:bg-emerald-500 hover:text-white text-xs font-medium transition-all">
|
|
จัดสรรห้อง <i class="bi bi-arrow-right"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div x-show="waitingQueues.length === 0" class="text-center py-12 text-subtle text-sm">ไม่มีคิวที่กำลังรอ</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 2: Assigned & Ready (จัดสรรหมอนวดแล้ว - พร้อมเรียก) -->
|
|
<div class="glass-card p-5 rounded-2xl border flex flex-col h-[650px]">
|
|
<div class="flex items-center justify-between pb-3 mb-4 border-b border-cyan-500/20">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-cyan-400"></span>
|
|
<h3 class="font-bold text-base">พร้อมเรียกรับบริการ (Assigned)</h3>
|
|
</div>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-cyan-500/20 text-cyan-300 text-xs font-bold font-mono" x-text="assignedQueues.length">0</span>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto space-y-3.5 pr-1">
|
|
<template x-for="q in assignedQueues" :key="q.id">
|
|
<div class="p-4 rounded-xl bg-cyan-950/30 border border-cyan-500/30 hover:border-cyan-400 transition-all relative">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-xl font-bold font-mono text-cyan-400" x-text="q.queue_no">A002</span>
|
|
<span class="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-300 text-[10px] font-bold font-mono" x-text="`ห้อง ${q.room_no || 'R-101'}`">R-101</span>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-xs my-2">
|
|
<p class="font-semibold text-sm truncate" x-text="q.patient_name"></p>
|
|
<p class="text-emerald-400 font-medium truncate" x-text="`หมอนวด: ${q.therapist_name || 'AI Assigned'}`"></p>
|
|
<p class="text-subtle truncate" x-text="`บริการ: ${q.service_name}`"></p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="pt-2.5 mt-2.5 border-t border-cyan-500/20 flex items-center justify-between">
|
|
<button @click="callQueueTv(q)" class="px-2.5 py-1 rounded bg-amber-500/20 text-amber-300 hover:bg-amber-500 hover:text-white text-xs font-medium transition-all flex items-center gap-1">
|
|
<i class="bi bi-volume-up-fill"></i> เรียก TV
|
|
</button>
|
|
<button @click="updateStatus(q.id, 'In_Progress')" class="px-3 py-1 rounded-lg bg-emerald-500 text-white hover:bg-emerald-600 text-xs font-medium transition-all">
|
|
เข้าห้องนวด <i class="bi bi-play-fill"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div x-show="assignedQueues.length === 0" class="text-center py-12 text-subtle text-sm">ไม่มีคิวที่รอเข้าห้องพัก</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 3: In Progress (กำลังนวดในห้อง) -->
|
|
<div class="glass-card p-5 rounded-2xl border flex flex-col h-[650px]">
|
|
<div class="flex items-center justify-between pb-3 mb-4 border-b border-emerald-500/20">
|
|
<div class="flex items-center gap-2">
|
|
<span class="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></span>
|
|
<h3 class="font-bold text-base">กำลังให้บริการ (In Progress)</h3>
|
|
</div>
|
|
<span class="px-2.5 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 text-xs font-bold font-mono" x-text="inProgressQueues.length">0</span>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto space-y-3.5 pr-1">
|
|
<template x-for="q in inProgressQueues" :key="q.id">
|
|
<div class="p-4 rounded-xl bg-emerald-950/20 border border-emerald-500/30 hover:border-emerald-400 transition-all relative">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<span class="text-xl font-bold font-mono text-emerald-400" x-text="q.queue_no">A003</span>
|
|
<span class="px-2 py-0.5 rounded bg-emerald-500/20 text-emerald-300 text-[10px] font-bold font-mono" x-text="`ห้อง ${q.room_no || 'R-101'}`">R-101</span>
|
|
</div>
|
|
|
|
<div class="space-y-1 text-xs my-2">
|
|
<p class="font-semibold text-sm truncate" x-text="q.patient_name"></p>
|
|
<p class="text-emerald-400 font-medium truncate" x-text="`หมอนวด: ${q.therapist_name}`"></p>
|
|
<p class="text-subtle truncate" x-text="`เริ่มเมื่อ: ${q.actual_start_time ? q.actual_start_time.split(' ')[1] : '-'}`"></p>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="pt-2.5 mt-2.5 border-t border-emerald-500/20 flex items-center justify-between">
|
|
<a :href="`/clinical/soap?queue_id=${q.id}&patient_id=${q.patient_id}`" class="text-xs text-cyan-400 hover:underline flex items-center gap-1">
|
|
<i class="bi bi-file-medical"></i> บันทึก SOAP Note
|
|
</a>
|
|
<button @click="updateStatus(q.id, 'Completed')" class="px-3 py-1 rounded-lg bg-teal-600 text-white hover:bg-teal-500 text-xs font-medium transition-all">
|
|
นวดเสร็จ <i class="bi bi-check2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<div x-show="inProgressQueues.length === 0" class="text-center py-12 text-subtle text-sm">ไม่มีคิวที่กำลังให้บริการ</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Walk-in Booking & Smart AI Modal (Alpine Dialog) -->
|
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
<div x-show="showModal" x-transition.opacity @click="showModal = false" class="fixed inset-0 bg-black/60 backdrop-blur-sm"></div>
|
|
|
|
<div x-show="showModal" x-transition:enter="transition ease-out duration-300 transform" x-transition:enter-start="opacity-0 translate-y-8 scale-95" x-transition:enter-end="opacity-100 translate-y-0 scale-100" class="relative w-full max-w-lg glass-modal p-6 md:p-8 rounded-3xl shadow-2xl border max-h-[90vh] overflow-y-auto">
|
|
|
|
<div class="flex items-center justify-between pb-4 mb-6 border-b border-emerald-500/20">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 rounded-xl bg-gradient-to-tr from-emerald-500 to-cyan-500 flex items-center justify-center text-white text-lg shadow">
|
|
<i class="bi bi-magic"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-bold text-lg">ออกคิวนวด Walk-in พร้อม AI</h3>
|
|
<p class="text-xs text-subtle">ระบบจัดสรรหมอนวดและห้องพักที่เหมาะสมที่สุดโดยอัตโนมัติ</p>
|
|
</div>
|
|
</div>
|
|
<button @click="showModal = false" class="text-subtle hover:text-white text-xl"><i class="bi bi-x-lg"></i></button>
|
|
</div>
|
|
|
|
<form @submit.prevent="submitWalkin()" class="space-y-5">
|
|
|
|
<!-- Patient Selection / Smart Card Auto-fill -->
|
|
<div>
|
|
<div class="flex justify-between items-center mb-1.5">
|
|
<label class="block text-sm font-medium">1. ผู้รับบริการ (Patient / HN)</label>
|
|
<button type="button" @click="readSmartCard()" class="text-xs text-cyan-400 underline flex items-center gap-1">
|
|
<i class="bi bi-credit-card-2-front"></i> ดึงจาก Smart Card
|
|
</button>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<input type="text" x-model="form.patient_query" placeholder="ระบุชื่อ, HN หรือเลขบัตร 13 หลัก" required class="glass-input flex-1">
|
|
<button type="button" @click="searchPatient()" class="px-4 py-2 rounded-xl bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 hover:bg-emerald-500 hover:text-white transition-all">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
</div>
|
|
<p x-show="selectedPatient" class="text-xs text-emerald-400 mt-1.5 font-medium flex items-center gap-1">
|
|
<i class="bi bi-check-circle-fill"></i> ผู้ป่วย: <span x-text="selectedPatient?.first_name_th + ' ' + selectedPatient?.last_name_th"></span> <span x-text="`(${selectedPatient?.hn})`"></span>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Service Selection -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1.5">2. เลือกบริการหรือคอร์สนวดแผนไทย</label>
|
|
<select x-model="form.service_id" required class="glass-input">
|
|
<option value="">-- กรุณาเลือกบริการ --</option>
|
|
<template x-for="s in services" :key="s.id">
|
|
<option :value="s.id" x-text="`${s.name_th} (${s.duration_mins} นาที - ${s.price} บาท)`"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Priority Selection -->
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1.5">3. ระดับความสำคัญของคิว (Priority)</label>
|
|
<div class="grid grid-cols-3 gap-3">
|
|
<label class="cursor-pointer p-3 rounded-xl border text-center transition-all"
|
|
:class="form.priority === 'Normal' ? 'bg-emerald-500/20 border-emerald-500 text-emerald-400' : 'border-slate-700/60 text-subtle'">
|
|
<input type="radio" x-model="form.priority" value="Normal" class="hidden">
|
|
<span class="block text-sm font-semibold">ทั่วไป</span>
|
|
<span class="block text-[10px] opacity-75">Normal Queue</span>
|
|
</label>
|
|
<label class="cursor-pointer p-3 rounded-xl border text-center transition-all"
|
|
:class="form.priority === 'VIP' ? 'bg-purple-500/20 border-purple-500 text-purple-300' : 'border-slate-700/60 text-subtle'">
|
|
<input type="radio" x-model="form.priority" value="VIP" class="hidden">
|
|
<span class="block text-sm font-semibold">VIP / คอร์ส</span>
|
|
<span class="block text-[10px] opacity-75">Fast Track</span>
|
|
</label>
|
|
<label class="cursor-pointer p-3 rounded-xl border text-center transition-all"
|
|
:class="form.priority === 'Emergency' ? 'bg-red-500/20 border-red-500 text-red-300' : 'border-slate-700/60 text-subtle'">
|
|
<input type="radio" x-model="form.priority" value="Emergency" class="hidden">
|
|
<span class="block text-sm font-semibold">เร่งด่วน / คนชรา</span>
|
|
<span class="block text-[10px] opacity-75">Immediate</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-4 border-t border-emerald-500/20 flex justify-end gap-3">
|
|
<button type="button" @click="showModal = false" class="px-5 py-2.5 rounded-xl border border-slate-500/30 text-subtle hover:text-white transition-all">ยกเลิก</button>
|
|
<button type="submit" :disabled="submitting || !selectedPatient || !form.service_id" class="glass-btn-primary px-6 py-2.5 rounded-xl font-medium">
|
|
<span x-show="!submitting"><i class="bi bi-cpu-fill"></i> ออกคิว & ให้ AI จัดสรร</span>
|
|
<span x-show="submitting"><i class="bi bi-arrow-repeat animate-spin"></i> กำลังคำนวณ...</span>
|
|
</button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('queueApp', () => ({
|
|
loading: false,
|
|
submitting: false,
|
|
showModal: false,
|
|
waitingQueues: [],
|
|
assignedQueues: [],
|
|
inProgressQueues: [],
|
|
services: [],
|
|
selectedPatient: null,
|
|
form: {
|
|
patient_query: '',
|
|
patient_id: null,
|
|
service_id: '',
|
|
priority: 'Normal',
|
|
branch_id: 1
|
|
},
|
|
|
|
init() {
|
|
this.fetchQueues();
|
|
this.fetchServices();
|
|
setInterval(() => this.fetchQueues(true), 15000);
|
|
},
|
|
|
|
async fetchQueues(silent = false) {
|
|
if (!silent) this.loading = true;
|
|
try {
|
|
const res = await this.apiFetch('/queues?branch_id=1', { silent });
|
|
if (res && res.success) {
|
|
const all = res.data.queues || [];
|
|
this.waitingQueues = all.filter(q => q.status === 'Waiting');
|
|
this.assignedQueues = all.filter(q => q.status === 'Assigned');
|
|
this.inProgressQueues = all.filter(q => q.status === 'In_Progress');
|
|
} else {
|
|
this.loadMockQueues();
|
|
}
|
|
} catch (err) {
|
|
this.loadMockQueues();
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async fetchServices() {
|
|
try {
|
|
const res = await this.apiFetch('/services');
|
|
if (res && res.success) {
|
|
this.services = res.data;
|
|
} else {
|
|
this.services = [
|
|
{ id: 1, name_th: 'นวดแผนไทยราชสำนัก (60 นาที)', duration_mins: 60, price: 350 },
|
|
{ id: 2, name_th: 'นวดแผนไทยราชสำนัก (120 นาที)', duration_mins: 120, price: 600 },
|
|
{ id: 3, name_th: 'นวดกดจุดฝ่าเท้าเพื่อสุขภาพ (60 นาที)', duration_mins: 60, price: 400 },
|
|
{ id: 4, name_th: 'นวดน้ำมันอโรมาเธอราปี (90 นาที)', duration_mins: 90, price: 950 }
|
|
];
|
|
}
|
|
} catch (e) {
|
|
this.services = [
|
|
{ id: 1, name_th: 'นวดแผนไทยราชสำนัก (60 นาที)', duration_mins: 60, price: 350 },
|
|
{ id: 2, name_th: 'นวดแผนไทยราชสำนัก (120 นาที)', duration_mins: 120, price: 600 }
|
|
];
|
|
}
|
|
},
|
|
|
|
openModal() {
|
|
this.selectedPatient = { id: 1, hn: 'HN-2407001', first_name_th: 'สมศักดิ์', last_name_th: 'บัตรทอง' };
|
|
this.form.patient_query = 'HN-2407001 (สมศักดิ์ บัตรทอง)';
|
|
this.form.patient_id = 1;
|
|
this.form.service_id = 1;
|
|
this.showModal = true;
|
|
},
|
|
|
|
async readSmartCard() {
|
|
this.toast('กำลังอ่านข้อมูลจากเครื่องอ่านบัตร Smart Card (WebUSB/PCSC)...', 'info');
|
|
try {
|
|
const res = await this.apiFetch('/his/smartcard');
|
|
if (res && res.success) {
|
|
this.selectedPatient = res.data.patient;
|
|
this.form.patient_id = res.data.patient.id;
|
|
this.form.patient_query = `${res.data.patient.hn} - ${res.data.patient.first_name_th} ${res.data.patient.last_name_th}`;
|
|
this.toast('อ่านข้อมูลบัตรประชาชนไทยและลงทะเบียนเรียบร้อยแล้ว', 'success');
|
|
if (!this.showModal) this.showModal = true;
|
|
}
|
|
} catch (err) {
|
|
// Demo fallback
|
|
this.selectedPatient = { id: 2, hn: 'HN-2407002', first_name_th: 'วิภาวี', last_name_th: 'ดีเลิศ' };
|
|
this.form.patient_id = 2;
|
|
this.form.patient_query = 'HN-2407002 - วิภาวี ดีเลิศ (จำลอง Smart Card)';
|
|
this.toast('[Demo Mode] อ่านข้อมูลบัตรประชาชนจำลองสำเร็จ', 'success');
|
|
if (!this.showModal) this.showModal = true;
|
|
}
|
|
},
|
|
|
|
searchPatient() {
|
|
if (!this.form.patient_query) return;
|
|
this.selectedPatient = { id: 1, hn: 'HN-2407001', first_name_th: 'สมศักดิ์', last_name_th: 'บัตรทอง' };
|
|
this.form.patient_id = 1;
|
|
this.toast('พบข้อมูลผู้ป่วยในระบบ', 'success');
|
|
},
|
|
|
|
async submitWalkin() {
|
|
this.submitting = true;
|
|
try {
|
|
const res = await this.apiFetch('/queues/walkin', {
|
|
method: 'POST',
|
|
body: {
|
|
patient_id: this.form.patient_id,
|
|
service_id: Number(this.form.service_id),
|
|
priority: this.form.priority,
|
|
branch_id: 1
|
|
}
|
|
});
|
|
|
|
this.showModal = false;
|
|
|
|
if (res && res.success) {
|
|
const q = res.data;
|
|
Swal.fire({
|
|
title: `🟢 ออกคิวสำเร็จ: ${q.queue_no}`,
|
|
html: `
|
|
<div class="text-left space-y-2 mt-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-sm">
|
|
<p><strong>หมอนวดที่จัดสรร:</strong> T-${q.assigned_therapist_id || 'AI Assigned'}</p>
|
|
<p><strong>ห้องพักที่จัดสรร:</strong> R-${q.assigned_room_id || '101'}</p>
|
|
<p><strong>เวลาเริ่มโดยประมาณ:</strong> ${q.est_start_time || 'ทันที'}</p>
|
|
<p class="text-emerald-400 font-mono text-xs mt-2">✨ คำนวณโดย Smart AI Workload Balancer</p>
|
|
</div>
|
|
`,
|
|
icon: 'success',
|
|
showCancelButton: true,
|
|
confirmButtonText: '<i class="bi bi-printer"></i> พิมพ์ใบคิว (ESC/POS)',
|
|
cancelButtonText: 'ปิดหน้าต่าง',
|
|
confirmButtonColor: '#10b981'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) this.printTicket(q);
|
|
});
|
|
this.fetchQueues();
|
|
} else {
|
|
// Demo alert
|
|
const mockNo = `A00${this.waitingQueues.length + this.assignedQueues.length + 5}`;
|
|
Swal.fire({
|
|
title: `🟢 [Demo] ออกคิวสำเร็จ: ${mockNo}`,
|
|
html: `
|
|
<div class="text-left space-y-2 mt-3 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-sm">
|
|
<p><strong>หมอนวดที่จัดสรร:</strong> หมอสุชาดา (T-101)</p>
|
|
<p><strong>ห้องพักที่จัดสรร:</strong> R-101 (VIP Private)</p>
|
|
<p><strong>เวลาเริ่มโดยประมาณ:</strong> 11:15 น.</p>
|
|
<p class="text-emerald-400 font-mono text-xs mt-2">✨ คำนวณโดย Smart AI Workload Balancer</p>
|
|
</div>
|
|
`,
|
|
icon: 'success',
|
|
showCancelButton: true,
|
|
confirmButtonText: '<i class="bi bi-printer"></i> พิมพ์ใบคิว (ESC/POS)',
|
|
cancelButtonText: 'ปิดหน้าต่าง',
|
|
confirmButtonColor: '#10b981'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) this.printTicket({ queue_no: mockNo, id: 99 });
|
|
});
|
|
this.fetchQueues();
|
|
}
|
|
} catch (err) {
|
|
Swal.fire({ icon: 'error', title: 'ออกคิวไม่สำเร็จ', text: err.message });
|
|
} finally {
|
|
this.submitting = false;
|
|
}
|
|
},
|
|
|
|
async updateStatus(id, status) {
|
|
try {
|
|
const res = await this.apiFetch(`/queues/${id}/status`, {
|
|
method: 'PUT',
|
|
body: { status: status }
|
|
});
|
|
this.toast(`เปลี่ยนสถานะคิวเป็น ${status} เรียบร้อยแล้ว`, 'success');
|
|
this.fetchQueues();
|
|
} catch (e) {
|
|
this.toast(`[Demo] เปลี่ยนสถานะคิวเป็น ${status} แล้ว`, 'success');
|
|
this.fetchQueues();
|
|
}
|
|
},
|
|
|
|
callQueueTv(q) {
|
|
this.playCallSound();
|
|
this.toast(`📣 กำลังเรียกคิว ${q.queue_no} ออกหน้าจอ TV เสียงบรรยายไทย/อังกฤษ`, 'success');
|
|
},
|
|
|
|
async printTicket(q) {
|
|
this.toast(`กำลังส่งคำสั่งพิมพ์ใบคิว ${q.queue_no} ไปยังเครื่องพิมพ์ Thermal Printer (ESC/POS)...`, 'info');
|
|
try {
|
|
const res = await this.apiFetch(`/billing/print-ticket/${q.id}`);
|
|
if (res && res.success) {
|
|
this.toast(`พิมพ์ใบคิว ${q.queue_no} ผ่าน ESC/POS เรียบร้อยแล้ว`, 'success');
|
|
}
|
|
} catch (e) {
|
|
this.toast(`[Demo] จำลองการพิมพ์ใบคิว ${q.queue_no} ผ่านเครื่องพิมพ์ความร้อนสำเร็จ`, 'success');
|
|
}
|
|
},
|
|
|
|
loadMockQueues() {
|
|
this.waitingQueues = [
|
|
{ id: 10, queue_no: 'A005', priority: 'Normal', patient_name: 'คุณกิตติศักดิ์ พึ่งบุญ', service_name: 'นวดแผนไทยราชสำนัก (60 น.)', est_start_time: '2026-07-27 11:30:00' },
|
|
{ id: 11, queue_no: 'V001', priority: 'VIP', patient_name: 'คุณโสภิตา เสน่ห์จันทร์', service_name: 'นวดน้ำมันอโรมา (90 น.)', est_start_time: '2026-07-27 11:15:00' }
|
|
];
|
|
this.assignedQueues = [
|
|
{ id: 12, queue_no: 'A004', priority: 'Normal', patient_name: 'คุณสมรัก ศรัทธา', therapist_name: 'หมอวิไลพร (T-108)', room_no: 'R-102', service_name: 'นวดแผนไทย (60 น.)' }
|
|
];
|
|
this.inProgressQueues = [
|
|
{ id: 1, queue_no: 'A003', priority: 'Normal', patient_name: 'คุณสมชาย รักสุขภาพ', therapist_name: 'หมอสมศรี (T-102)', room_no: 'R-101', actual_start_time: '2026-07-27 10:15:00', patient_id: 1 }
|
|
];
|
|
}
|
|
}));
|
|
});
|
|
</script>
|