Files
gravity/Dashboard ลดแออัด-ลดรอคอย/views/tab_settings.php
T
2026-09-16 23:20:08 +07:00

906 lines
54 KiB
PHP

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