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
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
<?php
// views/dashboard.php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
</head>
<body class="bg-gradient-to-br from-emerald-50 to-emerald-100 text-slate-800 font-sans antialiased min-h-screen flex">
<?php require_once("../components/layout_sidebar.php"); ?>
<main class="flex-1 flex flex-col min-w-0">
<?php require_once("../components/layout_topbar.php"); ?>
<div class="p-4 md:p-8 lg:p-10 flex-1 overflow-y-auto">
<div class="max-w-7xl mx-auto">
<?php require_once("../components/card_patient_stats.php"); ?>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script src="../assets/js/micro-interactions.js"></script>
</body>
</html>
+75
View File
@@ -0,0 +1,75 @@
<?php
// views/lab_image.php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
$ln_enc = $_GET['ln'] ?? '';
$ln = decrypt_param($ln_enc);
if (empty($ln)) {
die("Invalid or missing lab order number.");
}
$sqlLn = "SELECT image1, image2, image3 FROM lab_order_image WHERE lab_order_number=?";
$stmt = $conn1->prepare($sqlLn);
$stmt->bind_param("s", $ln);
$stmt->execute();
$resultLn = $stmt->get_result();
$rowLn = $resultLn->fetch_assoc();
$stmt->close();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php
$page_title = "ภาพผลการตรวจทางห้องปฏิบัติการ (Lab Image)";
require_once("../components/head.php");
?>
</head>
<body class="bg-gradient-to-br from-emerald-50 to-emerald-100 text-slate-800 font-sans antialiased min-h-screen flex flex-col">
<main class="flex-1 p-4 lg:p-8 flex items-center justify-center overflow-y-auto">
<div class="max-w-4xl w-full mx-auto">
<div class="glass-card animate-enter overflow-hidden">
<div class="px-6 py-4 border-b border-white/50 bg-white/60 flex justify-between items-center sticky top-0 z-10 backdrop-blur-xl">
<h2 class="text-xl font-bold flex items-center gap-3 text-emerald-800">
<svg class="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
ภาพผลแล็บ (Lab Number: <?php echo htmlspecialchars($ln); ?>)
</h2>
<button onclick="window.close()" class="btn-outline !py-2 !px-4 text-sm bg-white/50">
ปิดหน้าต่าง
</button>
</div>
<div class="p-8 flex flex-col gap-8 items-center bg-slate-50/50">
<?php
$has_image = false;
if ($rowLn) {
for ($i = 1; $i <= 3; $i++) {
$img_col = "image$i";
if (!empty($rowLn[$img_col])) {
$has_image = true;
echo '<div class="w-full bg-white p-2 rounded-xl shadow-sm border border-slate-200 overflow-hidden">';
echo '<img src="data:image/jpeg;base64,' . base64_encode($rowLn[$img_col]) . '" alt="Lab Image ' . $i . '" class="w-full h-auto object-contain rounded-lg hover:scale-[1.02] transition-transform duration-300"/>';
echo '</div>';
}
}
}
if (!$has_image) {
echo '<div class="py-16 text-center text-slate-500 w-full flex flex-col items-center">
<svg class="w-16 h-16 mb-4 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<p class="text-lg">ไม่พบรูปภาพประกอบสำหรับผลแล็บนี้</p>
</div>';
}
?>
</div>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script src="../assets/js/micro-interactions.js"></script>
</body>
</html>
+444
View File
@@ -0,0 +1,444 @@
<?php
ini_set("display_errors", 1);
error_reporting(E_ALL);
// views/login.php
require_once("../core/security.php");
require_once("../core/settings.php");
require_once("../config/db.php");
configure_secure_session();
session_start();
send_security_headers();
// Handle Logout
if (isset($_GET['action']) && $_GET['action'] == 'logout') {
if (isset($_SESSION['account'])) {
require_once("../core/utils.php");
system_log($conn2, $_SESSION['account'], 'LOGOUT', ['reason' => 'User initiated']);
}
session_destroy();
header("Location: login.php");
exit();
}
// Handle Auto Logout (Timeout)
if (isset($_GET['action']) && $_GET['action'] == 'timeout') {
if (isset($_SESSION['account'])) {
require_once("../core/utils.php");
system_log($conn2, $_SESSION['account'], 'AUTO_LOGOUT', ['reason' => 'Inactivity timeout']);
}
session_destroy();
header("Location: login.php?error=timeout");
exit();
}
// Redirect if already logged in
if (!empty($_SESSION['sess_userid']) && $_SESSION['sess_userid'] === session_id()) {
header("Location: dashboard.php");
exit();
}
$error_msg = "";
if (isset($_GET['error']) && $_GET['error'] === 'timeout') {
$error_msg = "เซสชั่นหมดอายุ<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>เนื่องจากไม่มีการใช้งานระบบเป็นเวลานาน กรุณาล็อกอินใหม่อีกครั้ง</span>";
}
if ($_SERVER["REQUEST_METHOD"] == "POST") {
require_once("../core/utils.php");
require_once("../inc/rfc6238.php");
// CSRF Check
if (!verify_csrf_token($_POST['csrf_token'] ?? '')) {
die("CSRF Token Validation Failed");
}
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
$ip_address = $_SERVER['REMOTE_ADDR'];
$otp_arr = $_POST['otp'] ?? [];
$otp_val = implode('', $otp_arr);
if (empty($username) || empty($password)) {
$error_msg = "ข้อมูลไม่ครบถ้วน<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณากรอก Username และ Password ให้ครบถ้วน</span>";
} else if (!check_rate_limit($conn2, $ip_address)) {
$error_msg = "บัญชีถูกระงับชั่วคราว<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>ท่านพยายามเข้าสู่ระบบผิดพลาดหลายครั้ง กรุณารอ 15 นาที</span>";
system_log($conn2, $username, 'LOGIN_BLOCKED', ['reason' => 'Rate limit exceeded']);
} else {
$stmt = $conn1->prepare("SELECT * FROM opduser WHERE loginname = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
$stmt->close();
if ($result->num_rows == 1) {
$row = $result->fetch_assoc();
if (isset($row['account_disable']) && $row['account_disable'] === 'Y') {
$error_msg = "บัญชีนี้ถูกยกเลิกการใช้งาน<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาติดต่อผู้ดูแลระบบ (Admin) เพื่อขอเปิดสิทธิ์</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Account disabled']);
} else if (md5($password) === $row['passweb']) {
// Check 2FA if user has secret key
$secretkey = '';
$stmt2 = $conn2->prepare("SELECT secret_key FROM sys_user_2fa WHERE loginname = ?");
if ($stmt2) {
$stmt2->bind_param("s", $username);
$stmt2->execute();
$res2 = $stmt2->get_result();
if ($res2->num_rows > 0) {
$secretkey = $res2->fetch_assoc()['secret_key'];
}
$stmt2->close();
}
$pass_2fa = true;
if (!empty($secretkey)) {
if (empty($otp_val)) {
$pass_2fa = false;
$error_msg = "ตรวจสอบสิทธิ์ล้มเหลว (2FA)<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณากรอกรหัสยืนยันตัวตน 6 หลัก จากแอปพลิเคชัน</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Missing 2FA code']);
record_failed_login($conn2, $ip_address, $username);
} else {
$pass_2fa = TokenAuth6238::verify($secretkey, $otp_val);
if (!$pass_2fa) {
$error_msg = "รหัส 2FA ไม่ถูกต้อง<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาตรวจสอบรหัสจาก Google Authenticator และลองอีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Invalid 2FA code']);
record_failed_login($conn2, $ip_address, $username);
}
}
}
if ($pass_2fa) {
session_regenerate_id();
$_SESSION['sess_userid'] = session_id();
$_SESSION['account'] = $row['loginname'];
$_SESSION['name'] = $row['name'];
$_SESSION['position'] = 'เจ้าหน้าที่'; // simplified
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$client_os = getOS();
$client_browser = getBrowser();
system_log($conn2, $username, 'LOGIN_SUCCESS', [
'message' => 'Logged in successfully',
'os' => $client_os,
'browser' => $client_browser
]);
clear_failed_logins($conn2, $ip_address);
header("Location: dashboard.php");
exit();
}
} else {
record_failed_login($conn2, $ip_address, $username);
$error_msg = "รหัสผ่านไม่ถูกต้อง<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>กรุณาตรวจสอบตัวสะกด พิมพ์เล็ก/ใหญ่ แล้วลองอีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Invalid password']);
}
} else {
record_failed_login($conn2, $ip_address, $username);
$error_msg = "ไม่พบชื่อผู้ใช้งานนี้<br><span class='text-[11px] font-normal text-rose-400 mt-1 block'>ไม่มีชื่อผู้ใช้งานนี้ในระบบ HOSxP กรุณาตรวจสอบ Username อีกครั้ง</span>";
system_log($conn2, $username, 'LOGIN_FAILED', ['reason' => 'Username not found']);
}
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
<style>
/* Hide number arrows */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
/* Custom animations */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
@keyframes shine {
0% {
left: -100%;
opacity: 0;
}
20% {
opacity: 1;
}
50% {
left: 100%;
opacity: 0;
}
100% {
left: 100%;
opacity: 0;
}
}
.animate-shine {
position: absolute;
top: 0;
width: 50%;
height: 100%;
background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 100%);
animation: shine 4s infinite;
transform: skewX(-20deg);
z-index: 20;
pointer-events: none;
}
</style>
</head>
<body class="flex items-center justify-center min-h-screen bg-[#cfe6d8] text-slate-800 font-sans antialiased relative overflow-hidden">
<!-- Background Blobs -->
<div class="absolute inset-0 overflow-hidden pointer-events-none z-0">
<div class="absolute top-[-10%] left-[-10%] w-[50vw] h-[50vw] rounded-full bg-emerald-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob"></div>
<div class="absolute top-[20%] right-[-10%] w-[40vw] h-[40vw] rounded-full bg-teal-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob animation-delay-2000"></div>
<div class="absolute bottom-[-20%] left-[20%] w-[60vw] h-[60vw] rounded-full bg-green-200/60 mix-blend-multiply filter blur-[100px] opacity-70 animate-blob animation-delay-4000"></div>
</div>
<!-- Main Card -->
<div class="relative w-full max-w-[460px] bg-white/40 backdrop-blur-2xl border-2 border-white/70 shadow-[0_8px_32px_rgba(16,185,129,0.15)] rounded-[2.5rem] p-8 sm:p-12 text-center m-4 z-10 before:absolute before:inset-0 before:bg-gradient-to-b before:from-white/50 before:to-transparent before:rounded-[2.5rem] before:pointer-events-none">
<!-- Logo -->
<div class="relative w-28 h-28 mx-auto mb-6 group">
<div class="absolute inset-0 bg-emerald-400/30 rounded-full filter blur-xl group-hover:bg-emerald-400/40 transition-colors duration-500"></div>
<div class="relative w-full h-full bg-white/80 backdrop-blur-md rounded-full shadow-[0_4px_20px_rgba(16,185,129,0.2)] border-[3px] border-white flex items-center justify-center p-0.5 overflow-hidden">
<img src="../img/logo.png" alt="Logo" class="w-full h-full object-contain drop-shadow-sm rounded-full relative z-10">
<div class="animate-shine"></div>
</div>
</div>
<!-- Titles -->
<h2 class="text-3xl font-extrabold text-slate-800 mb-1 tracking-tight">โรงพยาบาลเกาะสมุย</h2>
<p class="text-slate-500 font-medium tracking-wide mb-8">ระบบฐานข้อมูล HOSxP Data</p>
<div class="mb-8" style="display: none;">
<p class="text-emerald-600 font-bold text-[15px] tracking-wide mb-0.5">ยินดีต้อนรับเข้าสู่ระบบ</p>
<p class="text-slate-400 text-sm">กรุณาเข้าสู่ระบบเพื่อใช้งาน</p>
</div>
<?php if (!empty($error_msg)): ?>
<div class="bg-rose-50 text-rose-600 px-4 py-3 rounded-2xl mb-6 text-sm text-left font-medium flex gap-3 items-start border border-rose-100 shadow-sm animate-enter">
<svg class="w-5 h-5 shrink-0 mt-0.5 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
</svg>
<div>
<?php echo $error_msg; ?>
</div>
</div>
<?php endif; ?>
<form id="loginForm" method="post" action="login.php" class="text-left space-y-5" autocomplete="off">
<input type="hidden" name="csrf_token" value="<?php echo generate_csrf_token(); ?>">
<!-- Honeypot / Fake inputs to defeat browser autofill -->
<input type="text" style="display:none" name="fake_username" autocomplete="username">
<input type="password" style="display:none" name="fake_password" autocomplete="current-password">
<!-- Username Input -->
<div>
<label class="block text-[13px] font-bold text-slate-600 mb-2">ชื่อผู้ใช้งาน (Username)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-emerald-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
</svg>
</div>
<input type="text" name="username" class="w-full pl-11 pr-4 py-3.5 bg-white/80 border border-white focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 rounded-2xl outline-none transition-all font-medium text-slate-700 placeholder-slate-400 shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)]" placeholder="บัญชีผู้ใช้" required autocomplete="new-password">
</div>
<p id="usernameWarning" class="text-rose-500 text-[11px] mt-1.5 ml-1 hidden font-medium animate-pulse">⚠️ ตรวจพบภาษาไทย กรุณาเปลี่ยนแป้นพิมพ์เป็นภาษาอังกฤษ</p>
</div>
<!-- Password Input -->
<div>
<label class="block text-[13px] font-bold text-slate-600 mb-2">รหัสผ่าน (Password)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none text-emerald-600">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path>
</svg>
</div>
<input type="password" id="pwdInput" name="password" class="w-full pl-11 pr-12 py-3.5 bg-white/80 border border-white focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 rounded-2xl outline-none transition-all font-medium text-slate-700 placeholder-slate-400 shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)]" placeholder="รหัสผ่าน" required autocomplete="new-password">
<button type="button" tabindex="-1" onclick="const p = document.getElementById('pwdInput'); p.type = p.type === 'password' ? 'text' : 'password';" class="absolute inset-y-0 right-0 pr-4 flex items-center text-emerald-600 hover:text-emerald-700 transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</div>
<p id="pwdWarning" class="text-rose-500 text-[11px] mt-1.5 ml-1 hidden font-medium animate-pulse">⚠️ ตรวจพบภาษาไทย กรุณาเปลี่ยนแป้นพิมพ์เป็นภาษาอังกฤษ</p>
</div>
<!-- 2FA Input -->
<div>
<label class="flex items-center text-[13px] font-bold text-slate-600 mb-2">
รหัส 2FA
<span class="ml-1.5 bg-white rounded-full shadow-[0_1px_3px_rgba(0,0,0,0.1)] p-0.5 inline-flex" title="Google Authenticator">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" class="w-[18px] h-[18px] animate-spin" style="animation-duration: 6s; animation-timing-function: linear;">
<line x1="50" y1="12" x2="50" y2="88" stroke="#34A853" stroke-width="16" stroke-linecap="round" />
<line x1="23" y1="23" x2="77" y2="77" stroke="#FBBC05" stroke-width="16" stroke-linecap="round" />
<line x1="12" y1="50" x2="88" y2="50" stroke="#4285F4" stroke-width="16" stroke-linecap="round" />
<line x1="77" y1="23" x2="23" y2="77" stroke="#EA4335" stroke-width="16" stroke-linecap="round" />
<circle cx="50" cy="50" r="14" fill="#ffffff" />
</svg>
</span>
</label>
<div class="flex gap-2 justify-between" id="otp-container">
<?php for ($i = 0; $i < 6; $i++): ?>
<input type="number" name="otp[]" maxlength="1" class="w-[50px] h-[56px] text-center text-2xl font-extrabold text-emerald-700 rounded-2xl border border-white bg-white/80 focus:outline-none focus:border-emerald-400 focus:ring-4 focus:ring-emerald-500/20 transition-all shadow-[inset_0_3px_8px_rgba(0,0,0,0.06),0_1px_2px_rgba(255,255,255,0.9)] otp-input" autocomplete="off" oninput="if(this.value.length > 1) this.value = this.value.slice(0,1);">
<?php endfor; ?>
</div>
</div>
<!-- Submit Button -->
<button type="submit" id="submitBtn" class="w-full bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 text-white font-bold py-4 px-6 rounded-full shadow-[0_8px_20px_rgba(16,185,129,0.4),inset_0_2px_0_rgba(255,255,255,0.3)] hover:shadow-[0_10px_25px_rgba(16,185,129,0.5),inset_0_2px_0_rgba(255,255,255,0.4)] transition-all flex items-center justify-center gap-2 mt-8 transform hover:-translate-y-0.5 border border-emerald-400/50">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
เข้าสู่ระบบ
</button>
</form>
<!-- Footer in Card -->
<div class="mt-8 pt-6 border-t border-emerald-500/10 flex items-center justify-center gap-2 text-[11px] font-semibold text-emerald-600/80">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path>
</svg>
ระบบมีความปลอดภัยสูง ข้อมูลของท่านได้รับการปกป้อง
</div>
</div>
<!-- Absolute Footer -->
<?php
$app_settings = get_app_settings();
$changelogs = get_changelogs();
$latest_version = !empty($changelogs) ? $changelogs[0]['version'] : '';
$footer_html = str_replace('{YEAR}', date('Y'), $app_settings['footer_text'] ?? '');
?>
<div class="absolute bottom-6 left-0 w-full text-center text-xs text-emerald-800/50 font-medium z-10 flex flex-col sm:flex-row items-center justify-center gap-2 sm:gap-3">
<?php if (!empty($latest_version)): ?>
<span><?php echo htmlspecialchars($latest_version); ?></span>
<span class="text-emerald-800/30 hidden sm:inline">|</span>
<?php endif; ?>
<span><?php echo $footer_html; ?></span>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const otpInputs = document.querySelectorAll('.otp-input');
const loginForm = document.getElementById('loginForm');
// Language check for Username and Password
const checkLang = (inputEl, warningEl) => {
inputEl.addEventListener('input', (e) => {
// Check if value contains non-ASCII characters (e.g. Thai)
if (/[^\x00-\x7F]/.test(e.target.value)) {
warningEl.classList.remove('hidden');
} else {
warningEl.classList.add('hidden');
}
});
};
const usernameInput = document.querySelector('input[name="username"]');
const usernameWarning = document.getElementById('usernameWarning');
if (usernameInput && usernameWarning) checkLang(usernameInput, usernameWarning);
const pwdInput = document.getElementById('pwdInput');
const pwdWarning = document.getElementById('pwdWarning');
if (pwdInput && pwdWarning) checkLang(pwdInput, pwdWarning);
function checkAndSubmit() {
let allFilled = true;
otpInputs.forEach(inp => {
if (!inp.value) allFilled = false;
});
const uname = document.querySelector('input[name="username"]').value;
const pwd = document.querySelector('input[name="password"]').value;
if (allFilled && uname && pwd) {
// Optional: show a loading state on the button
const btn = document.getElementById('submitBtn');
if (btn) {
btn.innerHTML = '<svg class="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> กำลังเข้าสู่ระบบ...';
btn.disabled = true;
}
loginForm.submit();
}
}
otpInputs.forEach((input, index) => {
input.addEventListener('input', (e) => {
if (e.target.value.length === 1) {
if (index < otpInputs.length - 1) {
otpInputs[index + 1].focus();
} else {
checkAndSubmit();
}
}
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Backspace' && !e.target.value && index > 0) {
otpInputs[index - 1].focus();
}
});
// Allow pasting 6 digits
input.addEventListener('paste', (e) => {
e.preventDefault();
const pastedData = e.clipboardData.getData('text').slice(0, 6);
if (/^\d+$/.test(pastedData)) {
pastedData.split('').forEach((char, i) => {
if (i < otpInputs.length) {
otpInputs[i].value = char;
}
});
const focusIndex = Math.min(pastedData.length, 5);
otpInputs[focusIndex].focus();
if (pastedData.length === 6) {
checkAndSubmit();
}
}
});
});
});
</script>
</body>
</html>
+255
View File
@@ -0,0 +1,255 @@
<?php
// views/manual.php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
$current_page = 'manual.php';
$username = $_SESSION['account'];
$is_admin = function_exists('is_admin') ? is_admin($username) : false;
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
<style>
.manual-content h2 {
font-size: 1.25rem;
font-weight: 700;
color: #047857;
margin-top: 2rem;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.manual-content h3 {
font-size: 1.1rem;
font-weight: 600;
color: #334155;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.manual-content p {
margin-bottom: 1rem;
line-height: 1.7;
}
.manual-content ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-bottom: 1.5rem;
line-height: 1.7;
}
.manual-content li {
margin-bottom: 0.5rem;
}
.manual-step {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 9999px;
background-color: #10b981;
color: white;
font-size: 0.875rem;
font-weight: bold;
margin-right: 0.5rem;
}
</style>
</head>
<body class="bg-[#f0fdf4] text-slate-700 font-sans antialiased selection:bg-emerald-200 selection:text-emerald-900 min-h-screen flex">
<?php require_once("../components/layout_sidebar.php"); ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 h-screen relative z-0">
<?php require_once("../components/layout_topbar.php"); ?>
<!-- Decorative Background -->
<div class="absolute top-[73px] left-0 w-full h-96 bg-gradient-to-b from-emerald-500/10 to-transparent pointer-events-none -z-10"></div>
<div class="p-4 md:p-8 lg:p-10 flex-1 overflow-y-auto">
<div class="max-w-4xl mx-auto">
<div class="mb-8 animate-enter">
<h1 class="text-3xl font-extrabold text-slate-800 tracking-tight flex items-center gap-3">
<span class="bg-emerald-100 text-emerald-600 p-2.5 rounded-2xl">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"></path></svg>
</span>
คู่มือการใช้งานระบบ
</h1>
<p class="text-slate-500 mt-2 text-lg">เอกสารอธิบายวิธีการใช้งานฟังก์ชันต่างๆ ของระบบสารสนเทศ</p>
</div>
<!-- Navigation Tabs -->
<div class="bg-white/70 backdrop-blur-md rounded-2xl p-2 flex flex-wrap gap-2 mb-8 shadow-sm border border-white animate-enter" style="animation-delay: 100ms;">
<button onclick="showSection('dashboard')" id="btn-dashboard" class="px-5 py-2.5 rounded-xl font-semibold text-sm transition-all tab-btn bg-emerald-500 text-white shadow-md">หน้าหลัก (Dashboard)</button>
<button onclick="showSection('search')" id="btn-search" class="px-5 py-2.5 rounded-xl font-semibold text-sm transition-all tab-btn text-slate-600 hover:bg-emerald-50">ค้นหาเวชระเบียน</button>
<button onclick="showSection('2fa')" id="btn-2fa" class="px-5 py-2.5 rounded-xl font-semibold text-sm transition-all tab-btn text-slate-600 hover:bg-emerald-50">การใช้งาน 2FA</button>
<?php if ($is_admin): ?>
<button onclick="showSection('admin')" id="btn-admin" class="px-5 py-2.5 rounded-xl font-semibold text-sm transition-all tab-btn text-slate-600 hover:bg-emerald-50">การตั้งค่าระบบ (สำหรับแอดมิน)</button>
<?php endif; ?>
</div>
<div class="glass-card p-6 md:p-10 animate-enter manual-content" style="animation-delay: 200ms;">
<!-- Dashboard Section -->
<div id="sec-dashboard" class="manual-section">
<h2 class="!mt-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
การใช้งานหน้าหลัก (Dashboard)
</h2>
<p>หน้าหลักของระบบถูกออกแบบมาเพื่อให้เจ้าหน้าที่สามารถดูสถิติและภาพรวมของการให้บริการในโรงพยาบาล ณ วันปัจจุบันได้อย่างรวดเร็ว ข้อมูลจะถูกดึงมาจากฐานข้อมูล HOSxP โดยตรง</p>
<h3>ภาพรวมสถิติประจำวัน</h3>
<ul>
<li><strong>ผู้ป่วยนอก (OPD):</strong> แสดงจำนวนผู้ป่วยที่ลงทะเบียนรับบริการในแผนกผู้ป่วยนอกของวันนี้</li>
<li><strong>ผู้ป่วยใน (IPD):</strong> แสดงจำนวนผู้ป่วยที่กำลังนอนพักรักษาตัวอยู่ในโรงพยาบาลขณะนี้ (Admit)</li>
<li><strong>ผู้ป่วยใหม่ (New Patient):</strong> แสดงจำนวนผู้ป่วยที่เพิ่งได้รับการขึ้นทะเบียนประวัติใหม่ (ทำบัตรใหม่) ในวันนี้</li>
<li><strong>ยอดนัดหมาย (Appointments):</strong> แสดงจำนวนผู้ป่วยที่มีคิวนัดหมายมารับบริการในวันนี้ทั้งหมด</li>
</ul>
<div class="bg-blue-50 border-l-4 border-blue-500 p-4 rounded-r-lg my-6">
<div class="flex items-start">
<svg class="w-5 h-5 text-blue-500 mt-0.5 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<p class="text-blue-800 text-sm mb-0">ข้อมูลสถิติในหน้า Dashboard จะดึงข้อมูลแบบ Real-time ตามฐานข้อมูลหลัก หากมีการลงทะเบียนผู้ป่วยใหม่ ตัวเลขในหน้านี้จะอัปเดตโดยอัตโนมัติเมื่อทำการรีเฟรชหน้าเว็บ (F5)</p>
</div>
</div>
</div>
<!-- Search Section -->
<div id="sec-search" class="manual-section hidden">
<h2 class="!mt-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
การค้นหาเวชระเบียนผู้ป่วย
</h2>
<p>เมนูนี้ใช้สำหรับค้นหาข้อมูลประวัติผู้ป่วย (Patient Profile) โดยสามารถค้นหาได้จากหลายเงื่อนไข เพื่ออำนวยความสะดวกในการค้นหาที่รวดเร็วและแม่นยำที่สุด</p>
<h3>วิธีการค้นหาข้อมูล</h3>
<p>คุณสามารถพิมพ์ข้อมูลอย่างใดอย่างหนึ่งลงในช่องค้นหา:</p>
<ul>
<li><strong>HN (Hospital Number):</strong> กรอกเลขประจำตัวผู้ป่วย (เช่น 0001234)</li>
<li><strong>หมายเลขบัตรประชาชน:</strong> กรอกเลขบัตรประชาชน 13 หลัก โดยไม่ต้องใส่เครื่องหมายขีด (เช่น 1101234567890)</li>
<li><strong>ชื่อ-นามสกุล:</strong> สามารถกรอกชื่อ หรือนามสกุล หรือทั้งชื่อและนามสกุล โดยเว้นวรรค (เช่น "สมชาย รักดี" หรือพิมพ์แค่ "สมชาย")</li>
</ul>
<div class="bg-amber-50 border-l-4 border-amber-500 p-4 rounded-r-lg my-6">
<div class="flex items-start">
<svg class="w-5 h-5 text-amber-500 mt-0.5 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<div class="text-amber-800 text-sm">
<strong>เงื่อนไขการค้นหาที่ต้องระวัง:</strong>
<ul class="mb-0 mt-1 pl-4">
<li class="mb-1"><strong>ค้นหาด้วย HN หรือ บัตรประชาชน:</strong> ต้องกรอกตัวเลขให้ครบถ้วนและถูกต้อง (บัตรประชาชน 13 หลัก)</li>
<li class="mb-0"><strong>ค้นหาด้วย ชื่อ-นามสกุล:</strong> ต้องพิมพ์ความยาวอย่างน้อย <strong>2 ตัวอักษรขึ้นไป</strong> หากพิมพ์น้อยกว่า 2 ตัวอักษร ระบบจะไม่อนุญาตให้ค้นหาเพื่อป้องกันการดึงข้อมูลที่มีปริมาณมากเกินไป</li>
</ul>
</div>
</div>
</div>
<h3>ขั้นตอนการดูรายละเอียดผู้ป่วย</h3>
<p><span class="manual-step">1</span> พิมพ์คำค้นหาในช่องค้นหา แล้วคลิกปุ่ม <strong>"ค้นหา"</strong> หรือกดปุ่ม Enter บนคีย์บอร์ด</p>
<p><span class="manual-step">2</span> ระบบจะแสดงรายชื่อผู้ป่วยที่ตรงกับเงื่อนไข หากพบหลายรายการ ระบบจะแสดงรายการทั้งหมด (สูงสุดตามที่ตั้งค่าไว้ในระบบ)</p>
<p><span class="manual-step">3</span> คลิกที่ปุ่ม <strong>"ดูประวัติ"</strong> ท้ายรายชื่อผู้ป่วยที่ต้องการ</p>
<p><span class="manual-step">4</span> ระบบจะพาไปยังหน้า <strong>รายละเอียดผู้ป่วย (Patient Detail)</strong> ซึ่งจะประกอบไปด้วย ข้อมูลทั่วไป, ข้อมูลการแพ้ยา, โรคประจำตัว และประวัติการรับบริการ (Visit History) ย้อนหลัง</p>
</div>
<!-- 2FA Section -->
<div id="sec-2fa" class="manual-section hidden">
<h2 class="!mt-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
การใช้งานระบบยืนยันตัวตน 2 ขั้นตอน (2FA)
</h2>
<p>เพื่อความปลอดภัยของข้อมูล ระบบรองรับการยืนยันตัวตนแบบ 2 ขั้นตอน (Two-Factor Authentication) ผ่านแอปพลิเคชัน Google Authenticator หรือแอปที่รองรับ TOTP</p>
<h3>สำหรับเจ้าหน้าที่ทั่วไป (การสแกนและล็อกอิน)</h3>
<p><span class="manual-step">1</span> ดาวน์โหลดแอปพลิเคชัน <strong>Google Authenticator</strong> ลงในสมาร์ทโฟน (รองรับทั้ง iOS และ Android)</p>
<p><span class="manual-step">2</span> ติดต่อผู้ดูแลระบบ (Admin) เพื่อขอให้ระบบสร้าง QR Code สำหรับบัญชีของคุณ</p>
<p><span class="manual-step">3</span> เปิดแอป Google Authenticator กดปุ่ม <strong>+</strong> และเลือก <strong>"Scan a QR code"</strong></p>
<p><span class="manual-step">4</span> นำสมาร์ทโฟนไปสแกนหน้าจอ QR Code ที่ผู้ดูแลระบบแสดงให้ เมื่อสแกนสำเร็จ ในแอปจะแสดงชื่อบัญชีและตัวเลข 6 หลักที่เปลี่ยนไปทุกๆ 30 วินาที</p>
<p><span class="manual-step">5</span> ในการเข้าใช้งานระบบ ให้กรอก Username, Password และตามด้วย <strong>รหัส 6 หลักจากแอปพลิเคชัน</strong> ในหน้า Login</p>
<?php if ($is_admin): ?>
<div class="bg-indigo-50 border-l-4 border-indigo-500 p-4 rounded-r-lg my-6">
<h3 class="!mt-0 text-indigo-800">สำหรับผู้ดูแลระบบ (การตั้งค่าให้เจ้าหน้าที่)</h3>
<p class="text-sm text-indigo-700 mb-2">แอดมินสามารถสร้าง QR Code ให้ผู้ใช้งานได้ดังนี้:</p>
<p class="text-sm text-indigo-700 mb-1"><span class="manual-step !bg-indigo-500 !w-5 !h-5 !text-[11px]">1</span> ไปที่เมนู <strong>"การตั้งค่าระบบ" (Admin Settings)</strong> ในแถบด้านซ้าย</p>
<p class="text-sm text-indigo-700 mb-1"><span class="manual-step !bg-indigo-500 !w-5 !h-5 !text-[11px]">2</span> ไปที่แท็บ <strong>"จัดการผู้ใช้งาน"</strong></p>
<p class="text-sm text-indigo-700 mb-1"><span class="manual-step !bg-indigo-500 !w-5 !h-5 !text-[11px]">3</span> ค้นหาชื่อเจ้าหน้าที่ที่ต้องการ แล้วคลิกปุ่ม <strong>"จัดการ 2FA"</strong> (รูปกุญแจ) ด้านขวาสุด</p>
<p class="text-sm text-indigo-700 mb-1"><span class="manual-step !bg-indigo-500 !w-5 !h-5 !text-[11px]">4</span> กดปุ่ม <strong>"สร้าง QR Code ใหม่"</strong></p>
<p class="text-sm text-indigo-700 mb-1"><span class="manual-step !bg-indigo-500 !w-5 !h-5 !text-[11px]">5</span> ให้เจ้าหน้าที่สแกน QR Code จากนั้นกดปุ่ม <strong>"บันทึกข้อมูล"</strong></p>
<p class="text-sm text-indigo-700 mb-0 mt-2"><em>* หากต้องการยกเลิกการใช้ 2FA สำหรับผู้ใช้นั้น ให้กดปุ่ม <strong>"รีเซ็ต (ล้างค่า 2FA)"</strong></em></p>
</div>
<?php endif; ?>
</div>
<?php if ($is_admin): ?>
<!-- Admin Settings Section -->
<div id="sec-admin" class="manual-section hidden">
<h2 class="!mt-0">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
การตั้งค่าระบบ (สำหรับผู้ดูแลระบบ)
</h2>
<p>เมนูนี้สงวนสิทธิ์เฉพาะผู้ใช้ที่มีสถานะเป็น Admin ในระบบ HOSxP เท่านั้น โดยใช้สำหรับตั้งค่าการแสดงผลต่างๆ ของเว็บแอปพลิเคชัน</p>
<h3>การตั้งค่าทั่วไป (General Settings)</h3>
<ul>
<li><strong>ชื่อโรงพยาบาล (Hospital Name):</strong> ข้อความที่จะแสดงเป็นหัวข้อใหญ่ที่หน้า Login</li>
<li><strong>ข้อความต้อนรับ (Welcome Text):</strong> ข้อความรองที่หน้า Login</li>
<li><strong>ข้อความ Footer (Footer Text):</strong> ข้อความลิขสิทธิ์ด้านล่างสุดของเว็บ (รองรับตัวแปร {YEAR} สำหรับปีปัจจุบัน)</li>
</ul>
<h3>การตั้งค่าข้อจำกัด (Limits & Performance)</h3>
<ul>
<li><strong>จำกัดการค้นหา (Max Search Results):</strong> กำหนดจำนวนรายชื่อผู้ป่วยสูงสุดที่จะแสดงผลในการค้นหาแต่ละครั้ง (ป้องกันการดึงข้อมูลที่มากเกินไปจนทำให้ฐานข้อมูลทำงานหนัก)</li>
<li><strong>จำกัดประวัติ Visit (Max Visit History):</strong> กำหนดจำนวนประวัติการมารับบริการย้อนหลังที่จะแสดงในหน้าประวัติผู้ป่วย</li>
<li><strong>ระยะเวลา Session (Session Timeout):</strong> เวลา(นาที) ที่ระบบจะบังคับให้ออกจากระบบอัตโนมัติ หากไม่มีการเคลื่อนไหวหรือใช้งานหน้าจอ (เพื่อความปลอดภัยของข้อมูล)</li>
</ul>
<h3>ระบบความปลอดภัย (Security Settings)</h3>
<ul>
<li><strong>ระบบล็อกอิน 2 ขั้นตอน (Enable 2FA):</strong> เปิดหรือปิดการบังคับให้ผู้ใช้งานต้องกรอกรหัส 2FA (Google Authenticator) ทุกครั้งที่ล็อกอิน (หากปิด จะใช้เพียงแค่ Username/Password)</li>
<li><strong>รหัส 2FA หลัก (Master 2FA Code):</strong> ในกรณีฉุกเฉิน หรือยังไม่ได้ผูกแอปพลิเคชัน สามารถตั้งรหัส Master Code เพื่อใช้ผ่านระบบ 2FA ได้ชั่วคราว</li>
</ul>
<div class="bg-rose-50 border-l-4 border-rose-500 p-4 rounded-r-lg my-6">
<div class="flex items-start">
<svg class="w-5 h-5 text-rose-500 mt-0.5 mr-3 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path></svg>
<p class="text-rose-800 text-sm mb-0"><strong>คำเตือน:</strong> การแก้ไขการตั้งค่าจะส่งผลต่อผู้ใช้งานทุกคนในระบบทันที ควรตรวจสอบข้อมูลให้ถูกต้องก่อนกดบันทึก</p>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script>
function showSection(sectionId) {
// Hide all sections
document.querySelectorAll('.manual-section').forEach(el => {
el.classList.add('hidden');
});
// Reset tab styles
document.querySelectorAll('.tab-btn').forEach(el => {
el.classList.remove('bg-emerald-500', 'text-white', 'shadow-md');
el.classList.add('text-slate-600', 'hover:bg-emerald-50');
});
// Show target section
document.getElementById('sec-' + sectionId).classList.remove('hidden');
// Highlight target tab
const activeBtn = document.getElementById('btn-' + sectionId);
activeBtn.classList.remove('text-slate-600', 'hover:bg-emerald-50');
activeBtn.classList.add('bg-emerald-500', 'text-white', 'shadow-md');
}
</script>
</body>
</html>
+901
View File
@@ -0,0 +1,901 @@
<?php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
require_once("../core/settings.php");
$current_page = 'ncd_registry.php';
// Initialize variables and clinic data
$t_diag = ['E119', 'I10', 'E785', 'N183'];
$curr_clinic = $_GET['clinic'] ?? 'ALL';
$ncd_clinics = [
'DM' => 'คลินิกเบาหวาน (DM)',
'HT' => 'คลินิกความดันฯ (HT)',
'DLP' => 'คลินิกไขมันฯ (DLP)',
'CKD' => 'คลินิกโรคไต (CKD)'
];
$app_settings = get_app_settings();
$clinic_codes = [
'DM' => $app_settings['ncd_clinic_dm_code'] ?? '001',
'HT' => $app_settings['ncd_clinic_ht_code'] ?? '002',
'DLP' => $app_settings['ncd_clinic_dlp_code'] ?? '039',
'CKD' => $app_settings['ncd_clinic_ckd_code'] ?? '051'
];
$clinic_code = $clinic_codes[$curr_clinic] ?? null;
// Date parameters for filtering
$ds1 = $_GET['ds1'] ?? date('Y-m-d', strtotime('-1 month'));
$ds2 = $_GET['ds2'] ?? date('Y-m-d');
// Create reverse mapping for clinic codes to names
$clinic_code_to_name = [];
foreach ($clinic_codes as $key => $code) {
$clinic_code_to_name[$code] = $ncd_clinics[$key];
}
$patientsData = [];
// Execute query only if ds1 and ds2 are submitted
$dashboardData = [];
if (isset($_GET['ds1']) && isset($_GET['ds2'])) {
if ($curr_clinic === 'ALL') {
// Run dashboard queries instead of fetching all patients
$dashboard_keys = [
'dashboard_dm_all', 'dashboard_dm_hba1c',
'dashboard_dm_1', 'dashboard_dm_2', 'dashboard_dm_3', 'dashboard_dm_4', 'dashboard_dm_5', 'dashboard_dm_eye',
'dashboard_ht_all', 'dashboard_ht_1', 'dashboard_ht_2',
'dashboard_dlp_all',
'dashboard_ckd_all', 'dashboard_ckd_1'
];
foreach ($dashboard_keys as $k) {
$dashboardData[$k] = ($k === 'dashboard_ckd_1') ? [] : 0;
if (!empty($app_settings[$k])) {
$sql = str_replace(['{ds1}', '{ds2}'], [$ds1, $ds2], $app_settings[$k]);
if ($res = $conn1->query($sql)) {
if ($k === 'dashboard_ckd_1') {
while ($r = $res->fetch_assoc()) {
$dashboardData[$k][$r['stage']] = $r['total_patient'];
}
} else {
if ($r = $res->fetch_assoc()) {
$dashboardData[$k] = $r['total_patient'] ?? 0;
}
}
}
}
}
} else {
$clinic_cond = "AND cl.clinic = '" . $conn1->real_escape_string($clinic_code) . "'";
$raw_sql = $app_settings['ncd_sql'] ?? '';
$sql = str_replace('{clinic_cond}', $clinic_cond, $raw_sql);
if ($stmt = $conn1->prepare($sql)) {
$stmt->bind_param("ss", $ds1, $ds2);
$stmt->execute();
$result = $stmt->get_result();
if ($result) {
while ($row = $result->fetch_assoc()) {
$patientsData[] = $row;
}
}
$stmt->close();
}
// Bulk fetch additional clinical data (Labs & Screenings)
if (!empty($patientsData)) {
$hn_list = array_column($patientsData, 'hn');
$hn_sql_in = "'" . implode("','", array_map(function($hn) use ($conn1) {
return $conn1->real_escape_string($hn);
}, array_unique($hn_list))) . "'";
$extraData = [];
foreach ($hn_list as $hn) {
$extraData[$hn] = [
'hba1c' => ['res' => '-', 'date' => '-'],
'urine_prot' => ['res' => '-', 'date' => '-'],
'cr' => ['res' => '-', 'date' => '-'],
'urine_alb' => ['res' => '-', 'date' => '-'],
'urine_cr' => ['res' => '-', 'date' => '-'],
'hbsag' => ['res' => '-', 'date' => '-'],
'anti_hcv' => ['res' => '-', 'date' => '-'],
'hpv' => ['res' => '-', 'date' => '-'],
'colon_cancer' => ['res' => '-', 'date' => '-'],
'ldl' => ['res' => '-', 'date' => '-'],
'eye_screen' => '-',
'foot_screen' => '-',
'oral_cancer' => '-'
];
}
// 1. LAB Data
$lab_sql = "SELECT a.hn, b.lab_items_code, b.lab_order_result, a.order_date
FROM lab_head a
JOIN lab_order b ON a.lab_order_number=b.lab_order_number
WHERE a.hn IN ($hn_sql_in)
AND b.lab_items_code IN ('288','223','224','802','78','226','227','641','656','659','2215','1896','1549','479')
AND (
(b.lab_items_code != '1549' AND a.order_date BETWEEN '$ds1' AND '$ds2') OR
(b.lab_items_code = '1549' AND a.order_date BETWEEN '2024-10-01' AND '2026-06-11')
)
ORDER BY a.order_date ASC";
if ($res_lab = $conn1->query($lab_sql)) {
while ($r = $res_lab->fetch_assoc()) {
$hn = $r['hn'];
$code = $r['lab_items_code'];
$res = trim($r['lab_order_result'] ?? '');
$date = (!empty($r['order_date']) && $r['order_date'] != '0000-00-00') ? thai_date2($r['order_date']) : '-';
if ($code == '288') $extraData[$hn]['hba1c'] = ['res' => $res, 'date' => $date];
else if (in_array($code, ['223','224'])) $extraData[$hn]['urine_prot'] = ['res' => $res, 'date' => $date];
else if (in_array($code, ['802','78','226','227'])) {
$extraData[$hn]['cr'] = ['res' => $res, 'date' => $date];
if ($code == '227') $extraData[$hn]['urine_cr'] = ['res' => $res, 'date' => $date];
}
else if ($code == '641') $extraData[$hn]['urine_alb'] = ['res' => $res, 'date' => $date];
else if ($code == '656') $extraData[$hn]['hbsag'] = ['res' => $res, 'date' => $date];
else if ($code == '659') $extraData[$hn]['anti_hcv'] = ['res' => $res, 'date' => $date];
else if (in_array($code, ['2215','1896'])) $extraData[$hn]['hpv'] = ['res' => $res, 'date' => $date];
else if ($code == '1549') $extraData[$hn]['colon_cancer'] = ['res' => $res, 'date' => $date];
else if ($code == '479') $extraData[$hn]['ldl'] = ['res' => $res, 'date' => $date];
}
}
// 2. Eye/Foot Screening
$screen_sql = "SELECT hn, screen_date, do_eye_screen, do_foot_screen
FROM clinicmember_cormobidity_screen
WHERE hn IN ($hn_sql_in) AND screen_date BETWEEN '$ds1' AND '$ds2'
ORDER BY screen_date ASC";
if ($res_scr = $conn1->query($screen_sql)) {
while ($r = $res_scr->fetch_assoc()) {
$hn = $r['hn'];
$date = (!empty($r['screen_date']) && $r['screen_date'] != '0000-00-00') ? thai_date2($r['screen_date']) : '-';
if ($r['do_eye_screen'] == 'Y') $extraData[$hn]['eye_screen'] = $date;
if ($r['do_foot_screen'] == 'Y') $extraData[$hn]['foot_screen'] = $date;
}
}
// 3. Oral Cancer Screening
$oral_sql = "SELECT hn, vstdate
FROM dtmain
WHERE hn IN ($hn_sql_in) AND tmcode IN ('001480') AND vstdate BETWEEN '$ds1' AND '$ds2'
ORDER BY vstdate ASC";
if ($res_oral = $conn1->query($oral_sql)) {
while ($r = $res_oral->fetch_assoc()) {
$date = (!empty($r['vstdate']) && $r['vstdate'] != '0000-00-00') ? thai_date2($r['vstdate']) : '-';
$extraData[$r['hn']]['oral_cancer'] = $date;
}
}
// Merge back into patientsData
foreach ($patientsData as &$p) {
$h = $p['hn'];
$p['extra'] = $extraData[$h] ?? [];
}
unset($p);
}
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php
$page_title = "ทะเบียนผู้ป่วย NCD";
require_once("../components/head.php");
?>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.dataTables.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/fixedcolumns/4.3.0/css/fixedColumns.dataTables.min.css">
<script src="https://cdn.datatables.net/fixedcolumns/4.3.0/js/dataTables.fixedColumns.min.js"></script>
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { font-family: 'Sarabun', sans-serif; }
.glass-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border-radius: 1rem;
}
.tab-btn.active {
border-bottom: 2px solid #3b82f6;
color: #3b82f6;
font-weight: 600;
}
.ncd-table th {
position: sticky;
top: 0;
background: #f8fafc;
z-index: 10;
border-bottom: 2px solid #cbd5e1;
white-space: nowrap;
text-align: center !important;
}
.ncd-table td {
white-space: nowrap;
}
/* Sticky first 3 columns */
.ncd-table th:nth-child(1), .ncd-table td:nth-child(1) { position: sticky; left: 0; z-index: 20; background: inherit; }
.ncd-table th:nth-child(2), .ncd-table td:nth-child(2) { position: sticky; left: 50px; z-index: 20; background: inherit; }
.ncd-table th:nth-child(3), .ncd-table td:nth-child(3) { position: sticky; left: 130px; z-index: 20; background: inherit; }
.ncd-table th:nth-child(1) { z-index: 30; }
.ncd-table th:nth-child(2) { z-index: 30; }
.ncd-table th:nth-child(3) { z-index: 30; }
/* Shadow for fixed columns */
.ncd-table td:nth-child(3)::after, .ncd-table th:nth-child(3)::after {
content: '';
position: absolute;
top: 0; right: 0; bottom: -1px; width: 30px;
transform: translateX(100%);
transition: box-shadow 0.3s;
pointer-events: none;
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.15);
}
.dashboard-gradient-dm {
background: linear-gradient(135deg, #4f46e5 0%, #3b82f6 100%);
}
.dashboard-gradient-ht {
background: linear-gradient(135deg, #f97316 0%, #f59e0b 100%);
}
.dashboard-gradient-dlp {
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
}
.dashboard-gradient-ckd {
background: linear-gradient(135deg, #0d9488 0%, #14b8a6 100%);
}
.progress-bar-bg { background-color: #e2e8f0; border-radius: 9999px; height: 6px; width: 100%; overflow: hidden; margin-top: 8px; }
.progress-bar-fill-dm { background-color: #4f46e5; height: 100%; border-radius: 9999px; }
.progress-bar-fill-ht { background-color: #f97316; height: 100%; border-radius: 9999px; }
.progress-bar-fill-dlp { background-color: #8b5cf6; height: 100%; border-radius: 9999px; }
.progress-bar-fill-ckd { background-color: #0d9488; height: 100%; border-radius: 9999px; }
/* Custom CSS to make DataTables look good with Tailwind */
.dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_filter input {
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
outline: none;
}
.dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 0.5rem !important;
padding: 0.25rem 0.75rem !important;
margin: 0 0.125rem !important;
border: 1px solid transparent !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: #10b981 !important;
color: white !important;
border: none !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover:not(.current) {
background: #f1f5f9 !important;
color: #334155 !important;
}
table.dataTable.no-footer {
border-bottom: 1px solid #cbd5e1;
}
/* Fixed columns styling & Shadow */
.ncd-table th.dtfc-fixed-left {
background-color: #f1f5f9 !important;
z-index: 50 !important;
}
.ncd-table tr.bg-white td.dtfc-fixed-left {
background-color: #ffffff !important;
z-index: 40 !important;
}
.ncd-table tr.bg-slate-50 td.dtfc-fixed-left {
background-color: #f8fafc !important;
z-index: 40 !important;
}
.ncd-table tr:hover td.dtfc-fixed-left {
background-color: #eff6ff !important;
}
/* Add shadow to the rightmost fixed column to show elevation */
.ncd-table th:nth-child(3).dtfc-fixed-left,
.ncd-table td:nth-child(3).dtfc-fixed-left {
box-shadow: 4px 0 6px -2px rgba(0, 0, 0, 0.1) !important;
border-right: 1px solid #e2e8f0;
}
/* Custom Sort Icons using SVG Backgrounds */
table.dataTable thead th.sorting,
table.dataTable thead th.sorting_asc,
table.dataTable thead th.sorting_desc {
background-image: none !important;
position: relative;
}
table.dataTable thead th.sorting:before,
table.dataTable thead th.sorting:after,
table.dataTable thead th.sorting_asc:before,
table.dataTable thead th.sorting_asc:after,
table.dataTable thead th.sorting_desc:before,
table.dataTable thead th.sorting_desc:after {
display: none !important;
content: none !important;
}
table.dataTable thead th.sorting::after,
table.dataTable thead th.sorting_asc::after,
table.dataTable thead th.sorting_desc::after {
content: "" !important;
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
width: 10px;
height: 14px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
display: block !important;
opacity: 1 !important;
}
table.dataTable thead th.sorting::after {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="%23cbd5e1" d="M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8H32c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8h256c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z"/></svg>') !important;
}
table.dataTable thead th.sorting_asc::after {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="%2310b981" d="M182.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-128 128c-9.2 9.2-11.9 22.9-6.9 34.9s16.6 19.8 29.6 19.8h256c12.9 0 24.6 7.8 29.6 19.8s-2.2-25.7-6.9-34.9l-128-128z"/></svg>') !important;
}
table.dataTable thead th.sorting_desc::after {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path fill="%2310b981" d="M182.6 470.6c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8h256c12.9 0 24.6 7.8 29.6 19.8s-2.2 25.7-6.9 34.9l-128 128z"/></svg>') !important;
}
</style>
</head>
<body class="bg-gradient-to-br from-emerald-50 to-emerald-100 text-slate-800 font-sans antialiased min-h-screen flex">
<?php require_once("../components/layout_sidebar.php"); ?>
<main class="flex-1 flex flex-col min-w-0 h-screen overflow-hidden">
<?php require_once("../components/layout_topbar.php"); ?>
<div class="flex-1 overflow-y-auto p-4 md:p-6 lg:p-8">
<div class="max-w-7xl mx-auto">
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<div>
<h1 class="text-2xl font-bold text-slate-800 flex items-center gap-3">
<svg class="w-7 h-7 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"></path></svg>
ทะเบียนผู้ป่วย<?= ($curr_clinic !== 'ALL') ? " " . ($ncd_clinics[$curr_clinic] ?? $curr_clinic) : " NCD รวม" ?>
<?php
$show_dm = ($curr_clinic === 'ALL' || $curr_clinic === 'DM');
$show_ht = ($curr_clinic === 'ALL' || $curr_clinic === 'HT');
$show_dlp = ($curr_clinic === 'ALL' || $curr_clinic === 'DLP');
$show_ckd = ($curr_clinic === 'ALL' || $curr_clinic === 'CKD');
if ($curr_clinic !== 'ALL') {
echo " - " . ($ncd_clinics[$curr_clinic] ?? '');
}
?>
</h1>
<p class="text-slate-500 mt-1">ระบบลงทะเบียนและติดตามผู้ป่วยโรคเรื้อรัง</p>
</div>
</div>
<!-- CONDITIONAL VIEW: DASHBOARD OR TABLE -->
<?php if ($curr_clinic === 'ALL'): ?>
<!-- DASHBOARD VIEW -->
<div id="view-dash" class="space-y-6">
<div class="glass-card p-4 flex flex-col md:flex-row justify-between items-center gap-4 border-b border-slate-200">
<h3 class="font-bold text-slate-800 flex items-center gap-2">
ตัวชี้วัดสถิติ NCD
</h3>
<form method="GET" action="ncd_registry.php" class="flex flex-col sm:flex-row items-center gap-3" onsubmit="return showProcessingModal();">
<input type="hidden" name="clinic" value="<?= htmlspecialchars($curr_clinic) ?>">
<div class="flex items-center gap-2">
<label class="text-sm font-semibold text-slate-600">ตั้งแต่:</label>
<input type="date" name="ds1" value="<?= htmlspecialchars($ds1) ?>" class="border border-slate-200 rounded px-3 py-1.5 text-sm focus:ring-emerald-500 focus:border-emerald-500 bg-white" required>
</div>
<div class="flex items-center gap-2">
<label class="text-sm font-semibold text-slate-600">ถึง:</label>
<input type="date" name="ds2" value="<?= htmlspecialchars($ds2) ?>" class="border border-slate-200 rounded px-3 py-1.5 text-sm focus:ring-emerald-500 focus:border-emerald-500 bg-white" required>
</div>
<button type="submit" class="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-1.5 rounded-md text-sm font-semibold transition-colors shadow-sm whitespace-nowrap">ประมวลผล (Dashboard)</button>
</form>
</div>
<!-- DM Section -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-blue-600"></div>
คลินิกโรคเบาหวาน (Diabetes Mellitus)
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ผู้ป่วยเบาหวานทั้งหมด</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_all'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจ HBA1C</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_hba1c'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">DM ไม่มีโรคร่วม (HbA1C < 7)</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_1'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">DM มีโรคร่วม (HbA1C < 8)</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_2'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจ LDL</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_3'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจไต</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_4'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจเท้า</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_5'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจตา</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dm_eye'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
</div>
</div>
<!-- HT Section -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-emerald-500"></div>
คลินิกโรคความดันโลหิตสูง (Hypertension)
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ผู้ป่วยความดันโลหิตสูงทั้งหมด</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_ht_all'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ความดัน 2 ครั้งสุดท้าย < 139/89</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_ht_1'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ได้รับการตรวจไต</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_ht_2'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
</div>
</div>
<!-- DLP Section -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-purple-600"></div>
คลินิกโรคไขมันในเลือดสูง (Dyslipidemia)
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ผู้ป่วยไขมันในเลือดสูงทั้งหมด</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_dlp_all'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
</div>
</div>
<!-- CKD Section -->
<div>
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-bold text-slate-800 flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-teal-500"></div>
คลินิกโรคไตเรื้อรัง (CKD)
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-2">ผู้ป่วยโรคไตเรื้อรังทั้งหมด</div>
<div class="text-3xl font-bold text-slate-800"><?php echo number_format($dashboardData['dashboard_ckd_all'] ?? 0); ?> <span class="text-sm font-normal text-slate-400">คน</span></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 border border-slate-200 shadow-sm">
<div class="text-sm text-slate-500 mb-4 font-bold">จำนวนผู้ป่วยโรคไตแยกตาม Stage</div>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-3">
<?php
$stages = ['1' => 'Stage 1', '2' => 'Stage 2', '3a' => 'Stage 3a', '3b' => 'Stage 3b', '4' => 'Stage 4', '5' => 'Stage 5'];
foreach ($stages as $st => $st_name):
$val = isset($dashboardData['dashboard_ckd_1'][$st]) ? $dashboardData['dashboard_ckd_1'][$st] : 0;
?>
<div class="bg-slate-50 rounded-lg p-3 border border-slate-100 text-center">
<div class="text-xs text-slate-500 mb-1 font-semibold"><?php echo $st_name; ?></div>
<div class="text-xl font-bold text-slate-800"><?php echo number_format($val); ?></div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php else: ?>
<!-- TABLE VIEW -->
<div id="view-table" class="glass-card overflow-hidden">
<div class="p-4 border-b border-slate-200 flex flex-col xl:flex-row justify-between items-center gap-4 bg-white/50">
<h3 class="font-bold text-slate-800 flex items-center gap-2">
ทะเบียนผู้ป่วย<?= ($curr_clinic !== 'ALL') ? " " . ($ncd_clinics[$curr_clinic] ?? $curr_clinic) : " NCD รวม" ?>
</h3>
<form method="GET" action="ncd_registry.php" class="flex flex-col sm:flex-row items-center gap-3" onsubmit="return showProcessingModal();">
<input type="hidden" name="clinic" value="<?= htmlspecialchars($curr_clinic) ?>">
<div class="flex items-center gap-2">
<label class="text-sm font-semibold text-slate-600">ตั้งแต่:</label>
<input type="date" name="ds1" value="<?= htmlspecialchars($ds1) ?>" class="border border-slate-200 rounded px-3 py-1.5 text-sm focus:ring-emerald-500 focus:border-emerald-500 bg-white" required>
</div>
<div class="flex items-center gap-2">
<label class="text-sm font-semibold text-slate-600">ถึง:</label>
<input type="date" name="ds2" value="<?= htmlspecialchars($ds2) ?>" class="border border-slate-200 rounded px-3 py-1.5 text-sm focus:ring-emerald-500 focus:border-emerald-500 bg-white" required>
</div>
<button type="submit" class="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-1.5 rounded-md text-sm font-semibold transition-colors shadow-sm whitespace-nowrap">ประมวลผล (ค้นหา)</button>
</form>
</div>
<div class="overflow-y-auto overflow-x-hidden w-full p-4" style="max-height: calc(100vh - 250px);">
<table class="ncd-table w-full text-left text-sm" id="ncdTable">
<thead class="bg-slate-100">
<tr>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[50px]">ลำดับ</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[80px]">HN</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[150px]">ชื่อ-สกุล</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[140px]">CID</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[100px]">วันเกิด</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[50px]">อายุ</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[60px]">เพศ</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[180px]">ที่อยู่</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[50px]">หมู่</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[100px]">ตำบล</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[100px]">ประเภทบ้าน</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[100px]">เบอร์โทร</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[120px]">รหัสทะเบียน</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[120px]">วันที่ขึ้นทะเบียน</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[120px]">สิทธิการรักษา</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[150px]">โรงพยาบาล</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[100px]">Person ID</th>
<th class="py-3 pl-3 pr-8 font-semibold text-slate-700 text-center whitespace-nowrap min-w-[80px]">Diag</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50 border-l border-indigo-100">HbA1C</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[100px] bg-indigo-50">Urine Protein</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50">Cr</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[100px] bg-indigo-50">Urine Albumin</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[100px] bg-indigo-50">Urine Cr</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50">HBsAg</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50">Anti HCV</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50">HPV</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[100px] bg-indigo-50">มะเร็งลำไส้</th>
<th class="py-3 pl-3 pr-8 font-semibold text-indigo-700 text-center whitespace-nowrap min-w-[80px] bg-indigo-50">LDL</th>
<th class="py-3 pl-3 pr-8 font-semibold text-amber-700 text-center whitespace-nowrap min-w-[100px] bg-amber-50 border-l border-amber-100">ตรวจตา</th>
<th class="py-3 pl-3 pr-8 font-semibold text-amber-700 text-center whitespace-nowrap min-w-[100px] bg-amber-50">ตรวจเท้า</th>
<th class="py-3 pl-3 pr-8 font-semibold text-amber-700 text-center whitespace-nowrap min-w-[120px] bg-amber-50">มะเร็งช่องปาก</th>
</tr>
</thead>
<tbody>
<?php
// Helper functions for rendering cells and JSON mapping
$fmtLabObj = function($data) {
if (empty($data) || $data['res'] === '-') return '-';
return $data['res'] . ' (' . $data['date'] . ')';
};
if (!function_exists('fmtLabCell')) {
function fmtLabCell($data) {
if(empty($data) || $data['res'] === '-') return '<span class="text-slate-300">-</span>';
return '<span class="font-bold text-indigo-700">' . htmlspecialchars($data['res']) . '</span> <span class="text-[10px] text-slate-500 block leading-tight">(' . htmlspecialchars($data['date']) . ')</span>';
}
}
if (!function_exists('fmtScrCell')) {
function fmtScrCell($date) {
if(empty($date) || $date === '-') return '<span class="text-slate-300">-</span>';
return '<span class="font-medium text-amber-700">' . htmlspecialchars($date) . '</span>';
}
}
$no = 1;
foreach ($patientsData as $row):
$bg = $no % 2 == 0 ? 'bg-white' : 'bg-slate-50';
?>
<tr class="<?php echo $bg; ?> hover:bg-blue-50 border-b border-slate-100 table-row-item">
<td class="p-3 text-slate-500 text-center"><?php echo $no++; ?></td>
<td class="p-3 font-medium text-slate-700 hn-col"><?php echo htmlspecialchars($row['hn'] ?? ''); ?></td>
<td class="p-3 name-col">
<?php
$bday_formatted = '';
if (!empty($row['birthday']) && $row['birthday'] !== '0000-00-00') {
$time = strtotime($row['birthday']);
if ($time) {
$bday_formatted = date('d-m-', $time) . (date('Y', $time) + 543);
}
}
$ext = $row['extra'] ?? [];
$patient_json = htmlspecialchars(json_encode([
'hn' => $row['hn'] ?? '',
'name' => $row['pt_name'] ?? '',
'cid' => $row['cid'] ?? '',
'birthday' => $bday_formatted,
'age' => $row['age_y'] ?? '',
'sex' => $row['sex_name'] ?? '',
'address' => $row['addrpart'] ?? '',
'moo' => $row['moopart'] ?? '',
'tumbon' => $row['tmb_name'] ?? '',
'house_type' => $row['house_regist_type_id'] ?? '',
'phone' => $row['mobile_phone_number'] ?? '',
'member_id' => $row['clinicmember_id'] ?? '',
'clinic' => $row['clinic_name'] ?? $clinic_code_to_name[$row['clinic'] ?? ''] ?? $row['clinic'] ?? '-',
'regdate' => (!empty($row['regdate']) && $row['regdate'] != '0000-00-00') ? thai_date2($row['regdate']) : '-',
'dchdate' => (!empty($row['dchdate']) && $row['dchdate'] != '0000-00-00') ? thai_date2($row['dchdate']) : 'ยังไม่จำหน่าย',
'pttype' => $row['pttype_name'] ?? '',
'hospital' => $row['hosp_name'] ?? '',
'person_id' => $row['person_id'] ?? '',
'icd10' => $row['icd10'] ?? '',
'nextdate' => (!empty($row['nextdate']) && $row['nextdate'] != '0000-00-00') ? thai_date2($row['nextdate']) : 'ไม่มีนัด',
'nexttime' => (!empty($row['nexttime']) && $row['nexttime'] != '00:00:00') ? substr($row['nexttime'], 0, 5) . ' น.' : '-',
'labs' => [
'hba1c' => $fmtLabObj($ext['hba1c'] ?? null),
'urine_prot' => $fmtLabObj($ext['urine_prot'] ?? null),
'cr' => $fmtLabObj($ext['cr'] ?? null),
'urine_alb' => $fmtLabObj($ext['urine_alb'] ?? null),
'urine_cr' => $fmtLabObj($ext['urine_cr'] ?? null),
'hbsag' => $fmtLabObj($ext['hbsag'] ?? null),
'anti_hcv' => $fmtLabObj($ext['anti_hcv'] ?? null),
'hpv' => $fmtLabObj($ext['hpv'] ?? null),
'colon' => $fmtLabObj($ext['colon_cancer'] ?? null),
'ldl' => $fmtLabObj($ext['ldl'] ?? null)
],
'screen' => [
'eye' => $ext['eye_screen'] ?? '-',
'foot' => $ext['foot_screen'] ?? '-',
'oral' => $ext['oral_cancer'] ?? '-'
]
]), ENT_QUOTES, 'UTF-8');
?>
<button onclick="showPatientDetails(this)" data-patient="<?php echo $patient_json; ?>"
class="inline-flex items-center gap-3 group text-blue-600 hover:text-emerald-600 transition-colors cursor-pointer text-left focus:outline-none"
title="คลิกเพื่อดูรายละเอียดผู้ป่วย">
<div class="w-8 h-8 rounded-full bg-blue-50 text-blue-500 flex items-center justify-center shrink-0 group-hover:bg-emerald-50 group-hover:text-emerald-500 group-hover:scale-110 transition-all duration-300 shadow-sm border border-blue-100 group-hover:border-emerald-200">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
</div>
<span class="font-bold tracking-tight underline-offset-4 group-hover:underline">
<?php echo htmlspecialchars($row['pt_name'] ?? ''); ?>
</span>
</button>
</td>
<td class="p-3 text-slate-600 cid-col"><?php echo htmlspecialchars($row['cid'] ?? ''); ?></td>
<td class="p-3 text-slate-500 text-center"><?php echo htmlspecialchars($bday_formatted); ?></td>
<td class="p-3 text-slate-700 text-center"><?php echo htmlspecialchars($row['age_y'] ?? ''); ?></td>
<td class="p-3 text-slate-500 text-center"><?php echo htmlspecialchars($row['sex_name'] ?? ''); ?></td>
<td class="p-3 text-slate-500 truncate max-w-[180px]" title="<?php echo htmlspecialchars($row['addrpart'] ?? ''); ?>"><?php echo htmlspecialchars($row['addrpart'] ?? ''); ?></td>
<td class="p-3 text-slate-500 text-center"><?php echo htmlspecialchars($row['moopart'] ?? ''); ?></td>
<td class="p-3 text-slate-600"><?php echo htmlspecialchars($row['tmb_name'] ?? ''); ?></td>
<td class="p-3 text-slate-500 text-center"><?php echo htmlspecialchars($row['house_regist_type_id'] ?? ''); ?></td>
<td class="p-3 text-slate-600"><?php echo htmlspecialchars($row['mobile_phone_number'] ?? ''); ?></td>
<td class="p-3 text-slate-600"><?php echo htmlspecialchars($row['clinicmember_id'] ?? ''); ?></td>
<td class="p-3 text-slate-500 text-center"><?php echo htmlspecialchars($row['regdate'] ?? ''); ?></td>
<td class="p-3 text-slate-700"><?php echo htmlspecialchars($row['pttype_name'] ?? ''); ?></td>
<td class="p-3 text-slate-600"><?php echo htmlspecialchars($row['hosp_name'] ?? ''); ?></td>
<td class="p-3 text-slate-500"><?php echo htmlspecialchars($row['person_id'] ?? ''); ?></td>
<td class="p-3 font-bold text-slate-700 text-center"><?php echo htmlspecialchars($row['icd10'] ?? ''); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30 border-l border-indigo-100/50"><?php echo fmtLabCell($ext['hba1c'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['urine_prot'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['cr'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['urine_alb'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['urine_cr'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['hbsag'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['anti_hcv'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['hpv'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['colon_cancer'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-indigo-50/30"><?php echo fmtLabCell($ext['ldl'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-amber-50/30 border-l border-amber-100/50"><?php echo fmtScrCell($ext['eye_screen'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-amber-50/30"><?php echo fmtScrCell($ext['foot_screen'] ?? null); ?></td>
<td class="p-3 text-center whitespace-nowrap bg-amber-50/30"><?php echo fmtScrCell($ext['oral_cancer'] ?? null); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<div class="p-3 border-t border-slate-200 bg-slate-50 text-xs text-slate-500 flex justify-between items-center">
<span>ดึงข้อมูลจาก HOSxP Database (<?php echo count($patientsData); ?> รายการ)</span>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script src="../assets/js/micro-interactions.js"></script>
<script>
$(document).ready(function() {
// Disabled prototype alert since we are querying real DB now
// Swal.fire({
// icon: 'info',
// ...
// });
$('#ncdTable').DataTable({
"pageLength": 10,
"language": {
"lengthMenu": "แสดง _MENU_ รายการต่อหน้า",
"zeroRecords": "ไม่พบข้อมูล",
"info": "แสดงหน้า _PAGE_ จาก _PAGES_",
"infoEmpty": "ไม่มีข้อมูล",
"infoFiltered": "(กรองจากทั้งหมด _MAX_ รายการ)",
"search": "ค้นหา:",
"paginate": {
"first": "หน้าแรก",
"last": "หน้าสุดท้าย",
"next": "ถัดไป",
"previous": "ก่อนหน้า"
}
},
"dom": '<"flex flex-col sm:flex-row justify-between items-center mb-4"<"flex items-center gap-4"l><"flex items-center gap-2 text-sm"fB>>rt<"flex flex-col sm:flex-row justify-between items-center mt-4"<"text-sm text-slate-500"i><"text-sm"p>>',
"buttons": {
dom: {
button: {
className: ''
}
},
buttons: [
{
extend: 'excelHtml5',
text: '<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M15.8,20H14L12,16.6L10,20H8.2L11.1,15.5L8.2,11H10L12,14.4L14,11H15.8L12.9,15.5L15.8,20M13,9V3.5L18.5,9H13Z" /></svg>',
className: 'inline-flex items-center justify-center w-9 h-9 bg-gradient-to-r from-emerald-500 to-teal-600 rounded-lg text-white shadow-sm hover:shadow-md hover:from-emerald-600 hover:to-teal-700 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-1 transition-all duration-200 p-0 ml-2 border-0 cursor-pointer',
titleAttr: 'ส่งออก Excel',
title: 'NCD_Registry_Export'
}
]
},
"scrollX": true,
"fixedColumns": {
left: 3
},
"ordering": true,
"order": [[ 1, "asc" ]] // Order by HN (Column index 1)
});
});
</script>
<!-- Processing Modal -->
<div id="processingModal" class="fixed inset-0 z-[100] hidden items-center justify-center bg-slate-900/50 backdrop-blur-sm transition-all duration-300 opacity-0">
<div class="bg-white rounded-2xl shadow-2xl p-8 max-w-sm w-full mx-4 transform scale-95 transition-all duration-300 flex flex-col items-center border border-emerald-100">
<!-- Spinner -->
<div class="relative w-20 h-20 mb-6">
<div class="absolute inset-0 border-4 border-slate-100 rounded-full"></div>
<div class="absolute inset-0 border-4 border-emerald-500 rounded-full border-t-transparent animate-spin"></div>
<div class="absolute inset-0 flex items-center justify-center text-emerald-600">
<svg class="w-8 h-8 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg>
</div>
</div>
<h3 class="text-xl font-bold text-slate-800 mb-2">กำลังประมวลผลข้อมูล...</h3>
<p class="text-sm text-slate-500 text-center mb-6">กรุณารอสักครู่ ระบบกำลังดึงและรวบรวมข้อมูลผู้ป่วยจากฐานข้อมูล HOSxP</p>
<!-- Timer -->
<div class="bg-emerald-50/50 border border-emerald-100 rounded-xl px-6 py-4 w-full flex justify-center items-center gap-3">
<svg class="w-5 h-5 text-emerald-600 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span id="processingTimer" class="font-mono text-2xl font-bold text-emerald-700 tracking-wider">00:00</span>
</div>
</div>
</div>
<script>
let timerInterval;
let secondsElapsed = 0;
function showProcessingModal() {
const modal = document.getElementById('processingModal');
const timerDisplay = document.getElementById('processingTimer');
secondsElapsed = 0;
timerDisplay.textContent = '00:00';
modal.classList.remove('hidden');
modal.classList.add('flex');
// Trigger reflow
void modal.offsetWidth;
modal.classList.remove('opacity-0');
modal.querySelector('div').classList.remove('scale-95');
modal.querySelector('div').classList.add('scale-100');
timerInterval = setInterval(() => {
secondsElapsed++;
const mins = String(Math.floor(secondsElapsed / 60)).padStart(2, '0');
const secs = String(secondsElapsed % 60).padStart(2, '0');
timerDisplay.textContent = `${mins}:${secs}`;
}, 1000);
return true;
}
function showPatientDetails(btn) {
const data = JSON.parse(btn.getAttribute('data-patient'));
Swal.fire({
title: '<div class="text-2xl font-bold text-slate-800 flex items-center gap-3 justify-center"><div class="w-10 h-10 rounded-full bg-emerald-100 text-emerald-600 flex items-center justify-center shrink-0"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg></div>รายละเอียดผู้ป่วย NCD</div>',
html: `
<div class="mt-4 text-left space-y-4">
<div class="bg-slate-50 rounded-xl p-5 border border-slate-100 shadow-inner grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-4">
<!-- Section: ข้อมูลบุคคล -->
<div class="md:col-span-2 border-b border-slate-200 pb-2 mb-2">
<h4 class="font-bold text-slate-800 flex items-center gap-2"><svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg> ข้อมูลส่วนตัว</h4>
</div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">ชื่อ-สกุล</span><span class="text-sm font-bold text-emerald-700">${data.name}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">เลขบัตร ปชช.</span><span class="text-sm font-medium text-slate-700 tracking-wider">${data.cid}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">อายุ / เพศ</span><span class="text-sm font-medium text-slate-700">${data.age} ปี / ${data.sex} <span class="text-[11px] text-slate-400 font-normal ml-1 bg-white px-1.5 py-0.5 rounded border border-slate-200">เกิด ${data.birthday}</span></span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">เบอร์โทรติดต่อ</span><span class="text-sm font-medium text-slate-700">${data.phone || '-'}</span></div>
<div class="md:col-span-2 flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">ที่อยู่ปัจจุบัน</span><span class="text-sm font-medium text-slate-700">บ้านเลขที่ ${data.address} หมู่ ${data.moo} ต.${data.tumbon} <span class="text-[11px] bg-slate-200 text-slate-600 px-1.5 py-0.5 rounded">ประเภทบ้าน: ${data.house_type}</span></span></div>
<!-- Section: ข้อมูลคลินิกและการรักษา -->
<div class="md:col-span-2 border-b border-slate-200 pb-2 mt-2 mb-2">
<h4 class="font-bold text-slate-800 flex items-center gap-2"><svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path></svg> ข้อมูลการรับบริการ</h4>
</div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">HN / Person ID</span><span class="text-sm font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 w-max">${data.hn} / ${data.person_id}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">รหัสทะเบียน / Diag</span><span class="text-sm font-medium text-slate-700">${data.member_id} / <span class="font-bold text-slate-800">${data.icd10}</span></span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">คลินิกโรคเรื้อรัง</span><span class="text-sm font-bold text-slate-800">${data.clinic}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">สิทธิการรักษา / รพ.</span><span class="text-sm font-medium text-slate-700">${data.pttype} (${data.hospital})</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">วันที่ขึ้นทะเบียน</span><span class="text-sm font-medium text-slate-700">${data.regdate}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">สถานะจำหน่าย</span><span class="text-sm font-medium ${data.dchdate === 'ยังไม่จำหน่าย' ? 'text-emerald-600' : 'text-amber-600'}">${data.dchdate}</span></div>
<!-- Section: ผลทางห้องปฏิบัติการ (LAB) -->
<div class="md:col-span-2 border-b border-slate-200 pb-2 mt-4 mb-2">
<h4 class="font-bold text-slate-800 flex items-center gap-2"><svg class="w-4 h-4 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg> ผลทางห้องปฏิบัติการ (LAB ล่าสุด)</h4>
</div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">HbA1C</span><span class="text-sm font-bold text-indigo-700">${data.labs.hba1c}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">Urine Protein</span><span class="text-sm font-bold text-indigo-700">${data.labs.urine_prot}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">Cr</span><span class="text-sm font-bold text-indigo-700">${data.labs.cr}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">Urine Albumin</span><span class="text-sm font-bold text-indigo-700">${data.labs.urine_alb}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">Urine Cr</span><span class="text-sm font-bold text-indigo-700">${data.labs.urine_cr}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">HBsAg</span><span class="text-sm font-bold text-indigo-700">${data.labs.hbsag}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">Anti HCV</span><span class="text-sm font-bold text-indigo-700">${data.labs.anti_hcv}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">HPV</span><span class="text-sm font-bold text-indigo-700">${data.labs.hpv}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">มะเร็งลำไส้</span><span class="text-sm font-bold text-indigo-700">${data.labs.colon}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">LDL</span><span class="text-sm font-bold text-indigo-700">${data.labs.ldl}</span></div>
<!-- Section: การตรวจคัดกรอง -->
<div class="md:col-span-2 border-b border-slate-200 pb-2 mt-2 mb-2">
<h4 class="font-bold text-slate-800 flex items-center gap-2"><svg class="w-4 h-4 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path></svg> การตรวจคัดกรอง (ล่าสุด)</h4>
</div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">ตรวจตา</span><span class="text-sm font-bold text-amber-700">${data.screen.eye}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">ตรวจเท้า</span><span class="text-sm font-bold text-amber-700">${data.screen.foot}</span></div>
<div class="flex flex-col gap-1"><span class="text-xs font-semibold text-slate-500">มะเร็งช่องปาก</span><span class="text-sm font-bold text-amber-700">${data.screen.oral}</span></div>
</div>
</div>
`,
showCloseButton: true,
showConfirmButton: true,
confirmButtonText: 'ปิดหน้าต่าง',
confirmButtonColor: '#10b981',
customClass: {
container: 'backdrop-blur-sm',
popup: 'rounded-3xl shadow-2xl border border-slate-100 p-2',
title: 'border-b border-slate-100 pb-4',
closeButton: 'focus:outline-none hover:text-red-500 transition-colors',
confirmButton: 'rounded-xl px-6 py-2.5 font-bold shadow-sm'
},
width: '42rem'
});
}
</script>
</body>
</html>
+701
View File
@@ -0,0 +1,701 @@
<?php
// views/patient_detail.php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
$hn_raw = $_POST['hn'] ?? $_GET['hn'] ?? '';
$vdate_raw = $_POST['vdate'] ?? $_GET['vdate'] ?? '';
$nn_raw = $_POST['nn'] ?? $_GET['nn'] ?? '';
$hn = !empty($hn_raw) ? decrypt_param($hn_raw) : '';
$vdate = !empty($vdate_raw) ? decrypt_param($vdate_raw) : '';
$nn = !empty($nn_raw) ? decrypt_param($nn_raw) : '';
// Filters
$filter_type = $_POST['filter_type'] ?? $_GET['filter_type'] ?? 'all';
$start_date = $_POST['start_date'] ?? $_GET['start_date'] ?? '';
$end_date = $_POST['end_date'] ?? $_GET['end_date'] ?? '';
$filter_ipd = $_POST['filter_ipd'] ?? $_GET['filter_ipd'] ?? '';
// Fetch Patient Info
$patient_name = "ไม่พบข้อมูล";
$is_ipd_visit = false;
$upcoming_appointments = [];
if ($hn) {
$sql_pt = "SELECT * FROM patient WHERE hn=?";
$stmt = $conn1->prepare($sql_pt);
if($stmt) {
$stmt->bind_param("s", $hn);
$stmt->execute();
$res = $stmt->get_result();
if($row = $res->fetch_assoc()) {
$patient_name = $row['pname'] . $row['fname'] . " " . $row['lname'];
}
$stmt->close();
}
// Auto-select latest visit if $vdate is empty so user doesn't see a blank page
if (empty($vdate)) {
$sql_latest = "SELECT vstdate, vn, an FROM ovst WHERE hn=? ORDER BY vstdate DESC LIMIT 1";
$stmt_latest = $conn1->prepare($sql_latest);
if($stmt_latest) {
$stmt_latest->bind_param("s", $hn);
$stmt_latest->execute();
$res_latest = $stmt_latest->get_result();
if($row_latest = $res_latest->fetch_assoc()) {
$vdate = $row_latest['vstdate'];
$nn = ($row_latest['an'] != NULL) ? $row_latest['an'] : $row_latest['vn'];
}
$stmt_latest->close();
}
}
// Log that the patient was viewed
if (!empty($_SESSION['account'])) {
system_log($conn2, $_SESSION['account'], 'VIEW_PATIENT', ['hn' => $hn, 'vdate' => $vdate, 'nn' => $nn]);
}
// Check if current visit is IPD
if (!empty($vdate) && !empty($nn)) {
$sql_check_ipd = "SELECT an FROM ovst WHERE hn=? AND vstdate=? AND (vn=? OR an=?)";
$stmt_check = $conn1->prepare($sql_check_ipd);
if($stmt_check) {
$stmt_check->bind_param("ssss", $hn, $vdate, $nn, $nn);
$stmt_check->execute();
$res_check = $stmt_check->get_result();
if($row_check = $res_check->fetch_assoc()) {
if (!empty($row_check['an'])) {
$is_ipd_visit = true;
}
}
$stmt_check->close();
}
}
// Fetch Upcoming Appointments
$upcoming_appointments = [];
try {
$sql_app = "SELECT a.nextdate, a.nexttime, c.name as clinic_name
FROM oapp a
LEFT OUTER JOIN clinic c ON a.clinic = c.clinic
LEFT OUTER JOIN patient p ON a.hn = p.hn
WHERE DATEDIFF(a.nextdate, CURDATE()) > 1 AND a.hn = ?
ORDER BY a.nextdate ASC, a.nexttime ASC";
$stmt_app = $conn1->prepare($sql_app);
if($stmt_app) {
$stmt_app->bind_param("s", $hn);
$stmt_app->execute();
$res_app = $stmt_app->get_result();
while($row_app = $res_app->fetch_assoc()) {
$upcoming_appointments[] = $row_app;
}
$stmt_app->close();
}
} catch (Exception $e) {
// Log the error but don't break the page
error_log("Failed to fetch appointments: " . $e->getMessage());
}
// Fetch Vaccines
$vaccines_by_group = [];
try {
$app_settings = get_app_settings();
$vaccine_sql_template = $app_settings['vaccine_sql'] ?? '';
if (!empty($vaccine_sql_template)) {
// Protect against basic SQL injection while supporting the arbitrary query design
$safe_hn = $conn1->real_escape_string($hn);
$vaccine_sql = str_replace("'ระบุ_HN'", "'" . $safe_hn . "'", $vaccine_sql_template);
$res_vax = $conn1->query($vaccine_sql);
if ($res_vax) {
while($row_vax = $res_vax->fetch_assoc()) {
$group = $row_vax['vaccine_group'] ?? 'อื่นๆ (Others)';
if (!isset($vaccines_by_group[$group])) {
$vaccines_by_group[$group] = [];
}
$vaccines_by_group[$group][] = $row_vax;
}
}
}
} catch (Exception $e) {
error_log("Failed to fetch vaccines: " . $e->getMessage());
}
}
$bg_class = $is_ipd_visit ? "bg-gradient-to-br from-yellow-50 to-amber-100" : "bg-gradient-to-br from-emerald-50 to-emerald-100";
ob_start();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
</head>
<body class="<?php echo $bg_class; ?> text-slate-800 font-sans antialiased min-h-screen flex">
<?php require_once("../components/layout_sidebar.php"); ?>
<main class="flex-1 flex flex-col min-w-0">
<?php require_once("../components/layout_topbar.php"); ?>
<div class="p-4 md:p-8 lg:p-10 flex-1 overflow-y-auto">
<div class="max-w-screen-2xl mx-auto flex flex-col xl:flex-row gap-8 animate-enter">
<!-- Left: Side Panel -->
<div class="w-full xl:w-80 flex-shrink-0 flex flex-col gap-6">
<!-- Visit History List -->
<div class="glass-card flex flex-col h-[500px] max-h-[calc(100vh-16rem)]">
<div class="p-6 pb-4 border-b border-emerald-500/10">
<div class="flex justify-between items-start">
<div>
<h3 class="font-bold text-lg text-emerald-600">ประวัติรับบริการ</h3>
<p class="text-sm text-slate-500 mt-1">HN: <?php echo htmlspecialchars($hn); ?></p>
</div>
</div>
<!-- Filter Form -->
<form method="POST" action="patient_detail.php" class="mt-4 border-t border-emerald-500/10 pt-3">
<input type="hidden" name="hn" value="<?php echo htmlspecialchars(encrypt_param($hn)); ?>">
<input type="hidden" name="vdate" value="<?php echo htmlspecialchars(encrypt_param($vdate)); ?>">
<input type="hidden" name="nn" value="<?php echo htmlspecialchars(encrypt_param($nn)); ?>">
<div class="mb-2">
<label class="flex items-center gap-2 mb-2 text-xs text-slate-700 font-medium cursor-pointer hover:bg-slate-50 p-1.5 rounded-md transition-colors">
<input type="checkbox" name="filter_ipd" value="1" <?php echo !empty($filter_ipd) ? 'checked' : ''; ?> class="rounded text-emerald-500 focus:ring-emerald-500 border-slate-300" onchange="this.form.submit()">
<span>แสดงเฉพาะผู้ป่วยใน (IPD)</span>
</label>
<select name="filter_type" class="w-full text-xs px-2 py-1.5 border border-slate-200 rounded-md focus:ring-1 focus:ring-emerald-500 focus:border-emerald-500 bg-white text-slate-700 outline-none" onchange="if(this.value === 'custom') { document.getElementById('custom_date_range').classList.remove('hidden'); } else { document.getElementById('custom_date_range').classList.add('hidden'); this.form.submit(); }">
<option value="all" <?php echo $filter_type === 'all' ? 'selected' : ''; ?>>รายการล่าสุด (50 ครั้ง)</option>
<option value="1_year" <?php echo $filter_type === '1_year' ? 'selected' : ''; ?>>1 ปีย้อนหลัง</option>
<option value="3_years" <?php echo $filter_type === '3_years' ? 'selected' : ''; ?>>3 ปีย้อนหลัง</option>
<option value="5_years" <?php echo $filter_type === '5_years' ? 'selected' : ''; ?>>5 ปีย้อนหลัง</option>
<option value="all_time" <?php echo $filter_type === 'all_time' ? 'selected' : ''; ?>>ทั้งหมด (ประวัติทั้งหมด)</option>
<option value="custom" <?php echo $filter_type === 'custom' ? 'selected' : ''; ?>>เลือกช่วงวันที่...</option>
</select>
</div>
<div id="custom_date_range" class="<?php echo $filter_type === 'custom' ? '' : 'hidden'; ?> space-y-2">
<div class="flex gap-2">
<input type="date" name="start_date" value="<?php echo htmlspecialchars($start_date); ?>" class="w-1/2 text-[11px] px-2 py-1 border border-slate-200 rounded-md focus:ring-1 focus:ring-emerald-500 outline-none text-slate-600">
<input type="date" name="end_date" value="<?php echo htmlspecialchars($end_date); ?>" class="w-1/2 text-[11px] px-2 py-1 border border-slate-200 rounded-md focus:ring-1 focus:ring-emerald-500 outline-none text-slate-600">
</div>
<button type="submit" class="w-full text-xs bg-emerald-50 text-emerald-700 font-semibold py-1.5 rounded-md border border-emerald-200 hover:bg-emerald-100 transition-colors shadow-sm">
กรองข้อมูล
</button>
</div>
</form>
</div>
<ul class="overflow-y-auto flex-1 p-0 m-0 list-none divide-y divide-emerald-500/10 custom-scrollbar">
<?php
if($hn) {
$where_clause = "hn=?";
$params = [$hn];
$types = "s";
$limit_clause = "LIMIT 50";
if (!empty($filter_ipd)) {
$where_clause .= " AND an IS NOT NULL AND an != ''";
}
if ($filter_type === '1_year') {
$where_clause .= " AND vstdate >= DATE_SUB(CURDATE(), INTERVAL 1 YEAR)";
$limit_clause = "";
} else if ($filter_type === '3_years') {
$where_clause .= " AND vstdate >= DATE_SUB(CURDATE(), INTERVAL 3 YEAR)";
$limit_clause = "";
} else if ($filter_type === '5_years') {
$where_clause .= " AND vstdate >= DATE_SUB(CURDATE(), INTERVAL 5 YEAR)";
$limit_clause = "";
} else if ($filter_type === 'all_time') {
$limit_clause = "";
} else if ($filter_type === 'custom' && !empty($start_date) && !empty($end_date)) {
$where_clause .= " AND vstdate BETWEEN ? AND ?";
$params[] = $start_date;
$params[] = $end_date;
$types .= "ss";
$limit_clause = "";
}
$sql3 = "SELECT * FROM ovst WHERE $where_clause ORDER BY vstdate DESC $limit_clause";
$stmt3 = $conn1->prepare($sql3);
if($stmt3) {
$stmt3->bind_param($types, ...$params);
$stmt3->execute();
$res3 = $stmt3->get_result();
if ($res3->num_rows === 0) {
echo "<div class='text-center p-6 text-sm text-slate-400'>ไม่พบประวัติในช่วงเวลาที่เลือก</div>";
}
$all_visits = [];
$selected_visit = null;
while ($row3 = $res3->fetch_assoc()) {
$current_nn_val = ($row3['an'] != NULL) ? $row3['an'] : $row3['vn'];
if ($vdate == $row3['vstdate'] && $nn == $current_nn_val) {
$selected_visit = $row3;
} else {
$all_visits[] = $row3;
}
}
// Check if a visit was selected but not found in the list (e.g. it's older than the limit/filter)
if ($vdate && $nn && !$selected_visit) {
$sql_missing = "SELECT * FROM ovst WHERE hn=? AND vstdate=? AND (vn=? OR an=?)";
$stmt_m = $conn1->prepare($sql_missing);
if($stmt_m) {
$stmt_m->bind_param("ssss", $hn, $vdate, $nn, $nn);
$stmt_m->execute();
$res_m = $stmt_m->get_result();
if ($row_m = $res_m->fetch_assoc()) {
$selected_visit = $row_m;
}
$stmt_m->close();
}
}
// Render Selected Visit First (Pinned at Top)
if ($selected_visit) {
$nn_val = ($selected_visit['an'] != NULL) ? $selected_visit['an'] : $selected_visit['vn'];
$enc_hn = htmlspecialchars(encrypt_param($hn));
$enc_vdate = htmlspecialchars(encrypt_param($selected_visit['vstdate']));
$enc_nn = htmlspecialchars(encrypt_param($nn_val));
echo "<div class='p-3 bg-slate-50/95 sticky top-0 z-10 border-b border-emerald-500/10 backdrop-blur-xl'>";
echo "<form action='patient_detail.php' method='POST' class='m-0'>";
echo "<input type='hidden' name='hn' value='{$enc_hn}'>";
echo "<input type='hidden' name='vdate' value='{$enc_vdate}'>";
echo "<input type='hidden' name='nn' value='{$enc_nn}'>";
echo "<button type='button' class='w-full flex items-center justify-between p-4 bg-white border-2 border-emerald-400 rounded-xl shadow-[0_4px_15px_rgba(16,185,129,0.15)] ring-4 ring-emerald-500/10 transition-all cursor-default'>";
echo "<div class='flex flex-col items-start'>";
echo "<span class='text-xs font-semibold text-emerald-500 mb-0.5'>รายการที่กำลังดู</span>";
echo "<span class='text-base font-bold text-emerald-800'>" . thai_date2($selected_visit['vstdate']) . "</span>";
echo "</div>";
echo "<div class='flex gap-2'>";
if ($selected_visit['an'] != NULL) { echo '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-amber-100 text-amber-800 border border-amber-200">IPD</span>'; }
echo '<span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-bold bg-emerald-500 text-white shadow-sm">กำลังดูข้อมูล</span>';
echo "</div></button></form></div>";
}
// Render the rest of the visits
foreach ($all_visits as $row3) {
$nn_val = ($row3['an'] != NULL) ? $row3['an'] : $row3['vn'];
$enc_hn = htmlspecialchars(encrypt_param($hn));
$enc_vdate = htmlspecialchars(encrypt_param($row3['vstdate']));
$enc_nn = htmlspecialchars(encrypt_param($nn_val));
$filter_inputs = "<input type='hidden' name='filter_type' value='" . htmlspecialchars($filter_type) . "'>" .
"<input type='hidden' name='filter_ipd' value='" . htmlspecialchars($filter_ipd) . "'>" .
"<input type='hidden' name='start_date' value='" . htmlspecialchars($start_date) . "'>" .
"<input type='hidden' name='end_date' value='" . htmlspecialchars($end_date) . "'>";
echo "<form action='patient_detail.php' method='POST' class='m-0'>";
echo "<input type='hidden' name='hn' value='{$enc_hn}'>";
echo "<input type='hidden' name='vdate' value='{$enc_vdate}'>";
echo "<input type='hidden' name='nn' value='{$enc_nn}'>";
echo $filter_inputs;
echo "<button type='submit' class='w-full flex items-center justify-between p-4 transition-all duration-300 hover:bg-white/60 border-l-4 border-transparent hover:border-emerald-300'>";
echo "<span class='text-sm text-slate-600 font-medium'>" . thai_date2($row3['vstdate']) . "</span>";
echo "<div class='flex gap-2'>";
if ($row3['an'] != NULL) { echo '<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 text-amber-800">IPD</span>'; }
echo "</div></button></form>";
}
$stmt3->close();
}
}
?>
</ul>
</div>
<!-- Search History -->
<?php if($vdate && $nn): ?>
<div class="glass-card flex flex-col h-[380px]">
<div class="p-6 pb-4 border-b border-emerald-500/10">
<h3 class="font-bold text-lg text-emerald-600 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
ค้นหารายการ
</h3>
<div class="mt-3 relative">
<input type="text" id="historySearchInput" placeholder="พิมพ์ชื่อยา, แล็บ, เอกซเรย์..." class="w-full pl-10 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 transition-all">
<div class="absolute left-3 top-2.5 text-slate-400">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
</div>
<!-- Loader -->
<div id="historySearchLoader" class="absolute right-3 top-2.5 hidden">
<svg class="w-4 h-4 text-emerald-500 animate-spin" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto p-4 custom-scrollbar" id="historySearchResults">
<div class="text-center text-slate-400 text-sm py-8">
พิมพ์คำค้นหาเพื่อเริ่มค้นหา
</div>
</div>
</div>
<?php endif; ?>
</div>
<!-- Right: Patient Detail -->
<div class="flex-1 min-w-0 flex flex-col gap-8">
<?php if($hn && $vdate && $nn): ?>
<div class="flex flex-col gap-6">
<div class="glass-card relative z-50">
<div class="glass-header flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b-0">
<div>
<h2 class="text-2xl font-bold text-slate-800"><?php echo htmlspecialchars($patient_name); ?></h2>
<div class="flex flex-wrap gap-4 mt-2 text-sm text-slate-500 font-medium">
<span class="flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg> วันที่รับบริการ: <?php echo thai_date_full($vdate); ?></span>
<span class="flex items-center gap-1"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"></path></svg> VN/AN: <?php echo htmlspecialchars($nn); ?></span>
</div>
</div>
<div class="shrink-0 flex flex-wrap items-center gap-3">
<?php if (!empty($upcoming_appointments)): ?>
<div class="relative group">
<button class="inline-flex items-center gap-2 px-3 py-2 bg-amber-50 text-amber-600 rounded-lg border border-amber-200 hover:bg-amber-100 transition-colors shadow-sm focus:outline-none">
<svg class="w-5 h-5 animate-ring origin-top" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"></path></svg>
<span class="font-bold text-sm">นัดหมาย (<?php echo count($upcoming_appointments); ?>)</span>
</button>
<!-- Dropdown -->
<div class="absolute right-0 top-full pt-2 w-72 sm:w-80 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 transform origin-top-right scale-95 group-hover:scale-100">
<div class="bg-white rounded-xl shadow-xl border border-slate-100">
<div class="p-3 border-b border-slate-100 bg-amber-50/50 rounded-t-xl">
<h3 class="font-bold text-amber-800 flex items-center gap-2 text-sm">
<svg class="w-4 h-4 text-amber-500" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
รายการนัดหมายล่วงหน้า
</h3>
</div>
<div class="max-h-64 overflow-y-auto p-1.5 custom-scrollbar">
<?php foreach($upcoming_appointments as $app): ?>
<div class="p-3 hover:bg-slate-50 rounded-lg border-b border-slate-50 last:border-0 transition-colors">
<div class="text-sm font-bold text-slate-700 flex items-center gap-2 mb-1">
<svg class="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<?php echo $app['nextdate'] ? thai_date2($app['nextdate']) : 'ไม่ระบุวันที่'; ?>
</div>
<div class="flex items-center gap-3 mt-2">
<div class="text-xs text-slate-600 flex items-center gap-1">
<svg class="w-3.5 h-3.5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<?php echo $app['nexttime'] ? substr($app['nexttime'], 0, 5) . ' น.' : 'ไม่ระบุเวลา'; ?>
</div>
<div class="text-xs text-indigo-600 font-medium bg-indigo-50 px-2 py-0.5 rounded border border-indigo-100 flex-1 truncate">
<?php echo htmlspecialchars($app['clinic_name'] ?? 'ไม่ระบุคลินิก'); ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (!empty($vaccines_by_group)):
$total_vax = 0;
foreach($vaccines_by_group as $vax_list) { $total_vax += count($vax_list); }
?>
<div class="relative group">
<button class="inline-flex items-center gap-2 px-3 py-2 bg-pink-50 text-pink-600 rounded-lg border border-pink-200 hover:bg-pink-100 transition-colors shadow-sm focus:outline-none">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"></path></svg>
<span class="font-bold text-sm">วัคซีน (<?php echo $total_vax; ?>)</span>
</button>
<!-- Dropdown -->
<div class="absolute right-0 top-full pt-2 w-80 sm:w-96 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50 transform origin-top-right scale-95 group-hover:scale-100">
<div class="bg-white rounded-xl shadow-xl border border-slate-100">
<div class="p-3 border-b border-slate-100 bg-pink-50/50 rounded-t-xl">
<h3 class="font-bold text-pink-800 flex items-center gap-2 text-sm">
<svg class="w-4 h-4 text-pink-500" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
ประวัติการรับวัคซีน
</h3>
</div>
<div class="max-h-80 overflow-y-auto p-1.5 custom-scrollbar space-y-2">
<?php foreach($vaccines_by_group as $group_name => $vax_list): ?>
<div class="bg-slate-50 rounded-lg p-2">
<div class="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2 px-1 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-pink-400"></span>
<?php echo htmlspecialchars($group_name); ?>
</div>
<div class="space-y-1.5">
<?php foreach($vax_list as $vax): ?>
<div class="bg-white p-2.5 rounded border border-slate-100 shadow-sm hover:border-pink-200 transition-colors">
<div class="flex items-start justify-between gap-2">
<div class="text-sm font-bold text-slate-700 leading-tight">
<?php echo htmlspecialchars($vax['vaccine_name'] ?? '-'); ?>
</div>
<div class="text-[10px] font-medium px-1.5 py-0.5 rounded bg-slate-100 text-slate-600 whitespace-nowrap">
<?php echo htmlspecialchars($vax['vaccine_code'] ?? ''); ?>
</div>
</div>
<div class="flex flex-wrap items-center gap-2 mt-1.5">
<div class="text-[11px] text-slate-500 flex items-center gap-1">
<svg class="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path></svg>
<?php echo $vax['vaccine_date'] ? thai_date2($vax['vaccine_date']) : '-'; ?>
</div>
<?php if (!empty($vax['vaccine_lot_no'])): ?>
<div class="text-[11px] text-slate-500 flex items-center gap-1 border-l border-slate-200 pl-2">
<span class="font-semibold">Lot:</span> <?php echo htmlspecialchars($vax['vaccine_lot_no']); ?>
</div>
<?php endif; ?>
</div>
<?php if (!empty($vax['vaccine_place_name'])): ?>
<div class="text-[10px] text-slate-400 mt-1 flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
<?php echo htmlspecialchars($vax['vaccine_place_name']); ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<a href="search.php" class="inline-flex items-center gap-2 px-4 py-2 bg-emerald-50 text-emerald-600 rounded-lg hover:bg-emerald-100 hover:text-emerald-700 transition-all font-semibold shadow-sm border border-emerald-100 text-sm">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
ค้นหาคนอื่น
</a>
</div>
</div>
</div>
<div class="w-full">
<?php require_once("../components/card_patient_details.php"); ?>
</div>
</div>
<?php else: ?>
<div class="glass-card flex flex-col items-center justify-center min-h-[400px] text-center p-8">
<div class="w-20 h-20 bg-emerald-100 rounded-full flex items-center justify-center mb-6 text-emerald-500">
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</div>
<h3 class="text-xl font-bold text-slate-700 mb-2">ยังไม่ได้เลือกรายการรับบริการ</h3>
<p class="text-slate-500 max-w-sm mb-6">กรุณาคลิกเลือกวันที่มารับบริการจากเมนูด้านซ้ายเพื่อดูรายละเอียดการตรวจรักษาและผลการวินิจฉัยโรค</p>
<a href="search.php" class="inline-flex items-center gap-2 px-6 py-3 bg-emerald-500 text-white rounded-xl hover:bg-emerald-600 transition-all font-bold shadow-sm hover:shadow-md">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
ค้นหาผู้รับบริการคนอื่น
</a>
</div>
<?php endif; ?>
</div>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script src="../assets/js/micro-interactions.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const searchInput = document.getElementById('historySearchInput');
const resultsContainer = document.getElementById('historySearchResults');
const loader = document.getElementById('historySearchLoader');
// The encrypted HN is available in the PHP variable $enc_hn from the loop above,
// but we can just grab it from one of the hidden inputs in the page.
const hnInput = document.querySelector('input[name="hn"]');
const enc_hn = hnInput ? hnInput.value : '';
let debounceTimer;
if (searchInput) {
searchInput.addEventListener('input', function(e) {
clearTimeout(debounceTimer);
const keyword = e.target.value.trim();
if (keyword.length < 2) {
resultsContainer.innerHTML = '<div class="text-center text-slate-400 text-sm py-8">พิมพ์คำค้นหาเพื่อเริ่มค้นหา</div>';
loader.classList.add('hidden');
return;
}
loader.classList.remove('hidden');
debounceTimer = setTimeout(() => {
fetch('../api/search_history.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `hn=${encodeURIComponent(enc_hn)}&keyword=${encodeURIComponent(keyword)}`
})
.then(response => response.json())
.then(res => {
loader.classList.add('hidden');
if (res.status === 'success') {
renderResults(res.data);
} else {
resultsContainer.innerHTML = `<div class="text-center text-red-400 text-sm py-8">เกิดข้อผิดพลาด: ${res.message}</div>`;
}
})
.catch(err => {
loader.classList.add('hidden');
resultsContainer.innerHTML = '<div class="text-center text-red-400 text-sm py-8">ไม่สามารถเชื่อมต่อเซิร์ฟเวอร์ได้</div>';
});
}, 500);
});
}
function renderResults(data) {
if (!data || data.length === 0) {
resultsContainer.innerHTML = '<div class="text-center text-slate-400 text-sm py-8">ไม่พบข้อมูลที่ค้นหา</div>';
return;
}
let html = '<div class="space-y-2">';
data.forEach(item => {
const color = item.color; // e.g. indigo, purple, amber
html += `
<form action="patient_detail.php" method="POST" class="m-0">
<input type="hidden" name="hn" value="${enc_hn}">
<input type="hidden" name="vdate" value="${item.enc_vdate}">
<input type="hidden" name="nn" value="${item.enc_nn}">
<button type="submit" class="w-full text-left p-3 bg-white rounded-lg border border-slate-100 shadow-sm hover:border-${color}-300 hover:shadow-md transition-all group">
<div class="flex items-start justify-between gap-2 mb-1.5">
<span class="text-xs font-bold text-${color}-600 bg-${color}-50 px-2 py-0.5 rounded border border-${color}-100 shrink-0">
${item.type_label}
</span>
<span class="text-xs text-slate-400 font-medium whitespace-nowrap group-hover:text-${color}-500 transition-colors">
${item.vstdate_thai}
</span>
</div>
<div class="text-sm font-bold text-slate-700 leading-tight group-hover:text-${color}-700 transition-colors">
${item.item_name}
</div>
</button>
</form>
`;
});
html += '</div>';
resultsContainer.innerHTML = html;
}
});
window.openCheckupCompareModal = function() {
const modal = document.getElementById('checkupCompareModal');
const loader = document.getElementById('checkupCompareLoader');
const content = document.getElementById('checkupCompareContent');
const table = document.getElementById('checkupCompareTable');
modal.classList.remove('hidden');
loader.classList.remove('hidden');
content.classList.add('hidden');
const hnInput = document.querySelector('input[name="hn"]');
const enc_hn = hnInput ? hnInput.value : '';
const showAll = document.getElementById('showAllCheckups').checked;
fetch('../api/get_health_checkup_history.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `hn=${encodeURIComponent(enc_hn)}&show_all=${showAll}`
})
.then(res => res.json())
.then(data => {
loader.classList.add('hidden');
content.classList.remove('hidden');
if(data.status === 'success' && data.data && data.data.length > 0) {
renderCheckupCompareTable(data.data, table);
} else {
table.innerHTML = '<tr><td class="p-8 text-center text-slate-500">ไม่พบประวัติการตรวจสุขภาพย้อนหลัง</td></tr>';
}
})
.catch(err => {
loader.classList.add('hidden');
content.classList.remove('hidden');
table.innerHTML = '<tr><td class="p-8 text-center text-red-500">เกิดข้อผิดพลาดในการดึงข้อมูล</td></tr>';
});
};
window.closeCheckupCompareModal = function() {
document.getElementById('checkupCompareModal').classList.add('hidden');
};
function renderCheckupCompareTable(visits, table) {
if (visits.length === 0) return;
const allLabs = new Map();
visits.forEach(v => v.labs.forEach(l => {
if(!allLabs.has(l.lab_items_name)) {
allLabs.set(l.lab_items_name, l.lab_items_normal_value || '-');
}
}));
const allXrays = new Set();
visits.forEach(v => v.xrays.forEach(x => allXrays.add(x.item_name)));
let html = `<thead><tr class="bg-indigo-50/50">`;
html += `<th class="p-3 border-b border-indigo-100 text-indigo-900 font-bold sticky left-0 top-0 bg-indigo-50/95 backdrop-blur z-30 w-56 shadow-[1px_1px_0_rgba(224,231,255,1)]">รายการตรวจ</th>`;
visits.forEach((v, index) => {
html += `<th class="p-3 border-b border-indigo-100 text-center min-w-[150px] sticky top-0 bg-indigo-50/95 backdrop-blur z-20 shadow-[0_1px_0_rgba(224,231,255,1)]">
<div class="text-indigo-600 font-bold">${v.vstdate_thai}</div>`;
if(index < visits.length - 1 && v.gap_days > 0) {
html += `<div class="text-[10px] text-slate-500 font-bold mt-1 bg-white rounded-full px-2 py-0.5 inline-flex items-center gap-1 shadow-sm border border-slate-100 whitespace-nowrap"><svg class="w-3 h-3 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> ${v.gap_text}</div>`;
} else if (index === visits.length - 1) {
html += `<div class="text-[10px] text-slate-400 font-medium mt-1">ครั้งแรกสุด</div>`;
}
html += `</th>`;
});
html += `</tr></thead><tbody>`;
if(allLabs.size > 0) {
html += `<tr><td colspan="${visits.length + 1}" class="p-2 bg-slate-50 font-bold text-slate-600 text-sm border-b border-slate-100 sticky left-0 z-10">🔬 ผลแล็บ (Lab)</td></tr>`;
Array.from(allLabs.entries()).sort((a, b) => a[0].localeCompare(b[0])).forEach(([labName, normalVal]) => {
html += `<tr class="hover:bg-slate-50 transition-colors">`;
html += `<td class="p-3 border-b border-slate-100 text-sm text-slate-700 font-medium sticky left-0 bg-white z-10 shadow-[1px_0_0_rgba(241,245,249,1)] group-hover:bg-slate-50">
${labName}
<div class="text-[10px] text-slate-400 font-normal mt-0.5">Normal: ${normalVal}</div>
</td>`;
visits.forEach(v => {
const labFound = v.labs.find(l => l.lab_items_name === labName);
if (labFound) {
if (labFound.is_abnormal) {
html += `<td class="p-3 border-b border-slate-100 text-center text-sm font-bold text-rose-600 bg-rose-50/50">${labFound.lab_order_result}</td>`;
} else {
html += `<td class="p-3 border-b border-slate-100 text-center text-sm font-bold text-emerald-600 bg-emerald-50/10">${labFound.lab_order_result}</td>`;
}
} else {
html += `<td class="p-3 border-b border-slate-100 text-center text-sm text-slate-300">-</td>`;
}
});
html += `</tr>`;
});
}
if(allXrays.size > 0) {
html += `<tr><td colspan="${visits.length + 1}" class="p-2 bg-slate-50 font-bold text-slate-600 text-sm border-b border-slate-100 sticky left-0 z-10 mt-2">🩻 ผลเอกซเรย์ (X-Ray)</td></tr>`;
Array.from(allXrays).sort().forEach(xrayName => {
html += `<tr class="hover:bg-slate-50 transition-colors">`;
html += `<td class="p-3 border-b border-slate-100 text-sm text-slate-700 font-medium sticky left-0 bg-white z-10 shadow-[1px_0_0_rgba(241,245,249,1)] group-hover:bg-slate-50">${xrayName}</td>`;
visits.forEach(v => {
const xrayFound = v.xrays.find(x => x.item_name === xrayName);
if (xrayFound) {
html += `<td class="p-3 border-b border-slate-100 text-center text-xs text-slate-600 bg-amber-50/10" title="${xrayFound.result}">${xrayFound.result ? "มีผลอ่าน" : "รอผล"}</td>`;
} else {
html += `<td class="p-3 border-b border-slate-100 text-center text-sm text-slate-300">-</td>`;
}
});
html += `</tr>`;
});
}
html += `</tbody>`;
table.innerHTML = html;
}
</script>
</body>
</html>
<?php
$html = ob_get_clean();
if ($is_ipd_visit) {
// Replace all tailwind emerald classes with amber classes
$html = preg_replace('/\b(bg|text|border|ring|shadow|from|to|via|fill|stroke)-emerald-([0-9]{2,3}(?:\/[0-9]{1,3})?)\b/', '$1-amber-$2', $html);
// Replace the specific rgba emerald green (16, 185, 129) with amber-500 (245, 158, 11) for box-shadows and scrollbars
$html = preg_replace('/rgba\(\s*16\s*,\s*185\s*,\s*129/i', 'rgba(245, 158, 11', $html);
}
echo $html;
?>
+197
View File
@@ -0,0 +1,197 @@
<?php
// views/search.php
require_once("../core/auth.php");
require_once("../config/db.php");
require_once("../core/utils.php");
$current_user = $_SESSION['account'] ?? '';
$can_search_by_name = can_search_name($current_user);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<?php require_once("../components/head.php"); ?>
<meta name="csrf-token" content="<?php echo generate_csrf_token(); ?>">
<!-- jQuery & DataTables -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<style>
/* Custom CSS to make DataTables look good with Tailwind */
.dataTables_wrapper .dataTables_length select, .dataTables_wrapper .dataTables_filter input {
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
outline: none;
margin-bottom: 0.5rem;
}
.dataTables_wrapper .dataTables_length select:focus, .dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981;
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
border-radius: 0.5rem !important;
padding: 0.25rem 0.75rem !important;
margin: 0 0.125rem !important;
border: 1px solid transparent !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
background: #10b981 !important;
color: white !important;
border: none !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button:hover:not(.current) {
background: #f1f5f9 !important;
color: #334155 !important;
}
table.dataTable.no-footer {
border-bottom: 1px solid #f1f5f9;
}
table.dataTable thead th {
border-bottom: 1px solid #f1f5f9;
}
.dt-center {
text-align: center;
}
</style>
</head>
<body class="bg-gradient-to-br from-emerald-50 to-emerald-100 text-slate-800 font-sans antialiased min-h-screen flex">
<?php require_once("../components/layout_sidebar.php"); ?>
<main class="flex-1 flex flex-col min-w-0">
<?php require_once("../components/layout_topbar.php"); ?>
<div class="p-4 md:p-8 lg:p-10 flex-1 overflow-y-auto">
<div class="max-w-5xl mx-auto">
<div class="glass-card p-5 md:p-8 animate-enter">
<div class="mb-8">
<h2 class="text-2xl font-bold flex items-center gap-3 text-slate-800">
<svg class="w-8 h-8 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path></svg>
ค้นหาเวชระเบียนผู้รับบริการ
</h2>
<p class="text-slate-500 mt-2">กรุณาเลือกประเภทการค้นหา และระบุข้อมูลที่ต้องการ</p>
</div>
<form id="searchForm" class="flex flex-wrap gap-4 items-end mb-8">
<div class="min-w-[200px]">
<label class="block text-sm font-medium text-slate-700 mb-2">ประเภทการค้นหา</label>
<select id="searchType" class="form-input appearance-none bg-white">
<option value="hn">เลขที่เวชระเบียน (HN)</option>
<option value="cid">เลขบัตรประชาชน (CID)</option>
<option value="passport">เลขพาสปอร์ต (Passport)</option>
<?php if($can_search_by_name): ?>
<option value="name">ชื่อ - สกุล</option>
<?php endif; ?>
</select>
</div>
<div class="flex-1 min-w-[250px]">
<label class="block text-sm font-medium text-slate-700 mb-2">ระบุข้อมูลค้นหา</label>
<input type="text" id="searchQuery" class="form-input" placeholder="พิมพ์คำค้นหาที่นี่..." required>
</div>
<button type="submit" class="btn-primary w-full sm:w-auto ripple-wrapper h-[46px] px-8 sm:mb-[2px]">
ค้นหา
</button>
</form>
<div class="overflow-x-auto bg-white/50 rounded-2xl border border-white/60 p-4">
<table id="searchResultTable" class="w-full text-left border-collapse">
<thead>
<tr>
<th class="table-header w-20 text-center">ลำดับ</th>
<th class="table-header w-40 text-center">HN</th>
<th class="table-header text-center">ชื่อ - สกุล</th>
<th class="table-header w-48 text-center">เลขประจำตัว</th>
<th class="table-header w-32 !text-center">จัดการ</th>
</tr>
</thead>
<tbody id="resultBody">
<tr>
<td colspan="5" class="py-16 text-center">
<div class="flex flex-col items-center justify-center text-slate-400">
<svg class="w-12 h-12 mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16l2.879-2.879m0 0a3 3 0 104.243-4.242 3 3 0 00-4.243 4.242zM21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<span>ผลการค้นหาจะแสดงที่นี่</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<?php require_once("../components/layout_footer.php"); ?>
</main>
<script src="../assets/js/micro-interactions.js"></script>
<script>
// Change placeholder dynamically based on search type
document.getElementById('searchType').addEventListener('change', function(e) {
const input = document.getElementById('searchQuery');
if (e.target.value === 'name') {
input.placeholder = "พิมพ์ชื่อ หรือ ชื่อ สกุลเว้นวรรค (เช่น สมชาย หรือ สมชาย ใจดี)...";
} else {
input.placeholder = "พิมพ์คำค้นหาที่นี่...";
}
});
document.getElementById('searchForm').addEventListener('submit', function(e) {
e.preventDefault();
const type = document.getElementById('searchType').value;
const query = document.getElementById('searchQuery').value;
const resultBody = document.getElementById('resultBody');
resultBody.innerHTML = '<tr><td colspan="5" class="py-16 text-center text-emerald-600 font-medium animate-pulse">กำลังค้นหาข้อมูล...</td></tr>';
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
fetch('../core/search_action.php', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `type=${encodeURIComponent(type)}&query=${encodeURIComponent(query)}&csrf_token=${encodeURIComponent(csrfToken)}`
})
.then(response => response.text())
.then(html => {
// Destroy existing DataTable if it exists
if ($.fn.DataTable.isDataTable('#searchResultTable')) {
$('#searchResultTable').DataTable().destroy();
}
resultBody.innerHTML = html;
// Only initialize DataTable if there are results (not showing error message)
if (html.indexOf('py-16 text-center') === -1) {
let dt = $('#searchResultTable').DataTable({
"pageLength": 10,
"lengthChange": false,
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
"columnDefs": [
{ "className": "dt-center", "targets": "_all" },
{ "searchable": false, "orderable": false, "targets": [0, 4] }
],
"order": [[ 2, "asc" ]] // Sort by Name A-Z by default (column index 2)
});
// Re-calculate sequence numbers on sort or filter
dt.on('order.dt search.dt', function () {
let i = 1;
dt.cells(null, 0, { search: 'applied', order: 'applied' }).every(function (cell) {
this.data(i++);
});
}).draw();
}
})
.catch(error => {
console.error('Error:', error);
resultBody.innerHTML = '<tr><td colspan="5" class="py-16 text-center text-red-500 font-medium">เกิดข้อผิดพลาดในการเชื่อมต่อ</td></tr>';
});
});
</script>
</body>
</html>