899 lines
54 KiB
PHP
899 lines
54 KiB
PHP
<?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"> </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
|
|
?>
|