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
@@ -0,0 +1,91 @@
<?php
// views/auth/login.php
?>
<div class="min-h-[80vh] flex items-center justify-center">
<div class="glass-card w-full max-w-md p-8 relative overflow-hidden">
<!-- Decorative blob -->
<div class="absolute -top-10 -right-10 w-32 h-32 bg-emerald-500/20 rounded-full blur-2xl"></div>
<div class="absolute -bottom-10 -left-10 w-32 h-32 bg-teal-500/20 rounded-full blur-2xl"></div>
<div class="text-center mb-8 relative z-10">
<div class="inline-flex items-center justify-center w-20 h-20 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm rounded-full mb-4 shadow-sm border border-white/20">
<img src="assets/img/logo.png" alt="Logo" class="w-12 h-12 object-contain">
</div>
<h2 class="text-2xl font-bold bg-gradient-to-r from-emerald-600 to-teal-500 bg-clip-text text-transparent">ระบบบริหารจัดการงานเปล</h2>
<p class="text-slate-500 dark:text-slate-400 mt-2">โรงพยาบาลเกาะสมุย</p>
</div>
<form id="loginForm" class="relative z-10">
<input type="hidden" name="action" value="login">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<div class="mb-5">
<label for="username" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ชื่อผู้ใช้งาน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-solid fa-user text-slate-400"></i>
</div>
<input type="text" class="glass-input block w-full pl-10 pr-3 py-3" id="username" name="username" required placeholder="รหัสเจ้าหน้าที่">
</div>
</div>
<div class="mb-6">
<label for="password" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">รหัสผ่าน</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fa-solid fa-lock text-slate-400"></i>
</div>
<input type="password" class="glass-input block w-full pl-10 pr-3 py-3" id="password" name="password" required placeholder="••••••••">
</div>
</div>
<button type="submit" class="btn-primary-glass w-full py-3 text-lg font-medium flex items-center justify-center">
<span class="text">เข้าสู่ระบบ <i class="fa-solid fa-arrow-right-to-bracket ml-2"></i></span>
</button>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#loginForm').submit(function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
$.ajax({
url: 'api/users.php',
type: 'POST',
data: $(this).serialize(),
success: function(response) {
if (response.status === 'success') {
Swal.fire({
icon: 'success',
title: 'เข้าสู่ระบบสำเร็จ',
text: 'กำลังพาท่านเข้าสู่ระบบ...',
timer: 1500,
showConfirmButton: false,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then(() => {
window.location.href = '?page=dashboard';
});
} else {
btn.removeClass('btn-loading');
Swal.fire({
icon: 'error',
title: 'เข้าสู่ระบบล้มเหลว',
text: response.message,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
});
}
},
error: function() {
btn.removeClass('btn-loading');
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
}
});
});
});
</script>
@@ -0,0 +1,215 @@
<?php
// views/dashboard/executive.php
?>
<div class="mb-6 flex justify-between items-end flex-wrap gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">การวิเคราะห์ระบบ (Executive Dashboard)</h2>
<p class="text-slate-500 dark:text-slate-400">ดูสถิติประสิทธิภาพและตัวชี้วัด (KPI) ของหน่วยงาน</p>
</div>
<div class="flex gap-2">
<select class="glass-input text-sm py-2 px-3" id="periodSelect" onchange="loadExecutiveData()">
<option value="today">วันนี้</option>
<option value="week">สัปดาห์นี้</option>
<option value="month">เดือนนี้</option>
</select>
<button class="btn-primary-glass py-2 px-4 text-sm font-medium flex items-center">
<i class="fa-solid fa-file-export me-2"></i> Export CSV
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<div class="glass-card p-6 border-t-4 border-t-emerald-500">
<h6 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">SLA Compliance (< 15 นาที)</h6>
<h2 class="text-4xl font-bold text-slate-800 dark:text-white" id="slaPercent">0%</h2>
<div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2 mt-4 overflow-hidden">
<div class="bg-emerald-500 h-2 rounded-full" id="slaBar" style="width: 0%"></div>
</div>
</div>
<div class="glass-card p-6 border-t-4 border-t-blue-500">
<h6 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">เวลารอคอยเฉลี่ย (Wait Time)</h6>
<h2 class="text-4xl font-bold text-slate-800 dark:text-white"><span id="avgWaitTime">0</span> <span class="text-xl text-slate-500">นาที</span></h2>
<p class="text-xs text-red-500 mt-2"><i class="fa-solid fa-arrow-trend-up"></i> ช้าลงจากเมื่อวาน 2 นาที</p>
</div>
<div class="glass-card p-6 border-t-4 border-t-purple-500">
<h6 class="text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">เวลาให้บริการเฉลี่ย (Service Time)</h6>
<h2 class="text-4xl font-bold text-slate-800 dark:text-white"><span id="avgServiceTime">0</span> <span class="text-xl text-slate-500">นาที</span></h2>
<p class="text-xs text-green-500 mt-2"><i class="fa-solid fa-arrow-trend-down"></i> เร็วขึ้นจากเมื่อวาน 1 นาที</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="glass-card p-6">
<h5 class="font-bold text-slate-800 dark:text-white mb-4">แนวโน้มงาน 7 วันย้อนหลัง (Trend)</h5>
<div class="relative h-72">
<canvas id="trendChart"></canvas>
</div>
</div>
<div class="glass-card p-6">
<h5 class="font-bold text-slate-800 dark:text-white mb-4">ภาระงานแยกตามแผนก (Workload by Dept)</h5>
<div class="relative h-72">
<canvas id="deptChart"></canvas>
</div>
</div>
</div>
<div class="glass-card overflow-hidden mb-6">
<div class="p-6 border-b border-slate-200 dark:border-slate-700">
<h5 class="font-bold text-slate-800 dark:text-white">สถิติรายบุคคล (Staff Performance)</h5>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-100/50 dark:bg-slate-800/50 text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wider">
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700">พนักงาน</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">รับงานทั้งหมด</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">สำเร็จ</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">ยกเลิก/ปฏิเสธ</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">เวลาบริการเฉลี่ย</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800 text-sm" id="staffPerformanceTable">
<tr>
<td colspan="5" class="p-8 text-center text-slate-500"><i class="fa-solid fa-spinner fa-spin me-2"></i>กำลังโหลด...</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
let trendChartInstance = null;
let deptChartInstance = null;
document.addEventListener('DOMContentLoaded', function() {
loadExecutiveData();
});
function loadExecutiveData() {
const period = document.getElementById('periodSelect').value;
fetch(`api/executive.php?period=${period}`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Update KPI Cards
const kpi = data.kpi;
document.getElementById('slaPercent').textContent = kpi.sla_percent + '%';
document.getElementById('slaBar').style.width = kpi.sla_percent + '%';
document.getElementById('avgWaitTime').textContent = kpi.avg_wait_time;
document.getElementById('avgServiceTime').textContent = kpi.avg_service_time;
// Update Charts
initCharts(data.charts);
// Update Staff Performance Table
renderStaffPerformance(data.staff_performance);
}
})
.catch(error => console.error('Error fetching dashboard data:', error));
}
function renderStaffPerformance(staffData) {
const tbody = document.getElementById('staffPerformanceTable');
tbody.innerHTML = '';
if (!staffData || staffData.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="p-8 text-center text-slate-500">ไม่มีข้อมูลในช่วงเวลานี้</td></tr>';
return;
}
staffData.forEach(staff => {
const total = staff.total_jobs || 0;
const completed = staff.completed_jobs || 0;
const cancelled = staff.cancelled_jobs || 0;
const avgTime = staff.avg_service_time ? Math.round(staff.avg_service_time) : 0;
tbody.innerHTML += `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="p-4 font-medium text-slate-800 dark:text-slate-200">
<div class="flex items-center gap-3">
<img src="api/avatar.php?name=${encodeURIComponent(staff.full_name)}" class="w-8 h-8 rounded-full border border-slate-200 dark:border-slate-700">
${staff.full_name}
</div>
</td>
<td class="p-4 text-center font-bold text-blue-600 dark:text-blue-400">${total}</td>
<td class="p-4 text-center font-bold text-emerald-600 dark:text-emerald-400">${completed}</td>
<td class="p-4 text-center font-bold text-red-500 dark:text-red-400">${cancelled}</td>
<td class="p-4 text-center font-bold text-purple-600 dark:text-purple-400">${avgTime} นาที</td>
</tr>
`;
});
}
function initCharts(chartData) {
// Determine text colors based on theme
const isDark = document.documentElement.classList.contains('dark');
const textColor = isDark ? '#cbd5e1' : '#475569';
const gridColor = isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)';
// Trend Chart
const ctxTrend = document.getElementById('trendChart').getContext('2d');
if (trendChartInstance) {
trendChartInstance.destroy();
}
let gradient = ctxTrend.createLinearGradient(0, 0, 0, 400);
gradient.addColorStop(0, 'rgba(16, 185, 129, 0.5)'); // emerald-500
gradient.addColorStop(1, 'rgba(16, 185, 129, 0)');
trendChartInstance = new Chart(ctxTrend, {
type: 'line',
data: {
labels: chartData.trend.labels,
datasets: [{
label: 'จำนวนงาน',
data: chartData.trend.data,
borderColor: '#10b981',
backgroundColor: gradient,
borderWidth: 2,
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: gridColor }, ticks: { color: textColor } },
x: { grid: { display: false }, ticks: { color: textColor } }
}
}
});
// Department Chart
const ctxDept = document.getElementById('deptChart').getContext('2d');
if (deptChartInstance) {
deptChartInstance.destroy();
}
deptChartInstance = new Chart(ctxDept, {
type: 'bar',
data: {
labels: chartData.departments.labels,
datasets: [{
label: 'จำนวนงาน (Job)',
data: chartData.departments.data,
backgroundColor: '#3b82f6',
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { beginAtZero: true, grid: { color: gridColor }, ticks: { color: textColor } },
x: { grid: { display: false }, ticks: { color: textColor } }
}
}
});
}
</script>
@@ -0,0 +1,339 @@
<?php
// views/dashboard/index.php
?>
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ภาพรวมงานเปลวันนี้</h2>
<p class="text-slate-500 dark:text-slate-400">สถานะแบบ Real-time ข้อมูลอัปเดตตลอดเวลา</p>
</div>
<!-- Stats Row -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div class="glass-card p-6 border-l-4 border-l-blue-500 hover:scale-[1.02] transition-transform">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">งานทั้งหมด (วันนี้)</p>
<h3 class="text-3xl font-bold text-slate-800 dark:text-white mt-1" id="stat-total">0</h3>
</div>
<div class="p-3 bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-400 rounded-lg">
<i class="fa-solid fa-list-check fa-lg"></i>
</div>
</div>
</div>
<div class="glass-card p-6 border-l-4 border-l-yellow-500 hover:scale-[1.02] transition-transform">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">รอดำเนินการ</p>
<h3 class="text-3xl font-bold text-slate-800 dark:text-white mt-1" id="stat-pending">0</h3>
</div>
<div class="p-3 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-600 dark:text-yellow-400 rounded-lg">
<i class="fa-solid fa-clock fa-lg"></i>
</div>
</div>
</div>
<div class="glass-card p-6 border-l-4 border-l-emerald-500 hover:scale-[1.02] transition-transform">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">กำลังดำเนินการ</p>
<h3 class="text-3xl font-bold text-slate-800 dark:text-white mt-1" id="stat-progress">0</h3>
</div>
<div class="p-3 bg-emerald-100 dark:bg-emerald-900/50 text-emerald-600 dark:text-emerald-400 rounded-lg">
<i class="fa-solid fa-person-walking fa-lg"></i>
</div>
</div>
</div>
<div class="glass-card p-6 border-l-4 border-l-green-500 hover:scale-[1.02] transition-transform">
<div class="flex justify-between items-start">
<div>
<p class="text-sm font-medium text-slate-500 dark:text-slate-400">เสร็จสิ้น</p>
<h3 class="text-3xl font-bold text-slate-800 dark:text-white mt-1" id="stat-completed">0</h3>
</div>
<div class="p-3 bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 rounded-lg">
<i class="fa-solid fa-check-double fa-lg"></i>
</div>
</div>
</div>
</div>
<!-- Quick Actions & Table -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="lg:col-span-2">
<div class="glass-card overflow-hidden">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-800/50 flex justify-between items-center">
<h5 class="font-bold text-slate-800 dark:text-white m-0">งานล่าสุด</h5>
<button class="text-emerald-600 hover:text-emerald-700 dark:text-emerald-400 text-sm font-medium" onclick="loadDashboardData()"><i class="fa-solid fa-rotate-right"></i> รีเฟรช</button>
</div>
<div class="p-6 overflow-x-auto">
<table class="w-full text-left border-collapse" id="jobsTable">
<thead>
<tr class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider border-b border-slate-200 dark:border-slate-700">
<th class="pb-3 px-4">เลขที่งาน</th>
<th class="pb-3 px-4">เวลาขอ</th>
<th class="pb-3 px-4">จุดรับ -> จุดส่ง</th>
<th class="pb-3 px-4">อุปกรณ์</th>
<th class="pb-3 px-4 text-center">สถานะ</th>
</tr>
</thead>
<tbody class="text-sm text-slate-700 dark:text-slate-300 divide-y divide-slate-200 dark:divide-slate-700" id="jobsTableBody">
<tr><td colspan="5" class="text-center py-4">กำลังโหลดข้อมูล...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="lg:col-span-1">
<div class="glass-card mb-6">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-800/50">
<h5 class="font-bold text-slate-800 dark:text-white m-0">ทางลัด (Quick Actions)</h5>
</div>
<div class="p-6 flex flex-col gap-3">
<a href="?page=request" class="btn-primary-glass py-3 px-4 text-center font-medium w-full block">
<i class="fa-solid fa-plus-circle me-2"></i> ร้องของานเปลใหม่
</a>
<?php if ($_SESSION['role'] !== 'ward'): ?>
<a href="?page=staff" class="btn-secondary-glass py-3 px-4 text-center font-medium w-full block">
<i class="fa-solid fa-list-check me-2"></i> ดูงานที่ต้องรับผิดชอบ
</a>
<?php endif; ?>
</div>
</div>
<div class="glass-card">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 bg-white/50 dark:bg-slate-800/50">
<h5 class="font-bold text-slate-800 dark:text-white m-0">เจ้าหน้าที่ศูนย์เปล (Online)</h5>
</div>
<div class="p-4 max-h-64 overflow-y-auto">
<ul class="space-y-3" id="staffOnlineList">
<li class="text-center text-sm text-slate-500 py-2">กำลังโหลดข้อมูล...</li>
</ul>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
loadDashboardData();
setInterval(loadDashboardData, 30000); // 30s auto refresh
});
function loadDashboardData() {
fetch('api/dashboard.php')
.then(response => response.json())
.then(res => {
if (res.status === 'success') {
const data = res.data;
// Update Stats
document.getElementById('stat-total').textContent = data.stats.total;
document.getElementById('stat-pending').textContent = data.stats.pending;
document.getElementById('stat-progress').textContent = data.stats.in_progress;
document.getElementById('stat-completed').textContent = data.stats.completed;
// Update Jobs Table
const tbody = document.getElementById('jobsTableBody');
if (data.jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">ไม่มีข้อมูลงานในวันนี้</td></tr>';
} else {
let html = '';
data.jobs.forEach(job => {
let statusBadge = '';
if (job.status === 'pending') {
statusBadge = '<div class="flex flex-col sm:flex-row gap-2 justify-center items-center"><span class="bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-500 px-3 py-1 rounded-full text-xs font-medium border border-yellow-200 dark:border-yellow-800 whitespace-nowrap">รอดำเนินการ</span>';
// Add Accept button for pending jobs
statusBadge += `<button onclick="acceptJob('${job.job_number}')" class="text-xs bg-emerald-500 hover:bg-emerald-600 text-white px-3 py-1 rounded-full transition-colors shadow-sm whitespace-nowrap flex items-center gap-1"><i class="fa-solid fa-hand-holding-hand"></i> รับงาน</button></div>`;
} else if (job.status === 'assigned' || job.status === 'in_progress' || job.status === 'waiting_for_return') {
statusBadge = '<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400 px-3 py-1 rounded-full text-xs font-medium border border-blue-200 dark:border-blue-800 whitespace-nowrap">กำลังดำเนินการ</span>';
} else if (job.status === 'completed') {
statusBadge = '<div class="flex flex-col sm:flex-row gap-2 justify-center items-center"><span class="bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-500 px-3 py-1 rounded-full text-xs font-medium border border-green-200 dark:border-green-800 whitespace-nowrap">เสร็จสิ้น</span>';
if (!job.rating) {
statusBadge += `<button onclick="openRatingModal(${job.id}, '${job.job_number}')" class="text-xs bg-yellow-400 hover:bg-yellow-500 text-yellow-900 px-3 py-1 rounded-full transition-colors shadow-sm whitespace-nowrap flex items-center gap-1"><i class="fa-solid fa-star"></i> ให้คะแนน</button></div>`;
} else {
statusBadge += `<span class="text-xs text-yellow-500 bg-yellow-50 dark:bg-yellow-900/20 px-2 py-1 rounded-full whitespace-nowrap"><i class="fa-solid fa-star"></i> ${job.rating}</span></div>`;
}
} else {
statusBadge = '<span class="bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-300 px-3 py-1 rounded-full text-xs font-medium border border-slate-200 dark:border-slate-600 whitespace-nowrap">ยกเลิก</span>';
}
// Format equipment
let equipmentBadge = '';
if (job.equipment_type === 'wheelchair') equipmentBadge = 'รถนั่ง';
else if (job.equipment_type === 'stretcher') equipmentBadge = 'รถนอน';
else if (job.equipment_type === 'bed') equipmentBadge = 'เตียง';
else equipmentBadge = 'เดินไปเอง';
html += `
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors border-b border-slate-100 dark:border-slate-800 last:border-0">
<td class="py-3 px-4 font-medium text-emerald-600 dark:text-emerald-400 whitespace-nowrap">${job.job_number}</td>
<td class="py-3 px-4 whitespace-nowrap text-slate-500">${new Date(job.created_at).toLocaleTimeString('th-TH', {hour: '2-digit', minute:'2-digit'})} น.</td>
<td class="py-3 px-4 text-sm"><div class="flex items-center gap-2"><span class="truncate max-w-[120px] inline-block" title="${job.from_dept}">${job.from_dept}</span> <i class="fa-solid fa-arrow-right text-slate-300 text-[10px]"></i> <span class="truncate max-w-[120px] inline-block font-medium" title="${job.to_dept}">${job.to_dept}</span></div></td>
<td class="py-3 px-4 whitespace-nowrap"><span class="bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2 py-1 rounded text-xs font-medium">${equipmentBadge}</span></td>
<td class="py-3 px-4 text-center">${statusBadge}</td>
</tr>
`;
});
tbody.innerHTML = html;
}
// Update Staff Online List
const staffList = document.getElementById('staffOnlineList');
if (data.staff.length === 0) {
staffList.innerHTML = '<li class="text-center text-sm text-slate-500 py-2">ไม่มีเจ้าหน้าที่ออนไลน์</li>';
} else {
let staffHtml = '';
data.staff.forEach(user => {
let statusColor = user.status === 'online' ? 'bg-green-500' : 'bg-yellow-500';
let statusText = user.status === 'online' ? 'ว่าง' : 'ติดงาน';
staffHtml += `
<li class="flex items-center justify-between p-2 hover:bg-slate-50 dark:hover:bg-slate-800/50 rounded-lg transition-colors">
<div class="flex items-center gap-3">
<div class="relative">
<img src="api/avatar.php?username=${encodeURIComponent(user.username)}&name=${encodeURIComponent(user.full_name || user.username)}" class="w-8 h-8 rounded-full object-cover shadow-sm">
<span class="absolute bottom-0 right-0 w-2.5 h-2.5 ${statusColor} border-2 border-white dark:border-slate-900 rounded-full"></span>
</div>
<span class="text-sm font-medium">${user.username}</span>
</div>
<span class="text-xs text-slate-500 dark:text-slate-400">${statusText}</span>
</li>
`;
});
staffList.innerHTML = staffHtml;
}
} else {
console.error('Error fetching dashboard data:', res.message);
}
})
.catch(err => {
console.error('Fetch error:', err);
});
}
// Rating Modal Functions
function openRatingModal(jobId, jobNumber) {
document.getElementById('rating_job_id').value = jobId;
document.getElementById('ratingJobNumber').textContent = jobNumber;
// Reset stars
document.querySelectorAll('.rating-star').forEach(star => {
star.classList.remove('text-yellow-400', 'fas');
star.classList.add('text-slate-300', 'far');
});
document.getElementById('rating_value').value = '0';
document.getElementById('rating_feedback').value = '';
document.getElementById('ratingModal').classList.remove('hidden');
}
function closeRatingModal() {
document.getElementById('ratingModal').classList.add('hidden');
}
function setRating(rating) {
document.getElementById('rating_value').value = rating;
document.querySelectorAll('.rating-star').forEach((star, index) => {
if (index < rating) {
star.classList.remove('text-slate-300', 'far');
star.classList.add('text-yellow-400', 'fas');
} else {
star.classList.remove('text-yellow-400', 'fas');
star.classList.add('text-slate-300', 'far');
}
});
}
function submitRating() {
const jobId = document.getElementById('rating_job_id').value;
const rating = document.getElementById('rating_value').value;
const feedback = document.getElementById('rating_feedback').value;
if (rating == '0') {
Swal.fire('กรุณาให้คะแนน', 'โปรดเลือกระดับความพึงพอใจ', 'warning');
return;
}
fetch('api/feedback.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `action=rate_job&job_id=${jobId}&rating=${rating}&feedback=${encodeURIComponent(feedback)}&csrf_token=<?= $_SESSION['csrf_token'] ?? '' ?>`
})
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
Swal.fire('ขอบคุณ!', 'บันทึกคะแนนความพึงพอใจแล้ว', 'success');
closeRatingModal();
loadDashboardData();
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
})
.catch(err => {
Swal.fire('ข้อผิดพลาด', 'เกิดการเชื่อมต่อล้มเหลว', 'error');
});
}
function acceptJob(jobNumber) {
Swal.fire({
title: 'รับงานนี้?',
text: "คุณต้องการรับงานหมายเลข " + jobNumber + " ใช่หรือไม่?",
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#10b981',
cancelButtonColor: '#ef4444',
confirmButtonText: 'ใช่, รับงาน',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: 'api/jobs.php',
type: 'POST',
data: {
action: 'assign_job',
job_id: jobNumber
},
success: function(response) {
if(response.status === 'success') {
Swal.fire('สำเร็จ', response.message, 'success').then(() => {
window.location.href = '?page=staff';
});
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
Swal.fire('ผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อ', 'error');
}
});
}
});
}
</script>
<!-- Rating Modal -->
<div id="ratingModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/50 backdrop-blur-sm">
<div class="glass-card w-full max-w-md p-6 animate-fade-in-up">
<h3 class="text-xl font-bold text-slate-800 dark:text-white mb-4">ประเมินความพึงพอใจ</h3>
<p class="text-sm text-slate-500 mb-4">หมายเลขงาน: <span id="ratingJobNumber" class="font-bold"></span></p>
<input type="hidden" id="rating_job_id" value="">
<input type="hidden" id="rating_value" value="0">
<div class="flex justify-center gap-2 mb-6">
<i class="rating-star far fa-star text-4xl text-slate-300 cursor-pointer hover:scale-110 transition-transform" onclick="setRating(1)"></i>
<i class="rating-star far fa-star text-4xl text-slate-300 cursor-pointer hover:scale-110 transition-transform" onclick="setRating(2)"></i>
<i class="rating-star far fa-star text-4xl text-slate-300 cursor-pointer hover:scale-110 transition-transform" onclick="setRating(3)"></i>
<i class="rating-star far fa-star text-4xl text-slate-300 cursor-pointer hover:scale-110 transition-transform" onclick="setRating(4)"></i>
<i class="rating-star far fa-star text-4xl text-slate-300 cursor-pointer hover:scale-110 transition-transform" onclick="setRating(5)"></i>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ข้อเสนอแนะเพิ่มเติม (ถ้ามี)</label>
<textarea id="rating_feedback" class="glass-input w-full px-4 py-2" rows="3" placeholder="ระบุข้อเสนอแนะ..."></textarea>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="closeRatingModal()" class="btn-secondary-glass px-4 py-2">ยกเลิก</button>
<button type="button" onclick="submitRating()" class="btn-primary-glass px-6 py-2">ส่งผลประเมิน</button>
</div>
</div>
</div>
@@ -0,0 +1,175 @@
<?php
// views/jobs/detail.php
require_once 'models/Job.php';
require_once 'models/Department.php';
$job_number = $_GET['id'] ?? '';
if (empty($job_number)) {
echo "<div class='text-center p-8 text-red-500 font-bold'>ไม่พบรหัสงาน</div>";
return;
}
$jobModel = new Job();
$job = $jobModel->getByJobNumber($job_number);
if (!$job) {
echo "<div class='text-center p-8 text-red-500 font-bold'>ไม่พบข้อมูลงาน {$job_number}</div>";
return;
}
$timelines = $jobModel->getJobTimelines($job['id']);
$statusColors = [
'pending' => 'bg-yellow-100 text-yellow-800 border-yellow-200',
'assigned' => 'bg-blue-100 text-blue-800 border-blue-200',
'accepted' => 'bg-indigo-100 text-indigo-800 border-indigo-200',
'in_progress' => 'bg-purple-100 text-purple-800 border-purple-200',
'completed' => 'bg-green-100 text-green-800 border-green-200',
'cancelled' => 'bg-red-100 text-red-800 border-red-200',
];
$statusNames = [
'pending' => 'รอดำเนินการ',
'assigned' => 'มอบหมายแล้ว',
'accepted' => 'กำลังไปรับ',
'in_progress' => 'กำลังไปส่ง',
'completed' => 'เสร็จสิ้น',
'cancelled' => 'ยกเลิก',
];
$priorityColors = [
'normal' => 'emerald',
'urgent' => 'amber',
'emergency' => 'red'
];
$priorityNames = [
'normal' => 'ทั่วไป (Normal)',
'urgent' => 'ด่วน (Urgent)',
'emergency' => 'ฉุกเฉิน (Emergency)'
];
$badgeClass = $statusColors[$job['status']] ?? 'bg-slate-100 text-slate-800';
$badgeName = $statusNames[$job['status']] ?? $job['status'];
$pColor = $priorityColors[$job['priority']] ?? 'emerald';
$pName = $priorityNames[$job['priority']] ?? $job['priority'];
?>
<div class="max-w-4xl mx-auto">
<div class="flex items-center justify-between mb-6">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white flex items-center flex-wrap gap-2">
รายละเอียดงาน <span class="px-3 py-1 text-sm rounded-full border <?= $badgeClass ?>"><?= $badgeName ?></span>
</h2>
<p class="text-slate-500 dark:text-slate-400 mt-1">หมายเลขงาน: <?= $job['job_number'] ?></p>
</div>
<button onclick="window.history.back()" class="bg-white/50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700 text-slate-700 dark:text-slate-300 px-4 py-2 rounded-xl transition-colors shrink-0">
<i class="fa-solid fa-arrow-left me-1"></i> กลับ
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- ข้อมูลหลัก -->
<div class="md:col-span-2 space-y-6">
<div class="glass-card p-6 border-t-4 border-t-<?= $pColor ?>-500">
<h5 class="text-lg font-bold text-slate-800 dark:text-white mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">ข้อมูลผู้ป่วย</h5>
<div class="flex flex-col sm:flex-row gap-6">
<?php if (!empty($job['patient_image'])): ?>
<div class="w-full sm:w-1/3">
<img src="<?= htmlspecialchars($job['patient_image']) ?>" class="w-full h-auto rounded-lg shadow-sm border border-slate-200 object-cover" alt="รูปผู้ป่วย">
</div>
<?php endif; ?>
<div class="flex-1">
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">ชื่อผู้ป่วย</p>
<h4 class="text-xl font-bold text-slate-800 dark:text-white mb-3">
<?= htmlspecialchars($job['patient_name'] ?: 'ไม่ระบุชื่อ (ดูรูปภาพ)') ?>
<?php if ($job['patient_hn']): ?>
<span class="text-sm font-normal text-slate-500">(HN: <?= htmlspecialchars($job['patient_hn']) ?>)</span>
<?php endif; ?>
</h4>
<div class="grid grid-cols-2 gap-4 mt-4">
<div>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">ความเร่งด่วน</p>
<span class="px-2 py-1 bg-<?= $pColor ?>-100 text-<?= $pColor ?>-800 rounded text-xs font-medium border border-<?= $pColor ?>-200">
<?= $pName ?>
</span>
</div>
<div>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">อุปกรณ์</p>
<p class="font-medium text-slate-800 dark:text-white"><?= strtoupper($job['equipment_type']) ?></p>
</div>
</div>
<?php if ($job['infection_control'] == 1): ?>
<div class="mt-4 bg-red-100 border border-red-400 text-red-700 px-3 py-2 rounded-lg flex items-center text-sm shadow-sm font-medium">
<i class="fa-solid fa-biohazard me-2 text-lg"></i> ผู้ป่วยโรคติดต่อ / แยกกักโรค
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="glass-card p-6">
<h5 class="text-lg font-bold text-slate-800 dark:text-white mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">เส้นทางการเคลื่อนย้าย</h5>
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-full bg-red-100 dark:bg-red-900/50 text-red-600 dark:text-red-400 flex items-center justify-center font-bold text-lg z-10 shadow-sm border border-red-200 shrink-0">A</div>
<div class="ml-4 flex-1">
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider">รับที่ (From)</p>
<p class="font-bold text-lg text-slate-800 dark:text-white"><?= htmlspecialchars($job['from_department_name'] ?? 'ไม่ระบุ') ?></p>
</div>
</div>
<div class="relative ml-5 border-l-2 border-dashed border-slate-300 dark:border-slate-600 h-8 -my-5 z-0"></div>
<div class="flex items-center mt-4">
<div class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 flex items-center justify-center font-bold text-lg z-10 shadow-sm border border-green-200 shrink-0">B</div>
<div class="ml-4 flex-1">
<p class="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wider">ส่งที่ (To)</p>
<p class="font-bold text-lg text-slate-800 dark:text-white"><?= htmlspecialchars($job['to_department_name'] ?? $job['to_department_custom'] ?? 'ไม่ระบุ') ?></p>
</div>
</div>
</div>
<?php if (!empty($job['notes'])): ?>
<div class="glass-card p-6">
<h5 class="text-lg font-bold text-slate-800 dark:text-white mb-2 border-b border-slate-200 dark:border-slate-700 pb-2">หมายเหตุ</h5>
<p class="text-slate-700 dark:text-slate-300 whitespace-pre-line"><?= htmlspecialchars($job['notes']) ?></p>
</div>
<?php endif; ?>
</div>
<!-- Timeline -->
<div class="md:col-span-1">
<div class="glass-card p-6 h-full">
<h5 class="text-lg font-bold text-slate-800 dark:text-white mb-6 border-b border-slate-200 dark:border-slate-700 pb-2">ไทม์ไลน์ (Timeline)</h5>
<div class="relative border-l border-slate-200 dark:border-slate-700 ml-3 space-y-6 pb-4">
<?php if (empty($timelines)): ?>
<p class="text-slate-500 ml-4">ไม่มีข้อมูลไทม์ไลน์</p>
<?php else: ?>
<?php foreach($timelines as $index => $tl):
$isLast = ($index === count($timelines) - 1);
$tlColor = 'slate';
$icon = 'fa-circle-dot';
if (strpos($tl['status'], 'cancelled') !== false) { $tlColor = 'red'; $icon = 'fa-times'; }
elseif (strpos($tl['status'], 'completed') !== false) { $tlColor = 'green'; $icon = 'fa-check'; }
elseif (strpos($tl['status'], 'accepted') !== false) { $tlColor = 'blue'; $icon = 'fa-user-check'; }
?>
<div class="relative pl-6">
<div class="absolute -left-3 top-1 w-6 h-6 rounded-full bg-<?= $tlColor ?>-100 text-<?= $tlColor ?>-600 flex items-center justify-center border-2 border-white dark:border-slate-800 shadow-sm">
<i class="fa-solid <?= $icon ?> text-xs"></i>
</div>
<div class="<?= $isLast ? 'opacity-100' : 'opacity-75' ?>">
<p class="text-xs text-slate-500 mb-1"><?= date('d/m/Y H:i', strtotime($tl['created_at'])) ?></p>
<p class="font-medium text-sm text-slate-800 dark:text-white"><?= htmlspecialchars($tl['description']) ?></p>
<p class="text-xs text-slate-500 mt-1">โดย: <?= htmlspecialchars($tl['created_by_name'] ?? 'ระบบ') ?></p>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
</div>
@@ -0,0 +1,184 @@
<?php
// views/jobs/request.php
?>
<div class="max-w-3xl mx-auto">
<div class="mb-6">
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ร้องของานเปล (Request Job)</h2>
<p class="text-slate-500 dark:text-slate-400">กรอกข้อมูลผู้ป่วยและสถานที่ให้ครบถ้วน</p>
</div>
<div class="glass-card p-6 md:p-8">
<form id="requestForm">
<input type="hidden" name="action" value="create_job">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<h4 class="text-lg font-semibold text-emerald-600 dark:text-emerald-400 mb-4 border-b border-emerald-100 dark:border-emerald-900 pb-2">1. ข้อมูลผู้ป่วย</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">HN (ถ้ามี)</label>
<input type="text" class="glass-input w-full px-4 py-2" name="hn" placeholder="เช่น 6601234">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อ-สกุลผู้ป่วย <span class="text-red-500">*</span></label>
<input type="text" class="glass-input w-full px-4 py-2" name="patient_name" required placeholder="ชื่อ นามสกุล">
</div>
</div>
<h4 class="text-lg font-semibold text-emerald-600 dark:text-emerald-400 mb-4 border-b border-emerald-100 dark:border-emerald-900 pb-2">2. เส้นทางและอุปกรณ์</h4>
<?php
require_once dirname(__DIR__, 2) . '/models/Department.php';
require_once dirname(__DIR__, 2) . '/models/Setting.php';
$deptModel = new Department();
$departments = $deptModel->getAllActive();
$settingModel = new Setting();
$equipmentsJson = $settingModel->get('equipment_types');
$equipments = $equipmentsJson ? json_decode($equipmentsJson, true) : [
['id' => 'wheelchair', 'name' => 'รถนั่ง (Wheelchair)', 'icon' => 'fa-wheelchair'],
['id' => 'stretcher', 'name' => 'รถนอน (Stretcher)', 'icon' => 'fa-bed'],
['id' => 'bed', 'name' => 'เตียง (Bed)', 'icon' => 'fa-bed-pulse']
];
$prioritiesJson = $settingModel->get('job_priorities');
$priorities = $prioritiesJson ? json_decode($prioritiesJson, true) : [
['id' => 'normal', 'name' => 'ทั่วไป (Normal)', 'color' => 'blue'],
['id' => 'urgent', 'name' => 'ด่วน (Urgency)', 'color' => 'amber'],
['id' => 'emergency', 'name' => 'ฉุกเฉิน (Emergency)', 'color' => 'red']
];
?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-5">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">จุดรับผู้ป่วย <span class="text-red-500">*</span></label>
<select class="glass-input w-full px-4 py-2" name="from_department_id" required>
<option value="">-- เลือกจุดรับ --</option>
<?php foreach($departments as $dept): ?>
<option value="<?= $dept['id'] ?>"><?= escape($dept['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">จุดส่งผู้ป่วย <span class="text-red-500">*</span></label>
<select class="glass-input w-full px-4 py-2" name="to_department_id" required>
<option value="">-- เลือกจุดส่ง --</option>
<?php foreach($departments as $dept): ?>
<option value="<?= $dept['id'] ?>"><?= escape($dept['name']) ?></option>
<?php endforeach; ?>
</select>
<div class="mt-2 flex items-center bg-blue-50 dark:bg-blue-900/20 p-2 rounded-lg border border-blue-100 dark:border-blue-800">
<input type="checkbox" id="is_round_trip" name="is_round_trip" value="1" class="w-4 h-4 text-blue-600 rounded border-blue-300 focus:ring-blue-500 cursor-pointer">
<label for="is_round_trip" class="ml-2 text-sm font-medium text-blue-800 dark:text-blue-300 cursor-pointer">
<i class="fa-solid fa-rotate-left me-1"></i> ไป-กลับ (รอรับกลับทันที)
</label>
</div>
</div>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ประเภทอุปกรณ์ <span class="text-red-500">*</span></label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<?php foreach($equipments as $index => $eq): ?>
<label class="relative flex items-center p-3 border border-slate-200 dark:border-slate-700 rounded-xl cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
<input type="radio" name="equipment_type" value="<?= escape($eq['id']) ?>" class="w-4 h-4 text-emerald-600 border-gray-300 focus:ring-emerald-500 dark:bg-slate-700 dark:border-slate-600" <?= $index === 0 ? 'required checked' : '' ?>>
<span class="ml-3 flex flex-col">
<span class="text-sm font-medium text-slate-900 dark:text-white flex items-center gap-2">
<i class="fa-solid <?= escape($eq['icon']) ?> text-emerald-500"></i> <?= escape($eq['name']) ?>
</span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ระดับความฉุกเฉิน (Priority) <span class="text-red-500">*</span></label>
<select class="glass-input w-full px-4 py-2" name="priority" required>
<?php foreach($priorities as $index => $pri): ?>
<option value="<?= escape($pri['id']) ?>" class="text-<?= escape($pri['color']) ?>-600 font-medium" <?= $index === 0 ? 'selected' : '' ?>>
<?= escape($pri['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-5 flex items-center gap-2 bg-red-50 dark:bg-red-900/20 p-4 rounded-xl border border-red-200 dark:border-red-800">
<input type="checkbox" id="infection_control" name="infection_control" value="1" class="w-5 h-5 text-red-600 rounded border-red-300 focus:ring-red-500">
<label for="infection_control" class="text-sm font-medium text-red-800 dark:text-red-300">
<i class="fa-solid fa-biohazard me-1"></i> ผู้ป่วยโรคติดต่อ / ต้องการการแยกกักโรค (Infection Control)
</label>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">เวลาที่ต้องการให้มารับ (Booking)</label>
<div class="flex gap-4 mb-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="is_scheduled" value="0" checked class="text-emerald-500 focus:ring-emerald-500" onchange="document.getElementById('scheduleTimeDiv').classList.add('hidden')">
<span class="text-sm dark:text-slate-300">ด่วนทันที</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="is_scheduled" value="1" class="text-emerald-500 focus:ring-emerald-500" onchange="document.getElementById('scheduleTimeDiv').classList.remove('hidden')">
<span class="text-sm dark:text-slate-300">จองเวลาล่วงหน้า</span>
</label>
</div>
<div id="scheduleTimeDiv" class="hidden">
<input type="datetime-local" class="glass-input w-full px-4 py-2" name="scheduled_time">
</div>
</div>
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">หมายเหตุ (ถ้ามี)</label>
<textarea class="glass-input w-full px-4 py-2" name="notes" rows="2" placeholder="เช่น ผู้ป่วยให้ออกซิเจน, ระวังสายน้ำเกลือ"></textarea>
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="reset" class="btn-secondary-glass py-2 px-6">
ล้างข้อมูล
</button>
<button type="submit" class="btn-primary-glass py-2 px-8 flex items-center">
<span class="text"><i class="fa-solid fa-paper-plane me-2"></i> ส่งคำขอ</span>
</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#requestForm').submit(function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
const originalText = btn.html();
btn.addClass('btn-loading');
$.ajax({
url: 'api/jobs.php',
type: 'POST',
data: $(this).serialize(),
success: function(response) {
if (response.status === 'success') {
Swal.fire({
icon: 'success',
title: 'บันทึกสำเร็จ',
text: 'หมายเลขงาน: ' + response.job_number,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then(() => {
window.location.href = '?page=dashboard';
});
} else {
btn.removeClass('btn-loading').html(originalText);
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
btn.removeClass('btn-loading').html(originalText);
Swal.fire('ผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อ', 'error');
}
});
});
});
</script>
@@ -0,0 +1,596 @@
<?php
// views/jobs/staff.php
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-emerald-600 to-teal-400 dark:from-emerald-400 dark:to-teal-200">งานของฉัน</h2>
<p class="text-slate-500 dark:text-slate-400 text-sm">รายการงานเปลที่คุณได้รับมอบหมาย</p>
</div>
<button onclick="openMapModal()" class="bg-white/80 dark:bg-slate-800/80 hover:bg-white dark:hover:bg-slate-700 border border-slate-200 dark:border-slate-700 shadow-sm text-slate-700 dark:text-slate-300 px-4 py-2 rounded-xl text-sm font-medium transition-all backdrop-blur-sm">
<i class="fa-solid fa-map-location-dot text-emerald-500 mr-2"></i> แผนผังตึก
</button>
</div>
<div id="jobContainer">
<div class="glass-card p-8 text-center text-slate-500 flex flex-col items-center justify-center min-h-[300px]">
<i class="fa-solid fa-spinner fa-spin text-4xl mb-4 text-emerald-500"></i>
<p>กำลังตรวจสอบงาน...</p>
</div>
</div>
<!-- Signature & QR Modal (Tailwind version) -->
<div id="signatureModal" class="fixed inset-0 z-[100] hidden">
<!-- Backdrop -->
<div class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm transition-opacity"></div>
<!-- Modal Content -->
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg border border-slate-200 dark:border-slate-700">
<div class="bg-emerald-600 px-4 py-3 sm:px-6 flex justify-between items-center">
<h3 class="text-base font-semibold leading-6 text-white">ยืนยันการส่งผู้ป่วย</h3>
<button type="button" class="text-white hover:text-emerald-100 close-modal">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="px-4 pb-4 pt-5 sm:p-6 sm:pb-4 text-center">
<h6 class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">แสกน QR Code เพื่อดูประวัติ</h6>
<div id="qrcode" class="mb-4 flex justify-center bg-white p-2 rounded-lg inline-block mx-auto"></div>
<div class="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700">
<h6 class="text-sm font-medium text-slate-700 dark:text-slate-300 mb-3">ลายเซ็นพยาบาลผู้รับ (Digital Signature)</h6>
<div class="bg-slate-50 dark:bg-slate-900 rounded-xl p-1 border border-slate-200 dark:border-slate-700">
<canvas id="signature-pad" class="w-full h-48 rounded-lg cursor-crosshair touch-none"></canvas>
</div>
</div>
</div>
<div class="bg-slate-50 dark:bg-slate-700/50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
<button type="button" class="inline-flex w-full justify-center rounded-xl bg-emerald-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-emerald-500 sm:ml-3 sm:w-auto transition-colors" id="saveSignature">
บันทึกและปิดงาน
</button>
<button type="button" class="mt-3 inline-flex w-full justify-center rounded-xl bg-white dark:bg-slate-800 px-3 py-2 text-sm font-semibold text-slate-900 dark:text-slate-200 shadow-sm ring-1 ring-inset ring-slate-300 dark:ring-slate-600 hover:bg-slate-50 dark:hover:bg-slate-700 sm:mt-0 sm:w-auto transition-colors" id="clearSignature">
ล้างลายเซ็น
</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.1.5/dist/signature_pad.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
let currentJob = null;
let signaturePad = null;
let qrGeneratedFor = null;
const modal = document.getElementById('signatureModal');
// Init Signature Pad
const canvas = document.getElementById('signature-pad');
signaturePad = new SignaturePad(canvas, {
backgroundColor: 'rgba(255, 255, 255, 0)'
});
function resizeCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
signaturePad.clear();
}
window.addEventListener("resize", resizeCanvas);
document.querySelectorAll('.close-modal').forEach(btn => {
btn.addEventListener('click', () => modal.classList.add('hidden'));
});
document.getElementById('clearSignature').addEventListener('click', () => {
signaturePad.clear();
});
function fetchActiveJob() {
$.ajax({
url: 'api/jobs.php',
type: 'GET',
data: { action: 'get_active_job' },
dataType: 'json',
success: function(res) {
if (res.status === 'success' && res.data) {
currentJob = res.data;
renderJob(currentJob);
} else {
renderNoJob();
}
},
error: function() {
renderNoJob(true);
}
});
}
function renderNoJob(isError = false) {
currentJob = null;
const msg = isError ? 'เกิดข้อผิดพลาดในการดึงข้อมูล' : 'ขณะนี้คุณยังไม่มีงานที่ได้รับมอบหมาย';
const icon = isError ? 'fa-triangle-exclamation text-red-500' : 'fa-check-circle text-emerald-500';
document.getElementById('jobContainer').innerHTML = `
<div class="glass-card p-8 text-center text-slate-500 flex flex-col items-center justify-center min-h-[300px]">
<div class="w-16 h-16 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center mb-4 text-3xl">
<i class="fa-solid ${icon}"></i>
</div>
<h3 class="text-xl font-medium text-slate-800 dark:text-white mb-2">${msg}</h3>
<p class="text-sm">ระบบจะแจ้งเตือนเมื่อมีงานใหม่เข้ามา</p>
<button onclick="location.reload()" class="mt-6 btn-secondary-glass px-6 py-2">
<i class="fa-solid fa-rotate-right me-2"></i> รีเฟรช
</button>
</div>
`;
}
function renderJob(job) {
let statusBadge = '';
let btnArrivedDisabled = '';
let btnPickedUpDisabled = 'opacity-50 cursor-not-allowed" disabled';
let btnCompletedDisabled = 'opacity-50 cursor-not-allowed" disabled';
let btnArrivedClass = 'btn-secondary-glass';
let btnPickedUpClass = 'btn-secondary-glass';
let btnArrivedText = '<i class="fa-solid fa-person-walking-arrow-right me-2"></i> ถึงจุดรับผู้ป่วย';
let btnPickedUpText = '<i class="fa-solid fa-bed-pulse me-2"></i> รับผู้ป่วยขึ้นเปล';
if (job.status === 'assigned') {
statusBadge = '<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300 text-xs font-semibold px-2.5 py-0.5 rounded border border-blue-200 dark:border-blue-800">กำลังเดินทางไปรับ</span>';
} else if (job.status === 'accepted') {
statusBadge = '<span class="bg-indigo-100 text-indigo-800 dark:bg-indigo-900/50 dark:text-indigo-300 text-xs font-semibold px-2.5 py-0.5 rounded border border-indigo-200 dark:border-indigo-800">ถึงจุดรับผู้ป่วยแล้ว</span>';
btnArrivedDisabled = 'opacity-50 cursor-not-allowed" disabled';
btnArrivedClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
btnArrivedText = '<i class="fa-solid fa-check me-2"></i> ถึงจุดรับแล้ว';
btnPickedUpDisabled = '';
} else if (job.status === 'in_progress') {
statusBadge = '<span class="bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300 text-xs font-semibold px-2.5 py-0.5 rounded border border-amber-200 dark:border-amber-800">กำลังเดินทางไปส่ง</span>';
btnArrivedDisabled = 'opacity-50 cursor-not-allowed" disabled';
btnArrivedClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
btnArrivedText = '<i class="fa-solid fa-check me-2"></i> ถึงจุดรับแล้ว';
btnPickedUpDisabled = 'opacity-50 cursor-not-allowed" disabled';
btnPickedUpClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
btnPickedUpText = '<i class="fa-solid fa-check me-2"></i> รับผู้ป่วยแล้ว';
btnCompletedDisabled = '';
} else if (job.status === 'waiting_for_return') {
statusBadge = '<span class="bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300 text-xs font-semibold px-2.5 py-0.5 rounded border border-blue-200 dark:border-blue-800">กำลังรอรับกลับ</span>';
btnArrivedDisabled = 'opacity-50 cursor-not-allowed" disabled';
btnArrivedClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
btnArrivedText = '<i class="fa-solid fa-check me-2"></i> ถึงจุดรับแล้ว';
btnPickedUpDisabled = 'opacity-50 cursor-not-allowed" disabled';
btnPickedUpClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
btnPickedUpText = '<i class="fa-solid fa-check me-2"></i> รับผู้ป่วยแล้ว';
btnCompletedDisabled = '';
}
let equipmentIcon = job.equipment_icon || 'fa-wheelchair';
let equipmentName = job.equipment_name || job.equipment_type.toUpperCase();
const html = `
<div class="glass-card mb-8 border-l-4 border-${job.priority_color}-500" id="activeJobCard">
<div class="px-6 py-4 border-b border-emerald-500/20 bg-emerald-50/50 dark:bg-emerald-900/20 rounded-t-2xl flex justify-between items-center">
<h5 class="font-bold text-emerald-700 dark:text-emerald-400 m-0 flex items-center">
<span class="relative flex h-3 w-3 mr-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span>
งานที่กำลังดำเนินการ (Active)
</h5>
<div class="flex items-center gap-2">
${job.is_round_trip == 1 ? '<span class="bg-purple-100 text-purple-800 dark:bg-purple-900/50 dark:text-purple-300 text-xs font-bold px-2 py-1 rounded-full"><i class="fa-solid fa-rotate-left me-1"></i> ไป-กลับ</span>' : ''}
<button onclick="openChatModal(${job.id}, '${job.job_number}')" class="bg-blue-100 hover:bg-blue-200 text-blue-700 p-2 rounded-full transition-colors" title="แชทกับจุดรับ">
<i class="fa-solid fa-comment-dots"></i>
</button>
${statusBadge}
</div>
</div>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">หมายเลขงาน</p>
<h4 class="text-xl font-bold text-slate-800 dark:text-white">${job.job_number}</h4>
</div>
<span class="bg-${job.priority_color}-100 text-${job.priority_color}-800 text-xs font-medium px-2.5 py-0.5 rounded border border-${job.priority_color}-200">${job.priority_name}</span>
</div>
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">ชื่อผู้ป่วย / รูปถ่าย</p>
${job.patient_image ? `<div class="mb-3"><img src="${job.patient_image}" class="w-full max-w-[200px] h-auto rounded-lg border border-slate-200 shadow-sm object-cover" alt="รูปผู้เรียก"></div>` : ''}
<p class="text-lg font-medium text-slate-800 dark:text-white mb-4">${job.patient_name || 'ไม่ระบุชื่อ (ดูรูปภาพ)'} ${job.patient_hn ? '(HN: ' + job.patient_hn + ')' : ''}</p>
${job.infection_control == 1 ? '<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-2 rounded-lg flex items-center shadow-lg animate-pulse"><i class="fa-solid fa-biohazard text-xl mr-2"></i><span class="font-bold">ผู้ป่วยโรคติดต่อ / แยกกักโรค โปรดสวม PPE</span></div>' : ''}
<p class="text-sm text-slate-500 dark:text-slate-400 mb-1">อุปกรณ์</p>
<p class="font-medium text-slate-800 dark:text-white"><i class="fa-solid ${equipmentIcon} text-emerald-500 me-2"></i> ${equipmentName}</p>
</div>
<div class="bg-slate-50/50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
<h6 class="font-semibold text-slate-700 dark:text-slate-300 mb-3 border-b border-slate-200 dark:border-slate-700 pb-2">เส้นทาง (Route)</h6>
<div class="flex items-center mb-4">
<div class="w-8 h-8 rounded-full bg-red-100 dark:bg-red-900/50 text-red-600 dark:text-red-400 flex items-center justify-center font-bold z-10">A</div>
<div class="ml-4">
<p class="text-xs text-slate-500 dark:text-slate-400">รับที่ (From)</p>
<p class="font-bold text-slate-800 dark:text-white">${job.from_department_name}</p>
</div>
</div>
<div class="relative ml-4 border-l-2 border-dashed border-slate-300 dark:border-slate-600 h-6 -my-4 z-0"></div>
<div class="flex items-center mt-4">
<div class="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900/50 text-green-600 dark:text-green-400 flex items-center justify-center font-bold z-10">B</div>
<div class="ml-4">
<p class="text-xs text-slate-500 dark:text-slate-400">ส่งที่ (To)</p>
<p class="font-bold text-slate-800 dark:text-white">${job.to_department_name || job.to_department_custom}</p>
</div>
</div>
</div>
</div>
<div class="mt-8 pt-6 border-t border-slate-200 dark:border-slate-700">
<h6 class="font-medium text-slate-700 dark:text-slate-300 mb-4">อัปเดตสถานะงาน (Actions)</h6>
<div class="flex flex-col sm:flex-row gap-3">
<button class="flex-1 py-3 px-4 font-medium ${btnArrivedClass} ${btnArrivedDisabled}" id="btnArrived" onclick="updateJobStatus('accepted')">
${btnArrivedText}
</button>
<button class="flex-1 py-3 px-4 font-medium ${btnPickedUpClass} ${btnPickedUpDisabled}" id="btnPickedUp" onclick="updateJobStatus('in_progress')">
${btnPickedUpText}
</button>
${job.is_round_trip == 1 && job.status === 'in_progress' ?
`<button class="btn-primary-glass flex-1 py-3 px-4 font-medium ${btnCompletedDisabled}" id="btnWaitReturn" onclick="updateJobStatus('waiting_for_return')">
<i class="fa-solid fa-clock me-2"></i> รอรับกลับ
</button>` :
`<button class="btn-primary-glass flex-1 py-3 px-4 font-medium ${btnCompletedDisabled}" id="btnCompleted" onclick="openSignatureModal()">
<i class="fa-solid fa-check-double me-2"></i> ${job.status === 'waiting_for_return' ? 'ส่งกลับเรียบร้อย' : 'ส่งผู้ป่วยเรียบร้อย'}
</button>`}
</div>
<div class="mt-3">
<button class="w-full py-2 px-4 font-medium bg-white/50 dark:bg-slate-800 border border-red-200 dark:border-red-900/50 text-red-500 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-xl transition-colors shadow-sm" onclick="promptCancelJob('${job.job_number}')">
<i class="fa-solid fa-times-circle me-1"></i> ปฏิเสธ/ยกเลิกงาน (พบปัญหาระหว่างงาน)
</button>
</div>
</div>
</div>
</div>
`;
document.getElementById('jobContainer').innerHTML = html;
}
window.updateJobStatus = function(newStatus) {
if (!currentJob) return;
$.ajax({
url: 'api/jobs.php',
type: 'POST',
data: {
action: 'update_status',
job_id: currentJob.job_number,
status: newStatus
},
dataType: 'json',
success: function(res) {
if (res.status === 'success') {
// Refetch job to update UI
fetchActiveJob();
} else {
Swal.fire('ผิดพลาด', res.message, 'error');
}
}
});
}
window.promptCancelJob = function(jobNumber) {
Swal.fire({
title: 'ปฏิเสธ/ยกเลิกงาน',
input: 'text',
inputLabel: 'โปรดระบุเหตุผลที่ยกเลิกงาน (เช่น ผู้ป่วยปฏิเสธ, หาผู้ป่วยไม่พบ)',
inputPlaceholder: 'พิมพ์เหตุผลที่นี่...',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#94a3b8',
confirmButtonText: 'ยืนยันยกเลิก',
cancelButtonText: 'ปิด',
inputValidator: (value) => {
if (!value) {
return 'กรุณาระบุเหตุผล!';
}
}
}).then((result) => {
if (result.isConfirmed) {
Swal.showLoading();
$.ajax({
url: 'api/jobs.php',
type: 'POST',
data: {
action: 'cancel_job',
job_id: jobNumber,
reason: result.value
},
dataType: 'json',
success: function(res) {
if (res.status === 'success') {
Swal.fire({
title: 'สำเร็จ!',
text: 'ยกเลิกงานเรียบร้อยแล้ว',
icon: 'success',
timer: 1500,
showConfirmButton: false
}).then(() => {
fetchActiveJob(); // Will show no jobs UI
});
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
},
error: function() {
Swal.fire('ข้อผิดพลาด', 'ไม่สามารถติดต่อเซิร์ฟเวอร์ได้', 'error');
}
});
}
});
}
window.openSignatureModal = function() {
if (!currentJob) return;
modal.classList.remove('hidden');
resizeCanvas();
const qrContainer = document.getElementById('qrcode');
if (qrGeneratedFor !== currentJob.job_number) {
qrContainer.innerHTML = "";
new QRCode(qrContainer, {
text: window.location.origin + window.location.pathname + "?page=job_detail&id=" + currentJob.job_number,
width: 128,
height: 128,
colorDark : "#0f172a",
colorLight : "#ffffff",
});
qrGeneratedFor = currentJob.job_number;
}
}
document.getElementById('saveSignature').addEventListener('click', function() {
if (signaturePad.isEmpty()) {
Swal.fire({
title: 'แจ้งเตือน',
text: 'กรุณาเซ็นชื่อรับผู้ป่วย',
icon: 'warning',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
});
return;
}
const dataURL = signaturePad.toDataURL('image/png');
const formData = new FormData();
formData.append('action', 'save_signature');
formData.append('job_id', currentJob.job_number);
formData.append('signature', dataURL);
const btn = $(this);
const originalText = btn.html();
btn.html('<i class="fa-solid fa-spinner fa-spin"></i>').prop('disabled', true);
fetch('api/jobs.php', {
method: 'POST',
body: formData
}).then(res => res.json()).then(data => {
btn.html(originalText).prop('disabled', false);
if (data.status === 'success') {
modal.classList.add('hidden');
Swal.fire({
title: 'เสร็จสิ้น',
text: 'ปิดงานและบันทึกลายเซ็นเรียบร้อย',
icon: 'success',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then(() => {
fetchActiveJob(); // Should return No Job
});
} else {
Swal.fire('ผิดพลาด', data.message, 'error');
}
}).catch(err => {
btn.html(originalText).prop('disabled', false);
Swal.fire('ผิดพลาด', 'เกิดข้อผิดพลาดในการเชื่อมต่อ', 'error');
});
});
// Start fetching
fetchActiveJob();
});
// Chat Functions
let chatInterval;
function openChatModal(jobId, jobNumber) {
document.getElementById('chat_job_id').value = jobId;
document.getElementById('chatJobNumber').textContent = jobNumber;
document.getElementById('chatModal').classList.remove('hidden');
loadChatMessages();
chatInterval = setInterval(loadChatMessages, 3000);
}
function closeChatModal() {
document.getElementById('chatModal').classList.add('hidden');
clearInterval(chatInterval);
}
function loadChatMessages() {
const jobId = document.getElementById('chat_job_id').value;
fetch(`api/chat.php?action=get_messages&job_id=${jobId}`)
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
const chatBox = document.getElementById('chatBox');
let html = '';
res.data.forEach(msg => {
const isMe = msg.user_id == <?= $_SESSION['user_id'] ?>;
if (isMe) {
html += `
<div class="flex justify-end mb-3">
<div class="bg-emerald-500 text-white rounded-l-xl rounded-br-xl px-4 py-2 max-w-[80%] shadow-sm">
<p class="text-sm">${msg.message}</p>
<p class="text-[10px] text-emerald-100 text-right mt-1">${new Date(msg.created_at).toLocaleTimeString('th-TH', {hour: '2-digit', minute:'2-digit'})}</p>
</div>
</div>`;
} else {
html += `
<div class="flex justify-start mb-3">
<div class="bg-white dark:bg-slate-700 text-slate-800 dark:text-white rounded-r-xl rounded-bl-xl px-4 py-2 max-w-[80%] shadow-sm border border-slate-100 dark:border-slate-600">
<p class="text-xs text-slate-500 dark:text-slate-400 font-bold mb-1">${msg.full_name || 'ผู้ใช้งาน'}</p>
<p class="text-sm">${msg.message}</p>
<p class="text-[10px] text-slate-400 text-right mt-1">${new Date(msg.created_at).toLocaleTimeString('th-TH', {hour: '2-digit', minute:'2-digit'})}</p>
</div>
</div>`;
}
});
chatBox.innerHTML = html;
chatBox.scrollTop = chatBox.scrollHeight;
}
});
}
function sendChatMessage() {
const jobId = document.getElementById('chat_job_id').value;
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message) return;
input.value = '';
fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `action=send_message&job_id=${jobId}&message=${encodeURIComponent(message)}`
}).then(() => loadChatMessages());
}
// Map Functions
let hospitalMapsData = [];
function openMapModal() {
document.getElementById('mapModal').classList.remove('hidden');
loadHospitalMaps();
}
function closeMapModal() {
document.getElementById('mapModal').classList.add('hidden');
}
function loadHospitalMaps() {
fetch('api/maps.php?action=list&status=active')
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
hospitalMapsData = res.data;
const selector = document.getElementById('mapSelector');
if (hospitalMapsData.length === 0) {
selector.innerHTML = '<option value="">ไม่มีข้อมูลแผนผังในระบบ</option>';
selector.disabled = true;
showMapDetails(null);
return;
}
selector.disabled = false;
let html = '<option value="">-- เลือกอาคารและชั้น --</option>';
hospitalMapsData.forEach(map => {
html += `<option value="${map.id}">${map.name} (${map.building} ชั้น ${map.floor})</option>`;
});
selector.innerHTML = html;
// Auto select first map if available
if(hospitalMapsData.length > 0) {
selector.value = hospitalMapsData[0].id;
showMapDetails(hospitalMapsData[0].id);
}
}
});
}
function showMapDetails(mapId) {
const container = document.getElementById('mapImageContainer');
const locText = document.getElementById('mapTransportLocationText');
const locBox = document.getElementById('mapTransportLocationBox');
if (!mapId) {
container.innerHTML = '<div class="text-slate-500 py-12 text-center w-full">กรุณาเลือกแผนผังที่ต้องการดู</div>';
locBox.classList.add('hidden');
return;
}
const map = hospitalMapsData.find(m => m.id == mapId);
if (map) {
container.innerHTML = `<img src="${map.image_path}" alt="${map.name}" class="max-w-full rounded shadow-sm border border-slate-200 dark:border-slate-700 animate-fade-in">`;
if (map.transport_center_location) {
locText.textContent = map.transport_center_location;
locBox.classList.remove('hidden');
} else {
locBox.classList.add('hidden');
}
}
}
</script>
<!-- Chat Modal -->
<div id="chatModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/50 backdrop-blur-sm">
<div class="bg-slate-50 dark:bg-slate-800 w-full max-w-md rounded-2xl shadow-2xl flex flex-col h-[600px] max-h-[90vh] animate-fade-in-up border border-slate-200 dark:border-slate-700 overflow-hidden">
<div class="bg-emerald-600 px-4 py-3 flex justify-between items-center text-white">
<h3 class="font-bold flex items-center">
<i class="fa-solid fa-comments me-2"></i> แชทงาน <span id="chatJobNumber" class="ml-2 bg-emerald-500 px-2 py-0.5 rounded text-sm"></span>
</h3>
<button onclick="closeChatModal()" class="text-white hover:text-emerald-200"><i class="fa-solid fa-xmark text-xl"></i></button>
</div>
<input type="hidden" id="chat_job_id">
<div id="chatBox" class="flex-1 p-4 overflow-y-auto bg-slate-100 dark:bg-slate-900/50 flex flex-col gap-1">
<!-- Messages will be loaded here -->
</div>
<div class="p-3 bg-white dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 flex gap-2">
<input type="text" id="chatInput" class="flex-1 bg-slate-100 dark:bg-slate-900 border-none rounded-full px-4 py-2 text-sm focus:ring-2 focus:ring-emerald-500 dark:text-white" placeholder="พิมพ์ข้อความ..." onkeypress="if(event.key === 'Enter') sendChatMessage()">
<button onclick="sendChatMessage()" class="bg-emerald-500 hover:bg-emerald-600 text-white w-10 h-10 rounded-full flex items-center justify-center transition-colors shadow-sm">
<i class="fa-solid fa-paper-plane text-sm"></i>
</button>
</div>
</div>
</div>
<!-- Map Modal -->
<div id="mapModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/80 backdrop-blur-sm">
<div class="bg-white dark:bg-slate-800 w-full max-w-4xl rounded-2xl shadow-2xl flex flex-col max-h-[90vh] animate-fade-in-up overflow-hidden">
<div class="px-6 py-4 border-b border-slate-200 dark:border-slate-700 flex flex-col sm:flex-row justify-between items-start sm:items-center bg-slate-50 dark:bg-slate-800/50 gap-4">
<h3 class="font-bold text-slate-800 dark:text-white flex items-center text-lg whitespace-nowrap">
<i class="fa-solid fa-map-location-dot text-emerald-500 me-2"></i> แผนผังอาคารโรงพยาบาล
</h3>
<div class="flex items-center w-full sm:w-auto gap-3">
<select id="mapSelector" class="glass-input px-4 py-2 w-full sm:w-64 bg-white dark:bg-slate-900" onchange="showMapDetails(this.value)">
<option value="">กำลังโหลด...</option>
</select>
<button onclick="closeMapModal()" class="text-slate-500 hover:text-slate-700 dark:hover:text-white bg-slate-200 dark:bg-slate-700 w-10 h-10 rounded-xl flex items-center justify-center shrink-0"><i class="fa-solid fa-xmark"></i></button>
</div>
</div>
<div id="mapTransportLocationBox" class="bg-blue-50 dark:bg-blue-900/30 border-b border-blue-100 dark:border-blue-800 px-6 py-3 hidden">
<p class="text-blue-800 dark:text-blue-300 font-medium flex items-center text-sm">
<i class="fa-solid fa-location-crosshairs text-blue-500 mr-2"></i>
<strong>จุดประจำศูนย์เปล / จุดพักคอย:&nbsp;</strong> <span id="mapTransportLocationText"></span>
</p>
</div>
<div class="p-4 flex-1 overflow-auto bg-slate-100 dark:bg-slate-900 flex justify-center items-center" id="mapImageContainer">
<!-- Map Image -->
<div class="text-slate-500 py-12 text-center w-full">กำลังโหลดข้อมูล...</div>
</div>
</div>
</div>
@@ -0,0 +1,109 @@
<?php
// views/jobs/staff_summary.php
require_once 'models/Job.php';
$jobModel = new Job();
$user_id = $_SESSION['user_id'];
$period = $_GET['period'] ?? 'today';
$stats = $jobModel->getStaffStats($user_id, $period);
$history = $jobModel->getStaffHistory($user_id, 50); // Get last 50 jobs
$total_jobs = $stats['total_jobs'] ?? 0;
$completed_jobs = $stats['completed_jobs'] ?? 0;
$cancelled_jobs = $stats['cancelled_jobs'] ?? 0;
$avg_service_time = $stats['avg_service_time'] ? round($stats['avg_service_time']) : 0;
?>
<div class="mb-6 flex justify-between items-end flex-wrap gap-4">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">สรุปผลงานของฉัน</h2>
<p class="text-slate-500 dark:text-slate-400">ประวัติการทำงานและสถิติส่วนตัว</p>
</div>
<form method="GET" class="flex gap-2">
<input type="hidden" name="page" value="staff_summary">
<select name="period" class="glass-input text-sm py-2 px-3" onchange="this.form.submit()">
<option value="today" <?= $period == 'today' ? 'selected' : '' ?>>วันนี้</option>
<option value="week" <?= $period == 'week' ? 'selected' : '' ?>>สัปดาห์นี้</option>
<option value="month" <?= $period == 'month' ? 'selected' : '' ?>>เดือนนี้</option>
</select>
</form>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 mb-8">
<div class="glass-card p-4 md:p-6 border-l-4 border-blue-500">
<h6 class="text-xs md:text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">รับงานทั้งหมด</h6>
<h2 class="text-2xl md:text-3xl font-bold text-slate-800 dark:text-white"><?= $total_jobs ?> <span class="text-base text-slate-500 font-normal">งาน</span></h2>
</div>
<div class="glass-card p-4 md:p-6 border-l-4 border-emerald-500">
<h6 class="text-xs md:text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">สำเร็จ</h6>
<h2 class="text-2xl md:text-3xl font-bold text-emerald-600 dark:text-emerald-400"><?= $completed_jobs ?> <span class="text-base text-slate-500 font-normal">งาน</span></h2>
</div>
<div class="glass-card p-4 md:p-6 border-l-4 border-red-500">
<h6 class="text-xs md:text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">ยกเลิก/ปฏิเสธ</h6>
<h2 class="text-2xl md:text-3xl font-bold text-red-600 dark:text-red-400"><?= $cancelled_jobs ?> <span class="text-base text-slate-500 font-normal">งาน</span></h2>
</div>
<div class="glass-card p-4 md:p-6 border-l-4 border-purple-500">
<h6 class="text-xs md:text-sm font-medium text-slate-500 dark:text-slate-400 mb-1">เวลาบริการเฉลี่ย</h6>
<h2 class="text-2xl md:text-3xl font-bold text-purple-600 dark:text-purple-400"><?= $avg_service_time ?> <span class="text-base text-slate-500 font-normal">นาที</span></h2>
</div>
</div>
<div class="glass-card overflow-hidden">
<div class="p-4 md:p-6 border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50 flex justify-between items-center">
<h3 class="font-bold text-slate-800 dark:text-white">ประวัติงานล่าสุด (50 รายการ)</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-100/50 dark:bg-slate-800/50 text-slate-500 dark:text-slate-400 text-xs uppercase tracking-wider">
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700">เลขที่งาน</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700">วันที่-เวลา</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 hidden md:table-cell">ผู้ป่วย</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 hidden sm:table-cell">เส้นทาง</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">สถานะ</th>
<th class="p-4 font-medium border-b border-slate-200 dark:border-slate-700 text-center">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-800 text-sm">
<?php if(empty($history)): ?>
<tr>
<td colspan="6" class="p-8 text-center text-slate-500">ยังไม่มีประวัติการรับงาน</td>
</tr>
<?php else: ?>
<?php foreach($history as $job): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors group">
<td class="p-4 font-medium text-slate-800 dark:text-slate-200 whitespace-nowrap"><?= $job['job_number'] ?></td>
<td class="p-4 text-slate-600 dark:text-slate-400 whitespace-nowrap">
<?= date('d/m/Y', strtotime($job['created_at'])) ?><br>
<span class="text-xs text-slate-400"><?= date('H:i', strtotime($job['created_at'])) ?> น.</span>
</td>
<td class="p-4 text-slate-800 dark:text-slate-200 hidden md:table-cell">
<?= htmlspecialchars($job['patient_name'] ?: 'ไม่ระบุชื่อ') ?>
</td>
<td class="p-4 hidden sm:table-cell text-xs">
<div class="flex items-center gap-1 text-slate-500 dark:text-slate-400 max-w-[200px]">
<span class="truncate"><?= htmlspecialchars($job['from_department_name']) ?></span>
<i class="fa-solid fa-arrow-right text-[10px]"></i>
<span class="truncate"><?= htmlspecialchars($job['to_department_name'] ?? $job['to_department_custom']) ?></span>
</div>
</td>
<td class="p-4 text-center">
<?php if($job['status'] === 'completed'): ?>
<span class="bg-green-100 text-green-800 border-green-200 border text-xs font-medium px-2.5 py-0.5 rounded-full whitespace-nowrap"><i class="fa-solid fa-check me-1"></i>สำเร็จ</span>
<?php elseif($job['status'] === 'cancelled'): ?>
<span class="bg-red-100 text-red-800 border-red-200 border text-xs font-medium px-2.5 py-0.5 rounded-full whitespace-nowrap"><i class="fa-solid fa-times me-1"></i>ยกเลิก</span>
<?php endif; ?>
</td>
<td class="p-4 text-center">
<a href="?page=job_detail&id=<?= $job['job_number'] ?>" class="inline-block px-3 py-1.5 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg text-xs font-medium transition-colors">
<i class="fa-solid fa-file-lines me-1"></i> รายละเอียด
</a>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,289 @@
<?php
// views/public/request.php
require_once dirname(__DIR__, 2) . '/models/Department.php';
require_once dirname(__DIR__, 2) . '/models/Setting.php';
$deptModel = new Department();
$departments = $deptModel->getAllActive();
$settingModel = new Setting();
$equipmentsJson = $settingModel->get('equipment_types');
$equipments = $equipmentsJson ? json_decode($equipmentsJson, true) : [
['id' => 'wheelchair', 'name' => 'รถนั่ง (Wheelchair)', 'icon' => 'fa-wheelchair'],
['id' => 'stretcher', 'name' => 'รถนอน (Stretcher)', 'icon' => 'fa-bed'],
['id' => 'bed', 'name' => 'เตียง (Bed)', 'icon' => 'fa-bed-pulse']
];
$prioritiesJson = $settingModel->get('job_priorities');
$priorities = $prioritiesJson ? json_decode($prioritiesJson, true) : [
['id' => 'normal', 'name' => 'ทั่วไป (Normal)', 'color' => 'blue'],
['id' => 'urgent', 'name' => 'ด่วน (Urgency)', 'color' => 'amber'],
['id' => 'emergency', 'name' => 'ฉุกเฉิน (Emergency)', 'color' => 'red']
];
// Get fixed origin from URL
$fixed_dept_id = isset($_GET['dept_id']) ? (int)$_GET['dept_id'] : 0;
$fixed_dept = null;
if ($fixed_dept_id > 0) {
foreach ($departments as $dept) {
if ($dept['id'] == $fixed_dept_id) {
$fixed_dept = $dept;
break;
}
}
}
?>
<div class="max-w-3xl mx-auto py-8">
<div class="mb-6 text-center">
<div class="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center text-3xl mx-auto mb-4">
<i class="fa-solid fa-qrcode"></i>
</div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white mb-2">เรียกรถเข็น / เวรเปลด่วน</h2>
<?php if ($fixed_dept): ?>
<p class="text-lg text-emerald-600 dark:text-emerald-400 font-medium">📍 จุดรับผู้ป่วย: <?= escape($fixed_dept['name']) ?></p>
<?php else: ?>
<p class="text-red-500 font-medium">⚠️ ไม่พบจุดรับผู้ป่วย กรุณาสแกน QR Code ใหม่อีกครั้ง</p>
<?php endif; ?>
</div>
<?php if ($fixed_dept): ?>
<div class="glass-card p-6 md:p-8">
<form id="publicRequestForm">
<input type="hidden" name="action" value="create_public_job">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<input type="hidden" name="from_department_id" value="<?= $fixed_dept['id'] ?>">
<h4 class="text-lg font-semibold text-emerald-600 dark:text-emerald-400 mb-4 border-b border-emerald-100 dark:border-emerald-900 pb-2">1. ข้อมูลผู้ป่วย</h4>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">HN (ถ้าทราบ)</label>
<input type="text" class="glass-input w-full px-4 py-3" name="patient_hn" placeholder="ระบุ HN">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อ-สกุลผู้ป่วย</label>
<input type="text" class="glass-input w-full px-4 py-3" name="patient_name" id="patient_name" placeholder="ชื่อ นามสกุล">
</div>
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">หรือถ่ายรูปเพื่อความรวดเร็วในการระบุตัวตน</label>
<input type="file" name="patient_image" id="patient_image" accept="image/*" capture="environment" class="glass-input w-full px-4 py-2 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-emerald-50 file:text-emerald-700 hover:file:bg-emerald-100">
<p class="text-xs text-slate-500 mt-1">* กรุณากรอกชื่อ หรือ ถ่ายรูปอย่างใดอย่างหนึ่ง</p>
</div>
</div>
<h4 class="text-lg font-semibold text-emerald-600 dark:text-emerald-400 mb-4 border-b border-emerald-100 dark:border-emerald-900 pb-2">2. ปลายทางและอุปกรณ์</h4>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">จุดส่งผู้ป่วย <span class="text-red-500">*</span></label>
<select class="glass-input w-full px-4 py-3 text-lg" name="to_department_id" id="to_department_id">
<option value="">-- เลือกจุดส่ง (ปลายทาง) --</option>
<?php foreach($departments as $dept): ?>
<?php if($dept['id'] != $fixed_dept['id']): ?>
<option value="<?= $dept['id'] ?>"><?= escape($dept['name']) ?></option>
<?php endif; ?>
<?php endforeach; ?>
<option value="custom">อื่นๆ (พิมพ์ระบุเอง)</option>
</select>
<div id="custom_to_department_div" class="hidden mt-3">
<input type="text" name="to_department_custom" id="to_department_custom" class="glass-input w-full px-4 py-3" placeholder="โปรดระบุจุดปลายทาง...">
</div>
<div class="mt-3 flex items-center bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg border border-blue-100 dark:border-blue-800">
<input type="checkbox" id="is_round_trip" name="is_round_trip" value="1" class="w-5 h-5 text-blue-600 rounded border-blue-300 focus:ring-blue-500 cursor-pointer">
<label for="is_round_trip" class="ml-3 text-base font-medium text-blue-800 dark:text-blue-300 cursor-pointer">
<i class="fa-solid fa-rotate-left me-1"></i> ไป-กลับ (รอรับกลับทันที)
</label>
</div>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2">ประเภทอุปกรณ์ <span class="text-red-500">*</span></label>
<div class="grid grid-cols-1 gap-3">
<?php foreach($equipments as $index => $eq): ?>
<label class="relative flex items-center p-4 border border-slate-200 dark:border-slate-700 rounded-xl cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors">
<input type="radio" name="equipment_type" value="<?= escape($eq['id']) ?>" class="w-5 h-5 text-emerald-600 border-gray-300 focus:ring-emerald-500 dark:bg-slate-700 dark:border-slate-600" <?= $index === 0 ? 'required checked' : '' ?>>
<span class="ml-3 flex flex-col">
<span class="text-base font-medium text-slate-900 dark:text-white flex items-center gap-2">
<i class="fa-solid <?= escape($eq['icon']) ?> text-emerald-500 text-xl w-6"></i> <?= escape($eq['name']) ?>
</span>
</span>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="mb-5">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ระดับความฉุกเฉิน</label>
<select class="glass-input w-full px-4 py-3" name="priority" required>
<?php foreach($priorities as $index => $pri): ?>
<option value="<?= escape($pri['id']) ?>" class="text-<?= escape($pri['color']) ?>-600 font-medium" <?= $index === 0 ? 'selected' : '' ?>>
<?= escape($pri['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-5 flex items-center gap-2 bg-red-50 dark:bg-red-900/20 p-4 rounded-xl border border-red-200 dark:border-red-800">
<input type="checkbox" id="infection_control" name="infection_control" value="1" class="w-5 h-5 text-red-600 rounded border-red-300 focus:ring-red-500">
<label for="infection_control" class="text-base font-medium text-red-800 dark:text-red-300">
<i class="fa-solid fa-biohazard me-1"></i> ผู้ป่วยโรคติดต่อ / แยกกักโรค
</label>
</div>
<div class="mb-8">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">หมายเหตุ (ถ้ามี)</label>
<textarea class="glass-input w-full px-4 py-3" name="notes" rows="2" placeholder="เช่น ผู้ป่วยให้ออกซิเจน, ระวังสายน้ำเกลือ"></textarea>
</div>
<div class="pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="submit" class="btn-primary-glass w-full py-4 text-lg font-bold flex items-center justify-center">
<span class="text"><i class="fa-solid fa-paper-plane me-2"></i> ยืนยันการเรียกเวรเปล</span>
</button>
</div>
</form>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
$('#to_department_id').change(function() {
if ($(this).val() === 'custom') {
$('#custom_to_department_div').removeClass('hidden');
$('#to_department_custom').prop('required', true);
} else {
$('#custom_to_department_div').addClass('hidden');
$('#to_department_custom').prop('required', false).val('');
}
});
$('#publicRequestForm').submit(function(e) {
e.preventDefault();
// Validate at least one of name or image
const patientName = $('#patient_name').val().trim();
const fileInput = document.getElementById('patient_image');
const file = fileInput.files[0];
if (!patientName && !file) {
Swal.fire('ข้อมูลไม่ครบถ้วน', 'กรุณากรอกชื่อ-สกุลผู้ป่วย หรือถ่ายรูปอย่างใดอย่างหนึ่ง', 'warning');
return;
}
const toDept = $('#to_department_id').val();
if (!toDept) {
Swal.fire('ข้อมูลไม่ครบถ้วน', 'กรุณาเลือกหรือระบุจุดส่งปลายทาง', 'warning');
return;
}
const btn = $(this).find('button[type="submit"]');
const originalText = btn.html();
btn.addClass('btn-loading');
// Use FormData for file upload
const formData = new FormData(this);
// Client-side Image Compression function
const compressImage = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = event => {
const img = new Image();
img.src = event.target.result;
img.onload = () => {
const canvas = document.createElement('canvas');
const MAX_WIDTH = 800;
const MAX_HEIGHT = 800;
let width = img.width;
let height = img.height;
if (width > height) {
if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width;
width = MAX_WIDTH;
}
} else {
if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height;
height = MAX_HEIGHT;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Handle rotation if needed (simplified) and draw image
// Draw white background for transparent images
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob((blob) => {
if(blob) {
resolve(new File([blob], 'compressed.jpg', { type: 'image/jpeg' }));
} else {
reject(new Error('Canvas to Blob failed'));
}
}, 'image/jpeg', 0.7); // 70% quality
};
img.onerror = error => reject(error);
};
reader.onerror = error => reject(error);
});
};
const submitForm = () => {
$.ajax({
url: 'api/jobs.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(response) {
if (response.status === 'success') {
Swal.fire({
icon: 'success',
title: 'เรียกเวรเปลสำเร็จ',
text: 'ระบบได้รับข้อมูลของคุณแล้ว หมายเลขงาน: ' + response.job_number,
confirmButtonText: 'ตกลง',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then(() => {
window.location.reload();
});
} else {
btn.removeClass('btn-loading').html(originalText);
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function(xhr) {
btn.removeClass('btn-loading').html(originalText);
let errorMsg = 'เกิดข้อผิดพลาดในการเชื่อมต่อ';
try {
let res = JSON.parse(xhr.responseText);
if (res.message) errorMsg = res.message;
} catch(e) {}
Swal.fire('ผิดพลาด', errorMsg, 'error');
}
});
};
if (file) {
compressImage(file).then(compressedFile => {
formData.set('patient_image', compressedFile);
submitForm();
}).catch(err => {
console.error('Image compression failed', err);
submitForm(); // proceed without compression if failed
});
} else {
submitForm();
}
});
});
</script>
@@ -0,0 +1,898 @@
<?php
// views/settings/index.php
require_once dirname(__DIR__, 2) . '/models/Setting.php';
require_once dirname(__DIR__, 2) . '/models/Department.php';
$settingModel = new Setting();
$deptModel = new Department();
$flatSettings = $settingModel->getAllFlat();
// Handle Department Actions
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'department') {
if (ob_get_length()) ob_clean();
header('Content-Type: application/json');
try {
if ($_POST['sub_action'] === 'add') {
$deptModel->add($_POST['name']);
echo json_encode(['status' => 'success', 'message' => 'เพิ่มจุดรับ-ส่งเรียบร้อยแล้ว']);
} elseif ($_POST['sub_action'] === 'edit') {
$deptModel->update($_POST['id'], $_POST['name'], $_POST['status']);
echo json_encode(['status' => 'success', 'message' => 'แก้ไขจุดรับ-ส่งเรียบร้อยแล้ว']);
} elseif ($_POST['sub_action'] === 'delete') {
$deptModel->delete($_POST['id']);
echo json_encode(['status' => 'success', 'message' => 'ลบจุดรับ-ส่งเรียบร้อยแล้ว']);
}
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'save_settings') {
if (ob_get_length()) ob_clean(); // Clear any HTML output buffered by index.php
header('Content-Type: application/json');
try {
if (isset($_POST['settings']) && is_array($_POST['settings'])) {
foreach ($_POST['settings'] as $key => $value) {
$settingModel->set($key, $value);
}
}
echo json_encode(['status' => 'success', 'message' => 'บันทึกการตั้งค่าเรียบร้อยแล้ว']);
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
exit;
}
?>
<div class="mb-8">
<h2 class="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-emerald-600 to-teal-400 dark:from-emerald-400 dark:to-teal-200 mb-2">ตั้งค่าระบบ (System Settings)</h2>
<p class="text-slate-500 dark:text-slate-400 text-lg">จัดการข้อมูลหน่วยงาน การแจ้งเตือน และการจ่ายงาน</p>
</div>
<?php
// Prepare SweetAlert message if saved
$swal_msg = '';
if (isset($_GET['saved'])) {
if ($_GET['saved'] === 'dept_add') $swal_msg = 'เพิ่มจุดรับ-ส่งเรียบร้อยแล้ว';
elseif ($_GET['saved'] === 'dept_edit') $swal_msg = 'แก้ไขจุดรับ-ส่งเรียบร้อยแล้ว';
elseif ($_GET['saved'] === 'dept_delete') $swal_msg = 'ลบจุดรับ-ส่งเรียบร้อยแล้ว';
else $swal_msg = 'บันทึกการตั้งค่าเรียบร้อยแล้ว';
}
?>
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6 pb-10">
<!-- Sidebar Navigation -->
<div class="lg:col-span-1">
<div class="glass-card sticky top-24 overflow-hidden border border-white/40 dark:border-slate-700/50 shadow-xl shadow-emerald-900/5 rounded-2xl">
<nav class="flex flex-col text-sm font-medium p-2 space-y-1" id="settingsTabs">
<a href="#tab-general" class="tab-link px-4 py-3 rounded-xl text-emerald-700 dark:text-emerald-300 bg-emerald-100/50 dark:bg-emerald-900/30 font-bold transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-emerald-100 dark:bg-emerald-800/50 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-hospital text-emerald-600 dark:text-emerald-400"></i>
</div>
ข้อมูลหน่วยงาน
</a>
<a href="#tab-line" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-brands fa-line text-green-500"></i>
</div>
LINE Notify / Bot
</a>
<a href="#tab-telegram" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-brands fa-telegram text-blue-500"></i>
</div>
Telegram Bot
</a>
<a href="#tab-dispatch" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-robot text-purple-500"></i>
</div>
ระบบจ่ายงานอัตโนมัติ
</a>
<a href="#tab-equipments" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-wheelchair text-emerald-500"></i>
</div>
ประเภทอุปกรณ์
</a>
<a href="#tab-priorities" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-triangle-exclamation text-red-500"></i>
</div>
ระดับความฉุกเฉิน
</a>
<a href="#tab-points" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-map-location-dot text-rose-500"></i>
</div>
จุดรับ-ส่ง (จุดบริการ)
</a>
<a href="#tab-maps" class="tab-link px-4 py-3 rounded-xl text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800/50 hover:text-slate-900 dark:hover:text-slate-200 transition-all flex items-center group">
<div class="w-8 h-8 rounded-lg bg-slate-100 dark:bg-slate-800 flex items-center justify-center mr-3 group-hover:scale-110 transition-transform">
<i class="fa-solid fa-map text-amber-500"></i>
</div>
แผนผังอาคาร (Floor Plans)
</a>
</nav>
</div>
</div>
<!-- Settings Content -->
<div class="lg:col-span-3 relative min-h-[500px]">
<form id="mainSettingsForm">
<input type="hidden" name="action" value="save_settings">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<!-- General Settings -->
<div id="tab-general" class="glass-card p-6 tab-content">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center">
<i class="fa-solid fa-hospital text-emerald-500 mr-2"></i> ข้อมูลหน่วยงาน
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อโรงพยาบาล</label>
<input type="text" class="glass-input w-full px-4 py-2" name="settings[hospital_name]" value="<?= escape($settings['general']['hospital_name'] ?? 'โรงพยาบาลเกาะสมุย') ?>">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อหน่วยงาน</label>
<input type="text" class="glass-input w-full px-4 py-2" name="settings[department_name]" value="<?= escape($settings['general']['department_name'] ?? 'ศูนย์เปล (Patient Transport)') ?>">
</div>
</div>
</div>
<!-- LINE Settings -->
<div id="tab-line" class="glass-card p-6 tab-content hidden">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center">
<i class="fa-brands fa-line text-green-500 mr-2"></i> LINE Notify / Bot
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">LINE Notify Token</label>
<div class="relative">
<input type="password" class="glass-input w-full px-4 py-2 pr-10" name="settings[line_notify_token]" value="<?= escape($flatSettings['line_notify_token'] ?? '') ?>">
<button type="button" class="absolute inset-y-0 right-0 px-3 flex items-center text-slate-400 hover:text-slate-600 toggle-password">
<i class="fa-solid fa-eye"></i>
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">LINE Group ID</label>
<input type="text" class="glass-input w-full px-4 py-2" name="settings[line_group_id]" value="<?= escape($flatSettings['line_group_id'] ?? '') ?>">
</div>
</div>
</div>
<!-- Telegram Settings -->
<div id="tab-telegram" class="glass-card p-6 tab-content hidden">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center">
<i class="fa-brands fa-telegram text-blue-500 mr-2"></i> Telegram Bot
</h3>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Telegram Bot Token</label>
<div class="relative">
<input type="password" class="glass-input w-full px-4 py-2 pr-10" name="settings[telegram_bot_token]" value="<?= escape($flatSettings['telegram_bot_token'] ?? '') ?>">
<button type="button" class="absolute inset-y-0 right-0 px-3 flex items-center text-slate-400 hover:text-slate-600 toggle-password">
<i class="fa-solid fa-eye"></i>
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">Telegram Chat ID</label>
<input type="text" class="glass-input w-full px-4 py-2" name="settings[telegram_chat_id]" value="<?= escape($flatSettings['telegram_chat_id'] ?? '') ?>">
</div>
</div>
</div>
<!-- Dispatch Settings -->
<div id="tab-dispatch" class="glass-card p-6 tab-content hidden">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center">
<i class="fa-solid fa-robot text-purple-500 mr-2"></i> ระบบจ่ายงานอัตโนมัติ
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between p-4 bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700">
<div>
<p class="font-medium text-slate-800 dark:text-white">เปิดใช้งาน Smart Auto Dispatch</p>
<p class="text-sm text-slate-500">ระบบจะค้นหาและจ่ายงานให้เจ้าหน้าที่ที่ว่างและอยู่ใกล้ที่สุดอัตโนมัติ</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="hidden" name="settings[auto_dispatch_enabled]" value="0">
<input type="checkbox" name="settings[auto_dispatch_enabled]" value="1" class="sr-only peer" <?= (isset($flatSettings['auto_dispatch_enabled']) && $flatSettings['auto_dispatch_enabled'] == '1') ? 'checked' : '' ?>>
<div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer dark:bg-slate-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-emerald-500"></div>
</label>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">เวลามาตรฐาน SLA สำหรับงานส่งผู้ป่วย (นาที)</label>
<input type="number" class="glass-input w-full px-4 py-2" name="settings[sla_minutes_default]" value="<?= escape($flatSettings['sla_minutes_default'] ?? '15') ?>">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">น้ำหนักระยะทาง (กม.) ในการพิจารณา</label>
<p class="text-xs text-slate-500 mb-2">ระยะทางสูงสุดที่จะนำมาคำนวณการจ่ายงานอัตโนมัติ</p>
<input type="number" step="0.1" class="glass-input w-full px-4 py-2" name="settings[dispatch_max_distance]" value="<?= escape($flatSettings['dispatch_max_distance'] ?? '2.0') ?>">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ความสำคัญของระยะทาง (คะแนน)</label>
<p class="text-xs text-slate-500 mb-2">คะแนนเพิ่มสำหรับเจ้าหน้าที่ที่อยู่ใกล้ที่สุด (0-100)</p>
<input type="number" class="glass-input w-full px-4 py-2" name="settings[dispatch_distance_weight]" value="<?= escape($flatSettings['dispatch_distance_weight'] ?? '50') ?>">
</div>
</div>
</div>
<!-- Equipments Settings -->
<div id="tab-equipments" class="glass-card p-6 tab-content hidden">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center justify-between">
<span><i class="fa-solid fa-wheelchair text-emerald-500 mr-2"></i> ประเภทอุปกรณ์</span>
<button type="button" class="btn-secondary-glass py-1 px-3 text-sm" onclick="addEquipmentItem()">
<i class="fa-solid fa-plus me-1"></i> เพิ่มอุปกรณ์
</button>
</h3>
<div class="space-y-4">
<p class="text-sm text-slate-500 mb-2">กำหนดประเภทอุปกรณ์ที่สามารถเลือกได้ในหน้าแบบฟอร์มร้องของาน</p>
<input type="hidden" name="settings[equipment_types]" id="equipmentTypesJson">
<div id="equipmentList" class="space-y-3">
<!-- Dynamic content via JS -->
</div>
</div>
</div>
<!-- Priorities Settings -->
<div id="tab-priorities" class="glass-card p-6 tab-content hidden">
<h3 class="text-lg font-bold text-slate-800 dark:text-white mb-4 flex items-center justify-between">
<span><i class="fa-solid fa-triangle-exclamation text-red-500 mr-2"></i> ระดับความฉุกเฉิน</span>
<button type="button" class="btn-secondary-glass py-1 px-3 text-sm" onclick="addPriorityItem()">
<i class="fa-solid fa-plus me-1"></i> เพิ่มความฉุกเฉิน
</button>
</h3>
<div class="space-y-4">
<p class="text-sm text-slate-500 mb-2">กำหนดระดับความฉุกเฉินและสีที่จะใช้แสดงผล</p>
<input type="hidden" name="settings[job_priorities]" id="jobPrioritiesJson">
<div id="priorityList" class="space-y-3">
<!-- Dynamic content via JS -->
</div>
</div>
</div>
<!-- Save Button Area -->
<div class="sticky bottom-4 z-10 p-4 mt-6 flex justify-end bg-white/70 dark:bg-slate-900/70 backdrop-blur-md rounded-2xl border border-emerald-500/20 shadow-[0_8px_30px_rgb(0,0,0,0.12)]">
<button type="submit" class="btn-primary-glass py-2 px-8 font-medium text-lg rounded-xl flex items-center hover:scale-105 transition-transform duration-200">
<i class="fa-solid fa-save me-2"></i> บันทึกการตั้งค่า
</button>
</div>
</form>
<!-- Tab Content for Points (Outside the main form so it can have its own forms) -->
<div id="tab-points" class="tab-content hidden animate-[fadeUp_0.3s_ease-out]">
<div class="glass-card p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-slate-800 dark:text-white flex items-center">
<i class="fa-solid fa-map-location-dot text-rose-500 mr-2"></i> จุดรับ-ส่ง (จุดบริการ)
</h3>
<button type="button" onclick="openAddPointModal()" class="btn-primary-glass py-1 px-4 text-sm font-medium">
<i class="fa-solid fa-plus me-1"></i> เพิ่มจุดรับ-ส่ง
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider border-b border-slate-200 dark:border-slate-700">
<th class="pb-3 px-4">ชื่อจุดบริการ</th>
<th class="pb-3 px-4 text-center">สถานะ</th>
<th class="pb-3 px-4 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="text-sm text-slate-700 dark:text-slate-300 divide-y divide-slate-200 dark:divide-slate-700">
<?php
$departments = $deptModel->getAll();
foreach ($departments as $dept):
?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="py-3 px-4 font-medium"><?= escape($dept['name']) ?></td>
<td class="py-3 px-4 text-center">
<?php if ($dept['status'] === 'active'): ?>
<span class="bg-green-100 text-green-800 px-2 py-1 rounded-full text-xs font-medium">เปิดใช้งาน</span>
<?php else: ?>
<span class="bg-slate-100 text-slate-800 px-2 py-1 rounded-full text-xs font-medium">ปิดใช้งาน</span>
<?php endif; ?>
</td>
<td class="py-3 px-4 text-right">
<a href="?page=public_request&dept_id=<?= $dept['id'] ?>" target="_blank" class="text-emerald-500 hover:text-emerald-700 mr-3" title="ดู/พิมพ์ QR Code (หน้าสแกนเรียกเวรเปล)">
<i class="fa-solid fa-qrcode"></i>
</a>
<button type="button" onclick="openEditPointModal(<?= $dept['id'] ?>, '<?= escape(addslashes($dept['name'])) ?>', '<?= $dept['status'] ?>')" class="text-blue-500 hover:text-blue-700 mr-3" title="แก้ไข">
<i class="fa-solid fa-edit"></i>
</button>
<form class="inline dept-delete-form">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<input type="hidden" name="action" value="department">
<input type="hidden" name="sub_action" value="delete">
<input type="hidden" name="id" value="<?= $dept['id'] ?>">
<button type="submit" class="text-red-500 hover:text-red-700"><i class="fa-solid fa-trash"></i></button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<!-- Add/Edit Point Modal -->
<div id="pointModal" class="fixed inset-0 z-50 hidden bg-slate-900/50 backdrop-blur-sm flex items-center justify-center">
<div class="glass-card w-full max-w-md p-6 transform transition-all">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-slate-800 dark:text-white" id="pointModalTitle">เพิ่มจุดรับ-ส่ง</h3>
<button type="button" onclick="closePointModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<i class="fa-solid fa-times fa-lg"></i>
</button>
</div>
<form id="deptModalForm">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<input type="hidden" name="action" value="department">
<input type="hidden" name="sub_action" id="pointSubAction" value="add">
<input type="hidden" name="id" id="pointId" value="">
<div class="space-y-4 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อจุดบริการ <span class="text-red-500">*</span></label>
<input type="text" id="pointName" name="name" class="glass-input w-full px-4 py-2" required>
</div>
<div id="statusContainer" class="hidden">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">สถานะ</label>
<select id="pointStatus" name="status" class="glass-input w-full px-4 py-2">
<option value="active">เปิดใช้งาน</option>
<option value="inactive">ปิดใช้งาน</option>
</select>
</div>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="closePointModal()" class="btn-secondary-glass py-2 px-4">ยกเลิก</button>
<button type="submit" class="btn-primary-glass py-2 px-6">บันทึก</button>
</div>
</form>
</div>
</div>
<!-- Tab Content for Maps -->
<div id="tab-maps" class="tab-content hidden animate-[fadeUp_0.3s_ease-out]">
<div class="glass-card p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-bold text-slate-800 dark:text-white flex items-center">
<i class="fa-solid fa-map text-amber-500 mr-2"></i> แผนผังอาคาร
</h3>
<button type="button" onclick="openMapUploadModal()" class="btn-primary-glass py-1 px-4 text-sm font-medium">
<i class="fa-solid fa-plus me-1"></i> เพิ่มแผนผัง
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6" id="mapsGrid">
<!-- Dynamic Map Cards -->
<div class="col-span-full text-center text-slate-500 py-8">กำลังโหลดข้อมูลแผนผัง...</div>
</div>
</div>
</div>
<!-- Map Upload Modal -->
<div id="mapUploadModal" class="fixed inset-0 z-50 hidden bg-slate-900/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="glass-card w-full max-w-md p-6 transform transition-all">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold text-slate-800 dark:text-white">เพิ่มแผนผังอาคาร</h3>
<button type="button" onclick="closeMapUploadModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<i class="fa-solid fa-times fa-lg"></i>
</button>
</div>
<form id="mapUploadForm">
<input type="hidden" name="action" value="upload">
<div class="space-y-4 mb-6">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อแผนผัง <span class="text-red-500">*</span></label>
<input type="text" name="name" class="glass-input w-full px-4 py-2" required placeholder="เช่น ชั้น 1 อาคารผู้ป่วยนอก">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ตึก/อาคาร <span class="text-red-500">*</span></label>
<input type="text" name="building" class="glass-input w-full px-4 py-2" required>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชั้น <span class="text-red-500">*</span></label>
<input type="text" name="floor" class="glass-input w-full px-4 py-2" required>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ตำแหน่งศูนย์เปล/จุดพักคอย (ถ้ามี)</label>
<input type="text" name="transport_center_location" class="glass-input w-full px-4 py-2" placeholder="เช่น บริเวณหน้าลิฟต์ฝั่งตะวันออก">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ไฟล์รูปภาพแผนผัง <span class="text-red-500">*</span></label>
<input type="file" name="map_image" class="block w-full text-sm text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-emerald-50 file:text-emerald-700 hover:file:bg-emerald-100" accept="image/jpeg, image/png, image/webp" required>
</div>
</div>
<div class="flex justify-end gap-3">
<button type="button" onclick="closeMapUploadModal()" class="btn-secondary-glass py-2 px-4">ยกเลิก</button>
<button type="submit" class="btn-primary-glass py-2 px-6">อัปโหลด</button>
</div>
</form>
</div>
</div>
<script>
// Data from server
let equipmentTypesData = <?= isset($settings['general']['equipment_types']) && !empty($settings['general']['equipment_types']) ? $settings['general']['equipment_types'] : '[{"id":"wheelchair","name":"รถนั่ง (Wheelchair)","icon":"fa-wheelchair"},{"id":"stretcher","name":"รถนอน (Stretcher)","icon":"fa-bed"},{"id":"bed","name":"เตียง (Bed)","icon":"fa-bed-pulse"}]' ?>;
let jobPrioritiesData = <?= isset($settings['general']['job_priorities']) && !empty($settings['general']['job_priorities']) ? $settings['general']['job_priorities'] : '[{"id":"normal","name":"ทั่วไป (Normal)","color":"blue"},{"id":"urgent","name":"ด่วน (Urgency)","color":"amber"},{"id":"emergency","name":"ฉุกเฉิน (Emergency)","color":"red"}]' ?>;
// Ensure data is array if it's string
if (typeof equipmentTypesData === 'string') {
try { equipmentTypesData = JSON.parse(equipmentTypesData); } catch (e) { equipmentTypesData = []; }
}
if (typeof jobPrioritiesData === 'string') {
try { jobPrioritiesData = JSON.parse(jobPrioritiesData); } catch (e) { jobPrioritiesData = []; }
}
// Equipment Logic
function renderEquipments() {
const container = document.getElementById('equipmentList');
container.innerHTML = '';
equipmentTypesData.forEach((item, index) => {
const html = `
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="w-10 text-center text-slate-400"><i class="fa-solid ${item.icon} text-lg"></i></div>
<div class="flex-1 grid grid-cols-3 gap-3">
<input type="text" class="glass-input px-3 py-1 text-sm" placeholder="ID (เช่น wheelchair)" value="${item.id}" onchange="updateEquipment(${index}, 'id', this.value)">
<input type="text" class="glass-input px-3 py-1 text-sm" placeholder="ชื่อ (เช่น รถนั่ง)" value="${item.name}" onchange="updateEquipment(${index}, 'name', this.value)">
<input type="text" class="glass-input px-3 py-1 text-sm" placeholder="Icon (เช่น fa-wheelchair)" value="${item.icon}" onchange="updateEquipment(${index}, 'icon', this.value)">
</div>
<button type="button" class="text-red-500 hover:text-red-700 p-2" onclick="removeEquipment(${index})"><i class="fa-solid fa-trash"></i></button>
</div>`;
container.insertAdjacentHTML('beforeend', html);
});
document.getElementById('equipmentTypesJson').value = JSON.stringify(equipmentTypesData);
}
window.addEquipmentItem = function() {
equipmentTypesData.push({id: '', name: '', icon: 'fa-box'});
renderEquipments();
};
window.removeEquipment = function(index) {
equipmentTypesData.splice(index, 1);
renderEquipments();
};
window.updateEquipment = function(index, field, value) {
equipmentTypesData[index][field] = value;
document.getElementById('equipmentTypesJson').value = JSON.stringify(equipmentTypesData);
if(field === 'icon') renderEquipments(); // re-render to update icon preview
};
// Priority Logic
function renderPriorities() {
const container = document.getElementById('priorityList');
container.innerHTML = '';
jobPrioritiesData.forEach((item, index) => {
const html = `
<div class="flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700">
<div class="w-10 text-center text-${item.color}-500"><i class="fa-solid fa-circle text-lg"></i></div>
<div class="flex-1 grid grid-cols-3 gap-3">
<input type="text" class="glass-input px-3 py-1 text-sm" placeholder="ID (เช่น normal)" value="${item.id}" onchange="updatePriority(${index}, 'id', this.value)">
<input type="text" class="glass-input px-3 py-1 text-sm" placeholder="ชื่อ (เช่น ทั่วไป)" value="${item.name}" onchange="updatePriority(${index}, 'name', this.value)">
<select class="glass-input px-3 py-1 text-sm" onchange="updatePriority(${index}, 'color', this.value)">
<option value="blue" ${item.color==='blue'?'selected':''}>Blue (ฟ้า)</option>
<option value="emerald" ${item.color==='emerald'?'selected':''}>Emerald (เขียว)</option>
<option value="amber" ${item.color==='amber'?'selected':''}>Amber (เหลือง)</option>
<option value="orange" ${item.color==='orange'?'selected':''}>Orange (ส้ม)</option>
<option value="red" ${item.color==='red'?'selected':''}>Red (แดง)</option>
<option value="purple" ${item.color==='purple'?'selected':''}>Purple (ม่วง)</option>
<option value="slate" ${item.color==='slate'?'selected':''}>Slate (เทา)</option>
</select>
</div>
<button type="button" class="text-red-500 hover:text-red-700 p-2" onclick="removePriority(${index})"><i class="fa-solid fa-trash"></i></button>
</div>`;
container.insertAdjacentHTML('beforeend', html);
});
document.getElementById('jobPrioritiesJson').value = JSON.stringify(jobPrioritiesData);
}
window.addPriorityItem = function() {
jobPrioritiesData.push({id: '', name: '', color: 'blue'});
renderPriorities();
};
window.removePriority = function(index) {
jobPrioritiesData.splice(index, 1);
renderPriorities();
};
window.updatePriority = function(index, field, value) {
jobPrioritiesData[index][field] = value;
document.getElementById('jobPrioritiesJson').value = JSON.stringify(jobPrioritiesData);
if(field === 'color') renderPriorities(); // re-render to update color preview
};
// Initialize
renderEquipments();
renderPriorities();
document.addEventListener('DOMContentLoaded', function() {
// Password Toggle
document.querySelectorAll('.toggle-password').forEach(btn => {
btn.addEventListener('click', function() {
const input = this.previousElementSibling;
const icon = this.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
input.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
});
});
// Tab Switching Logic
const tabLinks = document.querySelectorAll('.tab-link');
const tabContents = document.querySelectorAll('.tab-content');
tabLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
// Remove active classes from all links
tabLinks.forEach(l => {
l.classList.remove('text-emerald-700', 'dark:text-emerald-300', 'bg-emerald-100/50', 'dark:bg-emerald-900/30', 'font-bold');
l.classList.add('text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-50', 'dark:hover:bg-slate-800/50', 'hover:text-slate-900', 'dark:hover:text-slate-200');
// Change icon bg back
const iconBg = l.querySelector('.w-8.h-8');
if (iconBg) {
iconBg.classList.remove('bg-emerald-100', 'dark:bg-emerald-800/50');
iconBg.classList.add('bg-slate-100', 'dark:bg-slate-800');
}
});
// Add active class to clicked link
this.classList.remove('text-slate-600', 'dark:text-slate-400', 'hover:bg-slate-50', 'dark:hover:bg-slate-800/50', 'hover:text-slate-900', 'dark:hover:text-slate-200');
this.classList.add('text-emerald-700', 'dark:text-emerald-300', 'bg-emerald-100/50', 'dark:bg-emerald-900/30', 'font-bold');
// Change icon bg to active
const iconBg = this.querySelector('.w-8.h-8');
if (iconBg) {
iconBg.classList.remove('bg-slate-100', 'dark:bg-slate-800');
iconBg.classList.add('bg-emerald-100', 'dark:bg-emerald-800/50');
}
// Hide all tab contents
tabContents.forEach(content => {
content.classList.add('hidden');
});
// Show target tab content
const targetId = this.getAttribute('href');
const targetContent = document.querySelector(targetId);
if (targetContent) {
targetContent.classList.remove('hidden');
targetContent.classList.add('animate-[fadeUp_0.3s_ease-out]');
// Save active tab to localStorage
localStorage.setItem('activeSettingsTab', targetId);
// If it's the points tab or maps tab, hide the main form's submit button section
const mainSaveBtn = document.querySelector('#mainSettingsForm .sticky.bottom-4');
if (targetId === '#tab-points' || targetId === '#tab-maps') {
if(mainSaveBtn) mainSaveBtn.classList.add('hidden');
} else {
if(mainSaveBtn) mainSaveBtn.classList.remove('hidden');
}
}
});
});
// Handle AJAX Submission for Main Settings
$('#mainSettingsForm').on('submit', function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
btn.addClass('btn-loading');
$.ajax({
url: '?page=settings',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
btn.removeClass('btn-loading');
if (response.status === 'success') {
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: response.message,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b',
timer: 2000,
showConfirmButton: false
});
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
btn.removeClass('btn-loading');
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
}
});
});
// Handle AJAX Submission for Department Delete
$('.dept-delete-form').on('submit', function(e) {
e.preventDefault();
Swal.fire({
title: 'ยืนยันการลบ?',
text: "คุณต้องการลบจุดบริการนี้ใช่หรือไม่",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#94a3b8',
confirmButtonText: 'ลบ',
cancelButtonText: 'ยกเลิก',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: '?page=settings',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
if (response.status === 'success') {
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: response.message,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b',
timer: 1500,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
}
});
}
});
});
// Handle AJAX Submission for Department Modal (Add/Edit)
$('#deptModalForm').on('submit', function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
btn.addClass('btn-loading');
$.ajax({
url: '?page=settings',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
btn.removeClass('btn-loading');
if (response.status === 'success') {
closePointModal();
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: response.message,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b',
timer: 1500,
showConfirmButton: false
}).then(() => {
window.location.reload();
});
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
btn.removeClass('btn-loading');
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
}
});
});
// Restore active tab from localStorage
const savedTab = localStorage.getItem('activeSettingsTab');
if (savedTab) {
const linkToClick = document.querySelector(`.tab-link[href="${savedTab}"]`);
if (linkToClick) {
linkToClick.click();
}
}
// Trigger SweetAlert if saved
<?php if (!empty($swal_msg)): ?>
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: '<?= $swal_msg ?>',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b',
timer: 3000,
showConfirmButton: false
}).then(() => {
// Clean up URL to prevent alert on reload
window.history.replaceState(null, null, window.location.pathname + '?page=settings');
});
<?php endif; ?>
});
function openAddPointModal() {
document.getElementById('pointModalTitle').textContent = 'เพิ่มจุดรับ-ส่ง';
document.getElementById('pointSubAction').value = 'add';
document.getElementById('pointId').value = '';
document.getElementById('pointName').value = '';
document.getElementById('statusContainer').classList.add('hidden');
document.getElementById('pointModal').classList.remove('hidden');
}
function openEditPointModal(id, name, status) {
document.getElementById('pointModalTitle').textContent = 'แก้ไขจุดรับ-ส่ง';
document.getElementById('pointSubAction').value = 'edit';
document.getElementById('pointId').value = id;
document.getElementById('pointName').value = name;
document.getElementById('pointStatus').value = status;
document.getElementById('statusContainer').classList.remove('hidden');
document.getElementById('pointModal').classList.remove('hidden');
}
function closePointModal() {
document.getElementById('pointModal').classList.add('hidden');
}
// --- MAPS MANAGEMENT LOGIC ---
function loadMaps() {
fetch('api/maps.php?action=list')
.then(res => res.json())
.then(res => {
const grid = document.getElementById('mapsGrid');
if (res.status === 'success') {
if (res.data.length === 0) {
grid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8">ยังไม่มีข้อมูลแผนผังอาคาร</div>';
return;
}
let html = '';
res.data.forEach(map => {
html += `
<div class="bg-white dark:bg-slate-800 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden group">
<div class="h-40 bg-slate-100 dark:bg-slate-900 relative overflow-hidden">
<img src="${map.image_path}" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300">
<div class="absolute top-2 right-2 flex gap-1">
<span class="bg-white/90 dark:bg-slate-800/90 backdrop-blur-sm text-xs px-2 py-1 rounded-md font-bold text-slate-700 dark:text-slate-200 shadow-sm">
${map.building} ชั้น ${map.floor}
</span>
</div>
</div>
<div class="p-4">
<h4 class="font-bold text-slate-800 dark:text-white mb-1 truncate" title="${map.name}">${map.name}</h4>
<div class="text-xs text-slate-500 dark:text-slate-400 mb-3 flex items-start">
<i class="fa-solid fa-location-dot mt-0.5 text-rose-500 mr-1.5 w-3"></i>
<span class="line-clamp-2">${map.transport_center_location || '- ไม่ระบุจุดศูนย์เปล -'}</span>
</div>
<div class="flex justify-between items-center mt-4">
<span class="text-xs ${map.status === 'active' ? 'text-green-500 bg-green-50 dark:bg-green-900/20' : 'text-slate-500 bg-slate-100 dark:bg-slate-800'} px-2 py-1 rounded-full">
${map.status === 'active' ? 'ใช้งาน' : 'ไม่ใช้งาน'}
</span>
<button onclick="deleteMap(${map.id}, '${map.name}')" class="text-red-500 hover:text-red-700 bg-red-50 hover:bg-red-100 dark:bg-red-900/20 dark:hover:bg-red-900/40 w-8 h-8 rounded-full flex items-center justify-center transition-colors">
<i class="fa-solid fa-trash text-xs"></i>
</button>
</div>
</div>
</div>`;
});
grid.innerHTML = html;
}
});
}
function openMapUploadModal() {
document.getElementById('mapUploadForm').reset();
document.getElementById('mapUploadModal').classList.remove('hidden');
}
function closeMapUploadModal() {
document.getElementById('mapUploadModal').classList.add('hidden');
}
document.getElementById('mapUploadForm').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const btn = this.querySelector('button[type="submit"]');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin mr-2"></i> กำลังอัปโหลด...';
btn.disabled = true;
fetch('api/maps.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
btn.innerHTML = originalText;
btn.disabled = false;
if (data.status === 'success') {
Swal.fire({
icon: 'success',
title: 'อัปโหลดสำเร็จ',
showConfirmButton: false,
timer: 1500
});
closeMapUploadModal();
loadMaps();
} else {
Swal.fire('ข้อผิดพลาด', data.message, 'error');
}
}).catch(err => {
btn.innerHTML = originalText;
btn.disabled = false;
Swal.fire('ข้อผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
});
});
function deleteMap(id, name) {
Swal.fire({
title: 'ยืนยันการลบแผนผัง?',
text: `ต้องการลบแผนผัง "${name}" ใช่หรือไม่? (รูปภาพจะถูกลบด้วย)`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#94a3b8',
confirmButtonText: 'ลบข้อมูล',
cancelButtonText: 'ยกเลิก',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then((result) => {
if (result.isConfirmed) {
const formData = new FormData();
formData.append('action', 'delete');
formData.append('id', id);
fetch('api/maps.php', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
loadMaps();
Swal.fire({
title: 'ลบสำเร็จ!',
icon: 'success',
timer: 1500,
showConfirmButton: false,
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
});
} else {
Swal.fire('ข้อผิดพลาด', data.message, 'error');
}
});
}
});
}
// Initialize maps on load
document.addEventListener('DOMContentLoaded', function() {
if (window.location.hash === '#tab-maps') {
loadMaps();
}
// Also add click listener to map tab link to load data if not loaded
document.querySelector('a[href="#tab-maps"]').addEventListener('click', function() {
if(document.getElementById('mapsGrid').innerHTML.includes('กำลังโหลด')) {
loadMaps();
}
});
});
</script>
@@ -0,0 +1,109 @@
<?php
// views/tracking/map.php
?>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<div class="mb-4 flex flex-wrap gap-4 justify-between items-end">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">Live Tracking (พิกัดเจ้าหน้าที่)</h2>
<p class="text-slate-500 dark:text-slate-400">ติดตามตำแหน่งเจ้าหน้าที่แบบ Real-time</p>
</div>
<div class="flex items-center gap-2 bg-emerald-50 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400 px-3 py-1.5 rounded-lg border border-emerald-200 dark:border-emerald-800 text-sm">
<span class="relative flex h-2.5 w-2.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
</span>
กำลังอัปเดต (Live)
</div>
</div>
<div class="glass-card overflow-hidden h-[70vh] relative border-4 border-white/50 dark:border-slate-800/50">
<div id="map" class="w-full h-full"></div>
<!-- Floating Staff List Panel -->
<div class="absolute top-4 right-4 z-[400] w-64 glass-card bg-white/90 dark:bg-slate-900/90 shadow-xl max-h-[80%] flex flex-col">
<div class="px-4 py-3 border-b border-slate-200 dark:border-slate-700 font-medium text-sm flex justify-between items-center">
เจ้าหน้าที่ในพื้นที่
<span class="bg-emerald-100 text-emerald-800 dark:bg-emerald-900 text-emerald-300 text-xs px-2 py-0.5 rounded-full" id="staffCount">0</span>
</div>
<div class="overflow-y-auto p-2 space-y-1" id="mapStaffList">
<!-- Populated by JS -->
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Map centered at Koh Samui Hospital approx
const map = L.map('map').setView([9.537, 99.938], 16);
// Modern Map style depending on Dark Mode
const isDark = document.documentElement.classList.contains('dark');
const tileUrl = isDark
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
L.tileLayer(tileUrl, {
maxZoom: 19,
attribution: '© OpenStreetMap contributors © CARTO'
}).addTo(map);
const markers = {};
function updateStaffLocations() {
// Mock data
const staffData = [
{ id: 1, name: "นายสมชาย ใจดี", lat: 9.5375, lng: 99.9385, status: "available" },
{ id: 2, name: "นางสาวสมศรี มีสุข", lat: 9.5368, lng: 99.9372, status: "busy" }
];
document.getElementById('staffCount').textContent = staffData.length;
const listContainer = document.getElementById('mapStaffList');
listContainer.innerHTML = '';
staffData.forEach(staff => {
// Update List
const statusColor = staff.status === 'available' ? 'bg-emerald-500' : 'bg-yellow-500';
const statusText = staff.status === 'available' ? 'ว่าง' : 'ติดงาน';
listContainer.innerHTML += `
<div class="flex items-center gap-2 p-2 hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg cursor-pointer transition-colors text-sm" onclick="focusStaff(${staff.lat}, ${staff.lng})">
<div class="relative flex-shrink-0">
<img src="api/avatar.php?name=${encodeURIComponent(staff.name)}" class="w-6 h-6 rounded-full object-cover">
<span class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 border border-white dark:border-slate-800 rounded-full ${statusColor}"></span>
</div>
<span class="truncate text-slate-700 dark:text-slate-300 flex-1">${staff.name}</span>
</div>
`;
// Update Map Markers
const iconColor = staff.status === 'available' ? 'green' : 'orange';
const customIcon = L.divIcon({
className: 'custom-div-icon',
html: `<div class="w-8 h-8 rounded-full border-2 border-white shadow-lg bg-${iconColor}-500 overflow-hidden"><img src="api/avatar.php?name=${encodeURIComponent(staff.name)}" class="w-full h-full object-cover"></div>`,
iconSize: [32, 32],
iconAnchor: [16, 16]
});
if (markers[staff.id]) {
markers[staff.id].setLatLng([staff.lat, staff.lng]);
} else {
markers[staff.id] = L.marker([staff.lat, staff.lng], {icon: customIcon})
.bindPopup(`<b>${staff.name}</b><br>สถานะ: ${statusText}`)
.addTo(map);
}
});
}
window.focusStaff = function(lat, lng) {
map.flyTo([lat, lng], 18, {
animate: true,
duration: 1.5
});
};
updateStaffLocations();
setInterval(updateStaffLocations, 10000); // 10s auto refresh
});
</script>
@@ -0,0 +1,448 @@
<?php
// views/users/index.php
require_once dirname(__DIR__, 2) . '/models/User.php';
require_once dirname(__DIR__, 2) . '/models/Department.php';
// Check if user is admin
if (!isset($_SESSION['role']) || $_SESSION['role'] !== 'admin') {
echo '<div class="glass-card p-8 text-center text-red-500">
<i class="fa-solid fa-lock text-4xl mb-4"></i>
<h2 class="text-xl font-bold">Access Denied</h2>
<p>คุณไม่มีสิทธิ์เข้าถึงหน้านี้</p>
</div>';
return;
}
$userModel = new User();
$deptModel = new Department();
$users = $userModel->getAll();
$departments = $deptModel->getAll();
?>
<div class="mb-6 flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold text-slate-800 dark:text-white">ทะเบียนบุคลากร (User Management)</h2>
<p class="text-slate-500 dark:text-slate-400">จัดการข้อมูลผู้ใช้งานระบบ และเชื่อมต่อฐานข้อมูล HR</p>
</div>
<button onclick="openUserModal('add')" class="btn-primary-glass py-2 px-4 font-medium flex items-center">
<i class="fa-solid fa-user-plus me-2"></i> เพิ่มผู้ใช้งาน
</button>
</div>
<!-- Users Table -->
<div class="glass-card overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="text-xs font-semibold text-slate-500 dark:text-slate-400 uppercase tracking-wider border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50">
<th class="py-4 px-6">ชื่อ - สกุล</th>
<th class="py-4 px-6">ชื่อผู้ใช้ (Username)</th>
<th class="py-4 px-6 text-center">สิทธิ์การใช้งาน</th>
<th class="py-4 px-6">แผนก (จุดบริการ)</th>
<th class="py-4 px-6 text-center">สถานะ</th>
<th class="py-4 px-6 text-right">จัดการ</th>
</tr>
</thead>
<tbody class="text-sm text-slate-700 dark:text-slate-300 divide-y divide-slate-200 dark:divide-slate-700">
<?php foreach ($users as $u): ?>
<tr class="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors">
<td class="py-4 px-6 font-medium">
<div class="flex items-center gap-3">
<img src="api/avatar.php?username=<?= urlencode($u['username']) ?>&name=<?= urlencode($u['full_name']) ?>" class="rounded-full w-8 h-8 object-cover shadow-sm border border-slate-200 dark:border-slate-700">
<?= escape($u['full_name']) ?>
</div>
</td>
<td class="py-4 px-6"><?= escape($u['username']) ?></td>
<td class="py-4 px-6 text-center">
<?php
$roleClass = 'bg-slate-100 text-slate-800';
$roleName = 'ไม่ระบุ';
if ($u['role'] === 'admin') { $roleClass = 'bg-purple-100 text-purple-800'; $roleName = 'ผู้ดูแลระบบ'; }
if ($u['role'] === 'manager') { $roleClass = 'bg-blue-100 text-blue-800'; $roleName = 'หัวหน้างาน'; }
if ($u['role'] === 'nurse') { $roleClass = 'bg-pink-100 text-pink-800'; $roleName = 'พยาบาล'; }
if ($u['role'] === 'staff') { $roleClass = 'bg-emerald-100 text-emerald-800'; $roleName = 'เวรเปล'; }
?>
<span class="<?= $roleClass ?> px-2 py-1 rounded-full text-xs font-medium"><?= $roleName ?></span>
</td>
<td class="py-4 px-6 text-slate-500">
<?= escape($u['department_name'] ?? '-') ?>
</td>
<td class="py-4 px-6 text-center">
<?php if ($u['status'] === 'online'): ?>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> Online
</span>
<?php elseif ($u['status'] === 'busy'): ?>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
<span class="w-1.5 h-1.5 rounded-full bg-orange-500"></span> Busy
</span>
<?php else: ?>
<span class="inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-300">
<span class="w-1.5 h-1.5 rounded-full bg-slate-400"></span> Offline
</span>
<?php endif; ?>
</td>
<td class="py-4 px-6 text-right space-x-2">
<button onclick="openUserModal('edit', <?= htmlspecialchars(json_encode([
'id' => $u['id'],
'username' => $u['username'],
'full_name' => $u['full_name'],
'role' => $u['role'],
'department_id' => $u['department_id']
])) ?>)" class="text-blue-500 hover:text-blue-700 p-1" title="แก้ไขข้อมูล">
<i class="fa-solid fa-edit"></i>
</button>
<button onclick="openPasswordModal(<?= $u['id'] ?>, '<?= escape($u['username']) ?>')" class="text-amber-500 hover:text-amber-700 p-1" title="เปลี่ยนรหัสผ่าน">
<i class="fa-solid fa-key"></i>
</button>
<?php if ($u['id'] != $_SESSION['user_id']): ?>
<button onclick="deleteUser(<?= $u['id'] ?>)" class="text-red-500 hover:text-red-700 p-1" title="ลบผู้ใช้งาน">
<i class="fa-solid fa-trash"></i>
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<!-- User Add/Edit Modal -->
<div id="userModal" class="fixed inset-0 z-50 hidden bg-slate-900/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="glass-card w-full max-w-2xl transform transition-all max-h-[90vh] overflow-y-auto">
<div class="p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center sticky top-0 bg-white/80 dark:bg-slate-900/80 backdrop-blur z-10">
<h3 class="text-xl font-bold text-slate-800 dark:text-white" id="userModalTitle">เพิ่มผู้ใช้งานใหม่</h3>
<button type="button" onclick="closeUserModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<i class="fa-solid fa-times fa-lg"></i>
</button>
</div>
<div class="p-6 space-y-6">
<!-- HR Search Box (Only for Add) -->
<div id="hrSearchBox" class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl p-4">
<label class="block text-sm font-medium text-blue-800 dark:text-blue-300 mb-2">
<i class="fa-solid fa-magnifying-glass me-1"></i> ค้นหาจากฐานข้อมูล HR (HOSxP)
</label>
<div class="flex gap-2">
<input type="text" id="hrCid" class="glass-input flex-1 px-4 py-2 bg-white dark:bg-slate-800" placeholder="ระบุเลขบัตรประชาชน หรือ ชื่อ-สกุล">
<button type="button" onclick="searchHR()" id="btnSearchHR" class="btn-primary-glass px-4 py-2 whitespace-nowrap">
ค้นหา
</button>
</div>
<div id="hrResult" class="mt-3 text-sm hidden">
<!-- Results populated by JS -->
</div>
</div>
<form id="userForm">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<input type="hidden" name="action" id="userAction" value="add">
<input type="hidden" name="id" id="userId" value="">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Full Name -->
<div class="md:col-span-2">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อ - สกุล <span class="text-red-500">*</span></label>
<input type="text" name="full_name" id="userFullName" class="glass-input w-full px-4 py-2" required>
</div>
<!-- Username -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อผู้ใช้ (Username) <span class="text-red-500">*</span></label>
<input type="text" name="username" id="userUsername" class="glass-input w-full px-4 py-2" required>
<p class="text-xs text-slate-500 mt-1" id="usernameHint">แนะนำใช้เลขบัตรประชาชน หรือรหัสพนักงาน</p>
</div>
<!-- Password (Only for Add) -->
<div id="passwordField">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">รหัสผ่าน <span class="text-red-500">*</span></label>
<input type="password" name="password" id="userPassword" class="glass-input w-full px-4 py-2">
</div>
<!-- Role -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">สิทธิ์การใช้งาน <span class="text-red-500">*</span></label>
<select name="role" id="userRole" class="glass-input w-full px-4 py-2" required>
<option value="">-- เลือกสิทธิ์ --</option>
<option value="staff">เวรเปล (Staff)</option>
<option value="nurse">พยาบาล (Nurse)</option>
<option value="manager">หัวหน้างาน (Manager)</option>
<option value="admin">ผู้ดูแลระบบ (Admin)</option>
</select>
</div>
<!-- Department -->
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">แผนก / จุดบริการ (ระบบเปล)</label>
<select name="department_id" id="userDepartment" class="glass-input w-full px-4 py-2">
<option value="">-- ไม่ระบุ --</option>
<?php foreach ($departments as $d): ?>
<option value="<?= $d['id'] ?>"><?= escape($d['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mt-6 flex justify-end gap-3 pt-4 border-t border-slate-200 dark:border-slate-700">
<button type="button" onclick="closeUserModal()" class="btn-secondary-glass py-2 px-4">ยกเลิก</button>
<button type="submit" class="btn-primary-glass py-2 px-8">บันทึกข้อมูล</button>
</div>
</form>
</div>
</div>
</div>
<!-- Password Reset Modal -->
<div id="passwordModal" class="fixed inset-0 z-50 hidden bg-slate-900/50 backdrop-blur-sm flex items-center justify-center p-4">
<div class="glass-card w-full max-w-sm transform transition-all">
<div class="p-6 border-b border-slate-200 dark:border-slate-700 flex justify-between items-center">
<h3 class="text-lg font-bold text-slate-800 dark:text-white">เปลี่ยนรหัสผ่าน</h3>
<button type="button" onclick="closePasswordModal()" class="text-slate-400 hover:text-slate-600 dark:hover:text-slate-200">
<i class="fa-solid fa-times"></i>
</button>
</div>
<form id="passwordForm" class="p-6 space-y-4">
<input type="hidden" name="csrf_token" value="<?= generateCSRFToken() ?>">
<input type="hidden" name="action" value="edit_password">
<input type="hidden" name="id" id="pwdUserId" value="">
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อผู้ใช้งาน</label>
<input type="text" id="pwdUsername" class="glass-input w-full px-4 py-2 bg-slate-100 dark:bg-slate-800" readonly disabled>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">รหัสผ่านใหม่ <span class="text-red-500">*</span></label>
<input type="text" name="password" class="glass-input w-full px-4 py-2" required minlength="4">
</div>
<div class="flex justify-end gap-3 pt-4">
<button type="button" onclick="closePasswordModal()" class="btn-secondary-glass py-2 px-4">ยกเลิก</button>
<button type="submit" class="btn-primary-glass py-2 px-6">บันทึก</button>
</div>
</form>
</div>
</div>
<script>
function openUserModal(action, data = null) {
document.getElementById('userAction').value = action;
const form = document.getElementById('userForm');
if (action === 'add') {
document.getElementById('userModalTitle').textContent = 'เพิ่มผู้ใช้งานใหม่';
document.getElementById('hrSearchBox').classList.remove('hidden');
document.getElementById('passwordField').classList.remove('hidden');
document.getElementById('userUsername').readOnly = false;
document.getElementById('userUsername').classList.remove('bg-slate-100', 'dark:bg-slate-800');
document.getElementById('userPassword').required = true;
form.reset();
document.getElementById('userId').value = '';
document.getElementById('hrResult').classList.add('hidden');
} else if (action === 'edit' && data) {
document.getElementById('userModalTitle').textContent = 'แก้ไขข้อมูลผู้ใช้งาน';
document.getElementById('hrSearchBox').classList.add('hidden');
document.getElementById('passwordField').classList.add('hidden');
document.getElementById('userUsername').readOnly = true;
document.getElementById('userUsername').classList.add('bg-slate-100', 'dark:bg-slate-800');
document.getElementById('userPassword').required = false;
document.getElementById('userId').value = data.id;
document.getElementById('userUsername').value = data.username;
document.getElementById('userFullName').value = data.full_name;
document.getElementById('userRole').value = data.role;
document.getElementById('userDepartment').value = data.department_id || '';
}
document.getElementById('userModal').classList.remove('hidden');
}
function closeUserModal() {
document.getElementById('userModal').classList.add('hidden');
}
function selectHRResult(cid, fullName, position, department, workDuration) {
document.getElementById('userFullName').value = fullName;
document.getElementById('userUsername').value = cid;
document.getElementById('userPassword').value = '123456'; // Default pass is 123456
const resultBox = document.getElementById('hrResult');
resultBox.innerHTML = `
<div class="bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 p-3 rounded border border-green-200 dark:border-green-800">
<p><strong><i class="fa-solid fa-check-circle"></i> เลือก: ${fullName}</strong></p>
<ul class="mt-2 space-y-1 list-disc list-inside">
<li><strong>ชื่อ:</strong> ${fullName}</li>
<li><strong>ตำแหน่ง:</strong> ${position}</li>
<li><strong>หน่วยงาน (HR):</strong> ${department}</li>
<li><strong>อายุงาน:</strong> ${workDuration}</li>
</ul>
<p class="mt-2 text-xs opacity-80">* ระบบได้นำเลข CID ไปตั้งเป็น Username (รหัสผ่านเริ่มต้นคือ 123456)</p>
</div>
`;
}
function openPasswordModal(id, username) {
document.getElementById('passwordForm').reset();
document.getElementById('pwdUserId').value = id;
document.getElementById('pwdUsername').value = username;
document.getElementById('passwordModal').classList.remove('hidden');
}
function closePasswordModal() {
document.getElementById('passwordModal').classList.add('hidden');
}
function searchHR() {
const query = document.getElementById('hrCid').value.trim();
if (!query) {
Swal.fire('แจ้งเตือน', 'กรุณาระบุเลขบัตรประชาชน หรือ ชื่อ-สกุล', 'warning');
return;
}
const btn = document.getElementById('btnSearchHR');
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> กำลังค้นหา...';
btn.disabled = true;
$.ajax({
url: 'api/hr.php',
type: 'POST',
data: { query: query },
dataType: 'json',
success: function(response) {
btn.innerHTML = originalText;
btn.disabled = false;
const resultBox = document.getElementById('hrResult');
if (response.status === 'success') {
const data = response.data;
selectHRResult(data.cid, data.full_name, data.position, data.department, data.work_duration);
resultBox.classList.remove('hidden');
} else if (response.status === 'multiple') {
let html = `
<div class="bg-amber-50 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300 p-3 rounded border border-amber-200 dark:border-amber-800 mb-2">
<strong><i class="fa-solid fa-users"></i> ${response.message}</strong>
</div>
<div class="space-y-2 max-h-60 overflow-y-auto pr-2 custom-scrollbar">
`;
response.data.forEach(item => {
html += `
<button type="button" onclick="selectHRResult('${item.cid}', '${item.full_name}', '${item.position}', '${item.department}', '${item.work_duration}')"
class="w-full text-left bg-white dark:bg-slate-800 p-3 rounded-lg border border-slate-200 dark:border-slate-700 hover:border-emerald-500 hover:shadow-md transition-all">
<div class="font-medium text-slate-800 dark:text-white">${item.full_name}</div>
<div class="text-xs text-slate-500 mt-1">
<span class="mr-3"><i class="fa-solid fa-briefcase mr-1"></i> ${item.position}</span>
<span><i class="fa-solid fa-hospital-user mr-1"></i> ${item.department}</span>
</div>
</button>
`;
});
html += `</div>`;
resultBox.innerHTML = html;
resultBox.classList.remove('hidden');
} else {
resultBox.innerHTML = `
<div class="bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 p-3 rounded border border-red-200 dark:border-red-800">
<i class="fa-solid fa-triangle-exclamation"></i> ${response.message}
</div>
`;
resultBox.classList.remove('hidden');
}
},
error: function() {
btn.innerHTML = originalText;
btn.disabled = false;
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อระบบ HR ได้', 'error');
}
});
}
function deleteUser(id) {
Swal.fire({
title: 'ยืนยันการลบ?',
text: "คุณต้องการลบผู้ใช้งานนี้ใช่หรือไม่",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#94a3b8',
confirmButtonText: 'ลบ',
cancelButtonText: 'ยกเลิก',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#fff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#1e293b'
}).then((result) => {
if (result.isConfirmed) {
$.post('api/users.php', {
action: 'delete',
id: id,
csrf_token: '<?= generateCSRFToken() ?>'
}, function(response) {
if (response.status === 'success') {
Swal.fire({icon: 'success', title: 'สำเร็จ', text: response.message, timer: 1500, showConfirmButton: false})
.then(() => window.location.reload());
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
}, 'json');
}
});
}
document.addEventListener('DOMContentLoaded', function() {
$('#userForm').on('submit', function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
btn.addClass('btn-loading');
$.ajax({
url: 'api/users.php',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
btn.removeClass('btn-loading');
if (response.status === 'success') {
closeUserModal();
Swal.fire({icon: 'success', title: 'สำเร็จ', text: response.message, timer: 1500, showConfirmButton: false})
.then(() => window.location.reload());
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
btn.removeClass('btn-loading');
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
}
});
});
$('#passwordForm').on('submit', function(e) {
e.preventDefault();
const btn = $(this).find('button[type="submit"]');
btn.addClass('btn-loading');
$.ajax({
url: 'api/users.php',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
btn.removeClass('btn-loading');
if (response.status === 'success') {
closePasswordModal();
Swal.fire({icon: 'success', title: 'สำเร็จ', text: response.message, timer: 1500, showConfirmButton: false});
} else {
Swal.fire('ผิดพลาด', response.message, 'error');
}
},
error: function() {
btn.removeClass('btn-loading');
Swal.fire('ผิดพลาด', 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้', 'error');
}
});
});
});
</script>