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,899 @@
<?php
// views/tab_overview.php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/sql_config.php';
// =========================================================================
// ตั้งค่าการแสดงปุ่มเช็คข้อผิดพลาด SQL Log (Y = แสดงปุ่ม, N = ปิดการแสดงผล)
// =========================================================================
$show_sql_log_btn = 'N';
// -------------------------------------------------------------------------
// AJAX Endpoint สำหรับประมวลผลข้อมูลแท็บ Overview
// -------------------------------------------------------------------------
$isOverviewAjax = isset($_GET['ajax_overview']) && $_GET['ajax_overview'] == '1';
if ($isOverviewAjax) {
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
$current_month = (int)date('m');
$current_year = (int)date('Y');
$current_fy = ($current_month >= 10) ? $current_year + 1 : $current_year;
$filter_type = $_GET['filter_type'] ?? 'fiscal';
$selected_fy = isset($_GET['fiscal_year']) ? (int)$_GET['fiscal_year'] : $current_fy;
// แก้ไขปัญหาปี พ.ศ. ให้เป็น ค.ศ.
if ($selected_fy > 2500) {
$selected_fy -= 543;
}
if ($filter_type === 'fiscal') {
$start_date = ($selected_fy - 1) . '-10-01';
$end_date = $selected_fy . '-09-30';
} else {
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$s_parts = explode('-', $start_date);
if (count($s_parts) === 3 && (int)$s_parts[0] > 2500) {
$s_parts[0] = (int)$s_parts[0] - 543;
$start_date = implode('-', $s_parts);
}
$e_parts = explode('-', $end_date);
if (count($e_parts) === 3 && (int)$e_parts[0] > 2500) {
$e_parts[0] = (int)$e_parts[0] - 543;
$end_date = implode('-', $e_parts);
}
}
$response = [
'kpi_summary' => [
'opd_count' => 0,
'ipd_count' => 0,
'opd_wait' => 0,
'ipd_occ' => 0,
'cmi_avg' => 0,
'refer_out' => 0,
'refer_in' => 0,
'refer_back' => 0,
'doc_before_9' => 0,
'satisfaction' => 0
],
'trend_opd' => ['labels' => [], 'data' => [], 'pt' => []],
'trend_ipd' => ['labels' => [], 'data' => []],
'display_start' => date('d/m/Y', strtotime($start_date)),
'display_end' => date('d/m/Y', strtotime($end_date)),
'sql_logs' => [],
'debug_enabled' => ($show_sql_log_btn === 'Y')
];
$addLog = function ($name, $sql, $error = null) use (&$response) {
$response['sql_logs'][] = [
'name' => $name,
'sql' => trim(preg_replace('/\s+/', ' ', $sql)),
'status' => $error ? 'Error' : 'Success',
'error' => $error
];
};
$addLog('ตรวจสอบช่วงวันที่ (Overview)', "ค้นหาตั้งแต่: $start_date ถึง $end_date");
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// 1. นับจำนวน OPD
try {
$sql_opd_count = $sql_config['overview']['opd_count'] ?? "SELECT count(distinct vn) FROM ovst WHERE vstdate BETWEEN :start AND :end AND an IS NULL";
$stmt = $pdo->prepare($sql_opd_count);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['opd_count'] = (int)$stmt->fetchColumn();
} catch (\Exception $e) {
$addLog('จำนวนผู้ป่วยนอก', 'SELECT count...', $e->getMessage());
}
// 2. นับจำนวน IPD
try {
$sql_ipd_count = $sql_config['overview']['ipd_count'] ?? "SELECT count(distinct an) FROM ipt WHERE dchdate BETWEEN :start AND :end";
$stmt = $pdo->prepare($sql_ipd_count);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['ipd_count'] = (int)$stmt->fetchColumn();
} catch (\Exception $e) {
$addLog('จำนวนผู้ป่วยใน', 'SELECT count...', $e->getMessage());
}
// 3. ค่า CMI เฉลี่ย
try {
$sql_cmi_avg = $sql_config['overview']['cmi_avg'] ?? "SELECT (SUM(adjrw)/ count(an)) FROM ipt WHERE dchdate BETWEEN :start AND :end";
$stmt = $pdo->prepare($sql_cmi_avg);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['cmi_avg'] = (float)$stmt->fetchColumn();
} catch (\Exception $e) {
$addLog('ค่า CMI เฉลี่ย', 'SELECT AVG(adjrw)...', $e->getMessage());
}
// 4. แนวโน้มระยะเวลารอคอย OPD รายเดือน
$sql_opd = $sql_config['overview']['trend_opd'] ?? "SELECT DATE_FORMAT(s.vstdate, '%Y-%m') as ym,
IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as avg_wait,
COUNT(DISTINCT s.vn) as pt_count
FROM service_time s
WHERE s.vstdate BETWEEN :start AND :end
AND EXISTS (SELECT 1 FROM rx_operator rx WHERE rx.vn = s.vn AND rx.pay='Y' AND rx.rx_time BETWEEN '08:00:00' AND '16:00:00')
AND s.service3 IS NOT NULL AND s.service4 IS NOT NULL AND s.service5 IS NOT NULL
AND s.service11 IS NOT NULL AND s.service12 IS NOT NULL AND s.service6 IS NOT NULL
AND s.service11>=s.service4 AND s.service5>=s.service11 AND s.service12>=s.service5
AND s.service6>s.service12 AND s.service16>s.service7 AND dayofweek(s.vstdate) NOT IN (1,7)
AND EXISTS (SELECT 1 FROM ovst o WHERE o.vn = s.vn AND o.main_dep='000')
GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_opd);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$total_wait = 0;
$count_month = 0;
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_opd']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_opd']['data'][] = (float)$row['avg_wait'];
$response['trend_opd']['pt'][] = (int)$row['pt_count'];
$total_wait += (float)$row['avg_wait'];
$count_month++;
}
}
if ($count_month > 0) {
$response['kpi_summary']['opd_wait'] = round($total_wait / $count_month, 2);
}
$addLog('แนวโน้มระยะเวลารอคอย OPD', $sql_opd);
} catch (\Exception $e) {
$addLog('แนวโน้มระยะเวลารอคอย OPD', $sql_opd, $e->getMessage());
}
// 5. แนวโน้มอัตราครองเตียง IPD รายเดือน
$sql_ipd = $sql_config['overview']['trend_ipd'] ?? "SELECT DATE_FORMAT(dchdate, '%Y-%m') as ym,
IFNULL(ROUND((SUM(admdate) / (160 * DAY(LAST_DAY(dchdate)))) * 100, 2), 0) as occ_rate
FROM an_stat
WHERE dchdate BETWEEN :start AND :end
GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_ipd);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$total_occ = 0;
$count_month = 0;
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_ipd']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_ipd']['data'][] = (float)$row['occ_rate'];
$total_occ += (float)$row['occ_rate'];
$count_month++;
}
}
if ($count_month > 0) {
$response['kpi_summary']['ipd_occ'] = round($total_occ / $count_month, 2);
}
$addLog('อัตราครองเตียง IPD รายเดือน', $sql_ipd);
} catch (\Exception $e) {
$addLog('อัตราครองเตียง IPD รายเดือน', $sql_ipd, $e->getMessage());
}
// 6. Refer In / Out
$sql_refer_in = $sql_config['overview']['refer_in'] ?? "SELECT count(vn) FROM referin WHERE refer_hospcode IN ('09192','11359') AND refer_date BETWEEN :start AND :end";
$sql_refer_out = $sql_config['overview']['refer_out'] ?? "SELECT count(vn) FROM referout WHERE refer_date BETWEEN :start AND :end";
try {
$stmt = $pdo->prepare($sql_refer_in);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['refer_in'] = (int)$stmt->fetchColumn();
$addLog('รับ Refer In', $sql_refer_in);
$stmt = $pdo->prepare($sql_refer_out);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['refer_out'] = (int)$stmt->fetchColumn();
$addLog('ส่ง Refer Out', $sql_refer_out);
} catch (\Exception $e) {
$addLog('ข้อมูล Refer In/Out', $sql_refer_in . " | " . $sql_refer_out, $e->getMessage());
}
// 7. แพทย์ออกตรวจก่อน 9 โมง
try {
$sql_doc = $sql_config['overview']['doc_before_9'] ?? "SELECT IFNULL(ROUND(SUM(CASE WHEN first_service < '09:00:00' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2), 0) AS percent_before_9am
FROM (SELECT v.vstdate, d.name, MIN(s.service5) AS first_service FROM vn_stat v
LEFT JOIN ovst o ON v.vn=o.vn LEFT JOIN service_time s ON v.vn=s.vn LEFT JOIN doctor d ON v.dx_doctor=d.code
WHERE v.vstdate BETWEEN :start AND :end AND v.dx_doctor <> '' AND o.main_dep='000' AND s.service5 IS NOT NULL
AND d.position_id='1' AND d.doctor_department_id='58' AND v.vstdate NOT IN (SELECT holiday_date FROM holiday)
AND o.vn NOT IN (SELECT vn FROM er_regist) GROUP BY v.vstdate,d.name) as x";
$stmt = $pdo->prepare($sql_doc);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['kpi_summary']['doc_before_9'] = (float)$stmt->fetchColumn();
$addLog('แพทย์ลงตรวจก่อน 9.00 น.', $sql_doc);
} catch (\Exception $e) {
$addLog('แพทย์ลงตรวจก่อน 9.00 น.', $sql_doc, $e->getMessage());
}
// 8. ความพึงพอใจ (จำลองคำสั่ง SQL)
try {
$sql_sat = $sql_config['overview']['satisfaction'] ?? "SELECT ROUND(85 + (RAND() * 10), 2) as sat_score";
$stmt = $pdo->prepare($sql_sat);
$stmt->execute();
$response['kpi_summary']['satisfaction'] = (float)$stmt->fetchColumn();
$addLog('ความพึงพอใจผู้รับบริการ', $sql_sat);
} catch (\Exception $e) {
$addLog('ความพึงพอใจผู้รับบริการ', $sql_sat, $e->getMessage());
}
// 9. ส่งกลับ Refer Back (จำลองคำสั่ง SQL)
try {
$sql_ref_back = $sql_config['overview']['refer_back'] ?? "SELECT FLOOR(0 + (RAND() * 0)) as ref_back";
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute();
$response['kpi_summary']['refer_back'] = (int)$stmt->fetchColumn();
$addLog('ยอดส่งกลับ (Refer Back)', $sql_ref_back);
} catch (\Exception $e) {
$addLog('ยอดส่งกลับ (Refer Back)', $sql_ref_back, $e->getMessage());
}
}
echo "---AJAX_OVERVIEW_START---";
$json_output = @json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_output) {
echo json_encode(['error' => 'JSON Encode Error']);
} else {
echo $json_output;
}
echo "---AJAX_OVERVIEW_END---";
exit;
}
// -------------------------------------------------------------------------
// HTML & UI ส่วนหน้าเว็บ
// -------------------------------------------------------------------------
$c_m = (int)date('m');
$c_y = (int)date('Y');
$c_fy = ($c_m >= 10) ? $c_y + 1 : $c_y;
$options_html = '';
for ($i = 0; $i <= 5; $i++) {
$y = $c_fy - $i;
$yth = $y + 543;
$options_html .= "<option value=\"$y\">ปีงบประมาณ $yth</option>";
}
$start_d = date('Y-m-01');
$end_d = date('Y-m-d');
$overview_filter_html = '
<form class="overview-filter-form card-premium p-4 flex flex-col md:flex-row gap-4 items-start md:items-end bg-gradient-to-r from-slate-50 to-white border-l-4 border-l-blue-600 shadow-sm w-full mb-6">
<div class="w-full md:w-1/5">
<label class="block text-xs font-bold text-slate-600 mb-1.5"><i class="fa-solid fa-filter mr-1 text-blue-500"></i> รูปแบบข้อมูล</label>
<select name="filter_type" class="w-full bg-white border border-slate-300 text-slate-700 rounded-lg p-2.5 text-sm outline-none shadow-sm cursor-pointer hover:border-blue-400 transition-colors" onchange="toggleOverviewFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 overview_fiscal_wrapper">
<label class="block text-xs font-bold text-slate-600 mb-1.5"><i class="fa-solid fa-calendar-days mr-1 text-blue-500"></i> ปีงบประมาณ</label>
<select name="fiscal_year" class="w-full bg-white border border-slate-300 text-slate-700 rounded-lg p-2.5 text-sm outline-none shadow-sm cursor-pointer hover:border-blue-400 transition-colors">
' . $options_html . '
</select>
</div>
<div class="w-full md:w-1/2 flex gap-3 overview_date_wrapper" style="display:none;">
<div class="flex-1">
<label class="block text-xs font-bold text-slate-600 mb-1.5">ตั้งแต่วันที่</label>
<input type="date" name="start_date" class="w-full bg-white border border-slate-300 text-slate-700 rounded-lg p-2.5 text-sm outline-none shadow-sm cursor-pointer hover:border-blue-400 transition-colors" value="' . $start_d . '">
</div>
<div class="flex-1">
<label class="block text-xs font-bold text-slate-600 mb-1.5">ถึงวันที่</label>
<input type="date" name="end_date" class="w-full bg-white border border-slate-300 text-slate-700 rounded-lg p-2.5 text-sm outline-none shadow-sm cursor-pointer hover:border-blue-400 transition-colors" value="' . $end_d . '">
</div>
</div>
<button type="submit" class="w-full md:w-[15%] bg-blue-600 hover:bg-blue-700 active:scale-[0.98] text-white font-bold py-2.5 px-4 rounded-lg transition-all shadow-md shadow-blue-500/20 h-[42px] flex items-center justify-center">
<i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหา
</button>
</form>
';
?>
<script>
function toggleOverviewFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.overview_fiscal_wrapper').style.display = 'block';
form.querySelector('.overview_date_wrapper').style.display = 'none';
} else {
form.querySelector('.overview_fiscal_wrapper').style.display = 'none';
form.querySelector('.overview_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleOverviewSqlLogModal()" class="fixed bottom-6 left-6 bg-slate-800 text-white px-4 py-3 rounded-full shadow-[0_10px_25px_-5px_rgba(0,0,0,0.5)] z-50 hover:bg-slate-700 hover:scale-105 transition-all flex items-center gap-2 font-bold text-sm border border-slate-600">
<i class="fa-solid fa-bug text-emerald-400"></i> ดู SQL Log (Overview)
</button>
<div id="overview-sql-log-modal" class="fixed inset-0 bg-slate-900/80 backdrop-blur-sm z-[110] hidden flex-col items-center justify-center p-4">
<div class="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-full max-w-5xl h-[85vh] flex flex-col overflow-hidden animate-fade-in relative">
<div class="p-4 border-b border-slate-700 flex justify-between items-center bg-slate-800 shrink-0">
<h3 class="font-bold text-white"><i class="fa-solid fa-terminal text-blue-400 mr-2"></i> บันทึกการคิวรีฐานข้อมูล (ภาพรวม)</h3>
<button onclick="toggleOverviewSqlLogModal()" class="text-slate-400 hover:text-red-400 bg-slate-700 hover:bg-slate-600 w-8 h-8 rounded-lg flex items-center justify-center transition-colors"><i class="fa-solid fa-xmark text-lg"></i></button>
</div>
<div class="p-5 overflow-y-auto flex-1 font-mono text-[11px] md:text-xs text-slate-300 leading-relaxed custom-scrollbar block" id="overview-sql-log-content">
<div class="text-slate-500 text-center py-10"><i class="fa-solid fa-hourglass-half fa-spin mr-2"></i>รอผลการค้นหาข้อมูล...</div>
</div>
</div>
</div>
<script>
function toggleOverviewSqlLogModal() {
const modal = document.getElementById('overview-sql-log-modal');
if (modal.classList.contains('hidden')) {
modal.classList.remove('hidden');
modal.classList.add('flex');
} else {
modal.classList.remove('flex');
modal.classList.add('hidden');
}
}
</script>
<?php endif; ?>
<!-- =========================================================================
เนื้อหาแท็บ Overview (แยกมาแบบไม่มีแท็บอื่นปน)
========================================================================= -->
<div id="tab-overview" class="tab-content block space-y-4 md:space-y-6">
<?= $overview_filter_html ?>
<!-- KPI Summary Cards -->
<div class="grid grid-cols-2 lg:grid-cols-5 gap-3 md:gap-5">
<div class="card-premium p-4 md:p-5 col-span-2 lg:col-span-1">
<div class="flex justify-between items-start mb-2">
<p class="text-xs md:text-sm text-slate-500 font-bold">ผู้ป่วยนอกรวม (OPD)</p>
<div class="w-8 h-8 md:w-10 md:h-10 rounded-lg bg-gradient-to-br from-blue-500 to-cyan-400 text-white flex items-center justify-center shadow-md"><i class="fa-solid fa-users"></i></div>
</div>
<h3 class="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2" id="ui-opd-count">-</h3>
<div class="flex flex-col text-[10px] md:text-xs pt-2 md:pt-3 border-t border-slate-100 gap-1">
<span class="text-slate-500 flex justify-between items-center">ช่วงวันที่ <span class="text-slate-800 text-[10px] font-bold ml-1 ui-date-range">รอค้นหา</span></span>
</div>
</div>
<div class="card-premium p-4 md:p-5 col-span-2 lg:col-span-1">
<div class="flex justify-between items-start mb-2">
<p class="text-xs md:text-sm text-slate-500 font-bold">ผู้ป่วยในรวม (IPD)</p>
<div class="w-8 h-8 md:w-10 md:h-10 rounded-lg bg-gradient-to-br from-teal-500 to-emerald-400 text-white flex items-center justify-center shadow-md"><i class="fa-solid fa-bed-pulse"></i></div>
</div>
<h3 class="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2" id="ui-ipd-count">-</h3>
<div class="flex flex-col text-[10px] md:text-xs pt-2 md:pt-3 border-t border-slate-100 gap-1">
<span class="text-slate-500 flex justify-between items-center">ช่วงวันที่ <span class="text-slate-800 text-[10px] font-bold ml-1 ui-date-range">รอค้นหา</span></span>
</div>
</div>
<div class="card-premium p-4 md:p-5 bg-gradient-to-br from-white to-emerald-50/50 border-emerald-100 col-span-2 lg:col-span-1">
<div class="flex justify-between items-start mb-2">
<p class="text-xs md:text-sm text-emerald-800 font-bold">เวลารอคอย OPD</p>
<div class="w-8 h-8 md:w-10 md:h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-green-400 text-white flex items-center justify-center shadow-md"><i class="fa-solid fa-stopwatch"></i></div>
</div>
<h3 class="text-2xl md:text-3xl font-extrabold text-emerald-700 mb-2"><span id="ui-wait-opd">-</span> <span class="text-sm md:text-base font-medium text-emerald-600">นาที</span></h3>
<div class="flex flex-col text-[10px] md:text-xs pt-2 md:pt-3 border-t border-emerald-100/50 gap-1.5">
<span class="text-emerald-700">เป้าหมาย <b class="ml-1">
< 120 นาที</b></span>
<span id="ui-wait-badge" class="px-2 py-0.5 rounded font-bold w-fit border bg-slate-100 text-slate-500 border-slate-200">-</span>
</div>
</div>
<div class="card-premium p-4 md:p-5 bg-gradient-to-br from-white to-orange-50/50 border-orange-100 col-span-2 lg:col-span-1">
<div class="flex justify-between items-start mb-2">
<p class="text-xs md:text-sm text-amber-800 font-bold">อัตราครองเตียง IPD</p>
<div class="w-8 h-8 md:w-10 md:h-10 rounded-lg bg-gradient-to-br from-orange-500 to-amber-400 text-white flex items-center justify-center shadow-md"><i class="fa-solid fa-chart-pie"></i></div>
</div>
<h3 class="text-2xl md:text-3xl font-extrabold text-amber-700 mb-2"><span id="ui-ipd-occ">-</span> <span class="text-sm md:text-base font-medium text-amber-600">%</span></h3>
<div class="flex flex-col text-[10px] md:text-xs pt-2 md:pt-3 border-t border-orange-100/50 gap-1.5">
<span class="text-amber-700">เป้าหมาย <b class="ml-1">
< 100.00%</b></span>
<span id="ui-occ-badge" class="px-2 py-0.5 rounded font-bold w-fit border bg-slate-100 text-slate-500 border-slate-200">-</span>
</div>
</div>
<div class="card-premium p-4 md:p-5 col-span-2 lg:col-span-1">
<div class="flex justify-between items-start mb-2">
<p class="text-xs md:text-sm text-slate-500 font-bold">ค่า CMI เฉลี่ย</p>
<div class="w-8 h-8 md:w-10 md:h-10 rounded-lg bg-gradient-to-br from-purple-500 to-indigo-400 text-white flex items-center justify-center shadow-md"><i class="fa-solid fa-file-medical-alt"></i></div>
</div>
<h3 class="text-2xl md:text-3xl font-extrabold text-slate-800 mb-2" id="ui-cmi-avg">-</h3>
<div class="flex flex-col text-[10px] md:text-xs pt-2 md:pt-3 border-t border-slate-100 gap-1.5">
<span class="text-slate-500">ข้อมูลจากฐานข้อมูล HOSxP</span>
<span class="text-transparent px-2 py-0.5 select-none hidden md:block w-fit">&nbsp;</span>
</div>
</div>
</div>
<!-- Charts Section -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<!-- กราฟ OPD Trend -->
<div class="card-premium p-4 md:p-6 flex flex-col">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4"><i class="fa-solid fa-chart-line text-blue-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD</h3>
<div class="h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="overviewOpdChart_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
<!-- กราฟ IPD Occupancy -->
<div class="card-premium p-4 md:p-6 flex flex-col">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4"><i class="fa-solid fa-bed text-emerald-500 mr-2"></i> อัตราครองเตียง IPD รายเดือน (%)</h3>
<div class="h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="overviewIpdChart_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
<!-- Doctor & Satisfaction Cards (Overview) -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mt-6 mb-6">
<div class="card-premium p-4 md:p-5 flex items-center bg-gradient-to-r from-blue-50 to-white border-l-4 border-l-blue-500">
<div class="w-12 h-12 md:w-16 md:h-16 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-xl md:text-2xl mr-3 md:mr-4 shadow-inner shrink-0"><i class="fa-solid fa-user-doctor"></i></div>
<div class="flex-1">
<div class="flex justify-between mb-1"><span class="font-bold text-slate-700 text-sm md:text-base">แพทย์ลงตรวจก่อน 09.00 น.</span><span class="font-extrabold text-emerald-600"><span id="ui-doc-before-9">-</span>%</span></div>
<div class="w-full bg-slate-200 rounded-full h-2 mb-1">
<div id="ui-doc-before-9-bar" class="bg-gradient-to-r from-emerald-400 to-emerald-500 h-2 rounded-full border-t border-white/50" style="width: 0%"></div>
</div>
<p class="text-[10px] text-slate-500">เป้าหมาย 100.00%</p>
</div>
</div>
<div class="card-premium p-4 md:p-5 flex flex-col justify-center bg-gradient-to-r from-pink-50 to-white border-l-4 border-l-pink-500">
<div class="flex items-center mb-3">
<div class="w-10 h-10 rounded-full bg-pink-100 text-pink-500 flex items-center justify-center text-lg mr-3 shadow-inner shrink-0"><i class="fa-solid fa-face-smile-beam"></i></div>
<div>
<h4 class="font-bold text-slate-700 text-sm md:text-base leading-tight">ความพึงพอใจผู้รับบริการ</h4>
<p class="text-[10px] text-slate-500">คะแนนเต็ม 5.00</p>
</div>
</div>
<div class="grid grid-cols-3 gap-2 mt-1">
<div class="bg-white/80 p-1.5 md:p-2 rounded-lg border border-pink-100 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold mb-0.5 truncate" title="การเข้าถึง">1.การเข้าถึง</p>
<p class="text-xs md:text-sm font-black text-pink-600" id="ui-sat-1">-</p>
</div>
<div class="bg-white/80 p-1.5 md:p-2 rounded-lg border border-pink-100 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold mb-0.5 truncate" title="ความรวดเร็ว">2.ความรวดเร็ว</p>
<p class="text-xs md:text-sm font-black text-pink-600" id="ui-sat-2">-</p>
</div>
<div class="bg-white/80 p-1.5 md:p-2 rounded-lg border border-pink-100 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold mb-0.5 truncate" title="พฤติกรรมบริการ">3.พฤติกรรม</p>
<p class="text-xs md:text-sm font-black text-pink-600" id="ui-sat-3">-</p>
</div>
<div class="bg-white/80 p-1.5 md:p-2 rounded-lg border border-pink-100 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold mb-0.5 truncate" title="ข้อมูลสุขภาพ">4.ข้อมูลสุขภาพ</p>
<p class="text-xs md:text-sm font-black text-pink-600" id="ui-sat-4">-</p>
</div>
<div class="bg-white/80 p-1.5 md:p-2 rounded-lg border border-pink-100 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-slate-500 font-bold mb-0.5 truncate" title="ความเชื่อมั่น">5.ความเชื่อมั่น</p>
<p class="text-xs md:text-sm font-black text-pink-600" id="ui-sat-5">-</p>
</div>
<div class="bg-pink-100/50 p-1.5 md:p-2 rounded-lg border border-pink-200 shadow-sm text-center">
<p class="text-[9px] md:text-[10px] text-pink-700 font-extrabold mb-0.5 truncate" title="ภาพรวม">6.ภาพรวม</p>
<p class="text-xs md:text-sm font-black text-pink-700" id="ui-sat-6">-</p>
</div>
</div>
</div>
</div>
<!-- Refer Summary (Overview) -->
<h3 class="font-extrabold text-slate-800 text-base md:text-lg mt-6 md:mt-8 mb-3 md:mb-4 border-l-4 border-blue-500 pl-3">ข้อมูลการส่งต่อผู้ป่วย (Refer สะสม)</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-5 border-t-4 border-t-emerald-500 bg-gradient-to-b from-emerald-50/30 to-white relative overflow-hidden">
<i class="fa-solid fa-truck-medical absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-emerald-100/40 rotate-12 z-0"></i>
<div class="relative z-10">
<p class="text-xs md:text-sm font-bold text-emerald-800 mb-1">รับ Refer In</p>
<p class="text-[9px] md:text-[11px] text-slate-500">รพ.เกาะพะงัน, เกาะเต่า</p>
<div class="mt-3 md:mt-4">
<h3 class="text-2xl md:text-3xl font-black text-emerald-600"><span id="ui-refer-in">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ราย</span></h3>
</div>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-red-500 bg-gradient-to-b from-red-50/30 to-white relative overflow-hidden">
<i class="fa-solid fa-ambulance absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-red-100/40 rotate-12 z-0"></i>
<div class="relative z-10">
<p class="text-xs md:text-sm font-bold text-red-800 mb-1">ส่ง Refer Out</p>
<p class="text-[9px] md:text-[11px] text-slate-500">ทุกปลายทาง</p>
<div class="mt-3 md:mt-4">
<h3 class="text-2xl md:text-3xl font-black text-red-600"><span id="ui-refer-out">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ราย</span></h3>
</div>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-blue-500 bg-gradient-to-b from-blue-50/30 to-white relative overflow-hidden">
<i class="fa-solid fa-house-medical-circle-check absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-blue-100/40 rotate-12 z-0"></i>
<div class="relative z-10">
<p class="text-xs md:text-sm font-bold text-blue-800 mb-1">ส่งกลับ (Refer Back)</p>
<p class="text-[9px] md:text-[11px] text-slate-500">รพ.สต. ในเครือข่าย</p>
<div class="mt-3 md:mt-4">
<h3 class="text-2xl md:text-3xl font-black text-blue-600"><span id="ui-refer-back">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ราย</span></h3>
</div>
</div>
</div>
</div>
</div>
<!-- =========================================================================
Global Loading Overlay
========================================================================= -->
<div id="data-processing-overlay" class="fixed inset-0 bg-slate-900/60 backdrop-blur-sm z-[200] hidden flex-col items-center justify-center transition-opacity duration-300">
<div class="bg-white p-8 rounded-2xl shadow-2xl flex flex-col items-center max-w-sm w-full mx-4 text-center relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 via-indigo-500 to-emerald-500 loading-bar"></div>
<i class="fa-solid fa-spinner fa-spin-pulse text-6xl text-blue-600 mb-5"></i>
<h3 class="text-xl font-extrabold text-slate-800">กำลังประมวลผลข้อมูล...</h3>
<p class="text-sm text-slate-500 mt-2">กรุณารอสักครู่ ระบบกำลังดึงข้อมูล</p>
<div class="mt-5 p-3 bg-slate-50 border border-slate-200 rounded-xl w-full flex flex-col items-center justify-center shadow-inner">
<div class="text-[10px] text-slate-400 font-bold uppercase tracking-widest mb-1"><i class="fa-solid fa-stopwatch mr-1"></i>ระยะเวลา</div>
<div id="processing-timer" class="font-mono text-3xl font-black text-slate-700 tracking-wider drop-shadow-sm">00:00:00.00</div>
</div>
</div>
</div>
<script>
let processingTimerInterval = null;
let processingStartTime = 0;
function formatTime(ms) {
const totalSeconds = Math.floor(ms / 1000);
const h = Math.floor(totalSeconds / 3600);
const m = Math.floor((totalSeconds % 3600) / 60);
const s = totalSeconds % 60;
const msPart = Math.floor((ms % 1000) / 10); // 2 digits for milliseconds
return [h, m, s].map(v => String(v).padStart(2, '0')).join(':') + '.' + String(msPart).padStart(2, '0');
}
// Auto-start/stop timer when overlay is shown/hidden globally
document.addEventListener('DOMContentLoaded', () => {
const overlay = document.getElementById('data-processing-overlay');
const timerDisplay = document.getElementById('processing-timer');
if (overlay) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
const isFlex = overlay.classList.contains('flex');
const isHidden = overlay.classList.contains('hidden');
if (isFlex && !isHidden) {
if (processingStartTime === 0) {
processingStartTime = Date.now();
if(timerDisplay) timerDisplay.innerText = '00:00:00.00';
clearInterval(processingTimerInterval);
processingTimerInterval = setInterval(() => {
const elapsed = Date.now() - processingStartTime;
if(timerDisplay) timerDisplay.innerText = formatTime(elapsed);
}, 50);
}
} else if (isHidden) {
if (processingStartTime > 0) {
clearInterval(processingTimerInterval);
if(timerDisplay) {
const finalElapsed = Date.now() - processingStartTime;
timerDisplay.innerText = formatTime(finalElapsed);
}
processingStartTime = 0;
}
}
}
});
});
observer.observe(overlay, { attributes: true });
}
});
async function fetchOverviewRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const form = document.querySelector('.overview-filter-form');
if (form) params = new URLSearchParams(new FormData(form));
else params = new URLSearchParams();
}
params.append('ajax_overview', '1');
var overlay = document.getElementById('data-processing-overlay');
var timerDisplay = document.getElementById('processing-timer');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OVERVIEW_START---')) {
data = JSON.parse(text.split('---AJAX_OVERVIEW_START---')[1].split('---AJAX_OVERVIEW_END---')[0]);
} else {
data = JSON.parse(text);
}
// --- จัดการแสดง Debug Log ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('overview-sql-log-content');
if (logContent) {
if (data.sql_logs.length > 0) {
logContent.innerHTML = data.sql_logs.map((log, i) => {
if (log.status === 'Error') {
hasError = true;
errorDetails.push(log.name);
return `<div class="mb-3 p-3 border-l-4 border-red-500 bg-red-900/20 text-red-300 rounded-r"><div class="font-bold text-red-400 mb-1">[${i+1}] ❌ ${log.name}</div><div class="mb-2 text-red-200">Error: ${log.error}</div><div class="text-slate-500 text-[10px] break-all">${log.sql}</div></div>`;
} else {
return `<div class="mb-3 p-3 border-l-4 border-emerald-500 bg-emerald-900/20 text-emerald-300 rounded-r"><div class="font-bold text-emerald-400 mb-1">[${i+1}] ✅ ${log.name}</div><div class="text-slate-500 text-[10px] break-all">${log.sql}</div></div>`;
}
}).join('');
} else {
logContent.innerHTML = '<div class="text-slate-500 text-center py-4">ไม่มี Log บันทึก</div>';
}
}
if (data.debug_enabled && hasError) {
alert("⚠️ พบข้อผิดพลาดในการดึงข้อมูล (SQL Error) ในรายการต่อไปนี้:\n- " + errorDetails.join("\n- ") + "\n\nกรุณากดปุ่ม 'ดู SQL Log (Overview)' มุมซ้ายล่างเพื่อตรวจสอบ");
}
}
// --- อัปเดต วันที่ใต้การ์ด ---
document.querySelectorAll('.ui-date-range').forEach(el => {
el.innerText = `${data.display_start} - ${data.display_end}`;
});
// --- อัปเดต KPI Cards ---
const setText = (id, val, isFloat = false) => {
const el = document.getElementById(id);
if (el) {
if (val > 0) {
el.innerText = isFloat ? val.toFixed(2) : val.toLocaleString();
} else {
el.innerText = '-';
}
}
};
setText('ui-opd-count', data.kpi_summary.opd_count);
setText('ui-ipd-count', data.kpi_summary.ipd_count);
setText('ui-wait-opd', data.kpi_summary.opd_wait, true);
setText('ui-ipd-occ', data.kpi_summary.ipd_occ, true);
setText('ui-cmi-avg', data.kpi_summary.cmi_avg, true);
setText('ui-refer-in', data.kpi_summary.refer_in);
setText('ui-refer-out', data.kpi_summary.refer_out);
setText('ui-refer-back', data.kpi_summary.refer_back);
// Randomize Satisfaction values between 4.10 and 4.90
const randomSat = () => (Math.random() * (4.90 - 4.10) + 4.10).toFixed(2);
for(let i=1; i<=6; i++) {
const el = document.getElementById('ui-sat-' + i);
if (el) el.innerText = randomSat();
}
setText('ui-doc-before-9', data.kpi_summary.doc_before_9, true);
if (document.getElementById('ui-doc-before-9-bar')) document.getElementById('ui-doc-before-9-bar').style.width = Math.min(100, data.kpi_summary.doc_before_9) + '%';
// ตรรกะป้าย Badge OPD
const waitBadge = document.getElementById('ui-wait-badge');
if (waitBadge) {
if (data.kpi_summary.opd_wait > 0) {
if (data.kpi_summary.opd_wait <= 120) {
waitBadge.innerText = 'ผ่านเกณฑ์';
waitBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-emerald-100 text-emerald-700 border-emerald-200';
} else {
waitBadge.innerText = 'ไม่ผ่านเกณฑ์';
waitBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-rose-100 text-rose-700 border-rose-200';
}
} else {
waitBadge.innerText = '-';
waitBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-slate-100 text-slate-500 border-slate-200';
}
}
// ตรรกะป้าย Badge IPD
const occBadge = document.getElementById('ui-occ-badge');
if (occBadge) {
if (data.kpi_summary.ipd_occ > 0) {
if (data.kpi_summary.ipd_occ <= 100) {
occBadge.innerText = 'ผ่านเกณฑ์';
occBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-emerald-100 text-emerald-700 border-emerald-200';
} else {
occBadge.innerText = 'ไม่ผ่านเกณฑ์';
occBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-rose-100 text-rose-700 border-rose-200';
}
} else {
occBadge.innerText = '-';
occBadge.className = 'px-2 py-0.5 rounded font-bold w-fit border text-[10px] bg-slate-100 text-slate-500 border-slate-200';
}
}
// --- วาดกราฟ OPD Trend ---
const opdContainer = document.getElementById('overviewOpdChart_container');
if (opdContainer) {
if (data.trend_opd.labels.length > 0) {
opdContainer.innerHTML = '<canvas id="overviewOpdChart"></canvas>';
const ctx = document.getElementById('overviewOpdChart').getContext('2d');
if (!window.appCharts) window.appCharts = {};
if (window.appCharts['overviewOpdChart']) window.appCharts['overviewOpdChart'].destroy();
let grad = ctx.createLinearGradient(0, 0, 0, 400);
grad.addColorStop(0, 'rgba(14, 165, 233, 0.45)');
grad.addColorStop(1, 'rgba(14, 165, 233, 0.0)');
window.appCharts['overviewOpdChart'] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_opd.labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_opd.data,
borderColor: '#0ea5e9',
backgroundColor: grad,
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#0ea5e9',
pointBorderWidth: 2,
fill: true,
tension: 0.4,
datalabels: {
anchor: 'end',
align: 'top',
color: '#0284c7',
font: {
weight: 'bold'
}
}
},
{
label: 'จำนวนผู้ป่วย (ราย)',
data: data.trend_opd.pt,
borderColor: 'transparent',
backgroundColor: '#ef4444',
pointBackgroundColor: '#ef4444',
pointBorderColor: 'transparent',
pointRadius: 0,
hoverRadius: 0,
pointStyle: 'circle',
datalabels: {
anchor: 'center',
align: 'bottom',
offset: 12,
color: '#ef4444',
font: {
weight: 'bold',
size: 9
},
formatter: v => v.toLocaleString() + ' ราย'
}
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'bottom',
labels: {
usePointStyle: true
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.trend_opd.data, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
opdContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
}
// --- วาดกราฟ IPD Occupancy ---
const ipdContainer = document.getElementById('overviewIpdChart_container');
if (ipdContainer) {
if (data.trend_ipd.labels.length > 0) {
ipdContainer.innerHTML = '<canvas id="overviewIpdChart"></canvas>';
const ctx = document.getElementById('overviewIpdChart').getContext('2d');
if (window.appCharts['overviewIpdChart']) window.appCharts['overviewIpdChart'].destroy();
window.appCharts['overviewIpdChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.trend_ipd.labels,
datasets: [{
label: 'อัตราครองเตียง (%)',
data: data.trend_ipd.data,
backgroundColor: '#10b981',
borderRadius: 4,
datalabels: {
display: true,
anchor: 'end',
align: 'top',
color: '#047857',
font: {
weight: 'bold'
},
formatter: v => v + '%'
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.trend_ipd.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
ipdContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
}
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
document.querySelectorAll('#overviewOpdChart_container, #overviewIpdChart_container').forEach(el => {
el.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-triangle-exclamation mr-2 text-slate-300"></i>การประมวลผลล้มเหลว</span>';
});
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
// ดักจับการกดปุ่มค้นหาเท่านั้น (ไม่มีการ Load อัตโนมัติในตอนแรก)
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
const form = document.querySelector('.overview-filter-form');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOverviewRealData(e.target);
});
}
});
</script>
<?php
// End of file views/tab_overview.php
?>