1762 lines
121 KiB
PHP
1762 lines
121 KiB
PHP
<?php
|
|
require_once 'functions.php';
|
|
|
|
// ==========================================
|
|
// Base64 Upload Interceptor (Bypass Cloudflare WAF)
|
|
// ==========================================
|
|
foreach ($_POST as $key => $value) {
|
|
if (strpos($key, '_base64') !== false && !empty($value)) {
|
|
$originalKey = str_replace('_base64', '', $key);
|
|
$fileName = $_POST[$originalKey . '_name'] ?? 'uploaded_file';
|
|
$parts = explode(';', $value);
|
|
if (count($parts) == 2 && strpos($parts[1], 'base64,') === 0) {
|
|
$mimeType = str_replace('data:', '', $parts[0]);
|
|
$base64Str = substr($parts[1], 7);
|
|
$tmpFilePath = tempnam(sys_get_temp_dir(), 'b64_');
|
|
file_put_contents($tmpFilePath, base64_decode($base64Str));
|
|
$_FILES[$originalKey] = [
|
|
'name' => $fileName,
|
|
'type' => $mimeType,
|
|
'tmp_name' => $tmpFilePath,
|
|
'error' => UPLOAD_ERR_OK,
|
|
'size' => filesize($tmpFilePath)
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// คลาสช่วยเหลือสำหรับ 2FA (TOTP)
|
|
// ==========================================
|
|
class MiniTOTP
|
|
{
|
|
private static $base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
|
|
public static function createSecret($length = 16)
|
|
{
|
|
$secret = '';
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$secret .= self::$base32chars[random_int(0, 31)];
|
|
}
|
|
return $secret;
|
|
}
|
|
|
|
public static function verifyCode($secret, $code, $discrepancy = 1)
|
|
{
|
|
$currentTimeSlice = floor(time() / 30);
|
|
for ($i = -$discrepancy; $i <= $discrepancy; $i++) {
|
|
$calculatedCode = self::getCode($secret, $currentTimeSlice + $i);
|
|
if (hash_equals($calculatedCode, $code)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static function getCode($secret, $timeSlice)
|
|
{
|
|
$secretKey = self::base32Decode($secret);
|
|
$time = chr(0) . chr(0) . chr(0) . chr(0) . pack('N*', $timeSlice);
|
|
$hm = hash_hmac('SHA1', $time, $secretKey, true);
|
|
$offset = ord(substr($hm, -1)) & 0x0F;
|
|
$hashpart = substr($hm, $offset, 4);
|
|
$value = unpack('N', $hashpart);
|
|
$value = $value[1];
|
|
$value = $value & 0x7FFFFFFF;
|
|
$modulo = pow(10, 6);
|
|
return str_pad($value % $modulo, 6, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
private static function base32Decode($secret)
|
|
{
|
|
if (empty($secret)) return '';
|
|
$base32chars = self::$base32chars;
|
|
$base32charsFlipped = array_flip(str_split($base32chars));
|
|
$paddingCharCount = substr_count($secret, $base32chars[32] ?? '=');
|
|
$allowedValues = array(6, 4, 3, 1, 0);
|
|
if (!in_array($paddingCharCount, $allowedValues)) return false;
|
|
for ($i = 0; $i < 4; $i++) {
|
|
if ($paddingCharCount == $allowedValues[$i] && substr($secret, - ($allowedValues[$i])) != str_repeat($base32chars[32] ?? '=', $allowedValues[$i])) return false;
|
|
}
|
|
$secret = str_replace('=', '', $secret);
|
|
$secret = str_split($secret);
|
|
$binaryString = '';
|
|
for ($i = 0; $i < count($secret); $i = $i + 8) {
|
|
$x = '';
|
|
if (!in_array($secret[$i], str_split($base32chars))) return false;
|
|
for ($j = 0; $j < 8; $j++) {
|
|
$x .= str_pad(base_convert(@$base32charsFlipped[@$secret[$i + $j]], 10, 2), 5, '0', STR_PAD_LEFT);
|
|
}
|
|
$eightBits = str_split($x, 8);
|
|
for ($z = 0; $z < count($eightBits); $z++) {
|
|
$binaryString .= (($y = chr(base_convert($eightBits[$z], 2, 10))) || ord($y) == 48) ? $y : '';
|
|
}
|
|
}
|
|
return $binaryString;
|
|
}
|
|
}
|
|
|
|
// ==========================================
|
|
// ส่วน Export Excel สำหรับหน้าความคิดเห็น
|
|
// ==========================================
|
|
if (isset($_GET['export_feedback'])) {
|
|
header("Content-Type: application/vnd.ms-excel");
|
|
header("Content-Disposition: attachment; filename=feedbacks_export.xls");
|
|
header("Pragma: no-cache");
|
|
header("Expires: 0");
|
|
echo '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">';
|
|
echo '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>';
|
|
echo '<table border="1">';
|
|
echo '<tr><th>วันที่</th><th>ชื่อ-นามสกุล</th><th>หัวข้อ</th><th>รายละเอียด</th></tr>';
|
|
$whereExport = "";
|
|
if (!empty($_GET['topic_filter'])) $whereExport = "WHERE topic = " . $pdo->quote($_GET['topic_filter']);
|
|
$stmtEx = $pdo->query("SELECT * FROM feedbacks $whereExport ORDER BY created_at DESC");
|
|
while ($row = $stmtEx->fetch()) {
|
|
echo "<tr><td>{$row['created_at']}</td><td>" . htmlspecialchars($row['name']) . "</td><td>" . htmlspecialchars($row['topic']) . "</td><td>" . htmlspecialchars($row['message']) . "</td></tr>";
|
|
}
|
|
echo '</table></body></html>';
|
|
exit;
|
|
}
|
|
|
|
// ==========================================
|
|
// ตรวจสอบการเข้าสู่ระบบ & 2FA
|
|
// ==========================================
|
|
|
|
// ขั้นที่ 1: ตรวจสอบรหัสผ่าน
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login_step1'])) {
|
|
$cid = $_POST['cid'];
|
|
$pass = $_POST['password'];
|
|
|
|
$stmt = $pdo->prepare("SELECT * FROM users WHERE cid = ?");
|
|
$stmt->execute([$cid]);
|
|
$user = $stmt->fetch();
|
|
|
|
if ($user && password_verify($pass, $user['password_hash'])) {
|
|
// หากผู้ใช้มี Secret 2FA ให้ข้ามไปขั้นตอนที่ 2
|
|
if (!empty($user['two_factor_secret'])) {
|
|
$_SESSION['pending_2fa_id'] = $user['id'];
|
|
$_SESSION['pending_2fa_name'] = $user['name'];
|
|
$_SESSION['pending_2fa_role'] = $user['role'];
|
|
$_SESSION['pending_2fa_secret'] = $user['two_factor_secret'];
|
|
} else {
|
|
// หากไม่มี 2FA ให้เข้าสู่ระบบได้เลย
|
|
$_SESSION['user_id'] = $user['id'];
|
|
$_SESSION['user_name'] = $user['name'];
|
|
$_SESSION['user_role'] = $user['role'];
|
|
logAction('LOGIN', 'auth', 'เข้าสู่ระบบสำเร็จ (ไม่มี 2FA)');
|
|
setFlash('success', 'เข้าสู่ระบบสำเร็จ ยินดีต้อนรับ');
|
|
header('Location: admin.php');
|
|
exit;
|
|
}
|
|
} else {
|
|
setFlash('error', 'เลขประจำตัวประชาชนหรือรหัสผ่านไม่ถูกต้อง');
|
|
}
|
|
}
|
|
|
|
// ขั้นที่ 2: ตรวจสอบรหัส 2FA 6 หลัก
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['login_step2'])) {
|
|
$code = $_POST['totp_code'];
|
|
$secret = $_SESSION['pending_2fa_secret'] ?? '';
|
|
|
|
if (MiniTOTP::verifyCode($secret, $code)) {
|
|
$_SESSION['user_id'] = $_SESSION['pending_2fa_id'];
|
|
$_SESSION['user_name'] = $_SESSION['pending_2fa_name'];
|
|
$_SESSION['user_role'] = $_SESSION['pending_2fa_role'];
|
|
|
|
// ล้างข้อมูล temp
|
|
unset($_SESSION['pending_2fa_id'], $_SESSION['pending_2fa_name'], $_SESSION['pending_2fa_role'], $_SESSION['pending_2fa_secret']);
|
|
|
|
logAction('LOGIN', 'auth', 'เข้าสู่ระบบสำเร็จ (ผ่าน 2FA)');
|
|
setFlash('success', 'ยืนยันตัวตนสำเร็จ ยินดีต้อนรับ');
|
|
header('Location: admin.php');
|
|
exit;
|
|
} else {
|
|
setFlash('error', 'รหัส Authenticator ไม่ถูกต้อง กรุณาลองใหม่');
|
|
}
|
|
}
|
|
|
|
// กรณียกเลิกการล็อกอินกลางคัน
|
|
if (isset($_GET['cancel_login'])) {
|
|
unset($_SESSION['pending_2fa_id'], $_SESSION['pending_2fa_name'], $_SESSION['pending_2fa_role'], $_SESSION['pending_2fa_secret']);
|
|
header('Location: admin.php');
|
|
exit;
|
|
}
|
|
|
|
// แสดงหน้า Login หรือหน้ากรอกรหัส 2FA หากยังไม่ได้เข้าสู่ระบบ
|
|
if (!isset($_SESSION['user_id'])) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
|
|
<head>
|
|
<meta name="referrer" content="no-referrer" />
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>เข้าสู่ระบบจัดการ - โรงพยาบาลเกาะสมุย</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">
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
body {
|
|
font-family: 'Sarabun', sans-serif;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-slate-100 flex items-center justify-center h-screen">
|
|
<?php displayFlash(); ?>
|
|
|
|
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md">
|
|
|
|
<?php if (isset($_SESSION['pending_2fa_id'])): ?>
|
|
<!-- หน้าจอ Step 2: กรอกรหัส Authenticator -->
|
|
<div class="text-center mb-6">
|
|
<div class="w-16 h-16 bg-blue-50 rounded-full flex items-center justify-center mx-auto mb-4 text-blue-600">
|
|
<i data-lucide="shield-check" class="w-8 h-8"></i>
|
|
</div>
|
|
<h2 class="text-2xl font-bold text-gray-800">ยืนยันตัวตน 2 ขั้นตอน</h2>
|
|
<p class="text-gray-500 mt-2 text-sm">กรุณากรอกรหัส 6 หลักจากแอปพลิเคชัน<br />Google Authenticator ของคุณ</p>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="login_step2" value="1">
|
|
<div class="mb-6 flex justify-between space-x-1 sm:space-x-3">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autofocus autocomplete="off">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autocomplete="off">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autocomplete="off">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autocomplete="off">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autocomplete="off">
|
|
<input type="text" inputmode="numeric" pattern="[0-9]*" maxlength="1" class="otp-box w-10 h-12 sm:w-14 sm:h-16 bg-gray-50 border border-gray-300 rounded-xl text-center text-2xl font-extrabold focus:outline-none focus:ring-2 focus:ring-blue-500" required autocomplete="off">
|
|
</div>
|
|
<input type="hidden" name="totp_code" id="totp_code_hidden">
|
|
<button type="submit" class="w-full bg-blue-600 text-white py-3.5 rounded-xl font-bold shadow-lg shadow-blue-500/30 hover:bg-blue-700 transition-colors flex justify-center items-center">
|
|
<span class="btn-text">ยืนยันรหัส</span>
|
|
</button>
|
|
<a href="admin.php?cancel_login=1" class="block text-center mt-4 text-gray-400 hover:text-gray-600 text-sm transition-colors">ยกเลิก / กลับไปหน้าแรก</a>
|
|
</form>
|
|
|
|
<?php else: ?>
|
|
<!-- หน้าจอ Step 1: ล็อกอินปกติ -->
|
|
<div class="text-center mb-6">
|
|
<h2 class="text-2xl font-bold text-[#056839]">เข้าสู่ระบบ (CMS)</h2>
|
|
<p class="text-gray-500 mt-2">กรุณากรอกข้อมูลยืนยันตัวตน</p>
|
|
</div>
|
|
<form method="POST">
|
|
<input type="hidden" name="login_step1" value="1">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-bold mb-2">เลขประจำตัวประชาชน</label>
|
|
<input type="text" name="cid" class="w-full px-4 py-3 bg-gray-50 border rounded-xl" required autofocus>
|
|
</div>
|
|
<div class="mb-6 relative">
|
|
<label class="block text-sm font-bold mb-2">รหัสผ่าน</label>
|
|
<input type="password" id="login-pwd" name="password" class="w-full px-4 py-3 bg-gray-50 border rounded-xl pr-10" required>
|
|
<button type="button" class="absolute inset-y-0 right-0 pt-7 pr-3 flex items-center text-gray-400 hover:text-emerald-600" onclick="togglePassword('login-pwd', this)">
|
|
<i data-lucide="eye" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<button type="submit" class="w-full bg-[#056839] text-white py-3 rounded-xl font-bold hover:bg-[#04522b] transition-colors flex items-center justify-center">
|
|
<span class="btn-text">เข้าสู่ระบบ</span>
|
|
</button>
|
|
<a href="index.php" class="block text-center mt-4 text-gray-500 text-sm hover:text-[#056839]">กลับหน้าหลักเว็บไซต์</a>
|
|
</form>
|
|
<?php endif; ?>
|
|
|
|
</div>
|
|
<script>
|
|
lucide.createIcons();
|
|
|
|
function togglePassword(inputId, btn) {
|
|
const input = document.getElementById(inputId);
|
|
const icon = btn.querySelector('i');
|
|
if (input.type === 'password') {
|
|
input.type = 'text';
|
|
icon.setAttribute('data-lucide', 'eye-off');
|
|
} else {
|
|
input.type = 'password';
|
|
icon.setAttribute('data-lucide', 'eye');
|
|
}
|
|
lucide.createIcons();
|
|
}
|
|
|
|
document.addEventListener('submit', function(e) {
|
|
if (e.target && e.target.tagName === 'FORM') {
|
|
const btn = e.target.querySelector('button[type="submit"]');
|
|
if (btn) {
|
|
btn.classList.add('opacity-70', 'cursor-not-allowed');
|
|
const textSpan = btn.querySelector('.btn-text');
|
|
if (textSpan) textSpan.innerText = 'กำลังโหลด...';
|
|
}
|
|
}
|
|
});
|
|
|
|
// OTP Input Logic
|
|
const otpBoxes = document.querySelectorAll('.otp-box');
|
|
const hiddenOtp = document.getElementById('totp_code_hidden');
|
|
|
|
if (otpBoxes.length > 0) {
|
|
function updateHiddenOtp() {
|
|
let code = '';
|
|
otpBoxes.forEach(b => code += b.value);
|
|
hiddenOtp.value = code;
|
|
|
|
if (code.length === 6) {
|
|
const form = hiddenOtp.closest('form');
|
|
if (form) {
|
|
setTimeout(() => {
|
|
const btn = form.querySelector('button[type="submit"]');
|
|
if (btn) btn.click();
|
|
}, 50);
|
|
}
|
|
}
|
|
}
|
|
|
|
otpBoxes.forEach((box, index) => {
|
|
box.addEventListener('input', (e) => {
|
|
box.value = box.value.replace(/[^0-9]/g, ''); // Number only
|
|
if (box.value.length === 1) {
|
|
if (index < otpBoxes.length - 1) {
|
|
otpBoxes[index + 1].focus();
|
|
}
|
|
}
|
|
updateHiddenOtp();
|
|
});
|
|
|
|
box.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Backspace' && box.value === '') {
|
|
if (index > 0) {
|
|
otpBoxes[index - 1].focus();
|
|
}
|
|
}
|
|
});
|
|
|
|
box.addEventListener('paste', (e) => {
|
|
e.preventDefault();
|
|
const pasteData = e.clipboardData.getData('text').replace(/[^0-9]/g, '').slice(0, 6);
|
|
if (pasteData) {
|
|
pasteData.split('').forEach((char, i) => {
|
|
if (otpBoxes[i]) {
|
|
otpBoxes[i].value = char;
|
|
}
|
|
});
|
|
const nextIndex = Math.min(pasteData.length, otpBoxes.length - 1);
|
|
otpBoxes[nextIndex].focus();
|
|
updateHiddenOtp();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
// ==========================================
|
|
// ส่วนจัดการข้อมูลเมื่อเข้าสู่ระบบแล้ว (CMS)
|
|
// ==========================================
|
|
$tab = $_GET['tab'] ?? 'news';
|
|
$userRole = $_SESSION['user_role'] ?? 'editor';
|
|
|
|
// ------------------------------------------
|
|
// เช็คสิทธิ์การเข้าถึง (RBAC)
|
|
// ------------------------------------------
|
|
$adminOnlyTabs = ['settings', 'users'];
|
|
if (in_array($tab, $adminOnlyTabs) && $userRole !== 'admin') {
|
|
setFlash('error', 'คุณไม่มีสิทธิ์เข้าถึงเมนูนี้');
|
|
header("Location: admin.php?tab=news");
|
|
exit;
|
|
}
|
|
|
|
// 1. จัดการอัปโหลดไฟล์/เพิ่มเนื้อหา
|
|
if (isset($_POST['add_content'])) {
|
|
$title = $_POST['title'];
|
|
$category = $_POST['category_slug'];
|
|
$driveId = null;
|
|
$driveUrl = null;
|
|
|
|
$details = [];
|
|
if (!empty($_POST['extra_detail'])) $details['amount'] = $_POST['extra_detail'];
|
|
if (!empty($_POST['external_link'])) $details['gphoto_link'] = $_POST['external_link'];
|
|
$detailsJson = !empty($details) ? json_encode($details, JSON_UNESCAPED_UNICODE) : null;
|
|
|
|
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
|
|
$fileInfo = uploadToGoogleDrive($_FILES['file']['tmp_name'], $_FILES['file']['name'], $_FILES['file']['type']);
|
|
$driveId = $fileInfo['id'];
|
|
$driveUrl = $fileInfo['url'];
|
|
}
|
|
|
|
$stmt = $pdo->prepare("INSERT INTO contents (category_slug, title, details, drive_file_id, drive_file_url, created_by) VALUES (?, ?, ?, ?, ?, ?)");
|
|
$stmt->execute([$category, $title, $detailsJson, $driveId, $driveUrl, $_SESSION['user_id']]);
|
|
|
|
logAction('CREATE', 'contents', "เพิ่มเนื้อหาใหม่: $title");
|
|
setFlash('success', 'บันทึกข้อมูลเรียบร้อยแล้ว');
|
|
header("Location: admin.php?tab=" . $_GET['tab']);
|
|
exit;
|
|
}
|
|
|
|
// 1.1 จัดการแก้ไขเนื้อหาและไฟล์
|
|
if (isset($_POST['edit_content'])) {
|
|
$id = $_POST['content_id'];
|
|
$title = $_POST['title'];
|
|
$category = $_POST['category_slug'];
|
|
|
|
$details = [];
|
|
if (!empty($_POST['extra_detail'])) $details['amount'] = $_POST['extra_detail'];
|
|
if (!empty($_POST['external_link'])) $details['gphoto_link'] = $_POST['external_link'];
|
|
$detailsJson = !empty($details) ? json_encode($details, JSON_UNESCAPED_UNICODE) : null;
|
|
|
|
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
|
|
$stmt = $pdo->prepare("SELECT drive_file_id FROM contents WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$oldFile = $stmt->fetch();
|
|
if ($oldFile && $oldFile['drive_file_id']) {
|
|
deleteFromGoogleDrive($oldFile['drive_file_id']);
|
|
}
|
|
|
|
$fileInfo = uploadToGoogleDrive($_FILES['file']['tmp_name'], $_FILES['file']['name'], $_FILES['file']['type']);
|
|
$driveId = $fileInfo['id'];
|
|
$driveUrl = $fileInfo['url'];
|
|
|
|
$stmt = $pdo->prepare("UPDATE contents SET category_slug = ?, title = ?, details = ?, drive_file_id = ?, drive_file_url = ? WHERE id = ?");
|
|
$stmt->execute([$category, $title, $detailsJson, $driveId, $driveUrl, $id]);
|
|
} else {
|
|
$stmt = $pdo->prepare("UPDATE contents SET category_slug = ?, title = ?, details = ? WHERE id = ?");
|
|
$stmt->execute([$category, $title, $detailsJson, $id]);
|
|
}
|
|
|
|
logAction('UPDATE', 'contents', "แก้ไขเนื้อหา: $title");
|
|
setFlash('success', 'อัปเดตข้อมูลเรียบร้อยแล้ว');
|
|
header("Location: admin.php?tab=" . $_GET['tab']);
|
|
exit;
|
|
}
|
|
|
|
// 2. จัดการลบเนื้อหา
|
|
if (isset($_GET['delete_id'])) {
|
|
$id = $_GET['delete_id'];
|
|
$stmt = $pdo->prepare("SELECT drive_file_id, title FROM contents WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$item = $stmt->fetch();
|
|
if ($item) {
|
|
if ($item['drive_file_id']) deleteFromGoogleDrive($item['drive_file_id']);
|
|
$pdo->prepare("DELETE FROM contents WHERE id = ?")->execute([$id]);
|
|
logAction('DELETE', 'contents', "ลบข้อมูล: " . $item['title']);
|
|
setFlash('success', 'ลบข้อมูลและไฟล์เรียบร้อยแล้ว');
|
|
}
|
|
header("Location: admin.php?tab=" . $_GET['tab']);
|
|
exit;
|
|
}
|
|
|
|
// 3. จัดการอัปเดต Settings
|
|
if (isset($_POST['update_settings'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
foreach ($_POST['settings'] as $key => $val) {
|
|
$pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?")->execute([$val, $key]);
|
|
}
|
|
|
|
if (isset($_FILES['director_photo']) && $_FILES['director_photo']['error'] === UPLOAD_ERR_OK) {
|
|
$stmtOld = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'director_photo_id'");
|
|
$oldDriveId = $stmtOld->fetchColumn();
|
|
if ($oldDriveId) deleteFromGoogleDrive($oldDriveId);
|
|
$fileInfo = uploadToGoogleDrive($_FILES['director_photo']['tmp_name'], $_FILES['director_photo']['name'], $_FILES['director_photo']['type']);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('director_photo_id', ?, 'ID รูปผู้อำนวยการ') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['id']]);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('director_photo_url', ?, 'URL รูปผู้อำนวยการ') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['url']]);
|
|
}
|
|
|
|
if (isset($_FILES['hero_photo']) && $_FILES['hero_photo']['error'] === UPLOAD_ERR_OK) {
|
|
$stmtOld = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'hero_photo_id'");
|
|
$oldDriveId = $stmtOld->fetchColumn();
|
|
if ($oldDriveId) deleteFromGoogleDrive($oldDriveId);
|
|
$fileInfo = uploadToGoogleDrive($_FILES['hero_photo']['tmp_name'], $_FILES['hero_photo']['name'], $_FILES['hero_photo']['type']);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('hero_photo_id', ?, 'ID รูปหน้าปก') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['id']]);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('hero_photo_url', ?, 'URL รูปหน้าปก') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['url']]);
|
|
}
|
|
|
|
if (isset($_FILES['donate_qrcode']) && $_FILES['donate_qrcode']['error'] === UPLOAD_ERR_OK) {
|
|
$stmtOld = $pdo->query("SELECT setting_value FROM settings WHERE setting_key = 'donate_qrcode_id'");
|
|
$oldDriveId = $stmtOld->fetchColumn();
|
|
if ($oldDriveId) deleteFromGoogleDrive($oldDriveId);
|
|
$fileInfo = uploadToGoogleDrive($_FILES['donate_qrcode']['tmp_name'], $_FILES['donate_qrcode']['name'], $_FILES['donate_qrcode']['type']);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('donate_qrcode_id', ?, 'ID รูป QR Code บริจาค') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['id']]);
|
|
$pdo->prepare("INSERT INTO settings (setting_key, setting_value, setting_label) VALUES ('donate_qrcode_url', ?, 'URL รูป QR Code บริจาค') ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)")->execute([$fileInfo['url']]);
|
|
}
|
|
|
|
logAction('UPDATE', 'settings', "อัปเดตข้อมูลโครงสร้าง/การตั้งค่า");
|
|
setFlash('success', 'อัปเดตข้อมูลเรียบร้อยแล้ว');
|
|
header("Location: admin.php?tab=" . $_GET['tab']);
|
|
exit;
|
|
}
|
|
|
|
// 4. จัดการผู้ใช้งาน (เพิ่ม/แก้ไข/ลบ/จัดการ 2FA)
|
|
if (isset($_POST['add_user'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
$cid = trim($_POST['cid']);
|
|
$name = trim($_POST['name']);
|
|
$role = $_POST['role'];
|
|
$pass = password_hash($_POST['password'], PASSWORD_DEFAULT);
|
|
|
|
$stmt = $pdo->prepare("SELECT id FROM users WHERE cid = ?");
|
|
$stmt->execute([$cid]);
|
|
if ($stmt->fetch()) {
|
|
setFlash('error', 'เลขประจำตัวประชาชนนี้มีในระบบแล้ว');
|
|
} else {
|
|
$stmt = $pdo->prepare("INSERT INTO users (cid, password_hash, name, role) VALUES (?, ?, ?, ?)");
|
|
$stmt->execute([$cid, $pass, $name, $role]);
|
|
logAction('CREATE', 'users', "เพิ่มผู้ใช้งาน: $name");
|
|
setFlash('success', 'เพิ่มผู้ใช้งานเรียบร้อยแล้ว');
|
|
}
|
|
header("Location: admin.php?tab=users");
|
|
exit;
|
|
}
|
|
|
|
if (isset($_POST['edit_user'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
$id = $_POST['user_id'];
|
|
$name = trim($_POST['name']);
|
|
$role = $_POST['role'];
|
|
|
|
if (!empty($_POST['password'])) {
|
|
$pass = password_hash($_POST['password'], PASSWORD_DEFAULT);
|
|
$stmt = $pdo->prepare("UPDATE users SET name = ?, role = ?, password_hash = ? WHERE id = ?");
|
|
$stmt->execute([$name, $role, $pass, $id]);
|
|
} else {
|
|
$stmt = $pdo->prepare("UPDATE users SET name = ?, role = ? WHERE id = ?");
|
|
$stmt->execute([$name, $role, $id]);
|
|
}
|
|
logAction('UPDATE', 'users', "แก้ไขข้อมูลผู้ใช้ ID: $id");
|
|
setFlash('success', 'อัปเดตข้อมูลผู้ใช้งานเรียบร้อยแล้ว');
|
|
header("Location: admin.php?tab=users");
|
|
exit;
|
|
}
|
|
|
|
if (isset($_GET['delete_user'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
$id = $_GET['delete_user'];
|
|
if ($id == $_SESSION['user_id']) {
|
|
setFlash('error', 'ระบบไม่อนุญาตให้ลบบัญชีของตนเองได้');
|
|
} else {
|
|
$pdo->prepare("DELETE FROM users WHERE id = ?")->execute([$id]);
|
|
logAction('DELETE', 'users', "ลบผู้ใช้งาน ID: $id");
|
|
setFlash('success', 'ลบผู้ใช้งานเรียบร้อยแล้ว');
|
|
}
|
|
header("Location: admin.php?tab=users");
|
|
exit;
|
|
}
|
|
|
|
// 5. จัดการลบความคิดเห็น (เพิ่มใหม่ - Admin Only)
|
|
if (isset($_GET['delete_feedback'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
$id = $_GET['delete_feedback'];
|
|
$pdo->prepare("DELETE FROM feedbacks WHERE id = ?")->execute([$id]);
|
|
logAction('DELETE', 'feedbacks', "ลบความคิดเห็น ID: $id");
|
|
setFlash('success', 'ลบความคิดเห็นเรียบร้อยแล้ว');
|
|
header("Location: admin.php?tab=feedback");
|
|
exit;
|
|
}
|
|
|
|
// จัดการการสร้างและลบ 2FA
|
|
if (isset($_POST['action_2fa'])) {
|
|
if ($userRole !== 'admin') exit('Access Denied');
|
|
$userId = (int)$_POST['user_id'];
|
|
|
|
if ($_POST['action_2fa'] === 'generate') {
|
|
$secret = MiniTOTP::createSecret();
|
|
$pdo->prepare("UPDATE users SET two_factor_secret = ? WHERE id = ?")->execute([$secret, $userId]);
|
|
logAction('UPDATE', 'users', "เปิดใช้งาน 2FA ให้ผู้ใช้ ID: $userId");
|
|
setFlash('success', 'สร้างรหัส 2FA เรียบร้อยแล้ว กรุณาให้ผู้ใช้งานสแกน QR Code');
|
|
// ส่งให้ UI เปิด modal QR Code อัตโนมัติหลัง reload
|
|
$_SESSION['show_qr_for_user'] = $userId;
|
|
} elseif ($_POST['action_2fa'] === 'remove') {
|
|
$pdo->prepare("UPDATE users SET two_factor_secret = NULL WHERE id = ?")->execute([$userId]);
|
|
logAction('UPDATE', 'users', "ยกเลิก 2FA ของผู้ใช้ ID: $userId");
|
|
setFlash('success', 'ยกเลิกการใช้งาน 2FA ของผู้ใช้นี้แล้ว');
|
|
}
|
|
header("Location: admin.php?tab=users");
|
|
exit;
|
|
}
|
|
|
|
// ==========================================
|
|
// ระบบ จัดการ Tab & Sidebar Logic
|
|
// ==========================================
|
|
$contentTabs = ['news', 'services', 'links', 'gallery', 'ita'];
|
|
$tabTitle = "";
|
|
$inSlugs = "''";
|
|
|
|
if (in_array($tab, $contentTabs)) {
|
|
if ($tab == 'news') {
|
|
$tabTitle = 'จัดการข่าวสารและประกาศ';
|
|
$inSlugs = "'pr_news', 'jobs', 'procurement_supplies', 'procurement_meds', 'plan', 'finance', 'academic', 'laws', 'download', 'infographic'";
|
|
} elseif ($tab == 'services') {
|
|
$tabTitle = 'จัดการบริการและแพ็คเกจ';
|
|
$inSlugs = "'clinic', 'eservices', 'packages'";
|
|
} elseif ($tab == 'links') {
|
|
$tabTitle = 'จัดการเมนูและลิงก์';
|
|
$inSlugs = "'header_menu', 'internal_systems', 'related_agencies'";
|
|
} elseif ($tab == 'gallery') {
|
|
$tabTitle = 'จัดการแกลเลอรี่ภาพกิจกรรม';
|
|
$inSlugs = "'gallery'";
|
|
} elseif ($tab == 'ita') {
|
|
$tabTitle = 'จัดการข้อมูลประเมินคุณธรรม (ITA)';
|
|
$inSlugs = "'ita'";
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
|
|
<head>
|
|
<meta name="referrer" content="no-referrer" />
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="theme-color" content="#056839">
|
|
<link rel="manifest" href="manifest.json">
|
|
<title>CMS - โรงพยาบาลเกาะสมุย</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
}
|
|
</script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<style>
|
|
body {
|
|
font-family: 'Sarabun', sans-serif;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar {
|
|
height: 6px;
|
|
width: 6px;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.animate-fade-in {
|
|
animation: fade-in 0.3s ease-out forwards;
|
|
}
|
|
|
|
/* Generic Dark Mode for Dashboard */
|
|
.dark body, .dark .bg-slate-100 { background-color: #0f172a !important; color: #f1f5f9 !important; }
|
|
.dark .bg-white { background-color: #1e293b !important; border-color: #334155 !important; }
|
|
.dark .text-gray-900, .dark .text-gray-800, .dark .text-gray-700 { color: #f8fafc !important; }
|
|
.dark .text-gray-600, .dark .text-gray-500 { color: #94a3b8 !important; }
|
|
.dark .border-gray-200, .dark .border-slate-100 { border-color: #334155 !important; }
|
|
.dark input, .dark select, .dark textarea { background-color: #0f172a !important; color: white !important; border-color: #334155 !important; }
|
|
</style>
|
|
</head>
|
|
|
|
<script>
|
|
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
document.documentElement.classList.add('dark');
|
|
}
|
|
</script>
|
|
<body class="bg-slate-100 dark:bg-slate-900 flex h-screen overflow-hidden">
|
|
<?php displayFlash(); ?>
|
|
|
|
<aside class="w-64 bg-[#02311b] text-emerald-100 flex flex-col shrink-0 shadow-lg z-20">
|
|
<div class="p-6 bg-[#012012] border-b border-emerald-900/30 text-center">
|
|
<h1 class="text-white font-extrabold text-xl tracking-wider">SAMUI CMS</h1>
|
|
<p class="text-xs text-emerald-400 mt-2 font-medium bg-emerald-900/40 py-1 rounded-full border border-emerald-800/50">
|
|
<i data-lucide="user" class="w-3 h-3 inline mr-1"></i> <?= htmlspecialchars($_SESSION['user_name']) ?> (<?= strtoupper($userRole) ?>)
|
|
</p>
|
|
</div>
|
|
<nav class="flex-1 py-4 space-y-1 px-3 overflow-y-auto custom-scrollbar">
|
|
|
|
<p class="px-4 text-xs font-bold text-emerald-500/50 mb-2 mt-2 uppercase tracking-wider">จัดการเนื้อหาเว็บไซต์</p>
|
|
<a href="?tab=news" class="block px-4 py-2.5 rounded-lg <?= $tab == 'news' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">📰 ข่าวสารและประกาศ</a>
|
|
<a href="?tab=services" class="block px-4 py-2.5 rounded-lg <?= $tab == 'services' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">🏥 บริการและแพ็คเกจ</a>
|
|
<a href="?tab=links" class="block px-4 py-2.5 rounded-lg <?= $tab == 'links' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">🔗 เมนูและลิงก์</a>
|
|
<a href="?tab=gallery" class="block px-4 py-2.5 rounded-lg <?= $tab == 'gallery' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">🖼️ แกลเลอรี่ภาพกิจกรรม</a>
|
|
<a href="?tab=ita" class="block px-4 py-2.5 rounded-lg <?= $tab == 'ita' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">📋 ประเมินคุณธรรม (ITA)</a>
|
|
|
|
<p class="px-4 text-xs font-bold text-emerald-500/50 mb-2 mt-6 uppercase tracking-wider">จัดการโครงสร้าง</p>
|
|
<a href="?tab=web_structure" class="block px-4 py-2.5 rounded-lg <?= $tab == 'web_structure' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">🖥️ หน้าปก / ผู้บริหาร / บริจาค</a>
|
|
|
|
<p class="px-4 text-xs font-bold text-emerald-500/50 mb-2 mt-6 uppercase tracking-wider">ระบบโต้ตอบ</p>
|
|
<a href="?tab=feedback" class="block px-4 py-2.5 rounded-lg <?= $tab == 'feedback' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">💬 ระบบความคิดเห็น</a>
|
|
|
|
<p class="px-4 text-xs font-bold text-emerald-500/50 mb-2 mt-6 uppercase tracking-wider">ระบบ</p>
|
|
<?php if ($userRole === 'admin'): ?>
|
|
<a href="?tab=users" class="block px-4 py-2.5 rounded-lg <?= $tab == 'users' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">👥 จัดการผู้ใช้งาน</a>
|
|
<a href="?tab=settings" class="block px-4 py-2.5 rounded-lg <?= $tab == 'settings' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">⚙️ ตั้งค่าระบบ</a>
|
|
<?php endif; ?>
|
|
<a href="?tab=logs" class="block px-4 py-2.5 rounded-lg <?= $tab == 'logs' ? 'bg-[#056839] text-white' : 'hover:bg-[#056839]/50 text-sm' ?>">📝 ประวัติการทำงาน</a>
|
|
</nav>
|
|
<div class="p-4 border-t border-emerald-900/30 bg-[#01180d]">
|
|
<button onclick="confirmLogout()" class="block w-full text-center py-2.5 bg-red-500/20 text-red-400 font-bold rounded-xl hover:bg-red-500 hover:text-white transition-colors border border-red-500/30 cursor-pointer">ออกจากระบบ</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="flex-1 overflow-y-auto p-8 relative">
|
|
|
|
<!-- ==========================================
|
|
ส่วนจัดการเนื้อหา
|
|
========================================== -->
|
|
<?php if (in_array($tab, $contentTabs)): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 mb-8 overflow-hidden">
|
|
<div class="p-5 bg-emerald-50/50 border-b border-slate-100 flex justify-between items-center cursor-pointer hover:bg-emerald-50 transition-colors" onclick="document.getElementById('add-form').classList.toggle('hidden')">
|
|
<h2 class="text-lg font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="plus-circle" class="w-5 h-5 mr-2"></i> เพิ่มข้อมูลในหมวด "<?= $tabTitle ?>"
|
|
</h2>
|
|
<i data-lucide="chevron-down" class="w-5 h-5 text-emerald-700"></i>
|
|
</div>
|
|
<div id="add-form" class="p-6 hidden animate-fade-in">
|
|
<form method="POST" enctype="multipart/form-data" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<input type="hidden" name="add_content" value="1">
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">หัวข้อเนื้อหา / ชื่อเรื่อง <span class="text-red-500">*</span></label>
|
|
<input type="text" name="title" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">หมวดหมู่ <span class="text-red-500">*</span></label>
|
|
<select name="category_slug" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
<?php
|
|
$cats = $pdo->query("SELECT * FROM categories WHERE slug IN ($inSlugs)")->fetchAll();
|
|
foreach ($cats as $c) echo "<option value='{$c['slug']}'>{$c['name']}</option>";
|
|
?>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">รายละเอียดเพิ่มเติม (ไม่บังคับ)</label>
|
|
<input type="text" name="extra_detail" placeholder="เช่น 5 อัตรา, ราคา 990 บาท, ชื่อหมอ" class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">ลิงก์ภายนอก / Google Photos (ไม่บังคับ)</label>
|
|
<input type="url" name="external_link" placeholder="https://..." class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div class="md:col-span-2">
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">อัปโหลดไฟล์แนบ / รูปภาพหน้าปก</label>
|
|
<div class="relative flex items-center justify-center w-full">
|
|
<label class="flex flex-col items-center justify-center w-full h-32 border-2 border-gray-300 border-dashed rounded-xl cursor-pointer bg-gray-50 hover:bg-gray-100 transition-colors">
|
|
<div class="flex flex-col items-center justify-center pt-5 pb-6">
|
|
<i data-lucide="upload-cloud" class="w-8 h-8 text-gray-400 mb-2"></i>
|
|
<p class="text-sm text-gray-500"><span class="font-bold">คลิกเพื่ออัปโหลด</span> หรือลากไฟล์มาวาง</p>
|
|
</div>
|
|
<input type="file" name="file" class="hidden" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="md:col-span-2 flex justify-end mt-2">
|
|
<button type="submit" class="bg-[#056839] text-white px-8 py-3 rounded-xl font-bold hover:bg-[#04522b] shadow-md shadow-emerald-500/20 flex items-center transition-all">
|
|
<i data-lucide="save" class="w-4 h-4 mr-2"></i> บันทึกข้อมูล
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 relative">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
|
<h2 class="text-xl font-bold text-gray-800 flex items-center">
|
|
<i data-lucide="database" class="w-5 h-5 mr-2 text-[#056839]"></i> <?= $tabTitle ?>
|
|
</h2>
|
|
</div>
|
|
|
|
<?php
|
|
// --- Logic สำหรับ Pagination & หมวดหมู่ ---
|
|
$cat_filter = $_GET['cat'] ?? '';
|
|
$page = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
|
|
$limit = 10;
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
// ตรวจสอบเงื่อนไข Where ตามหมวดหมู่ที่อยู่ใน Tab
|
|
$where = "WHERE c.category_slug IN ($inSlugs)";
|
|
$params = [];
|
|
if ($cat_filter) {
|
|
$where = "WHERE c.category_slug = ?";
|
|
$params[] = $cat_filter;
|
|
}
|
|
|
|
$stmtCount = $pdo->prepare("SELECT COUNT(*) FROM contents c $where");
|
|
$stmtCount->execute($params);
|
|
$totalItems = $stmtCount->fetchColumn();
|
|
$totalPages = ceil($totalItems / $limit);
|
|
|
|
$query = "SELECT c.*, cat.name as cat_name FROM contents c JOIN categories cat ON c.category_slug = cat.slug $where ORDER BY c.id DESC LIMIT $limit OFFSET $offset";
|
|
$stmt = $pdo->prepare($query);
|
|
$stmt->execute($params);
|
|
$contents = $stmt->fetchAll();
|
|
?>
|
|
|
|
<?php if ($tab != 'gallery' && $tab != 'ita'): ?>
|
|
<div class="flex overflow-x-auto space-x-2 mb-6 custom-scrollbar pb-3">
|
|
<a href="?tab=<?= $tab ?>" class="px-5 py-2 rounded-full <?= !$cat_filter ? 'bg-[#056839] text-white shadow-md' : 'bg-slate-100 text-gray-600 hover:bg-emerald-50' ?> text-sm font-bold whitespace-nowrap transition-colors border border-transparent">ทั้งหมด</a>
|
|
<?php foreach ($cats as $c): ?>
|
|
<a href="?tab=<?= $tab ?>&cat=<?= $c['slug'] ?>" class="px-5 py-2 rounded-full <?= $cat_filter == $c['slug'] ? 'bg-[#056839] text-white shadow-md' : 'bg-slate-100 text-gray-600 hover:bg-emerald-50' ?> text-sm font-bold whitespace-nowrap transition-colors border border-transparent"><?= htmlspecialchars($c['name']) ?></a>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<div class="overflow-x-auto border border-slate-100 rounded-xl">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead>
|
|
<tr class="bg-slate-50 border-b border-slate-100 text-sm text-gray-600">
|
|
<th class="p-4 font-bold">วันที่</th>
|
|
<th class="p-4 font-bold">หัวข้อเนื้อหา</th>
|
|
<th class="p-4 font-bold">หมวดหมู่</th>
|
|
<th class="p-4 font-bold text-center">ลิงก์ / ไฟล์แนบ</th>
|
|
<th class="p-4 font-bold text-center w-32">จัดการ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($contents) > 0): ?>
|
|
<?php foreach ($contents as $item):
|
|
$details = json_decode($item['details'], true) ?? [];
|
|
?>
|
|
<tr class="border-b border-slate-50 hover:bg-emerald-50/30 transition-colors group">
|
|
<td class="p-4 text-sm text-gray-500 whitespace-nowrap"><?= date('d/m/Y H:i', strtotime($item['created_at'])) ?></td>
|
|
<td class="p-4 font-medium text-gray-800 line-clamp-2" title="<?= htmlspecialchars($item['title']) ?>">
|
|
<?= htmlspecialchars($item['title']) ?>
|
|
</td>
|
|
<td class="p-4">
|
|
<span class="px-3 py-1 bg-gray-100 text-gray-600 text-xs font-bold rounded-lg whitespace-nowrap border border-gray-200">
|
|
<?= $item['cat_name'] ?>
|
|
</span>
|
|
</td>
|
|
<td class="p-4 text-center">
|
|
<div class="flex justify-center space-x-2">
|
|
<?php if ($item['drive_file_url']): ?>
|
|
<a href="<?= $item['drive_file_url'] ?>" target="_blank" class="inline-flex items-center justify-center p-2 bg-blue-50 text-blue-600 hover:bg-blue-100 rounded-lg transition-colors border border-blue-100" title="ดูไฟล์แนบ">
|
|
<i data-lucide="file" class="w-4 h-4"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (isset($details['gphoto_link'])): ?>
|
|
<a href="<?= $details['gphoto_link'] ?>" target="_blank" class="inline-flex items-center justify-center p-2 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-lg transition-colors border border-purple-100" title="ลิงก์ภายนอก">
|
|
<i data-lucide="link" class="w-4 h-4"></i>
|
|
</a>
|
|
<?php endif; ?>
|
|
<?php if (!$item['drive_file_url'] && !isset($details['gphoto_link'])) echo "<span class='text-gray-300'>-</span>"; ?>
|
|
</div>
|
|
</td>
|
|
<td class="p-4 text-center">
|
|
<div class="flex items-center justify-center space-x-2">
|
|
<button type="button" onclick='openEditModal(<?= json_encode($item['id']) ?>, <?= htmlspecialchars(json_encode($item['title']), ENT_QUOTES, 'UTF-8') ?>, <?= json_encode($item['category_slug']) ?>, <?= json_encode($item['drive_file_url']) ?>, <?= htmlspecialchars(json_encode($details['amount'] ?? ''), ENT_QUOTES, 'UTF-8') ?>, <?= htmlspecialchars(json_encode($details['gphoto_link'] ?? ''), ENT_QUOTES, 'UTF-8') ?>)' class="inline-flex items-center justify-center p-2 bg-amber-50 text-amber-500 hover:bg-amber-500 hover:text-white rounded-lg transition-colors border border-amber-100" title="แก้ไขข้อมูล">
|
|
<i data-lucide="edit" class="w-4 h-4"></i>
|
|
</button>
|
|
<a href="?tab=<?= $tab ?>&delete_id=<?= $item['id'] ?>" onclick="return confirm('ข้อมูลและไฟล์แนบ จะถูกลบถาวร ยืนยันหรือไม่?');" class="inline-flex items-center justify-center p-2 bg-red-50 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-colors border border-red-100" title="ลบข้อมูล">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else: ?>
|
|
<tr>
|
|
<td colspan="5" class="p-8 text-center text-gray-400">
|
|
<div class="flex flex-col items-center justify-center">
|
|
<div class="bg-gray-50 p-4 rounded-full mb-3"><i data-lucide="inbox" class="w-8 h-8 text-gray-400"></i></div>
|
|
<p class="font-medium">ไม่มีข้อมูลในหมวดหมู่นี้</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<?php if ($totalItems > 0): ?>
|
|
<div class="mt-6 flex flex-col sm:flex-row items-center justify-between border-t border-slate-100 pt-4 gap-4">
|
|
<p class="text-sm text-gray-500 font-medium">
|
|
แสดงหน้า <?= $page ?> จาก <?= max(1, $totalPages) ?> <span class="hidden sm:inline">(รวมทั้งหมด <?= $totalItems ?> รายการ)</span>
|
|
</p>
|
|
<div class="flex space-x-1">
|
|
<?php if ($page > 1): ?>
|
|
<a href="?tab=<?= $tab ?>&cat=<?= $cat_filter ?>&page=<?= $page - 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
<?php
|
|
$startPage = max(1, $page - 2);
|
|
$endPage = min($totalPages, $page + 2);
|
|
for ($i = $startPage; $i <= $endPage; $i++):
|
|
?>
|
|
<a href="?tab=<?= $tab ?>&cat=<?= $cat_filter ?>&page=<?= $i ?>" class="flex items-center justify-center w-8 h-8 border rounded-lg text-sm font-bold transition-colors <?= $i == $page ? 'bg-[#056839] text-white border-[#056839] shadow-md' : 'border-slate-200 text-gray-600 hover:bg-emerald-50' ?>"><?= $i ?></a>
|
|
<?php endfor; ?>
|
|
<?php if ($page < $totalPages): ?>
|
|
<a href="?tab=<?= $tab ?>&cat=<?= $cat_filter ?>&page=<?= $page + 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Edit Modal Content -->
|
|
<div id="edit-modal" class="hidden fixed inset-0 z-50 items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 animate-fade-in overflow-y-auto">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-2xl overflow-hidden relative mx-auto my-auto border border-white/20">
|
|
<div class="p-5 bg-emerald-50/50 border-b border-slate-100 flex justify-between items-center">
|
|
<h2 class="text-lg font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="edit" class="w-5 h-5 mr-2"></i> แก้ไขข้อมูลเนื้อหา
|
|
</h2>
|
|
<button type="button" onclick="closeEditModal()" class="text-gray-400 hover:text-gray-600 bg-gray-100 rounded-full p-2 transition-colors">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6">
|
|
<form method="POST" enctype="multipart/form-data" class="space-y-4">
|
|
<input type="hidden" name="edit_content" value="1">
|
|
<input type="hidden" name="content_id" id="edit_content_id">
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">หัวข้อเนื้อหา <span class="text-red-500">*</span></label>
|
|
<input type="text" name="title" id="edit_title" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">หมวดหมู่ <span class="text-red-500">*</span></label>
|
|
<select name="category_slug" id="edit_category" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
<?php foreach ($cats as $c): ?>
|
|
<option value="<?= $c['slug'] ?>"><?= htmlspecialchars($c['name']) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">รายละเอียดเพิ่มเติม</label>
|
|
<input type="text" name="extra_detail" id="edit_extra_detail" class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">ลิงก์ภายนอก / Google Photos</label>
|
|
<input type="url" name="external_link" id="edit_external_link" class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">ไฟล์แนบปัจจุบัน</label>
|
|
<div id="edit_current_file" class="p-4 bg-gray-50 rounded-xl border border-gray-200">
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">อัปโหลดไฟล์ใหม่ <span class="text-gray-400 font-normal">(จะแทนที่ไฟล์เดิมบน Drive)</span></label>
|
|
<input type="file" name="file" class="w-full px-4 py-3 border border-gray-200 border-dashed rounded-xl bg-gray-50 hover:bg-gray-100 cursor-pointer">
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t border-slate-100">
|
|
<button type="button" onclick="closeEditModal()" class="px-6 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors">
|
|
ยกเลิก
|
|
</button>
|
|
<button type="submit" class="bg-[#056839] text-white px-6 py-2.5 rounded-xl font-bold hover:bg-[#04522b] shadow-md transition-all flex items-center">
|
|
<i data-lucide="save" class="w-4 h-4 mr-2"></i> บันทึกการแก้ไข
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ==========================================
|
|
ส่วนตั้งค่าโครงสร้าง
|
|
========================================== -->
|
|
<?php elseif ($tab === 'web_structure'): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm p-6 max-w-4xl mb-8">
|
|
<h2 class="text-xl font-bold mb-6 text-[#056839] flex items-center"><i data-lucide="layout" class="w-5 h-5 mr-2"></i> จัดการโครงสร้างหน้าเว็บ (หน้าปก / ผู้อำนวยการ / บริจาค)</h2>
|
|
<form method="POST" enctype="multipart/form-data" class="space-y-8">
|
|
<input type="hidden" name="update_settings" value="1">
|
|
<?php
|
|
$web_settings_keys = ['hero_title', 'hero_subtitle', 'director_name', 'director_message', 'donate_promptpay', 'hero_photo_url', 'director_photo_url', 'donate_qrcode_url'];
|
|
$in = str_repeat('?,', count($web_settings_keys) - 1) . '?';
|
|
$stmtSet = $pdo->prepare("SELECT * FROM settings WHERE setting_key IN ($in)");
|
|
$stmtSet->execute($web_settings_keys);
|
|
$setVals = [];
|
|
$setLbls = [];
|
|
while ($row = $stmtSet->fetch()) {
|
|
$setVals[$row['setting_key']] = $row['setting_value'];
|
|
$setLbls[$row['setting_key']] = $row['setting_label'];
|
|
}
|
|
?>
|
|
|
|
<!-- 1. ส่วนหน้าปก (Cover) -->
|
|
<div class="p-5 border border-slate-200 rounded-2xl bg-slate-50/50">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center"><i data-lucide="image" class="w-5 h-5 mr-2 text-emerald-600"></i> 1. ส่วนหน้าปกเว็บไซต์</h3>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($setLbls['hero_title'] ?? 'ข้อความหลัก (Title)') ?></label>
|
|
<input type="text" name="settings[hero_title]" value="<?= htmlspecialchars($setVals['hero_title'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($setLbls['hero_subtitle'] ?? 'ข้อความรอง (Subtitle)') ?></label>
|
|
<input type="text" name="settings[hero_subtitle]" value="<?= htmlspecialchars($setVals['hero_subtitle'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2 text-gray-700">รูปภาพหน้าปก</label>
|
|
<?php if (!empty($setVals['hero_photo_url'])): ?>
|
|
<div class="mb-3"><img src="<?= htmlspecialchars($setVals['hero_photo_url']) ?>" alt="Hero Photo" class="h-40 w-full md:w-1/2 object-cover rounded-xl border border-gray-200 shadow-sm"></div>
|
|
<?php endif; ?>
|
|
<input type="file" name="hero_photo" class="w-full px-4 py-2 border border-gray-200 rounded-xl bg-white cursor-pointer">
|
|
<p class="text-xs text-gray-500 mt-1">อัปโหลดไฟล์รูปภาพใหม่เพื่อแทนที่รูปหน้าปกเดิม แนะนำสัดส่วนภาพแนวนอนกว้าง</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. ส่วนผู้อำนวยการ -->
|
|
<div class="p-5 border border-slate-200 rounded-2xl bg-slate-50/50">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center"><i data-lucide="user" class="w-5 h-5 mr-2 text-emerald-600"></i> 2. ส่วนผู้อำนวยการ</h3>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($setLbls['director_name'] ?? 'ชื่อผู้อำนวยการ') ?></label>
|
|
<input type="text" name="settings[director_name]" value="<?= htmlspecialchars($setVals['director_name'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($setLbls['director_message'] ?? 'สารจากผู้อำนวยการ') ?></label>
|
|
<textarea name="settings[director_message]" rows="3" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none"><?= htmlspecialchars($setVals['director_message'] ?? '') ?></textarea>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2 text-gray-700">รูปผู้อำนวยการ</label>
|
|
<?php if (!empty($setVals['director_photo_url'])): ?>
|
|
<div class="mb-3"><img src="<?= htmlspecialchars($setVals['director_photo_url']) ?>" alt="Director Photo" class="w-32 h-32 object-cover rounded-xl border border-gray-200 shadow-sm"></div>
|
|
<?php endif; ?>
|
|
<input type="file" name="director_photo" class="w-full px-4 py-2 border border-gray-200 rounded-xl bg-white cursor-pointer">
|
|
<p class="text-xs text-gray-500 mt-1">อัปโหลดไฟล์รูปภาพใหม่เพื่อแทนที่รูปผู้อำนวยการเดิม แนะนำภาพแนวตั้ง</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. ส่วนรับบริจาค -->
|
|
<div class="p-5 border border-slate-200 rounded-2xl bg-slate-50/50">
|
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center"><i data-lucide="gift" class="w-5 h-5 mr-2 text-emerald-600"></i> 3. ส่วนรับบริจาคสมทบทุน</h3>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($setLbls['donate_promptpay'] ?? 'เบอร์ PromptPay สำหรับบริจาค') ?></label>
|
|
<input type="text" name="settings[donate_promptpay]" value="<?= htmlspecialchars($setVals['donate_promptpay'] ?? '') ?>" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold mb-2 text-gray-700">รูป QR Code รับบริจาค</label>
|
|
<?php if (!empty($setVals['donate_qrcode_url'])): ?>
|
|
<div class="mb-3"><img src="<?= htmlspecialchars($setVals['donate_qrcode_url']) ?>" alt="QR Code" class="w-32 h-32 object-contain bg-white rounded-xl border border-gray-200 shadow-sm p-2"></div>
|
|
<?php endif; ?>
|
|
<input type="file" name="donate_qrcode" class="w-full px-4 py-2 border border-gray-200 rounded-xl bg-white cursor-pointer">
|
|
<p class="text-xs text-gray-500 mt-1">อัปโหลดไฟล์รูปภาพ QR Code พร้อมเพย์ใหม่ (แนะนำพื้นหลังสีขาว)</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-4 flex justify-end">
|
|
<button type="submit" class="bg-[#056839] text-white px-8 py-3.5 rounded-xl font-bold hover:bg-[#04522b] shadow-lg shadow-emerald-900/20 transition-all flex items-center text-lg">
|
|
<i data-lucide="save" class="w-5 h-5 mr-2"></i> บันทึกการตั้งค่าโครงสร้างเว็บ
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- ==========================================
|
|
ส่วนระบบความคิดเห็น
|
|
========================================== -->
|
|
<?php elseif ($tab === 'feedback'): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm p-6 relative border border-slate-100">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
|
<h2 class="text-xl font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="message-square" class="w-5 h-5 mr-2"></i> ระบบความคิดเห็น
|
|
</h2>
|
|
<div class="flex items-center gap-2">
|
|
<form method="GET" class="flex items-center">
|
|
<input type="hidden" name="tab" value="feedback">
|
|
<select name="topic_filter" class="border border-gray-300 px-3 py-2 rounded-lg text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#056839]" onchange="this.form.submit()">
|
|
<option value="">-- กรองทุกหัวข้อ --</option>
|
|
<option value="ชื่นชมการให้บริการ" <?= isset($_GET['topic_filter']) && $_GET['topic_filter'] == 'ชื่นชมการให้บริการ' ? 'selected' : '' ?>>ชื่นชมการให้บริการ</option>
|
|
<option value="เสนอแนะการให้บริการ" <?= isset($_GET['topic_filter']) && $_GET['topic_filter'] == 'เสนอแนะการให้บริการ' ? 'selected' : '' ?>>เสนอแนะการให้บริการ</option>
|
|
<option value="แจ้งปัญหา / ร้องเรียน" <?= isset($_GET['topic_filter']) && $_GET['topic_filter'] == 'แจ้งปัญหา / ร้องเรียน' ? 'selected' : '' ?>>แจ้งปัญหา / ร้องเรียน</option>
|
|
<option value="สอบถามข้อมูลทั่วไป" <?= isset($_GET['topic_filter']) && $_GET['topic_filter'] == 'สอบถามข้อมูลทั่วไป' ? 'selected' : '' ?>>สอบถามข้อมูลทั่วไป</option>
|
|
</select>
|
|
</form>
|
|
<a href="?export_feedback=1&topic_filter=<?= htmlspecialchars($_GET['topic_filter'] ?? '') ?>" class="bg-emerald-600 hover:bg-emerald-700 text-white px-4 py-2 rounded-lg text-sm font-bold shadow flex items-center transition-colors">
|
|
<i data-lucide="download" class="w-4 h-4 mr-2"></i> Export Excel
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<?php
|
|
$topic_filter = $_GET['topic_filter'] ?? '';
|
|
$page = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
|
|
$limit = 10;
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
$whereFb = "";
|
|
$paramsFb = [];
|
|
if (!empty($topic_filter)) {
|
|
$whereFb = "WHERE topic = ?";
|
|
$paramsFb[] = $topic_filter;
|
|
}
|
|
|
|
$stmtCountFb = $pdo->prepare("SELECT COUNT(*) FROM feedbacks $whereFb");
|
|
$stmtCountFb->execute($paramsFb);
|
|
$totalFbItems = $stmtCountFb->fetchColumn();
|
|
$totalFbPages = ceil($totalFbItems / $limit);
|
|
|
|
$stmtFb = $pdo->prepare("SELECT * FROM feedbacks $whereFb ORDER BY created_at DESC LIMIT $limit OFFSET $offset");
|
|
$stmtFb->execute($paramsFb);
|
|
$feedbacks = $stmtFb->fetchAll();
|
|
?>
|
|
|
|
<div class="overflow-x-auto border border-slate-100 rounded-xl">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead class="bg-slate-50 border-b border-slate-100 text-sm text-gray-600">
|
|
<tr>
|
|
<th class="p-4 font-bold">วันที่</th>
|
|
<th class="p-4 font-bold">ชื่อ-นามสกุล</th>
|
|
<th class="p-4 font-bold">หัวข้อ</th>
|
|
<th class="p-4 font-bold">รายละเอียด</th>
|
|
<?php if ($userRole === 'admin'): ?>
|
|
<th class="p-4 font-bold text-center w-24">จัดการ</th>
|
|
<?php endif; ?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($feedbacks) > 0): foreach ($feedbacks as $fb): ?>
|
|
<tr class="border-b border-slate-50 hover:bg-emerald-50/30 transition-colors">
|
|
<td class="p-4 text-sm text-gray-500 whitespace-nowrap"><?= date('d/m/Y H:i', strtotime($fb['created_at'])) ?></td>
|
|
<td class="p-4 font-medium text-gray-800 whitespace-nowrap"><?= htmlspecialchars($fb['name'] ?: 'ไม่ระบุนาม') ?></td>
|
|
<td class="p-4">
|
|
<span class="px-2 py-1 bg-amber-100 text-amber-700 text-xs font-bold rounded-md whitespace-nowrap border border-amber-200"><?= htmlspecialchars($fb['topic']) ?></span>
|
|
</td>
|
|
<td class="p-4 text-sm text-gray-700"><?= htmlspecialchars($fb['message']) ?></td>
|
|
<?php if ($userRole === 'admin'): ?>
|
|
<td class="p-4 text-center">
|
|
<a href="?tab=feedback&delete_feedback=<?= $fb['id'] ?>" onclick="return confirm('ยืนยันการลบความคิดเห็นนี้?');" class="inline-flex items-center justify-center p-2 bg-red-50 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-colors border border-red-100" title="ลบข้อมูล">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</a>
|
|
</td>
|
|
<?php endif; ?>
|
|
</tr>
|
|
<?php endforeach;
|
|
else: ?>
|
|
<tr>
|
|
<td colspan="<?= $userRole === 'admin' ? '5' : '4' ?>" class="p-10 text-center text-gray-400">
|
|
<div class="flex flex-col items-center justify-center">
|
|
<div class="bg-gray-50 p-4 rounded-full mb-3"><i data-lucide="inbox" class="w-8 h-8 text-gray-400"></i></div>
|
|
<p class="font-medium">ไม่มีข้อมูลความคิดเห็น</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination Feedback -->
|
|
<?php if ($totalFbItems > 0): ?>
|
|
<div class="mt-6 flex flex-col sm:flex-row items-center justify-between border-t border-slate-100 pt-4 gap-4">
|
|
<p class="text-sm text-gray-500 font-medium">
|
|
แสดงหน้า <?= $page ?> จาก <?= max(1, $totalFbPages) ?> <span class="hidden sm:inline">(รวมทั้งหมด <?= $totalFbItems ?> รายการ)</span>
|
|
</p>
|
|
<div class="flex space-x-1">
|
|
<?php if ($page > 1): ?>
|
|
<a href="?tab=feedback&topic_filter=<?= urlencode($topic_filter) ?>&page=<?= $page - 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
<?php
|
|
$startPageFb = max(1, $page - 2);
|
|
$endPageFb = min($totalFbPages, $page + 2);
|
|
for ($i = $startPageFb; $i <= $endPageFb; $i++):
|
|
?>
|
|
<a href="?tab=feedback&topic_filter=<?= urlencode($topic_filter) ?>&page=<?= $i ?>" class="flex items-center justify-center w-8 h-8 border rounded-lg text-sm font-bold transition-colors <?= $i == $page ? 'bg-[#056839] text-white border-[#056839] shadow-md' : 'border-slate-200 text-gray-600 hover:bg-emerald-50' ?>"><?= $i ?></a>
|
|
<?php endfor; ?>
|
|
<?php if ($page < $totalFbPages): ?>
|
|
<a href="?tab=feedback&topic_filter=<?= urlencode($topic_filter) ?>&page=<?= $page + 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- ==========================================
|
|
ส่วนผู้ใช้งาน (Admin Only)
|
|
========================================== -->
|
|
<?php elseif ($tab === 'users' && $userRole === 'admin'): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 mb-8 overflow-hidden">
|
|
<div class="p-5 bg-emerald-50/50 border-b border-slate-100 flex justify-between items-center cursor-pointer hover:bg-emerald-50 transition-colors" onclick="document.getElementById('add-user-form').classList.toggle('hidden')">
|
|
<h2 class="text-lg font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="user-plus" class="w-5 h-5 mr-2"></i> เพิ่มผู้ใช้งานระบบ
|
|
</h2>
|
|
<i data-lucide="chevron-down" class="w-5 h-5 text-emerald-700"></i>
|
|
</div>
|
|
<div id="add-user-form" class="p-6 hidden animate-fade-in">
|
|
<form method="POST" class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<input type="hidden" name="add_user" value="1">
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">เลขประจำตัวประชาชน <span class="text-red-500">*</span></label>
|
|
<input type="text" name="cid" required maxlength="13" class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]" placeholder="13 หลัก">
|
|
</div>
|
|
<div class="relative">
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">รหัสผ่าน <span class="text-red-500">*</span></label>
|
|
<input type="password" id="add-user-pwd" name="password" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839] pr-10">
|
|
<button type="button" class="absolute inset-y-0 right-0 pt-7 pr-3 flex items-center text-gray-400 hover:text-emerald-600" onclick="togglePassword('add-user-pwd', this)">
|
|
<i data-lucide="eye" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">ชื่อ - นามสกุล <span class="text-red-500">*</span></label>
|
|
<input type="text" name="name" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">สิทธิ์การใช้งาน <span class="text-red-500">*</span></label>
|
|
<select name="role" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
<option value="admin">ผู้ดูแลระบบ (Admin)</option>
|
|
<option value="editor">จัดการเว็บไซต์ (Editor)</option>
|
|
</select>
|
|
</div>
|
|
<div class="md:col-span-2 flex justify-end mt-2">
|
|
<button type="submit" class="bg-[#056839] text-white px-8 py-3 rounded-xl font-bold hover:bg-[#04522b] shadow-md transition-all flex items-center">
|
|
<i data-lucide="save" class="w-4 h-4 mr-2"></i> บันทึกข้อมูล
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl shadow-sm p-6 relative border border-slate-100">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h2 class="text-xl font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="users" class="w-5 h-5 mr-2"></i> รายชื่อผู้ใช้งานในระบบ
|
|
</h2>
|
|
</div>
|
|
|
|
<?php
|
|
$page = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
|
|
$limit = 10;
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
$stmtCountUser = $pdo->query("SELECT COUNT(*) FROM users");
|
|
$totalUserItems = $stmtCountUser->fetchColumn();
|
|
$totalUserPages = ceil($totalUserItems / $limit);
|
|
|
|
$stmtUser = $pdo->query("SELECT * FROM users ORDER BY id DESC LIMIT $limit OFFSET $offset");
|
|
$users = $stmtUser->fetchAll();
|
|
?>
|
|
|
|
<div class="overflow-x-auto border border-slate-100 rounded-xl">
|
|
<table class="w-full text-left border-collapse">
|
|
<thead class="bg-slate-50 border-b border-slate-100 text-sm text-gray-600">
|
|
<tr>
|
|
<th class="p-4 font-bold w-16 text-center">ID</th>
|
|
<th class="p-4 font-bold">เลขประจำตัวประชาชน</th>
|
|
<th class="p-4 font-bold">ชื่อ-นามสกุล</th>
|
|
<th class="p-4 font-bold text-center">สิทธิ์การใช้งาน</th>
|
|
<th class="p-4 font-bold text-center">สถานะ 2FA</th>
|
|
<th class="p-4 font-bold text-center w-32">จัดการ</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($users) > 0): foreach ($users as $u): ?>
|
|
<tr class="border-b border-slate-50 hover:bg-emerald-50/30 transition-colors">
|
|
<td class="p-4 text-gray-500 text-center font-medium"><?= $u['id'] ?></td>
|
|
<td class="p-4 font-medium text-gray-800"><?= htmlspecialchars($u['cid']) ?></td>
|
|
<td class="p-4 text-gray-700"><?= htmlspecialchars($u['name']) ?></td>
|
|
<td class="p-4 text-center">
|
|
<?php if ($u['role'] == 'admin'): ?>
|
|
<span class="px-3 py-1 bg-red-100 text-red-700 text-xs font-bold rounded-full border border-red-200">Admin</span>
|
|
<?php else: ?>
|
|
<span class="px-3 py-1 bg-blue-100 text-blue-700 text-xs font-bold rounded-full border border-blue-200">Editor</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="p-4 text-center">
|
|
<?php if (!empty($u['two_factor_secret'])): ?>
|
|
<span class="px-3 py-1 bg-emerald-100 text-emerald-700 text-xs font-bold rounded-full border border-emerald-200"><i data-lucide="shield-check" class="w-3 h-3 inline mr-1"></i> เปิดใช้งาน</span>
|
|
<?php else: ?>
|
|
<span class="px-3 py-1 bg-gray-100 text-gray-500 text-xs font-bold rounded-full border border-gray-200"><i data-lucide="shield" class="w-3 h-3 inline mr-1"></i> ปิด</span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td class="p-4 text-center">
|
|
<div class="flex items-center justify-center space-x-2">
|
|
<!-- ปุ่มจัดการ 2FA -->
|
|
<button type="button" onclick='open2FAModal(<?= $u['id'] ?>, <?= json_encode(htmlspecialchars($u['name'], ENT_QUOTES)) ?>, <?= !empty($u['two_factor_secret']) ? "true" : "false" ?>)' class="inline-flex items-center justify-center p-2 bg-blue-50 text-blue-600 hover:bg-blue-600 hover:text-white rounded-lg transition-colors border border-blue-100" title="ตั้งค่า 2FA">
|
|
<i data-lucide="scan-line" class="w-4 h-4"></i>
|
|
</button>
|
|
<!-- ปุ่มแก้ไขผู้ใช้ -->
|
|
<button type="button" onclick='openUserEditModal(<?= $u['id'] ?>, <?= json_encode(htmlspecialchars($u['name'], ENT_QUOTES)) ?>, <?= json_encode($u['role']) ?>)' class="inline-flex items-center justify-center p-2 bg-amber-50 text-amber-500 hover:bg-amber-500 hover:text-white rounded-lg transition-colors border border-amber-100" title="แก้ไขข้อมูล">
|
|
<i data-lucide="user-cog" class="w-4 h-4"></i>
|
|
</button>
|
|
<!-- ปุ่มลบผู้ใช้ -->
|
|
<?php if ($u['id'] != $_SESSION['user_id']): ?>
|
|
<a href="?tab=users&delete_user=<?= $u['id'] ?>" onclick="return confirm('ยืนยันการลบผู้ใช้งานท่านนี้ออกจากระบบ?');" class="inline-flex items-center justify-center p-2 bg-red-50 text-red-500 hover:bg-red-500 hover:text-white rounded-lg transition-colors border border-red-100" title="ลบข้อมูล">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</a>
|
|
<?php else: ?>
|
|
<span class="inline-flex items-center justify-center p-2 bg-gray-100 text-gray-400 rounded-lg border border-gray-200 cursor-not-allowed" title="ไม่สามารถลบตัวเองได้">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i>
|
|
</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach;
|
|
else: ?>
|
|
<tr>
|
|
<td colspan="6" class="p-8 text-center text-gray-400">ไม่มีข้อมูลผู้ใช้งาน</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ($totalUserItems > 0): ?>
|
|
<div class="mt-6 flex flex-col sm:flex-row items-center justify-between border-t border-slate-100 pt-4 gap-4">
|
|
<p class="text-sm text-gray-500 font-medium">
|
|
แสดงหน้า <?= $page ?> จาก <?= max(1, $totalUserPages) ?>
|
|
</p>
|
|
<div class="flex space-x-1">
|
|
<?php if ($page > 1): ?>
|
|
<a href="?tab=users&page=<?= $page - 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
<?php
|
|
$startPageU = max(1, $page - 2);
|
|
$endPageU = min($totalUserPages, $page + 2);
|
|
for ($i = $startPageU; $i <= $endPageU; $i++):
|
|
?>
|
|
<a href="?tab=users&page=<?= $i ?>" class="flex items-center justify-center w-8 h-8 border rounded-lg text-sm font-bold transition-colors <?= $i == $page ? 'bg-[#056839] text-white border-[#056839] shadow-md' : 'border-slate-200 text-gray-600 hover:bg-emerald-50' ?>"><?= $i ?></a>
|
|
<?php endfor; ?>
|
|
<?php if ($page < $totalUserPages): ?>
|
|
<a href="?tab=users&page=<?= $page + 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div id="edit-user-modal" class="hidden fixed inset-0 z-50 items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 animate-fade-in overflow-y-auto">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-lg overflow-hidden relative mx-auto my-auto border border-white/20">
|
|
<div class="p-5 bg-emerald-50/50 border-b border-slate-100 flex justify-between items-center">
|
|
<h2 class="text-lg font-bold text-[#056839] flex items-center">
|
|
<i data-lucide="user-cog" class="w-5 h-5 mr-2"></i> แก้ไขข้อมูลผู้ใช้งาน
|
|
</h2>
|
|
<button type="button" onclick="closeUserEditModal()" class="text-gray-400 hover:text-gray-600 bg-gray-100 rounded-full p-2 transition-colors">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6">
|
|
<form method="POST" class="space-y-4">
|
|
<input type="hidden" name="edit_user" value="1">
|
|
<input type="hidden" name="user_id" id="edit_user_id">
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">ชื่อ - นามสกุล <span class="text-red-500">*</span></label>
|
|
<input type="text" name="name" id="edit_user_name" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-bold text-gray-700 mb-2">สิทธิ์การใช้งาน <span class="text-red-500">*</span></label>
|
|
<select name="role" id="edit_user_role" required class="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#056839]">
|
|
<option value="admin">ผู้ดูแลระบบ (Admin)</option>
|
|
<option value="editor">จัดการเว็บไซต์ (Editor)</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="p-4 bg-amber-50 border border-amber-200 rounded-xl mt-4 relative">
|
|
<label class="block text-sm font-bold text-amber-800 mb-2">เปลี่ยนรหัสผ่านใหม่ (ไม่บังคับ)</label>
|
|
<input type="password" id="edit-user-pwd" name="password" class="w-full px-4 py-3 bg-white border border-amber-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 pr-10" placeholder="ปล่อยว่างไว้หากไม่ต้องการเปลี่ยน">
|
|
<button type="button" class="absolute inset-y-0 right-0 pt-7 pr-3 flex items-center text-gray-400 hover:text-emerald-600" onclick="togglePassword('edit-user-pwd', this)">
|
|
<i data-lucide="eye" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex justify-end space-x-3 mt-6 pt-4 border-t border-slate-100">
|
|
<button type="button" onclick="closeUserEditModal()" class="px-6 py-2.5 rounded-xl border border-gray-200 text-gray-600 font-bold hover:bg-gray-50 transition-colors">
|
|
ยกเลิก
|
|
</button>
|
|
<button type="submit" class="bg-[#056839] text-white px-6 py-2.5 rounded-xl font-bold hover:bg-[#04522b] shadow-md transition-all flex items-center">
|
|
<i data-lucide="save" class="w-4 h-4 mr-2"></i> บันทึก
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Manage 2FA Modal -->
|
|
<div id="manage-2fa-modal" class="hidden fixed inset-0 z-50 items-center justify-center bg-slate-900/60 backdrop-blur-sm p-4 animate-fade-in overflow-y-auto">
|
|
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-md overflow-hidden relative mx-auto my-auto border border-white/20">
|
|
<div class="p-5 bg-blue-50 border-b border-blue-100 flex justify-between items-center">
|
|
<h2 class="text-lg font-bold text-blue-700 flex items-center">
|
|
<i data-lucide="shield" class="w-5 h-5 mr-2"></i> จัดการระบบ 2FA
|
|
</h2>
|
|
<button type="button" onclick="close2FAModal()" class="text-gray-400 hover:text-gray-600 bg-white rounded-full p-2 transition-colors">
|
|
<i data-lucide="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
<div class="p-6 text-center">
|
|
<h3 id="fa_user_name" class="font-extrabold text-gray-800 text-xl mb-4">ชื่อผู้ใช้</h3>
|
|
|
|
<div id="fa_status_disabled" class="hidden">
|
|
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4 text-gray-400">
|
|
<i data-lucide="shield-off" class="w-8 h-8"></i>
|
|
</div>
|
|
<p class="text-gray-600 mb-6 text-sm">ผู้ใช้งานรายนี้ยังไม่ได้เปิดระบบยืนยันตัวตน 2 ขั้นตอน (2FA)</p>
|
|
<form method="POST">
|
|
<input type="hidden" name="action_2fa" value="generate">
|
|
<input type="hidden" name="user_id" id="fa_gen_user_id">
|
|
<button type="submit" class="w-full bg-blue-600 text-white py-3 rounded-xl font-bold shadow-lg shadow-blue-500/30 hover:bg-blue-700 transition-colors">สร้างรหัส 2FA (QR Code)</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div id="fa_status_enabled" class="hidden">
|
|
<div class="w-16 h-16 bg-emerald-100 rounded-full flex items-center justify-center mx-auto mb-4 text-emerald-600 shadow-inner">
|
|
<i data-lucide="shield-check" class="w-8 h-8"></i>
|
|
</div>
|
|
<p class="text-emerald-600 font-bold mb-6">ผู้ใช้งานรายนี้เปิดระบบ 2FA แล้ว</p>
|
|
<form method="POST" onsubmit="return confirm('ยืนยันการยกเลิก 2FA ของผู้ใช้นี้?');">
|
|
<input type="hidden" name="action_2fa" value="remove">
|
|
<input type="hidden" name="user_id" id="fa_rm_user_id">
|
|
<button type="submit" class="w-full bg-red-50 text-red-600 border border-red-200 py-3 rounded-xl font-bold hover:bg-red-500 hover:text-white transition-colors">ยกเลิก 2FA (ปิดการใช้งาน)</button>
|
|
</form>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Show QR Code Modal (Auto Triggered after Generation) -->
|
|
<?php if (isset($_SESSION['show_qr_for_user'])):
|
|
$qrUserId = $_SESSION['show_qr_for_user'];
|
|
unset($_SESSION['show_qr_for_user']);
|
|
$stmtQR = $pdo->prepare("SELECT name, two_factor_secret FROM users WHERE id = ?");
|
|
$stmtQR->execute([$qrUserId]);
|
|
$qrUser = $stmtQR->fetch();
|
|
if ($qrUser && $qrUser['two_factor_secret']):
|
|
$issuer = rawurlencode($settings['hospital_name'] ?? 'SamuiHospital');
|
|
$accountName = rawurlencode($qrUser['name']);
|
|
$otpauthUrl = "otpauth://totp/{$issuer}:{$accountName}?secret={$qrUser['two_factor_secret']}&issuer={$issuer}";
|
|
$qrImageUrl = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=" . urlencode($otpauthUrl);
|
|
?>
|
|
<div id="show-qr-modal" class="fixed inset-0 z-[100] flex items-center justify-center bg-slate-900/80 backdrop-blur-md p-4 animate-fade-in overflow-y-auto">
|
|
<div class="bg-white rounded-3xl shadow-2xl w-full max-w-sm overflow-hidden relative mx-auto my-auto text-center p-8 border-4 border-blue-500">
|
|
<h2 class="text-2xl font-extrabold text-blue-700 mb-2">สแกน QR Code</h2>
|
|
<p class="text-gray-500 text-sm mb-6">กรุณาให้ผู้ใช้งาน (<?= htmlspecialchars($qrUser['name']) ?>) <br>ใช้แอป Google Authenticator สแกนรูปด้านล่าง</p>
|
|
|
|
<div class="bg-white p-4 rounded-2xl border-2 border-slate-100 inline-block mb-6 shadow-sm">
|
|
<img src="<?= $qrImageUrl ?>" alt="2FA QR Code" class="w-48 h-48">
|
|
</div>
|
|
|
|
<div class="bg-blue-50 p-3 rounded-xl border border-blue-100 mb-6">
|
|
<p class="text-xs text-blue-600 font-bold mb-1">หรือกรอกรหัส (Setup Key) แทน:</p>
|
|
<p class="text-sm font-mono font-bold tracking-widest text-gray-800 bg-white py-1 px-2 rounded border border-gray-200"><?= $qrUser['two_factor_secret'] ?></p>
|
|
</div>
|
|
|
|
<button type="button" onclick="document.getElementById('show-qr-modal').remove()" class="w-full bg-slate-800 text-white px-6 py-3 rounded-xl font-bold hover:bg-slate-900 shadow-md transition-all">
|
|
ปิดหน้าต่างนี้
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<?php endif;
|
|
endif; ?>
|
|
|
|
<!-- ==========================================
|
|
ส่วนการตั้งค่า (Admin Only)
|
|
========================================== -->
|
|
<?php elseif ($tab === 'settings' && $userRole === 'admin'): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm p-6 max-w-2xl border border-slate-100">
|
|
<h2 class="text-xl font-bold mb-6 text-[#056839] flex items-center"><i data-lucide="settings" class="w-5 h-5 mr-2"></i> ตั้งค่าระบบ และ Google Drive</h2>
|
|
<form method="POST">
|
|
<input type="hidden" name="update_settings" value="1">
|
|
<?php
|
|
// ยกเว้น keys ของ web_structure และรวมการตั้งค่า Telegram
|
|
$sys_settings_keys = "('hospital_name', 'contact_phone', 'hospital_address', 'system_version', 'drive_folder_id', 'facebook_url', 'enable_grayscale', 'telegram_bot_token', 'telegram_chat_id')";
|
|
$settings = $pdo->query("SELECT * FROM settings WHERE setting_key IN $sys_settings_keys")->fetchAll();
|
|
foreach ($settings as $s):
|
|
?>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-bold mb-2 text-gray-700"><?= htmlspecialchars($s['setting_label']) ?></label>
|
|
<?php if ($s['setting_key'] === 'hospital_address'): ?>
|
|
<textarea name="settings[<?= $s['setting_key'] ?>]" rows="3" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none"><?= htmlspecialchars($s['setting_value']) ?></textarea>
|
|
<?php elseif ($s['setting_key'] === 'enable_grayscale'): ?>
|
|
<select name="settings[<?= $s['setting_key'] ?>]" class="w-full px-4 py-2 border border-gray-200 rounded-xl bg-gray-50 focus:ring-2 focus:ring-[#056839] outline-none font-bold">
|
|
<option value="0" <?= $s['setting_value'] == '0' ? 'selected' : '' ?>>ปิด (แสดงสีปกติ)</option>
|
|
<option value="1" <?= $s['setting_value'] == '1' ? 'selected' : '' ?> class="text-gray-500">เปิด (โทนสีขาว-ดำ / ร่วมไว้อาลัย)</option>
|
|
</select>
|
|
<?php elseif ($s['setting_key'] === 'telegram_bot_token' || $s['setting_key'] === 'telegram_chat_id'): ?>
|
|
<input type="text" name="settings[<?= $s['setting_key'] ?>]" value="<?= htmlspecialchars($s['setting_value']) ?>" class="w-full px-4 py-2 border border-blue-200 bg-blue-50 rounded-xl focus:ring-2 focus:ring-blue-500 outline-none" placeholder="<?= $s['setting_key'] === 'telegram_bot_token' ? 'เช่น 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11' : 'เช่น -100123456789' ?>">
|
|
<?php else: ?>
|
|
<input type="text" name="settings[<?= $s['setting_key'] ?>]" value="<?= htmlspecialchars($s['setting_value']) ?>" class="w-full px-4 py-2 border border-gray-200 rounded-xl focus:ring-2 focus:ring-[#056839] outline-none">
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
<button type="submit" class="bg-[#056839] text-white px-8 py-3.5 rounded-xl font-bold hover:bg-emerald-700 mt-6 shadow-lg shadow-emerald-900/20 flex items-center transition-all text-lg">
|
|
<i data-lucide="save" class="w-5 h-5 mr-2"></i> บันทึกการตั้งค่า
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- ==========================================
|
|
ส่วน Logs
|
|
========================================== -->
|
|
<?php elseif ($tab === 'logs'): ?>
|
|
<div class="bg-white rounded-2xl shadow-sm p-6 border border-slate-100">
|
|
<div class="flex flex-col md:flex-row md:items-center justify-between mb-6 gap-4">
|
|
<h2 class="text-xl font-bold text-[#056839] flex items-center"><i data-lucide="file-clock" class="w-5 h-5 mr-2"></i> ประวัติการทำงานของระบบ (Logs)</h2>
|
|
<form method="GET" class="flex items-center">
|
|
<input type="hidden" name="tab" value="logs">
|
|
<select name="filter" class="border border-gray-300 px-3 py-2 rounded-lg text-sm bg-gray-50 focus:outline-none focus:ring-2 focus:ring-[#056839]" onchange="this.form.submit()">
|
|
<option value="">-- กรองสถานะทั้งหมด --</option>
|
|
<option value="CREATE" <?= isset($_GET['filter']) && $_GET['filter'] == 'CREATE' ? 'selected' : '' ?>>เพิ่มข้อมูล (CREATE)</option>
|
|
<option value="UPDATE" <?= isset($_GET['filter']) && $_GET['filter'] == 'UPDATE' ? 'selected' : '' ?>>แก้ไขข้อมูล (UPDATE)</option>
|
|
<option value="DELETE" <?= isset($_GET['filter']) && $_GET['filter'] == 'DELETE' ? 'selected' : '' ?>>ลบข้อมูล (DELETE)</option>
|
|
<option value="LOGIN" <?= isset($_GET['filter']) && $_GET['filter'] == 'LOGIN' ? 'selected' : '' ?>>เข้าสู่ระบบ (LOGIN)</option>
|
|
</select>
|
|
</form>
|
|
</div>
|
|
|
|
<?php
|
|
$log_filter = $_GET['filter'] ?? '';
|
|
$page = max(1, isset($_GET['page']) ? (int)$_GET['page'] : 1);
|
|
$limit = 15;
|
|
$offset = ($page - 1) * $limit;
|
|
|
|
$whereLog = "";
|
|
$paramsLog = [];
|
|
if (!empty($log_filter)) {
|
|
$whereLog = "WHERE action_type = ?";
|
|
$paramsLog[] = $log_filter;
|
|
}
|
|
|
|
$stmtCountLog = $pdo->prepare("SELECT COUNT(*) FROM system_logs $whereLog");
|
|
$stmtCountLog->execute($paramsLog);
|
|
$totalLogItems = $stmtCountLog->fetchColumn();
|
|
$totalLogPages = ceil($totalLogItems / $limit);
|
|
|
|
$stmtLog = $pdo->prepare("SELECT * FROM system_logs $whereLog ORDER BY id DESC LIMIT $limit OFFSET $offset");
|
|
$stmtLog->execute($paramsLog);
|
|
$logs = $stmtLog->fetchAll();
|
|
?>
|
|
|
|
<div class="overflow-x-auto border border-slate-100 rounded-xl">
|
|
<table class="w-full text-left border-collapse text-sm">
|
|
<thead class="bg-slate-50 border-b border-slate-100">
|
|
<tr>
|
|
<th class="p-3 font-bold">วันเวลา</th>
|
|
<th class="p-3 font-bold">ผู้ใช้งาน (ID)</th>
|
|
<th class="p-3 font-bold">สถานะ</th>
|
|
<th class="p-3 font-bold">โมดูล</th>
|
|
<th class="p-3 font-bold">รายละเอียด</th>
|
|
<th class="p-3 font-bold">IP Address</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (count($logs) > 0): foreach ($logs as $log): ?>
|
|
<tr class="border-b border-slate-50 hover:bg-slate-50">
|
|
<td class="p-3 text-gray-500 whitespace-nowrap"><?= $log['created_at'] ?></td>
|
|
<td class="p-3 font-medium text-gray-800"><?= $log['user_id'] ? 'Admin (' . $log['user_id'] . ')' : 'System' ?></td>
|
|
<td class="p-3">
|
|
<?php
|
|
$bgColor = 'bg-gray-100 text-gray-700 border-gray-200';
|
|
if ($log['action_type'] === 'CREATE') $bgColor = 'bg-emerald-100 text-emerald-700 border-emerald-200';
|
|
if ($log['action_type'] === 'UPDATE') $bgColor = 'bg-amber-100 text-amber-700 border-amber-200';
|
|
if ($log['action_type'] === 'DELETE') $bgColor = 'bg-red-100 text-red-700 border-red-200';
|
|
if ($log['action_type'] === 'LOGIN') $bgColor = 'bg-blue-100 text-blue-700 border-blue-200';
|
|
?>
|
|
<span class="px-2 py-1 rounded-md border text-[10px] font-bold <?= $bgColor ?>"><?= $log['action_type'] ?></span>
|
|
</td>
|
|
<td class="p-3 text-gray-500"><?= $log['module'] ?></td>
|
|
<td class="p-3 text-gray-700"><?= htmlspecialchars($log['description']) ?></td>
|
|
<td class="p-3 text-gray-400 text-xs"><?= $log['ip_address'] ?></td>
|
|
</tr>
|
|
<?php endforeach;
|
|
else: ?>
|
|
<tr>
|
|
<td colspan="6" class="p-10 text-center text-gray-400">
|
|
<div class="flex flex-col items-center justify-center">
|
|
<div class="bg-gray-50 p-4 rounded-full mb-3"><i data-lucide="inbox" class="w-8 h-8 text-gray-400"></i></div>
|
|
<p class="font-medium">ไม่มีข้อมูลประวัติการทำงาน</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Pagination Logs -->
|
|
<?php if ($totalLogItems > 0): ?>
|
|
<div class="mt-6 flex flex-col sm:flex-row items-center justify-between border-t border-slate-100 pt-4 gap-4">
|
|
<p class="text-sm text-gray-500 font-medium">
|
|
แสดงหน้า <?= $page ?> จาก <?= max(1, $totalLogPages) ?> <span class="hidden sm:inline">(รวมทั้งหมด <?= $totalLogItems ?> รายการ)</span>
|
|
</p>
|
|
<div class="flex space-x-1">
|
|
<?php if ($page > 1): ?>
|
|
<a href="?tab=logs&filter=<?= urlencode($log_filter) ?>&page=<?= $page - 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-left" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
<?php
|
|
$startPageLog = max(1, $page - 2);
|
|
$endPageLog = min($totalLogPages, $page + 2);
|
|
for ($i = $startPageLog; $i <= $endPageLog; $i++):
|
|
?>
|
|
<a href="?tab=logs&filter=<?= urlencode($log_filter) ?>&page=<?= $i ?>" class="flex items-center justify-center w-8 h-8 border rounded-lg text-sm font-bold transition-colors <?= $i == $page ? 'bg-[#056839] text-white border-[#056839] shadow-md' : 'border-slate-200 text-gray-600 hover:bg-emerald-50' ?>"><?= $i ?></a>
|
|
<?php endfor; ?>
|
|
<?php if ($page < $totalLogPages): ?>
|
|
<a href="?tab=logs&filter=<?= urlencode($log_filter) ?>&page=<?= $page + 1 ?>" class="flex items-center justify-center w-8 h-8 border border-slate-200 rounded-lg text-gray-600 hover:bg-emerald-50 hover:text-[#056839]"><i data-lucide="chevron-right" class="w-4 h-4"></i></a>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
</main>
|
|
<script>
|
|
lucide.createIcons(); // Initialize Icons
|
|
|
|
// Cloudflare WAF Bypass for File Uploads
|
|
document.addEventListener('submit', function(e) {
|
|
const form = e.target;
|
|
if (form.getAttribute('enctype') === 'multipart/form-data' && !form.dataset.base64Processed) {
|
|
e.preventDefault();
|
|
|
|
const fileInputs = form.querySelectorAll('input[type="file"]');
|
|
let filesToProcess = 0;
|
|
let filesProcessed = 0;
|
|
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
if (submitBtn) {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 mr-2 animate-spin"></i> กำลังอัปโหลด...';
|
|
lucide.createIcons();
|
|
}
|
|
|
|
fileInputs.forEach(input => {
|
|
if (input.files.length > 0) {
|
|
filesToProcess++;
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
reader.onload = function(evt) {
|
|
const base64Data = evt.target.result;
|
|
|
|
const hiddenInput = document.createElement('input');
|
|
hiddenInput.type = 'hidden';
|
|
hiddenInput.name = input.name + '_base64';
|
|
hiddenInput.value = base64Data;
|
|
form.appendChild(hiddenInput);
|
|
|
|
const nameInput = document.createElement('input');
|
|
nameInput.type = 'hidden';
|
|
nameInput.name = input.name + '_name';
|
|
nameInput.value = file.name;
|
|
form.appendChild(nameInput);
|
|
|
|
input.removeAttribute('name');
|
|
|
|
filesProcessed++;
|
|
if (filesProcessed === filesToProcess) {
|
|
form.dataset.base64Processed = "true";
|
|
form.submit();
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
|
|
if (filesToProcess === 0) {
|
|
form.dataset.base64Processed = "true";
|
|
form.submit();
|
|
}
|
|
}
|
|
});
|
|
|
|
function confirmLogout() {
|
|
Swal.fire({
|
|
title: 'ยืนยันการออกจากระบบ?',
|
|
text: "คุณต้องการออกจากระบบจัดการข้อมูลใช่หรือไม่",
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#ef4444',
|
|
cancelButtonColor: '#94a3b8',
|
|
confirmButtonText: 'ออกจากระบบ',
|
|
cancelButtonText: 'ยกเลิก',
|
|
reverseButtons: true,
|
|
customClass: {
|
|
title: 'font-bold text-xl',
|
|
popup: 'rounded-[2rem] dark:bg-slate-800 dark:text-gray-100',
|
|
confirmButton: 'rounded-xl font-bold px-6 py-2.5 text-sm',
|
|
cancelButton: 'rounded-xl font-bold px-6 py-2.5 text-sm'
|
|
}
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
window.location.href = 'logout.php';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Content Edit Modal
|
|
function openEditModal(id, title, category, fileUrl, extraDetail, externalLink) {
|
|
document.getElementById('edit_content_id').value = id;
|
|
document.getElementById('edit_title').value = title;
|
|
document.getElementById('edit_category').value = category;
|
|
|
|
if (document.getElementById('edit_extra_detail')) document.getElementById('edit_extra_detail').value = extraDetail || '';
|
|
if (document.getElementById('edit_external_link')) document.getElementById('edit_external_link').value = externalLink || '';
|
|
|
|
const fileLinkContainer = document.getElementById('edit_current_file');
|
|
if (fileUrl) {
|
|
fileLinkContainer.innerHTML = `<a href="${fileUrl}" target="_blank" class="text-blue-600 hover:text-blue-800 underline text-sm flex items-center font-bold"><i data-lucide="external-link" class="w-4 h-4 mr-2"></i> กดเพื่อดูไฟล์ปัจจุบันในระบบ</a>`;
|
|
} else {
|
|
fileLinkContainer.innerHTML = `<span class="text-gray-400 text-sm flex items-center font-medium"><i data-lucide="file-minus" class="w-4 h-4 mr-2"></i> ไม่มีไฟล์แนบในระบบ</span>`;
|
|
}
|
|
lucide.createIcons();
|
|
|
|
const modal = document.getElementById('edit-modal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function closeEditModal() {
|
|
const modal = document.getElementById('edit-modal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
// User Edit Modal
|
|
function openUserEditModal(id, name, role) {
|
|
document.getElementById('edit_user_id').value = id;
|
|
document.getElementById('edit_user_name').value = name;
|
|
document.getElementById('edit_user_role').value = role;
|
|
|
|
const modal = document.getElementById('edit-user-modal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function closeUserEditModal() {
|
|
const modal = document.getElementById('edit-user-modal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
// 2FA Management Modal
|
|
function open2FAModal(id, name, isEnabled) {
|
|
document.getElementById('fa_user_name').innerText = name;
|
|
document.getElementById('fa_gen_user_id').value = id;
|
|
document.getElementById('fa_rm_user_id').value = id;
|
|
|
|
if (isEnabled) {
|
|
document.getElementById('fa_status_enabled').classList.remove('hidden');
|
|
document.getElementById('fa_status_disabled').classList.add('hidden');
|
|
} else {
|
|
document.getElementById('fa_status_enabled').classList.add('hidden');
|
|
document.getElementById('fa_status_disabled').classList.remove('hidden');
|
|
}
|
|
|
|
const modal = document.getElementById('manage-2fa-modal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
}
|
|
|
|
function close2FAModal() {
|
|
const modal = document.getElementById('manage-2fa-modal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|