340 lines
20 KiB
PHP
340 lines
20 KiB
PHP
<?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>
|