Files
2026-09-16 23:20:08 +07:00

885 lines
56 KiB
PHP

<?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>