Initial commit

This commit is contained in:
Porawit Dongwang
2026-09-16 23:20:08 +07:00
commit 0041668dbb
32577 changed files with 3687927 additions and 0 deletions
@@ -0,0 +1,226 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
checkAdmin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>จัดการผู้ดูแลระบบ (Admins) - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
/* Custom DataTables Styling */
.dataTables_wrapper { padding: 1rem 0; }
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.35rem 0.75rem; margin-left: 0.5rem; outline: none; transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_length select {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.25rem 1.5rem 0.25rem 0.75rem; margin: 0 0.5rem; outline: none;
}
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_length {
margin-bottom: 1rem;
}
table.dataTable.no-footer { border-bottom: 1px solid #e5e7eb; margin-top: 1rem !important; margin-bottom: 1rem !important; }
table.dataTable thead th {
border-bottom: 2px solid #e5e7eb; background-color: #f3f4f6; color: #374151; font-weight: 600; padding: 0.75rem 1rem; white-space: nowrap;
}
table.dataTable tbody td {
border-bottom: 1px solid #f3f4f6; padding: 1rem; color: #4b5563; vertical-align: top;
}
table.dataTable tbody tr:hover { background-color: #f9fafb; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.35rem 0.85rem; margin-left: 0.25rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: white; color: #374151 !important; transition: all 0.2s; font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: #10b981 !important; color: white !important; border-color: #10b981; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover {
background: #f3f4f6 !important; border-color: #d1d5db; color: #111827 !important;
}
.dataTables_wrapper .dataTables_info { color: #6b7280; font-size: 0.875rem; padding-top: 0.85rem; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div class="mb-4">
<a href="settings.php" class="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
กลับไปหน้าตั้งค่าระบบ
</a>
</div>
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">จัดการผู้ดูแลระบบ (System Admins)</h3>
<p class="mt-1 text-sm text-gray-500">เพิ่ม/ลบ สิทธิ์ผู้ดูแลระบบด้วยเลขบัตรประชาชน (CID) หรือรหัสผู้ใช้งาน</p>
</div>
<button onclick="openModal()" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
เพิ่มผู้ดูแลระบบ
</button>
</div>
<div class="px-6 py-5 overflow-x-auto">
<table id="adminsTable" class="display w-full text-sm">
<thead>
<tr>
<th class="w-10">ID</th>
<th>เลขบัตรประชาชน (CID)</th>
<th>ชื่อ-นามสกุล</th>
<th>วันที่เพิ่ม</th>
<th class="w-24">จัดการ</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!-- Modal Form -->
<div id="adminModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
<form id="adminForm">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4" id="modal-title">เพิ่มผู้ดูแลระบบ</h3>
<input type="hidden" id="admin_id" name="id">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="cid">เลขบัตรประชาชน (13 หลัก)</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="cid" name="cid" type="text" maxlength="13" required placeholder="x-xxxx-xxxxx-xx-x">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="name">ชื่อ-นามสกุล</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="name" name="name" type="text" required placeholder="เช่น นพ.ทดสอบ ระบบดี">
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
บันทึกข้อมูล
</button>
<button type="button" onclick="closeModal()" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
ยกเลิก
</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
<!-- jQuery, DataTables, and SweetAlert2 -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
let table;
$(document).ready(function() {
table = $('#adminsTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
"ajax": "api_admins.php?action=list",
"columns": [
{ "data": "id" },
{ "data": "cid" },
{ "data": "name" },
{ "data": "created_at" },
{ "data": null, "render": function(data, type, row) {
return `
<div class="flex items-center">
<button onclick="deleteAdmin(${row.id})" class="text-red-500 hover:text-red-700 hover:bg-red-50 p-1.5 rounded-md transition-colors" title="ลบสิทธิ์">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
`;
}}
],
"order": [[ 0, "asc" ]]
});
$('#adminForm').on('submit', function(e) {
e.preventDefault();
$.ajax({
url: 'api_admins.php?action=save',
type: 'POST',
data: $(this).serialize(),
success: function(res) {
if (res.status === 'success') {
Swal.fire('สำเร็จ', res.message, 'success');
closeModal();
table.ajax.reload();
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
}
});
});
});
function openModal() {
$('#adminForm')[0].reset();
$('#admin_id').val('');
$('#modal-title').text('เพิ่มผู้ดูแลระบบ');
$('#adminModal').removeClass('hidden');
}
function closeModal() {
$('#adminModal').addClass('hidden');
}
function deleteAdmin(id) {
Swal.fire({
title: 'ยืนยันการลบสิทธิ์?',
text: "ผู้ใช้งานนี้จะไม่สามารถเข้าถึงเมนูตั้งค่าได้อีกต่อไป",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'ใช่, ลบเลย!',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
$.post('api_admins.php?action=delete', { id: id }, function(res) {
if (res.status === 'success') {
Swal.fire('ลบสำเร็จ!', res.message, 'success');
table.ajax.reload();
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
});
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,67 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
checkAdmin();
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$action = $_REQUEST['action'] ?? '';
try {
if ($action === 'list') {
$stmt = $log_db->query("SELECT * FROM system_admins ORDER BY id DESC");
$admins = $stmt->fetchAll();
foreach ($admins as &$admin) {
$admin['created_at'] = formatThaiDate($admin['created_at']);
}
echo json_encode(['data' => $admins]);
}
elseif ($action === 'save') {
$cid = trim($_POST['cid'] ?? '');
$name = trim($_POST['name'] ?? '');
if (empty($cid) || empty($name)) {
echo json_encode(['status' => 'error', 'message' => 'กรุณากรอกข้อมูลให้ครบถ้วน']);
exit;
}
// Check if CID already exists
$stmt = $log_db->prepare("SELECT id FROM system_admins WHERE cid = ?");
$stmt->execute([$cid]);
if ($stmt->fetch()) {
echo json_encode(['status' => 'error', 'message' => 'เลขบัตรประชาชนนี้ถูกเพิ่มเป็นผู้ดูแลระบบไปแล้ว']);
exit;
}
// Insert
$stmt = $log_db->prepare("INSERT INTO system_admins (cid, name) VALUES (?, ?)");
$stmt->execute([$cid, $name]);
addSystemLog('ADD_ADMIN', "เพิ่มผู้ดูแลระบบ: $name ($cid)");
echo json_encode(['status' => 'success', 'message' => 'เพิ่มผู้ดูแลระบบสำเร็จ']);
}
elseif ($action === 'delete') {
$id = $_POST['id'] ?? 0;
// Get name before delete
$stmt = $log_db->prepare("SELECT name, cid FROM system_admins WHERE id = ?");
$stmt->execute([$id]);
$admin = $stmt->fetch();
if ($admin) {
$stmt = $log_db->prepare("DELETE FROM system_admins WHERE id = ?");
$stmt->execute([$id]);
addSystemLog('DELETE_ADMIN', "ลบสิทธิ์ผู้ดูแลระบบ: {$admin['name']} ({$admin['cid']})");
}
echo json_encode(['status' => 'success', 'message' => 'ลบสิทธิ์ผู้ดูแลระบบสำเร็จ']);
}
else {
echo json_encode(['status' => 'error', 'message' => 'Unknown action']);
}
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'Database Error: ' . $e->getMessage()]);
}
@@ -0,0 +1,117 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
checkAdmin();
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$action = $_REQUEST['action'] ?? '';
try {
if ($action === 'list') {
// ดึงชุดข้อมูลทั้งหมดมาก่อน
$stmt = $log_db->query("SELECT * FROM export_profiles ORDER BY id DESC");
$profiles = $stmt->fetchAll();
// รวบรวม CID ของผู้สร้างทั้งหมดที่ไม่ซ้ำกัน
$cids = [];
foreach ($profiles as $profile) {
if (!empty($profile['created_by'])) {
$cids[] = $profile['created_by'];
}
}
// ถ้ามีผู้สร้าง ให้ไปดึงชื่อจากฐานข้อมูล HR (เพื่อป้องกันปัญหา Cross-Database Join และ Permission)
$creator_names = [];
if (!empty($cids)) {
$cids = array_unique($cids);
$inQuery = implode(',', array_fill(0, count($cids), '?'));
try {
$hr_db = getDB(DB_HR_HOST, DB_HR_NAME, DB_HR_USER, DB_HR_PASS);
$hr_stmt = $hr_db->prepare("
SELECT a.HR_CID, CONCAT(e.HR_PREFIX_NAME, a.HR_FNAME, ' ', a.HR_LNAME) as full_name
FROM hr_person a
LEFT JOIN hr_prefix e ON a.HR_PREFIX_ID = e.HR_PREFIX_ID
WHERE a.HR_CID IN ($inQuery)
");
$hr_stmt->execute(array_values($cids));
while ($row = $hr_stmt->fetch()) {
$creator_names[$row['HR_CID']] = $row['full_name'];
}
} catch (Exception $ex) {
// ถ้าเชื่อมต่อ HR_DB ไม่ได้หรือ query ผิดพลาด ให้ข้ามไป (ปล่อยให้ชื่อเป็นว่างๆ)
error_log("Failed to fetch creator names: " . $ex->getMessage());
}
}
foreach ($profiles as &$profile) {
$cid = $profile['created_by'];
$profile['creator_name'] = !empty($cid) && isset($creator_names[$cid]) ? $creator_names[$cid] : null;
$profile['created_at'] = formatThaiDate($profile['created_at']);
}
echo json_encode(['data' => $profiles]);
}
elseif ($action === 'get') {
$id = $_GET['id'] ?? 0;
$stmt = $log_db->prepare("SELECT * FROM export_profiles WHERE id = ?");
$stmt->execute([$id]);
$profile = $stmt->fetch();
echo json_encode(['status' => 'success', 'data' => $profile]);
}
elseif ($action === 'save') {
$id = $_POST['id'] ?? '';
$profile_name = trim($_POST['profile_name'] ?? '');
$excel_columns = trim($_POST['excel_columns'] ?? '');
$sql_query = trim($_POST['sql_query'] ?? '');
if (empty($profile_name) || empty($excel_columns) || empty($sql_query)) {
echo json_encode(['status' => 'error', 'message' => 'กรุณากรอกข้อมูลให้ครบถ้วน']);
exit;
}
if (empty($id)) {
// Insert
$cid = $_SESSION['cid'] ?? null;
$stmt = $log_db->prepare("INSERT INTO export_profiles (profile_name, excel_columns, sql_query, created_by) VALUES (?, ?, ?, ?)");
$stmt->execute([$profile_name, $excel_columns, $sql_query, $cid]);
addSystemLog('CREATE_PROFILE', "สร้างชุดข้อมูลใหม่: $profile_name");
echo json_encode(['status' => 'success', 'message' => 'บันทึกชุดข้อมูลสำเร็จ']);
} else {
// Update
$stmt = $log_db->prepare("UPDATE export_profiles SET profile_name = ?, excel_columns = ?, sql_query = ? WHERE id = ?");
$stmt->execute([$profile_name, $excel_columns, $sql_query, $id]);
addSystemLog('EDIT_PROFILE', "แก้ไขชุดข้อมูล: $profile_name (ID: $id)");
echo json_encode(['status' => 'success', 'message' => 'แก้ไขชุดข้อมูลสำเร็จ']);
}
}
elseif ($action === 'delete') {
$id = $_POST['id'] ?? 0;
// Get name before delete
$stmt = $log_db->prepare("SELECT profile_name FROM export_profiles WHERE id = ?");
$stmt->execute([$id]);
$profile = $stmt->fetch();
if ($profile) {
$stmt = $log_db->prepare("DELETE FROM export_profiles WHERE id = ?");
$stmt->execute([$id]);
addSystemLog('DELETE_PROFILE', "ลบชุดข้อมูล: {$profile['profile_name']} (ID: $id)");
}
echo json_encode(['status' => 'success', 'message' => 'ลบชุดข้อมูลสำเร็จ']);
}
else {
echo json_encode(['status' => 'error', 'message' => 'Unknown action']);
}
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'Database Error: ' . $e->getMessage()]);
}
@@ -0,0 +1,25 @@
<?php
require_once 'config.php';
header('Content-Type: application/json');
checkAdmin();
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$action = $_REQUEST['action'] ?? '';
try {
if ($action === 'list') {
$stmt = $log_db->query("SELECT * FROM system_logs ORDER BY id DESC");
$logs = $stmt->fetchAll();
foreach ($logs as &$log) {
$log['created_at'] = formatThaiDate($log['created_at']);
}
echo json_encode(['data' => $logs]);
}
else {
echo json_encode(['status' => 'error', 'message' => 'Unknown action']);
}
} catch (Exception $e) {
echo json_encode(['status' => 'error', 'message' => 'Database Error: ' . $e->getMessage()]);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

@@ -0,0 +1,8 @@
{
"name": "antigravity/hosxp-export",
"description": "HOSxP Excel import and validation system",
"require": {
"php": ">=8.1",
"phpoffice/phpspreadsheet": "^1.29"
}
}
@@ -0,0 +1,131 @@
<?php
session_start();
// การตั้งค่าเชื่อมต่อฐานข้อมูล HOSxP (his)
define('DB_HIS_HOST', 'localhost');
define('DB_HIS_NAME', 'his');
define('DB_HIS_USER', 'root');
define('DB_HIS_PASS', '');
// การตั้งค่าเชื่อมต่อฐานข้อมูล HR (สำหรับ Login)
define('DB_HR_HOST', 'localhost');
define('DB_HR_NAME', 'hr_db');
define('DB_HR_USER', 'root');
define('DB_HR_PASS', '');
// การตั้งค่าเชื่อมต่อฐานข้อมูลประวัติ (hosxp_export_person)
define('DB_LOG_HOST', 'localhost');
define('DB_LOG_NAME', 'hosxp_export_person');
define('DB_LOG_USER', 'root');
define('DB_LOG_PASS', '');
function getDB($host, $name, $user, $pass) {
try {
$dsn = "mysql:host=$host;dbname=$name;charset=utf8mb4";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
return new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
throw $e;
}
}
// ตรวจสอบการ Login
function checkLogin() {
if (!isset($_SESSION['loggedin']) || $_SESSION['loggedin'] !== true) {
header("Location: login.php");
exit;
}
}
// ตรวจสอบสิทธิ์ผู้ดูแลระบบ (Admin)
function checkAdmin() {
checkLogin();
if (!isset($_SESSION['is_admin']) || $_SESSION['is_admin'] !== true) {
// Not an admin, redirect to index
header("Location: index.php");
exit;
}
}
// แปลงวันที่ให้อยู่ในรูปแบบ วัน-เดือน-ปี(พ.ศ.)
function formatThaiDate($datetime) {
if (empty($datetime)) return '';
$timestamp = strtotime($datetime);
if (!$timestamp) return '';
$d = date('d', $timestamp);
$m = date('m', $timestamp);
$y = date('Y', $timestamp) + 543;
// Check if it has time
if (date('H:i:s', $timestamp) !== '00:00:00') {
$time = date('H:i', $timestamp);
return "$d-$m-$y $time น.";
}
return "$d-$m-$y";
}
// บันทึกประวัติการใช้งานระบบ (System Logs)
function addSystemLog($action_type, $details = '') {
try {
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$cid = isset($_SESSION['cid']) ? substr(str_replace('-', '', $_SESSION['cid']), 0, 13) : null;
$name = isset($_SESSION['fullname']) ? mb_substr($_SESSION['fullname'], 0, 100) : null;
$ip = isset($_SERVER['REMOTE_ADDR']) ? substr($_SERVER['REMOTE_ADDR'], 0, 45) : null;
$stmt = $log_db->prepare("INSERT INTO system_logs (action_type, user_cid, user_name, details, ip_address) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$action_type, $cid, $name, $details, $ip]);
} catch (Exception $e) {
// บันทึก error ลงไฟล์ error.log เพื่อให้ตรวจสอบได้ง่ายขึ้น
error_log("addSystemLog Error: " . $e->getMessage());
}
}
// ==========================================
// System Settings Functions
// ==========================================
function getSystemSetting($key, $default = '') {
try {
$db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$stmt = $db->prepare("SELECT setting_value FROM general_settings WHERE setting_key = ?");
$stmt->execute([$key]);
$row = $stmt->fetch();
return $row ? $row['setting_value'] : $default;
} catch (PDOException $e) {
return $default;
}
}
function setSystemSetting($key, $value) {
try {
$db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$stmt = $db->prepare("INSERT INTO general_settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
return $stmt->execute([$key, $value]);
} catch (PDOException $e) {
return false;
}
}
// Auto-create and initialize general_settings table if it doesn't exist
try {
$db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$db->exec("CREATE TABLE IF NOT EXISTS general_settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;");
// Insert defaults if not exists
$db->exec("INSERT IGNORE INTO general_settings (setting_key, setting_value) VALUES
('topbar_text', 'HOSxP Mapping Data'),
('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย')
;");
} catch (PDOException $e) {
// Ignore error if DB connection fails during config load
}
@@ -0,0 +1,56 @@
-- สำหรับฐานข้อมูล HR (ตัวอย่าง)
CREATE DATABASE IF NOT EXISTS hr_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hr_db;
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, -- ควรเป็น hash ของรหัสผ่าน (เช่น password_hash)
name VARCHAR(100) NOT NULL
);
-- สร้างผู้ใช้เริ่มต้น (รหัสผ่านคือ 'password' hash ด้วย bcrypt)
INSERT IGNORE INTO users (username, password, name) VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'ผู้ดูแลระบบ');
-- สำหรับฐานข้อมูล hosxp_export_person (ประวัติการทำงาน)
CREATE DATABASE IF NOT EXISTS hosxp_export_person DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE hosxp_export_person;
CREATE TABLE IF NOT EXISTS system_admins (
id INT AUTO_INCREMENT PRIMARY KEY,
cid VARCHAR(13) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS general_settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS export_profiles (
id INT AUTO_INCREMENT PRIMARY KEY,
profile_name VARCHAR(100) NOT NULL,
excel_columns VARCHAR(255) NOT NULL, -- e.g. "pid, hn"
sql_query TEXT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS export_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
profile_id INT NULL,
file_name VARCHAR(255) NOT NULL,
total_rows INT NOT NULL,
found_rows INT NOT NULL,
upload_datetime DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (profile_id) REFERENCES export_profiles(id) ON DELETE SET NULL
);
-- เพิ่มข้อมูลตัวอย่าง
INSERT IGNORE INTO export_profiles (id, profile_name, excel_columns, sql_query) VALUES (
1,
'ค้นหาด้วย PID เบื้องต้น',
'pid',
'SELECT p.person_id, p.patient_hn, p.cid, concat(p.pname, p.fname, '' '', p.lname) as nme, pt.birthday, pt.hometel, pt.mobile_phone_number FROM person p LEFT OUTER JOIN patient pt ON p.patient_hn = pt.hn WHERE p.person_id = :pid'
);
@@ -0,0 +1,71 @@
<?php
require_once 'config.php';
if (file_exists('vendor/autoload.php')) {
require 'vendor/autoload.php';
} else {
die('ไม่พบ Library PhpSpreadsheet');
}
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
checkLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['export_data'])) {
$data = json_decode($_POST['export_data'], true);
if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
die("Invalid data format for export.");
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Write headers dynamically based on data keys
$colIndex = 1; // 1 = A
$firstRow = $data[0];
foreach ($firstRow as $key => $value) {
$cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . '1';
$sheet->setCellValue($cellCoordinate, strtoupper($key));
$sheet->getStyle($cellCoordinate)->getFont()->setBold(true);
$colIndex++;
}
// Write data rows
$rowNum = 2;
foreach ($data as $row) {
$colIndex = 1;
foreach ($row as $key => $value) {
$cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowNum;
$sheet->setCellValueExplicit($cellCoordinate, $value, \PhpOffice\PhpSpreadsheet\Cell\DataType::TYPE_STRING);
$colIndex++;
}
$rowNum++;
}
// Auto-size columns
$maxCol = count($firstRow);
for ($i = 1; $i <= $maxCol; $i++) {
$colStr = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($i);
$sheet->getColumnDimension($colStr)->setAutoSize(true);
}
// Output file
$fileName = 'HOSxP_Export_' . date('Ymd_His') . '.xlsx';
if (!empty($_POST['original_filename'])) {
$original = $_POST['original_filename'];
$info = pathinfo($original);
$basename = $info['filename'];
$fileName = $basename . '_ตรวจสอบแล้ว.xlsx';
}
header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
header('Content-Disposition: attachment;filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
$writer = new Xlsx($spreadsheet);
$writer->save('php://output');
exit;
} else {
die("No data to export.");
}
@@ -0,0 +1,17 @@
<?php
// footer.php
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
?>
<!-- Global Footer -->
<footer class="bg-white border-t border-gray-200 mt-auto py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex-1"></div>
<div class="flex-1 text-center text-gray-600 text-sm">
&copy; <?php echo date('Y') + 543; ?> <?php echo htmlspecialchars($footer_text); ?>
</div>
<div class="flex-1 flex justify-end items-center text-gray-500 text-xs">
<span class="mr-2">Powered By น้องมะลิ</span>
<img src="assets/img/mali.jpg" alt="น้องมะลิ" class="h-8 w-8 rounded-full object-cover border border-gray-300" onerror="this.src='https://ui-avatars.com/api/?name=มะลิ&background=fdf4ff&color=c026d3';">
</div>
</div>
</footer>
@@ -0,0 +1,86 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
checkAdmin();
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['topbar_text']) && isset($_POST['footer_text'])) {
$topbar = trim($_POST['topbar_text']);
$footer = trim($_POST['footer_text']);
$success1 = setSystemSetting('topbar_text', $topbar);
$success2 = setSystemSetting('footer_text', $footer);
if ($success1 && $success2) {
$message = '<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">บันทึกการตั้งค่าสำเร็จ</div>';
// Add system log
addSystemLog('UPDATE_SETTINGS', 'อัปเดตการตั้งค่าทั่วไป (Topbar / Footer)');
} else {
$message = '<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">เกิดข้อผิดพลาดในการบันทึกการตั้งค่า</div>';
}
}
}
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ตั้งค่าทั่วไป - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; display: flex; flex-direction: column; min-height: 100vh; }
.main-content { flex: 1; }
</style>
</head>
<body class="bg-gray-50">
<?php include 'topbar.php'; ?>
<div class="main-content max-w-7xl mx-auto py-10 px-4 sm:px-6 lg:px-8 w-full">
<div class="flex items-center mb-6">
<a href="settings.php" class="text-gray-500 hover:text-gray-900 mr-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
</a>
<h2 class="text-2xl font-bold text-gray-900">ตั้งค่าทั่วไป (General Settings)</h2>
</div>
<?php echo $message; ?>
<div class="bg-white shadow rounded-lg p-6 max-w-3xl">
<form method="POST" action="">
<div class="mb-6">
<label for="topbar_text" class="block text-sm font-medium text-gray-700 mb-2">ข้อความแถบเมนูด้านบน (Topbar)</label>
<input type="text" name="topbar_text" id="topbar_text" required value="<?php echo htmlspecialchars($topbar_text); ?>"
class="block w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">ข้อความนี้จะแสดงอยู่ข้างๆ โลโก้ในแถบเมนูสีเขียวด้านบนของทุกหน้า</p>
</div>
<div class="mb-8">
<label for="footer_text" class="block text-sm font-medium text-gray-700 mb-2">ข้อความลิขสิทธิ์ด้านล่างสุด (Footer)</label>
<input type="text" name="footer_text" id="footer_text" required value="<?php echo htmlspecialchars($footer_text); ?>"
class="block w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">ข้อความนี้จะแสดงอยู่ส่วนท้ายสุดของเว็บ โดยมีคำว่า &copy; ตามด้วยปีปัจจุบันนำหน้า</p>
</div>
<div class="flex items-center justify-end">
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-6 rounded focus:outline-none focus:shadow-outline transition duration-150">
บันทึกการตั้งค่า
</button>
</div>
</form>
</div>
</div>
<?php include 'footer.php'; ?>
</body>
</html>
@@ -0,0 +1,136 @@
<?php
require_once 'config.php';
checkLogin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
// Fetch history
try {
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$stmt = $log_db->query("SELECT l.*, p.profile_name FROM export_logs l LEFT JOIN export_profiles p ON l.profile_id = p.id ORDER BY l.upload_datetime DESC");
$logs = $stmt->fetchAll();
} catch (Exception $e) {
$logs = [];
$error = "ไม่สามารถเชื่อมต่อฐานข้อมูลประวัติได้ (อาจจะยังไม่ได้สร้างตาราง)";
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ประวัติการทำงาน - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- DataTables CSS -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
/* Custom DataTables Styling */
.dataTables_wrapper { padding: 1rem 0; }
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.35rem 0.75rem; margin-left: 0.5rem; outline: none; transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_length select {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.25rem 1.5rem 0.25rem 0.75rem; margin: 0 0.5rem; outline: none;
}
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_length {
margin-bottom: 1rem;
}
table.dataTable.no-footer { border-bottom: 1px solid #e5e7eb; margin-top: 1rem !important; margin-bottom: 1rem !important; }
table.dataTable thead th {
border-bottom: 2px solid #e5e7eb; background-color: #f3f4f6; color: #374151; font-weight: 600; padding: 0.75rem 1rem; white-space: nowrap; text-align: center;
}
table.dataTable tbody td {
border-bottom: 1px solid #f3f4f6; padding: 1rem; color: #4b5563; vertical-align: top;
}
table.dataTable tbody tr:hover { background-color: #f9fafb; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.35rem 0.85rem; margin-left: 0.25rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: white; color: #374151 !important; transition: all 0.2s; font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: #10b981 !important; color: white !important; border-color: #10b981; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover {
background: #f3f4f6 !important; border-color: #d1d5db; color: #111827 !important;
}
.dataTables_wrapper .dataTables_info { color: #6b7280; font-size: 0.875rem; padding-top: 0.85rem; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<!-- Top Navigation -->
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-5 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">ประวัติการทำงาน</h3>
<p class="mt-1 text-sm text-gray-500">แสดงประวัติการนำเข้าไฟล์และค้นหาข้อมูลทั้งหมด</p>
</div>
<div class="px-6 py-5 overflow-x-auto">
<?php if(isset($error)): ?>
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4 text-sm" role="alert">
<span class="block sm:inline"><?php echo htmlspecialchars($error); ?></span>
</div>
<?php else: ?>
<table id="historyTable" class="display w-full text-sm">
<thead>
<tr>
<th>ลำดับ</th>
<th>ชุดข้อมูล</th>
<th>ผู้ใช้งาน</th>
<th>ชื่อไฟล์</th>
<th>จำนวนที่นำเข้า</th>
<th>จำนวนที่พบ</th>
<th>วัน-เวลาที่อัปโหลด</th>
</tr>
</thead>
<tbody>
<?php foreach($logs as $log): ?>
<tr>
<td class="text-center align-middle"><?php echo htmlspecialchars($log['id']); ?></td>
<td class="align-middle"><?php echo htmlspecialchars($log['profile_name'] ?? 'ไม่มีข้อมูล'); ?></td>
<td class="text-center align-middle"><?php echo htmlspecialchars($log['username']); ?></td>
<td class="align-middle"><?php echo htmlspecialchars($log['file_name']); ?></td>
<td class="text-center align-middle"><?php echo htmlspecialchars($log['total_rows']); ?></td>
<td class="text-center align-middle"><?php echo htmlspecialchars($log['found_rows']); ?></td>
<td class="text-center align-middle">
<?php echo htmlspecialchars(formatThaiDate($log['upload_datetime'])); ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
<!-- jQuery and DataTables -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
$('#historyTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
"order": [[ 6, "desc" ]] // เรียงตามวันที่ล่าสุด (คอลัมน์ที่ 7, index 6)
});
});
</script>
</body>
</html>
@@ -0,0 +1,261 @@
<?php
require_once 'config.php';
checkLogin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
// Fetch available profiles
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$stmt = $log_db->query("SELECT id, profile_name FROM export_profiles ORDER BY profile_name ASC");
$profiles = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>นำเข้าและตรวจสอบข้อมูล - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600&display=swap" rel="stylesheet">
<!-- DataTables CSS -->
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
/* Custom DataTables Styling */
.dataTables_wrapper { padding: 1rem 0; }
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.35rem 0.75rem; margin-left: 0.5rem; outline: none; transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_length select {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.25rem 1.5rem 0.25rem 0.75rem; margin: 0 0.5rem; outline: none;
}
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_length {
margin-bottom: 1rem;
}
table.dataTable.no-footer { border-bottom: 1px solid #e5e7eb; margin-top: 1rem !important; margin-bottom: 1rem !important; }
table.dataTable thead th {
border-bottom: 2px solid #e5e7eb; background-color: #f3f4f6; color: #374151; font-weight: 600; padding: 0.75rem 1rem; white-space: nowrap; text-align: center;
}
table.dataTable tbody td {
border-bottom: 1px solid #f3f4f6; padding: 1rem; color: #4b5563; vertical-align: top;
}
table.dataTable tbody tr:hover { background-color: #f9fafb; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.35rem 0.85rem; margin-left: 0.25rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: white; color: #374151 !important; transition: all 0.2s; font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: #10b981 !important; color: white !important; border-color: #10b981; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover {
background: #f3f4f6 !important; border-color: #d1d5db; color: #111827 !important;
}
.dataTables_wrapper .dataTables_info { color: #6b7280; font-size: 0.875rem; padding-top: 0.85rem; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<!-- Upload Card -->
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-5 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">นำเข้าไฟล์ Excel เพื่อตรวจสอบ</h3>
<p class="mt-1 text-sm text-gray-500">เลือกชุดข้อมูลและไฟล์ Excel ที่ต้องการตรวจสอบกับฐานข้อมูล HOSxP</p>
</div>
<div class="px-6 py-5">
<form id="uploadForm" enctype="multipart/form-data">
<div class="flex flex-col md:flex-row items-end gap-4">
<div class="w-full md:w-1/2">
<label class="block text-sm font-medium text-gray-700 mb-1">ชุดข้อมูล (Profile)</label>
<select name="profile_id" id="profile_id" required class="block w-full border border-gray-300 rounded-md py-2 px-3 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm bg-white h-[42px]">
<option value="">-- เลือกชุดข้อมูล --</option>
<?php foreach($profiles as $p): ?>
<option value="<?php echo htmlspecialchars($p['id']); ?>"><?php echo htmlspecialchars($p['profile_name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="w-full md:flex-grow">
<label class="block text-sm font-medium text-gray-700 mb-1">ไฟล์ Excel</label>
<input type="file" id="excel_file" name="excel_file" accept=".xlsx, .xls" required class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:border-0 file:text-sm file:font-semibold file:bg-green-50 file:text-green-700 hover:file:bg-green-100 border border-gray-300 rounded-md bg-white h-[42px]">
</div>
<div class="w-full md:w-auto flex-shrink-0">
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition duration-150 h-[42px] whitespace-nowrap" id="btnUpload">
อัปโหลดและประมวลผล
</button>
</div>
</div>
</form>
<div id="loadingIndicator" class="hidden mt-4 text-center text-blue-600">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 inline" 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>
กำลังประมวลผลข้อมูล...
</div>
<div id="uploadMessage" class="mt-4 text-sm hidden"></div>
</div>
</div>
<!-- Results Card -->
<div id="resultsCard" class="bg-white shadow rounded-lg hidden">
<div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-medium text-gray-900">ผลการตรวจสอบ</h3>
<form action="export.php" method="POST" id="exportForm">
<input type="hidden" name="export_data" id="exportData">
<input type="hidden" name="original_filename" id="originalFilename">
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline transition duration-150 flex items-center text-sm">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
ส่งออกเป็น Excel
</button>
</form>
</div>
<div class="px-6 py-5 overflow-x-auto">
<table id="resultsTable" class="display w-full text-sm">
<thead id="resultsHead">
<tr>
<!-- Headers will be generated dynamically -->
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
<!-- jQuery, DataTables, and SweetAlert2 -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
let table;
$(document).ready(function() {
// DataTable will be initialized after upload is complete
$('#uploadForm').on('submit', function(e) {
e.preventDefault();
var formData = new FormData(this);
$('#btnUpload').prop('disabled', true).addClass('opacity-50');
$('#loadingIndicator').removeClass('hidden');
$('#uploadMessage').addClass('hidden');
$('#resultsCard').addClass('hidden');
$.ajax({
url: 'process.php',
type: 'POST',
data: formData,
success: function(response) {
$('#btnUpload').prop('disabled', false).removeClass('opacity-50');
$('#loadingIndicator').addClass('hidden');
try {
var res = typeof response === 'string' ? JSON.parse(response) : response;
if (res.status === 'success') {
Swal.fire({
icon: 'success',
title: 'ประมวลผลสำเร็จ',
text: res.message,
confirmButtonColor: '#10b981'
});
if(res.data && res.data.length > 0) {
// Destroy old table
if ($.fn.DataTable.isDataTable('#resultsTable')) {
table.destroy();
$('#resultsTable').empty();
}
// Generate Columns from data keys
let columns = [];
let firstRow = res.data[0];
for (let key in firstRow) {
columns.push({
title: key.toUpperCase(),
data: key,
className: 'whitespace-nowrap'
});
}
// Re-initialize DataTable with dynamic columns
table = $('#resultsTable').DataTable({
data: res.data,
columns: columns,
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
destroy: true
});
$('#resultsCard').removeClass('hidden');
// Store data for export
$('#exportData').val(JSON.stringify(res.data));
// Store original filename for export
var originalName = $('#excel_file').prop('files')[0].name;
$('#originalFilename').val(originalName);
} else {
Swal.fire({
icon: 'warning',
title: 'ประมวลผลเสร็จสิ้น',
text: 'แต่ไม่พบข้อมูลที่ตรงกันในฐานข้อมูล HOSxP',
confirmButtonColor: '#f59e0b'
});
}
} else {
Swal.fire({
icon: 'error',
title: 'เกิดข้อผิดพลาด',
text: res.message,
confirmButtonColor: '#ef4444'
});
}
} catch (error) {
Swal.fire({
icon: 'error',
title: 'ระบบขัดข้อง',
text: 'เกิดข้อผิดพลาดในการประมวลผล (Invalid JSON)',
confirmButtonColor: '#ef4444'
});
console.error(error, response);
}
},
error: function(jqXHR, textStatus, errorThrown) {
$('#btnUpload').prop('disabled', false).removeClass('opacity-50');
$('#loadingIndicator').addClass('hidden');
var errorMsg = 'เกิดข้อผิดพลาดในการส่งข้อมูล (HTTP ' + jqXHR.status + ' ' + errorThrown + ')';
Swal.fire({
icon: 'error',
title: 'การเชื่อมต่อผิดพลาด',
html: errorMsg + (jqXHR.responseText ? `<br><span class="text-xs text-red-500">${jqXHR.responseText}</span>` : ''),
confirmButtonColor: '#ef4444'
});
},
cache: false,
contentType: false,
processData: false
});
});
});
</script>
</body>
</html>
@@ -0,0 +1,158 @@
<?php
require_once 'config.php';
if (isset($_SESSION['username'])) {
header("Location: index.php");
exit;
}
$topbar_text = getSystemSetting('topbar_text', 'HOSxP Mapping Data');
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = trim($_POST['username'] ?? '');
$pass = trim($_POST['password'] ?? '');
if (empty($user) || empty($pass)) {
$error = "กรุณากรอก Username และ Password";
} else {
$pass_md5 = MD5($pass);
$sql = "SELECT HR_CID, HR_PASSWORD, HR_STATUS_ID, HR_PREFIX_NAME, HR_FNAME, HR_LNAME
FROM hr_person a
LEFT OUTER JOIN hr_prefix e ON a.HR_PREFIX_ID=e.HR_PREFIX_ID
WHERE a.HR_USERNAME = :user LIMIT 1";
try {
$pdo_hos = getDB(DB_HR_HOST, DB_HR_NAME, DB_HR_USER, DB_HR_PASS);
$stmt = $pdo_hos->prepare($sql);
$stmt->execute(['user' => $user]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) {
$error = "ไม่พบบัญชีผู้ใช้นี้";
} elseif (strtolower($row['HR_PASSWORD']) !== strtolower($pass_md5)) {
$error = "รหัสผ่านไม่ถูกต้อง";
} elseif ($row['HR_STATUS_ID'] !== '01') {
$error = "บัญชีผู้ใช้นี้ถูกปิดการใช้งานไปแล้ว";
} else {
$_SESSION['loggedin'] = true;
$_SESSION['fullname'] = $row['HR_PREFIX_NAME'] . $row['HR_FNAME'] . " " . $row['HR_LNAME'];
$_SESSION['cid'] = $row['HR_CID'];
// Check if user is an admin
$_SESSION['is_admin'] = false;
try {
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$admin_stmt = $log_db->prepare("SELECT id FROM system_admins WHERE cid = ?");
$admin_stmt->execute([$row['HR_CID']]);
if ($admin_stmt->fetch()) {
$_SESSION['is_admin'] = true;
}
} catch (Exception $ex) {
// Ignore DB error, just default to non-admin
}
// Add system log
addSystemLog('LOGIN', 'เข้าสู่ระบบสำเร็จ');
header("Location: index.php");
exit;
}
} catch (PDOException $e) {
$error = "เกิดข้อผิดพลาดของระบบ: " . $e->getMessage();
}
}
}
?>
<!DOCTYPE html>
<html lang="th" class="antialiased">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>เข้าสู่ระบบ - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
.glass-panel {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
@media (prefers-color-scheme: dark) {
.glass-panel {
background: rgba(30, 41, 59, 0.85);
border: 1px solid rgba(255, 255, 255, 0.1);
}
}
.animate-fade-in {
animation: fadeIn 0.8s ease-out forwards;
opacity: 0;
transform: translateY(10px);
}
@keyframes fadeIn {
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body class="bg-gradient-to-br from-emerald-50 via-teal-100 to-emerald-200 dark:from-gray-900 dark:via-teal-900 dark:to-gray-800 min-h-screen flex flex-col items-center justify-center p-4">
<div class="w-full max-w-md animate-fade-in">
<div class="glass-panel p-8 sm:p-10 rounded-[20px] shadow-2xl relative overflow-hidden">
<!-- Decorative circle -->
<div class="absolute -top-10 -right-10 w-32 h-32 bg-emerald-400 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
<div class="text-center mb-8 relative">
<div class="inline-block p-2 rounded-full bg-white dark:bg-gray-800 shadow-lg mb-4 transform hover:scale-105 transition-transform duration-300">
<img src="assets/img/logo.png" alt="Logo" class="w-24 h-24 object-contain rounded-full" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMTA5NzZkIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTEyIDJMMiA3bDEwIDUgMTAtNS0xMC01ek0yIDE3bDEwIDUgMTAtNU0yIDEybDEwIDUgMTAtNSIvPjwvc3ZnPg=='">
</div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-2 tracking-tight">เข้าสู่ระบบ</h2>
<p class="text-sm text-gray-500 dark:text-gray-400 font-medium"><?php echo htmlspecialchars($topbar_text); ?></p>
</div>
<?php if($error): ?>
<div class="bg-red-50 dark:bg-red-900/30 border-l-4 border-red-500 text-red-700 dark:text-red-400 p-4 rounded-r-lg mb-6 text-sm flex items-start shadow-sm" role="alert">
<svg class="w-5 h-5 mr-3 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path></svg>
<span><?php echo htmlspecialchars($error); ?></span>
</div>
<?php endif; ?>
<form method="POST" action="" class="space-y-6 relative">
<div>
<label for="username" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">ชื่อผู้ใช้งาน</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-emerald-500 transition-colors">
<svg class="h-5 w-5" 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>
<input type="text" id="username" name="username" class="block w-full pl-10 pr-4 py-3 border border-gray-200 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm dark:text-white sm:text-sm transition-all shadow-sm hover:bg-white dark:hover:bg-gray-800" placeholder="Username" required autofocus>
</div>
</div>
<div>
<label for="password" class="block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">รหัสผ่าน</label>
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-gray-400 group-focus-within:text-emerald-500 transition-colors">
<svg class="h-5 w-5" 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>
</div>
<input type="password" id="password" name="password" class="block w-full pl-10 pr-4 py-3 border border-gray-200 dark:border-gray-600 rounded-xl focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 bg-white/70 dark:bg-gray-800/70 backdrop-blur-sm dark:text-white sm:text-sm transition-all shadow-sm hover:bg-white dark:hover:bg-gray-800" placeholder="Password" required>
</div>
</div>
<button type="submit" class="w-full flex justify-center py-3 px-4 border border-transparent rounded-xl shadow-lg text-sm font-bold text-white bg-emerald-600 hover:bg-emerald-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-emerald-500 transform transition-all duration-200 hover:-translate-y-0.5 active:translate-y-0">
เข้าสู่ระบบ
</button>
</form>
</div>
<div class="text-center mt-8 animate-fade-in" style="animation-delay: 0.2s;">
<p class="text-sm font-medium text-emerald-800/70 dark:text-gray-400/80">
&copy; <?php echo date('Y') + 543; ?> <?php echo htmlspecialchars($topbar_text); ?>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,11 @@
<?php
require_once 'config.php';
session_start();
// Add system log before clearing session
addSystemLog('LOGOUT', 'ออกจากระบบ');
session_unset();
session_destroy();
header("Location: login.php");
exit;
@@ -0,0 +1,173 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
checkLogin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>คู่มือการใช้งาน - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
.step-card:hover { transform: translateY(-5px); transition: transform 0.3s ease; }
</style>
</head>
<body class="bg-gray-50 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-3xl font-bold text-gray-900 mb-4">คู่มือการใช้งาน <?php echo htmlspecialchars($topbar_text); ?></h1>
<p class="text-lg text-gray-600 max-w-3xl mx-auto">ยินดีต้อนรับเข้าสู่ระบบ <?php echo htmlspecialchars($topbar_text); ?> ด้านล่างนี้คือขั้นตอนการใช้งานระบบตั้งแต่การเตรียมไฟล์ไปจนถึงการส่งออกข้อมูลที่ประมวลผลแล้ว</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Step 1 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-green-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-green-100 rounded-full text-green-600 font-bold text-xl">1</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">การเข้าสู่ระบบ (Login)</h3>
<p class="text-gray-600 text-sm leading-relaxed">
ระบบนี้เชื่อมต่อกับฐานข้อมูลบุคลากร (HR) ของโรงพยาบาล คุณสามารถใช้ <b>ชื่อผู้ใช้งาน (Username)</b> และ <b>รหัสผ่าน (Password)</b> เดียวกันกับระบบ HR เพื่อเข้าใช้งานได้ทันที หากบัญชีถูกปิดการใช้งาน จะไม่สามารถเข้าสู่ระบบได้
</p>
</div>
</div>
</div>
<!-- Step 2 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-blue-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-blue-100 rounded-full text-blue-600 font-bold text-xl">2</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">การเตรียมไฟล์ Excel</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-3">
ไฟล์ที่จะนำเข้าต้องเป็นไฟล์ Excel นามสกุล <b>.xlsx</b> หรือ <b>.xls</b> เท่านั้น และมีข้อกำหนดสำคัญคือ:
</p>
<ul class="list-disc list-inside text-sm text-gray-600 space-y-1">
<li>ต้องมีคอลัมน์ที่ชื่อว่า <code class="bg-gray-100 px-2 py-1 rounded text-red-500 font-mono">pid</code> (พิมพ์เล็กหรือพิมพ์ใหญ่ก็ได้) อยู่ในแถวแรกของไฟล์</li>
<li>ข้อมูลรหัสบัตรประชาชน (หรือ Person ID) จะต้องอยู่ใต้คอลัมน์นี้</li>
<li>สามารถมีคอลัมน์อื่นๆ ปะปนอยู่ด้วยได้ ระบบจะดึงเฉพาะคอลัมน์ pid มาใช้งาน</li>
</ul>
</div>
</div>
</div>
<!-- Step 3 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-purple-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-purple-100 rounded-full text-purple-600 font-bold text-xl">3</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">การนำเข้าและประมวลผล</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-3">
ที่เมนู <b>"นำเข้าข้อมูล"</b> ให้คุณทำตามขั้นตอนดังนี้:
</p>
<ol class="list-decimal list-inside text-sm text-gray-600 space-y-1">
<li>คลิกที่ปุ่ม <span class="bg-gray-100 px-2 py-1 rounded border">Choose File</span> หรือ <span class="bg-gray-100 px-2 py-1 rounded border">เรียกดู</span> เพื่อเลือกไฟล์ Excel ที่เตรียมไว้</li>
<li>กดปุ่ม <span class="bg-blue-600 text-white px-2 py-1 rounded">อัปโหลดและประมวลผล</span></li>
<li>รอระบบทำงาน (ระยะเวลาขึ้นอยู่กับจำนวนข้อมูล) ระบบจะค้นหาข้อมูลจากฐานข้อมูล HOSxP ให้อัตโนมัติ</li>
</ol>
</div>
</div>
</div>
<!-- Step 4 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-yellow-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-yellow-100 rounded-full text-yellow-600 font-bold text-xl">4</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">4. เมนูตั้งค่าระบบ (สำหรับผู้ดูแลระบบ)</h3>
<p class="text-gray-700 mb-4">
ผู้ดูแลระบบ (Admin) สามารถเข้าถึงเมนูตั้งค่าได้ โดยระบบจะแบ่งออกเป็น 4 ส่วนหลัก:
</p>
<ul class="list-disc pl-6 text-gray-700 space-y-2 mb-4">
<li><strong>ตั้งค่าทั่วไป:</strong> ใช้เปลี่ยนชื่อระบบที่แถบด้านบน (Topbar) และข้อความลิขสิทธิ์ด้านล่าง (Footer)</li>
<li><strong>จัดการผู้ดูแลระบบ:</strong> เพิ่มหรือลบสิทธิ์การเข้าถึงเมนูตั้งค่าด้วยหมายเลขบัตรประชาชน (CID)</li>
<li><strong>จัดการชุดข้อมูล (Export Profiles):</strong> สร้าง แก้ไข และลบชุดข้อมูลสำหรับใช้ในหน้าหลัก</li>
<li><strong>ประวัติการใช้งานระบบ (Logs):</strong> ตรวจสอบประวัติการ Export ข้อมูล, การแก้ไขการตั้งค่า และการเข้าสู่ระบบต่างๆ ของผู้ใช้ในระบบทั้งหมด</li>
</ul>
<p class="text-gray-600 text-sm leading-relaxed mb-3">
เมื่อต้องการนำข้อมูลไปใช้ต่อ ให้กดปุ่ม <span class="bg-green-600 text-white px-2 py-1 rounded">ส่งออกเป็น Excel</span>
</p>
<ul class="list-disc list-inside text-sm text-gray-600 space-y-1">
<li>ไฟล์ที่ได้จะมีชื่อเดียวกันกับไฟล์ที่คุณอัปโหลด และต่อท้ายด้วย <code>_ตรวจสอบแล้ว.xlsx</code></li>
</ul>
</div>
</div>
</div>
<!-- Step 5 -->
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden md:col-span-2">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-indigo-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-indigo-100 rounded-full text-indigo-600 font-bold text-xl">5</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">ประวัติการทำงาน (Work History)</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-3">
ทุกครั้งที่มีการอัปโหลดไฟล์และประมวลผลสำเร็จ ระบบจะบันทึกประวัติการทำงานไว้เป็นหลักฐาน คุณสามารถเข้าไปดูได้ที่เมนู <b>"ประวัติการทำงาน"</b> บนแถบเมนูด้านบน
</p>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100 mt-2">
<p class="text-sm text-gray-600">
<b>ข้อมูลที่ถูกบันทึกประกอบด้วย:</b> ชื่อชุดข้อมูลที่เลือกใช้, ชื่อผู้ใช้งาน, ชื่อไฟล์ต้นฉบับที่อัปโหลด, จำนวนข้อมูลที่อ่านได้จากไฟล์, จำนวนข้อมูลที่ค้นพบใน HOSxP, และ วัน-เวลาที่ทำการประมวลผล (รูปแบบ พ.ศ.)
</p>
</div>
</div>
</div>
</div>
<!-- Step 6 -->
<?php if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true): ?>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 step-card relative overflow-hidden md:col-span-2">
<div class="absolute top-0 right-0 -mt-4 -mr-4 w-24 h-24 bg-teal-50 rounded-full z-0"></div>
<div class="relative z-10 flex items-start">
<div class="flex-shrink-0 mt-1">
<div class="flex items-center justify-center w-12 h-12 bg-teal-100 rounded-full text-teal-600 font-bold text-xl">6</div>
</div>
<div class="ml-5">
<h3 class="text-xl font-semibold text-gray-900 mb-2">ประวัติการใช้งานระบบ (System Logs)</h3>
<p class="text-gray-600 text-sm leading-relaxed mb-3">
เมนู <b>"ประวัติการใช้งานระบบ"</b> (อยู่ในหน้าตั้งค่า) เป็นเมนูพิเศษสำหรับ <b>ผู้ดูแลระบบ (Admin)</b> เท่านั้น โดยจะมีความแตกต่างจาก "ประวัติการทำงาน" ทั่วไปคือ:
</p>
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100 mt-2 space-y-2">
<p class="text-sm text-gray-600"><b>1. ประวัติการทำงาน (บน Topbar):</b> เก็บเฉพาะสถิติการอัปโหลดไฟล์และส่งออกข้อมูล (Export) เพื่อให้ทุกคนดูประวัติการดึงข้อมูลของตัวเองได้ง่ายๆ</p>
<p class="text-sm text-gray-600"><b>2. ประวัติการใช้งานระบบ (System Logs):</b> คือระบบ Audit Log ที่แอบบันทึกการกระทำ <b>"ทุกอย่าง"</b> ในระบบ เช่น ใคร Login เข้ามาตอนไหน, ใครแก้ไขหรือลบชุดข้อมูล, รวมถึงใครเพิ่ม/ลดสิทธิ์แอดมิน เพื่อความปลอดภัยสูงสุด</p>
</div>
</div>
</div>
</div>
<?php endif; ?>
</div>
</div>
<?php include 'footer.php'; ?>
</body>
</html>
@@ -0,0 +1,209 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 0); // Don't output errors directly
header('Content-Type: application/json');
register_shutdown_function(function() {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR])) {
// Clear any previous output
while (ob_get_level()) { ob_end_clean(); }
echo json_encode([
'status' => 'error',
'message' => 'Fatal Error: ' . $error['message'] . ' in ' . $error['file'] . ' on line ' . $error['line']
]);
}
});
ob_start(); // Start output buffering
require_once 'config.php';
// ต้องรัน composer require phpoffice/phpspreadsheet ก่อน
if (file_exists('vendor/autoload.php')) {
require 'vendor/autoload.php';
} else {
ob_clean();
echo json_encode(['status' => 'error', 'message' => 'ไม่พบ Library PhpSpreadsheet กรุณารัน composer install']);
exit;
}
use PhpOffice\PhpSpreadsheet\IOFactory;
checkLogin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method']);
exit;
}
if (!isset($_FILES['excel_file']) || $_FILES['excel_file']['error'] !== UPLOAD_ERR_OK) {
echo json_encode(['status' => 'error', 'message' => 'Upload failed or no file selected']);
exit;
}
if (empty($_POST['profile_id'])) {
echo json_encode(['status' => 'error', 'message' => 'กรุณาเลือกชุดข้อมูล (Profile)']);
exit;
}
$profile_id = $_POST['profile_id'];
// Get Profile Info
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
$stmt = $log_db->prepare("SELECT * FROM export_profiles WHERE id = ?");
$stmt->execute([$profile_id]);
$profile = $stmt->fetch();
if (!$profile) {
echo json_encode(['status' => 'error', 'message' => 'ไม่พบชุดข้อมูลที่เลือก']);
exit;
}
$expectedCols = array_map('trim', explode(',', strtolower($profile['excel_columns'])));
$file = $_FILES['excel_file']['tmp_name'];
$fileName = $_FILES['excel_file']['name'];
try {
set_time_limit(300); // Allow up to 5 minutes for processing
// Load Excel file
$spreadsheet = IOFactory::load($file);
$worksheet = $spreadsheet->getActiveSheet();
// Find column indexes
$headerRow = $worksheet->getRowIterator(1)->current();
$cellIterator = $headerRow->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
$colIndexes = [];
foreach ($cellIterator as $cell) {
$val = strtolower(trim($cell->getValue() ?? ''));
if (in_array($val, $expectedCols)) {
$colIndexes[$val] = $cell->getColumn();
}
}
foreach ($expectedCols as $ec) {
if (!isset($colIndexes[$ec])) {
echo json_encode(['status' => 'error', 'message' => "ไม่พบคอลัมน์ '$ec' ในไฟล์ Excel แถวแรก"]);
exit;
}
}
// Extract Data from Excel
$excelData = [];
foreach ($worksheet->getRowIterator(2) as $row) {
$rowData = [];
$hasData = false;
foreach ($expectedCols as $ec) {
$cell = $worksheet->getCell($colIndexes[$ec] . $row->getRowIndex());
$val = trim($cell->getValue() ?? '');
$rowData[$ec] = $val;
if ($val !== '') $hasData = true;
}
if ($hasData) {
$excelData[] = $rowData;
}
}
if (empty($excelData)) {
echo json_encode(['status' => 'error', 'message' => 'ไม่พบข้อมูลในไฟล์ Excel']);
exit;
}
// Check if SQL parameters match Excel columns
preg_match_all('/:([a-zA-Z0-9_]+)/', $profile['sql_query'], $matches);
$sqlParams = array_unique(array_map('strtolower', $matches[1]));
$missingInExcelCols = array_diff($sqlParams, $expectedCols);
if (!empty($missingInExcelCols)) {
echo json_encode(['status' => 'error', 'message' => 'คำสั่ง SQL เรียกใช้เงื่อนไข [:' . implode(', :', $missingInExcelCols) . '] แต่คุณไม่ได้กำหนดคอลัมน์นี้ไว้ในการตั้งค่าชุดข้อมูล (คอลัมน์ Excel)']);
exit;
}
// Query HOSxP Database
$his_db = getDB(DB_HIS_HOST, DB_HIS_NAME, DB_HIS_USER, DB_HIS_PASS);
$results = [];
try {
$stmt = $his_db->prepare($profile['sql_query']);
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'รูปแบบคำสั่ง SQL ไม่ถูกต้อง หรืออ้างอิงฟิลด์/ตารางที่ไม่มีอยู่จริง: ' . $e->getMessage()]);
exit;
}
// Execute query for each row
foreach ($excelData as $index => $rowData) {
$params = [];
// Bind ONLY the parameters that the SQL query expects to avoid PDO "Invalid parameter number" errors
foreach ($sqlParams as $sp) {
$params[':' . $sp] = $rowData[$sp];
}
try {
$stmt->execute($params);
$res = $stmt->fetchAll();
} catch (PDOException $e) {
echo json_encode(['status' => 'error', 'message' => 'เกิดข้อผิดพลาดในการรัน SQL แถวที่ ' . ($index + 2) . ' ของไฟล์ Excel: ' . $e->getMessage()]);
exit;
}
if (!empty($res)) {
// Convert TIS-620/Windows-874 to UTF-8
foreach ($res as &$r) {
foreach ($r as $key => $value) {
if (is_string($value)) {
$r[$key] = iconv('TIS-620', 'UTF-8//IGNORE', $value);
}
}
}
unset($r); // break reference
$results = array_merge($results, $res);
}
}
// Sort globally by the first column returned by the query (usually person_id or hn)
// if $results is not empty
if (!empty($results)) {
$firstKey = array_key_first($results[0]);
usort($results, function($a, $b) use ($firstKey) {
// numeric sort if looks like numbers, else string sort
if (is_numeric($a[$firstKey]) && is_numeric($b[$firstKey])) {
return (float)$a[$firstKey] <=> (float)$b[$firstKey];
}
return strcmp($a[$firstKey], $b[$firstKey]);
});
}
// Log to hosxp_export_person
$logStmt = $log_db->prepare("INSERT INTO export_logs (username, profile_id, file_name, total_rows, found_rows) VALUES (?, ?, ?, ?, ?)");
$logStmt->execute([
$_SESSION['fullname'] ?? 'Unknown User',
$profile_id,
$fileName,
count($excelData),
count($results)
]);
// Add system log
addSystemLog('EXPORT_DATA', "ประมวลผลข้อมูล (ไฟล์: $fileName, พบ: " . count($results) . "/" . count($excelData) . " รายการ)");
ob_clean(); // Clear buffer before sending success
$json = json_encode([
'status' => 'success',
'message' => 'ประมวลผลสำเร็จ พบข้อมูล ' . count($results) . ' รายการ จากทั้งหมด ' . count($excelData) . ' รายการ',
'data' => $results
], JSON_INVALID_UTF8_SUBSTITUTE);
if ($json === false) {
echo json_encode(['status' => 'error', 'message' => 'JSON Encode Error: ' . json_last_error_msg()]);
} else {
echo $json;
}
} catch (Throwable $e) {
ob_clean(); // Clear buffer before sending error
echo json_encode(['status' => 'error', 'message' => 'Error: ' . $e->getMessage()]);
}
@@ -0,0 +1,266 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once 'config.php';
checkAdmin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
$log_db = getDB(DB_LOG_HOST, DB_LOG_NAME, DB_LOG_USER, DB_LOG_PASS);
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>จัดการชุดข้อมูล (Export Profiles) - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
/* Custom DataTables Styling */
.dataTables_wrapper { padding: 1rem 0; }
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.35rem 0.75rem; margin-left: 0.5rem; outline: none; transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_length select {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.25rem 1.5rem 0.25rem 0.75rem; margin: 0 0.5rem; outline: none;
}
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_length {
margin-bottom: 1rem;
}
table.dataTable.no-footer { border-bottom: 1px solid #e5e7eb; margin-top: 1rem !important; margin-bottom: 1rem !important; }
table.dataTable thead th {
border-bottom: 2px solid #e5e7eb; background-color: #f3f4f6; color: #374151; font-weight: 600; padding: 0.75rem 1rem; white-space: nowrap; text-align: center;
}
table.dataTable tbody td {
border-bottom: 1px solid #f3f4f6; padding: 1rem; color: #4b5563; vertical-align: top;
}
table.dataTable tbody tr:hover { background-color: #f9fafb; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.35rem 0.85rem; margin-left: 0.25rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: white; color: #374151 !important; transition: all 0.2s; font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: #10b981 !important; color: white !important; border-color: #10b981; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover {
background: #f3f4f6 !important; border-color: #d1d5db; color: #111827 !important;
}
.dataTables_wrapper .dataTables_info { color: #6b7280; font-size: 0.875rem; padding-top: 0.85rem; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div class="mb-4">
<a href="settings.php" class="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
กลับไปหน้าตั้งค่าระบบ
</a>
</div>
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-5 border-b border-gray-200 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium text-gray-900">จัดการชุดข้อมูล (Export Profiles)</h3>
<p class="mt-1 text-sm text-gray-500">ตั้งค่าคอลัมน์ Excel และคำสั่ง SQL ที่ใช้ในการค้นหา</p>
</div>
<button onclick="openModal()" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded text-sm flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
เพิ่มชุดข้อมูลใหม่
</button>
</div>
<div class="px-6 py-5 overflow-x-auto">
<table id="profilesTable" class="display w-full text-sm">
<thead>
<tr>
<th class="w-10 text-center">ลำดับ</th>
<th>ชื่อชุดข้อมูล</th>
<th>คอลัมน์ Excel</th>
<th>ผู้สร้าง</th>
<th>วันที่สร้าง</th>
<th class="w-24">จัดการ</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<!-- Modal Form -->
<div id="profileModal" class="fixed inset-0 z-50 hidden overflow-y-auto" aria-labelledby="modal-title" role="dialog" aria-modal="true">
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" aria-hidden="true"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl w-full">
<form id="profileForm">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4" id="modal-title">เพิ่มชุดข้อมูล</h3>
<input type="hidden" id="profile_id" name="id">
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="profile_name">ชื่อชุดข้อมูล</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="profile_name" name="profile_name" type="text" required placeholder="เช่น ค้นหาด้วย PID">
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="excel_columns">คอลัมน์ Excel (คั่นด้วยคอมม่า)</label>
<input class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="excel_columns" name="excel_columns" type="text" required placeholder="เช่น pid, hn">
<p class="text-xs text-gray-500 mt-1">ตั้งชื่อให้ตรงกับหัวคอลัมน์ในแถวแรกของไฟล์ Excel (พิมพ์เล็ก/ใหญ่มีผลเมื่อเรียกใช้ใน SQL)</p>
</div>
<div class="mb-4">
<label class="block text-gray-700 text-sm font-bold mb-2" for="sql_query">คำสั่ง SQL</label>
<textarea class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline font-mono text-xs" id="sql_query" name="sql_query" rows="6" required placeholder="SELECT * FROM person WHERE cid = :pid"></textarea>
<div class="text-xs text-gray-500 mt-2 space-y-1">
<p><b>ตัวอย่าง 1 คอลัมน์:</b> <code>SELECT * FROM person p WHERE p.cid = :pid</code></p>
<p><b>ตัวอย่าง 2 คอลัมน์:</b> <code>SELECT * FROM person p WHERE p.cid = :pid AND p.patient_hn = :hn</code></p>
<p class="text-red-500 font-semibold">* ใช้ <code>:ชื่อคอลัมน์</code> ในคำสั่ง SQL ระบบจะดึงข้อมูลจากไฟล์ Excel แต่ละบรรทัดมาแทนที่อัตโนมัติ</p>
<p class="text-red-500 font-semibold">* ชื่อคอลัมน์ผลลัพธ์ (Header) จะถูกสร้างตามชื่อที่ SELECT ออกมา สามารถใช้ <code>AS ชื่อคอลัมน์</code> เพื่อกำหนดชื่อให้สวยงามได้</p>
</div>
</div>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="submit" class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-blue-600 text-base font-medium text-white hover:bg-blue-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
บันทึกข้อมูล
</button>
<button type="button" onclick="closeModal()" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
ยกเลิก
</button>
</div>
</form>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
<!-- jQuery, DataTables, and SweetAlert2 -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
let table;
$(document).ready(function() {
table = $('#profilesTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
"ajax": "api_profiles.php?action=list",
"columns": [
{ "data": null, "orderable": false, "searchable": false, "className": "text-center align-middle", "render": function(data, type, row, meta) {
return meta.row + 1;
}},
{ "data": "profile_name", "className": "align-middle", "render": function(data) {
return '<div class="font-semibold text-gray-800 whitespace-nowrap">' + data + '</div>';
}},
{ "data": "excel_columns", "className": "text-center align-middle", "render": function(data) {
let cols = data.split(',').map(c => c.trim());
return '<div class="flex flex-wrap justify-center gap-1">' + cols.map(c => '<span class="bg-gray-100 border border-gray-200 text-gray-600 px-2 py-0.5 rounded text-xs">' + c + '</span>').join('') + '</div>';
}},
{ "data": "creator_name", "className": "text-center align-middle", "render": function(data) {
return '<div class="whitespace-nowrap text-gray-600 text-sm">' + (data ? data : '<span class="text-gray-400">-</span>') + '</div>';
}},
{ "data": "created_at", "className": "text-center align-middle", "render": function(data) {
return '<div class="whitespace-nowrap text-gray-500 text-sm">' + data + '</div>';
}},
{ "data": null, "orderable": false, "className": "text-center align-middle", "render": function(data, type, row) {
return `
<div class="flex items-center justify-center space-x-2">
<button onclick="editProfile(${row.id})" class="text-blue-500 hover:text-blue-700 hover:bg-blue-50 p-1.5 rounded-md transition-colors" title="แก้ไข">
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</button>
<button onclick="deleteProfile(${row.id})" class="text-red-500 hover:text-red-700 hover:bg-red-50 p-1.5 rounded-md transition-colors" title="ลบ">
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
`;
}}
],
"order": [] // Disable default sort to keep SQL order (descending ID)
});
$('#profileForm').on('submit', function(e) {
e.preventDefault();
$.ajax({
url: 'api_profiles.php?action=save',
type: 'POST',
data: $(this).serialize(),
success: function(res) {
if (res.status === 'success') {
Swal.fire('สำเร็จ', res.message, 'success');
closeModal();
table.ajax.reload();
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
}
});
});
});
function openModal() {
$('#profileForm')[0].reset();
$('#profile_id').val('');
$('#modal-title').text('เพิ่มชุดข้อมูล');
$('#profileModal').removeClass('hidden');
}
function closeModal() {
$('#profileModal').addClass('hidden');
}
function editProfile(id) {
$.get('api_profiles.php?action=get&id=' + id, function(res) {
if (res.status === 'success') {
$('#profile_id').val(res.data.id);
$('#profile_name').val(res.data.profile_name);
$('#excel_columns').val(res.data.excel_columns);
$('#sql_query').val(res.data.sql_query);
$('#modal-title').text('แก้ไขชุดข้อมูล');
$('#profileModal').removeClass('hidden');
}
});
}
function deleteProfile(id) {
Swal.fire({
title: 'ยืนยันการลบ?',
text: "หากลบแล้วจะไม่สามารถกู้คืนได้ (ประวัติที่เคยใช้ชุดข้อมูลนี้จะกลายเป็นค่าว่าง)",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'ใช่, ลบเลย!',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
$.post('api_profiles.php?action=delete', { id: id }, function(res) {
if (res.status === 'success') {
Swal.fire('ลบสำเร็จ!', res.message, 'success');
table.ajax.reload();
} else {
Swal.fire('ข้อผิดพลาด', res.message, 'error');
}
});
}
});
}
</script>
</body>
</html>
@@ -0,0 +1,79 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
checkAdmin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ตั้งค่าระบบ - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
</style>
</head>
<body class="bg-gray-50 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
<h2 class="text-2xl font-bold text-gray-900 mb-6">ตั้งค่าระบบ (System Settings)</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- ตั้งค่าทั่วไป -->
<a href="general_settings.php" class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow border-l-4 border-yellow-500 block">
<div class="flex items-center mb-4">
<div class="p-3 rounded-full bg-yellow-100 text-yellow-600 mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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>
</div>
<h3 class="text-xl font-bold text-gray-800">ตั้งค่าทั่วไป</h3>
</div>
<p class="text-gray-600">ตั้งค่าชื่อข้อความบนแถบเมนู (Topbar) และข้อความลิขสิทธิ์ส่วนท้ายเว็บ (Footer)</p>
</a>
<!-- จัดการผู้ดูแลระบบ -->
<a href="admins.php" class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow border-l-4 border-blue-500 block">
<div class="flex items-center mb-4">
<div class="p-3 rounded-full bg-blue-100 text-blue-600 mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path></svg>
</div>
<h3 class="text-xl font-bold text-gray-800">จัดการผู้ดูแลระบบ</h3>
</div>
<p class="text-gray-600">เพิ่ม ลบ หรือแก้ไขสิทธิ์การเป็นผู้ดูแลระบบ (Admin) เพื่อเข้าถึงเมนูตั้งค่านี้ ด้วยเลขบัตรประชาชน (CID)</p>
</a>
<!-- จัดการชุดข้อมูล -->
<a href="profiles.php" class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow border-l-4 border-green-500 block">
<div class="flex items-center mb-4">
<div class="p-3 rounded-full bg-green-100 text-green-600 mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4"></path></svg>
</div>
<h3 class="text-xl font-bold text-gray-800">จัดการโปรไฟล์ (ชุดข้อมูล)</h3>
</div>
<p class="text-gray-600">จัดการชุดข้อมูลสำหรับการส่งออก (Export Profiles) กำหนดคอลัมน์ Excel และคำสั่ง SQL ที่ใช้ในการค้นหาจากฐาน HOSxP</p>
</a>
<!-- ประวัติการใช้งานระบบ -->
<a href="system_logs.php" class="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow border-l-4 border-purple-500 block">
<div class="flex items-center mb-4">
<div class="p-3 rounded-full bg-purple-100 text-purple-600 mr-4">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
</div>
<h3 class="text-xl font-bold text-gray-800">ประวัติการใช้งานระบบ (Logs)</h3>
</div>
<p class="text-gray-600">ดูประวัติการใช้งานอย่างละเอียด เช่น การเข้าสู่ระบบ, การจัดการผู้ดูแล, การแก้ไขชุดข้อมูล และการ Export ข้อมูล</p>
</a>
</div>
</div>
<?php include 'footer.php'; ?>
</body>
</html>
@@ -0,0 +1,150 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once __DIR__ . '/config.php';
checkAdmin();
$topbar_text = getSystemSetting('topbar_text', 'ระบบตรวจสอบข้อมูล HOSxP');
$footer_text = getSystemSetting('footer_text', 'หน่วยงานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ประวัติการใช้งานระบบ (System Logs) - <?php echo htmlspecialchars($topbar_text); ?></title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="icon" href="assets/img/logo.png" type="image/png">
<style>
body { font-family: 'Sarabun', sans-serif; }
/* Custom DataTables Styling */
.dataTables_wrapper { padding: 1rem 0; }
.dataTables_wrapper .dataTables_filter input {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.35rem 0.75rem; margin-left: 0.5rem; outline: none; transition: all 0.2s;
}
.dataTables_wrapper .dataTables_filter input:focus {
border-color: #10b981; box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
.dataTables_wrapper .dataTables_length select {
border: 1px solid #d1d5db; border-radius: 0.5rem; padding: 0.25rem 1.5rem 0.25rem 0.75rem; margin: 0 0.5rem; outline: none;
}
.dataTables_wrapper .dataTables_filter,
.dataTables_wrapper .dataTables_length {
margin-bottom: 1rem;
}
table.dataTable.no-footer { border-bottom: 1px solid #e5e7eb; margin-top: 1rem !important; margin-bottom: 1rem !important; }
table.dataTable thead th {
border-bottom: 2px solid #e5e7eb; background-color: #f3f4f6; color: #374151; font-weight: 600; padding: 0.75rem 1rem; white-space: nowrap; text-align: center;
}
table.dataTable tbody td {
border-bottom: 1px solid #f3f4f6; padding: 1rem; color: #4b5563; vertical-align: top;
}
table.dataTable tbody tr:hover { background-color: #f9fafb; }
.dataTables_wrapper .dataTables_paginate .paginate_button {
padding: 0.35rem 0.85rem; margin-left: 0.25rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: white; color: #374151 !important; transition: all 0.2s; font-size: 0.875rem;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dataTables_wrapper .dataTables_paginate .paginate_button.current:hover {
background: #10b981 !important; color: white !important; border-color: #10b981; box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.dataTables_wrapper .dataTables_paginate .paginate_button:not(.current):hover {
background: #f3f4f6 !important; border-color: #d1d5db; color: #111827 !important;
}
.dataTables_wrapper .dataTables_info { color: #6b7280; font-size: 0.875rem; padding-top: 0.85rem; }
/* Action Type Badges */
.badge { padding: 0.25rem 0.6rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; display: inline-block; }
.badge-login { background-color: #dcfce7; color: #166534; }
.badge-logout { background-color: #f3f4f6; color: #4b5563; }
.badge-create { background-color: #dbeafe; color: #1e40af; }
.badge-edit { background-color: #fef08a; color: #854d0e; }
.badge-delete { background-color: #fee2e2; color: #991b1b; }
.badge-export { background-color: #f3e8ff; color: #6b21a8; }
.badge-default { background-color: #e5e7eb; color: #374151; }
</style>
</head>
<body class="bg-gray-100 flex flex-col min-h-screen">
<?php include 'topbar.php'; ?>
<div class="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
<div class="mb-4">
<a href="settings.php" class="text-blue-600 hover:text-blue-800 text-sm font-medium flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path></svg>
กลับไปหน้าตั้งค่าระบบ
</a>
</div>
<div class="bg-white shadow rounded-lg mb-8">
<div class="px-6 py-5 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">ประวัติการใช้งานระบบ (System Logs)</h3>
<p class="mt-1 text-sm text-gray-500">บันทึกการกระทำต่างๆ ในระบบ เช่น การเข้าสู่ระบบ, การตั้งค่า และการ Export ข้อมูล</p>
</div>
<div class="px-6 py-5 overflow-x-auto">
<table id="logsTable" class="display w-full text-sm">
<thead>
<tr>
<th class="w-10">ลำดับ</th>
<th>ประเภทรายการ</th>
<th>ผู้ใช้งาน</th>
<th>รายละเอียด</th>
<th>IP Address</th>
<th>วันเวลา</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
<?php include 'footer.php'; ?>
<!-- jQuery and DataTables -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
<script>
$(document).ready(function() {
$('#logsTable').DataTable({
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json"
},
"ajax": "api_system_logs.php?action=list",
"columns": [
{ "data": null, "orderable": false, "searchable": false, "className": "text-center align-middle", "render": function(data, type, row, meta) {
return meta.row + 1;
}},
{ "data": "action_type", "className": "text-center align-middle", "render": function(data) {
let badgeClass = 'badge-default';
if (data === 'LOGIN') badgeClass = 'badge-login';
else if (data === 'LOGOUT') badgeClass = 'badge-logout';
else if (data.includes('CREATE') || data.includes('ADD')) badgeClass = 'badge-create';
else if (data.includes('EDIT')) badgeClass = 'badge-edit';
else if (data.includes('DELETE') || data.includes('REMOVE')) badgeClass = 'badge-delete';
else if (data === 'EXPORT_DATA') badgeClass = 'badge-export';
return `<span class="badge ${badgeClass}">${data}</span>`;
}},
{ "data": null, "className": "text-center align-middle", "render": function(data, type, row) {
if (!row.user_name) return '<div class="text-gray-400">-</div>';
return `<div>${row.user_name}</div><div class="text-xs text-gray-500">${row.user_cid || ''}</div>`;
}},
{ "data": "details", "className": "align-middle", "render": function(data) {
return data ? data : '<span class="text-gray-400">-</span>';
}},
{ "data": "ip_address", "className": "text-center align-middle", "render": function(data) {
return data ? data : '<span class="text-gray-400">-</span>';
}},
{ "data": "created_at", "className": "text-center align-middle" }
],
"order": [[ 0, "desc" ]] // เรียงล่าสุดขึ้นก่อน
});
});
</script>
</body>
</html>
@@ -0,0 +1,25 @@
<?php
$topbar_text = getSystemSetting('topbar_text', 'HOSxP Mapping Data');
$current_page = basename($_SERVER['PHP_SELF']);
?>
<nav class="bg-green-700 shadow-md">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
<img src="assets/img/logo.png" alt="Logo" class="h-11 w-11 mr-3 bg-white rounded-full p-0.5 object-contain" onerror="this.style.display='none'">
<span class="text-white font-semibold text-lg"><?php echo htmlspecialchars($topbar_text); ?></span>
</div>
<div class="flex items-center space-x-4">
<a href="index.php" class="<?php echo ($current_page == 'index.php') ? 'text-white bg-green-800' : 'text-green-200 hover:text-white'; ?> px-3 py-2 rounded-md text-sm font-medium">นำเข้าข้อมูล</a>
<a href="history.php" class="<?php echo ($current_page == 'history.php') ? 'text-white bg-green-800' : 'text-green-200 hover:text-white'; ?> px-3 py-2 rounded-md text-sm font-medium">ประวัติการทำงาน</a>
<a href="manual.php" class="<?php echo ($current_page == 'manual.php') ? 'text-white bg-green-800' : 'text-green-200 hover:text-white'; ?> px-3 py-2 rounded-md text-sm font-medium">คู่มือการใช้งาน</a>
<?php if (isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true): ?>
<a href="settings.php" class="<?php echo (in_array($current_page, ['settings.php', 'admins.php', 'profiles.php', 'system_logs.php', 'general_settings.php'])) ? 'text-white bg-green-800' : 'text-green-200 hover:text-white'; ?> px-3 py-2 rounded-md text-sm font-medium">ตั้งค่าระบบ</a>
<?php endif; ?>
<div class="border-l border-green-600 h-6 mx-2"></div>
<span class="text-green-100 text-sm">ผู้ใช้งาน: <?php echo htmlspecialchars($_SESSION['fullname'] ?? ''); ?></span>
<a href="logout.php" class="text-red-300 hover:text-red-100 text-sm font-medium ml-4">ออกจากระบบ</a>
</div>
</div>
</div>
</nav>