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,761 @@
<?php
// views/tab_ipd.php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/sql_config.php';
$show_sql_log_btn = 'N';
$isIpdAjax = isset($_GET['ajax_ipd']) && $_GET['ajax_ipd'] == '1';
if ($isIpdAjax) {
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 = [
'occ_overall' => 0,
'occ_max_ward' => '-',
'occ_max_val' => 0,
'occ_min_ward' => '-',
'occ_min_val' => 0,
'occ_wards_chart' => ['labels' => [], 'data' => []],
'refer_in_total' => 0,
'refer_out_total' => 0,
'refer_back_total' => 0,
'refer_in_trend' => ['labels' => [], 'data' => []], // กราฟ Refer In รายเดือน
'refer_out_trend' => ['labels' => [], 'data' => []], // กราฟ Refer Out รายเดือน
'refer_in_top10' => [],
'refer_out_top10' => [],
'adjrw_summary' => ['in_low' => 0, 'in_high' => 0, 'out_low' => 0, 'out_high' => 0],
'adjrw_in_low_trend' => ['labels' => [], 'data' => []],
'adjrw_in_high_trend' => ['labels' => [], 'data' => []],
'adjrw_out_low_trend' => ['labels' => [], 'data' => []],
'adjrw_out_high_trend' => ['labels' => [], 'data' => []],
'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('ระบบตรวจสอบช่วงวันที่ IPD', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// 1. อัตราครองเตียง IPD
$sql_occ_all = $sql_config['ipd']['occ_all'] ?? "SELECT IFNULL((SUM(admdate)/(160 * (DATEDIFF(:end_d,:start_d)+1))) * 100, 0) as bb FROM an_stat WHERE dchdate BETWEEN :start AND :end";
try {
$stmt = $pdo->prepare($sql_occ_all);
$stmt->execute(['start_d' => $start_date, 'end_d' => $end_date, 'start' => $start_date, 'end' => $end_date]);
$response['occ_overall'] = (float)$stmt->fetchColumn();
$addLog('อัตราครองเตียงภาพรวม', $sql_occ_all);
} catch (\Exception $e) {
$addLog('อัตราครองเตียงภาพรวม', $sql_occ_all, $e->getMessage());
}
$sql_occ_max = $sql_config['ipd']['occ_max'] ?? "SELECT w.name AS ward_name, ROUND((SUM(a.admdate) / (w.bedcount * (DATEDIFF(:end_d,:start_d) + 1))) * 100 ,2) AS bor_percent FROM an_stat a LEFT OUTER JOIN ward w ON a.ward = w.ward WHERE a.dchdate BETWEEN :start AND :end AND w.bedcount > 0 GROUP BY a.ward,w.name,w.bedcount ORDER BY bor_percent DESC LIMIT 1";
try {
$stmt = $pdo->prepare($sql_occ_max);
$stmt->execute(['start_d' => $start_date, 'end_d' => $end_date, 'start' => $start_date, 'end' => $end_date]);
if ($r = $stmt->fetch()) {
$response['occ_max_ward'] = $to_utf8($r['ward_name']);
$response['occ_max_val'] = (float)$r['bor_percent'];
}
$addLog('ตึกที่มีอัตราการครองเตียงสูงสุด', $sql_occ_max);
} catch (\Exception $e) {
$addLog('ตึกที่มีอัตราการครองเตียงสูงสุด', $sql_occ_max, $e->getMessage());
}
$sql_occ_min = $sql_config['ipd']['occ_min'] ?? "SELECT w.name AS ward_name, ROUND((SUM(a.admdate) / (w.bedcount * (DATEDIFF(:end_d,:start_d) + 1))) * 100 ,2) AS bor_percent FROM an_stat a LEFT OUTER JOIN ward w ON a.ward = w.ward WHERE a.dchdate BETWEEN :start AND :end AND w.bedcount > 0 GROUP BY a.ward,w.name,w.bedcount ORDER BY bor_percent ASC LIMIT 1";
try {
$stmt = $pdo->prepare($sql_occ_min);
$stmt->execute(['start_d' => $start_date, 'end_d' => $end_date, 'start' => $start_date, 'end' => $end_date]);
if ($r = $stmt->fetch()) {
$response['occ_min_ward'] = $to_utf8($r['ward_name']);
$response['occ_min_val'] = (float)$r['bor_percent'];
}
$addLog('ตึกที่มีอัตราการครองเตียงต่ำสุด', $sql_occ_min);
} catch (\Exception $e) {
$addLog('ตึกที่มีอัตราการครองเตียงต่ำสุด', $sql_occ_min, $e->getMessage());
}
$sql_occ_wards = $sql_config['ipd']['occ_wards'] ?? "SELECT w.name AS ward_name, ROUND((SUM(a.admdate) / (w.bedcount * (DATEDIFF(:end_d,:start_d) + 1))) * 100 ,2) AS bor_percent FROM an_stat a LEFT OUTER JOIN ward w ON a.ward = w.ward WHERE a.dchdate BETWEEN :start AND :end AND w.bedcount > 0 GROUP BY a.ward,w.name,w.bedcount ORDER BY bor_percent DESC";
try {
$stmt = $pdo->prepare($sql_occ_wards);
$stmt->execute(['start_d' => $start_date, 'end_d' => $end_date, 'start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['occ_wards_chart']['labels'][] = $to_utf8($r['ward_name']);
$response['occ_wards_chart']['data'][] = (float)$r['bor_percent'];
}
$addLog('อัตราครองเตียงแยกราย WARD', $sql_occ_wards);
} catch (\Exception $e) {
$addLog('อัตราครองเตียงแยกราย WARD', $sql_occ_wards, $e->getMessage());
}
// ---------------------------------------------------------
// 2. ข้อมูลการส่งต่อผู้ป่วย (Refer IPD) - ยอดรวม
// ---------------------------------------------------------
$sql_ref_in = $sql_config['ipd']['refer_in'] ?? "SELECT count(vn) FROM referin WHERE refer_date BETWEEN :start AND :end AND vn IN(SELECT vn FROM ipt)";
$sql_ref_out = $sql_config['ipd']['refer_out'] ?? "SELECT count(vn) FROM referout WHERE refer_hospcode IN('10681') AND refer_date BETWEEN :start AND :end AND vn IN(SELECT an FROM ipt)";
$sql_ref_back = $sql_config['ipd']['refer_back'] ?? "SELECT count(distinct b.vn) FROM universal_head b WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end)";
try {
$stmt = $pdo->prepare($sql_ref_in);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_in_total'] = (int)$stmt->fetchColumn();
$addLog('รับ Refer In (IPD)', $sql_ref_in);
$stmt = $pdo->prepare($sql_ref_out);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_out_total'] = (int)$stmt->fetchColumn();
$addLog('ส่ง Refer Out (IPD)', $sql_ref_out);
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_back_total'] = (int)$stmt->fetchColumn();
$addLog('ส่งกลับ (Refer Back IPD)', $sql_ref_back);
} catch (\Exception $e) {
$addLog('Refer In/Out/Back (IPD)', '-', $e->getMessage());
}
// ---------------------------------------------------------
// 2.1 กราฟ Refer Trend (รายเดือน)
// ---------------------------------------------------------
$sql_ref_in_trend = $sql_config['ipd']['refer_in_trend'] ?? "SELECT DATE_FORMAT(refer_date, '%Y-%m') as ym, count(vn) as cnt FROM referin WHERE refer_date BETWEEN :start AND :end AND vn IN(SELECT vn FROM ipt) GROUP BY ym ORDER BY ym";
try {
$stmt = $pdo->prepare($sql_ref_in_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$response['refer_in_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr((int)$parts[0] + 543, 2, 2);
$response['refer_in_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟแนวโน้มรับ Refer In (รายเดือน)', $sql_ref_in_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มรับ Refer In (รายเดือน)', $sql_ref_in_trend, $e->getMessage());
}
$sql_ref_out_trend = $sql_config['ipd']['refer_out_trend'] ?? "SELECT DATE_FORMAT(refer_date, '%Y-%m') as ym, count(vn) as cnt FROM referout WHERE refer_hospcode IN('10681') AND refer_date BETWEEN :start AND :end AND vn IN(SELECT an FROM ipt) GROUP BY ym ORDER BY ym";
try {
$stmt = $pdo->prepare($sql_ref_out_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$response['refer_out_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr((int)$parts[0] + 543, 2, 2);
$response['refer_out_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟแนวโน้มส่ง Refer Out (รายเดือน)', $sql_ref_out_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มส่ง Refer Out (รายเดือน)', $sql_ref_out_trend, $e->getMessage());
}
// Top 10 Disease
$sql_ref_in_top = $sql_config['ipd']['refer_in_top'] ?? "SELECT count(a.icd10)as a ,b.name ,a.icd10 FROM referin a LEFT JOIN hospcode h ON a.refer_hospcode=h.hospcode LEFT JOIN ovst o ON a.vn=o.vn LEFT JOIN vn_stat vns ON o.vn=vns.vn LEFT JOIN an_stat ans ON o.an=ans.an LEFT JOIN icd101 b ON a.icd10=b.code WHERE a.refer_date BETWEEN :start AND :end AND (a.icd10 IS NOT NULL AND a.icd10 !='' AND a.icd10 !='U000') AND a.vn IN(SELECT vn FROM ipt) GROUP BY a.icd10, b.name ORDER BY a DESC LIMIT 10";
$sql_ref_out_top = $sql_config['ipd']['refer_out_top'] ?? "SELECT count(distinct d.vn)as a,b.icd10,c.name FROM referout d LEFT JOIN ipt i ON d.vn=i.an LEFT JOIN iptdiag b ON i.an=b.an LEFT JOIN icd101 c ON b.icd10=c.code WHERE d.refer_date BETWEEN :start AND :end AND b.diagtype='1' GROUP BY b.icd10, c.name ORDER BY a DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_ref_in_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_top10'][] = ['name' => $to_utf8($r['name']), 'val' => (int)$r['a']];
}
$addLog('10 อันดับโรคที่รับ Refer In', $sql_ref_in_top);
$stmt = $pdo->prepare($sql_ref_out_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_top10'][] = ['name' => $to_utf8($r['name']), 'val' => (int)$r['a']];
}
$addLog('10 อันดับโรคที่ส่ง Refer Out', $sql_ref_out_top);
} catch (\Exception $e) {
$addLog('Top 10 Disease Refer', '-', $e->getMessage());
}
// ---------------------------------------------------------
// 2.2 ข้อมูลเพิ่มเติม Refer Back (IPD)
// ---------------------------------------------------------
$response['refer_back_dest'] = [];
$response['refer_back_top10'] = [];
$response['refer_back_cause'] = [];
$sql_ref_back_dest = $sql_config['ipd']['refer_back_dest_trend'] ?? "SELECT COUNT(c.universal_item_value_text) AS cnt, concat(c.universal_item_value_text,\" \",h.name) AS dest FROM universal_head b LEFT JOIN universal_detail c ON b.universal_head_id = c.universal_head_id LEFT OUTER JOIN hospcode h ON c.universal_item_value_text=h.hospcode WHERE b.universal_form_id = '124' AND b.vn IN (SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end) AND c.universal_item_id = '4713' GROUP BY c.universal_item_value_text ORDER BY c.universal_item_value_text";
$sql_ref_back_top = $sql_config['ipd']['refer_back_top'] ?? "SELECT count(distinct b.vn) as a, i.icd10, c.name FROM universal_head b LEFT JOIN iptdiag i ON b.vn=i.an LEFT JOIN icd101 c ON i.icd10=c.code WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end) AND i.diagtype='1' GROUP BY i.icd10, c.name ORDER BY a DESC LIMIT 10";
$sql_ref_back_cause = $sql_config['ipd']['refer_back_cause'] ?? "SELECT 'ทำแผลต่อเนื่อง' as cause, 45 as cnt UNION SELECT 'รับยาต่อเนื่อง', 30 UNION SELECT 'ฉีดยา ATB', 15 UNION SELECT 'อื่นๆ', 10";
// Dest Trend (Now Dest Bar)
try {
$stmt = $pdo->prepare($sql_ref_back_dest);
$params = [];
if (preg_match_all('/\:start\b/i', $sql_ref_back_dest, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $start_date;
}
if (preg_match_all('/\:end\b/i', $sql_ref_back_dest, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $end_date;
}
$stmt->execute($params);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$r = array_change_key_case($row, CASE_LOWER);
$dest = $r['dest'] ?? '';
$cnt = $r['cnt'] ?? 0;
$response['refer_back_dest'][] = ['name' => $to_utf8($dest), 'val' => (int)$cnt];
}
$addLog('ส่งกลับ แยกปลายทาง', $sql_ref_back_dest);
} catch (\Exception $e) {
$addLog('ส่งกลับ แยกปลายทาง', $sql_ref_back_dest, $e->getMessage());
}
// Top 10 Disease
try {
$stmt = $pdo->prepare($sql_ref_back_top);
$params = [];
if (preg_match_all('/\:start\b/i', $sql_ref_back_top, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $start_date;
}
if (preg_match_all('/\:end\b/i', $sql_ref_back_top, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $end_date;
}
$stmt->execute($params);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$r = array_change_key_case($row, CASE_LOWER);
$name = $r['name'] ?? '';
$a = $r['a'] ?? 0;
$response['refer_back_top10'][] = ['name' => $to_utf8($name), 'val' => (int)$a];
}
$addLog('10 อันดับโรคที่ส่งกลับ', $sql_ref_back_top);
} catch (\Exception $e) {
$addLog('10 อันดับโรคที่ส่งกลับ', $sql_ref_back_top, $e->getMessage());
}
// Cause (Donut to Bar)
try {
$stmt = $pdo->prepare($sql_ref_back_cause);
$params = [];
if (preg_match_all('/\:start\b/i', $sql_ref_back_cause, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $start_date;
}
if (preg_match_all('/\:end\b/i', $sql_ref_back_cause, $matches)) {
foreach($matches[0] as $match) $params[ltrim($match, ':')] = $end_date;
}
$stmt->execute($params);
while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$r = array_change_key_case($row, CASE_LOWER);
$cause = $r['cause'] ?? '';
$cnt = $r['cnt'] ?? 0;
$response['refer_back_cause'][] = ['name' => $to_utf8($cause), 'val' => (int)$cnt];
}
$addLog('สาเหตุการส่งกลับ (Refer Back)', $sql_ref_back_cause);
} catch (\Exception $e) {
$addLog('สาเหตุการส่งกลับ (Refer Back)', $sql_ref_back_cause, $e->getMessage());
}
// ---------------------------------------------------------
// 3. ข้อมูล AdjRW (ผูกรวม Refer กับ IPT)
// ---------------------------------------------------------
$adj_queries = [
'in_low' => [$sql_config['ipd']['adjrw_in_low'] ?? "SELECT DATE_FORMAT(r.refer_date, '%Y-%m') as ym, count(r.vn) as cnt FROM referin r JOIN ipt i ON r.vn = i.vn WHERE r.refer_date BETWEEN :start AND :end AND i.adjrw < 0.5 GROUP BY ym ORDER BY ym", 'adjrw_in_low_trend', 'in_low'],
'in_high' => [$sql_config['ipd']['adjrw_in_high'] ?? "SELECT DATE_FORMAT(r.refer_date, '%Y-%m') as ym, count(r.vn) as cnt FROM referin r JOIN ipt i ON r.vn = i.vn WHERE r.refer_date BETWEEN :start AND :end AND i.adjrw >= 0.5 GROUP BY ym ORDER BY ym", 'adjrw_in_high_trend', 'in_high'],
'out_low' => [$sql_config['ipd']['adjrw_out_low'] ?? "SELECT DATE_FORMAT(r.refer_date, '%Y-%m') as ym, count(r.vn) as cnt FROM referout r JOIN ipt i ON r.vn = i.an WHERE r.refer_hospcode IN('10681') AND r.refer_date BETWEEN :start AND :end AND i.adjrw < 0.5 GROUP BY ym ORDER BY ym", 'adjrw_out_low_trend', 'out_low'],
'out_high' => [$sql_config['ipd']['adjrw_out_high'] ?? "SELECT DATE_FORMAT(r.refer_date, '%Y-%m') as ym, count(r.vn) as cnt FROM referout r JOIN ipt i ON r.vn = i.an WHERE r.refer_hospcode IN('10681') AND r.refer_date BETWEEN :start AND :end AND i.adjrw >= 0.5 GROUP BY ym ORDER BY ym", 'adjrw_out_high_trend', 'out_high']
];
foreach ($adj_queries as $key => $q) {
try {
$stmt = $pdo->prepare($q[0]);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$sum = 0;
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$response[$q[1]]['labels'][] = $thai_months[$parts[1]] . ' ' . substr((int)$parts[0] + 543, 2, 2);
$response[$q[1]]['data'][] = (int)$r['cnt'];
$sum += (int)$r['cnt'];
}
}
$response['adjrw_summary'][$q[2]] = $sum;
$addLog("กราฟ AdjRW: " . $key, $q[0]);
} catch (\Exception $e) {
$addLog("กราฟ AdjRW: " . $key, $q[0], $e->getMessage());
}
}
}
echo "---AJAX_IPD_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_IPD_END---";
exit;
}
// -------------------------------------------------------------------------
// ฟอร์มการกรอง
// -------------------------------------------------------------------------
$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');
$ipd_filter_html = '
<form class="ipd-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-emerald-500 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-emerald-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-emerald-400 transition-colors" onchange="toggleIpdFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 ipd_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-emerald-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-emerald-400 transition-colors">' . $options_html . '</select>
</div>
<div class="w-full md:w-1/2 flex gap-3 ipd_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" 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" value="' . $end_d . '"></div>
</div>
<button type="submit" class="w-full md:w-[15%] bg-emerald-600 hover:bg-emerald-700 active:scale-[0.98] text-white font-bold py-2.5 px-4 rounded-lg transition-all shadow-md shadow-emerald-500/20 h-[42px] flex items-center justify-center">
<i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหา
</button>
</form>
';
?>
<script>
function toggleIpdFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.ipd_fiscal_wrapper').style.display = 'block';
form.querySelector('.ipd_date_wrapper').style.display = 'none';
} else {
form.querySelector('.ipd_fiscal_wrapper').style.display = 'none';
form.querySelector('.ipd_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleIpdSqlLogModal()" class="fixed bottom-6 right-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 (IPD)</button>
<div id="ipd-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-emerald-400 mr-2"></i> บันทึกการคิวรีฐานข้อมูล IPD (SQL Logs)</h3>
<button onclick="toggleIpdSqlLogModal()" 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="ipd-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 toggleIpdSqlLogModal() {
const modal = document.getElementById('ipd-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; ?>
<!-- INCLUDE FILES -->
<?php include __DIR__ . '/tab_ipd_1_occ.php'; ?>
<?php include __DIR__ . '/tab_ipd_2_round.php'; ?>
<?php include __DIR__ . '/tab_ipd_3_refer.php'; ?>
<?php include __DIR__ . '/tab_ipd_4_adjrw.php'; ?>
<!-- JAVASCRIPT -->
<script>
const safeSetTextIPD = (id, text) => {
const el = document.getElementById(id);
if (el) el.innerText = text;
};
async function fetchIpdRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .ipd-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_ipd', '1');
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (!params.has(key)) params.append(key, value);
}
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_IPD_START---')) {
data = JSON.parse(text.split('---AJAX_IPD_START---')[1].split('---AJAX_IPD_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data) throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('ipd-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 (IPD):\n- " + errorDetails.join("\n- "));
}
// --- 1. Occupancy Cards ---
document.querySelectorAll('.dynamic-number-occ').forEach(el => el.innerText = data.occ_overall.toFixed(2));
document.querySelectorAll('.dynamic-width-occ').forEach(el => el.style.width = Math.min(data.occ_overall, 100) + '%');
safeSetTextIPD('max-occ-ward', data.occ_max_ward);
safeSetTextIPD('max-occ-val', data.occ_max_val.toFixed(2) + '%');
safeSetTextIPD('min-occ-ward', data.occ_min_ward);
safeSetTextIPD('min-occ-val', data.occ_min_val.toFixed(2) + '%');
const occChartContainer = document.getElementById('ward-occ-charts-container');
if (occChartContainer && data.occ_wards_chart.labels.length > 0) {
occChartContainer.innerHTML = '<canvas id="ipdWardOccBarChart" class="w-full h-[300px]"></canvas>';
const ctx = document.getElementById('ipdWardOccBarChart').getContext('2d');
if (window.appCharts['ipdWardOccBarChart']) window.appCharts['ipdWardOccBarChart'].destroy();
let gradBarNorm = ctx.createLinearGradient(0, 0, 0, 300);
gradBarNorm.addColorStop(0, '#10b981');
gradBarNorm.addColorStop(1, '#34d399');
let gradBarDang = ctx.createLinearGradient(0, 0, 0, 300);
gradBarDang.addColorStop(0, '#ef4444');
gradBarDang.addColorStop(1, '#f87171');
window.appCharts['ipdWardOccBarChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.occ_wards_chart.labels,
datasets: [{
label: 'อัตราครองเตียง (%)',
data: data.occ_wards_chart.data,
backgroundColor: c => c.dataset.data[c.dataIndex] > 100 ? gradBarDang : gradBarNorm,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
font: {
size: 10,
weight: 'bold'
},
color: c => c.dataset.data[c.dataIndex] > 100 ? '#ef4444' : '#10b981',
formatter: v => Number(v).toFixed(2) + '%'
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.occ_wards_chart.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (occChartContainer) {
occChartContainer.innerHTML = '<span class="text-slate-400 font-medium">ไม่พบข้อมูล</span>';
}
// --- 2. Refer In/Out ---
safeSetTextIPD('ipd-refer-in-total', data.refer_in_total.toLocaleString());
safeSetTextIPD('ipd-refer-out-total', data.refer_out_total.toLocaleString());
safeSetTextIPD('ipd-refer-back-total', data.refer_back_total.toLocaleString());
const updateBarChart = (id, dataObj, color) => {
const container = document.getElementById(id + '_container');
if (container) {
if (dataObj && dataObj.labels.length > 0) {
container.innerHTML = `<canvas id="${id}"></canvas>`;
const ctx = document.getElementById(id).getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
window.appCharts[id] = new Chart(ctx, {
type: 'bar',
data: {
labels: dataObj.labels,
datasets: [{
data: dataObj.data,
backgroundColor: color,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: color,
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...dataObj.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
};
// วาดกราฟแนวโน้มรายเดือนสำหรับ Refer
updateBarChart('referInTrendIpd', data.refer_in_trend, '#10b981');
updateBarChart('referOutTrendIpd', data.refer_out_trend, '#ef4444');
const makeList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
makeList('list-refer-in-disease-ipd', data.refer_in_top10);
makeList('list-refer-out-disease-ipd', data.refer_out_top10);
makeList('list-refer-back-disease-ipd', data.refer_back_top10);
// --- วาดกราฟ Refer Back Dest (Bar Chart) ---
const rbDestContainer = document.getElementById('referBackDestTrendIpd_container');
if (rbDestContainer) {
if (data.refer_back_dest && data.refer_back_dest.length > 0) {
const labels = data.refer_back_dest.map(item => item.name);
const values = data.refer_back_dest.map(item => item.val);
rbDestContainer.innerHTML = `<canvas id="referBackDestTrendIpd"></canvas>`;
const ctx = document.getElementById('referBackDestTrendIpd').getContext('2d');
if (window.appCharts['referBackDestTrendIpd']) window.appCharts['referBackDestTrendIpd'].destroy();
window.appCharts['referBackDestTrendIpd'] = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'จำนวนเคส',
data: values,
backgroundColor: ['#eab308', '#f97316', '#3b82f6', '#ef4444', '#10b981'],
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
datalabels: {
color: '#fff',
font: { weight: 'bold', size: 11 },
formatter: (value) => value > 0 ? value : ''
}
},
scales: {
y: { beginAtZero: true, grid: { display: false } },
x: { grid: { display: false } }
},
layout: { padding: { top: 10 } }
}
});
} else {
rbDestContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
// --- วาดกราฟ Refer Back Cause (Doughnut) ---
const rbCauseContainer = document.getElementById('referBackCauseIpd_container');
if (rbCauseContainer) {
if (data.refer_back_cause && data.refer_back_cause.length > 0) {
rbCauseContainer.innerHTML = `<canvas id="referBackCauseIpd"></canvas>`;
const ctx = document.getElementById('referBackCauseIpd').getContext('2d');
if (window.appCharts['referBackCauseIpd']) window.appCharts['referBackCauseIpd'].destroy();
const labels = data.refer_back_cause.map(d => d.name);
const values = data.refer_back_cause.map(d => d.val);
const colors = ['#1e3a8a', '#2563eb', '#38bdf8', '#bae6fd', '#7dd3fc'];
window.appCharts['referBackCauseIpd'] = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'จำนวนเคส',
data: values,
backgroundColor: '#6366f1',
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
plugins: {
legend: {
display: false
},
datalabels: {
color: '#fff',
font: { weight: 'bold', size: 10 },
formatter: (value) => value > 0 ? value : ''
}
},
scales: {
x: {
grid: { display: false },
beginAtZero: true
},
y: {
grid: { display: false }
}
},
layout: { padding: { top: 10, right: 30 } }
}
});
} else {
rbCauseContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
// --- 3. AdjRW Summary ---
document.querySelectorAll('.adjrw-in-low').forEach(el => el.innerText = data.adjrw_summary.in_low.toLocaleString());
document.querySelectorAll('.adjrw-in-high').forEach(el => el.innerText = data.adjrw_summary.in_high.toLocaleString());
document.querySelectorAll('.adjrw-out-low').forEach(el => el.innerText = data.adjrw_summary.out_low.toLocaleString());
document.querySelectorAll('.adjrw-out-high').forEach(el => el.innerText = data.adjrw_summary.out_high.toLocaleString());
// วาดกราฟแนวโน้มรายเดือนสำหรับ AdjRW
updateBarChart('adjrwInLowTrend', data.adjrw_in_low_trend, '#3b82f6');
updateBarChart('adjrwInHighTrend', data.adjrw_in_high_trend, '#6366f1');
updateBarChart('adjrwOutLowTrend', data.adjrw_out_low_trend, '#f43f5e');
updateBarChart('adjrwOutHighTrend', data.adjrw_out_high_trend, '#f97316');
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.ipd-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchIpdRealData(e.target);
});
});
});
</script>
@@ -0,0 +1,34 @@
<div id="tab-ipd-occ" class="tab-content hidden space-y-4 md:space-y-6">
<?= $ipd_filter_html ?>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mb-4 md:mb-6">
<div class="card-premium p-4 md:p-6 bg-gradient-to-br from-white to-orange-50 border-l-4 border-l-orange-500">
<div class="flex justify-between items-center mb-2">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base">อัตราครองเตียง (ภาพรวม)</h3><span class="text-2xl md:text-3xl font-black text-orange-600"><span class="dynamic-number-occ">0.00</span>%</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-3 mb-1 mt-3 md:mt-4 shadow-inner">
<div class="bg-gradient-to-r from-emerald-400 to-emerald-500 h-3 rounded-full border-t border-white/50 dynamic-width-occ" style="width: 0%"></div>
</div>
<p class="text-[9px] md:text-[10px] text-slate-500 text-right mt-2 font-bold">เป้าหมาย: ไม่เกิน 100.00%</p>
</div>
<div class="card-premium p-4 md:p-6 bg-gradient-to-br from-white to-rose-50 border-l-4 border-l-rose-500 flex flex-col justify-center">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-1">ตึกที่มีอัตราครองเตียงสูงสุด</h3>
<h2 class="text-xl md:text-2xl font-black text-rose-600 truncate" id="max-occ-ward">-</h2>
<p class="text-xs text-slate-500 mt-1 font-bold">อัตรา <span class="text-rose-500" id="max-occ-val">0.00%</span></p>
</div>
<div class="card-premium p-4 md:p-6 bg-gradient-to-br from-white to-emerald-50 border-l-4 border-l-emerald-500 flex flex-col justify-center">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-1">ตึกที่มีอัตราการครองเตียงต่ำสุด</h3>
<h2 class="text-xl md:text-2xl font-black text-emerald-600 truncate" id="min-occ-ward">-</h2>
<p class="text-xs text-slate-500 mt-1 font-bold">อัตรา <span class="text-emerald-500" id="min-occ-val">0.00%</span></p>
</div>
</div>
<div class="card-premium p-4 md:p-6">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base flex items-center mb-2 md:mb-0"><i class="fa-solid fa-building text-indigo-500 mr-2"></i> อัตราการครองเตียงแยกตามรายตึก (ข้อมูลรวมตามช่วงเวลาที่ค้นหา)</h3>
</div>
<div class="w-full" id="ward-occ-charts-container">
<div class="text-center text-slate-400 py-8 font-medium border border-dashed border-slate-300 rounded-lg">รอการค้นหาข้อมูล...</div>
</div>
</div>
</div>
@@ -0,0 +1,38 @@
<div id="tab-ipd-round" class="tab-content hidden space-y-4 md:space-y-6">
<?= $ipd_filter_html ?>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-6 mb-4 md:mb-6 max-w-4xl mx-auto">
<div class="card-premium p-4 md:p-6 bg-gradient-to-br from-white to-teal-50 border-l-4 border-l-teal-500">
<div class="flex justify-between items-center mb-2">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base">สัดส่วนแพทย์ Round ทัน (ภาพรวม)</h3><span class="text-2xl md:text-3xl font-black text-teal-600"><span class="dynamic-number-cond">100.00</span>%</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-3 mb-1 mt-3 md:mt-4 shadow-inner">
<div class="bg-teal-500 h-3 rounded-full dynamic-width border-t border-white/50" style="width: 100%"></div>
</div>
<p class="text-[9px] md:text-[10px] text-slate-500 text-right mt-2 font-bold">เป้าหมาย: 100.00% (ก่อน 12:00 น.)</p>
</div>
<div class="card-premium p-4 md:p-6 bg-gradient-to-br from-white to-blue-50 border-l-4 border-l-blue-500 flex flex-col justify-center items-center text-center">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-2">เวลาเฉลี่ยที่แพทย์ Round เสร็จ</h3>
<div class="flex items-center justify-center">
<i class="fa-regular fa-clock text-3xl text-blue-400 mr-3"></i>
<span class="text-3xl md:text-4xl font-black text-blue-600 dynamic-time-round">10:45 น.</span>
</div>
</div>
</div>
<div class="card-premium p-4 md:p-6 max-w-4xl mx-auto">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 md:mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base flex items-center mb-2 md:mb-0"><i class="fa-solid fa-user-md text-teal-500 mr-2"></i> สัดส่วนแพทย์ Round ทันก่อน 12.00 น. (รายตึก)</h3>
<div class="flex items-center">
<span class="text-xs font-bold text-slate-600 mr-2">แสดงผลเดือน:</span>
<select onchange="window.simulateRoundProgress()" class="bg-white border border-slate-300 text-slate-700 rounded-lg p-1.5 text-xs outline-none shadow-sm font-bold text-teal-600 cursor-pointer">
<option>ภาพรวม</option>
<option>ตุลาคม</option>
<option>พฤศจิกายน</option>
<option>ธันวาคม</option>
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-x-8" id="ward-round-progress-container"></div>
</div>
</div>
@@ -0,0 +1,84 @@
<div id="tab-ipd-refer" class="tab-content hidden space-y-4 md:space-y-6">
<?= $ipd_filter_html ?>
<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 (IPD)</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="ipd-refer-in-total">-</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 (IPD)</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="ipd-refer-out-total">-</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="ipd-refer-back-total">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ราย</span></h3></div>
</div>
</div>
</div>
<!-- Refer Charts & Lists -->
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-8 mb-4 border-l-4 border-emerald-500 pl-3">รายละเอียด รับ Refer In (IPD)</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-emerald-500 mr-2"></i> แนวโน้มรับ Refer In (รายเดือน)</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referInTrendIpd_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4 bg-emerald-50 p-2.5 rounded-lg border border-emerald-100"><i class="fa-solid fa-virus text-emerald-500 mr-2"></i> 10 อันดับโรคที่รับ Refer In (IPD)</h4>
<div class="h-[250px] md:h-[300px] overflow-y-auto pr-2 custom-scrollbar"><ul class="space-y-3 text-[11px] md:text-xs" id="list-refer-in-disease-ipd"><li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li></ul></div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-8 mb-4 border-l-4 border-red-500 pl-3">รายละเอียด ส่ง Refer Out (IPD)</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-red-500 mr-2"></i> แนวโน้มส่ง Refer Out (รายเดือน)</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referOutTrendIpd_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4 bg-red-50 p-2.5 rounded-lg border border-red-100"><i class="fa-solid fa-virus text-red-500 mr-2"></i> 10 อันดับโรคที่ส่ง Refer Out (IPD)</h4>
<div class="h-[250px] md:h-[300px] overflow-y-auto pr-2 custom-scrollbar"><ul class="space-y-3 text-[11px] md:text-xs" id="list-refer-out-disease-ipd"><li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li></ul></div>
</div>
</div>
<!-- Refer Back Section -->
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-8 mb-4 border-l-4 border-blue-500 pl-3">รายละเอียด ส่งกลับ (Refer Back)</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<!-- 1. จำนวนที่ส่งกลับ แยกปลายทาง (รายเดือน) -->
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-blue-500 mr-2"></i> จำนวนที่ส่งกลับ แยกปลายทาง (รายเดือน)</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referBackDestTrendIpd_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<!-- 2. 10 อันดับโรคที่ส่งกลับ -->
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4 bg-blue-50 p-2.5 rounded-lg border border-blue-100"><i class="fa-solid fa-virus text-blue-500 mr-2"></i> 10 อันดับโรคที่ส่งกลับ</h4>
<div class="h-[250px] md:h-[300px] overflow-y-auto pr-2 custom-scrollbar">
<ul class="space-y-3 text-[11px] md:text-xs" id="list-refer-back-disease-ipd">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<!-- 3. สาเหตุการส่งกลับ (Refer Back) -->
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-pie text-indigo-500 mr-2"></i> สาเหตุการส่งกลับ (Refer Back)</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referBackCauseIpd_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
</div>
</div>
@@ -0,0 +1,42 @@
<div id="tab-ipd-adjrw" class="tab-content hidden space-y-4 md:space-y-6">
<?= $ipd_filter_html ?>
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-4 mb-6 mt-4">
<div class="card-premium p-4 border-l-4 border-l-blue-500 flex flex-col justify-center">
<p class="text-[10px] md:text-xs font-bold text-slate-500">สะสมรับ Refer (Adj RW < 0.5)</p>
<h3 class="text-xl md:text-2xl font-black text-blue-600 mt-1 adjrw-in-low">-</h3>
</div>
<div class="card-premium p-4 border-l-4 border-l-indigo-500 flex flex-col justify-center">
<p class="text-[10px] md:text-xs font-bold text-slate-500">สะสมรับ Refer (Adj RW >= 0.5)</p>
<h3 class="text-xl md:text-2xl font-black text-indigo-600 mt-1 adjrw-in-high">-</h3>
</div>
<div class="card-premium p-4 border-l-4 border-l-rose-500 flex flex-col justify-center">
<p class="text-[10px] md:text-xs font-bold text-slate-500">สะสมส่ง Refer (Adj RW < 0.5)</p>
<h3 class="text-xl md:text-2xl font-black text-rose-600 mt-1 adjrw-out-low">-</h3>
</div>
<div class="card-premium p-4 border-l-4 border-l-orange-500 flex flex-col justify-center">
<p class="text-[10px] md:text-xs font-bold text-slate-500">สะสมส่ง Refer (Adj RW >= 0.5)</p>
<h3 class="text-xl md:text-2xl font-black text-orange-600 mt-1 adjrw-out-high">-</h3>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-base md:text-lg mt-6 mb-4 border-l-4 border-indigo-500 pl-3">สถิติ Sum AdjRW (แนวโน้มรายเดือน)</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-blue-500 mr-2"></i> จำนวนรับ Refer In (Adj RW < 0.5) รายเดือน</h4>
<div class="h-[250px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="adjrwInLowTrend_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-indigo-500 mr-2"></i> จำนวนรับ Refer In (Adj RW >= 0.5) รายเดือน</h4>
<div class="h-[250px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="adjrwInHighTrend_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-rose-500 mr-2"></i> จำนวนส่ง Refer Out (Adj RW < 0.5) รายเดือน</h4>
<div class="h-[250px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="adjrwOutLowTrend_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-orange-500 mr-2"></i> จำนวนส่ง Refer Out (Adj RW >= 0.5) รายเดือน</h4>
<div class="h-[250px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="adjrwOutHighTrend_container"><span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span></div>
</div>
</div>
</div>
@@ -0,0 +1,982 @@
<?php
// views/tab_opd.php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/sql_config.php';
$show_sql_log_btn = 'N';
$isOpdAjax = isset($_GET['ajax_opd']) && $_GET['ajax_opd'] == '1';
$isOpdCondAjax = isset($_GET['ajax_opd_cond']) && $_GET['ajax_opd_cond'] == '1';
// =========================================================================
// 1. ส่วนประมวลผล AJAX ดึงข้อมูล "แบบมีเงื่อนไข (Condition)"
// =========================================================================
if ($isOpdCondAjax) {
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
// รับค่าพารามิเตอร์ต่างๆ จากฟอร์มเฉพาะของหน้า Condition
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$appoint = $_GET['appoint'] ?? '';
$step_from = $_GET['step_from'] ?? 'vsttime';
$step_to = $_GET['step_to'] ?? 'service6';
$clinic_id = $_GET['clinic_id'] ?? '';
$time_period = $_GET['time_period'] ?? '';
$lab_status = $_GET['lab_status'] ?? '';
$xray_status = $_GET['xray_status'] ?? '';
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'custom_total' => 0],
'clinics_wait' => [],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'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];
};
// แปลงรหัสการนัดหมายเป็นข้อความให้อ่านง่าย
$appoint_names = ['' => 'ทั้งหมด', '1' => 'มาตามนัด', '2' => 'มาไม่ตรงนัด', '3' => 'Walk in (ไม่นัด)'];
$appoint_label = $appoint_names[$appoint] ?? 'ทั้งหมด';
$addLog("ตรวจสอบเงื่อนไขหลัก", "ค้นหา: $start_date ถึง $end_date | นัด: $appoint_label | คลินิก: $clinic_id | เวลา: $time_period | Lab: $lab_status | X-ray: $xray_status");
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'อายุรกรรมโรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'คลินิกหัวใจ'];
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
// 1. Mapping ฟิลด์สำหรับคำนวณและกรองเวลา (ทำก่อนสร้างเงื่อนไข)
$allowed_cols = ['vsttime' => 'o.vsttime', 'service4' => 's.service4', 'service11' => 's.service11', 'service5' => 's.service5', 'service12' => 's.service12', 'service6' => 's.service6'];
$col_from = isset($allowed_cols[$step_from]) ? $allowed_cols[$step_from] : "o.vsttime";
$col_to = isset($allowed_cols[$step_to]) ? $allowed_cols[$step_to] : "s.service6";
// --- สร้าง Dynamic WHERE Clause ---
$where_cond = "s.vstdate BETWEEN :start AND :end ";
$params = ['start' => $start_date, 'end' => $end_date];
// คงเงื่อนไขพื้นฐานเรื่องคนไข้ต้องผ่านช่องชำระเงิน
$where_cond .= " AND s.vn IN (SELECT vn FROM rx_operator WHERE pay='Y') ";
// กรองช่วงเวลา (อิงจากเวลาของจุดเริ่มต้น $col_from ที่ผู้ใช้เลือก)
if ($time_period === 'morning') {
$where_cond .= " AND $col_from BETWEEN '08:00:00' AND '12:00:00' ";
} elseif ($time_period === 'afternoon') {
$where_cond .= " AND $col_from BETWEEN '12:00:01' AND '16:00:00' ";
} else {
$where_cond .= " AND $col_from BETWEEN '08:00:00' AND '16:00:00' ";
}
// กรองคลินิก
if ($clinic_id !== '') {
$where_cond .= " AND o.main_dep = :clinic_id ";
$params['clinic_id'] = $clinic_id;
}
// กรอง Lab / X-Ray
if ($lab_status === '1') {
$where_cond .= " AND s.vn IN (SELECT vn FROM lab_head) ";
} elseif ($lab_status === '0') {
$where_cond .= " AND s.vn NOT IN (SELECT vn FROM lab_head) ";
}
if ($xray_status === '1') {
$where_cond .= " AND s.vn IN (SELECT vn FROM xray_head) ";
} elseif ($xray_status === '0') {
$where_cond .= " AND s.vn NOT IN (SELECT vn FROM xray_head) ";
}
// กรองการนัดหมาย พร้อมเก็บ Log แจกแจงว่าใช้คำสั่งย่อยอะไร
$appoint_sql_log = "ไม่ได้จำกัดเงื่อนไขการนัดหมาย (ค้นหาทั้งหมด)";
if ($appoint === '1') {
$appoint_sql_log = "AND s.vn IN (SELECT visit_vn FROM oapp)";
$where_cond .= " $appoint_sql_log ";
} elseif ($appoint === '2') {
// ชุดคำสั่งใหม่สำหรับ "มาไม่ตรงนัด" (ใช้ตัวแปร :start และ :end เพื่อให้กรองตามวันที่ที่เลือกได้)
$appoint_sql_log = "AND s.vn IN (SELECT o.vn FROM ovst o INNER JOIN oapp a ON a.visit_vn = o.vn WHERE o.vstdate BETWEEN :start AND :end AND o.vstdate = a.nextdate)";
$where_cond .= " $appoint_sql_log ";
} elseif ($appoint === '3') {
$appoint_sql_log = "AND NOT EXISTS (SELECT 1 FROM oapp WHERE visit_vn = s.vn)";
$where_cond .= " $appoint_sql_log ";
}
$addLog("ชุดคำสั่งย่อยเฉพาะ [การนัดหมาย: $appoint_label]", $appoint_sql_log);
// ผูกคำสั่งหลัก (Base Query)
$base_from = "FROM service_time s JOIN ovst o ON s.vn = o.vn
WHERE $where_cond
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)";
if ($pdo) {
// 1. SQL: Flowchart (ตามเงื่อนไข)
$sql_flow = "SELECT
IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1,
IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2,
IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3,
IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4,
IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5,
IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as custom_total
$base_from " . ($clinic_id === '' ? "AND o.main_dep='000'" : ""); // ถ้าไม่เลือกคลินิก ให้ default เป็น 000 ก่อน
try {
$stmt = $pdo->prepare($sql_flow);
$stmt->execute($params);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'custom_total' => (float)$row['custom_total']];
}
$addLog("Flowchart ระยะเวลารอคอย [นัดหมาย: $appoint_label]", $sql_flow);
} catch (\Exception $e) {
$addLog("Flowchart ระยะเวลารอคอย [นัดหมาย: $appoint_label]", $sql_flow, $e->getMessage());
}
// 2. SQL: Clinic Wait (ตามเงื่อนไข)
$sql_clinics = "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as start2finish
$base_from AND o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132')
GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute($params);
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$response['clinics_wait'][] = ['name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($response['clinics_wait'], function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$addLog("ระยะเวลารอคอยแต่ละคลินิก [นัดหมาย: $appoint_label]", $sql_clinics);
} catch (\Exception $e) {
$addLog("ระยะเวลารอคอยแต่ละคลินิก [นัดหมาย: $appoint_label]", $sql_clinics, $e->getMessage());
}
// 3. SQL: Trend Chart (ตามเงื่อนไข)
$sql_trend = "SELECT DATE_FORMAT(s.vstdate, '%Y-%m') as ym, IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as avg_wait, COUNT(DISTINCT s.vn) as pt_count
$base_from " . ($clinic_id === '' ? "AND o.main_dep='000'" : "") . " GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute($params);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog("กราฟแนวโน้มระยะเวลารอคอย [นัดหมาย: $appoint_label]", $sql_trend);
} catch (\Exception $e) {
$addLog("กราฟแนวโน้มระยะเวลารอคอย [นัดหมาย: $appoint_label]", $sql_trend, $e->getMessage());
}
}
echo "---AJAX_OPD_COND_START---";
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
echo "---AJAX_OPD_COND_END---";
exit;
}
// =========================================================================
// 2. ส่วนประมวลผล AJAX ดึงข้อมูล "ภาพรวม OPD ทั่วไป" (ทำงานเมื่อกดค้นหาหน้าอื่นๆ)
// =========================================================================
if ($isOpdAjax) {
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);
}
}
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'โรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'หัวใจ'];
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'total' => 0],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'clinics_wait' => [],
'telemed_total' => 0,
'telemed_clinics' => [],
'home_delivery_total' => 0,
'home_delivery_trend' => ['labels' => [], 'data' => []],
'refer_in_total' => 0,
'refer_out_total' => 0,
'refer_back_total' => 0,
'refer_in_dest' => ['labels' => [], 'data' => []],
'refer_out_dest' => ['labels' => [], 'data' => []],
'refer_in_top10' => [],
'refer_out_top10' => [],
'doc_time_overall' => 0,
'doc_time_clinics' => [],
'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('ระบบตรวจสอบช่วงวันที่ (Date Verification)', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// SQL: Flowchart
$sql1 = $sql_config['opd']['flowchart'] ?? "SELECT IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1, IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2, IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3, IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as total_wait FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000'";
try {
$stmt = $pdo->prepare($sql1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'total' => (float)$row['total_wait']];
}
$addLog('Flowchart ระยะเวลารอคอย', $sql1);
} catch (\Exception $e) {
$addLog('Flowchart ระยะเวลารอคอย', $sql1, $e->getMessage());
}
// SQL: Trend Chart
$sql_trend = $sql_config['opd']['trend'] ?? "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 JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000' GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend, $e->getMessage());
}
// SQL: Clinic Wait
$sql_clinics = $sql_config['opd']['clinics'] ?? "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as start2finish FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$clinics_data = [];
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$clinics_data[] = ['dep' => $row['main_dep'], 'name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($clinics_data, function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$response['clinics_wait'] = $clinics_data;
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics);
} catch (\Exception $e) {
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics, $e->getMessage());
}
// SQL: Telemedicine
$sql_tele1 = $sql_config['opd']['tele_total'] ?? "SELECT count(vn) FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end";
$sql_tele2 = $sql_config['opd']['tele_clinics'] ?? "SELECT main_dep, count(vn) as cnt FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end AND main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY main_dep ORDER BY cnt DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_tele1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['telemed_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวม Telemedicine', $sql_tele1);
$stmt2 = $pdo->prepare($sql_tele2);
$stmt2->execute(['start' => $start_date, 'end' => $end_date]);
$tele_clinics = [];
while ($r = $stmt2->fetch()) {
$dep_code = $r['main_dep'];
$name = isset($dep_names[$dep_code]) ? $dep_names[$dep_code] : 'รหัส: ' . $dep_code;
$tele_clinics[] = ['name' => $name, 'val' => (int)$r['cnt']];
}
$response['telemed_clinics'] = $tele_clinics;
$addLog('กราฟ 10 อันดับคลินิกให้บริการ Telemedicine', $sql_tele2);
} catch (\Exception $e) {
$addLog('Telemedicine', $sql_tele1 . ' | ' . $sql_tele2, $e->getMessage());
}
// SQL: ยอดรวม OPD Refill (การส่งยาที่บ้าน)
$sql_refill = $sql_config['opd']['refill_total'] ?? "SELECT count(vn) FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2)";
try {
$stmt = $pdo->prepare($sql_refill);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date]);
$response['home_delivery_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill);
} catch (\Exception $e) {
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill, $e->getMessage());
}
// SQL: กราฟ OPD Refill Trend (รายเดือน)
$sql_refill_trend = $sql_config['opd']['refill_trend'] ?? "SELECT DATE_FORMAT(vstdate, '%Y-%m') as ym, count(vn) as cnt FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2) AND vstdate BETWEEN :start3 AND :end3 GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_refill_trend);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date, 'start3' => $start_date, 'end3' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['home_delivery_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['home_delivery_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend);
} catch (\Exception $e) {
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend, $e->getMessage());
}
// SQL: Refer
$sql_ref_in1 = $sql_config['opd']['refer_in'] ?? "SELECT count(vn) FROM referin WHERE refer_hospcode IN ('09192','11359') AND refer_date BETWEEN :start AND :end";
$sql_ref_out1 = $sql_config['opd']['refer_out'] ?? "SELECT count(vn) FROM referout WHERE refer_hospcode IN ('10681') AND refer_date BETWEEN :start AND :end";
$sql_ref_back = $sql_config['opd']['refer_back'] ?? "SELECT count(distinct b.vn) FROM universal_head b WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end)";
$sql_ref_in_dest = $sql_config['opd']['refer_in_dest'] ?? "SELECT count(a.vn) as a, h.name FROM referin a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end AND a.refer_hospcode IS NOT NULL AND a.refer_hospcode NOT IN ('') GROUP BY a.refer_hospcode, h.name ORDER BY a DESC LIMIT 5";
$sql_ref_out_dest = $sql_config['opd']['refer_out_dest'] ?? "SELECT count(distinct a.vn) as vn, h.name FROM referout a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end GROUP BY a.refer_hospcode, h.name ORDER BY vn DESC LIMIT 5";
$sql_ref_in_top = $sql_config['opd']['refer_in_top'] ?? "SELECT count(a.icd10) as c, b.name FROM referin a LEFT JOIN icd101 b ON a.icd10=b.code WHERE a.refer_date BETWEEN :start AND :end AND a.icd10 !='' AND a.icd10 !='U000' GROUP BY a.icd10, b.name ORDER BY c DESC LIMIT 10";
$sql_ref_out_top = $sql_config['opd']['refer_out_top'] ?? "SELECT count(distinct d.vn) as c, c_icd.name FROM referout d LEFT JOIN ovstdiag b ON d.vn=b.vn LEFT JOIN ovst o ON d.vn=o.vn LEFT JOIN icd101 c_icd ON b.icd10=c_icd.code WHERE d.refer_date BETWEEN :start AND :end AND o.an IS NULL AND b.diagtype='1' GROUP BY b.icd10, c_icd.name ORDER BY c DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_ref_in1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_in_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมรับ Refer In', $sql_ref_in1);
$stmt = $pdo->prepare($sql_ref_out1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_out_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมส่ง Refer Out', $sql_ref_out1);
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_back_total'] = (int)$stmt->fetchColumn();
$addLog('ส่งกลับ (Refer Back)', $sql_ref_back);
$stmt = $pdo->prepare($sql_ref_in_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_in_dest']['data'][] = (int)$r['a'];
}
$addLog('กราฟ Refer In แยก รพ.ต้นทาง', $sql_ref_in_dest);
$stmt = $pdo->prepare($sql_ref_out_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_out_dest']['data'][] = (int)$r['vn'];
}
$addLog('กราฟ Refer Out แยก รพ.ปลายทาง', $sql_ref_out_dest);
$stmt = $pdo->prepare($sql_ref_in_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค รับ Refer In', $sql_ref_in_top);
$stmt = $pdo->prepare($sql_ref_out_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค ส่ง Refer Out', $sql_ref_out_top);
} catch (\Exception $e) {
$addLog('การส่งต่อ (Refer)', 'หลายคำสั่ง', $e->getMessage());
}
// SQL: Doctor Time
$sql_doc = $sql_config['opd']['doc_time'] ?? "SELECT x.main_dep, COUNT(*) AS total_days, SUM(CASE WHEN x.first_service < '09:00:00' THEN 1 ELSE 0 END) AS before_9am_days FROM (SELECT v.vstdate, d.name, o.main_dep, MIN(s.service5) AS first_service FROM vn_stat v JOIN ovst o ON v.vn=o.vn JOIN service_time s ON v.vn=s.vn JOIN doctor d ON v.dx_doctor=d.code WHERE v.vstdate BETWEEN :start AND :end AND v.dx_doctor <> '' 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, o.main_dep) as x GROUP BY x.main_dep";
try {
$stmt = $pdo->prepare($sql_doc);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$doc_totals = 0;
$doc_before9 = 0;
$clinics_doc = [];
while ($r = $stmt->fetch()) {
$doc_totals += $r['total_days'];
$doc_before9 += $r['before_9am_days'];
$dep = $r['main_dep'];
if (!isset($clinics_doc[$dep])) {
$clinics_doc[$dep] = ['tot' => 0, 'b9' => 0];
}
$clinics_doc[$dep]['tot'] += $r['total_days'];
$clinics_doc[$dep]['b9'] += $r['before_9am_days'];
}
$response['doc_time_overall'] = $doc_totals > 0 ? (float)number_format(($doc_before9 / $doc_totals) * 100, 2) : 0;
foreach ($clinics_doc as $dep => $val) {
if (isset($dep_names[$dep])) {
$pct = $val['tot'] > 0 ? ($val['b9'] / $val['tot']) * 100 : 0;
$response['doc_time_clinics'][] = ['name' => $dep_names[$dep], 'pct' => (float)number_format($pct, 2)];
}
}
usort($response['doc_time_clinics'], function ($a, $b) {
return $b['pct'] <=> $a['pct'];
});
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc);
} catch (\Exception $e) {
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc, $e->getMessage());
}
}
echo "---AJAX_OPD_START---";
$json_output = @json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_output) {
echo json_encode(['error' => 'JSON Encode Error: ' . json_last_error_msg(), 'flowchart' => ['total' => 0]]);
} else {
echo $json_output;
}
echo "---AJAX_OPD_END---";
exit;
}
// =========================================================================
// 3. สร้างฟอร์มการกรองมาตรฐาน
// =========================================================================
$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');
$opd_filter_html = '
<form class="opd-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-500 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="toggleOpdFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 opd_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="opd-year-selector 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 opd_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" 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" 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 toggleOpdFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.opd_fiscal_wrapper').style.display = 'block';
form.querySelector('.opd_date_wrapper').style.display = 'none';
} else {
form.querySelector('.opd_fiscal_wrapper').style.display = 'none';
form.querySelector('.opd_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleSqlLogModal()" class="fixed bottom-6 right-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 (Debug)</button>
<div id="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> บันทึกการคิวรีฐานข้อมูล (SQL Execution Logs)</h3>
<button onclick="toggleSqlLogModal()" 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="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 toggleSqlLogModal() {
const modal = document.getElementById('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; ?>
<!-- INCLUDE FILES -->
<?php include __DIR__ . '/tab_opd_1_overview.php'; ?>
<?php include __DIR__ . '/tab_opd_2_condition.php'; ?>
<?php include __DIR__ . '/tab_opd_3_service.php'; ?>
<?php include __DIR__ . '/tab_opd_4_refer.php'; ?>
<?php include __DIR__ . '/tab_opd_5_doctor.php'; ?>
<?php include __DIR__ . '/tab_opd_6_satisfaction.php'; ?>
<!-- JAVASCRIPT -->
<script>
const safeSetTextBySelector = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
async function fetchOpdRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .opd-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_opd', '1');
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (!params.has(key)) params.append(key, value);
}
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_START---')[1].split('---AJAX_OPD_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data || typeof data.flowchart === 'undefined') throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('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' เพื่อตรวจสอบสาเหตุ");
}
// --- 1. Flowchart (Overview) ---
safeSetTextBySelector('.opd-total-time-display-ov', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-1-ov', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-2-ov', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-3-ov', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-4-ov', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-5-ov', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// --- 2. Trend Charts ---
const updateTrendChart = (chartId) => {
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
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)');
if (window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
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_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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
};
updateTrendChart('opdWaitTimeChartTabOv');
// --- 3. Clinic Grid ---
const clinicIconsDb = {
'ผู้ป่วยนอก (ทั่วไป)': 'fa-hospital-user',
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'กระดูก': 'fa-bone',
'จิตเวช': 'fa-brain',
'ทันตกรรม': 'fa-tooth'
};
const renderClinicGrid = (id) => {
const el = document.getElementById(id);
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-emerald-500 via-teal-500 to-emerald-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300 text-lg block mb-2"></i>ไม่พบข้อมูลคลินิก</div>';
}
}
};
renderClinicGrid('db-clinic-wait-cards');
// --- 4. Services (Telemedicine & OPD Refill) ---
if (document.getElementById('db-telemed-total')) document.getElementById('db-telemed-total').innerText = data.telemed_total > 0 ? data.telemed_total.toLocaleString() : '-';
if (document.getElementById('db-home-delivery')) document.getElementById('db-home-delivery').innerText = data.home_delivery_total > 0 ? data.home_delivery_total.toLocaleString() : '-';
const teleContainer = document.getElementById('dbServiceTeleChart_container');
if (teleContainer) {
if (data.telemed_clinics && data.telemed_clinics.length > 0) {
teleContainer.innerHTML = '<canvas id="dbServiceTeleChart"></canvas>';
const ctx = document.getElementById('dbServiceTeleChart').getContext('2d');
if (window.appCharts['dbServiceTeleChart']) window.appCharts['dbServiceTeleChart'].destroy();
window.appCharts['dbServiceTeleChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.telemed_clinics.map(d => d.name),
datasets: [{
label: 'จำนวนครั้ง',
data: data.telemed_clinics.map(d => d.val),
backgroundColor: '#a855f7',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#a855f7',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.telemed_clinics.map(d => d.val), 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
teleContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
let delivContainer = document.getElementById('serviceDeliveryChart_container');
if (!delivContainer) {
document.querySelectorAll('#tab-opd-service h3').forEach(h => {
if (h.innerText.includes('OPD Refill (รายเดือน)')) delivContainer = h.nextElementSibling;
});
}
if (delivContainer) {
if (data.home_delivery_trend && data.home_delivery_trend.labels && data.home_delivery_trend.labels.length > 0) {
delivContainer.innerHTML = '<canvas id="serviceDeliveryChart"></canvas>';
const ctx = document.getElementById('serviceDeliveryChart').getContext('2d');
if (window.appCharts['serviceDeliveryChart']) window.appCharts['serviceDeliveryChart'].destroy();
window.appCharts['serviceDeliveryChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.home_delivery_trend.labels,
datasets: [{
label: 'จำนวน (ครั้ง)',
data: data.home_delivery_trend.data,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#1d4ed8',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.home_delivery_trend.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
delivContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
// --- 5. Refer ---
if (document.getElementById('db-refer-in-opd')) document.getElementById('db-refer-in-opd').innerText = data.refer_in_total > 0 ? data.refer_in_total.toLocaleString() : '-';
if (document.getElementById('db-refer-out-opd')) document.getElementById('db-refer-out-opd').innerText = data.refer_out_total > 0 ? data.refer_out_total.toLocaleString() : '-';
if (document.getElementById('db-refer-back-opd')) document.getElementById('db-refer-back-opd').innerText = data.refer_back_total > 0 ? data.refer_back_total.toLocaleString() : '-';
const updateBarChart = (id, dataObj, color) => {
const container = document.getElementById(id + '_container');
if (container) {
if (dataObj && dataObj.labels.length > 0) {
container.innerHTML = `<canvas id="${id}"></canvas>`;
const ctx = document.getElementById(id).getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
window.appCharts[id] = new Chart(ctx, {
type: 'bar',
data: {
labels: dataObj.labels,
datasets: [{
data: dataObj.data,
backgroundColor: color,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: color,
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...dataObj.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
};
updateBarChart('referInChartOpd', data.refer_in_dest, '#10b981');
updateBarChart('referOutChartOpd', data.refer_out_dest, '#ef4444');
const makeList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
makeList('db-list-refer-in', data.refer_in_top10);
makeList('db-list-refer-out', data.refer_out_top10);
// --- 6. Doctor Check ---
if (document.getElementById('db-doc-overall')) document.getElementById('db-doc-overall').innerText = data.doc_time_overall > 0 ? data.doc_time_overall.toFixed(2) : '-';
if (document.getElementById('db-doc-overall-bar')) document.getElementById('db-doc-overall-bar').style.width = Math.min(100, data.doc_time_overall) + '%';
const docGrid = document.getElementById('db-doctor-clinic-grid');
if (docGrid) {
if (data.doc_time_clinics && data.doc_time_clinics.length > 0) {
docGrid.innerHTML = data.doc_time_clinics.map(c => `<div class="card-premium p-3 md:p-4 border-l-4 border-l-blue-500 shadow-sm"><div class="flex justify-between items-center mb-1.5"><span class="text-xs md:text-sm font-bold text-slate-700">${c.name}</span><span class="text-xs md:text-sm font-black ${c.pct >= 80 ? 'text-emerald-600' : 'text-orange-500'}">${c.pct.toFixed(2)}%</span></div><div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden shadow-inner"><div class="bg-gradient-to-r ${c.pct >= 80 ? 'from-emerald-400 to-emerald-500' : 'from-orange-400 to-red-500'} h-full rounded-full w-full" style="width: ${c.pct}%"></div></div></div>`).join('');
} else {
docGrid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลแพทย์ลงตรวจ</div>';
}
}
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.opd-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdRealData(e.target);
});
});
});
</script>
@@ -0,0 +1,815 @@
<?php
// views/tab_opd.php
require_once __DIR__ . '/../includes/db.php';
$show_sql_log_btn = 'N';
$isOpdAjax = isset($_GET['ajax_opd']) && $_GET['ajax_opd'] == '1';
// =========================================================================
// 1. ส่วนประมวลผล AJAX ดึงข้อมูลจากฐานข้อมูล (รันเมื่อกดปุ่มค้นหา)
// =========================================================================
if ($isOpdAjax) {
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);
}
}
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'โรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'หัวใจ'];
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'total' => 0],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'clinics_wait' => [],
'telemed_total' => 0,
'telemed_clinics' => [],
'home_delivery_total' => 0,
'home_delivery_trend' => ['labels' => [], 'data' => []],
'refer_in_total' => 0,
'refer_out_total' => 0,
'refer_back_total' => 0, // เพิ่มตัวแปรยอดส่งกลับ
'refer_in_dest' => ['labels' => [], 'data' => []],
'refer_out_dest' => ['labels' => [], 'data' => []],
'refer_in_top10' => [],
'refer_out_top10' => [],
'doc_time_overall' => 0,
'doc_time_clinics' => [],
'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('ระบบตรวจสอบช่วงวันที่ (Date Verification)', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// SQL: Flowchart
$sql1 = "SELECT IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1, IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2, IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3, IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as total_wait FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000'";
try {
$stmt = $pdo->prepare($sql1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'total' => (float)$row['total_wait']];
}
$addLog('Flowchart ระยะเวลารอคอย', $sql1);
} catch (\Exception $e) {
$addLog('Flowchart ระยะเวลารอคอย', $sql1, $e->getMessage());
}
// SQL: Trend Chart
$sql_trend = "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 JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000' GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend, $e->getMessage());
}
// SQL: Clinic Wait
$sql_clinics = "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as start2finish FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$clinics_data = [];
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$clinics_data[] = ['dep' => $row['main_dep'], 'name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($clinics_data, function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$response['clinics_wait'] = $clinics_data;
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics);
} catch (\Exception $e) {
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics, $e->getMessage());
}
// SQL: Telemedicine
$sql_tele1 = "SELECT count(vn) FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end";
$sql_tele2 = "SELECT main_dep, count(vn) as cnt FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end AND main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY main_dep ORDER BY cnt DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_tele1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['telemed_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวม Telemedicine', $sql_tele1);
$stmt2 = $pdo->prepare($sql_tele2);
$stmt2->execute(['start' => $start_date, 'end' => $end_date]);
$tele_clinics = [];
while ($r = $stmt2->fetch()) {
$dep_code = $r['main_dep'];
$name = isset($dep_names[$dep_code]) ? $dep_names[$dep_code] : 'รหัส: ' . $dep_code;
$tele_clinics[] = ['name' => $name, 'val' => (int)$r['cnt']];
}
$response['telemed_clinics'] = $tele_clinics;
$addLog('กราฟ 10 อันดับคลินิกให้บริการ Telemedicine', $sql_tele2);
} catch (\Exception $e) {
$addLog('Telemedicine', $sql_tele1 . ' | ' . $sql_tele2, $e->getMessage());
}
// SQL: ยอดรวม OPD Refill (การส่งยาที่บ้าน)
$sql_refill = "SELECT count(vn) FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2)";
try {
$stmt = $pdo->prepare($sql_refill);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date]);
$response['home_delivery_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill);
} catch (\Exception $e) {
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill, $e->getMessage());
}
// SQL: กราฟ OPD Refill Trend (รายเดือน)
$sql_refill_trend = "SELECT DATE_FORMAT(vstdate, '%Y-%m') as ym, count(vn) as cnt FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2) AND vstdate BETWEEN :start3 AND :end3 GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_refill_trend);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date, 'start3' => $start_date, 'end3' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['home_delivery_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['home_delivery_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend);
} catch (\Exception $e) {
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend, $e->getMessage());
}
// SQL: Refer
$sql_ref_in1 = "SELECT count(vn) FROM referin WHERE refer_hospcode IN ('09192','11359') AND refer_date BETWEEN :start AND :end";
$sql_ref_out1 = "SELECT count(vn) FROM referout WHERE refer_hospcode IN ('10681') AND refer_date BETWEEN :start AND :end";
$sql_ref_back = "SELECT count(distinct b.vn) FROM universal_head b WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end)";
$sql_ref_in_dest = "SELECT count(a.vn) as a, h.name FROM referin a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end AND a.refer_hospcode IS NOT NULL AND a.refer_hospcode NOT IN ('') GROUP BY a.refer_hospcode, h.name ORDER BY a DESC LIMIT 5";
$sql_ref_out_dest = "SELECT count(distinct a.vn) as vn, h.name FROM referout a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end GROUP BY a.refer_hospcode, h.name ORDER BY vn DESC LIMIT 5";
$sql_ref_in_top = "SELECT count(a.icd10) as c, b.name FROM referin a LEFT JOIN icd101 b ON a.icd10=b.code WHERE a.refer_date BETWEEN :start AND :end AND a.icd10 !='' AND a.icd10 !='U000' GROUP BY a.icd10, b.name ORDER BY c DESC LIMIT 10";
$sql_ref_out_top = "SELECT count(distinct d.vn) as c, c_icd.name FROM referout d LEFT JOIN ovstdiag b ON d.vn=b.vn LEFT JOIN ovst o ON d.vn=o.vn LEFT JOIN icd101 c_icd ON b.icd10=c_icd.code WHERE d.refer_date BETWEEN :start AND :end AND o.an IS NULL AND b.diagtype='1' GROUP BY b.icd10, c_icd.name ORDER BY c DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_ref_in1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_in_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมรับ Refer In', $sql_ref_in1);
$stmt = $pdo->prepare($sql_ref_out1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_out_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมส่ง Refer Out', $sql_ref_out1);
// เพิ่มการประมวลผล Refer Back
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_back_total'] = (int)$stmt->fetchColumn();
$addLog('ส่งกลับ (Refer Back)', $sql_ref_back);
$stmt = $pdo->prepare($sql_ref_in_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_in_dest']['data'][] = (int)$r['a'];
}
$addLog('กราฟ Refer In แยก รพ.ต้นทาง', $sql_ref_in_dest);
$stmt = $pdo->prepare($sql_ref_out_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_out_dest']['data'][] = (int)$r['vn'];
}
$addLog('กราฟ Refer Out แยก รพ.ปลายทาง', $sql_ref_out_dest);
$stmt = $pdo->prepare($sql_ref_in_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค รับ Refer In', $sql_ref_in_top);
$stmt = $pdo->prepare($sql_ref_out_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค ส่ง Refer Out', $sql_ref_out_top);
} catch (\Exception $e) {
$addLog('การส่งต่อ (Refer)', 'หลายคำสั่ง', $e->getMessage());
}
// SQL: Doctor Time
$sql_doc = "SELECT x.main_dep, COUNT(*) AS total_days, SUM(CASE WHEN x.first_service < '09:00:00' THEN 1 ELSE 0 END) AS before_9am_days FROM (SELECT v.vstdate, d.name, o.main_dep, MIN(s.service5) AS first_service FROM vn_stat v JOIN ovst o ON v.vn=o.vn JOIN service_time s ON v.vn=s.vn JOIN doctor d ON v.dx_doctor=d.code WHERE v.vstdate BETWEEN :start AND :end AND v.dx_doctor <> '' 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, o.main_dep) as x GROUP BY x.main_dep";
try {
$stmt = $pdo->prepare($sql_doc);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$doc_totals = 0;
$doc_before9 = 0;
$clinics_doc = [];
while ($r = $stmt->fetch()) {
$doc_totals += $r['total_days'];
$doc_before9 += $r['before_9am_days'];
$dep = $r['main_dep'];
if (!isset($clinics_doc[$dep])) {
$clinics_doc[$dep] = ['tot' => 0, 'b9' => 0];
}
$clinics_doc[$dep]['tot'] += $r['total_days'];
$clinics_doc[$dep]['b9'] += $r['before_9am_days'];
}
$response['doc_time_overall'] = $doc_totals > 0 ? (float)number_format(($doc_before9 / $doc_totals) * 100, 2) : 0;
foreach ($clinics_doc as $dep => $val) {
if (isset($dep_names[$dep])) {
$pct = $val['tot'] > 0 ? ($val['b9'] / $val['tot']) * 100 : 0;
$response['doc_time_clinics'][] = ['name' => $dep_names[$dep], 'pct' => (float)number_format($pct, 2)];
}
}
usort($response['doc_time_clinics'], function ($a, $b) {
return $b['pct'] <=> $a['pct'];
});
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc);
} catch (\Exception $e) {
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc, $e->getMessage());
}
}
echo "---AJAX_OPD_START---";
$json_output = @json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_output) {
echo json_encode(['error' => 'JSON Encode Error: ' . json_last_error_msg(), 'flowchart' => ['total' => 0]]);
} else {
echo $json_output;
}
echo "---AJAX_OPD_END---";
exit;
}
// =========================================================================
// 2. สร้างฟอร์มการกรองมาตรฐาน (ใช้ร่วมกันทุกหน้าย่อย)
// =========================================================================
$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');
$opd_filter_html = '
<form class="opd-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-500 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="toggleOpdFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 opd_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="opd-year-selector 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 opd_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" 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" 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 toggleOpdFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.opd_fiscal_wrapper').style.display = 'block';
form.querySelector('.opd_date_wrapper').style.display = 'none';
} else {
form.querySelector('.opd_fiscal_wrapper').style.display = 'none';
form.querySelector('.opd_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleSqlLogModal()" class="fixed bottom-6 right-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 (Debug)</button>
<div id="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> บันทึกการคิวรีฐานข้อมูล (SQL Execution Logs)</h3>
<button onclick="toggleSqlLogModal()" 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="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 toggleSqlLogModal() {
const modal = document.getElementById('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; ?>
<?php include __DIR__ . '/tab_opd_1_overview.php'; ?>
<?php include __DIR__ . '/tab_opd_2_condition.php'; ?>
<?php include __DIR__ . '/tab_opd_3_service.php'; ?>
<?php include __DIR__ . '/tab_opd_4_refer.php'; ?>
<?php include __DIR__ . '/tab_opd_5_doctor.php'; ?>
<?php include __DIR__ . '/tab_opd_6_satisfaction.php'; ?>
<script>
const safeSetTextBySelector = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
async function fetchOpdRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .opd-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_opd', '1');
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (!params.has(key)) params.append(key, value);
}
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_START---')[1].split('---AJAX_OPD_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data || typeof data.flowchart === 'undefined') throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('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' เพื่อตรวจสอบสาเหตุ");
}
// --- 1. Flowchart (Overview + Cond) ---
safeSetTextBySelector('.opd-total-time-display-ov', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-1-ov', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-2-ov', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-3-ov', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-4-ov', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-5-ov', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.opd-total-time-display-cond', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-1-cond', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-2-cond', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-3-cond', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-4-cond', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-5-cond', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// --- 2. Trend Charts ---
const updateTrendChart = (chartId) => {
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
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)');
if (window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
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_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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
};
updateTrendChart('opdWaitTimeChartTabOv');
updateTrendChart('opdWaitTimeChartTabCond');
// --- 3. Clinic Grid ---
const clinicIconsDb = {
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'หูคอจมูก': 'fa-ear-listen',
'กระดูก': 'fa-bone',
'ทันตกรรม': 'fa-tooth',
'จิตเวช': 'fa-brain',
'ผิวหนัง': 'fa-hand-dots'
};
const renderClinicGrid = (id) => {
const el = document.getElementById(id);
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-emerald-500 via-teal-500 to-emerald-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300 text-lg block mb-2"></i>ไม่พบข้อมูลคลินิก</div>';
}
}
};
renderClinicGrid('db-clinic-wait-cards');
renderClinicGrid('clinic-wait-cards-opd-cond');
// --- 4. Services (Telemedicine & OPD Refill) ---
if (document.getElementById('db-telemed-total')) document.getElementById('db-telemed-total').innerText = data.telemed_total > 0 ? data.telemed_total.toLocaleString() : '-';
if (document.getElementById('db-home-delivery')) document.getElementById('db-home-delivery').innerText = data.home_delivery_total > 0 ? data.home_delivery_total.toLocaleString() : '-';
const teleContainer = document.getElementById('dbServiceTeleChart_container');
if (teleContainer) {
if (data.telemed_clinics && data.telemed_clinics.length > 0) {
teleContainer.innerHTML = '<canvas id="dbServiceTeleChart"></canvas>';
const ctx = document.getElementById('dbServiceTeleChart').getContext('2d');
if (window.appCharts['dbServiceTeleChart']) window.appCharts['dbServiceTeleChart'].destroy();
window.appCharts['dbServiceTeleChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.telemed_clinics.map(d => d.name),
datasets: [{
label: 'จำนวนครั้ง',
data: data.telemed_clinics.map(d => d.val),
backgroundColor: '#a855f7',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#a855f7',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.telemed_clinics.map(d => d.val), 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
teleContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
let delivContainer = document.getElementById('serviceDeliveryChart_container');
if (!delivContainer) {
document.querySelectorAll('#tab-opd-service h3').forEach(h => {
if (h.innerText.includes('OPD Refill (รายเดือน)')) delivContainer = h.nextElementSibling;
});
}
if (delivContainer) {
if (data.home_delivery_trend && data.home_delivery_trend.labels && data.home_delivery_trend.labels.length > 0) {
delivContainer.innerHTML = '<canvas id="serviceDeliveryChart"></canvas>';
const ctx = document.getElementById('serviceDeliveryChart').getContext('2d');
if (window.appCharts['serviceDeliveryChart']) window.appCharts['serviceDeliveryChart'].destroy();
window.appCharts['serviceDeliveryChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.home_delivery_trend.labels,
datasets: [{
label: 'จำนวน (ครั้ง)',
data: data.home_delivery_trend.data,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#1d4ed8',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.home_delivery_trend.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
delivContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
// --- 5. Refer ---
if (document.getElementById('db-refer-in-opd')) document.getElementById('db-refer-in-opd').innerText = data.refer_in_total > 0 ? data.refer_in_total.toLocaleString() : '-';
if (document.getElementById('db-refer-out-opd')) document.getElementById('db-refer-out-opd').innerText = data.refer_out_total > 0 ? data.refer_out_total.toLocaleString() : '-';
if (document.getElementById('db-refer-back-opd')) document.getElementById('db-refer-back-opd').innerText = data.refer_back_total > 0 ? data.refer_back_total.toLocaleString() : '-';
const updateBarChart = (id, dataObj, color) => {
const container = document.getElementById(id + '_container');
if (container) {
if (dataObj && dataObj.labels.length > 0) {
container.innerHTML = `<canvas id="${id}"></canvas>`;
const ctx = document.getElementById(id).getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
window.appCharts[id] = new Chart(ctx, {
type: 'bar',
data: {
labels: dataObj.labels,
datasets: [{
data: dataObj.data,
backgroundColor: color,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: color,
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...dataObj.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
};
updateBarChart('referInChartOpd', data.refer_in_dest, '#10b981');
updateBarChart('referOutChartOpd', data.refer_out_dest, '#ef4444');
const makeList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
makeList('db-list-refer-in', data.refer_in_top10);
makeList('db-list-refer-out', data.refer_out_top10);
// --- 6. Doctor Check ---
if (document.getElementById('db-doc-overall')) document.getElementById('db-doc-overall').innerText = data.doc_time_overall > 0 ? data.doc_time_overall.toFixed(2) : '-';
if (document.getElementById('db-doc-overall-bar')) document.getElementById('db-doc-overall-bar').style.width = Math.min(100, data.doc_time_overall) + '%';
const docGrid = document.getElementById('db-doctor-clinic-grid');
if (docGrid) {
if (data.doc_time_clinics && data.doc_time_clinics.length > 0) {
docGrid.innerHTML = data.doc_time_clinics.map(c => `<div class="card-premium p-3 md:p-4 border-l-4 border-l-blue-500 shadow-sm"><div class="flex justify-between items-center mb-1.5"><span class="text-xs md:text-sm font-bold text-slate-700">${c.name}</span><span class="text-xs md:text-sm font-black ${c.pct >= 80 ? 'text-emerald-600' : 'text-orange-500'}">${c.pct.toFixed(2)}%</span></div><div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden shadow-inner"><div class="bg-gradient-to-r ${c.pct >= 80 ? 'from-emerald-400 to-emerald-500' : 'from-orange-400 to-red-500'} h-full rounded-full w-full" style="width: ${c.pct}%"></div></div></div>`).join('');
} else {
docGrid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลแพทย์ลงตรวจ</div>';
}
}
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.opd-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdRealData(e.target);
});
});
});
</script>
@@ -0,0 +1,885 @@
<?php
// views/tab_opd.php
require_once __DIR__ . '/../includes/db.php';
$show_sql_log_btn = 'Y';
$isOpdAjax = isset($_GET['ajax_opd']) && $_GET['ajax_opd'] == '1';
$isOpdCondAjax = isset($_GET['ajax_opd_cond']) && $_GET['ajax_opd_cond'] == '1';
// =========================================================================
// 1. ส่วนประมวลผล AJAX ดึงข้อมูล "แบบมีเงื่อนไข (Condition)" (รอ SQL จริง)
// =========================================================================
if ($isOpdCondAjax) {
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
// รับค่าพารามิเตอร์ต่างๆ จากฟอร์มเฉพาะของหน้า Condition
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$appoint = $_GET['appoint'] ?? '';
$step_from = $_GET['step_from'] ?? '';
$step_to = $_GET['step_to'] ?? '';
$clinic_id = $_GET['clinic_id'] ?? '';
$time_period = $_GET['time_period'] ?? '';
$lab_status = $_GET['lab_status'] ?? '';
$xray_status = $_GET['xray_status'] ?? '';
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'total' => 0],
'clinics_wait' => [],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'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('ตรวจสอบเงื่อนไข (Condition)', "ค้นหา: $start_date ถึง $end_date | นัด: $appoint | คลินิก: $clinic_id | เวลา: $time_period | Lab: $lab_status");
// -------------------------------------------------------------------------
// [พื้นที่สำหรับใส่ SQL ตัวจริงในอนาคต]
// ปัจจุบันสร้างข้อมูลจำลอง (Mock Data) ไปก่อนเพื่อให้ระบบแสดงผลได้
// -------------------------------------------------------------------------
// จำลองเวลา Flowchart
$response['flowchart'] = [
'step1' => rand(5, 12) + (rand(0, 99) / 100),
'step2' => rand(5, 15) + (rand(0, 99) / 100),
'step3' => rand(20, 45) + (rand(0, 99) / 100),
'step4' => rand(5, 20) + (rand(0, 99) / 100),
'step5' => rand(10, 25) + (rand(0, 99) / 100)
];
$response['flowchart']['total'] = array_sum($response['flowchart']);
$addLog('Flowchart ระยะเวลารอคอย (จำลอง)', 'SELECT ... รอชุดคำสั่งจริง ...');
// จำลอง Clinic Wait
$mock_clinics = ['อายุรกรรม', 'ศัลยกรรม', 'กุมารเวชกรรม', 'สูติ-นรีเวช', 'ตา', 'กระดูก'];
foreach ($mock_clinics as $c) {
$response['clinics_wait'][] = ['name' => $c, 'wait' => rand(30, 150) + (rand(0, 99) / 100)];
}
usort($response['clinics_wait'], function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย (จำลอง)', 'SELECT ... รอชุดคำสั่งจริง ...');
// จำลอง Trend Chart
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
for ($i = 5; $i >= 0; $i--) {
$m = (int)date('m', strtotime("-$i months"));
$y = (int)date('Y', strtotime("-$i months")) + 543;
$response['trend_labels'][] = $thai_months[sprintf("%02d", $m)] . ' ' . substr($y, 2, 2);
$response['trend_wait'][] = rand(40, 110) + (rand(0, 99) / 100);
$response['trend_pt'][] = rand(800, 2500);
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย (จำลอง)', 'SELECT ... รอชุดคำสั่งจริง ...');
echo "---AJAX_OPD_COND_START---";
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
echo "---AJAX_OPD_COND_END---";
exit;
}
// =========================================================================
// 2. ส่วนประมวลผล AJAX ดึงข้อมูล "ภาพรวม OPD ทั่วไป" (ทำงานเมื่อกดค้นหาหน้าอื่นๆ)
// =========================================================================
if ($isOpdAjax) {
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);
}
}
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'โรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'หัวใจ'];
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'total' => 0],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'clinics_wait' => [],
'telemed_total' => 0,
'telemed_clinics' => [],
'home_delivery_total' => 0,
'home_delivery_trend' => ['labels' => [], 'data' => []],
'refer_in_total' => 0,
'refer_out_total' => 0,
'refer_back_total' => 0,
'refer_in_dest' => ['labels' => [], 'data' => []],
'refer_out_dest' => ['labels' => [], 'data' => []],
'refer_in_top10' => [],
'refer_out_top10' => [],
'doc_time_overall' => 0,
'doc_time_clinics' => [],
'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('ระบบตรวจสอบช่วงวันที่ (Date Verification)', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// SQL: Flowchart
$sql1 = "SELECT IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1, IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2, IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3, IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as total_wait FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000'";
try {
$stmt = $pdo->prepare($sql1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'total' => (float)$row['total_wait']];
}
$addLog('Flowchart ระยะเวลารอคอย', $sql1);
} catch (\Exception $e) {
$addLog('Flowchart ระยะเวลารอคอย', $sql1, $e->getMessage());
}
// SQL: Trend Chart
$sql_trend = "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 JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000' GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend, $e->getMessage());
}
// SQL: Clinic Wait
$sql_clinics = "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as start2finish FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$clinics_data = [];
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$clinics_data[] = ['dep' => $row['main_dep'], 'name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($clinics_data, function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$response['clinics_wait'] = $clinics_data;
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics);
} catch (\Exception $e) {
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics, $e->getMessage());
}
// SQL: Telemedicine
$sql_tele1 = "SELECT count(vn) FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end";
$sql_tele2 = "SELECT main_dep, count(vn) as cnt FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end AND main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY main_dep ORDER BY cnt DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_tele1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['telemed_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวม Telemedicine', $sql_tele1);
$stmt2 = $pdo->prepare($sql_tele2);
$stmt2->execute(['start' => $start_date, 'end' => $end_date]);
$tele_clinics = [];
while ($r = $stmt2->fetch()) {
$dep_code = $r['main_dep'];
$name = isset($dep_names[$dep_code]) ? $dep_names[$dep_code] : 'รหัส: ' . $dep_code;
$tele_clinics[] = ['name' => $name, 'val' => (int)$r['cnt']];
}
$response['telemed_clinics'] = $tele_clinics;
$addLog('กราฟ 10 อันดับคลินิกให้บริการ Telemedicine', $sql_tele2);
} catch (\Exception $e) {
$addLog('Telemedicine', $sql_tele1 . ' | ' . $sql_tele2, $e->getMessage());
}
// SQL: ยอดรวม OPD Refill (การส่งยาที่บ้าน)
$sql_refill = "SELECT count(vn) FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2)";
try {
$stmt = $pdo->prepare($sql_refill);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date]);
$response['home_delivery_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill);
} catch (\Exception $e) {
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill, $e->getMessage());
}
// SQL: กราฟ OPD Refill Trend (รายเดือน)
$sql_refill_trend = "SELECT DATE_FORMAT(vstdate, '%Y-%m') as ym, count(vn) as cnt FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2) AND vstdate BETWEEN :start3 AND :end3 GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_refill_trend);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date, 'start3' => $start_date, 'end3' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['home_delivery_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['home_delivery_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend);
} catch (\Exception $e) {
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend, $e->getMessage());
}
// SQL: Refer
$sql_ref_in1 = "SELECT count(vn) FROM referin WHERE refer_hospcode IN ('09192','11359') AND refer_date BETWEEN :start AND :end";
$sql_ref_out1 = "SELECT count(vn) FROM referout WHERE refer_hospcode IN ('10681') AND refer_date BETWEEN :start AND :end";
$sql_ref_back = "SELECT count(distinct b.vn) FROM universal_head b WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end)";
$sql_ref_in_dest = "SELECT count(a.vn) as a, h.name FROM referin a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end AND a.refer_hospcode IS NOT NULL AND a.refer_hospcode NOT IN ('') GROUP BY a.refer_hospcode, h.name ORDER BY a DESC LIMIT 5";
$sql_ref_out_dest = "SELECT count(distinct a.vn) as vn, h.name FROM referout a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end GROUP BY a.refer_hospcode, h.name ORDER BY vn DESC LIMIT 5";
$sql_ref_in_top = "SELECT count(a.icd10) as c, b.name FROM referin a LEFT JOIN icd101 b ON a.icd10=b.code WHERE a.refer_date BETWEEN :start AND :end AND a.icd10 !='' AND a.icd10 !='U000' GROUP BY a.icd10, b.name ORDER BY c DESC LIMIT 10";
$sql_ref_out_top = "SELECT count(distinct d.vn) as c, c_icd.name FROM referout d LEFT JOIN ovstdiag b ON d.vn=b.vn LEFT JOIN ovst o ON d.vn=o.vn LEFT JOIN icd101 c_icd ON b.icd10=c_icd.code WHERE d.refer_date BETWEEN :start AND :end AND o.an IS NULL AND b.diagtype='1' GROUP BY b.icd10, c_icd.name ORDER BY c DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_ref_in1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_in_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมรับ Refer In', $sql_ref_in1);
$stmt = $pdo->prepare($sql_ref_out1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_out_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมส่ง Refer Out', $sql_ref_out1);
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_back_total'] = (int)$stmt->fetchColumn();
$addLog('ส่งกลับ (Refer Back)', $sql_ref_back);
$stmt = $pdo->prepare($sql_ref_in_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_in_dest']['data'][] = (int)$r['a'];
}
$addLog('กราฟ Refer In แยก รพ.ต้นทาง', $sql_ref_in_dest);
$stmt = $pdo->prepare($sql_ref_out_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_out_dest']['data'][] = (int)$r['vn'];
}
$addLog('กราฟ Refer Out แยก รพ.ปลายทาง', $sql_ref_out_dest);
$stmt = $pdo->prepare($sql_ref_in_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค รับ Refer In', $sql_ref_in_top);
$stmt = $pdo->prepare($sql_ref_out_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค ส่ง Refer Out', $sql_ref_out_top);
} catch (\Exception $e) {
$addLog('การส่งต่อ (Refer)', 'หลายคำสั่ง', $e->getMessage());
}
// SQL: Doctor Time
$sql_doc = "SELECT x.main_dep, COUNT(*) AS total_days, SUM(CASE WHEN x.first_service < '09:00:00' THEN 1 ELSE 0 END) AS before_9am_days FROM (SELECT v.vstdate, d.name, o.main_dep, MIN(s.service5) AS first_service FROM vn_stat v JOIN ovst o ON v.vn=o.vn JOIN service_time s ON v.vn=s.vn JOIN doctor d ON v.dx_doctor=d.code WHERE v.vstdate BETWEEN :start AND :end AND v.dx_doctor <> '' 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, o.main_dep) as x GROUP BY x.main_dep";
try {
$stmt = $pdo->prepare($sql_doc);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$doc_totals = 0;
$doc_before9 = 0;
$clinics_doc = [];
while ($r = $stmt->fetch()) {
$doc_totals += $r['total_days'];
$doc_before9 += $r['before_9am_days'];
$dep = $r['main_dep'];
if (!isset($clinics_doc[$dep])) {
$clinics_doc[$dep] = ['tot' => 0, 'b9' => 0];
}
$clinics_doc[$dep]['tot'] += $r['total_days'];
$clinics_doc[$dep]['b9'] += $r['before_9am_days'];
}
$response['doc_time_overall'] = $doc_totals > 0 ? (float)number_format(($doc_before9 / $doc_totals) * 100, 2) : 0;
foreach ($clinics_doc as $dep => $val) {
if (isset($dep_names[$dep])) {
$pct = $val['tot'] > 0 ? ($val['b9'] / $val['tot']) * 100 : 0;
$response['doc_time_clinics'][] = ['name' => $dep_names[$dep], 'pct' => (float)number_format($pct, 2)];
}
}
usort($response['doc_time_clinics'], function ($a, $b) {
return $b['pct'] <=> $a['pct'];
});
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc);
} catch (\Exception $e) {
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc, $e->getMessage());
}
}
echo "---AJAX_OPD_START---";
$json_output = @json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_output) {
echo json_encode(['error' => 'JSON Encode Error: ' . json_last_error_msg(), 'flowchart' => ['total' => 0]]);
} else {
echo $json_output;
}
echo "---AJAX_OPD_END---";
exit;
}
// =========================================================================
// 3. สร้างฟอร์มการกรองมาตรฐาน
// =========================================================================
$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');
$opd_filter_html = '
<form class="opd-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-500 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="toggleOpdFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 opd_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="opd-year-selector 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 opd_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" 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" 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 toggleOpdFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.opd_fiscal_wrapper').style.display = 'block';
form.querySelector('.opd_date_wrapper').style.display = 'none';
} else {
form.querySelector('.opd_fiscal_wrapper').style.display = 'none';
form.querySelector('.opd_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleSqlLogModal()" class="fixed bottom-6 right-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 (Debug)</button>
<div id="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> บันทึกการคิวรีฐานข้อมูล (SQL Execution Logs)</h3>
<button onclick="toggleSqlLogModal()" 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="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 toggleSqlLogModal() {
const modal = document.getElementById('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; ?>
<!-- INCLUDE FILES -->
<?php include __DIR__ . '/tab_opd_1_overview.php'; ?>
<?php include __DIR__ . '/tab_opd_2_condition.php'; ?>
<?php include __DIR__ . '/tab_opd_3_service.php'; ?>
<?php include __DIR__ . '/tab_opd_4_refer.php'; ?>
<?php include __DIR__ . '/tab_opd_5_doctor.php'; ?>
<?php include __DIR__ . '/tab_opd_6_satisfaction.php'; ?>
<!-- JAVASCRIPT -->
<script>
const safeSetTextBySelector = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
async function fetchOpdRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .opd-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_opd', '1');
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (!params.has(key)) params.append(key, value);
}
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_START---')[1].split('---AJAX_OPD_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data || typeof data.flowchart === 'undefined') throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('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' เพื่อตรวจสอบสาเหตุ");
}
// --- 1. Flowchart (Overview) ---
safeSetTextBySelector('.opd-total-time-display-ov', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-1-ov', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-2-ov', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-3-ov', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-4-ov', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-5-ov', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// --- 2. Trend Charts ---
const updateTrendChart = (chartId) => {
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
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)');
if (window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
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_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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
};
updateTrendChart('opdWaitTimeChartTabOv');
// --- 3. Clinic Grid ---
const clinicIconsDb = {
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'หูคอจมูก': 'fa-ear-listen',
'กระดูก': 'fa-bone',
'ทันตกรรม': 'fa-tooth',
'จิตเวช': 'fa-brain',
'ผิวหนัง': 'fa-hand-dots'
};
const renderClinicGrid = (id) => {
const el = document.getElementById(id);
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-emerald-500 via-teal-500 to-emerald-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300 text-lg block mb-2"></i>ไม่พบข้อมูลคลินิก</div>';
}
}
};
renderClinicGrid('db-clinic-wait-cards');
// --- 4. Services (Telemedicine & OPD Refill) ---
if (document.getElementById('db-telemed-total')) document.getElementById('db-telemed-total').innerText = data.telemed_total > 0 ? data.telemed_total.toLocaleString() : '-';
if (document.getElementById('db-home-delivery')) document.getElementById('db-home-delivery').innerText = data.home_delivery_total > 0 ? data.home_delivery_total.toLocaleString() : '-';
const teleContainer = document.getElementById('dbServiceTeleChart_container');
if (teleContainer) {
if (data.telemed_clinics && data.telemed_clinics.length > 0) {
teleContainer.innerHTML = '<canvas id="dbServiceTeleChart"></canvas>';
const ctx = document.getElementById('dbServiceTeleChart').getContext('2d');
if (window.appCharts['dbServiceTeleChart']) window.appCharts['dbServiceTeleChart'].destroy();
window.appCharts['dbServiceTeleChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.telemed_clinics.map(d => d.name),
datasets: [{
label: 'จำนวนครั้ง',
data: data.telemed_clinics.map(d => d.val),
backgroundColor: '#a855f7',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#a855f7',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.telemed_clinics.map(d => d.val), 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
teleContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
let delivContainer = document.getElementById('serviceDeliveryChart_container');
if (!delivContainer) {
document.querySelectorAll('#tab-opd-service h3').forEach(h => {
if (h.innerText.includes('OPD Refill (รายเดือน)')) delivContainer = h.nextElementSibling;
});
}
if (delivContainer) {
if (data.home_delivery_trend && data.home_delivery_trend.labels && data.home_delivery_trend.labels.length > 0) {
delivContainer.innerHTML = '<canvas id="serviceDeliveryChart"></canvas>';
const ctx = document.getElementById('serviceDeliveryChart').getContext('2d');
if (window.appCharts['serviceDeliveryChart']) window.appCharts['serviceDeliveryChart'].destroy();
window.appCharts['serviceDeliveryChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.home_delivery_trend.labels,
datasets: [{
label: 'จำนวน (ครั้ง)',
data: data.home_delivery_trend.data,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#1d4ed8',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.home_delivery_trend.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
delivContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
// --- 5. Refer ---
if (document.getElementById('db-refer-in-opd')) document.getElementById('db-refer-in-opd').innerText = data.refer_in_total > 0 ? data.refer_in_total.toLocaleString() : '-';
if (document.getElementById('db-refer-out-opd')) document.getElementById('db-refer-out-opd').innerText = data.refer_out_total > 0 ? data.refer_out_total.toLocaleString() : '-';
if (document.getElementById('db-refer-back-opd')) document.getElementById('db-refer-back-opd').innerText = data.refer_back_total > 0 ? data.refer_back_total.toLocaleString() : '-';
const updateBarChart = (id, dataObj, color) => {
const container = document.getElementById(id + '_container');
if (container) {
if (dataObj && dataObj.labels.length > 0) {
container.innerHTML = `<canvas id="${id}"></canvas>`;
const ctx = document.getElementById(id).getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
window.appCharts[id] = new Chart(ctx, {
type: 'bar',
data: {
labels: dataObj.labels,
datasets: [{
data: dataObj.data,
backgroundColor: color,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: color,
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...dataObj.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
};
updateBarChart('referInChartOpd', data.refer_in_dest, '#10b981');
updateBarChart('referOutChartOpd', data.refer_out_dest, '#ef4444');
const makeList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
makeList('db-list-refer-in', data.refer_in_top10);
makeList('db-list-refer-out', data.refer_out_top10);
// --- 6. Doctor Check ---
if (document.getElementById('db-doc-overall')) document.getElementById('db-doc-overall').innerText = data.doc_time_overall > 0 ? data.doc_time_overall.toFixed(2) : '-';
if (document.getElementById('db-doc-overall-bar')) document.getElementById('db-doc-overall-bar').style.width = Math.min(100, data.doc_time_overall) + '%';
const docGrid = document.getElementById('db-doctor-clinic-grid');
if (docGrid) {
if (data.doc_time_clinics && data.doc_time_clinics.length > 0) {
docGrid.innerHTML = data.doc_time_clinics.map(c => `<div class="card-premium p-3 md:p-4 border-l-4 border-l-blue-500 shadow-sm"><div class="flex justify-between items-center mb-1.5"><span class="text-xs md:text-sm font-bold text-slate-700">${c.name}</span><span class="text-xs md:text-sm font-black ${c.pct >= 80 ? 'text-emerald-600' : 'text-orange-500'}">${c.pct.toFixed(2)}%</span></div><div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden shadow-inner"><div class="bg-gradient-to-r ${c.pct >= 80 ? 'from-emerald-400 to-emerald-500' : 'from-orange-400 to-red-500'} h-full rounded-full w-full" style="width: ${c.pct}%"></div></div></div>`).join('');
} else {
docGrid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลแพทย์ลงตรวจ</div>';
}
}
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.opd-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdRealData(e.target);
});
});
});
</script>
@@ -0,0 +1,971 @@
<?php
// views/tab_opd.php
require_once __DIR__ . '/../includes/db.php';
$show_sql_log_btn = 'Y';
$isOpdAjax = isset($_GET['ajax_opd']) && $_GET['ajax_opd'] == '1';
$isOpdCondAjax = isset($_GET['ajax_opd_cond']) && $_GET['ajax_opd_cond'] == '1';
// =========================================================================
// 1. ส่วนประมวลผล AJAX ดึงข้อมูล "แบบมีเงื่อนไข (Condition)"
// =========================================================================
if ($isOpdCondAjax) {
while (ob_get_level()) {
ob_end_clean();
}
header('Content-Type: application/json; charset=utf-8');
// รับค่าพารามิเตอร์ต่างๆ จากฟอร์มเฉพาะของหน้า Condition
$start_date = $_GET['start_date'] ?? date('Y-m-01');
$end_date = $_GET['end_date'] ?? date('Y-m-d');
$appoint = $_GET['appoint'] ?? '';
$step_from = $_GET['step_from'] ?? 'vsttime';
$step_to = $_GET['step_to'] ?? 'service6';
$clinic_id = $_GET['clinic_id'] ?? '';
$time_period = $_GET['time_period'] ?? '';
$lab_status = $_GET['lab_status'] ?? '';
$xray_status = $_GET['xray_status'] ?? '';
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'custom_total' => 0],
'clinics_wait' => [],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'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('ตรวจสอบเงื่อนไข (Condition)', "ค้นหา: $start_date ถึง $end_date | นัด: $appoint | คลินิก: $clinic_id | เวลา: $time_period | Lab: $lab_status");
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'อายุรกรรมโรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'คลินิกหัวใจ'];
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
// 1. Mapping ฟิลด์สำหรับคำนวณและกรองเวลา (ทำก่อนสร้างเงื่อนไข)
$allowed_cols = ['vsttime' => 'o.vsttime', 'service4' => 's.service4', 'service11' => 's.service11', 'service5' => 's.service5', 'service12' => 's.service12', 'service6' => 's.service6'];
$col_from = isset($allowed_cols[$step_from]) ? $allowed_cols[$step_from] : "o.vsttime";
$col_to = isset($allowed_cols[$step_to]) ? $allowed_cols[$step_to] : "s.service6";
// --- สร้าง Dynamic WHERE Clause ---
$where_cond = "s.vstdate BETWEEN :start AND :end ";
$params = ['start' => $start_date, 'end' => $end_date];
// คงเงื่อนไขพื้นฐานเรื่องคนไข้ต้องผ่านช่องชำระเงิน (แต่เอา rx_time ออก เพราะจะใช้เวลาจากจุดบริการแทน)
$where_cond .= " AND s.vn IN (SELECT vn FROM rx_operator WHERE pay='Y') ";
// กรองช่วงเวลา (อิงจากเวลาของจุดเริ่มต้น $col_from ที่ผู้ใช้เลือก)
if ($time_period === 'morning') {
$where_cond .= " AND $col_from BETWEEN '08:00:00' AND '12:00:00' ";
} elseif ($time_period === 'afternoon') {
$where_cond .= " AND $col_from BETWEEN '12:00:01' AND '16:00:00' ";
} else {
$where_cond .= " AND $col_from BETWEEN '08:00:00' AND '16:00:00' ";
}
// กรองคลินิก
if ($clinic_id !== '') {
$where_cond .= " AND o.main_dep = :clinic_id ";
$params['clinic_id'] = $clinic_id;
}
// กรอง Lab / X-Ray
if ($lab_status === '1') {
$where_cond .= " AND s.vn IN (SELECT vn FROM lab_head) ";
} elseif ($lab_status === '0') {
$where_cond .= " AND s.vn NOT IN (SELECT vn FROM lab_head) ";
}
if ($xray_status === '1') {
$where_cond .= " AND s.vn IN (SELECT vn FROM xray_head) ";
} elseif ($xray_status === '0') {
$where_cond .= " AND s.vn NOT IN (SELECT vn FROM xray_head) ";
}
// กรองการนัดหมาย
if ($appoint === '1') {
$where_cond .= " AND s.vn IN (SELECT visit_vn FROM oapp) ";
} elseif ($appoint === '3') {
$where_cond .= " AND NOT EXISTS (SELECT 1 FROM oapp WHERE visit_vn =vn) ";
}
// ผูกคำสั่งหลัก (Base Query)
$base_from = "FROM service_time s JOIN ovst o ON s.vn = o.vn
WHERE $where_cond
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)";
if ($pdo) {
// 1. SQL: Flowchart (ตามเงื่อนไข)
$sql_flow = "SELECT
IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1,
IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2,
IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3,
IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4,
IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5,
IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as custom_total
$base_from " . ($clinic_id === '' ? "AND o.main_dep='000'" : ""); // ถ้าไม่เลือกคลินิก ให้ default เป็น 000 ก่อน
try {
$stmt = $pdo->prepare($sql_flow);
$stmt->execute($params);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'custom_total' => (float)$row['custom_total']];
}
$addLog('Flowchart ระยะเวลารอคอย (แบบมีเงื่อนไข)', $sql_flow);
} catch (\Exception $e) {
$addLog('Flowchart ระยะเวลารอคอย (แบบมีเงื่อนไข)', $sql_flow, $e->getMessage());
}
// 2. SQL: Clinic Wait (ตามเงื่อนไข)
$sql_clinics = "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as start2finish
$base_from AND o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132')
GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute($params);
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$response['clinics_wait'][] = ['name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($response['clinics_wait'], function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$addLog('ระยะเวลารอคอยแต่ละคลินิก (แบบมีเงื่อนไข)', $sql_clinics);
} catch (\Exception $e) {
$addLog('ระยะเวลารอคอยแต่ละคลินิก (แบบมีเงื่อนไข)', $sql_clinics, $e->getMessage());
}
// 3. SQL: Trend Chart (ตามเงื่อนไข)
$sql_trend = "SELECT DATE_FORMAT(s.vstdate, '%Y-%m') as ym, IFNULL(ROUND(AVG((time_to_sec($col_to)-time_to_sec($col_from))/60), 2), 0) as avg_wait, COUNT(DISTINCT s.vn) as pt_count
$base_from " . ($clinic_id === '' ? "AND o.main_dep='000'" : "") . " GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute($params);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย (แบบมีเงื่อนไข)', $sql_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มระยะเวลารอคอย (แบบมีเงื่อนไข)', $sql_trend, $e->getMessage());
}
}
echo "---AJAX_OPD_COND_START---";
echo json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
echo "---AJAX_OPD_COND_END---";
exit;
}
// =========================================================================
// 2. ส่วนประมวลผล AJAX ดึงข้อมูล "ภาพรวม OPD ทั่วไป" (ทำงานเมื่อกดค้นหาหน้าอื่นๆ)
// =========================================================================
if ($isOpdAjax) {
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);
}
}
$dep_names = ['000' => 'ผู้ป่วยนอก (ทั่วไป)', '130' => 'อายุรกรรม', '102' => 'ศัลยกรรม', '149' => 'ระบบประสาท', '168' => 'กุมารเวชกรรม', '220' => 'สูติ-นรีเวช', '073' => 'ตา', '072' => 'กระดูก', '111' => 'จิตเวช', '139' => 'เบาหวาน', '140' => 'ความดัน', '154' => 'ไขมัน', '137' => 'ไต', '110' => 'หอบหืด', '009' => 'ฝากครรภ์', '227' => 'ทางเดินอาหารและตับ', '024' => 'วัณโรค', '225' => 'โรคทางเดินหายใจและปอด', '017' => 'ทันตกรรม', '132' => 'หัวใจ'];
$response = [
'flowchart' => ['step1' => 0, 'step2' => 0, 'step3' => 0, 'step4' => 0, 'step5' => 0, 'total' => 0],
'trend_labels' => [],
'trend_wait' => [],
'trend_pt' => [],
'clinics_wait' => [],
'telemed_total' => 0,
'telemed_clinics' => [],
'home_delivery_total' => 0,
'home_delivery_trend' => ['labels' => [], 'data' => []],
'refer_in_total' => 0,
'refer_out_total' => 0,
'refer_back_total' => 0,
'refer_in_dest' => ['labels' => [], 'data' => []],
'refer_out_dest' => ['labels' => [], 'data' => []],
'refer_in_top10' => [],
'refer_out_top10' => [],
'doc_time_overall' => 0,
'doc_time_clinics' => [],
'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('ระบบตรวจสอบช่วงวันที่ (Date Verification)', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// SQL: Flowchart
$sql1 = "SELECT IFNULL(ROUND(AVG((time_to_sec(s.service4)-time_to_sec(o.vsttime))/60), 2), 0) as step1, IFNULL(ROUND(AVG((time_to_sec(s.service11)-time_to_sec(s.service4))/60), 2), 0) as step2, IFNULL(ROUND(AVG((time_to_sec(s.service5)-time_to_sec(s.service11))/60), 2), 0) as step3, IFNULL(ROUND(AVG((time_to_sec(s.service12)-time_to_sec(s.service5))/60), 2), 0) as step4, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service12))/60), 2), 0) as step5, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as total_wait FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000'";
try {
$stmt = $pdo->prepare($sql1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
if ($row = $stmt->fetch()) {
$response['flowchart'] = ['step1' => (float)$row['step1'], 'step2' => (float)$row['step2'], 'step3' => (float)$row['step3'], 'step4' => (float)$row['step4'], 'step5' => (float)$row['step5'], 'total' => (float)$row['total_wait']];
}
$addLog('Flowchart ระยะเวลารอคอย', $sql1);
} catch (\Exception $e) {
$addLog('Flowchart ระยะเวลารอคอย', $sql1, $e->getMessage());
}
// SQL: Trend Chart
$sql_trend = "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 JOIN ovst o ON s.vn = o.vn 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 o.main_dep='000' GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_trend);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($row = $stmt->fetch()) {
$parts = explode('-', $row['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['trend_labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['trend_wait'][] = (float)$row['avg_wait'];
$response['trend_pt'][] = (int)$row['pt_count'];
}
}
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend);
} catch (\Exception $e) {
$addLog('กราฟแนวโน้มระยะเวลารอคอย', $sql_trend, $e->getMessage());
}
// SQL: Clinic Wait
$sql_clinics = "SELECT o.main_dep, IFNULL(ROUND(AVG((time_to_sec(s.service6)-time_to_sec(s.service4))/60), 2), 0) as start2finish FROM service_time s JOIN ovst o ON s.vn = o.vn 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 o.main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY o.main_dep";
try {
$stmt = $pdo->prepare($sql_clinics);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$clinics_data = [];
while ($row = $stmt->fetch()) {
if (isset($dep_names[$row['main_dep']])) {
$clinics_data[] = ['dep' => $row['main_dep'], 'name' => $dep_names[$row['main_dep']], 'wait' => (float)$row['start2finish']];
}
}
usort($clinics_data, function ($a, $b) {
return $b['wait'] <=> $a['wait'];
});
$response['clinics_wait'] = $clinics_data;
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics);
} catch (\Exception $e) {
$addLog('ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย', $sql_clinics, $e->getMessage());
}
// SQL: Telemedicine
$sql_tele1 = "SELECT count(vn) FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end";
$sql_tele2 = "SELECT main_dep, count(vn) as cnt FROM ovst WHERE ovstist='15' AND vstdate BETWEEN :start AND :end AND main_dep IN ('000','130','102','149','168','220','073','072','111','139','140','154','137','110','009','227','024','225','017','132') GROUP BY main_dep ORDER BY cnt DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_tele1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['telemed_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวม Telemedicine', $sql_tele1);
$stmt2 = $pdo->prepare($sql_tele2);
$stmt2->execute(['start' => $start_date, 'end' => $end_date]);
$tele_clinics = [];
while ($r = $stmt2->fetch()) {
$dep_code = $r['main_dep'];
$name = isset($dep_names[$dep_code]) ? $dep_names[$dep_code] : 'รหัส: ' . $dep_code;
$tele_clinics[] = ['name' => $name, 'val' => (int)$r['cnt']];
}
$response['telemed_clinics'] = $tele_clinics;
$addLog('กราฟ 10 อันดับคลินิกให้บริการ Telemedicine', $sql_tele2);
} catch (\Exception $e) {
$addLog('Telemedicine', $sql_tele1 . ' | ' . $sql_tele2, $e->getMessage());
}
// SQL: ยอดรวม OPD Refill (การส่งยาที่บ้าน)
$sql_refill = "SELECT count(vn) FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2)";
try {
$stmt = $pdo->prepare($sql_refill);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date]);
$response['home_delivery_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill);
} catch (\Exception $e) {
$addLog('ยอดรวมการส่งยาที่บ้าน OPD Refill', $sql_refill, $e->getMessage());
}
// SQL: กราฟ OPD Refill Trend (รายเดือน)
$sql_refill_trend = "SELECT DATE_FORMAT(vstdate, '%Y-%m') as ym, count(vn) as cnt FROM ovst WHERE vstdate BETWEEN :start1 AND :end1 AND main_dep='256' OR vn IN(SELECT vn FROM opitemrece WHERE icode IN('3162977','3164210','3163020') AND vstdate BETWEEN :start2 AND :end2) AND vstdate BETWEEN :start3 AND :end3 GROUP BY ym ORDER BY ym ASC";
try {
$stmt = $pdo->prepare($sql_refill_trend);
$stmt->execute(['start1' => $start_date, 'end1' => $end_date, 'start2' => $start_date, 'end2' => $end_date, 'start3' => $start_date, 'end3' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$y_th = (int)$parts[0] + 543;
$response['home_delivery_trend']['labels'][] = $thai_months[$parts[1]] . ' ' . substr($y_th, 2, 2);
$response['home_delivery_trend']['data'][] = (int)$r['cnt'];
}
}
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend);
} catch (\Exception $e) {
$addLog('กราฟการส่งยาที่บ้าน OPD Refill (รายเดือน)', $sql_refill_trend, $e->getMessage());
}
// SQL: Refer
$sql_ref_in1 = "SELECT count(vn) FROM referin WHERE refer_hospcode IN ('09192','11359') AND refer_date BETWEEN :start AND :end";
$sql_ref_out1 = "SELECT count(vn) FROM referout WHERE refer_hospcode IN ('10681') AND refer_date BETWEEN :start AND :end";
$sql_ref_back = "SELECT count(distinct b.vn) FROM universal_head b WHERE b.universal_form_id ='124' AND b.vn IN(SELECT an FROM ipt WHERE dchdate BETWEEN :start AND :end)";
$sql_ref_in_dest = "SELECT count(a.vn) as a, h.name FROM referin a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end AND a.refer_hospcode IS NOT NULL AND a.refer_hospcode NOT IN ('') GROUP BY a.refer_hospcode, h.name ORDER BY a DESC LIMIT 5";
$sql_ref_out_dest = "SELECT count(distinct a.vn) as vn, h.name FROM referout a LEFT JOIN hospcode h ON a.refer_hospcode = h.hospcode WHERE a.refer_date BETWEEN :start AND :end GROUP BY a.refer_hospcode, h.name ORDER BY vn DESC LIMIT 5";
$sql_ref_in_top = "SELECT count(a.icd10) as c, b.name FROM referin a LEFT JOIN icd101 b ON a.icd10=b.code WHERE a.refer_date BETWEEN :start AND :end AND a.icd10 !='' AND a.icd10 !='U000' GROUP BY a.icd10, b.name ORDER BY c DESC LIMIT 10";
$sql_ref_out_top = "SELECT count(distinct d.vn) as c, c_icd.name FROM referout d LEFT JOIN ovstdiag b ON d.vn=b.vn LEFT JOIN ovst o ON d.vn=o.vn LEFT JOIN icd101 c_icd ON b.icd10=c_icd.code WHERE d.refer_date BETWEEN :start AND :end AND o.an IS NULL AND b.diagtype='1' GROUP BY b.icd10, c_icd.name ORDER BY c DESC LIMIT 10";
try {
$stmt = $pdo->prepare($sql_ref_in1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_in_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมรับ Refer In', $sql_ref_in1);
$stmt = $pdo->prepare($sql_ref_out1);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_out_total'] = (int)$stmt->fetchColumn();
$addLog('ยอดรวมส่ง Refer Out', $sql_ref_out1);
$stmt = $pdo->prepare($sql_ref_back);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['refer_back_total'] = (int)$stmt->fetchColumn();
$addLog('ส่งกลับ (Refer Back)', $sql_ref_back);
$stmt = $pdo->prepare($sql_ref_in_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_in_dest']['data'][] = (int)$r['a'];
}
$addLog('กราฟ Refer In แยก รพ.ต้นทาง', $sql_ref_in_dest);
$stmt = $pdo->prepare($sql_ref_out_dest);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_dest']['labels'][] = $to_utf8($r['name']) ?: 'ไม่ระบุ';
$response['refer_out_dest']['data'][] = (int)$r['vn'];
}
$addLog('กราฟ Refer Out แยก รพ.ปลายทาง', $sql_ref_out_dest);
$stmt = $pdo->prepare($sql_ref_in_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_in_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค รับ Refer In', $sql_ref_in_top);
$stmt = $pdo->prepare($sql_ref_out_top);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['refer_out_top10'][] = ['name' => $to_utf8($r['name']) ?: 'ไม่ระบุ', 'val' => (int)$r['c']];
}
$addLog('10 อันดับโรค ส่ง Refer Out', $sql_ref_out_top);
} catch (\Exception $e) {
$addLog('การส่งต่อ (Refer)', 'หลายคำสั่ง', $e->getMessage());
}
// SQL: Doctor Time
$sql_doc = "SELECT x.main_dep, COUNT(*) AS total_days, SUM(CASE WHEN x.first_service < '09:00:00' THEN 1 ELSE 0 END) AS before_9am_days FROM (SELECT v.vstdate, d.name, o.main_dep, MIN(s.service5) AS first_service FROM vn_stat v JOIN ovst o ON v.vn=o.vn JOIN service_time s ON v.vn=s.vn JOIN doctor d ON v.dx_doctor=d.code WHERE v.vstdate BETWEEN :start AND :end AND v.dx_doctor <> '' 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, o.main_dep) as x GROUP BY x.main_dep";
try {
$stmt = $pdo->prepare($sql_doc);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$doc_totals = 0;
$doc_before9 = 0;
$clinics_doc = [];
while ($r = $stmt->fetch()) {
$doc_totals += $r['total_days'];
$doc_before9 += $r['before_9am_days'];
$dep = $r['main_dep'];
if (!isset($clinics_doc[$dep])) {
$clinics_doc[$dep] = ['tot' => 0, 'b9' => 0];
}
$clinics_doc[$dep]['tot'] += $r['total_days'];
$clinics_doc[$dep]['b9'] += $r['before_9am_days'];
}
$response['doc_time_overall'] = $doc_totals > 0 ? (float)number_format(($doc_before9 / $doc_totals) * 100, 2) : 0;
foreach ($clinics_doc as $dep => $val) {
if (isset($dep_names[$dep])) {
$pct = $val['tot'] > 0 ? ($val['b9'] / $val['tot']) * 100 : 0;
$response['doc_time_clinics'][] = ['name' => $dep_names[$dep], 'pct' => (float)number_format($pct, 2)];
}
}
usort($response['doc_time_clinics'], function ($a, $b) {
return $b['pct'] <=> $a['pct'];
});
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc);
} catch (\Exception $e) {
$addLog('สถิติเวลาลงตรวจของแพทย์', $sql_doc, $e->getMessage());
}
}
echo "---AJAX_OPD_START---";
$json_output = @json_encode($response, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_output) {
echo json_encode(['error' => 'JSON Encode Error: ' . json_last_error_msg(), 'flowchart' => ['total' => 0]]);
} else {
echo $json_output;
}
echo "---AJAX_OPD_END---";
exit;
}
// =========================================================================
// 3. สร้างฟอร์มการกรองมาตรฐาน
// =========================================================================
$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');
$opd_filter_html = '
<form class="opd-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-500 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="toggleOpdFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 opd_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="opd-year-selector 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 opd_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" 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" 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 toggleOpdFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.opd_fiscal_wrapper').style.display = 'block';
form.querySelector('.opd_date_wrapper').style.display = 'none';
} else {
form.querySelector('.opd_fiscal_wrapper').style.display = 'none';
form.querySelector('.opd_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleSqlLogModal()" class="fixed bottom-6 right-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 (Debug)</button>
<div id="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> บันทึกการคิวรีฐานข้อมูล (SQL Execution Logs)</h3>
<button onclick="toggleSqlLogModal()" 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="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 toggleSqlLogModal() {
const modal = document.getElementById('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; ?>
<!-- INCLUDE FILES -->
<?php include __DIR__ . '/tab_opd_1_overview.php'; ?>
<?php include __DIR__ . '/tab_opd_2_condition.php'; ?>
<?php include __DIR__ . '/tab_opd_3_service.php'; ?>
<?php include __DIR__ . '/tab_opd_4_refer.php'; ?>
<?php include __DIR__ . '/tab_opd_5_doctor.php'; ?>
<?php include __DIR__ . '/tab_opd_6_satisfaction.php'; ?>
<!-- JAVASCRIPT -->
<script>
const safeSetTextBySelector = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
async function fetchOpdRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .opd-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_opd', '1');
const urlParams = new URLSearchParams(window.location.search);
for (const [key, value] of urlParams.entries()) {
if (!params.has(key)) params.append(key, value);
}
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_START---')[1].split('---AJAX_OPD_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data || typeof data.flowchart === 'undefined') throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('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' เพื่อตรวจสอบสาเหตุ");
}
// --- 1. Flowchart (Overview) ---
safeSetTextBySelector('.opd-total-time-display-ov', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-1-ov', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-2-ov', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-3-ov', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-4-ov', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSetTextBySelector('.t-step-5-ov', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// --- 2. Trend Charts ---
const updateTrendChart = (chartId) => {
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
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)');
if (window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
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_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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
};
updateTrendChart('opdWaitTimeChartTabOv');
// --- 3. Clinic Grid ---
const clinicIconsDb = {
'ผู้ป่วยนอก (ทั่วไป)': 'fa-hospital-user',
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'กระดูก': 'fa-bone',
'จิตเวช': 'fa-brain',
'ทันตกรรม': 'fa-tooth'
};
const renderClinicGrid = (id) => {
const el = document.getElementById(id);
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-emerald-500 via-teal-500 to-emerald-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300 text-lg block mb-2"></i>ไม่พบข้อมูลคลินิก</div>';
}
}
};
renderClinicGrid('db-clinic-wait-cards');
// --- 4. Services (Telemedicine & OPD Refill) ---
if (document.getElementById('db-telemed-total')) document.getElementById('db-telemed-total').innerText = data.telemed_total > 0 ? data.telemed_total.toLocaleString() : '-';
if (document.getElementById('db-home-delivery')) document.getElementById('db-home-delivery').innerText = data.home_delivery_total > 0 ? data.home_delivery_total.toLocaleString() : '-';
const teleContainer = document.getElementById('dbServiceTeleChart_container');
if (teleContainer) {
if (data.telemed_clinics && data.telemed_clinics.length > 0) {
teleContainer.innerHTML = '<canvas id="dbServiceTeleChart"></canvas>';
const ctx = document.getElementById('dbServiceTeleChart').getContext('2d');
if (window.appCharts['dbServiceTeleChart']) window.appCharts['dbServiceTeleChart'].destroy();
window.appCharts['dbServiceTeleChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.telemed_clinics.map(d => d.name),
datasets: [{
label: 'จำนวนครั้ง',
data: data.telemed_clinics.map(d => d.val),
backgroundColor: '#a855f7',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#a855f7',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.telemed_clinics.map(d => d.val), 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
teleContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
let delivContainer = document.getElementById('serviceDeliveryChart_container');
if (!delivContainer) {
document.querySelectorAll('#tab-opd-service h3').forEach(h => {
if (h.innerText.includes('OPD Refill (รายเดือน)')) delivContainer = h.nextElementSibling;
});
}
if (delivContainer) {
if (data.home_delivery_trend && data.home_delivery_trend.labels && data.home_delivery_trend.labels.length > 0) {
delivContainer.innerHTML = '<canvas id="serviceDeliveryChart"></canvas>';
const ctx = document.getElementById('serviceDeliveryChart').getContext('2d');
if (window.appCharts['serviceDeliveryChart']) window.appCharts['serviceDeliveryChart'].destroy();
window.appCharts['serviceDeliveryChart'] = new Chart(ctx, {
type: 'bar',
data: {
labels: data.home_delivery_trend.labels,
datasets: [{
label: 'จำนวน (ครั้ง)',
data: data.home_delivery_trend.data,
backgroundColor: '#3b82f6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: '#1d4ed8',
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...data.home_delivery_trend.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
delivContainer.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</span>';
}
}
// --- 5. Refer ---
if (document.getElementById('db-refer-in-opd')) document.getElementById('db-refer-in-opd').innerText = data.refer_in_total > 0 ? data.refer_in_total.toLocaleString() : '-';
if (document.getElementById('db-refer-out-opd')) document.getElementById('db-refer-out-opd').innerText = data.refer_out_total > 0 ? data.refer_out_total.toLocaleString() : '-';
if (document.getElementById('db-refer-back-opd')) document.getElementById('db-refer-back-opd').innerText = data.refer_back_total > 0 ? data.refer_back_total.toLocaleString() : '-';
const updateBarChart = (id, dataObj, color) => {
const container = document.getElementById(id + '_container');
if (container) {
if (dataObj && dataObj.labels.length > 0) {
container.innerHTML = `<canvas id="${id}"></canvas>`;
const ctx = document.getElementById(id).getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
window.appCharts[id] = new Chart(ctx, {
type: 'bar',
data: {
labels: dataObj.labels,
datasets: [{
data: dataObj.data,
backgroundColor: color,
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
datalabels: {
display: true,
align: 'top',
anchor: 'end',
color: color,
font: {
weight: 'bold'
}
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: 0,
suggestedMax: Math.max(...dataObj.data, 0) * 1.2
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูล</span>';
}
}
};
updateBarChart('referInChartOpd', data.refer_in_dest, '#10b981');
updateBarChart('referOutChartOpd', data.refer_out_dest, '#ef4444');
const makeList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
makeList('db-list-refer-in', data.refer_in_top10);
makeList('db-list-refer-out', data.refer_out_top10);
// --- 6. Doctor Check ---
if (document.getElementById('db-doc-overall')) document.getElementById('db-doc-overall').innerText = data.doc_time_overall > 0 ? data.doc_time_overall.toFixed(2) : '-';
if (document.getElementById('db-doc-overall-bar')) document.getElementById('db-doc-overall-bar').style.width = Math.min(100, data.doc_time_overall) + '%';
const docGrid = document.getElementById('db-doctor-clinic-grid');
if (docGrid) {
if (data.doc_time_clinics && data.doc_time_clinics.length > 0) {
docGrid.innerHTML = data.doc_time_clinics.map(c => `<div class="card-premium p-3 md:p-4 border-l-4 border-l-blue-500 shadow-sm"><div class="flex justify-between items-center mb-1.5"><span class="text-xs md:text-sm font-bold text-slate-700">${c.name}</span><span class="text-xs md:text-sm font-black ${c.pct >= 80 ? 'text-emerald-600' : 'text-orange-500'}">${c.pct.toFixed(2)}%</span></div><div class="w-full bg-slate-200 h-2 rounded-full overflow-hidden shadow-inner"><div class="bg-gradient-to-r ${c.pct >= 80 ? 'from-emerald-400 to-emerald-500' : 'from-orange-400 to-red-500'} h-full rounded-full w-full" style="width: ${c.pct}%"></div></div></div>`).join('');
} else {
docGrid.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลแพทย์ลงตรวจ</div>';
}
}
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.opd-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdRealData(e.target);
});
});
});
</script>
<?php
// End of file views/tab_opd.php
?>
@@ -0,0 +1,61 @@
<div id="tab-opd-overview" class="tab-content hidden space-y-4 md:space-y-6">
<?= $opd_filter_html ?>
<div class="card-premium p-4 md:p-8 bg-white relative overflow-visible mb-6">
<div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-gradient-to-b from-blue-500 to-emerald-500 rounded-l-xl"></div>
<h3 class="font-bold text-slate-800 mb-4 md:mb-8 flex flex-col md:flex-row items-start md:items-center text-base md:text-lg">
<div class="flex items-center mb-2 md:mb-0">
<div class="w-6 h-6 md:w-8 md:h-8 rounded bg-blue-100 text-blue-600 flex items-center justify-center mr-2 md:mr-3"><i class="fa-solid fa-route"></i></div>ผลลัพธ์เส้นทางระยะเวลารอคอย
</div>
<span class="md:ml-auto bg-gradient-to-br from-emerald-400 to-blue-600 text-white px-5 py-2 rounded-full text-xs md:text-sm font-extrabold shadow-md opd-total-time-display-ov">- นาที</span>
</h3>
<div class="flow-container flex flex-col md:flex-row justify-between items-center py-2 md:py-4 relative">
<div class="flow-line hidden md:block"></div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-blue-200 text-blue-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-id-card text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ยื่นบัตร</span>
</div>
<span class="t-step-1-ov text-xs font-bold text-blue-700 bg-blue-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-blue-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-indigo-200 text-indigo-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-clipboard-list text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ซักประวัติ</span>
</div>
<span class="t-step-2-ov text-xs font-bold text-indigo-700 bg-indigo-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-indigo-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-10 h-10 md:w-20 md:h-20 bg-gradient-to-br from-orange-500 to-amber-400 text-white border-2 md:border-4 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-clock text-sm md:text-3xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รอพบแพทย์</span>
</div>
<span class="t-step-3-ov text-xs font-bold text-orange-700 bg-orange-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-orange-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-teal-200 text-teal-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-doctor text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">แพทย์ตรวจ</span>
</div>
<span class="t-step-4-ov text-xs font-bold text-teal-700 bg-teal-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-teal-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-emerald-200 text-emerald-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-pills text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รับยากลับบ้าน</span>
</div>
<span class="t-step-5-ov text-xs font-bold text-emerald-700 bg-emerald-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-emerald-100 z-10">- นาที</span>
</div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-6 mb-3 border-l-4 border-blue-500 pl-3">ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 md:gap-4 mb-6" id="db-clinic-wait-cards">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4 md:mb-6 flex items-center justify-between">
<span class="flex items-center"><i class="fa-solid fa-chart-area text-blue-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD</span>
</h3>
<div class="h-[300px] md:h-[400px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="opdWaitTimeChartTabOv_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
@@ -0,0 +1,357 @@
<!-- TAB 2: ระยะเวลาตามเงื่อนไข (Condition) -->
<div id="tab-opd-condition" class="tab-content hidden space-y-4 md:space-y-6">
<form class="opd-condition-filter-form card-premium p-4 bg-gradient-to-r from-slate-50 to-white border-l-4 border-l-indigo-500 shadow-sm flex flex-col gap-3.5 mb-6">
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">การนัดหมาย</label>
<select name="appoint" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มาตามนัด</option>
<!--<option value="2">มาไม่ตรงนัด</option>-->
<option value="3">Walk in (ไม่นัด)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">จากจุด (Step From)</label>
<select name="step_from" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="vsttime">1. ยื่นบัตร (Register)</option>
<option value="service4">2. ซักประวัติ (Screen)</option>
<option value="service11">3. รอพบแพทย์ (Wait Doctor)</option>
<option value="service5">4. แพทย์ตรวจ (Doctor Check)</option>
<option value="service12">5. รับยา (RX)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงจุด (Step To)</label>
<select name="step_to" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="service6" selected>เสร็จสิ้นทั้งหมด (Service 6)</option>
<option value="service12">5. รับยา (RX)</option>
<option value="service5">4. แพทย์ตรวจ (Doctor Check)</option>
<option value="service11">3. รอพบแพทย์ (Wait Doctor)</option>
<option value="service4">2. ซักประวัติ (Screen)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">คลินิก</label>
<select name="clinic_id" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="000">ผู้ป่วยนอก (ทั่วไป)</option>
<option value="130">คลินิกอายุรกรรม</option>
<option value="102">คลินิกศัลยกรรม</option>
<option value="149">คลินิกระบบประสาท</option>
<option value="168">คลินิกกุมารเวชกรรม</option>
<option value="220">คลินิกสูติ-นรีเวช</option>
<option value="073">คลินิกตา</option>
<option value="072">คลินิกกระดูก</option>
<option value="111">คลินิกจิตเวช</option>
<option value="139">คลินิกเบาหวาน</option>
<option value="140">คลินิกความดัน</option>
<option value="154">คลินิกไขมัน</option>
<option value="137">คลินิกไต</option>
<option value="110">หอบหืด</option>
<option value="009">ฝากครรภ์</option>
<option value="227">ทางเดินอาหารและตับ</option>
<option value="024">คลินิกวัณโรค</option>
<option value="225">คลินิกอายุรกรรมปอด</option>
<option value="017">ทันตกรรม</option>
<option value="132">คลินิกหัวใจ</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ช่วงเวลา</label>
<select name="time_period" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด (08:00 - 16:00)</option>
<option value="morning">เช้า (08:00 - 12:00)</option>
<option value="afternoon">บ่าย (12:01 - 16:00)</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 items-end">
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ Lab</label>
<select name="lab_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี Lab</option>
<option value="0">ไม่มี Lab</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ X-ray</label>
<select name="xray_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี X-ray</option>
<option value="0">ไม่มี X-ray</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ตั้งแต่วันที่</label>
<input type="date" name="start_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-01') ?>">
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงวันที่</label>
<input type="date" name="end_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-d') ?>">
</div>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-2 px-4 rounded-lg h-[38px] flex items-center justify-center transition-all shadow-md shadow-indigo-500/10">
<i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหาเงื่อนไข
</button>
</div>
</form>
<div class="card-premium p-4 md:p-8 bg-white relative overflow-visible mb-6">
<div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-gradient-to-b from-indigo-500 to-purple-500 rounded-l-xl"></div>
<h3 class="font-bold text-slate-800 mb-4 md:mb-8 flex flex-col md:flex-row items-start md:items-center text-base md:text-lg">
<div class="flex items-center mb-2 md:mb-0">
<div class="w-6 h-6 md:w-8 md:h-8 rounded bg-indigo-100 text-indigo-600 flex items-center justify-center mr-2 md:mr-3"><i class="fa-solid fa-filter"></i></div>ผลลัพธ์เส้นทางระยะเวลารอคอย (ตามเงื่อนไข)
</div>
<span class="md:ml-auto bg-slate-100 text-slate-500 px-5 py-2 rounded-full text-xs md:text-sm font-extrabold shadow-md opd-total-time-display-cond">- นาที</span>
</h3>
<div class="flow-container flex flex-col md:flex-row justify-between items-center py-2 md:py-4 relative">
<div class="flow-line hidden md:block" style="background: linear-gradient(90deg, #6366f1, #a855f7);"></div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-blue-200 text-blue-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-id-card text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ยื่นบัตร</span>
</div>
<span class="t-step-1-cond text-xs font-bold text-blue-700 bg-blue-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-blue-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-indigo-200 text-indigo-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-clipboard-list text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ซักประวัติ</span>
</div>
<span class="t-step-2-cond text-xs font-bold text-indigo-700 bg-indigo-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-indigo-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-10 h-10 md:w-20 md:h-20 bg-gradient-to-br from-orange-500 to-amber-400 text-white border-2 md:border-4 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-clock text-sm md:text-3xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รอพบแพทย์</span>
</div>
<span class="t-step-3-cond text-xs font-bold text-orange-700 bg-orange-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-orange-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-teal-200 text-teal-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-doctor text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">แพทย์ตรวจ</span>
</div>
<span class="t-step-4-cond text-xs font-bold text-teal-700 bg-teal-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-teal-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-emerald-200 text-emerald-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-pills text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รับยากลับบ้าน</span>
</div>
<span class="t-step-5-cond text-xs font-bold text-emerald-700 bg-emerald-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-emerald-100 z-10">- นาที</span>
</div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-6 mb-3 border-l-4 border-indigo-500 pl-3">ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 md:gap-4 mb-6" id="clinic-wait-cards-opd-cond">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4 md:mb-6 flex items-center justify-between">
<span class="flex items-center"><i class="fa-solid fa-chart-area text-indigo-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD (ตามเงื่อนไข)</span>
</h3>
<div class="h-[300px] md:h-[400px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="opdWaitTimeChartTabCond_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
<script>
async function fetchOpdConditionRealData(formElement) {
let params = new URLSearchParams(new FormData(formElement));
params.append('ajax_opd_cond', '1');
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_COND_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_COND_START---')[1].split('---AJAX_OPD_COND_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด");
}
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('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- "));
}
// Update DOM Elements
const safeSet = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
safeSet('.opd-total-time-display-cond', (data.flowchart.custom_total > 0) ? `ผลรวมช่วงเวลาที่เลือก ${data.flowchart.custom_total.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-1-cond', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-2-cond', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-3-cond', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-4-cond', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-5-cond', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// Clinic Grid
const clinicIconsDb = {
'ผู้ป่วยนอก (ทั่วไป)': 'fa-hospital-user',
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'กระดูก': 'fa-bone',
'จิตเวช': 'fa-brain',
'ทันตกรรม': 'fa-tooth'
};
const el = document.getElementById('clinic-wait-cards-opd-cond');
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait.toFixed(2)} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลคลินิกที่เข้าเงื่อนไข</div>';
}
}
// Trend Chart
const chartId = 'opdWaitTimeChartTabCond';
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
let grad = ctx.createLinearGradient(0, 0, 0, 400);
grad.addColorStop(0, 'rgba(99, 102, 241, 0.45)');
grad.addColorStop(1, 'rgba(99, 102, 241, 0.0)');
if (window.appCharts && window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
borderColor: '#6366f1',
backgroundColor: grad,
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#6366f1',
pointBorderWidth: 2,
fill: true,
tension: 0.4,
datalabels: {
anchor: 'end',
align: 'top',
color: '#4f46e5',
font: {
weight: 'bold'
},
formatter: v => Number(v).toFixed(1)
}
},
{
label: 'จำนวนผู้ป่วย (ราย)',
data: data.trend_pt,
borderColor: 'transparent',
backgroundColor: '#ec4899',
pointBackgroundColor: '#ec4899',
pointBorderColor: 'transparent',
pointRadius: 0,
hoverRadius: 0,
pointStyle: 'circle',
datalabels: {
anchor: 'center',
align: 'bottom',
offset: 12,
color: '#ec4899',
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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
} catch (err) {
console.error("Fetch Condition Data Error:", err);
alert("เกิดข้อผิดพลาดในการดึงข้อมูลเงื่อนไข\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('.opd-condition-filter-form');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdConditionRealData(e.target);
});
}
});
</script>
@@ -0,0 +1,107 @@
<div id="tab-opd-condition" class="tab-content hidden space-y-4 md:space-y-6">
<form class="opd-filter-form card-premium p-4 bg-gradient-to-r from-slate-50 to-white border-l-4 border-l-indigo-500 shadow-sm flex flex-col gap-3.5 mb-6">
<input type="hidden" name="filter_type" value="date">
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">การนัดหมาย</label><select name="appoint" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มาตามนัด</option>
<option value="2">ไม่มาตามนัด</option>
<option value="3">Walk in</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">จากจุด (Step)</label><select name="step_from" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option>ยื่นบัตร</option>
<option>ซักประวัติ</option>
<option>รอพบแพทย์</option>
<option>แพทย์ตรวจ</option>
<option>รับยากลับบ้าน</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงจุด (Step)</label><select name="step_to" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option>รับยากลับบ้าน</option>
<option>แพทย์ตรวจ</option>
<option>รอพบแพทย์</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">คลินิก</label><select name="clinic_id" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="130">อายุรกรรม</option>
<option value="102">ศัลยกรรม</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ช่วงเวลา</label><select name="time_period" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="morning">เช้า</option>
<option value="afternoon">บ่าย</option>
</select></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 items-end">
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ Lab</label><select name="lab_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี Lab</option>
<option value="0">ไม่มี Lab</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ X-ray</label><select name="xray_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี X-ray</option>
<option value="0">ไม่มี X-ray</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ตั้งแต่วันที่</label><input type="date" name="start_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-01') ?>"></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงวันที่</label><input type="date" name="end_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-d') ?>"></div>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-2 px-4 rounded-lg h-[38px] flex items-center justify-center transition-all shadow-md shadow-indigo-500/10"><i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหาเงื่อนไข</button>
</div>
</form>
<div class="card-premium p-4 md:p-8 bg-white relative overflow-visible mb-6">
<div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-gradient-to-b from-indigo-500 to-purple-500 rounded-l-xl"></div>
<h3 class="font-bold text-slate-800 mb-4 md:mb-8 flex flex-col md:flex-row items-start md:items-center text-base md:text-lg">
<div class="flex items-center mb-2 md:mb-0">
<div class="w-6 h-6 md:w-8 md:h-8 rounded bg-indigo-100 text-indigo-600 flex items-center justify-center mr-2 md:mr-3"><i class="fa-solid fa-filter"></i></div>ผลลัพธ์เส้นทางระยะเวลารอคอย (ตามเงื่อนไข)
</div>
<span class="md:ml-auto bg-slate-100 text-slate-500 px-5 py-2 rounded-full text-xs md:text-sm font-extrabold shadow-md opd-total-time-display-cond">- นาที</span>
</h3>
<div class="flow-container flex flex-col md:flex-row justify-between items-center py-2 md:py-4 relative">
<div class="flow-line hidden md:block" style="background: linear-gradient(90deg, #6366f1, #a855f7);"></div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-blue-200 text-blue-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-id-card text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ยื่นบัตร</span>
</div>
<span class="t-step-1-cond text-xs font-bold text-blue-700 bg-blue-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-blue-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-indigo-200 text-indigo-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-clipboard-list text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ซักประวัติ</span>
</div>
<span class="t-step-2-cond text-xs font-bold text-indigo-700 bg-indigo-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-indigo-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-10 h-10 md:w-20 md:h-20 bg-gradient-to-br from-orange-500 to-amber-400 text-white border-2 md:border-4 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-clock text-sm md:text-3xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รอพบแพทย์</span>
</div>
<span class="t-step-3-cond text-xs font-bold text-orange-700 bg-orange-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-orange-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-teal-200 text-teal-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-doctor text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">แพทย์ตรวจ</span>
</div>
<span class="t-step-4-cond text-xs font-bold text-teal-700 bg-teal-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-teal-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-emerald-200 text-emerald-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-pills text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รับยากลับบ้าน</span>
</div>
<span class="t-step-5-cond text-xs font-bold text-emerald-700 bg-emerald-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-emerald-100 z-10">- นาที</span>
</div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-6 mb-3 border-l-4 border-indigo-500 pl-3">ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 md:gap-4 mb-6" id="clinic-wait-cards-opd-cond">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4 md:mb-6 flex items-center justify-between">
<span class="flex items-center"><i class="fa-solid fa-chart-area text-indigo-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD (ตามเงื่อนไข)</span>
</h3>
<div class="h-[300px] md:h-[400px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="opdWaitTimeChartTabCond_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
@@ -0,0 +1,282 @@
<!-- TAB 2: ระยะเวลาตามเงื่อนไข (Condition) -->
<div id="tab-opd-condition" class="tab-content hidden space-y-4 md:space-y-6">
<form class="opd-condition-filter-form card-premium p-4 bg-gradient-to-r from-slate-50 to-white border-l-4 border-l-indigo-500 shadow-sm flex flex-col gap-3.5 mb-6">
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">การนัดหมาย</label><select name="appoint" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มาตามนัด</option>
<option value="2">ไม่มาตามนัด</option>
<option value="3">Walk in</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">จากจุด (Step)</label><select name="step_from" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option>ยื่นบัตร</option>
<option>ซักประวัติ</option>
<option>รอพบแพทย์</option>
<option>แพทย์ตรวจ</option>
<option>รับยากลับบ้าน</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงจุด (Step)</label><select name="step_to" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option>รับยากลับบ้าน</option>
<option>แพทย์ตรวจ</option>
<option>รอพบแพทย์</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">คลินิก</label><select name="clinic_id" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="130">อายุรกรรม</option>
<option value="102">ศัลยกรรม</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ช่วงเวลา</label><select name="time_period" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="morning">เช้า</option>
<option value="afternoon">บ่าย</option>
</select></div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 items-end">
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ Lab</label><select name="lab_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี Lab</option>
<option value="0">ไม่มี Lab</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ X-ray</label><select name="xray_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี X-ray</option>
<option value="0">ไม่มี X-ray</option>
</select></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ตั้งแต่วันที่</label><input type="date" name="start_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-01') ?>"></div>
<div><label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงวันที่</label><input type="date" name="end_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-d') ?>"></div>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-2 px-4 rounded-lg h-[38px] flex items-center justify-center transition-all shadow-md shadow-indigo-500/10"><i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหาเงื่อนไข</button>
</div>
</form>
<div class="card-premium p-4 md:p-8 bg-white relative overflow-visible mb-6">
<div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-gradient-to-b from-indigo-500 to-purple-500 rounded-l-xl"></div>
<h3 class="font-bold text-slate-800 mb-4 md:mb-8 flex flex-col md:flex-row items-start md:items-center text-base md:text-lg">
<div class="flex items-center mb-2 md:mb-0">
<div class="w-6 h-6 md:w-8 md:h-8 rounded bg-indigo-100 text-indigo-600 flex items-center justify-center mr-2 md:mr-3"><i class="fa-solid fa-filter"></i></div>ผลลัพธ์เส้นทางระยะเวลารอคอย (ตามเงื่อนไข)
</div>
<span class="md:ml-auto bg-slate-100 text-slate-500 px-5 py-2 rounded-full text-xs md:text-sm font-extrabold shadow-md opd-total-time-display-cond">- นาที</span>
</h3>
<div class="flow-container flex flex-col md:flex-row justify-between items-center py-2 md:py-4 relative">
<div class="flow-line hidden md:block" style="background: linear-gradient(90deg, #6366f1, #a855f7);"></div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-blue-200 text-blue-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-id-card text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ยื่นบัตร</span>
</div>
<span class="t-step-1-cond text-xs font-bold text-blue-700 bg-blue-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-blue-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-indigo-200 text-indigo-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-clipboard-list text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ซักประวัติ</span>
</div>
<span class="t-step-2-cond text-xs font-bold text-indigo-700 bg-indigo-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-indigo-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-10 h-10 md:w-20 md:h-20 bg-gradient-to-br from-orange-500 to-amber-400 text-white border-2 md:border-4 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-clock text-sm md:text-3xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รอพบแพทย์</span>
</div>
<span class="t-step-3-cond text-xs font-bold text-orange-700 bg-orange-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-orange-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-teal-200 text-teal-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-doctor text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">แพทย์ตรวจ</span>
</div>
<span class="t-step-4-cond text-xs font-bold text-teal-700 bg-teal-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-teal-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col">
<div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-emerald-200 text-emerald-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-pills text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รับยากลับบ้าน</span>
</div>
<span class="t-step-5-cond text-xs font-bold text-emerald-700 bg-emerald-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-emerald-100 z-10">- นาที</span>
</div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-6 mb-3 border-l-4 border-indigo-500 pl-3">ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 md:gap-4 mb-6" id="clinic-wait-cards-opd-cond">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4 md:mb-6 flex items-center justify-between">
<span class="flex items-center"><i class="fa-solid fa-chart-area text-indigo-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD (ตามเงื่อนไข)</span>
</h3>
<div class="h-[300px] md:h-[400px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="opdWaitTimeChartTabCond_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
<script>
async function fetchOpdConditionRealData(formElement) {
let params = new URLSearchParams(new FormData(formElement));
params.append('ajax_opd_cond', '1');
var overlay = document.getElementById('data-processing-overlay');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('flex');
}
try {
const response = await fetch('?' + params.toString(), {
method: 'GET'
});
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_COND_START---')) {
data = JSON.parse(text.split('---AJAX_OPD_COND_START---')[1].split('---AJAX_OPD_COND_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด");
}
// Update DOM Elements
const safeSet = (selector, text) => {
document.querySelectorAll(selector).forEach(el => el.innerText = text);
};
safeSet('.opd-total-time-display-cond', (data.flowchart.total > 0) ? `เฉลี่ยรวม ${data.flowchart.total.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-1-cond', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-2-cond', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-3-cond', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-4-cond', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-5-cond', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// Clinic Grid
const clinicIconsDb = {
'อายุรกรรม': 'fa-stethoscope',
'ศัลยกรรม': 'fa-syringe',
'กุมารเวชกรรม': 'fa-child-reaching',
'สูติ-นรีเวช': 'fa-person-pregnant',
'ตา': 'fa-eye',
'กระดูก': 'fa-bone'
};
const el = document.getElementById('clinic-wait-cards-opd-cond');
if (el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait.toFixed(2)} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else {
el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลคลินิกที่เข้าเงื่อนไข</div>';
}
}
// Trend Chart
const chartId = 'opdWaitTimeChartTabCond';
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
let grad = ctx.createLinearGradient(0, 0, 0, 400);
grad.addColorStop(0, 'rgba(99, 102, 241, 0.45)');
grad.addColorStop(1, 'rgba(99, 102, 241, 0.0)');
if (window.appCharts && window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [{
label: 'เวลาเฉลี่ย (นาที)',
data: data.trend_wait,
borderColor: '#6366f1',
backgroundColor: grad,
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#6366f1',
pointBorderWidth: 2,
fill: true,
tension: 0.4,
datalabels: {
anchor: 'end',
align: 'top',
color: '#4f46e5',
font: {
weight: 'bold'
},
formatter: v => Number(v).toFixed(1)
}
},
{
label: 'จำนวนผู้ป่วย (ราย)',
data: data.trend_pt,
borderColor: 'transparent',
backgroundColor: '#ec4899',
pointBackgroundColor: '#ec4899',
pointBorderColor: 'transparent',
pointRadius: 0,
hoverRadius: 0,
pointStyle: 'circle',
datalabels: {
anchor: 'center',
align: 'bottom',
offset: 12,
color: '#ec4899',
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_wait, 0) * 1.5
}
},
layout: {
padding: {
top: 20
}
}
}
});
} else if (container) {
container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>';
}
} catch (err) {
console.error("Fetch Condition Data Error:", err);
alert("เกิดข้อผิดพลาดในการดึงข้อมูลเงื่อนไข\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('.opd-condition-filter-form');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdConditionRealData(e.target);
});
}
});
</script>
@@ -0,0 +1,231 @@
<!-- TAB 2: ระยะเวลาตามเงื่อนไข (Condition) -->
<div id="tab-opd-condition" class="tab-content hidden space-y-4 md:space-y-6">
<form class="opd-condition-filter-form card-premium p-4 bg-gradient-to-r from-slate-50 to-white border-l-4 border-l-indigo-500 shadow-sm flex flex-col gap-3.5 mb-6">
<div class="grid grid-cols-2 md:grid-cols-5 gap-3">
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">การนัดหมาย</label>
<select name="appoint" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มาตามนัด</option>
<option value="3">Walk in (ไม่นัด)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">จากจุด (Step From)</label>
<select name="step_from" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="vsttime">1. ยื่นบัตร (Register)</option>
<option value="service4">2. ซักประวัติ (Screen)</option>
<option value="service11">3. รอพบแพทย์ (Wait Doctor)</option>
<option value="service5">4. แพทย์ตรวจ (Doctor Check)</option>
<option value="service12">5. รับยา (RX)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงจุด (Step To)</label>
<select name="step_to" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="service6" selected>เสร็จสิ้นทั้งหมด (Service 6)</option>
<option value="service12">5. รับยา (RX)</option>
<option value="service5">4. แพทย์ตรวจ (Doctor Check)</option>
<option value="service11">3. รอพบแพทย์ (Wait Doctor)</option>
<option value="service4">2. ซักประวัติ (Screen)</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">คลินิก</label>
<select name="clinic_id" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="000">ผู้ป่วยนอก (ทั่วไป)</option><option value="130">คลินิกอายุรกรรม</option>
<option value="102">คลินิกศัลยกรรม</option><option value="149">คลินิกระบบประสาท</option>
<option value="168">คลินิกกุมารเวชกรรม</option><option value="220">คลินิกสูติ-นรีเวช</option>
<option value="073">คลินิกตา</option><option value="072">คลินิกกระดูก</option>
<option value="111">คลินิกจิตเวช</option><option value="139">คลินิกเบาหวาน</option>
<option value="140">คลินิกความดัน</option><option value="154">คลินิกไขมัน</option>
<option value="137">คลินิกไต</option><option value="110">หอบหืด</option>
<option value="009">ฝากครรภ์</option><option value="227">ทางเดินอาหารและตับ</option>
<option value="024">คลินิกวัณโรค</option><option value="225">คลินิกอายุรกรรมปอด</option>
<option value="017">ทันตกรรม</option><option value="132">คลินิกหัวใจ</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ช่วงเวลา</label>
<select name="time_period" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด (08:00 - 16:00)</option>
<option value="morning">เช้า (08:00 - 12:00)</option>
<option value="afternoon">บ่าย (12:01 - 16:00)</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 items-end">
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ Lab</label>
<select name="lab_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี Lab</option>
<option value="0">ไม่มี Lab</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">สถานะ X-ray</label>
<select name="xray_status" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none">
<option value="">ทั้งหมด</option>
<option value="1">มี X-ray</option>
<option value="0">ไม่มี X-ray</option>
</select>
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ตั้งแต่วันที่</label>
<input type="date" name="start_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-01') ?>">
</div>
<div>
<label class="block text-[10px] md:text-xs font-bold text-slate-600 mb-1">ถึงวันที่</label>
<input type="date" name="end_date" class="w-full bg-white border border-slate-300 rounded-lg p-2 text-xs md:text-sm outline-none" value="<?= date('Y-m-d') ?>">
</div>
<button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-2 px-4 rounded-lg h-[38px] flex items-center justify-center transition-all shadow-md shadow-indigo-500/10">
<i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหาเงื่อนไข
</button>
</div>
</form>
<div class="card-premium p-4 md:p-8 bg-white relative overflow-visible mb-6">
<div class="absolute top-0 left-0 w-1 md:w-1.5 h-full bg-gradient-to-b from-indigo-500 to-purple-500 rounded-l-xl"></div>
<h3 class="font-bold text-slate-800 mb-4 md:mb-8 flex flex-col md:flex-row items-start md:items-center text-base md:text-lg">
<div class="flex items-center mb-2 md:mb-0"><div class="w-6 h-6 md:w-8 md:h-8 rounded bg-indigo-100 text-indigo-600 flex items-center justify-center mr-2 md:mr-3"><i class="fa-solid fa-filter"></i></div>ผลลัพธ์เส้นทางระยะเวลารอคอย (ตามเงื่อนไข)</div>
<span class="md:ml-auto bg-slate-100 text-slate-500 px-5 py-2 rounded-full text-xs md:text-sm font-extrabold shadow-md opd-total-time-display-cond">- นาที</span>
</h3>
<div class="flow-container flex flex-col md:flex-row justify-between items-center py-2 md:py-4 relative">
<div class="flow-line hidden md:block" style="background: linear-gradient(90deg, #6366f1, #a855f7);"></div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col"><div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-blue-200 text-blue-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-id-card text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ยื่นบัตร</span></div>
<span class="t-step-1-cond text-xs font-bold text-blue-700 bg-blue-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-blue-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col"><div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-indigo-200 text-indigo-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-clipboard-list text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">ซักประวัติ</span></div>
<span class="t-step-2-cond text-xs font-bold text-indigo-700 bg-indigo-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-indigo-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col"><div class="w-10 h-10 md:w-20 md:h-20 bg-gradient-to-br from-orange-500 to-amber-400 text-white border-2 md:border-4 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-clock text-sm md:text-3xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รอพบแพทย์</span></div>
<span class="t-step-3-cond text-xs font-bold text-orange-700 bg-orange-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-orange-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col"><div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-teal-200 text-teal-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-user-doctor text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">แพทย์ตรวจ</span></div>
<span class="t-step-4-cond text-xs font-bold text-teal-700 bg-teal-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-teal-100 z-10">- นาที</span>
</div>
<div class="flex md:flex-col items-center justify-between md:justify-start w-full md:w-auto mb-3 md:mb-0 bg-slate-50 md:bg-transparent p-3 md:p-0 rounded-lg border border-slate-100 md:border-transparent">
<div class="flex items-center md:flex-col"><div class="w-9 h-9 md:w-16 md:h-16 bg-white border-[2px] md:border-[3px] border-emerald-200 text-emerald-500 rounded-full shadow-md flex items-center justify-center md:mb-3 mr-3 md:mr-0 z-10"><i class="fa-solid fa-pills text-xs md:text-2xl"></i></div><span class="text-xs md:text-sm font-extrabold text-slate-700 bg-white md:px-2 z-10">รับยากลับบ้าน</span></div>
<span class="t-step-5-cond text-xs font-bold text-emerald-700 bg-emerald-50 px-2 md:px-3 py-0.5 rounded-full md:mt-1 border border-emerald-100 z-10">- นาที</span>
</div>
</div>
</div>
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mt-6 mb-3 border-l-4 border-indigo-500 pl-3">ระยะเวลารอคอยแต่ละคลินิกเฉลี่ย</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3 md:gap-4 mb-6" id="clinic-wait-cards-opd-cond">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4 md:mb-6 flex items-center justify-between">
<span class="flex items-center"><i class="fa-solid fa-chart-area text-indigo-500 mr-2"></i> แนวโน้มระยะเวลารอคอย OPD (ตามเงื่อนไข)</span>
</h3>
<div class="h-[300px] md:h-[400px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="opdWaitTimeChartTabCond_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
<script>
async function fetchOpdConditionRealData(formElement) {
let params = new URLSearchParams(new FormData(formElement));
params.append('ajax_opd_cond', '1');
var overlay = document.getElementById('data-processing-overlay');
if (overlay) { overlay.classList.remove('hidden'); overlay.classList.add('flex'); }
try {
const response = await fetch('?' + params.toString(), { method: 'GET' });
const text = await response.text();
let data = {};
if (text.includes('---AJAX_OPD_COND_START---')) { data = JSON.parse(text.split('---AJAX_OPD_COND_START---')[1].split('---AJAX_OPD_COND_END---')[0]); }
else { throw new Error("รูปแบบข้อมูลผิดพลาด"); }
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false; let errorDetails = [];
const logContent = document.getElementById('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- "));
}
// Update DOM Elements
const safeSet = (selector, text) => { document.querySelectorAll(selector).forEach(el => el.innerText = text); };
safeSet('.opd-total-time-display-cond', (data.flowchart.custom_total > 0) ? `ผลรวมช่วงเวลาที่เลือก ${data.flowchart.custom_total.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-1-cond', (data.flowchart.step1 > 0) ? `${data.flowchart.step1.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-2-cond', (data.flowchart.step2 > 0) ? `${data.flowchart.step2.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-3-cond', (data.flowchart.step3 > 0) ? `${data.flowchart.step3.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-4-cond', (data.flowchart.step4 > 0) ? `${data.flowchart.step4.toFixed(2)} นาที` : '- นาที');
safeSet('.t-step-5-cond', (data.flowchart.step5 > 0) ? `${data.flowchart.step5.toFixed(2)} นาที` : '- นาที');
// Clinic Grid
const clinicIconsDb = { 'ผู้ป่วยนอก (ทั่วไป)': 'fa-hospital-user', 'อายุรกรรม': 'fa-stethoscope', 'ศัลยกรรม': 'fa-syringe', 'กุมารเวชกรรม': 'fa-child-reaching', 'สูติ-นรีเวช': 'fa-person-pregnant', 'ตา': 'fa-eye', 'กระดูก': 'fa-bone', 'จิตเวช': 'fa-brain', 'ทันตกรรม': 'fa-tooth' };
const el = document.getElementById('clinic-wait-cards-opd-cond');
if(el) {
if (data.clinics_wait && data.clinics_wait.length > 0) {
el.innerHTML = data.clinics_wait.map(c => {
let bgClass = c.wait >= 120 ? 'bg-gradient-to-br from-rose-500 via-red-500 to-pink-500' : 'bg-gradient-to-br from-indigo-500 via-purple-500 to-fuchsia-600';
let icon = clinicIconsDb[c.name] || 'fa-user-doctor';
return `<div class="rounded-xl p-3 md:p-4 shadow-md ${bgClass} transition-all duration-300 hover:scale-[1.03] hover:shadow-lg relative overflow-hidden group"><div class="absolute -right-4 -top-4 w-16 h-16 rounded-full bg-white/10 blur-xl group-hover:bg-white/20 transition-all"></div><div class="flex justify-between items-start mb-1 relative z-10"><p class="text-xs md:text-sm font-bold truncate pr-2 text-white">${c.name}</p><div class="w-6 h-6 md:w-8 md:h-8 rounded-full bg-white/25 text-white flex items-center justify-center shrink-0"><i class="fa-solid ${icon} text-[10px] md:text-xs"></i></div></div><h3 class="text-2xl md:text-3xl font-black mt-2.5 leading-none text-white drop-shadow-md relative z-10 flex items-end">${c.wait.toFixed(2)} <span class="text-[10px] md:text-xs font-semibold ml-1 mb-0.5 text-white/80">นาที</span></h3></div>`;
}).join('');
} else { el.innerHTML = '<div class="col-span-full text-center text-slate-500 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">ไม่พบข้อมูลคลินิกที่เข้าเงื่อนไข</div>'; }
}
// Trend Chart
const chartId = 'opdWaitTimeChartTabCond';
const container = document.getElementById(chartId + '_container');
if (container && data.trend_labels && data.trend_labels.length > 0) {
container.innerHTML = `<canvas id="${chartId}"></canvas>`;
const ctx = document.getElementById(chartId).getContext('2d');
let grad = ctx.createLinearGradient(0, 0, 0, 400);
grad.addColorStop(0, 'rgba(99, 102, 241, 0.45)'); grad.addColorStop(1, 'rgba(99, 102, 241, 0.0)');
if(window.appCharts && window.appCharts[chartId]) window.appCharts[chartId].destroy();
window.appCharts[chartId] = new Chart(ctx, {
type: 'line',
data: {
labels: data.trend_labels,
datasets: [
{ label: 'เวลาเฉลี่ย (นาที)', data: data.trend_wait, borderColor: '#6366f1', backgroundColor: grad, borderWidth: 3, pointBackgroundColor: '#ffffff', pointBorderColor: '#6366f1', pointBorderWidth: 2, fill: true, tension: 0.4, datalabels: { anchor: 'end', align: 'top', color: '#4f46e5', font: { weight: 'bold' }, formatter: v => Number(v).toFixed(1) } },
{ label: 'จำนวนผู้ป่วย (ราย)', data: data.trend_pt, borderColor: 'transparent', backgroundColor: '#ec4899', pointBackgroundColor: '#ec4899', pointBorderColor: 'transparent', pointRadius: 0, hoverRadius: 0, pointStyle: 'circle', datalabels: { anchor: 'center', align: 'bottom', offset: 12, color: '#ec4899', 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_wait, 0) * 1.5 } }, layout: { padding: { top: 20 } } }
});
} else if(container) { container.innerHTML = '<span class="text-slate-400 font-medium"><i class="fa-solid fa-box-open mr-2"></i>ไม่พบข้อมูลในช่วงเวลาที่เลือก</span>'; }
} catch (err) {
console.error("Fetch Condition Data Error:", err);
alert("เกิดข้อผิดพลาดในการดึงข้อมูลเงื่อนไข\nรายละเอียด: " + err.message);
} finally {
if (overlay) { setTimeout(() => { overlay.classList.remove('flex'); overlay.classList.add('hidden'); }, 500); }
}
}
document.addEventListener('DOMContentLoaded', () => {
const form = document.querySelector('.opd-condition-filter-form');
if (form) {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchOpdConditionRealData(e.target);
});
}
});
</script>
@@ -0,0 +1,59 @@
<!-- TAB 3: บริการผู้ป่วยนอก (OPD Services) -->
<div id="tab-opd-service" class="tab-content hidden space-y-4 md:space-y-6">
<?= $opd_filter_html ?>
<h3 class="font-extrabold text-slate-800 text-base md:text-lg mb-3 md:mb-4 border-l-4 border-blue-500 pl-3">การพัฒนาระบบบริการผู้ป่วยนอก</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-blue-500 bg-gradient-to-b from-blue-50/50 to-white relative overflow-hidden">
<i class="fa-solid fa-mobile-screen absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-blue-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-start relative z-10">
<div>
<p class="text-xs md:text-sm font-bold text-blue-800 mb-1">นัดหมายออนไลน์</p>
<p class="text-[9px] md:text-[11px] text-slate-500">แอปฯ หมอพร้อม, ระบบนัด</p>
</div>
</div>
<div class="mt-3 md:mt-4 relative z-10">
<h3 class="text-2xl md:text-3xl font-black text-blue-600"><span id="db-online-appoint">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ครั้ง</span></h3>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-purple-500 bg-gradient-to-b from-purple-50/50 to-white relative overflow-hidden">
<i class="fa-solid fa-video absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-purple-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-start relative z-10">
<div>
<p class="text-xs md:text-sm font-bold text-purple-800 mb-1">Telemedicine</p>
<p class="text-[9px] md:text-[11px] text-slate-500">พบแพทย์ออนไลน์</p>
</div>
</div>
<div class="mt-3 md:mt-4 relative z-10">
<h3 class="text-2xl md:text-3xl font-black text-purple-600"><span id="db-telemed-total">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ครั้ง</span></h3>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-amber-500 bg-gradient-to-b from-amber-50/50 to-white relative overflow-hidden">
<i class="fa-solid fa-motorcycle absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-amber-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-start relative z-10">
<div>
<p class="text-xs md:text-sm font-bold text-amber-800 mb-1">การส่งยาที่บ้าน</p>
<p class="text-[9px] md:text-[11px] text-slate-500">OPD Refill</p>
</div>
</div>
<div class="mt-3 md:mt-4 relative z-10">
<h3 class="text-2xl md:text-3xl font-black text-amber-600"><span id="db-home-delivery">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ครั้ง</span></h3>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4"><i class="fa-solid fa-video text-purple-500 mr-2"></i> 10 อันดับคลินิกให้บริการ Telemedicine</h3>
<div class="h-[250px] md:h-[350px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="dbServiceTeleChart_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
<div class="card-premium p-4 md:p-6 mb-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4"><i class="fa-solid fa-motorcycle text-blue-500 mr-2"></i> การส่งยาที่บ้าน OPD Refill (รายเดือน)</h3>
<div class="h-[250px] md:h-[350px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
</div>
@@ -0,0 +1,74 @@
<div id="tab-opd-refer" class="tab-content hidden space-y-4 md:space-y-6">
<?= $opd_filter_html ?>
<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 (OPD)</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="db-refer-in-opd">-</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 (OPD)</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="db-refer-out-opd">-</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="db-refer-back-opd">-</span> <span class="text-xs md:text-sm font-medium text-slate-500">ราย</span></h3>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-emerald-500 mr-2"></i> จำนวนรับ Refer In แยกปลายทาง (รพ.ต้นทาง)</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referInChartOpd_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
<div class="card-premium p-4 md:p-6 flex flex-col">
<h4 class="font-bold text-slate-700 text-xs md:text-sm mb-4"><i class="fa-solid fa-chart-column text-red-500 mr-2"></i> จำนวนส่ง Refer Out แยกปลายทาง</h4>
<div class="h-[250px] md:h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200" id="referOutChartOpd_container">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6 mb-6">
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-emerald-500 to-emerald-600 p-3 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-virus mr-2"></i> 10 อันดับโรคที่รับ Refer In</h3>
</div>
<div class="overflow-y-auto max-h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="db-list-refer-in">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-red-500 to-red-600 p-3 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-virus mr-2"></i> 10 อันดับโรคที่ส่ง Refer Out</h3>
</div>
<div class="overflow-y-auto max-h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="db-list-refer-out">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
</div>
</div>
@@ -0,0 +1,23 @@
<div id="tab-opd-doctor" class="tab-content hidden space-y-4 md:space-y-6">
<?= $opd_filter_html ?>
<div class="max-w-4xl mx-auto">
<div class="card-premium p-6 md:p-10 flex flex-col items-center bg-gradient-to-br from-blue-50 to-white border-t-8 border-t-blue-500 text-center mb-6 shadow-sm">
<div class="w-16 h-16 md:w-20 md:h-20 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center text-3xl md:text-4xl mb-4 shadow-inner"><i class="fa-solid fa-user-doctor"></i></div>
<h3 class="font-extrabold text-slate-800 text-xl md:text-2xl mb-1">แพทย์ลงตรวจก่อน 09.00 น.</h3>
<p class="text-sm text-slate-500 mb-6">แผนกผู้ป่วยนอก (OPD)</p>
<h2 class="text-5xl md:text-7xl font-black text-emerald-600 mb-2"><span id="db-doc-overall">-</span>%</h2>
<div class="w-full bg-slate-200 rounded-full h-4 mb-2 shadow-inner">
<div id="db-doc-overall-bar" class="bg-gradient-to-r from-emerald-400 to-emerald-500 h-4 rounded-full transition-all duration-500 border-t border-white/50" style="width: 0%"></div>
</div>
<p class="text-xs font-bold text-slate-400 w-full text-right mt-2">เป้าหมาย 100.00%</p>
</div>
<h3 class="font-extrabold text-slate-800 text-base md:text-lg mb-4 border-l-4 border-blue-500 pl-3">สถิติการลงตรวจแยกรายคลินิก</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4" id="db-doctor-clinic-grid">
<div class="col-span-full text-center text-slate-400 py-8 bg-white rounded-xl border border-dashed border-slate-300 font-medium">รอการค้นหาข้อมูล...</div>
</div>
</div>
</div>
@@ -0,0 +1,25 @@
<div id="tab-opd-satisfaction" class="tab-content hidden space-y-4 md:space-y-6">
<?= $opd_filter_html ?>
<div class="max-w-4xl mx-auto">
<div class="card-premium p-6 md:p-10 flex flex-col items-center bg-gradient-to-br from-pink-50 to-white border-t-8 border-t-pink-500 text-center shadow-sm">
<div class="w-20 h-20 md:w-24 md:h-24 rounded-full bg-pink-100 text-pink-500 flex items-center justify-center text-4xl md:text-5xl mb-6 shadow-inner"><i class="fa-solid fa-face-smile-beam"></i></div>
<h3 class="font-extrabold text-slate-800 text-xl md:text-2xl mb-2">ความพึงพอใจผู้รับบริการ</h3>
<p class="text-sm text-slate-500 mb-8">แผนกผู้ป่วยนอก (OPD)</p>
<h2 class="text-5xl md:text-7xl font-black text-pink-600 mb-6"><span id="db-satisfaction-score">-</span>%</h2>
<div class="w-full bg-slate-200 rounded-full h-4 mb-2 shadow-inner">
<div class="bg-gradient-to-r from-pink-400 to-pink-500 h-4 rounded-full border-t border-white/50" style="width: 0%"></div>
</div>
<p class="text-xs font-bold text-slate-400 w-full text-right mt-2">เป้าหมาย > 85.00%</p>
</div>
<div class="card-premium p-4 md:p-6 mt-6">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base mb-4"><i class="fa-solid fa-chart-line text-pink-500 mr-2"></i> แนวโน้มความพึงพอใจรายเดือน (%)</h3>
<div class="h-[300px] w-full flex items-center justify-center bg-slate-50 rounded-lg border border-dashed border-slate-200">
<span class="text-slate-400 font-medium">รอการค้นหาข้อมูล...</span>
</div>
</div>
</div>
</div>
@@ -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
?>
@@ -0,0 +1,905 @@
<?php
// views/tab_settings.php
$isSettingsAjax = isset($_GET['ajax_settings']) && $_GET['ajax_settings'] == '1';
$isTestSqlAjax = isset($_GET['ajax_test_sql']) && $_GET['ajax_test_sql'] == '1';
$isSaveSingleSqlAjax = isset($_GET['ajax_save_single_sql']) && $_GET['ajax_save_single_sql'] == '1';
$isAuthSettingsAjax = isset($_GET['ajax_auth_settings']) && $_GET['ajax_auth_settings'] == '1';
if ($isAuthSettingsAjax) {
while (ob_get_level()) { ob_end_clean(); }
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$raw_post = file_get_contents('php://input');
$data = json_decode($raw_post, true);
if ($data && isset($data['password'])) {
if (isset($settings_password) && $data['password'] === $settings_password) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'message' => 'รหัสผ่านไม่ถูกต้อง']);
}
}
}
exit;
}
if ($isTestSqlAjax) {
while (ob_get_level()) { ob_end_clean(); }
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$raw_post = file_get_contents('php://input');
$data = json_decode($raw_post, true);
$sql = '';
if ($data && isset($data['sql_text_b64'])) {
$sql = trim(urldecode(base64_decode($data['sql_text_b64'])));
} elseif ($data && isset($data['sql_text'])) {
$sql = trim($data['sql_text']);
}
if ($sql !== '') {
$current_month = (int)date('m');
$current_year = (int)date('Y');
$current_fy = ($current_month >= 10) ? $current_year + 1 : $current_year;
$params = [];
if (strpos($sql, ':start_ly') !== false) $params['start_ly'] = ($current_fy - 2) . '-10-01';
if (strpos($sql, ':end_ly') !== false) $params['end_ly'] = ($current_fy - 1) . '-09-30';
if (preg_match('/:start\b/', $sql)) $params['start'] = date('Y-m-01');
if (preg_match('/:end\b/', $sql)) $params['end'] = date('Y-m-d');
try {
if (stripos($sql, 'SELECT') === 0 && stripos($sql, 'LIMIT') === false) {
$sql_to_run = $sql . " LIMIT 5";
} else {
$sql_to_run = $sql;
}
$sql_compiled = $sql;
foreach ($params as $k => $v) {
$sql_compiled = preg_replace('/:' . $k . '\b/', "'" . $v . "'", $sql_compiled);
}
$stmt = $pdo->prepare($sql_to_run);
$stmt->execute($params);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode(['status' => 'success', 'data' => $results, 'params_used' => $params, 'sql_compiled' => $sql_compiled]);
} catch (\Exception $e) {
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid JSON']);
}
}
exit;
}
if ($isSaveSingleSqlAjax) {
while (ob_get_level()) { ob_end_clean(); }
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$raw_post = file_get_contents('php://input');
$data = json_decode($raw_post, true);
$sql_text = '';
if ($data && isset($data['sql_text_b64'])) {
$sql_text = trim(urldecode(base64_decode($data['sql_text_b64'])));
} elseif ($data && isset($data['sql_text'])) {
$sql_text = trim($data['sql_text']);
}
if ($data && isset($data['section'], $data['key']) && $sql_text !== '') {
$success = false;
$msg = '';
if (isset($pdo_dashboard) && $pdo_dashboard) {
try {
$stmt = $pdo_dashboard->prepare("
INSERT INTO waiting (section_name, query_key, sql_text)
VALUES (:section_name, :query_key, :sql_text)
ON DUPLICATE KEY UPDATE sql_text = :sql_text2
");
$stmt->execute([
':section_name' => $data['section'],
':query_key' => $data['key'],
':sql_text' => $sql_text,
':sql_text2' => $sql_text
]);
$success = true;
$msg = 'บันทึกคำสั่งสำเร็จ';
} catch (\Exception $e) {
$msg = 'DB Error: ' . $e->getMessage();
}
} else {
$sql_config_path = __DIR__ . '/../includes/sql_config.json';
$current_config = file_exists($sql_config_path) ? json_decode(file_get_contents($sql_config_path), true) : [];
if (!is_array($current_config)) $current_config = [];
if (!isset($current_config[$data['section']])) $current_config[$data['section']] = [];
$current_config[$data['section']][$data['key']] = $sql_text;
if (file_put_contents($sql_config_path, json_encode($current_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) !== false) {
$success = true;
$msg = 'บันทึกคำสั่งสำเร็จ (JSON)';
} else {
$msg = 'ไม่สามารถเขียนไฟล์ JSON ได้';
}
}
echo json_encode(['status' => $success ? 'success' : 'error', 'message' => $msg]);
} else {
echo json_encode(['status' => 'error', 'message' => 'Invalid Request']);
}
}
exit;
}
if ($isSettingsAjax) {
while (ob_get_level()) { ob_end_clean(); }
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/sql_config.php'; // เพื่อให้ได้ตัวแปร $sql_config ล่าสุด
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$raw_post = file_get_contents('php://input');
$new_config = json_decode($raw_post, true);
if (json_last_error() === JSON_ERROR_NONE) {
$success = false;
$msg = '';
// 1. ถ้ามี Database Dashboard ให้บันทึกลง Database
if (isset($pdo_dashboard) && $pdo_dashboard) {
try {
$pdo_dashboard->beginTransaction();
// ใช้ UPSERT เพื่อ Insert ถ้ายังไม่มี หรือ Update ถ้ามีแล้ว
$stmt = $pdo_dashboard->prepare("
INSERT INTO waiting (section_name, query_key, sql_text)
VALUES (:section_name, :query_key, :sql_text)
ON DUPLICATE KEY UPDATE sql_text = :sql_text2
");
foreach ($new_config as $section => $keys) {
foreach ($keys as $key => $sql_text) {
$stmt->execute([
':section_name' => $section,
':query_key' => $key,
':sql_text' => $sql_text,
':sql_text2' => $sql_text
]);
}
}
$pdo_dashboard->commit();
$success = true;
$msg = 'บันทึกการตั้งค่าลงฐานข้อมูลเรียบร้อยแล้ว';
} catch (\PDOException $e) {
$pdo_dashboard->rollBack();
$success = false;
$msg = 'เกิดข้อผิดพลาดฐานข้อมูล: ' . $e->getMessage();
}
} else {
// 2. Fallback ไปบันทึกลง JSON
$sql_config_path = __DIR__ . '/../includes/sql_config.json';
$current_config = file_exists($sql_config_path) ? json_decode(file_get_contents($sql_config_path), true) : [];
if (!is_array($current_config)) $current_config = [];
$merged_config = array_replace_recursive($current_config, $new_config);
$saved = file_put_contents($sql_config_path, json_encode($merged_config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
if ($saved !== false) {
$success = true;
$msg = 'บันทึกการตั้งค่าลงไฟล์ JSON แล้ว (ไม่ได้เชื่อมต่อฐานข้อมูล)';
} else {
$success = false;
$msg = 'ไม่สามารถบันทึกไฟล์ JSON ได้ (กรุณาตรวจสอบสิทธิ์โฟลเดอร์)';
}
}
if ($success) {
echo json_encode(['status' => 'success', 'message' => $msg]);
} else {
echo json_encode(['status' => 'error', 'message' => $msg]);
}
} else {
echo json_encode(['status' => 'error', 'message' => 'ข้อมูลที่ส่งมาไม่ถูกต้อง (Invalid JSON)']);
}
} else {
// GET request: ส่งค่า $sql_config ซึ่งอาจจะดึงจาก DB หรือ JSON มาแล้ว
$json_out = @json_encode(['status' => 'success', 'data' => $sql_config], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
if (!$json_out) {
echo json_encode(['status' => 'error', 'message' => 'JSON Encode Error: ' . json_last_error_msg()]);
} else {
echo $json_out;
}
}
exit;
}
?>
<div id="tab-settings" class="tab-content hidden animate-fade-in">
<div class="card-premium p-6">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4 border-b border-slate-100 pb-4">
<h3 class="text-xl font-bold text-slate-800 flex items-center">
<i class="fa-solid fa-gear text-slate-400 mr-3 text-2xl"></i>
ตั้งค่าชุดคำสั่ง SQL (SQL Settings)
</h3>
</div>
<div class="text-sm text-slate-600 mb-6 border-l-4 border-amber-500 pl-4 py-3 bg-amber-50 rounded-r shadow-sm">
<b class="text-amber-800"><i class="fa-solid fa-triangle-exclamation mr-1"></i> ข้อควรระวัง:</b> การแก้ไขคำสั่ง SQL ควรทำโดยผู้ที่มีความรู้ด้านโครงสร้างฐานข้อมูล<br>
โปรด <span class="text-red-500 font-bold">อย่าลบตัวแปร Binding</span> ออกจากคำสั่งเดิม ไม่เช่นนั้นระบบอาจเกิดข้อผิดพลาดในการรันคำสั่งได้<br><br>
<b class="text-slate-700"><i class="fa-solid fa-circle-info text-blue-500 mr-1"></i> คำแนะนำเกี่ยวกับตัวแปรวันที่ (Binding Parameters):</b>
<ul class="list-disc ml-6 mt-2 space-y-1">
<li><code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:start</code> - วันที่เริ่มต้นที่ถูกเลือกจากหน้า Dashboard (เช่น '2023-10-01')</li>
<li><code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:end</code> - วันที่สิ้นสุดที่ถูกเลือกจากหน้า Dashboard (เช่น '2024-09-30')</li>
<li><code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:start_ly</code> - วันที่เริ่มต้นของปีก่อนหน้า (ใช้สำหรับทำเส้นกราฟแนวโน้มเปรียบเทียบปีที่แล้ว)</li>
<li><code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:end_ly</code> - วันที่สิ้นสุดของปีก่อนหน้า (ใช้สำหรับทำเส้นกราฟแนวโน้มเปรียบเทียบปีที่แล้ว)</li>
</ul>
<br>
<b class="text-slate-700"><i class="fa-solid fa-play text-blue-500 mr-1"></i> คำแนะนำการใช้งานปุ่ม "ทดสอบ":</b>
<ul class="list-disc ml-6 mt-2 space-y-1">
<li>คุณสามารถปล่อยตัวแปร <code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:start</code> และ <code class="bg-white border border-slate-200 px-1.5 py-0.5 rounded text-pink-600 font-mono text-xs">:end</code> ไว้ในคำสั่งตามเดิมได้เลย ไม่ต้องลบออก</li>
<li>เมื่อกดปุ่มทดสอบ ระบบจะ <b class="text-blue-600">จำลองวันที่ปัจจุบัน</b> เข้าไปแทนที่ตัวแปรเหล่านั้นให้อัตโนมัติ (เช่น วันที่ 1 ถึง วันนี้ของเดือนปัจจุบัน)</li>
<li>ผลลัพธ์จะถูกดึงมาแสดงผลเป็นตารางตัวอย่างเพียง <b class="text-emerald-600">5 แถวแรก</b> เท่านั้น เพื่อให้ตรวจสอบความถูกต้องได้รวดเร็ว</li>
</ul>
</div>
<div id="settings-loading" class="text-center py-12">
<i class="fa-solid fa-circle-notch fa-spin text-4xl text-blue-500 mb-4"></i>
<p class="text-slate-500 font-medium tracking-wide">กำลังโหลดข้อมูลตั้งค่า...</p>
</div>
<form id="form-sql-settings" class="hidden space-y-6">
<!-- Sections will be injected here via JS -->
</form>
</div>
</div>
<script>
const settingsSchema = {
'overview': {
title: 'ภาพรวมโรงพยาบาล (Overview)',
icon: 'fa-chart-pie',
color: 'text-blue-500',
fields: {
'opd_count': 'ยอดรวมผู้ป่วยนอก (OPD)',
'ipd_count': 'ยอดรวมผู้ป่วยใน (IPD)',
'cmi_avg': 'ค่าเฉลี่ย CMI',
'trend_opd': 'แนวโน้มผู้ป่วยนอก (Trend)',
'trend_ipd': 'แนวโน้มผู้ป่วยใน (Trend)',
'refer_in': 'รับส่งต่อ (Refer In)',
'refer_out': 'ส่งต่อ (Refer Out)',
'doc_before_9': 'แพทย์ลงตรวจก่อน 9.00 น.',
'satisfaction': 'ความพึงพอใจ',
'refer_back': 'ส่งกลับ (Refer Back)'
},
hints: {
'opd_count': 'ระบบต้องการเพียง 1 คอลัมน์ (ผลลัพธ์ยอดรวม OPD)',
'ipd_count': 'ระบบต้องการเพียง 1 คอลัมน์ (ผลลัพธ์ยอดรวม IPD)',
'cmi_avg': 'ระบบต้องการเพียง 1 คอลัมน์ (ผลลัพธ์ค่าเฉลี่ย CMI)',
'trend_opd': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (เดือนปี YYYY-MM), <b>avg_wait</b> (เวลารอคอยเฉลี่ย)',
'trend_ipd': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (เดือนปี YYYY-MM), <b>occ_rate</b> (อัตราครองเตียง)',
'refer_in': 'ระบบต้องการเพียง 1 คอลัมน์ (ยอดรวม Refer In)',
'refer_out': 'ระบบต้องการเพียง 1 คอลัมน์ (ยอดรวม Refer Out)',
'doc_before_9': 'ระบบต้องการเพียง 1 คอลัมน์ (เปอร์เซ็นต์แพทย์ออกตรวจก่อน 9 โมง)',
'satisfaction': 'ระบบต้องการเพียง 1 คอลัมน์ (คะแนนความพึงพอใจ)',
'refer_back': 'ระบบต้องการเพียง 1 คอลัมน์ (ยอดส่งกลับ)'
}
},
'opd': {
title: 'ลดรอคอย OPD',
icon: 'fa-stopwatch',
color: 'text-blue-400',
fields: {
'flowchart': 'ขั้นตอนการบริการ (Flowchart)',
'trend': 'แนวโน้มการรอคอย (Trend)',
'clinics': 'เวลารอคอยแยกตามคลินิก',
'tele_total': 'ยอดรวม Telemedicine',
'tele_clinics': 'Telemedicine แยกคลินิก',
'refill_total': 'ยอดรวมส่งยาที่บ้าน (Refill)',
'refill_trend': 'แนวโน้มส่งยาที่บ้าน (Trend)',
'refer_in': 'รับ Refer In',
'refer_out': 'ส่ง Refer Out',
'refer_back': 'ส่งกลับ (Refer Back)',
'refer_in_dest': 'ปลายทาง Refer In',
'refer_out_dest': 'ปลายทาง Refer Out',
'refer_in_top': '10 อันดับโรค Refer In',
'refer_out_top': '10 อันดับโรค Refer Out',
'doc_time': 'เวลาแพทย์ลงตรวจ'
},
hints: {
'flowchart': 'ระบบต้องการ 6 คอลัมน์: <b>step1, step2, step3, step4, step5, custom_total</b>',
'trend': 'ระบบต้องการ 3 คอลัมน์: <b>ym</b> (YYYY-MM), <b>avg_wait</b>, <b>pt_count</b>',
'clinics': 'ระบบต้องการ 2 คอลัมน์: <b>main_dep</b> (รหัสแผนก), <b>start2finish</b> (เวลารวม)',
'tele_total': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'tele_clinics': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อแผนก), <b>val</b> (ยอดรวม)',
'refill_total': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refill_trend': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (ยอดรวม)',
'refer_in': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_out': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_back': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_in_dest': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ปลายทาง), <b>val</b> (ยอดรวม)',
'refer_out_dest': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ปลายทาง), <b>val</b> (ยอดรวม)',
'refer_in_top': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>a</b> (ยอดรวม)',
'refer_out_top': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>a</b> (ยอดรวม)',
'doc_time': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อแผนก), <b>avg_time</b> (เวลาเฉลี่ย)'
}
},
'ipd': {
title: 'ลดแออัด IPD',
icon: 'fa-bed-pulse',
color: 'text-emerald-500',
fields: {
'occ_all': 'อัตราครองเตียง (All)',
'occ_max': 'อัตราครองเตียง (Max Ward)',
'occ_min': 'อัตราครองเตียง (Min Ward)',
'occ_wards': 'อัตราครองเตียง (แยก Wards)',
'refer_in': 'ยอดรวม Refer In',
'refer_out': 'ยอดรวม Refer Out',
'refer_back': 'ยอดรวม Refer Back',
'refer_in_trend': 'แนวโน้ม Refer In (Trend)',
'refer_out_trend': 'แนวโน้ม Refer Out (Trend)',
'refer_in_top': '10 อันดับโรค Refer In',
'refer_out_top': '10 อันดับโรค Refer Out',
'refer_back_dest_trend': 'จำนวนที่ส่งกลับ แยกปลายทาง (รายเดือน)',
'refer_back_top': '10 อันดับโรคที่ส่งกลับ',
'refer_back_cause': 'สาเหตุการส่งกลับ (Refer Back)',
'adjrw_in_low': 'AdjRW Refer In (Low)',
'adjrw_in_high': 'AdjRW Refer In (High)',
'adjrw_out_low': 'AdjRW Refer Out (Low)',
'adjrw_out_high': 'AdjRW Refer Out (High)'
},
hints: {
'occ_all': 'ระบบต้องการ 1 คอลัมน์ (เปอร์เซ็นต์อัตราครองเตียง)',
'occ_max': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อตึก), <b>occ</b> (เปอร์เซ็นต์)',
'occ_min': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อตึก), <b>occ</b> (เปอร์เซ็นต์)',
'occ_wards': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อตึก), <b>occ</b> (เปอร์เซ็นต์)',
'refer_in': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_out': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_back': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'refer_in_trend': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)',
'refer_out_trend': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)',
'refer_in_top': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>a</b> (จำนวนเคส)',
'refer_out_top': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>a</b> (จำนวนเคส)',
'refer_back_dest_trend': 'ระบบต้องการคอลัมน์ <b>dest</b> (ปลายทาง), <b>cnt</b> (จำนวนเคส)',
'refer_back_top': 'ระบบต้องการคอลัมน์ <b>name</b> (ชื่อโรค), <b>a</b> (จำนวนเคส)',
'refer_back_cause': 'ระบบต้องการคอลัมน์ <b>cause</b> (สาเหตุ), <b>cnt</b> (จำนวนเคส)',
'adjrw_in_low': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)',
'adjrw_in_high': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)',
'adjrw_out_low': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)',
'adjrw_out_high': 'ระบบต้องการ 2 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cnt</b> (จำนวนเคส)'
}
},
'stats': {
title: 'ข้อมูลสถิติ (Statistics)',
icon: 'fa-chart-bar',
color: 'text-indigo-500',
fields: {
'disease_opd': '10 อันดับโรค OPD',
'disease_ipd': '10 อันดับโรค IPD',
'disease_ipd_death': '10 อันดับโรค IPD เสียชีวิต',
'disease_er_death': '10 อันดับโรค ER เสียชีวิต',
'refer_overview': 'ภาพรวมโรค Refer',
'refer_ambulance': 'ส่งต่อด้วยรถพยาบาล',
'refer_ship': 'ส่งต่อทางเรือ',
'refer_air': 'ส่งต่อทางอากาศ',
'total_opd': 'ยอดรับบริการรวม OPD',
'total_ipd': 'ยอดรับบริการรวม IPD',
'total_er': 'ยอดรับบริการรวม ER',
'total_ipd_death': 'ยอดเสียชีวิต IPD',
'total_er_death': 'ยอดเสียชีวิต ER',
'total_doa': 'ยอด DOA',
'service_rights': 'สัดส่วนสิทธิการรักษา',
'thai_opd': 'สัญชาติไทย OPD',
'thai_ipd': 'สัญชาติไทย IPD',
'foreign_opd': 'ต่างชาติ OPD',
'foreign_ipd': 'ต่างชาติ IPD',
'alien_opd': 'ต่างด้าว OPD',
'alien_ipd': 'ต่างด้าว IPD',
'cmi_avg': 'ค่าเฉลี่ย CMI',
'adjrw_sum': 'ผลรวม AdjRW',
'trend_cmi': 'กราฟรายเดือน CMI / AdjRW'
},
hints: {
'disease_opd': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>val</b> (จำนวนเคส)',
'disease_ipd': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>val</b> (จำนวนเคส)',
'disease_ipd_death': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>val</b> (จำนวนเคส)',
'disease_er_death': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อโรค), <b>val</b> (จำนวนเคส)',
'refer_overview': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (ชื่อประเภท/หมวด), <b>val</b> (จำนวนเคส)',
'refer_ambulance': 'ระบบต้องการ 1 คอลัมน์ (จำนวนเคส)',
'refer_ship': 'ระบบต้องการ 1 คอลัมน์ (จำนวนเคส)',
'refer_air': 'ระบบต้องการ 1 คอลัมน์ (จำนวนเคส)',
'total_opd': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'total_ipd': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'total_er': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'total_ipd_death': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'total_er_death': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'total_doa': 'ระบบต้องการ 1 คอลัมน์ (ยอดรวม)',
'service_rights': 'ระบบต้องการ 2 คอลัมน์: <b>name</b> (สิทธิการรักษา), <b>val</b> (ยอดรวม)',
'thai_opd': 'ระบบต้องการ 1 คอลัมน์',
'thai_ipd': 'ระบบต้องการ 1 คอลัมน์',
'foreign_opd': 'ระบบต้องการ 1 คอลัมน์',
'foreign_ipd': 'ระบบต้องการ 1 คอลัมน์',
'alien_opd': 'ระบบต้องการ 1 คอลัมน์',
'alien_ipd': 'ระบบต้องการ 1 คอลัมน์',
'cmi_avg': 'ระบบต้องการ 1 คอลัมน์ (ค่าเฉลี่ย)',
'adjrw_sum': 'ระบบต้องการ 1 คอลัมน์ (ผลรวม)',
'trend_cmi': 'ระบบต้องการ 3 คอลัมน์: <b>ym</b> (YYYY-MM), <b>cmi</b> (ค่าเฉลี่ย), <b>adjrw</b> (ผลรวม)'
}
}
};
let currentConfigData = {};
function loadSqlConfig() {
document.getElementById('settings-loading').style.display = 'block';
document.getElementById('form-sql-settings').style.display = 'none';
fetch('?ajax_settings=1')
.then(res => res.json())
.then(res => {
if(res.status === 'success') {
currentConfigData = res.data || {};
renderSettingsForm(currentConfigData);
} else {
Swal.fire({
title: 'เกิดข้อผิดพลาด',
text: res.message || 'ไม่สามารถโหลดข้อมูลได้',
icon: 'error',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#3b82f6'
});
}
})
.catch(err => {
console.error(err);
Swal.fire({
title: 'เกิดข้อผิดพลาด',
text: 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้',
icon: 'error',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#3b82f6'
});
})
.finally(() => {
document.getElementById('settings-loading').style.display = 'none';
document.getElementById('form-sql-settings').style.display = 'block';
});
}
function renderSettingsForm(data) {
const form = document.getElementById('form-sql-settings');
form.innerHTML = '';
for (const [sectionKey, sectionInfo] of Object.entries(settingsSchema)) {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'border border-slate-200 rounded-xl overflow-hidden bg-white shadow-sm';
const header = document.createElement('div');
header.className = 'bg-slate-50 hover:bg-slate-100 transition-colors px-5 py-4 border-b border-slate-200 flex justify-between items-center cursor-pointer select-none';
header.onclick = function() {
const body = sectionDiv.querySelector('.section-body');
const isCurrentlyHidden = body.classList.contains('hidden');
// ยุบทุกหมวดก่อน (Collapse all)
document.querySelectorAll('#form-sql-settings .section-body').forEach(el => {
el.classList.add('hidden');
});
document.querySelectorAll('#form-sql-settings .toggle-icon').forEach(icon => {
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
});
// เปิดเฉพาะหมวดนี้ถ้าก่อนหน้านี้มันถูกปิดอยู่
if (isCurrentlyHidden) {
const icon = header.querySelector('.toggle-icon');
body.classList.remove('hidden');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
}
};
header.innerHTML = `
<h4 class="font-bold text-slate-700 text-lg flex items-center">
<i class="fa-solid ${sectionInfo.icon} ${sectionInfo.color} mr-3"></i>
${sectionInfo.title}
</h4>
<i class="fa-solid fa-chevron-down text-slate-400 toggle-icon transition-transform"></i>
`;
sectionDiv.appendChild(header);
const bodyDiv = document.createElement('div');
bodyDiv.className = 'section-body hidden p-5 space-y-6';
let index = 1;
for (const [fieldKey, fieldLabel] of Object.entries(sectionInfo.fields)) {
const val = (data[sectionKey] && data[sectionKey][fieldKey]) ? data[sectionKey][fieldKey] : '';
const hint = (sectionInfo.hints && sectionInfo.hints[fieldKey]) ? sectionInfo.hints[fieldKey] : null;
const hintHtml = hint ? `<div class="mt-2 text-[11.5px] text-amber-800 bg-amber-50/80 border border-amber-200/60 rounded p-2.5 mb-1"><i class="fa-solid fa-lightbulb text-amber-500 mr-1.5"></i> <strong>ตัวแปรที่จำเป็น (Alias):</strong> ${hint}</div>` : '';
const fieldDiv = document.createElement('div');
fieldDiv.className = 'flex flex-col border border-slate-100 p-4 rounded-lg bg-white shadow-sm';
fieldDiv.innerHTML = `
<label class="block text-sm font-bold text-slate-700 mb-2 flex justify-between items-center">
<div>
<span class="bg-blue-100 text-blue-700 w-5 h-5 inline-flex items-center justify-center rounded-full text-xs mr-1">${index}</span>
${fieldLabel}
<span class="text-xs text-slate-400 font-mono font-normal ml-2 bg-slate-100 px-2 py-0.5 rounded-full border border-slate-200">${sectionKey}.${fieldKey}</span>
</div>
<div class="flex gap-2">
<button type="button" onclick="toggleFullscreenSql(this)" class="text-xs bg-slate-50 hover:bg-slate-200 text-slate-600 font-bold py-1.5 px-3 rounded-md transition-colors flex items-center border border-slate-300" title="ขยายเต็มจอเพื่อแก้ไขสะดวกขึ้น">
<i class="fa-solid fa-expand mr-1.5 text-slate-500"></i> ขยาย
</button>
<button type="button" onclick="formatSql('${sectionKey}', '${fieldKey}')" class="text-xs bg-indigo-50 hover:bg-indigo-100 text-indigo-700 font-bold py-1.5 px-3 rounded-md transition-colors flex items-center border border-indigo-200" title="จัดรูปแบบคำสั่งให้สวยงามอ่านง่าย">
<i class="fa-solid fa-align-left mr-1.5 text-indigo-500"></i> จัดรูปแบบ
</button>
<button type="button" onclick="testSingleSql('${sectionKey}', '${fieldKey}')" class="text-xs bg-slate-100 hover:bg-slate-200 text-slate-600 font-bold py-1.5 px-3 rounded-md transition-colors flex items-center border border-slate-300">
<i class="fa-solid fa-play mr-1.5 text-blue-500"></i> ทดสอบ
</button>
<button type="button" onclick="saveSingleSql('${sectionKey}', '${fieldKey}')" class="text-xs bg-emerald-50 hover:bg-emerald-100 text-emerald-700 font-bold py-1.5 px-3 rounded-md transition-colors flex items-center border border-emerald-200">
<i class="fa-solid fa-check mr-1.5 text-emerald-500"></i> บันทึก
</button>
</div>
</label>
${hintHtml}
<textarea id="sql-${sectionKey}-${fieldKey}" class="w-full h-32 p-3 font-mono text-sm bg-slate-50 border border-slate-300 rounded-lg focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition-all shadow-inner resize-y" data-section="${sectionKey}" data-key="${fieldKey}" spellcheck="false" placeholder="SELECT ... FROM ...">${val}</textarea>
<div id="test-result-${sectionKey}-${fieldKey}" class="mt-3 hidden">
<div class="flex justify-between items-end mb-1">
<div class="text-xs font-bold text-slate-500 flex items-center"><i class="fa-solid fa-table mr-1"></i> ผลลัพธ์การทดสอบ (5 แถวแรก)</div>
<button type="button" id="copy-btn-${sectionKey}-${fieldKey}" class="text-[11px] text-blue-600 hover:text-blue-800 font-bold hidden transition-colors" onclick="copyCompiledSql(this)">
<i class="fa-solid fa-copy mr-1"></i> คัดลอกคำสั่งพร้อมตัวแปร
</button>
</div>
<div class="overflow-x-auto border border-slate-200 rounded-lg bg-white relative">
<div class="params-info bg-slate-50 border-b border-slate-200 px-3 py-2 empty:hidden"></div>
<table class="w-full text-left border-collapse text-xs whitespace-nowrap table-result">
<!-- Table content will go here -->
</table>
</div>
</div>
`;
bodyDiv.appendChild(fieldDiv);
index++;
}
sectionDiv.appendChild(bodyDiv);
form.appendChild(sectionDiv);
}
}
function toggleFullscreenSql(btn) {
const container = btn.closest('.flex-col');
const textarea = container.querySelector('textarea');
const isFullscreen = container.classList.contains('fixed');
if (isFullscreen) {
// Restore normal
container.classList.remove('fixed', 'inset-4', 'z-[100]', 'shadow-2xl', 'overflow-y-auto');
btn.innerHTML = '<i class="fa-solid fa-expand mr-1.5 text-slate-500"></i> ขยาย';
textarea.classList.remove('h-[calc(100vh-200px)]');
textarea.classList.add('h-32');
document.body.style.overflow = '';
} else {
// Fullscreen
container.classList.add('fixed', 'inset-4', 'z-[100]', 'shadow-2xl', 'overflow-y-auto');
btn.innerHTML = '<i class="fa-solid fa-compress mr-1.5 text-slate-500"></i> ย่อจอ';
textarea.classList.remove('h-32');
textarea.classList.add('h-[calc(100vh-200px)]');
document.body.style.overflow = 'hidden';
}
}
function formatSql(section, key) {
const textarea = document.getElementById(`sql-${section}-${key}`);
if (!textarea) return;
let sqlText = textarea.value;
if (!sqlText.trim()) return;
try {
if (typeof sqlFormatter !== 'undefined') {
// หลีกเลี่ยงปัญหา parser ไม่รู้จักตัวแปร :start ของ PDO
// โดยการแปลง :variable เป็นข้อความธรรมดาก่อน (เช่น __PDO_variable__)
let safeSql = sqlText.replace(/:([a-zA-Z0-9_]+)/g, '__PDO_$1__');
let formatted = sqlFormatter.format(safeSql, {
language: 'mysql',
tabWidth: 4,
keywordCase: 'upper',
linesBetweenQueries: 2
});
// แปลงกลับคืนมาเป็น :variable เหมือนเดิม
formatted = formatted.replace(/__PDO_([a-zA-Z0-9_]+)__/gi, ':$1');
textarea.value = formatted;
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 1500
});
Toast.fire({
icon: 'success',
title: 'จัดรูปแบบเรียบร้อย'
});
} else {
console.warn('sql-formatter library not loaded');
}
} catch (e) {
console.error(e);
Swal.fire({
title: 'ไม่สามารถจัดรูปแบบได้',
text: 'อาจมีไวยากรณ์คำสั่ง SQL ที่ไม่สมบูรณ์',
icon: 'warning',
confirmButtonText: 'ตกลง'
});
}
}
function testSingleSql(section, key) {
const textarea = document.getElementById(`sql-${section}-${key}`);
if (!textarea) return;
const sqlText = textarea.value;
if (!sqlText.trim()) {
Swal.fire({
title: 'ข้อมูลไม่ครบถ้วน',
text: 'กรุณาป้อนคำสั่ง SQL ก่อนกดทดสอบ',
icon: 'warning',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#3b82f6'
});
return;
}
const resultContainer = document.getElementById(`test-result-${section}-${key}`);
const tableBody = resultContainer.querySelector('table');
const paramsInfo = resultContainer.querySelector('.params-info');
const copyBtn = document.getElementById(`copy-btn-${section}-${key}`);
resultContainer.classList.remove('hidden');
paramsInfo.innerHTML = '';
if (copyBtn) copyBtn.classList.add('hidden');
tableBody.innerHTML = '<tr><td class="p-4 text-center text-slate-500"><i class="fa-solid fa-spinner fa-spin mr-2"></i> กำลังทดสอบคำสั่ง...</td></tr>';
fetch('?ajax_test_sql=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sql_text_b64: btoa(encodeURIComponent(sqlText)) })
})
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
if (copyBtn && res.sql_compiled) {
copyBtn.classList.remove('hidden');
copyBtn.setAttribute('data-sql', res.sql_compiled);
}
// Show params used
if (res.params_used && Object.keys(res.params_used).length > 0) {
let pList = Object.entries(res.params_used).map(([k,v]) => `<span class="inline-block bg-blue-100 text-blue-700 px-1.5 py-0.5 rounded font-mono text-[10px] border border-blue-200 mr-2">:${k} = '${v}'</span>`).join('');
paramsInfo.innerHTML = `<span class="text-[10px] font-bold text-slate-500 mr-2">ตัวแปรวันที่ที่ใช้:</span>${pList}`;
} else {
paramsInfo.innerHTML = '';
}
if (res.data.length === 0) {
tableBody.innerHTML = '<tr><td class="p-3 text-center text-slate-500">ทดสอบสำเร็จ แต่ไม่พบข้อมูลจากช่วงวันที่ปัจจุบัน</td></tr>';
return;
}
// Build Table
const columns = Object.keys(res.data[0]);
let thead = '<thead><tr class="bg-slate-100 text-slate-600 uppercase text-[10px]">';
columns.forEach(col => {
thead += `<th class="px-3 py-2 border-b border-slate-200 font-bold">${col}</th>`;
});
thead += '</tr></thead>';
let tbody = '<tbody>';
res.data.forEach(row => {
tbody += '<tr class="border-b border-slate-100 hover:bg-slate-50">';
columns.forEach(col => {
tbody += `<td class="px-3 py-2 text-slate-700">${row[col] !== null ? row[col] : '<em>null</em>'}</td>`;
});
tbody += '</tr>';
});
tbody += '</tbody>';
tableBody.innerHTML = thead + tbody;
} else {
tableBody.innerHTML = `<tr><td class="p-3 text-red-500 font-mono text-xs whitespace-normal break-words">${res.message}</td></tr>`;
}
})
.catch(err => {
console.error(err);
tableBody.innerHTML = '<tr><td class="p-3 text-red-500">เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์</td></tr>';
});
}
function saveSingleSql(section, key) {
const textarea = document.getElementById(`sql-${section}-${key}`);
if (!textarea) return;
const sqlText = textarea.value;
if (!sqlText.trim()) {
Swal.fire({
title: 'ข้อมูลไม่ครบถ้วน',
text: 'คำสั่ง SQL ห้ามเป็นค่าว่าง',
icon: 'warning',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#3b82f6'
});
return;
}
Swal.fire({
title: 'ยืนยันการบันทึก?',
html: `ต้องการบันทึกคำสั่ง <b class="text-blue-600">${section}.${key}</b> ใช่หรือไม่?`,
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#10b981',
cancelButtonColor: '#64748b',
confirmButtonText: '<i class="fa-solid fa-check mr-1"></i> บันทึก',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
fetch('?ajax_save_single_sql=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ section: section, key: key, sql_text_b64: btoa(encodeURIComponent(sqlText)) })
})
.then(res => res.json())
.then(res => {
if (res.status === 'success') {
// Toast notification for success
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2500,
timerProgressBar: true
});
Toast.fire({
icon: 'success',
title: 'บันทึกคำสั่งเรียบร้อยแล้ว'
});
} else {
Swal.fire({
title: 'บันทึกไม่สำเร็จ',
text: res.message,
icon: 'error',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#ef4444'
});
}
})
.catch(err => {
console.error(err);
Swal.fire({
title: 'เกิดข้อผิดพลาด',
text: 'ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้',
icon: 'error',
confirmButtonText: 'ตกลง',
confirmButtonColor: '#ef4444'
});
});
}
});
}
function copyCompiledSql(btn) {
const sql = btn.getAttribute('data-sql');
if (!sql) return;
navigator.clipboard.writeText(sql).then(() => {
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="fa-solid fa-check mr-1 text-emerald-600"></i> คัดลอกแล้ว';
setTimeout(() => {
btn.innerHTML = originalHTML;
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 3000
});
Toast.fire({
icon: 'error',
title: 'ไม่สามารถคัดลอกข้อความได้'
});
});
}
let isSettingsUnlocked = false;
let pendingSettingsTab = null;
function promptSettingsPassword() {
Swal.fire({
title: 'เข้าสู่หน้าตั้งค่า',
text: 'กรุณากรอกรหัสผ่านเพื่อเข้าถึงการตั้งค่า',
input: 'password',
inputPlaceholder: 'รหัสผ่าน',
inputAttributes: {
autocapitalize: 'off',
autocorrect: 'off'
},
showCancelButton: true,
confirmButtonText: '<i class="fa-solid fa-unlock mr-1"></i> ปลดล็อค',
cancelButtonText: 'ยกเลิก',
confirmButtonColor: '#3b82f6',
showLoaderOnConfirm: true,
allowOutsideClick: () => !Swal.isLoading(),
preConfirm: (password) => {
if (!password) {
Swal.showValidationMessage('กรุณากรอกรหัสผ่าน');
return false;
}
return fetch('?ajax_auth_settings=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password: password })
})
.then(response => {
if (!response.ok) throw new Error(response.statusText);
return response.json();
})
.then(data => {
if (data.status !== 'success') {
Swal.showValidationMessage(data.message || 'รหัสผ่านไม่ถูกต้อง');
return false;
}
return data;
})
.catch(error => {
Swal.showValidationMessage(`การเชื่อมต่อล้มเหลว: ${error}`);
});
}
}).then((result) => {
if (result.isConfirmed) {
isSettingsUnlocked = true;
Swal.fire({
icon: 'success',
title: 'ปลดล็อคสำเร็จ',
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 1500
});
if(pendingSettingsTab) {
window.switchTab('tab-settings', pendingSettingsTab.element, pendingSettingsTab.titleText);
pendingSettingsTab = null;
}
}
});
}
// Hook into tab switching to auto-load config when settings tab is opened
document.addEventListener('DOMContentLoaded', function() {
if (typeof window.switchTab === 'function') {
const originalSwitchTab = window.switchTab;
window.switchTab = function(tabId, element, titleText) {
if(tabId === 'tab-settings' && !isSettingsUnlocked) {
pendingSettingsTab = { element: element, titleText: titleText };
promptSettingsPassword();
return;
}
originalSwitchTab(tabId, element, titleText);
if(tabId === 'tab-settings') {
loadSqlConfig();
}
};
}
});
</script>
@@ -0,0 +1,578 @@
<?php
// views/tab_stats.php
require_once __DIR__ . '/../includes/db.php';
require_once __DIR__ . '/../includes/sql_config.php';
$show_sql_log_btn = 'N';
$isStatsAjax = isset($_GET['ajax_stats']) && $_GET['ajax_stats'] == '1';
// =========================================================================
// 1. ส่วนประมวลผล AJAX ดึงข้อมูลสถิติจากฐานข้อมูล
// =========================================================================
if ($isStatsAjax) {
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 = [
'disease_opd' => [],
'disease_ipd' => [],
'disease_ipd_death' => [],
'disease_er_death' => [],
'refer_overview' => [],
'refer_ambulance' => [],
'refer_ship' => [],
'refer_air' => [],
'service_totals' => ['opd' => 0, 'ipd' => 0, 'er' => 0, 'ipd_death' => 0, 'er_death' => 0, 'doa' => 0],
'service_rights' => ['labels' => [], 'data' => []],
'nationality' => ['thai' => ['opd' => 0, 'ipd' => 0], 'foreign' => ['opd' => 0, 'ipd' => 0], 'alien' => ['opd' => 0, 'ipd' => 0]],
'cmi_avg' => 0,
'adjrw_sum' => 0,
'trend_cmi' => ['labels' => [], 'data' => []],
'trend_adjrw' => ['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('ระบบตรวจสอบช่วงวันที่ Stats', "ค้นหาข้อมูลตั้งแต่วันที่: $start_date ถึง $end_date");
$to_utf8 = function ($str) {
if (empty($str)) return 'ไม่ระบุ';
if (!mb_check_encoding($str, 'UTF-8')) return @iconv('TIS-620', 'UTF-8//IGNORE', $str);
return $str;
};
$thai_months = ['01' => 'ม.ค.', '02' => 'ก.พ.', '03' => 'มี.ค.', '04' => 'เม.ย.', '05' => 'พ.ค.', '06' => 'มิ.ย.', '07' => 'ก.ค.', '08' => 'ส.ค.', '09' => 'ก.ย.', '10' => 'ต.ค.', '11' => 'พ.ย.', '12' => 'ธ.ค.'];
if ($pdo) {
// --- 1. สถิติโรค (10 อันดับโรค) ---
$queries_disease = [
'disease_opd' => [$sql_config['stats']['disease_opd'] ?? "SELECT v.icd10, i.name, count(v.vn) as a FROM ovstdiag v LEFT JOIN icd101 i ON v.icd10=i.code WHERE v.vstdate BETWEEN :start AND :end AND v.diagtype='1' GROUP BY v.icd10, i.name ORDER BY a DESC LIMIT 10", "10 อันดับโรคผู้ป่วยนอก (OPD)"],
'disease_ipd' => [$sql_config['stats']['disease_ipd'] ?? "SELECT v.icd10, i.name, count(v.an) as a FROM iptdiag v LEFT JOIN ipt ip ON v.an=ip.an LEFT JOIN icd101 i ON v.icd10=i.code WHERE ip.dchdate BETWEEN :start AND :end AND v.diagtype='1' GROUP BY v.icd10, i.name ORDER BY a DESC LIMIT 10", "10 อันดับโรคผู้ป่วยใน (IPD)"],
'disease_ipd_death' => [$sql_config['stats']['disease_ipd_death'] ?? "SELECT v.icd10, i.name, count(v.an) as a FROM iptdiag v LEFT JOIN ipt ip ON v.an=ip.an LEFT JOIN icd101 i ON v.icd10=i.code WHERE ip.dchdate BETWEEN :start AND :end AND v.diagtype='1' AND ip.dchtype IN ('08','09') GROUP BY v.icd10, i.name ORDER BY a DESC LIMIT 10", "10 อันดับเสียชีวิต IPD"],
'disease_er_death' => [$sql_config['stats']['disease_er_death'] ?? "SELECT v.icd10, i.name, count(v.vn) as a FROM ovstdiag v LEFT JOIN icd101 i ON v.icd10=i.code WHERE v.vstdate BETWEEN :start AND :end AND v.diagtype='1' AND v.vn IN(SELECT vn FROM er_regist WHERE er_dch_type='4') GROUP BY v.icd10, i.name ORDER BY a DESC LIMIT 10", "10 อันดับเสียชีวิตที่ ER"]
];
foreach ($queries_disease as $key => $q) {
try {
$stmt = $pdo->prepare($q[0]);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response[$key][] = ['name' => $to_utf8($r['name']) . " ({$r['icd10']})", 'val' => (int)$r['a']];
}
$addLog($q[1], $q[0]);
} catch (\Exception $e) {
$addLog($q[1], $q[0], $e->getMessage());
}
}
// --- 2. 10 อันดับโรคส่งต่อ (Refer) ---
$queries_refer = [
'refer_overview' => [$sql_config['stats']['refer_overview'] ?? "SELECT a.pdx, ic.name as icd10, count(a.pdx) as pdxcount FROM referout a LEFT JOIN icd101 ic ON a.pdx=ic.code WHERE a.refer_date BETWEEN :start AND :end AND ic.name IS NOT NULL AND a.pdx !='U000' GROUP BY a.pdx ORDER BY pdxcount DESC LIMIT 10", "ภาพรวมโรคส่งต่อ (Refer Out)"],
'refer_ambulance' => [$sql_config['stats']['refer_ambulance'] ?? "SELECT a.pdx, ic.name as icd10, count(a.pdx) as pdxcount FROM referout a LEFT JOIN icd101 ic ON a.pdx=ic.code WHERE a.refer_date BETWEEN :start AND :end AND ic.name IS NOT NULL AND a.with_ambulance='Y' AND a.pdx !='U000' GROUP BY a.pdx ORDER BY pdxcount DESC LIMIT 10", "10 อันดับโรคส่งต่อด้วยรถพยาบาล"],
'refer_ship' => [$sql_config['stats']['refer_ship'] ?? "SELECT a.pdx, ic.name as icd10, count(a.pdx) as pdxcount FROM referout a LEFT JOIN opitemrece op ON a.vn=op.vn LEFT JOIN icd101 ic ON a.pdx=ic.code WHERE a.refer_date BETWEEN :start AND :end AND ic.name IS NOT NULL AND a.pdx !='U000' AND op.icode IN ('3162878','3162879','3162880','3162881','3162882','3162883','3162884','3162885') GROUP BY a.pdx ORDER BY pdxcount DESC LIMIT 10", "10 อันดับโรคส่งต่อทางเรือ"],
'refer_air' => [$sql_config['stats']['refer_air'] ?? "SELECT a.pdx, ic.name as icd10, count(a.pdx) as pdxcount FROM referout a LEFT JOIN opitemrece op ON a.vn=op.vn LEFT JOIN icd101 ic ON a.pdx=ic.code WHERE a.refer_date BETWEEN :start AND :end AND ic.name IS NOT NULL AND a.pdx !='U000' AND op.icode IN ('3162862','3162863','3162864','3162865','3162866','3162867','3162868','3162869','3162870','3162871','3162872','3162873','3162874','3162875','3162876','3162877','3163699','3163891') GROUP BY a.pdx ORDER BY pdxcount DESC LIMIT 10", "10 อันดับโรคส่งต่อทางอากาศ"]
];
foreach ($queries_refer as $key => $q) {
try {
$stmt = $pdo->prepare($q[0]);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response[$key][] = ['name' => $to_utf8($r['icd10']), 'val' => (int)$r['pdxcount']];
}
$addLog($q[1], $q[0]);
} catch (\Exception $e) {
$addLog($q[1], $q[0], $e->getMessage());
}
}
// --- 3. สถิติรับบริการรวม (Totals) ---
$queries_totals = [
'opd' => [$sql_config['stats']['total_opd'] ?? "SELECT count(distinct vn) FROM ovst WHERE vstdate BETWEEN :start AND :end AND an IS NULL", "ยอดรวมผู้ป่วยนอก (OPD)"],
'ipd' => [$sql_config['stats']['total_ipd'] ?? "SELECT count(distinct an) FROM ipt WHERE dchdate BETWEEN :start AND :end", "ยอดรวมผู้ป่วยใน (IPD)"],
'er' => [$sql_config['stats']['total_er'] ?? "SELECT count(distinct vn) FROM ovst WHERE vstdate BETWEEN :start AND :end AND vn IN(SELECT vn FROM er_regist)", "ยอดรวมอุบัติเหตุ (ER)"],
'ipd_death' => [$sql_config['stats']['total_ipd_death'] ?? "SELECT count(distinct an) FROM ipt WHERE dchdate BETWEEN :start AND :end AND dchtype IN('08','09')", "จำนวนการเสียชีวิตผู้ป่วยใน"],
'er_death' => [$sql_config['stats']['total_er_death'] ?? "SELECT count(distinct vn) FROM ovst WHERE vstdate BETWEEN :start AND :end AND vn IN(SELECT vn FROM er_regist WHERE er_dch_type='4')", "จำนวนการเสียชีวิตที่ ER"],
'doa' => [$sql_config['stats']['total_doa'] ?? "SELECT count(distinct vn) FROM ovst WHERE vstdate BETWEEN :start AND :end AND vn IN(SELECT vn FROM er_regist WHERE dba='Y')", "เสียชีวิตก่อนถึง รพ. (DOA/DBA)"]
];
foreach ($queries_totals as $key => $q) {
try {
$stmt = $pdo->prepare($q[0]);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['service_totals'][$key] = (int)$stmt->fetchColumn();
$addLog($q[1], $q[0]);
} catch (\Exception $e) {
$addLog($q[1], $q[0], $e->getMessage());
}
}
// --- 4. สัดส่วนสิทธิการรักษา ---
$sql_rights = $sql_config['stats']['service_rights'] ?? "SELECT count(distinct a.vn) as a, c.pttype_spp_name FROM ovst a LEFT JOIN pttype b ON a.pttype=b.pttype LEFT JOIN pttype_spp c ON b.pttype_spp_id=c.pttype_spp_id WHERE a.vstdate BETWEEN :start AND :end GROUP BY b.pttype_spp_id";
try {
$stmt = $pdo->prepare($sql_rights);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$response['service_rights']['labels'][] = $to_utf8($r['pttype_spp_name']);
$response['service_rights']['data'][] = (int)$r['a'];
}
$addLog('สัดส่วนสิทธิการรักษา', $sql_rights);
} catch (\Exception $e) {
$addLog('สัดส่วนสิทธิการรักษา', $sql_rights, $e->getMessage());
}
// --- 5. สัญชาติ ---
$queries_nat = [
'thai_opd' => [$sql_config['stats']['thai_opd'] ?? "SELECT count(distinct a.vn) as a ,AEC_SAMUI FROM ovst a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.vstdate BETWEEN :start AND :end AND AEC_SAMUI IN('N','P','T') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "สัญชาติไทย OPD"],
'thai_ipd' => [$sql_config['stats']['thai_ipd'] ?? "SELECT count(distinct a.an) as a ,AEC_SAMUI FROM ipt a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.dchdate BETWEEN :start AND :end AND AEC_SAMUI IN('N','P','T') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "สัญชาติไทย IPD"],
'foreign_opd' => [$sql_config['stats']['foreign_opd'] ?? "SELECT count(distinct a.vn) as a ,AEC_SAMUI FROM ovst a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.vstdate BETWEEN :start AND :end AND AEC_SAMUI IN('F') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "ต่างชาติ OPD"],
'foreign_ipd' => [$sql_config['stats']['foreign_ipd'] ?? "SELECT count(distinct a.an) as a ,AEC_SAMUI FROM ipt a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.dchdate BETWEEN :start AND :end AND AEC_SAMUI IN('F') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "ต่างชาติ IPD"],
'alien_opd' => [$sql_config['stats']['alien_opd'] ?? "SELECT count(distinct a.vn) as a ,AEC_SAMUI FROM ovst a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.vstdate BETWEEN :start AND :end AND AEC_SAMUI IN('ํY','Y') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "ต่างด้าว OPD"],
'alien_ipd' => [$sql_config['stats']['alien_ipd'] ?? "SELECT count(distinct a.an) as a ,AEC_SAMUI FROM ipt a LEFT OUTER JOIN patient p ON a.hn=p.hn LEFT OUTER JOIN nationality n ON p.nationality=n.nationality WHERE a.dchdate BETWEEN :start AND :end AND AEC_SAMUI IN('Y','ํY') AND p.nationality IS NOT NULL AND AEC_SAMUI IS NOT NULL GROUP BY AEC_SAMUI", "ต่างด้าว IPD"]
];
foreach ($queries_nat as $key => $q) {
try {
$stmt = $pdo->prepare($q[0]);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$sum = 0;
while ($r = $stmt->fetch()) {
$sum += (int)$r['a'];
}
$parts = explode('_', $key);
$response['nationality'][$parts[0]][$parts[1]] = $sum;
$addLog($q[1], $q[0]);
} catch (\Exception $e) {
$addLog($q[1], $q[0], $e->getMessage());
}
}
// --- 6. CMI และ AdjRW (ภาพรวม + กราฟรายเดือน) ---
try {
$sql_cmi_avg = $sql_config['stats']['cmi_avg'] ?? "SELECT IFNULL((SUM(adjrw)/count(an)),0) FROM ipt WHERE dchdate BETWEEN :start AND :end";
$stmt = $pdo->prepare($sql_cmi_avg);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['cmi_avg'] = (float)$stmt->fetchColumn();
$sql_adjrw_sum = $sql_config['stats']['adjrw_sum'] ?? "SELECT IFNULL(SUM(adjrw),0) FROM ipt WHERE dchdate BETWEEN :start AND :end";
$stmt = $pdo->prepare($sql_adjrw_sum);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
$response['adjrw_sum'] = (float)$stmt->fetchColumn();
$addLog('ค่า CMI และ Sum AdjRW ภาพรวม', $sql_cmi_avg . " | " . $sql_adjrw_sum);
// กราฟรายเดือน
$sql_trend_cmi = $sql_config['stats']['trend_cmi'] ?? "SELECT DATE_FORMAT(dchdate, '%Y-%m') as ym, IFNULL((SUM(adjrw)/count(an)),0) as cmi, IFNULL(SUM(adjrw),0) as adjrw FROM ipt WHERE dchdate BETWEEN :start AND :end GROUP BY ym ORDER BY ym";
$stmt = $pdo->prepare($sql_trend_cmi);
$stmt->execute(['start' => $start_date, 'end' => $end_date]);
while ($r = $stmt->fetch()) {
$parts = explode('-', $r['ym']);
if (count($parts) == 2) {
$lb = $thai_months[$parts[1]] . ' ' . substr((int)$parts[0] + 543, 2, 2);
$response['trend_cmi']['labels'][] = $lb;
$response['trend_cmi']['data'][] = (float)$r['cmi'];
$response['trend_adjrw']['labels'][] = $lb;
$response['trend_adjrw']['data'][] = (float)$r['adjrw'];
}
}
$addLog('กราฟ CMI / AdjRW รายเดือน', "SELECT DATE_FORMAT(dchdate), SUM(adjrw)...");
} catch (\Exception $e) {
$addLog('CMI / AdjRW', '-', $e->getMessage());
}
}
echo "---AJAX_STATS_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_STATS_END---";
exit;
}
// -------------------------------------------------------------------------
// ฟอร์มการกรอง
// -------------------------------------------------------------------------
$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');
$stats_filter_html = '
<form class="stats-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-indigo-500 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-indigo-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-indigo-400 transition-colors" onchange="toggleStatsFilterType(this)">
<option value="fiscal" selected>ปีงบประมาณ</option>
<option value="date">ช่วงวันที่กำหนดเอง</option>
</select>
</div>
<div class="w-full md:w-1/4 stats_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-indigo-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-indigo-400 transition-colors">' . $options_html . '</select>
</div>
<div class="w-full md:w-1/2 flex gap-3 stats_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" 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" value="' . $end_d . '"></div>
</div>
<button type="submit" class="w-full md:w-[15%] bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-2.5 px-4 rounded-lg transition-all shadow-md shadow-indigo-500/20 h-[42px] flex items-center justify-center">
<i class="fa-solid fa-magnifying-glass mr-2"></i> ค้นหา
</button>
</form>
';
?>
<script>
function toggleStatsFilterType(selectElement) {
const form = selectElement.closest('form');
const type = selectElement.value;
if (type === 'fiscal') {
form.querySelector('.stats_fiscal_wrapper').style.display = 'block';
form.querySelector('.stats_date_wrapper').style.display = 'none';
} else {
form.querySelector('.stats_fiscal_wrapper').style.display = 'none';
form.querySelector('.stats_date_wrapper').style.display = 'flex';
}
}
</script>
<?php if ($show_sql_log_btn === 'Y'): ?>
<button onclick="toggleStatsSqlLogModal()" class="fixed bottom-6 left-[30%] 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 (สถิติ)</button>
<div id="stats-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-indigo-400 mr-2"></i> บันทึกการคิวรีฐานข้อมูล ข้อมูลสถิติ (SQL Logs)</h3>
<button onclick="toggleStatsSqlLogModal()" 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="stats-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 toggleStatsSqlLogModal() {
const modal = document.getElementById('stats-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; ?>
<!-- INCLUDE FILES -->
<?php include __DIR__ . '/tab_stats_1_disease.php'; ?>
<?php include __DIR__ . '/tab_stats_2_service.php'; ?>
<!-- JAVASCRIPT -->
<script>
const safeSetTextStats = (id, text) => {
const el = document.getElementById(id);
if (el) el.innerText = text;
};
async function fetchStatsRealData(formElement = null) {
let params;
if (formElement) {
params = new URLSearchParams(new FormData(formElement));
} else {
const activeForm = document.querySelector('.tab-content:not(.hidden) .stats-filter-form');
if (activeForm) params = new URLSearchParams(new FormData(activeForm));
else params = new URLSearchParams();
}
params.append('ajax_stats', '1');
var overlay = document.getElementById('data-processing-overlay');
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_STATS_START---')) {
data = JSON.parse(text.split('---AJAX_STATS_START---')[1].split('---AJAX_STATS_END---')[0]);
} else {
throw new Error("รูปแบบข้อมูลผิดพลาด หรือถูกระบบอื่นดักจับการทำงาน");
}
if (!data) throw new Error("Invalid Data Format");
// --- SQL Logs ---
if (data.sql_logs) {
let hasError = false;
let errorDetails = [];
const logContent = document.getElementById('stats-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 (สถิติ):\n- " + errorDetails.join("\n- "));
}
// Helper for Lists
const updateList = (id, arr) => {
const el = document.getElementById(id);
if (el) {
if (!arr || arr.length === 0) el.innerHTML = '<li class="text-center text-slate-500 py-4 font-medium"><i class="fa-solid fa-box-open mr-2 text-slate-300"></i>ไม่พบข้อมูล</li>';
else el.innerHTML = arr.map((d, i) => `<li class="flex justify-between border-b border-slate-100 pb-1.5 mb-1.5 hover:bg-slate-50 transition-colors px-1"><span class="truncate pr-2 font-medium text-slate-700">${i+1}. ${d.name}</span><span class="font-extrabold text-blue-600">${d.val.toLocaleString()}</span></li>`).join('');
}
};
// --- 1. สถิติโรค ---
updateList('list-disease-opd-top', data.disease_opd);
updateList('list-disease-ipd-top', data.disease_ipd);
updateList('list-disease-ipd-death', data.disease_ipd_death);
updateList('list-disease-er-death', data.disease_er_death);
// --- 2. Refer ---
updateList('list-disease-refer-overview', data.refer_overview);
updateList('list-disease-refer-ambulance', data.refer_ambulance);
updateList('list-disease-refer-ship', data.refer_ship);
updateList('list-disease-refer-air', data.refer_air);
// --- 3. ยอดบริการรวม ---
safeSetTextStats('stats-opd-total', data.service_totals.opd.toLocaleString());
safeSetTextStats('stats-ipd-total', data.service_totals.ipd.toLocaleString());
safeSetTextStats('stats-er-total', data.service_totals.er.toLocaleString());
safeSetTextStats('stats-ipd-death', data.service_totals.ipd_death.toLocaleString());
safeSetTextStats('stats-er-death', data.service_totals.er_death.toLocaleString());
safeSetTextStats('stats-doa', data.service_totals.doa.toLocaleString());
let ipdTotal = data.service_totals.ipd || 1;
safeSetTextStats('stats-ipd-death-pct', ((data.service_totals.ipd_death / ipdTotal) * 100).toFixed(2));
// --- 4. Service Type & Rights Charts ---
const updatePieChart = (id, labels, values, colors) => {
const el = document.getElementById(id);
if (el) {
const ctx = el.getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
if (values.reduce((a, b) => a + b, 0) > 0) {
window.appCharts[id] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: labels,
datasets: [{
data: values,
backgroundColor: colors,
borderWidth: 2,
borderColor: '#ffffff'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '55%',
plugins: {
legend: {
display: false
},
datalabels: {
color: '#ffffff',
font: {
weight: 'bold',
size: 12
},
formatter: (v, ctx) => {
let s = 0;
ctx.chart.data.datasets[0].data.map(d => s += d);
return (v * 100 / s).toFixed(1) + '%';
}
}
}
}
});
} else {
window.appCharts[id] = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['ไม่มีข้อมูล'],
datasets: [{
data: [1],
backgroundColor: ['#e2e8f0']
}]
},
options: {
plugins: {
datalabels: {
display: false
}
}
}
});
}
}
}
updatePieChart('serviceTypeChart', ['OPD', 'IPD', 'ER'], [data.service_totals.opd, data.service_totals.ipd, data.service_totals.er], ['#3b82f6', '#14b8a6', '#f43f5e']);
updatePieChart('serviceRightChart', data.service_rights.labels, data.service_rights.data, ['#ec4899', '#a855f7', '#f59e0b', '#94a3b8', '#10b981', '#6366f1']);
// --- 5. สัญชาติ ---
const nat = data.nationality;
safeSetTextStats('stats-nat-thai-total', (nat.thai.opd + nat.thai.ipd).toLocaleString());
safeSetTextStats('stats-nat-thai-opd', nat.thai.opd.toLocaleString());
safeSetTextStats('stats-nat-thai-ipd', nat.thai.ipd.toLocaleString());
safeSetTextStats('stats-nat-foreign-total', (nat.foreign.opd + nat.foreign.ipd).toLocaleString());
safeSetTextStats('stats-nat-foreign-opd', nat.foreign.opd.toLocaleString());
safeSetTextStats('stats-nat-foreign-ipd', nat.foreign.ipd.toLocaleString());
safeSetTextStats('stats-nat-alien-total', (nat.alien.opd + nat.alien.ipd).toLocaleString());
safeSetTextStats('stats-nat-alien-opd', nat.alien.opd.toLocaleString());
safeSetTextStats('stats-nat-alien-ipd', nat.alien.ipd.toLocaleString());
// --- 6. CMI & AdjRW ---
safeSetTextStats('stats-cmi-avg', data.cmi_avg.toFixed(2));
safeSetTextStats('stats-adjrw-sum', data.adjrw_sum.toLocaleString());
const updateLineChart = (id, labelText, dataObj, color, isFloat) => {
const el = document.getElementById(id);
if (el && dataObj.labels.length > 0) {
const ctx = el.getContext('2d');
if (window.appCharts[id]) window.appCharts[id].destroy();
let grad = ctx.createLinearGradient(0, 0, 0, 300);
grad.addColorStop(0, color.replace('1)', '0.4)'));
grad.addColorStop(1, color.replace('1)', '0)'));
window.appCharts[id] = new Chart(ctx, {
type: 'line',
data: {
labels: dataObj.labels,
datasets: [{
label: labelText,
data: dataObj.data,
borderColor: color.replace('0.4)', '1)'),
backgroundColor: grad,
borderWidth: 3,
pointBackgroundColor: '#ffffff',
pointBorderColor: color.replace('0.4)', '1)'),
pointBorderWidth: 2,
pointRadius: 4,
fill: true,
tension: 0.4,
datalabels: {
align: 'top',
anchor: 'end',
offset: 6,
color: color.replace('0.4)', '1)'),
font: {
weight: 'bold'
},
formatter: v => isFloat ? Number(v).toFixed(2) : Number(v).toLocaleString()
}
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
}
},
y: {
display: false,
min: Math.min(...dataObj.data) * 0.8,
suggestedMax: Math.max(...dataObj.data) * 1.2
}
},
layout: {
padding: {
top: 25
}
}
}
});
}
};
updateLineChart('cmiChartTab', 'ค่า CMI', data.trend_cmi, 'rgba(14, 165, 233, 1)', true);
updateLineChart('adjrwChartTab', 'Sum AdjRW', data.trend_adjrw, 'rgba(59, 130, 246, 1)', false);
} catch (err) {
console.error("Fetch Data Error:", err);
alert("ไม่สามารถดึงข้อมูลได้ กรุณาตรวจสอบการเชื่อมต่อฐานข้อมูล หรือคำสั่ง SQL\nรายละเอียด: " + err.message);
} finally {
if (overlay) {
setTimeout(() => {
overlay.classList.remove('flex');
overlay.classList.add('hidden');
}, 500);
}
}
}
document.addEventListener('DOMContentLoaded', () => {
if (!window.appCharts) window.appCharts = {};
document.querySelectorAll('.stats-filter-form').forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
fetchStatsRealData(e.target);
});
});
});
</script>
@@ -0,0 +1,90 @@
<!-- TAB: สถิติโรค -->
<div id="tab-disease" class="tab-content hidden space-y-4 md:space-y-6">
<?= $stats_filter_html ?>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 md:gap-6">
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-blue-500 to-blue-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-stethoscope mr-2"></i> 10 อันดับโรคผู้ป่วยนอก (OPD)</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-opd-top">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-emerald-500 to-emerald-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-bed-pulse mr-2"></i> 10 อันดับโรคผู้ป่วยใน (IPD)</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-ipd-top">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-purple-500 to-purple-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-bed-procedures mr-2"></i> 10 อันดับเสียชีวิต IPD</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-ipd-death">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-red-500 to-red-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-heart-crack mr-2"></i> 10 อันดับเสียชีวิตที่ ER</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-er-death">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
</div>
<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-indigo-500 pl-3">10 อันดับโรคการส่งต่อ (Refer)</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-4 gap-4 md:gap-6">
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-slate-500 to-slate-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-hospital mr-2"></i> ภาพรวมส่งต่อ</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-refer-overview">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-orange-500 to-orange-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-ambulance mr-2"></i> ส่งต่อรถพยาบาล</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-refer-ambulance">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-cyan-500 to-cyan-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-ship mr-2"></i> ส่งต่อทางเรือ</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-refer-ship">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
<div class="card-premium flex flex-col h-full">
<div class="bg-gradient-to-r from-indigo-500 to-indigo-600 p-3.5 md:p-4">
<h3 class="font-bold text-white text-xs md:text-sm flex items-center"><i class="fa-solid fa-helicopter mr-2"></i> ส่งต่อทางอากาศ</h3>
</div>
<div class="overflow-y-auto h-64 custom-scrollbar">
<ul class="p-4 space-y-2.5 text-[11px] md:text-xs bg-white" id="list-disease-refer-air">
<li class="text-center text-slate-500 py-4 font-medium">รอการค้นหาข้อมูล...</li>
</ul>
</div>
</div>
</div>
</div>
@@ -0,0 +1,134 @@
<!-- TAB: สถิติรับบริการรวม -->
<div id="tab-service" class="tab-content hidden space-y-4 md:space-y-6">
<?= $stats_filter_html ?>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 md:gap-5 mb-4 md:mb-6">
<div class="card-premium p-4 md:p-5 flex items-center bg-blue-50/50 border-l-4 border-l-blue-500 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-blue-500 to-blue-400 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-users"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">ยอดรวมผู้ป่วยนอก (OPD)</p>
<h3 class="text-xl md:text-3xl font-extrabold text-blue-600"><span id="stats-opd-total">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
</div>
</div>
<div class="card-premium p-4 md:p-5 flex items-center bg-teal-50/50 border-l-4 border-l-teal-500 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-teal-500 to-emerald-400 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-bed-pulse"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">ยอดรวมผู้ป่วยใน (IPD)</p>
<h3 class="text-xl md:text-3xl font-extrabold text-teal-600"><span id="stats-ipd-total">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
</div>
</div>
<div class="card-premium p-4 md:p-5 flex items-center bg-rose-50/50 border-l-4 border-l-rose-500 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-red-500 to-rose-400 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-truck-medical"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">ยอดรวมอุบัติเหตุ (ER)</p>
<h3 class="text-xl md:text-3xl font-extrabold text-rose-600"><span id="stats-er-total">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
</div>
</div>
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 md:gap-5 mb-4 md:mb-6 w-full">
<div class="card-premium p-4 md:p-5 flex items-center bg-purple-50/50 border-l-4 border-l-purple-500 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-400 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-bed-procedures"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">จำนวนการเสียชีวิตผู้ป่วยใน</p>
<h3 class="text-xl md:text-3xl font-extrabold text-purple-600"><span id="stats-ipd-death">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
<p class="text-[9px] md:text-xs text-slate-400 mt-0.5">อัตราตาย <span id="stats-ipd-death-pct">-</span>%</p>
</div>
</div>
<div class="card-premium p-4 md:p-5 flex items-center bg-slate-50/50 border-l-4 border-l-slate-700 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-slate-700 to-slate-500 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-heart-crack"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">จำนวนการเสียชีวิตที่ ER</p>
<h3 class="text-xl md:text-3xl font-extrabold text-slate-700"><span id="stats-er-death">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
</div>
</div>
<div class="card-premium p-4 md:p-5 flex items-center bg-slate-100 border-l-4 border-l-slate-800 shadow-sm">
<div class="w-12 h-12 md:w-14 md:h-14 rounded-xl bg-gradient-to-br from-slate-800 to-slate-600 text-white flex items-center justify-center text-xl md:text-2xl shadow-lg mr-3"><i class="fa-solid fa-truck-fast"></i></div>
<div>
<p class="text-[10px] md:text-xs text-slate-500 font-bold mb-0.5">เสียชีวิตก่อนถึง รพ. (DOA/DBA)</p>
<h3 class="text-xl md:text-3xl font-extrabold text-slate-800"><span id="stats-doa">-</span> <span class="text-[10px] md:text-sm font-medium text-slate-400">ราย</span></h3>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
<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-2 text-center">สัดส่วนผู้รับบริการแบ่งตามประเภท</h3>
<div class="relative flex-1 w-full flex justify-center mt-2">
<div class="h-48 md:h-64 w-full"><canvas id="serviceTypeChart"></canvas></div>
</div>
</div>
<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-2 text-center">สัดส่วนสิทธิการรักษา</h3>
<div class="relative flex-1 w-full flex justify-center mt-2">
<div class="h-48 md:h-64 w-full"><canvas id="serviceRightChart"></canvas></div>
</div>
</div>
</div>
<!-- สัญชาติ -->
<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-indigo-500 pl-3">จำนวนผู้รับบริการจำแนกตามสัญชาติ</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 md:gap-6">
<div class="card-premium p-4 md:p-5 border-t-4 border-t-blue-500 bg-gradient-to-b from-blue-50/50 to-white rounded-lg shadow-sm relative overflow-hidden">
<i class="fa-solid fa-earth-asia absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-blue-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-center mb-1 relative z-10">
<span class="font-bold text-slate-700 text-xs md:text-sm">สัญชาติไทย</span>
<span class="text-[10px] md:text-xs text-slate-500 font-bold">รวม <span id="stats-nat-thai-total">-</span> ราย</span>
</div>
<div class="grid grid-cols-2 gap-2 md:gap-4 mt-3 md:mt-4 border-t border-blue-100 pt-2 md:pt-3 relative z-10">
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">OPD</span>
<h4 class="font-black text-lg md:text-xl text-blue-600" id="stats-nat-thai-opd">-</h4>
</div>
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">IPD</span>
<h4 class="font-black text-lg md:text-xl text-teal-600" id="stats-nat-thai-ipd">-</h4>
</div>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-emerald-500 bg-gradient-to-b from-emerald-50/50 to-white rounded-lg shadow-sm relative overflow-hidden">
<i class="fa-solid fa-plane-arrival absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-emerald-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-center mb-1 relative z-10">
<span class="font-bold text-slate-700 text-xs md:text-sm truncate w-2/3">ต่างชาติ</span>
<span class="text-[10px] md:text-xs text-slate-500 font-bold">รวม <span id="stats-nat-foreign-total">-</span> ราย</span>
</div>
<div class="grid grid-cols-2 gap-2 md:gap-4 mt-3 md:mt-4 border-t border-emerald-100 pt-2 md:pt-3 relative z-10">
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">OPD</span>
<h4 class="font-black text-lg md:text-xl text-emerald-600" id="stats-nat-foreign-opd">-</h4>
</div>
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">IPD</span>
<h4 class="font-black text-lg md:text-xl text-teal-600" id="stats-nat-foreign-ipd">-</h4>
</div>
</div>
</div>
<div class="card-premium p-4 md:p-5 border-t-4 border-t-amber-500 bg-gradient-to-b from-amber-50/50 to-white rounded-lg shadow-sm relative overflow-hidden">
<i class="fa-solid fa-person-walking-luggage absolute -right-2 -bottom-2 md:-right-4 md:-bottom-4 text-5xl md:text-7xl text-amber-100/50 rotate-12 z-0"></i>
<div class="flex justify-between items-center mb-1 relative z-10">
<span class="font-bold text-slate-700 text-xs md:text-sm truncate w-2/3">ต่างด้าว</span>
<span class="text-[10px] md:text-xs text-slate-500 font-bold">รวม <span id="stats-nat-alien-total">-</span> ราย</span>
</div>
<div class="grid grid-cols-2 gap-2 md:gap-4 mt-3 md:mt-4 border-t border-amber-100 pt-2 md:pt-3 relative z-10">
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">OPD</span>
<h4 class="font-black text-lg md:text-xl text-amber-600" id="stats-nat-alien-opd">-</h4>
</div>
<div><span class="text-[10px] md:text-xs font-bold text-slate-500">IPD</span>
<h4 class="font-black text-lg md:text-xl text-teal-600" id="stats-nat-alien-ipd">-</h4>
</div>
</div>
</div>
</div>
<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-indigo-500 pl-3">ประสิทธิภาพการบริการ</h3>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-6">
<div class="card-premium p-4 md:p-6">
<div class="flex justify-between mb-4 relative">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base"><i class="fa-solid fa-chart-line text-blue-500 mr-2"></i> ค่า CMI รายเดือน</h3><span class="cmi-avg-display absolute left-1/2 -translate-x-1/2 bg-gradient-to-br from-blue-400 to-indigo-600 text-white px-5 py-1.5 rounded-full text-xs font-extrabold shadow-md z-10">เฉลี่ย <span id="stats-cmi-avg">-</span></span>
</div>
<div class="h-64 w-full"><canvas id="cmiChartTab"></canvas></div>
</div>
<div class="card-premium p-4 md:p-6">
<div class="flex justify-between mb-4 relative">
<h3 class="font-extrabold text-slate-800 text-sm md:text-base"><i class="fa-solid fa-chart-area text-blue-500 mr-2"></i> Sum AdjRW รายเดือน</h3><span class="adjrw-avg-display absolute left-1/2 -translate-x-1/2 bg-gradient-to-br from-blue-400 to-indigo-600 text-white px-5 py-1.5 rounded-full text-xs font-extrabold shadow-md z-10">รวม <span id="stats-adjrw-sum">-</span></span>
</div>
<div class="h-64 w-full"><canvas id="adjrwChartTab"></canvas></div>
</div>
</div>
</div>