702 lines
50 KiB
PHP
702 lines
50 KiB
PHP
<?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;
|
|
?>
|