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,53 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['admin_logged_in'])) {
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
// Find CIDs that have duplicates
$stmt = $pdo_bot->query("
SELECT cid, COUNT(*) as c
FROM line_staff_register
GROUP BY cid
HAVING c > 1
");
$duplicates = $stmt->fetchAll();
$deleted_total = 0;
foreach ($duplicates as $dup) {
$cid = $dup['cid'];
// Get all records for this CID ordered by date DESC, then id DESC
$stmt = $pdo_bot->prepare("
SELECT id
FROM line_staff_register
WHERE cid = ?
ORDER BY date DESC, id DESC
");
$stmt->execute([$cid]);
$records = $stmt->fetchAll();
if (count($records) > 1) {
// Keep the first one (most recent date)
$keep_id = $records[0]['id'];
// Delete the rest
for ($i = 1; $i < count($records); $i++) {
$del_stmt = $pdo_bot->prepare("DELETE FROM line_staff_register WHERE id = ?");
$del_stmt->execute([$records[$i]['id']]);
$deleted_total++;
}
}
}
echo json_encode(['success' => true, 'deleted_count' => $deleted_total, 'message' => "ลบข้อมูลที่ซ้ำซ้อนสำเร็จ จำนวน $deleted_total รายการ"]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'message' => 'Error: ' . $e->getMessage()]);
}
}
@@ -0,0 +1,45 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['admin_logged_in'])) {
echo json_encode(['success' => false, 'message' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$cid = $_POST['cid'] ?? '';
if (empty($cid)) {
echo json_encode(['success' => false, 'message' => 'ไม่พบข้อมูล CID']);
exit;
}
if (!$conn_hosoffice) {
echo json_encode(['success' => false, 'message' => 'ไม่สามารถเชื่อมต่อฐานข้อมูล HR (hosoffice) ได้']);
exit;
}
$cid_safe = mysqli_real_escape_string($conn_hosoffice, $cid);
$query = "SELECT HR_STATUS_ID FROM hr_person WHERE HR_CID = '$cid_safe' LIMIT 1";
$result = mysqli_query($conn_hosoffice, $query);
if ($result && mysqli_num_rows($result) > 0) {
$row = mysqli_fetch_assoc($result);
$hr_status = $row['HR_STATUS_ID'];
$new_status = ($hr_status === '01') ? 'Y' : 'N';
// Update line_staff_register
$stmt = $pdo_bot->prepare("UPDATE line_staff_register SET ksh_user = ? WHERE cid = ?");
$stmt->execute([$new_status, $cid]);
echo json_encode(['success' => true, 'new_status' => $new_status, 'message' => 'อัปเดตสถานะเรียบร้อย']);
} else {
// Not found in hr_person
$stmt = $pdo_bot->prepare("UPDATE line_staff_register SET ksh_user = 'N' WHERE cid = ?");
$stmt->execute([$cid]);
echo json_encode(['success' => true, 'new_status' => 'N', 'message' => 'ไม่พบ CID นี้ในระบบ HR (อัปเดตเป็นพ้นสภาพ)']);
}
}
@@ -0,0 +1,133 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['admin_logged_in'])) {
header("Location: login.php");
exit;
}
// Ensure 2FA is setup
$stmt_admin = $pdo->prepare("SELECT username, role, google2fa_secret FROM admin_users WHERE id = ?");
$stmt_admin->execute([$_SESSION['admin_id']]);
$current_admin = $stmt_admin->fetch();
if (empty($current_admin['google2fa_secret'])) {
header("Location: setup_2fa.php");
exit;
}
$is_superadmin = ($current_admin['role'] === 'superadmin');
// Handle settings update
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['update_settings'])) {
$require = isset($_POST['require_assessment']) ? '1' : '0';
$stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = 'require_assessment'");
$stmt->execute([$require]);
$msg = "บันทึกการตั้งค่าเรียบร้อยแล้ว";
}
$require_assessment = getSetting($pdo, 'require_assessment');
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Dashboard - KSH-STAFF</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
</head>
<body class="bg-gray-100 font-sans antialiased text-gray-800 flex min-h-screen">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 text-gray-300 flex flex-col fixed top-0 left-0 h-screen shadow-lg z-20">
<div class="px-6 py-5 bg-gray-900 border-b border-gray-700 text-center">
<div class="relative inline-block overflow-hidden rounded-full w-20 h-20 mx-auto mb-3 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h3 class="text-white text-lg font-bold tracking-wide mt-1">KSH-STAFF Admin</h3>
</div>
<nav class="flex-1 pt-6 pb-4 overflow-y-auto flex flex-col gap-1">
<a href="index.php" class="block px-6 py-3 bg-blue-600 text-white font-medium transition duration-200 shadow-sm">
<i class="fas fa-home w-6 text-center mr-2"></i> แดชบอร์ด
</a>
<a href="staff_list.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-users w-6 text-center mr-2"></i> รายชื่อเจ้าหน้าที่
</a>
<?php if ($is_superadmin): ?>
<a href="manage_admins.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-crown w-6 text-center mr-2"></i> จัดการแอดมิน
</a>
<?php endif; ?>
<div class="mt-auto px-6 pt-4">
<a href="logout.php" class="flex items-center text-red-400 hover:text-red-300 hover:bg-red-400/10 py-2 px-3 rounded transition duration-200">
<i class="fas fa-sign-out-alt w-6 text-center mr-1"></i> ออกจากระบบ
</a>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64 flex flex-col min-h-screen">
<header class="bg-white px-8 py-5 border-b border-gray-200 flex justify-between items-center shadow-sm z-10 sticky top-0">
<h4 class="text-xl font-bold text-gray-700">การตั้งค่าระบบ</h4>
<div class="text-sm text-gray-500 font-medium px-4 py-1.5 bg-gray-100 rounded-full border border-gray-200 flex items-center gap-2">
ผู้ใช้งาน: <span class="text-blue-600 font-bold"><?= htmlspecialchars($current_admin['username'] ?? $_SESSION['admin_username'] ?? 'Admin') ?></span>
<span class="bg-gray-200 text-gray-600 px-2 py-0.5 rounded text-xs"><?= htmlspecialchars($current_admin['role'] ?? '') ?></span>
</div>
</header>
<div class="p-8 flex-1">
<?php if (!empty($msg)): ?>
<script>
Swal.fire({ icon: 'success', title: 'สำเร็จ', text: '<?= $msg ?>', timer: 2000, confirmButtonColor: '#2563eb' });
</script>
<?php endif; ?>
<div class="bg-white rounded-xl p-8 shadow-sm border border-gray-200 max-w-3xl">
<h3 class="text-lg font-bold text-gray-800 border-b border-gray-100 pb-4 mb-6"><i class="fas fa-cog text-gray-400 mr-2"></i> ตั้งค่าระบบประเมิน</h3>
<form method="post">
<div class="flex items-center gap-4 mb-8 bg-gray-50 p-4 rounded-lg border border-gray-100">
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" name="require_assessment" value="1" class="sr-only peer" <?= $require_assessment == '1' ? 'checked' : '' ?>>
<div class="w-14 h-7 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-6 after:w-6 after:transition-all peer-checked:bg-blue-600"></div>
</label>
<span class="text-gray-700 font-medium">เปิดใช้งานการบังคับทำแบบประเมิน <span class="text-gray-500 text-sm font-normal block mt-1">(เมนูต่างๆ จะถูกล็อกหากเจ้าหน้าที่ยังไม่ทำประเมิน)</span></span>
</div>
<button type="submit" name="update_settings" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-lg transition duration-200 shadow-sm flex items-center gap-2">
<i class="fas fa-save"></i> บันทึกการตั้งค่า
</button>
</form>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,181 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/GoogleAuthenticator.php';
$ga = new PHPGangsta_GoogleAuthenticator();
$step = 1;
$error = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['login_step_1'])) {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$stmt = $pdo->prepare("SELECT * FROM admin_users WHERE username = ?");
$stmt->execute([$username]);
$admin = $stmt->fetch();
if ($admin && password_verify($password, $admin['password_hash'])) {
// Step 1 Success
$_SESSION['temp_admin_id'] = $admin['id'];
$_SESSION['temp_admin_username'] = $admin['username'];
$_SESSION['temp_admin_role'] = $admin['role'];
$_SESSION['temp_admin_secret'] = $admin['google2fa_secret'];
if (empty($admin['google2fa_secret'])) {
// Not setup 2FA yet, go to setup
$_SESSION['admin_logged_in'] = true;
$_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username'];
$_SESSION['admin_role'] = $admin['role'];
unset($_SESSION['temp_admin_id'], $_SESSION['temp_admin_username'], $_SESSION['temp_admin_role'], $_SESSION['temp_admin_secret']);
header("Location: setup_2fa.php");
exit;
} else {
$step = 2; // Move to 2FA step
}
} else {
$error = "ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง";
}
} elseif (isset($_POST['login_step_2'])) {
$code = $_POST['code2fa'] ?? '';
$secret = $_SESSION['temp_admin_secret'];
if ($ga->verifyCode($secret, $code, 2)) {
// Success
$_SESSION['admin_logged_in'] = true;
$_SESSION['admin_id'] = $_SESSION['temp_admin_id'];
$_SESSION['admin_username'] = $_SESSION['temp_admin_username'];
$_SESSION['admin_role'] = $_SESSION['temp_admin_role'];
unset($_SESSION['temp_admin_id'], $_SESSION['temp_admin_username'], $_SESSION['temp_admin_role'], $_SESSION['temp_admin_secret']);
header("Location: index.php");
exit;
} else {
$error = "รหัส Google Authenticator ไม่ถูกต้อง";
$step = 2; // Stay at step 2
}
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin Login - KSH-STAFF</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center font-sans antialiased">
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-md border border-gray-100">
<div class="text-center mb-6">
<div class="relative inline-block overflow-hidden rounded-full w-24 h-24 mx-auto mb-4 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h2 class="text-blue-600 text-2xl font-bold">Admin Login</h2>
</div>
<?php if (!empty($error)): ?>
<div class="text-red-500 text-center text-sm mb-4 font-medium bg-red-50 py-2 rounded-lg"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<?php if ($step === 1): ?>
<form method="post" class="space-y-4">
<div>
<label class="block text-gray-700 text-sm font-medium mb-1">Username (หรือ CID)</label>
<input type="text" name="username" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" required autofocus>
</div>
<div>
<label class="block text-gray-700 text-sm font-medium mb-1">Password</label>
<input type="password" name="password" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" required>
</div>
<button type="submit" name="login_step_1" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-4 rounded-lg transition duration-200 shadow-md hover:shadow-lg mt-2">เข้าสู่ระบบ</button>
</form>
<?php else: ?>
<form method="post" id="otp-form" class="space-y-6">
<div class="text-center text-gray-600 text-sm leading-relaxed">
กรุณาเปิดแอปพลิเคชัน <strong class="text-gray-800">Google Authenticator</strong><br>และนำรหัส 6 หลักมากรอก
</div>
<div class="flex justify-center gap-2">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp2')" id="otp1" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition" autofocus>
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp3')" onkeydown="moveToPrev(event, this, 'otp1')" id="otp2" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp4')" onkeydown="moveToPrev(event, this, 'otp2')" id="otp3" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp5')" onkeydown="moveToPrev(event, this, 'otp3')" id="otp4" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp6')" onkeydown="moveToPrev(event, this, 'otp4')" id="otp5" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="submitOtp()" onkeydown="moveToPrev(event, this, 'otp5')" id="otp6" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
</div>
<input type="hidden" name="code2fa" id="code2fa" required>
<button type="submit" name="login_step_2" id="submit-btn" class="hidden">ยืนยันตัวตน</button>
<div class="text-center mt-6">
<a href="login.php" class="text-sm text-gray-500 hover:text-gray-800 hover:underline transition">ยกเลิก / กลับไปหน้าแรก</a>
</div>
</form>
<script>
function moveToNext(current, nextFieldId) {
if (current.value.length === 1) {
if (nextFieldId) document.getElementById(nextFieldId).focus();
}
}
function moveToPrev(e, current, prevFieldId) {
if (e.key === 'Backspace' && current.value === '') {
if (prevFieldId) document.getElementById(prevFieldId).focus();
}
}
function submitOtp() {
let code = '';
for (let i = 1; i <= 6; i++) {
code += document.getElementById('otp' + i).value;
}
document.getElementById('code2fa').value = code;
if (code.length === 6) {
document.getElementById('submit-btn').click();
}
}
// Handle pasting 6 digits at once
document.getElementById('otp1').addEventListener('paste', function(e) {
let paste = (e.clipboardData || window.clipboardData).getData('text');
if (paste.length === 6 && /^\d+$/.test(paste)) {
for (let i = 0; i < 6; i++) {
document.getElementById('otp' + (i+1)).value = paste[i];
}
submitOtp();
e.preventDefault();
}
});
</script>
<?php endif; ?>
</div>
</body>
</html>
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header("Location: login.php");
exit;
@@ -0,0 +1,212 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_role'] !== 'superadmin') {
header("Location: index.php");
exit;
}
$msg = '';
$error = '';
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['add_admin'])) {
$cid = trim($_POST['cid']);
$password = $_POST['password'];
// Check if user already exists
$stmt_check = $pdo->prepare("SELECT id FROM admin_users WHERE username = ?");
$stmt_check->execute([$cid]);
if ($stmt_check->fetch()) {
$error = "เจ้าของหมายเลข CID นี้เป็น Admin อยู่แล้ว";
} else {
$hash = password_hash($password, PASSWORD_DEFAULT);
$insert = $pdo->prepare("INSERT INTO admin_users (username, cid, password_hash, role) VALUES (?, ?, ?, 'admin')");
if ($insert->execute([$cid, $cid, $hash])) {
$msg = "เพิ่มสิทธิ์ผู้ดูแลระบบสำเร็จ";
} else {
$error = "เกิดข้อผิดพลาดในการบันทึกข้อมูล";
}
}
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['delete_admin'])) {
$admin_id = $_POST['admin_id'];
$del = $pdo->prepare("DELETE FROM admin_users WHERE id = ? AND role != 'superadmin'");
if ($del->execute([$admin_id])) {
$msg = "ลบสิทธิ์ผู้ดูแลระบบสำเร็จ";
}
}
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['reset_2fa'])) {
$admin_id = $_POST['admin_id'];
$reset = $pdo->prepare("UPDATE admin_users SET google2fa_secret = NULL WHERE id = ?");
if ($reset->execute([$admin_id])) {
$msg = "รีเซ็ต 2FA สำเร็จ (เจ้าหน้าที่จะต้องตั้งค่า 2FA ใหม่เมื่อเข้าสู่ระบบครั้งถัดไป)";
}
}
$stmt = $pdo->query("SELECT * FROM admin_users ORDER BY role DESC, created_at ASC");
$admins = $stmt->fetchAll();
// Add $current_admin logic
$stmt_admin = $pdo->prepare("SELECT username, role FROM admin_users WHERE id = ?");
$stmt_admin->execute([$_SESSION['admin_id'] ?? 0]);
$current_admin = $stmt_admin->fetch();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Manage Admins - KSH-STAFF</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
</head>
<body class="bg-gray-100 font-sans antialiased text-gray-800 flex min-h-screen">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 text-gray-300 flex flex-col fixed top-0 left-0 h-screen shadow-lg z-20">
<div class="px-6 py-5 bg-gray-900 border-b border-gray-700 text-center">
<div class="relative inline-block overflow-hidden rounded-full w-20 h-20 mx-auto mb-3 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h3 class="text-white text-lg font-bold tracking-wide mt-1">KSH-STAFF Admin</h3>
</div>
<nav class="flex-1 pt-6 pb-4 overflow-y-auto flex flex-col gap-1">
<a href="index.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-home w-6 text-center mr-2"></i> แดชบอร์ด
</a>
<a href="staff_list.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-users w-6 text-center mr-2"></i> รายชื่อเจ้าหน้าที่
</a>
<?php if ($_SESSION['admin_role'] === 'superadmin'): ?>
<a href="manage_admins.php" class="block px-6 py-3 bg-blue-600 text-white font-medium transition duration-200 shadow-sm">
<i class="fas fa-crown w-6 text-center mr-2"></i> จัดการแอดมิน
</a>
<?php endif; ?>
<div class="mt-auto px-6 pt-4">
<a href="logout.php" class="flex items-center text-red-400 hover:text-red-300 hover:bg-red-400/10 py-2 px-3 rounded transition duration-200">
<i class="fas fa-sign-out-alt w-6 text-center mr-1"></i> ออกจากระบบ
</a>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64 flex flex-col min-h-screen">
<header class="bg-white px-8 py-5 border-b border-gray-200 flex justify-between items-center shadow-sm z-10 sticky top-0">
<h4 class="text-xl font-bold text-gray-700"><i class="fas fa-crown text-yellow-500 mr-2"></i> จัดการผู้ดูแลระบบ (Super Admin)</h4>
<div class="text-sm text-gray-500 font-medium px-4 py-1.5 bg-gray-100 rounded-full border border-gray-200 flex items-center gap-2">
ผู้ใช้งาน: <span class="text-blue-600 font-bold"><?= htmlspecialchars($current_admin['username'] ?? $_SESSION['admin_username'] ?? 'Admin') ?></span>
<span class="bg-gray-200 text-gray-600 px-2 py-0.5 rounded text-xs"><?= htmlspecialchars($current_admin['role'] ?? '') ?></span>
</div>
</header>
<div class="p-8 flex-1">
<?php if (!empty($msg)): ?>
<script>Swal.fire({icon: 'success', title: 'สำเร็จ', text: '<?= $msg ?>', timer: 2000, confirmButtonColor: '#2563eb'});</script>
<?php endif; ?>
<?php if (!empty($error)): ?>
<script>Swal.fire({icon: 'error', title: 'ผิดพลาด', text: '<?= $error ?>', confirmButtonColor: '#2563eb'});</script>
<?php endif; ?>
<div class="bg-white rounded-xl p-8 shadow-sm border border-gray-200 mb-8">
<h3 class="text-lg font-bold text-gray-800 border-b border-gray-100 pb-4 mb-6"><i class="fas fa-user-plus text-blue-500 mr-2"></i> เพิ่มผู้ดูแลระบบ (Admin)</h3>
<form method="post" class="flex flex-col md:flex-row gap-6 items-end">
<div class="flex-1 w-full">
<label class="block text-gray-700 text-sm font-medium mb-2">เลขบัตรประชาชน (CID)</label>
<input type="text" name="cid" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" required pattern="\d{13}" placeholder="กรอกเลข 13 หลัก">
</div>
<div class="flex-1 w-full">
<label class="block text-gray-700 text-sm font-medium mb-2">ตั้งรหัสผ่านเริ่มต้น</label>
<input type="password" name="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition" required placeholder="รหัสผ่าน">
</div>
<div class="w-full md:w-auto">
<button type="submit" name="add_admin" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-lg transition duration-200 shadow-sm flex items-center justify-center gap-2">
<i class="fas fa-plus"></i> เพิ่มสิทธิ์
</button>
</div>
</form>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div class="p-6 border-b border-gray-200">
<h3 class="text-lg font-bold text-gray-800"><i class="fas fa-list text-gray-400 mr-2"></i> รายชื่อผู้ดูแลระบบในระบบ</h3>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-600 text-sm uppercase tracking-wider">
<th class="px-6 py-4 font-semibold border-b border-gray-200">Username / CID</th>
<th class="px-6 py-4 font-semibold border-b border-gray-200">สิทธิ์ (Role)</th>
<th class="px-6 py-4 font-semibold border-b border-gray-200">สถานะ 2FA</th>
<th class="px-6 py-4 font-semibold border-b border-gray-200">จัดการ</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<?php foreach ($admins as $ad): ?>
<tr class="hover:bg-gray-50 transition duration-150">
<td class="px-6 py-4 text-gray-800 font-medium"><?= htmlspecialchars($ad['username']) ?></td>
<td class="px-6 py-4">
<?php if ($ad['role'] === 'superadmin'): ?>
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-red-100 text-red-700"><i class="fas fa-crown"></i> Super Admin</span>
<?php else: ?>
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-semibold bg-blue-100 text-blue-700"><i class="fas fa-user-shield"></i> Admin</span>
<?php endif; ?>
</td>
<td class="px-6 py-4 text-sm">
<?= $ad['google2fa_secret'] ? '<span class="text-emerald-600 font-medium flex items-center gap-1.5"><i class="fas fa-check-circle"></i> ตั้งค่าแล้ว</span>' : '<span class="text-red-500 font-medium flex items-center gap-1.5"><i class="fas fa-times-circle"></i> ยังไม่ตั้งค่า</span>' ?>
</td>
<td class="px-6 py-4">
<?php if ($ad['role'] !== 'superadmin'): ?>
<div class="flex gap-2">
<form method="post" onsubmit="return confirm('ยืนยันการรีเซ็ต 2FA?');">
<input type="hidden" name="admin_id" value="<?= $ad['id'] ?>">
<button type="submit" name="reset_2fa" class="bg-amber-500 hover:bg-amber-600 text-white px-3 py-1.5 rounded text-sm font-medium transition flex items-center gap-1.5"><i class="fas fa-sync-alt"></i> รีเซ็ต 2FA</button>
</form>
<form method="post" onsubmit="return confirm('ยืนยันการลบสิทธิ์ Admin?');">
<input type="hidden" name="admin_id" value="<?= $ad['id'] ?>">
<button type="submit" name="delete_admin" class="bg-red-500 hover:bg-red-600 text-white px-3 py-1.5 rounded text-sm font-medium transition flex items-center gap-1.5"><i class="fas fa-trash-alt"></i> ลบสิทธิ์</button>
</form>
</div>
<?php else: ?>
<span class="text-gray-400 font-medium">-</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
</main>
</body>
</html>
@@ -0,0 +1,159 @@
<?php
session_start();
require_once '../includes/db.php';
require_once '../includes/GoogleAuthenticator.php';
if (!isset($_SESSION['admin_logged_in'])) {
header("Location: login.php");
exit;
}
$stmt = $pdo->prepare("SELECT google2fa_secret FROM admin_users WHERE id = ?");
$stmt->execute([$_SESSION['admin_id']]);
$admin = $stmt->fetch();
// If already set up, redirect to dashboard
if (!empty($admin['google2fa_secret'])) {
header("Location: index.php");
exit;
}
$ga = new PHPGangsta_GoogleAuthenticator();
$error = '';
if (!isset($_SESSION['new_2fa_secret'])) {
$_SESSION['new_2fa_secret'] = $ga->createSecret();
}
$secret = $_SESSION['new_2fa_secret'];
$qrCodeUrl = $ga->getQRCodeGoogleUrl('KSH-STAFF-Admin', $secret);
if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['verify_code'])) {
$code = $_POST['code'] ?? '';
if ($ga->verifyCode($secret, $code, 2)) {
// Save to DB
$update = $pdo->prepare("UPDATE admin_users SET google2fa_secret = ? WHERE id = ?");
$update->execute([$secret, $_SESSION['admin_id']]);
unset($_SESSION['new_2fa_secret']);
echo "<script>alert('ตั้งค่า Google Authenticator สำเร็จ'); window.location.href='index.php';</script>";
exit;
} else {
$error = "รหัสไม่ถูกต้อง กรุณาลองใหม่อีกครั้ง";
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ตั้งค่า Google Authenticator</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center font-sans antialiased p-4">
<div class="bg-white rounded-xl shadow-lg p-8 w-full max-w-lg border border-gray-100 text-center">
<div class="relative inline-block overflow-hidden rounded-full w-20 h-20 mx-auto mb-4 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h3 class="text-blue-600 text-2xl font-bold mb-3">ตั้งค่าความปลอดภัย (2FA)</h3>
<p class="text-gray-600 text-sm mb-6">เนื่องจากเป็นการเข้าสู่ระบบครั้งแรก กรุณาตั้งค่า Google Authenticator เพื่อความปลอดภัยในการเข้าถึงข้อมูล</p>
<div class="flex justify-center mb-6">
<img src="<?= $qrCodeUrl ?>" alt="QR Code" class="rounded-lg border border-gray-200 p-3 shadow-sm bg-white">
</div>
<div class="bg-blue-50 text-blue-800 rounded-lg p-4 text-sm text-left mb-6 shadow-inner">
<ol class="list-decimal list-inside space-y-2 font-medium">
<li>โหลดแอป Google Authenticator</li>
<li>สแกน QR Code ด้านบน</li>
<li>นำรหัส 6 หลักที่ได้มากรอกด้านล่าง</li>
</ol>
</div>
<?php if (!empty($error)): ?>
<div class="text-red-500 text-sm mb-4 font-medium bg-red-50 py-2 rounded-lg"><?= htmlspecialchars($error) ?></div>
<?php endif; ?>
<form method="post" id="otp-form" class="space-y-6">
<div class="flex justify-center gap-2">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp2')" id="otp1" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition" autofocus>
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp3')" onkeydown="moveToPrev(event, this, 'otp1')" id="otp2" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp4')" onkeydown="moveToPrev(event, this, 'otp2')" id="otp3" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp5')" onkeydown="moveToPrev(event, this, 'otp3')" id="otp4" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="moveToNext(this, 'otp6')" onkeydown="moveToPrev(event, this, 'otp4')" id="otp5" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
<input type="text" maxlength="1" oninput="submitOtp()" onkeydown="moveToPrev(event, this, 'otp5')" id="otp6" class="w-12 h-14 text-center text-2xl font-bold border-2 border-gray-300 rounded-lg focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition">
</div>
<input type="hidden" name="code" id="code2fa" required>
<button type="submit" name="verify_code" id="submit-btn" class="hidden">ยืนยันรหัสและบันทึก</button>
</form>
<div class="mt-8">
<a href="logout.php" class="text-red-500 hover:text-red-700 text-sm font-medium hover:underline transition">ออกจากระบบ (ตั้งค่าภายหลัง)</a>
</div>
</div>
<script>
function moveToNext(current, nextFieldId) {
if (current.value.length === 1) {
if (nextFieldId) document.getElementById(nextFieldId).focus();
}
}
function moveToPrev(e, current, prevFieldId) {
if (e.key === 'Backspace' && current.value === '') {
if (prevFieldId) document.getElementById(prevFieldId).focus();
}
}
function submitOtp() {
let code = '';
for (let i = 1; i <= 6; i++) {
code += document.getElementById('otp' + i).value;
}
document.getElementById('code2fa').value = code;
if (code.length === 6) {
document.getElementById('submit-btn').click();
}
}
// Handle pasting 6 digits at once
document.getElementById('otp1').addEventListener('paste', function(e) {
let paste = (e.clipboardData || window.clipboardData).getData('text');
if (paste.length === 6 && /^\d+$/.test(paste)) {
for (let i = 0; i < 6; i++) {
document.getElementById('otp' + (i+1)).value = paste[i];
}
submitOtp();
e.preventDefault();
}
});
</script>
</body>
</html>
@@ -0,0 +1,431 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['admin_logged_in'])) {
header("Location: login.php");
exit;
}
$is_superadmin = ($_SESSION['admin_role'] ?? '') === 'superadmin';
$stmt = $pdo_bot->query("SELECT * FROM line_staff_register ORDER BY date DESC");
$staff_list = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Staff List - KSH-STAFF</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- DataTables & Buttons CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.6/css/jquery.dataTables.min.css">
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/2.4.1/css/buttons.dataTables.min.css">
<script src="https://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>
<!-- JS for Excel Export -->
<script src="https://cdn.datatables.net/buttons/2.4.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="https://cdn.datatables.net/buttons/2.4.1/js/buttons.html5.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="../assets/css/datatables_custom.css?v=1.3">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
<style>
/* เว้นระยะขอบซ้ายให้คอลัมน์ชื่อ-สกุล (คอลัมน์ที่ 3) */
#staffTable th:nth-child(3),
#staffTable td:nth-child(3) {
text-align: left !important;
padding-left: 30px !important;
}
/* DataTables length/filter Tailwind overrides */
.dataTables_length select { appearance: auto; background-color: #fff; }
</style>
</head>
<?php
$assessed_cids = [];
if (isset($pdo_assess) && $pdo_assess) {
$current_thai_year = date('Y') + 543;
try {
$stmt_assess = $pdo_assess->prepare("SELECT hr_cid FROM it_assessments WHERE assess_year = ?");
$stmt_assess->execute([(string)$current_thai_year]);
while ($r = $stmt_assess->fetch()) {
$assessed_cids[$r['hr_cid']] = true;
}
} catch (\PDOException $e) {
}
}
$active_count = 0;
$inactive_count = 0;
$assessed_count = 0;
$not_assessed_count = 0;
foreach ($staff_list as $row) {
$is_active = (($row['ksh_user'] ?? 'Y') === 'Y');
if ($is_active) {
$active_count++;
} else {
$inactive_count++;
}
if (isset($assessed_cids[$row['cid']])) {
$assessed_count++;
} else {
// นับคนที่ยังไม่ประเมิน เฉพาะคนที่ยังมีสถานะ "ปฏิบัติงาน" เท่านั้น
if ($is_active) {
$not_assessed_count++;
}
}
}
$total_count = count($staff_list);
// Add $current_admin logic
$stmt_admin = $pdo->prepare("SELECT username, role FROM admin_users WHERE id = ?");
$stmt_admin->execute([$_SESSION['admin_id'] ?? 0]);
$current_admin = $stmt_admin->fetch();
?>
<body class="bg-gray-100 font-sans antialiased text-gray-800 flex min-h-screen">
<!-- Sidebar -->
<aside class="w-64 bg-gray-800 text-gray-300 flex flex-col fixed top-0 left-0 h-screen shadow-lg z-20">
<div class="px-6 py-5 bg-gray-900 border-b border-gray-700 text-center">
<div class="relative inline-block overflow-hidden rounded-full w-20 h-20 mx-auto mb-3 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h3 class="text-white text-lg font-bold tracking-wide mt-1">KSH-STAFF Admin</h3>
</div>
<nav class="flex-1 pt-6 pb-4 overflow-y-auto flex flex-col gap-1">
<a href="index.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-home w-6 text-center mr-2"></i> แดชบอร์ด
</a>
<a href="staff_list.php" class="block px-6 py-3 bg-blue-600 text-white font-medium transition duration-200 shadow-sm">
<i class="fas fa-users w-6 text-center mr-2"></i> รายชื่อเจ้าหน้าที่
</a>
<?php if ($is_superadmin): ?>
<a href="manage_admins.php" class="block px-6 py-3 hover:bg-gray-700 hover:text-white transition duration-200">
<i class="fas fa-crown w-6 text-center mr-2"></i> จัดการแอดมิน
</a>
<?php endif; ?>
<div class="mt-auto px-6 pt-4">
<a href="logout.php" class="flex items-center text-red-400 hover:text-red-300 hover:bg-red-400/10 py-2 px-3 rounded transition duration-200">
<i class="fas fa-sign-out-alt w-6 text-center mr-1"></i> ออกจากระบบ
</a>
</div>
</nav>
</aside>
<!-- Main Content -->
<main class="flex-1 ml-64 flex flex-col min-h-screen">
<header class="bg-white px-8 py-5 border-b border-gray-200 flex flex-wrap justify-between items-center shadow-sm z-10 sticky top-0 gap-4">
<div class="flex items-center gap-3">
<h4 class="text-xl font-bold text-gray-700"><i class="fas fa-users text-blue-500 mr-2"></i> รายชื่อเจ้าหน้าที่ลงทะเบียนแล้ว</h4>
</div>
<div class="flex flex-wrap items-center gap-3">
<span id="syncProgress" class="hidden font-bold text-blue-600 text-sm bg-blue-50 px-3 py-1.5 rounded-full"></span>
<button id="btnCheckDuplicate" onclick="checkDuplicateCID()" class="bg-amber-500 hover:bg-amber-600 text-white font-medium py-2 px-4 rounded-lg transition duration-200 shadow-sm flex items-center gap-2 text-sm">
<i class="fas fa-users-slash"></i> เช็ค CID ซ้ำ
</button>
<button id="btnBulkUpdate" onclick="startBulkUpdate()" class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition duration-200 shadow-sm flex items-center gap-2 text-sm">
<i class="fas fa-sync-alt"></i> ตรวจสอบสถานะกับ HR
</button>
<div class="h-8 w-px bg-gray-300 mx-1 hidden md:block"></div>
<!-- User Profile -->
<div class="text-sm text-gray-500 font-medium px-4 py-1.5 bg-gray-100 rounded-full border border-gray-200 flex items-center gap-2">
ผู้ใช้งาน: <span class="text-blue-600 font-bold"><?= htmlspecialchars($current_admin['username'] ?? $_SESSION['admin_username'] ?? 'Admin') ?></span>
<span class="bg-gray-200 text-gray-600 px-2 py-0.5 rounded text-xs"><?= htmlspecialchars($current_admin['role'] ?? '') ?></span>
</div>
</div>
</header>
<div class="p-8 flex-1">
<!-- Summary Cards -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-8">
<div onclick="$('#statusFilter').val('').trigger('change'); window.table.column(7).search('').draw();" class="bg-white rounded-xl shadow-sm border-l-4 border-blue-500 p-4 text-center cursor-pointer hover:-translate-y-1 hover:shadow-md transition duration-200">
<h3 class="text-2xl font-bold text-blue-600 mb-1"><?= $total_count ?></h3>
<span class="text-gray-500 text-xs font-medium">ทั้งหมด (คน)</span>
</div>
<div onclick="$('#statusFilter').val('ปฏิบัติงาน').trigger('change'); window.table.column(7).search('').draw();" class="bg-white rounded-xl shadow-sm border-l-4 border-emerald-500 p-4 text-center cursor-pointer hover:-translate-y-1 hover:shadow-md transition duration-200">
<h3 class="text-2xl font-bold text-emerald-600 mb-1"><?= $active_count ?></h3>
<span class="text-gray-500 text-xs font-medium">ปฏิบัติงาน</span>
</div>
<div onclick="$('#statusFilter').val('พ้นสภาพ').trigger('change'); window.table.column(7).search('').draw();" class="bg-white rounded-xl shadow-sm border-l-4 border-red-500 p-4 text-center cursor-pointer hover:-translate-y-1 hover:shadow-md transition duration-200">
<h3 class="text-2xl font-bold text-red-500 mb-1"><?= $inactive_count ?></h3>
<span class="text-gray-500 text-xs font-medium">พ้นสภาพ</span>
</div>
<div onclick="$('#statusFilter').val('').trigger('change'); window.table.column(7).search('ประเมินแล้ว').draw();" class="bg-white rounded-xl shadow-sm border-l-4 border-indigo-500 p-4 text-center cursor-pointer hover:-translate-y-1 hover:shadow-md transition duration-200">
<h3 class="text-2xl font-bold text-indigo-500 mb-1"><?= $assessed_count ?></h3>
<span class="text-gray-500 text-xs font-medium">ประเมินแล้ว</span>
</div>
<div onclick="$('#statusFilter').val('ปฏิบัติงาน').trigger('change'); window.table.column(7).search('ยังไม่ประเมิน').draw();" class="bg-white rounded-xl shadow-sm border-l-4 border-orange-400 p-4 text-center cursor-pointer hover:-translate-y-1 hover:shadow-md transition duration-200">
<h3 class="text-2xl font-bold text-orange-400 mb-1"><?= $not_assessed_count ?></h3>
<span class="text-gray-500 text-xs font-medium">ยังไม่ประเมิน</span>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<!-- Custom Filter Dropdown (Moved via JS into DOM later) -->
<div id="customFilterContainer" style="display: none;">
<select id="statusFilter" class="border border-gray-300 rounded-lg px-3 py-1.5 ml-3 outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-sans text-sm text-gray-700 bg-white">
<option value="">-- กรองสถานะทั้งหมด --</option>
<option value="ปฏิบัติงาน">ปฏิบัติงาน</option>
<option value="พ้นสภาพ">พ้นสภาพ</option>
</select>
</div>
<table id="staffTable" class="display" style="width:100%">
<thead>
<tr>
<th width="12%">CID</th>
<th width="8%">HN</th>
<th width="15%">ชื่อ - นามสกุล</th>
<th width="10%">เบอร์โทร</th>
<th width="10%">Line UID</th>
<th width="15%">วันที่สมัคร</th>
<th width="15%">สถานะทำงาน</th>
<th width="15%">การประเมิน (<?= date('Y') + 543 ?>)</th>
</tr>
</thead>
<tbody>
<?php foreach ($staff_list as $row):
$date_formatted = '-';
if (!empty($row['date'])) {
$d = date_create($row['date']);
if ($d) {
$year_be = (int)date_format($d, 'Y') + 543;
$date_formatted = date_format($d, 'd/m/') . $year_be;
}
}
$phone = $row['phone'];
if (strpos($phone, '+66') === 0) {
$phone = '0' . substr($phone, 3);
}
// จัดรูปแบบ xx-xxxx-xxxx
$phone_clean = preg_replace('/[^0-9]/', '', $phone);
if (strlen($phone_clean) === 10) {
$phone = substr($phone_clean, 0, 2) . '-' . substr($phone_clean, 2, 4) . '-' . substr($phone_clean, 6);
} else if (strlen($phone_clean) === 9) {
$phone = substr($phone_clean, 0, 2) . '-' . substr($phone_clean, 2, 3) . '-' . substr($phone_clean, 5);
}
$is_assessed = isset($assessed_cids[$row['cid']]);
?>
<tr>
<td><?= htmlspecialchars($row['cid']) ?></td>
<td><?= htmlspecialchars($row['hn']) ?></td>
<td><?= htmlspecialchars($row['fname'] . ' ' . $row['lname']) ?></td>
<td><?= htmlspecialchars($phone) ?></td>
<td>
<?php if (!empty($row['user_id'])): ?>
<span style="color:#198754; font-weight: 500; cursor: help;" title="<?= htmlspecialchars($row['user_id']) ?>">
<i class="fas fa-check-circle"></i> ผูกแล้ว
</span>
<?php else: ?>
<span style="color:#dc3545; font-weight: 500;">
<i class="fas fa-times-circle"></i> ยังไม่ผูก
</span>
<?php endif; ?>
</td>
<td><?= $date_formatted ?></td>
<td id="status-td-<?= htmlspecialchars($row['cid']) ?>">
<?php if (($row['ksh_user'] ?? 'Y') === 'Y'): ?>
<span style="color:#198754; font-weight: 500;"><i class="fas fa-briefcase"></i> ปฏิบัติงาน</span>
<?php else: ?>
<span style="color:#dc3545; font-weight: 500;"><i class="fas fa-user-times"></i> พ้นสภาพ</span>
<?php endif; ?>
</td>
<td>
<?php if ($is_assessed): ?>
<span class="bg-indigo-100 text-indigo-700 px-3 py-1 rounded-full text-xs font-bold border border-indigo-200">
<i class="fas fa-clipboard-check mr-1"></i> ประเมินแล้ว
</span>
<?php else: ?>
<span class="bg-orange-100 text-orange-700 px-3 py-1 rounded-full text-xs font-bold border border-orange-200">
<i class="fas fa-exclamation-circle mr-1"></i> ยังไม่ประเมิน
</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
// DataTables Initialization
$(document).ready(function() {
window.table = $('#staffTable').DataTable({
"columnDefs": [
{ "className": "dt-center", "targets": "_all" },
{ "className": "dt-left", "targets": 2 }
],
"language": {
"url": "//cdn.datatables.net/plug-ins/1.13.6/i18n/th.json",
"search": "",
"searchPlaceholder": "ค้นหาด้วยชื่อ, CID, เบอร์โทร..."
},
"dom": '<"dt-top-row"<"dt-top-left"lB>f>rt<"dt-bottom-row"ip>',
"buttons": [
{
extend: 'excelHtml5',
text: '<i class="fas fa-file-excel"></i> ส่งออก Excel',
title: 'รายชื่อเจ้าหน้าที่ลงทะเบียน Line KSH-STAFF',
exportOptions: {
columns: [0, 1, 2, 3, 4, 5, 6, 7]
}
}
],
"pageLength": 10,
"lengthMenu": [[10, 25, 50, -1], [10, 25, 50, "ทั้งหมด"]]
});
// Append custom filter dropdown next to export button
$('#customFilterContainer').contents().appendTo('.dt-top-left');
// Handle Filter Change
$('#statusFilter').on('change', function() {
var val = $(this).val();
window.table.column(6).search(val).draw();
});
});
// Duplicate CID Check Logic
function checkDuplicateCID() {
const btn = document.getElementById('btnCheckDuplicate');
const originalHtml = btn.innerHTML;
Swal.fire({
title: 'ยืนยันการเคลียร์ข้อมูล',
text: "ระบบจะทำการลบข้อมูล CID ที่ซ้ำกันออก โดยเก็บเฉพาะรายการที่ลงทะเบียนล่าสุดไว้เท่านั้น ต้องการดำเนินการหรือไม่?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
confirmButtonText: 'ลบข้อมูลซ้ำ',
cancelButtonText: 'ยกเลิก'
}).then((result) => {
if (result.isConfirmed) {
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> กำลังตรวจสอบ...';
btn.disabled = true;
$.post('ajax_remove_duplicate_cid.php', {}, function(res) {
btn.innerHTML = originalHtml;
btn.disabled = false;
try {
const data = JSON.parse(res);
if (data.success) {
Swal.fire({
icon: 'success',
title: 'เรียบร้อย!',
text: data.message,
confirmButtonColor: '#007bff'
}).then(() => {
if(data.deleted_count > 0) location.reload();
});
} else {
Swal.fire('ผิดพลาด', data.message, 'error');
}
} catch(e) {
Swal.fire('ผิดพลาด', 'ระบบเซิร์ฟเวอร์ขัดข้อง', 'error');
}
});
}
});
}
const staffCIDs = [
<?php foreach ($staff_list as $row): ?>
"<?= htmlspecialchars($row['cid']) ?>",
<?php endforeach; ?>
];
async function startBulkUpdate() {
if(!confirm("ระบบจะทำการดึงข้อมูลสถานะล่าสุดจากฐานข้อมูล HR มาอัปเดตเจ้าหน้าที่ทั้งหมด " + staffCIDs.length + " รายการ\n\nต้องการดำเนินการต่อหรือไม่?")) {
return;
}
$('#btnBulkUpdate').prop('disabled', true).html('<i class="fas fa-spinner fa-spin"></i> กำลังตรวจสอบ...');
$('#syncProgress').show().text(`ตรวจสอบแล้ว 0 / ${staffCIDs.length}`);
let updatedCount = 0;
for (let cid of staffCIDs) {
// If row is visible on current page, show loading spinner
const td = document.getElementById('status-td-' + cid);
if(td) {
td.innerHTML = '<span style="color:#007bff; font-weight: 500;"><i class="fas fa-spinner fa-spin"></i> กำลังตรวจสอบ...</span>';
}
try {
const res = await $.post('ajax_update_hr_status.php', { cid: cid });
const data = JSON.parse(res);
if (data.success && td) {
if (data.new_status === 'Y') {
td.innerHTML = '<span style="color:#198754; font-weight: 500;"><i class="fas fa-briefcase"></i> ปฏิบัติงาน</span>';
} else {
td.innerHTML = '<span style="color:#dc3545; font-weight: 500;"><i class="fas fa-user-times"></i> พ้นสภาพ</span>';
}
} else if (td) {
td.innerHTML = '<span style="color:#dc3545; font-weight: 500;"><i class="fas fa-exclamation-triangle"></i> ขัดข้อง</span>';
}
} catch (e) {
if(td) td.innerHTML = '<span style="color:#dc3545; font-weight: 500;"><i class="fas fa-exclamation-triangle"></i> ขัดข้อง</span>';
}
updatedCount++;
$('#syncProgress').text(`ตรวจสอบแล้ว ${updatedCount} / ${staffCIDs.length}`);
}
$('#btnBulkUpdate').prop('disabled', false).html('<i class="fas fa-sync-alt"></i> ตรวจสอบสถานะกับ HR (ทั้งหมด)');
$('#syncProgress').text(`เสร็จสิ้น! อัปเดตแล้ว ${updatedCount} รายการ`).delay(3000).fadeOut();
Swal.fire({
icon: 'success',
title: 'เสร็จสิ้น!',
text: 'อัปเดตสถานะเจ้าหน้าที่ทั้งหมดเรียบร้อยแล้ว',
confirmButtonColor: '#007bff'
}).then(() => {
// Reload page to refresh DataTable internal cache and formatting
location.reload();
});
}
</script>
</body>
</html>
@@ -0,0 +1,56 @@
<?php
require_once '../includes/db.php';
try {
// Create admin_users table
$pdo->exec("CREATE TABLE IF NOT EXISTS admin_users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
cid VARCHAR(13) NULL,
password_hash VARCHAR(255) NOT NULL,
role VARCHAR(20) DEFAULT 'admin',
google2fa_secret VARCHAR(255) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)");
// Ensure columns exist (in case of old table structure)
try { $pdo->exec("ALTER TABLE admin_users ADD COLUMN cid VARCHAR(13) NULL"); } catch (Exception $e) {}
try { $pdo->exec("ALTER TABLE admin_users ADD COLUMN role VARCHAR(20) DEFAULT 'admin'"); } catch (Exception $e) {}
// Force update superadmin role just in case
$pdo->exec("UPDATE admin_users SET role = 'superadmin' WHERE username = 'superadmin'");
// Create settings table
$pdo->exec("CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(50) PRIMARY KEY,
setting_value VARCHAR(255)
)");
$pdo->exec("INSERT IGNORE INTO settings (setting_key, setting_value) VALUES ('require_assessment', '0')");
// Check if superadmin exists
$stmt = $pdo->prepare("SELECT id FROM admin_users WHERE username = 'superadmin'");
$stmt->execute();
if (!$stmt->fetch()) {
$defaultPass = password_hash('superadmin123', PASSWORD_DEFAULT);
$pdo->exec("INSERT INTO admin_users (username, password_hash, role) VALUES ('superadmin', '$defaultPass', 'superadmin')");
echo "✅ สร้างบัญชี Super Admin สำเร็จ (Username: superadmin / Password: superadmin123)<br>";
} else {
echo "️ บัญชี Super Admin มีอยู่แล้ว<br>";
}
echo "✅ ตาราง admin_users อัปเดตเรียบร้อย<br>";
// Debug: Show all users
echo "<hr><h3>ตรวจสอบรายชื่อ Admin ในฐานข้อมูล:</h3>";
$stmt = $pdo->query("SELECT id, username, role FROM admin_users");
while ($row = $stmt->fetch()) {
echo "ID: {$row['id']} | Username: {$row['username']} | Role: {$row['role']}<br>";
}
echo "<br><a href='login.php'>ไปที่หน้าเข้าสู่ระบบ</a>";
} catch (\PDOException $e) {
die("Error updating database: " . $e->getMessage());
}
?>
@@ -0,0 +1,163 @@
@import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Sarabun', sans-serif;
background-color: #f4f6f9;
margin: 0;
padding: 0;
color: #333;
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: 250px;
background: #343a40;
color: #c2c7d0;
display: flex;
flex-direction: column;
position: fixed;
top: 0;
left: 0;
height: 100vh;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
}
.sidebar-header {
padding: 20px;
text-align: center;
border-bottom: 1px solid #4f5962;
background: #23272b;
}
.sidebar-header h3 {
margin: 0;
color: #fff;
font-size: 20px;
font-weight: 600;
}
.sidebar-nav {
flex: 1;
padding-top: 20px;
}
.sidebar-nav a {
display: block;
padding: 12px 20px;
color: #c2c7d0;
text-decoration: none;
font-size: 16px;
transition: 0.2s;
}
.sidebar-nav a:hover, .sidebar-nav a.active {
background: #007bff;
color: #fff;
}
.sidebar-nav a i {
width: 25px;
text-align: center;
margin-right: 10px;
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 250px;
display: flex;
flex-direction: column;
}
.top-navbar {
background: #fff;
padding: 15px 30px;
border-bottom: 1px solid #dee2e6;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 1px 5px rgba(0,0,0,0.05);
}
.top-navbar h4 {
margin: 0;
font-size: 18px;
color: #495057;
}
.content-wrapper {
padding: 30px;
flex: 1;
}
/* Cards */
.card {
background: #fff;
border-radius: 8px;
padding: 25px;
box-shadow: 0 0 1px rgba(0,0,0,.125), 0 1px 3px rgba(0,0,0,.2);
margin-bottom: 20px;
}
.card h3 {
margin-top: 0;
color: #343a40;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
margin-bottom: 20px;
}
/* Buttons */
.btn-primary {
background: #007bff;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
transition: 0.2s;
font-family: inherit;
font-size: 15px;
}
.btn-primary:hover { background: #0056b3; }
.btn-danger { background: #dc3545; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 14px;}
.btn-warning { background: #ffc107; color: #212529; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; text-decoration: none; font-size: 14px;}
/* Inputs */
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-family: inherit;
box-sizing: border-box;
}
/* 2FA 6-digit Inputs (Login & Setup) */
.otp-wrapper {
display: flex;
justify-content: center;
gap: 10px;
margin: 20px 0;
}
.otp-wrapper input {
width: 45px;
height: 55px;
text-align: center;
font-size: 24px;
font-weight: 700;
border: 2px solid #ddd;
border-radius: 8px;
background: #fdfdfd;
transition: 0.3s;
font-family: inherit;
}
.otp-wrapper input:focus {
border-color: #007bff;
box-shadow: 0 0 8px rgba(0,123,255,0.2);
outline: none;
}
@@ -0,0 +1,106 @@
/* Modern Enterprise DataTables Styling */
.card { box-shadow: 0 0 15px rgba(0,0,0,0.05); border: 1px solid #e9ecef; }
table.dataTable { font-family: inherit; border-collapse: collapse; margin-top: 15px !important; margin-bottom: 15px !important; }
table.dataTable thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
border-top: 1px solid #dee2e6;
padding: 12px 30px 12px 15px;
color: #495057;
font-size: 15px;
font-weight: 600;
position: relative;
}
table.dataTable tbody td { padding: 12px 15px; border-bottom: 1px solid #e9ecef; vertical-align: middle; font-size: 15px; color: #333; }
table.dataTable tbody tr { transition: background-color 0.2s; }
table.dataTable tbody tr:hover { background-color: #f1f7fd; }
/* Remove default DataTables sort images completely */
table.dataTable thead th,
table.dataTable thead td {
background-image: none !important;
}
/* Hide any possible ::before pseudo-elements from DataTables default */
table.dataTable thead th::before,
table.dataTable thead td::before {
display: none !important;
content: none !important;
}
/* Custom FontAwesome Sorting Icons */
table.dataTable thead th.sorting::after,
table.dataTable thead th.sorting_asc::after,
table.dataTable thead th.sorting_desc::after {
font-family: "Font Awesome 6 Free";
font-weight: 900;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 15px;
}
table.dataTable thead th.sorting::after { content: "\f0dc" !important; color: blue !important; opacity: 0.3 !important; }
table.dataTable thead th.sorting_asc::after { content: "\f161" !important; color: blue !important; opacity: 0.7 !important; }
table.dataTable thead th.sorting_desc::after { content: "\f160" !important; color: blue !important; opacity: 0.7 !important; }
/* Excel Export Button Styling */
.dt-buttons { margin-left: 15px; }
.dt-buttons .dt-button.buttons-excel {
background: #fff !important;
color: #198754 !important;
border: 1px solid #198754 !important;
border-radius: 4px !important;
padding: 6px 14px !important;
font-size: 14px !important;
font-family: 'Sarabun', sans-serif !important;
font-weight: 600;
box-shadow: none !important;
transition: all 0.2s !important;
}
.dt-buttons .dt-button.buttons-excel:hover { background: #198754 !important; color: #fff !important; }
/* Controls Layout */
.dt-top-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 15px;}
.dt-top-left { display: flex; align-items: center; }
.dataTables_length select {
border: 1px solid #ced4da;
border-radius: 6px;
padding: 6px 30px 6px 12px;
font-family: 'Sarabun', sans-serif;
outline: none;
margin: 0 5px;
background-color: #fff;
transition: all 0.2s ease-in-out;
appearance: auto;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
}
.dataTables_length select:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
}
.dataTables_filter input {
border: 1px solid #ced4da;
border-radius: 20px;
padding: 8px 18px;
outline: none;
transition: all 0.3s ease-in-out;
font-family: 'Sarabun', sans-serif;
width: 250px;
background-color: #f8f9fa;
box-shadow: inset 0 1px 2px rgba(0,0,0,0.05);
}
.dataTables_filter input:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
background-color: #fff;
width: 280px;
}
.dt-bottom-row { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; font-size: 14px; color: #6c757d;}
.dataTables_paginate .paginate_button { padding: 5px 10px !important; margin: 0 2px; border-radius: 4px; border: 1px solid #dee2e6 !important; background: #fff !important; color: #007bff !important; cursor: pointer; }
.dataTables_paginate .paginate_button:hover { background: #e9ecef !important; color: #0056b3 !important; }
.dataTables_paginate .paginate_button.current { background: #007bff !important; color: #fff !important; border-color: #007bff !important; font-weight: bold;}
@@ -0,0 +1,132 @@
/* Base Styles for LIFF & Magic UI Feel */
body {
font-family: 'Sarabun', 'Prompt', sans-serif;
background-color: #f4f7f6;
margin: 0;
padding: 0;
color: #333;
}
/* Shiny Effect for Logo */
.shiny-logo {
position: relative;
display: inline-block;
overflow: hidden;
border-radius: 12px; /* adjust based on logo shape */
}
.shiny-logo img {
display: block;
max-width: 100%;
height: auto;
}
.shiny-logo::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 50%;
height: 100%;
background: linear-gradient(
to right,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.6) 50%,
rgba(255, 255, 255, 0) 100%
);
transform: skewX(-25deg);
animation: shine 3s infinite;
}
@keyframes shine {
0% {
left: -100%;
}
20% {
left: 200%;
}
100% {
left: 200%;
}
}
/* Magic UI Style Buttons & Cards */
.card {
background: #fff;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0,0,0,0.05);
padding: 20px;
margin-bottom: 20px;
}
.btn-magic {
background: linear-gradient(135deg, #00c6ff 0%, #0072ff 100%);
color: white;
border: none;
border-radius: 8px;
padding: 10px 20px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
text-align: center;
display: inline-block;
text-decoration: none;
width: 100%;
box-sizing: border-box;
}
.btn-magic:hover {
transform: translateY(-2px);
box-shadow: 0 6px 15px rgba(0, 114, 255, 0.3);
}
.menu-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
margin-top: 20px;
}
.menu-item {
background: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px 10px;
text-align: center;
text-decoration: none;
color: #333;
font-weight: 600;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 2px 10px rgba(0,0,0,0.02);
transition: all 0.3s ease;
}
.menu-item:hover {
background: #f0f7ff;
border-color: #0072ff;
transform: translateY(-3px);
}
.menu-icon {
font-size: 32px;
margin-bottom: 10px;
color: #0072ff;
}
/* Header */
.header {
text-align: center;
padding: 30px 20px;
background: #fff;
border-bottom: 1px solid #eaeaea;
}
.header h1 {
font-size: 24px;
margin: 10px 0 0;
color: #0072ff;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

@@ -0,0 +1,252 @@
<?php
/**
* PHP Class for handling Google Authenticator 2-factor authentication.
*
* @author Michael Kliewe
* @copyright 2012 Michael Kliewe
* @license http://www.opensource.org/licenses/bsd-license.php BSD License
*
* @link http://www.phpgangsta.de/
*/
class PHPGangsta_GoogleAuthenticator
{
protected $_codeLength = 6;
/**
* Create new secret.
* 16 characters, randomly chosen from the allowed base32 characters.
*
* @param int $secretLength
*
* @return string
*/
public function createSecret($secretLength = 16)
{
$validChars = $this->_getBase32LookupTable();
// Valid secret lengths are 80 to 640 bits
if ($secretLength < 16 || $secretLength > 128) {
throw new Exception('Bad secret length');
}
$secret = '';
$rnd = false;
if (function_exists('random_bytes')) {
$rnd = random_bytes($secretLength);
} elseif (function_exists('mcrypt_create_iv')) {
$rnd = mcrypt_create_iv($secretLength, MCRYPT_DEV_URANDOM);
} elseif (function_exists('openssl_random_pseudo_bytes')) {
$rnd = openssl_random_pseudo_bytes($secretLength, $cryptoStrong);
if (!$cryptoStrong) {
$rnd = false;
}
}
if ($rnd !== false) {
for ($i = 0; $i < $secretLength; ++$i) {
$secret .= $validChars[ord($rnd[$i]) & 31];
}
} else {
throw new Exception('No source of secure random');
}
return $secret;
}
/**
* Calculate the code, with given secret and point in time.
*
* @param string $secret
* @param int|null $timeSlice
*
* @return string
*/
public function getCode($secret, $timeSlice = null)
{
if ($timeSlice === null) {
$timeSlice = floor(time() / 30);
}
$secretkey = $this->_base32Decode($secret);
// Pack time into binary string
$time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice);
// Hash it with users secret key
$hm = hash_hmac('SHA1', $time, $secretkey, true);
// Use last nipple of result as index/offset
$offset = ord(substr($hm, -1)) & 0x0F;
// grab 4 bytes of the result
$hashpart = substr($hm, $offset, 4);
// Unpak binary value
$value = unpack('N', $hashpart);
$value = $value[1];
// Only 32 bits
$value = $value & 0x7FFFFFFF;
$modulo = pow(10, $this->_codeLength);
return str_pad($value % $modulo, $this->_codeLength, '0', STR_PAD_LEFT);
}
/**
* Get QR-Code URL for image, from google charts.
*
* @param string $name
* @param string $secret
* @param string $title
* @param array $params
*
* @return string
*/
public function getQRCodeGoogleUrl($name, $secret, $title = null, $params = array())
{
$width = !empty($params['width']) && (int) $params['width'] > 0 ? (int) $params['width'] : 200;
$height = !empty($params['height']) && (int) $params['height'] > 0 ? (int) $params['height'] : 200;
$level = !empty($params['level']) && array_search($params['level'], array('L', 'M', 'Q', 'H')) !== false ? $params['level'] : 'M';
$urlencoded = urlencode('otpauth://totp/'.$name.'?secret='.$secret.'');
if (isset($title)) {
$urlencoded .= urlencode('&issuer='.urlencode($title));
}
return "https://api.qrserver.com/v1/create-qr-code/?data=$urlencoded&size=${width}x${height}&ecc=$level";
}
/**
* Check if the code is correct. This will accept codes starting from $discrepancy*30sec ago to $discrepancy*30sec from now.
*
* @param string $secret
* @param string $code
* @param int $discrepancy This is the allowed time drift in 30 second units (8 means 4 minutes before or after)
* @param int|null $currentTimeSlice time slice if we want use other that time()
*
* @return bool
*/
public function verifyCode($secret, $code, $discrepancy = 1, $currentTimeSlice = null)
{
if ($currentTimeSlice === null) {
$currentTimeSlice = floor(time() / 30);
}
if (strlen($code) != 6) {
return false;
}
for ($i = -$discrepancy; $i <= $discrepancy; ++$i) {
$calculatedCode = $this->getCode($secret, $currentTimeSlice + $i);
if ($this->timingSafeEquals($calculatedCode, $code)) {
return true;
}
}
return false;
}
/**
* Set the code length, should be >=6.
*
* @param int $length
*
* @return PHPGangsta_GoogleAuthenticator
*/
public function setCodeLength($length)
{
$this->_codeLength = $length;
return $this;
}
/**
* Helper class to decode base32.
*
* @param $secret
*
* @return bool|string
*/
protected function _base32Decode($secret)
{
if (empty($secret)) {
return '';
}
$base32chars = $this->_getBase32LookupTable();
$base32charsFlipped = array_flip($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], $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;
}
/**
* Get array with all 32 characters for decoding from/encoding to base32.
*
* @return array
*/
protected function _getBase32LookupTable()
{
return array(
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=', // padding char
);
}
/**
* A timing safe equals comparison
* more info here: http://blog.ircmaxell.com/2014/11/its-all-about-time.html.
*
* @param string $safeString The internal (safe) value to be checked
* @param string $userString The user submitted (unsafe) value
*
* @return bool True if the two strings are identical
*/
private function timingSafeEquals($safeString, $userString)
{
if (function_exists('hash_equals')) {
return hash_equals($safeString, $userString);
}
$safeLen = strlen($safeString);
$userLen = strlen($userString);
if ($userLen != $safeLen) {
return false;
}
$result = 0;
for ($i = 0; $i < $userLen; ++$i) {
$result |= (ord($safeString[$i]) ^ ord($userString[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
}
@@ -0,0 +1,75 @@
<?php
// includes/db.php
$host = 'localhost';
$db = 'line_staff';
$user = 'root'; // Change as needed
$pass = '@Samui@1074200'; // Change as needed
$charset = 'utf8mb4';
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$bot_db = 'line_bot';
$dsn_bot = "mysql:host=$host;dbname=$bot_db;charset=$charset";
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
try {
// 1. Connection for local Line OA Database (line_staff) -> Settings & Admin
$pdo = new PDO($dsn, $user, $pass, $options);
// 2. Connection for Line Bot Database (line_bot) -> Staff Register
$pdo_bot = new PDO($dsn_bot, $user, $pass, $options);
} catch (\PDOException $e) {
die("Database connection failed. Please configure includes/db.php.");
}
// 2. Connection for HIS Database (hosxp)
$his_host = '10.0.250.250';
$his_db = 'hos';
$his_user = 'dmstele'; // Change as needed
$his_pass = 'dmstele10742'; // Change as needed
$conn_hosxp = mysqli_connect($his_host, $his_user, $his_pass, $his_db);
if (!$conn_hosxp) {
// die("HIS Database connection failed: " . mysqli_connect_error());
} else {
mysqli_set_charset($conn_hosxp, "utf8");
}
// 3. Connection for HR Database (hosoffice)
$hr_host = '10.0.250.115'; // *** อย่าลืมแก้ 10.0.x.x เป็น IP จริงของ HR นะครับ ***
$hr_db = 'hosoffice_2566';
$hr_user = 'hosoffice';
$hr_pass = 'hosoffice10742';
$conn_hosoffice = mysqli_connect($hr_host, $hr_user, $hr_pass, $hr_db);
if (!$conn_hosoffice) {
// die("HR Database connection failed: " . mysqli_connect_error());
} else {
mysqli_set_charset($conn_hosoffice, "utf8");
}
// 4. Connection for IT Assessment Database
$assess_host = '10.0.250.112'; // *** แก้เป็น IP ของ Host ประเมิน ***
$assess_db = 'it_assessment';
$assess_user = 'root'; // *** แก้ Username ***
$assess_pass = '@Samui@10742'; // *** แก้ Password ***
$dsn_assess = "mysql:host=$assess_host;dbname=$assess_db;charset=$charset";
try {
$pdo_assess = new PDO($dsn_assess, $assess_user, $assess_pass, $options);
} catch (\PDOException $e) {
$pdo_assess = null;
}
// Helper function to get settings
function getSetting($pdo, $key)
{
$stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?");
$stmt->execute([$key]);
$result = $stmt->fetch();
return $result ? $result['setting_value'] : null;
}
@@ -0,0 +1,29 @@
<?php
// liff/api_login.php
session_start();
require_once '../includes/db.php';
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$line_uid = $input['line_uid'] ?? '';
if (empty($line_uid)) {
echo json_encode(['status' => 'error', 'message' => 'No Line UID provided']);
exit;
}
// Check if this Line UID is already in the system and active
$stmt = $pdo_bot->prepare("SELECT * FROM line_staff_register WHERE user_id = ? AND ksh_user = 'Y'");
$stmt->execute([$line_uid]);
$staff = $stmt->fetch();
if ($staff) {
// User exists, set session
$_SESSION['cid'] = $staff['cid'];
echo json_encode(['status' => 'success', 'redirect' => 'index.php']);
} else {
// User not found, needs registration
echo json_encode(['status' => 'not_found']);
}
?>
@@ -0,0 +1,38 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['cid'])) { header("Location: register.php"); exit; }
$stmt = $pdo->prepare("SELECT * FROM attendances WHERE cid = ? ORDER BY work_date DESC LIMIT 30");
$stmt->execute([$_SESSION['cid']]);
$attendances = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อมูลเวลาเข้า-ออกงาน</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="header">
<h2>เวลาเข้า-ออกงาน (30 วันล่าสุด)</h2>
</div>
<div class="card" style="margin: 20px;">
<?php if (count($attendances) > 0): ?>
<?php foreach ($attendances as $att): ?>
<div style="border-bottom: 1px solid #eee; padding: 10px 0; display:flex; justify-content: space-between;">
<div><strong>วันที่:</strong> <?= $att['work_date'] ?></div>
<div><strong>เข้า:</strong> <?= $att['time_in'] ?: '-' ?> <strong>ออก:</strong> <?= $att['time_out'] ?: '-' ?></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="text-align:center;">ไม่มีข้อมูลการลงเวลา</p>
<?php endif; ?>
<br>
<a href="index.php" class="btn-magic" style="background: #ccc; color: #333;">กลับสู่เมนูหลัก</a>
</div>
</body>
</html>
@@ -0,0 +1,120 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['cid'])) {
// If not logged in, go to register which handles LIFF auto-login
header("Location: register.php");
exit;
}
$cid = $_SESSION['cid'];
// Fetch Staff Data
$stmt = $pdo_bot->prepare("SELECT * FROM line_staff_register WHERE cid = ?");
$stmt->execute([$cid]);
$staff = $stmt->fetch();
if (!$staff) {
// If not found, redirect to register
header("Location: register.php");
exit;
}
// Check Assessment Requirement
$require_assessment = getSetting($pdo, 'require_assessment');
$assessment_status = $staff['assessment_status'];
$can_access_menus = true;
if ($require_assessment == '1' && $assessment_status == '0') {
$can_access_menus = false;
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KSH-STAFF Menu</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
<!-- SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- FontAwesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="header">
<div class="shiny-logo" style="width: 100px; height: 100px; border-radius: 50%; margin: 0 auto; display: flex; align-items: center; justify-content: center; background: #fff;">
<img src="../assets/images/logo.png" alt="Logo" style="width: 100%; height: 100%; object-fit: contain;">
</div>
<h1>KSH-STAFF</h1>
<p>ยินดีต้อนรับ, คุณ <?= htmlspecialchars($staff['fname'] . ' ' . $staff['lname']) ?></p>
</div>
<div class="card" style="margin: 20px;">
<?php if (!$can_access_menus): ?>
<div style="background: #fff3cd; color: #856404; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #ffeeba;">
<i class="fas fa-exclamation-triangle"></i> <strong>แจ้งเตือน!</strong> คุณยังไม่ได้ทำแบบประเมินหน่วยงาน โปรดทำแบบประเมินเพื่อเข้าใช้งานเมนูต่างๆ
<br><br>
<button class="btn-magic" onclick="doAssessment()">ทำแบบประเมินตอนนี้</button>
</div>
<?php endif; ?>
<div class="menu-grid">
<a href="profile.php" class="menu-item" onclick="return checkAccess()">
<div class="menu-icon"><i class="fas fa-user"></i></div>
1. ข้อมูลส่วนตัว
</a>
<a href="leave.php" class="menu-item" onclick="return checkAccess()">
<div class="menu-icon"><i class="fas fa-calendar-alt"></i></div>
2. ข้อมูลวันลา
</a>
<a href="attendance.php" class="menu-item" onclick="return checkAccess()">
<div class="menu-icon"><i class="fas fa-clock"></i></div>
3. เวลาเข้า-ออก
</a>
<a href="salary.php" class="menu-item" onclick="return checkAccess()">
<div class="menu-icon"><i class="fas fa-file-invoice-dollar"></i></div>
4. ข้อมูลเงินเดือน
</a>
<a href="links.php" class="menu-item" onclick="return checkAccess()">
<div class="menu-icon"><i class="fas fa-link"></i></div>
5. ระบบอื่นๆ
</a>
</div>
</div>
<script>
const canAccess = <?= $can_access_menus ? 'true' : 'false' ?>;
function checkAccess() {
if (!canAccess) {
Swal.fire({
icon: 'warning',
title: 'ไม่สามารถเข้าใช้งานได้',
text: 'กรุณาทำแบบประเมินหน่วยงานก่อนเข้าใช้งานเมนูนี้',
confirmButtonText: 'รับทราบ',
confirmButtonColor: '#0072ff'
});
return false;
}
return true;
}
function doAssessment() {
Swal.fire({
title: 'จำลองการทำแบบประเมิน',
text: 'กำลังพาไปยังระบบประเมิน...',
icon: 'info'
}).then(() => {
// Mock redirect to assessment or API call
window.location.href = 'mock_assessment.php';
});
}
</script>
</body>
</html>
@@ -0,0 +1,39 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['cid'])) { header("Location: register.php"); exit; }
$stmt = $pdo->prepare("SELECT * FROM leaves WHERE cid = ? ORDER BY start_date DESC");
$stmt->execute([$_SESSION['cid']]);
$leaves = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อมูลวันลา</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="header">
<h2>ข้อมูลวันลา</h2>
</div>
<div class="card" style="margin: 20px;">
<?php if (count($leaves) > 0): ?>
<?php foreach ($leaves as $leave): ?>
<div style="border-bottom: 1px solid #eee; padding: 10px 0;">
<strong>ประเภท:</strong> <?= htmlspecialchars($leave['leave_type']) ?><br>
<strong>วันที่:</strong> <?= $leave['start_date'] ?> ถึง <?= $leave['end_date'] ?><br>
<strong>สถานะ:</strong> <?= htmlspecialchars($leave['status']) ?>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="text-align:center;">ไม่มีข้อมูลวันลา</p>
<?php endif; ?>
<br>
<a href="index.php" class="btn-magic" style="background: #ccc; color: #333;">กลับสู่เมนูหลัก</a>
</div>
</body>
</html>
@@ -0,0 +1,30 @@
<?php
session_start();
if (!isset($_SESSION['cid'])) { header("Location: register.php"); exit; }
$cid = $_SESSION['cid'];
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ระบบอื่นๆ</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="header">
<h2>ระบบอื่นๆ</h2>
</div>
<div class="card" style="margin: 20px;">
<p>คลิกเพื่อไปยังระบบต่างๆ (ส่งค่ารหัสประจำตัวไปด้วย)</p>
<div style="display: flex; flex-direction: column; gap: 10px;">
<a href="https://example.com/system1?cid=<?= htmlspecialchars($cid) ?>" target="_blank" class="btn-magic" style="background: #28a745;">ระบบสารบรรณ</a>
<a href="https://example.com/system2?cid=<?= htmlspecialchars($cid) ?>" target="_blank" class="btn-magic" style="background: #17a2b8;">ระบบแจ้งซ่อม</a>
<a href="https://example.com/system3?cid=<?= htmlspecialchars($cid) ?>" target="_blank" class="btn-magic" style="background: #6c757d;">ระบบเบิกวัสดุ</a>
</div>
<br><br>
<a href="index.php" class="btn-magic" style="background: #ccc; color: #333;">กลับสู่เมนูหลัก</a>
</div>
</body>
</html>
@@ -0,0 +1,39 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['cid'])) {
header("Location: register.php");
exit;
}
$stmt = $pdo_bot->prepare("SELECT * FROM line_staff_register WHERE cid = ?");
$stmt->execute([$_SESSION['cid']]);
$staff = $stmt->fetch();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อมูลส่วนตัว</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="header">
<h2><i class="fas fa-user"></i> ข้อมูลส่วนตัว</h2>
</div>
<div class="card" style="margin: 20px;">
<p><strong>CID:</strong> <?= htmlspecialchars($staff['cid']) ?></p>
<p><strong>HN:</strong> <?= htmlspecialchars($staff['hn']) ?></p>
<p><strong>ชื่อ-นามสกุล:</strong> <?= htmlspecialchars($staff['fname'] . ' ' . $staff['lname']) ?></p>
<p><strong>เบอร์โทรศัพท์:</strong> <?= htmlspecialchars($staff['phone']) ?></p>
<p><strong>สถานะประเมิน:</strong> <?= $staff['assessment_status'] == '1' ? '<span style="color:green;">ทำแล้ว</span>' : '<span style="color:red;">ยังไม่ทำ</span>' ?></p>
<br>
<a href="index.php" class="btn-magic" style="background: #ccc; color: #333;">กลับสู่เมนูหลัก</a>
</div>
</body>
</html>
@@ -0,0 +1,281 @@
<?php
session_start();
require_once '../includes/db.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$cid = trim($_POST['cid'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$line_uid = trim($_POST['line_uid'] ?? '');
$consent = $_POST['consent'] ?? '';
if (empty($line_uid)) {
$error = "เกิดข้อผิดพลาด: ไม่พบข้อมูล Line UID กรุณาเปิดผ่านแอปพลิเคชัน Line";
} elseif (empty($consent)) {
$error = "กรุณากดยอมรับข้อตกลงการใช้งาน";
} else {
// Find HN from HIS patient table using $conn_hosxp
if ($conn_hosxp) {
$cid_safe = mysqli_real_escape_string($conn_hosxp, $cid);
$getHN = "SELECT * FROM patient WHERE cid='$cid_safe'";
$result_hn = mysqli_query($conn_hosxp, $getHN);
$patient = mysqli_fetch_assoc($result_hn);
} else {
// Fallback for local testing if hosxp connection fails
// (Using the mock table in $pdo)
$stmt_hn = $pdo->prepare("SELECT hn, fname, lname FROM patient WHERE cid = ?");
$stmt_hn->execute([$cid]);
$patient = $stmt_hn->fetch();
}
if ($patient) {
$hn = $patient['hn'];
$fname = $patient['fname'];
$lname = $patient['lname'];
// Check if CID already registered in line_staff_register
$stmt_check = $pdo_bot->prepare("SELECT * FROM line_staff_register WHERE cid = ?");
$stmt_check->execute([$cid]);
$existing = $stmt_check->fetch();
if ($existing) {
if (!empty($existing['user_id']) && $existing['user_id'] !== $line_uid) {
$error = "เลขบัตรประชาชนนี้ถูกผูกกับบัญชี Line อื่นไปแล้ว";
} else {
// Update existing
$update = $pdo_bot->prepare("UPDATE line_staff_register SET user_id = ?, phone = ?, hn = ?, fname = ?, lname = ?, date = NOW(), ksh_user = 'Y' WHERE cid = ?");
$update->execute([$line_uid, $phone, $hn, $fname, $lname, $cid]);
$_SESSION['cid'] = $cid;
echo "<script>alert('อัปเดตข้อมูลและผูกบัญชี Line สำเร็จ'); window.location.href='index.php';</script>";
exit;
}
} else {
// Insert new
$insert = $pdo_bot->prepare("INSERT INTO line_staff_register (user_id, fname, lname, cid, phone, date, hn, ksh_user, assessment_status) VALUES (?, ?, ?, ?, ?, NOW(), ?, 'Y', 0)");
$insert->execute([$line_uid, $fname, $lname, $cid, $phone, $hn]);
$_SESSION['cid'] = $cid;
echo "<script>alert('ลงทะเบียนสำเร็จ'); window.location.href='index.php';</script>";
exit;
}
} else {
$error = "ไม่พบหมายเลขบัตรประชาชนในระบบข้อมูลผู้ป่วย (HIS) กรุณาติดต่อหน่วยงานที่ดูแล";
}
}
}
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>ลงทะเบียนเข้าใช้งาน</title>
<link rel="icon" type="image/png" href="../assets/img/logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Sarabun', 'sans-serif'],
},
animation: {
shine: 'shine 3s ease-in-out infinite'
},
keyframes: {
shine: {
'0%': { left: '-150%' },
'20%': { left: '150%' },
'100%': { left: '150%' }
}
}
}
}
}
</script>
</head>
<body class="bg-gray-50 font-sans antialiased text-gray-800 min-h-screen flex flex-col items-center justify-center p-4">
<div class="w-full max-w-md">
<!-- Header -->
<div class="text-center mb-8">
<div class="relative inline-block overflow-hidden rounded-full w-24 h-24 mx-auto mb-4 shadow-md bg-white p-1">
<img src="../assets/img/logo.png" alt="Logo" class="w-full h-full object-cover rounded-full relative z-10">
<div class="absolute inset-0 z-20 w-1/2 h-full bg-gradient-to-r from-transparent via-white/60 to-transparent -skew-x-12 animate-shine mix-blend-overlay"></div>
</div>
<h2 class="text-2xl font-bold text-gray-800">ลงทะเบียน KSH-STAFF</h2>
</div>
<!-- Loading State -->
<div id="loading" class="text-center bg-white rounded-2xl shadow-sm border border-gray-100 p-8">
<i class="fas fa-circle-notch fa-spin text-blue-500 text-4xl mb-4"></i>
<p class="text-gray-600 font-medium">กำลังตรวจสอบข้อมูล Line ของคุณ...</p>
</div>
<!-- Register Form -->
<div id="register-form" class="hidden bg-white rounded-2xl shadow-sm border border-gray-100 p-6 md:p-8">
<?php if (!empty($error)): ?>
<div class="bg-red-50 text-red-500 text-sm font-medium py-3 px-4 rounded-lg text-center mb-6 border border-red-100">
<?= htmlspecialchars($error) ?>
</div>
<?php endif; ?>
<form method="post" id="form-content">
<input type="hidden" name="line_uid" id="line_uid" value="">
<div class="mb-5">
<label class="block text-sm font-semibold text-gray-700 mb-2">เลขบัตรประชาชน (13 หลัก)</label>
<input type="text" name="cid" class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition duration-200" placeholder="ระบุเลข 13 หลัก" required pattern="\d{13}" oninvalid="this.setCustomValidity('รูปแบบเลขบัตรไม่ถูกต้อง')" oninput="this.setCustomValidity('')">
</div>
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">หมายเลขโทรศัพท์</label>
<input type="text" name="phone" class="w-full px-4 py-3 rounded-xl border border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 outline-none transition duration-200" placeholder="08xxxxxxxx" required pattern="\d{9,10}" oninvalid="this.setCustomValidity('รูปแบบเบอร์โทรศัพท์ไม่ถูกต้อง')" oninput="this.setCustomValidity('')">
</div>
<div class="mb-8 flex items-start gap-3 bg-gray-50 p-4 rounded-xl border border-gray-100">
<div class="flex items-center h-5 mt-0.5">
<input type="checkbox" name="consent" id="consent" value="1" required class="w-5 h-5 text-blue-600 bg-white border-gray-300 rounded focus:ring-blue-500">
</div>
<label for="consent" class="text-sm text-gray-600 cursor-pointer select-none">
ข้าพเจ้ายอมรับ <button type="button" onclick="document.getElementById('pdpaModal').classList.remove('hidden')" class="text-blue-600 font-semibold hover:underline">ข้อตกลงการใช้งาน</button>
</label>
</div>
<button type="submit" class="w-full bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-bold py-3.5 px-4 rounded-xl shadow-md transform hover:-translate-y-0.5 transition duration-200">
ยืนยันการลงทะเบียน
</button>
</form>
</div>
</div>
<!-- PDPA Modal -->
<div id="pdpaModal" class="hidden fixed inset-0 z-50 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">
<!-- Background overlay -->
<div class="fixed inset-0 bg-gray-900 bg-opacity-50 transition-opacity" aria-hidden="true" onclick="document.getElementById('pdpaModal').classList.add('hidden')"></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-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
<div class="bg-blue-600 px-6 py-4 flex justify-between items-center">
<h3 class="text-lg leading-6 font-bold text-white" id="modal-title">ข้อตกลงการใช้งาน</h3>
<button type="button" class="text-white hover:text-gray-200 focus:outline-none" onclick="document.getElementById('pdpaModal').classList.add('hidden')">
<i class="fas fa-times text-xl"></i>
</button>
</div>
<div class="bg-white px-6 py-6 h-96 overflow-y-auto text-sm text-gray-600 leading-relaxed">
<p class="mb-4 indent-4">โปรดอ่านข้อความด้านล่างนี้เพื่อทำความเข้าใจวัตถุประสงค์ในการเก็บรวบรวม ใช้ และเปิดเผยข้อมูลของท่าน โรงพยาบาลเกาะสมุยให้บริการ LINE Official Account KSH STAFF เพื่ออำนวยความสะดวกในการสื่อสารและให้บริการแก่ท่าน การที่ท่านกดเพิ่มเพื่อนใน LINE Official Account ของโรงพยาบาล หรือให้ข้อมูลส่วนบุคคลแก่เราผ่านช่องทาง LINE นี้ ถือว่าท่านได้อ่านและทำความเข้าใจ รวมถึงให้ความยินยอมตามวัตถุประสงค์ดังต่อไปนี้ :</p>
<ul class="space-y-4">
<li><b class="text-gray-800">1. วัตถุประสงค์ในการเก็บรวบรวม ใช้ และเปิดเผยข้อมูล:</b>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>เพื่อใช้เป็นช่องทางในการติดต่อสื่อสารระหว่างท่านกับโรงพยาบาล เช่น ข้อมูลส่วนตัว, ข้อมูลเงินเดือน, ข้อมูลการลา, ข้อมูลการเข้า-ออกงาน</li>
<li>เพื่อประชาสัมพันธ์ข้อมูลข่าวสาร, กิจกรรมที่เกี่ยวข้องกับบริการของโรงพยาบาล</li>
<li>เพื่อตอบคำถาม หรือให้ความช่วยเหลือตามที่ท่านสอบถามผ่านช่องทาง LINE</li>
<li>เพื่อปรับปรุงและพัฒนาบริการของโรงพยาบาลให้ดียิ่งขึ้น</li>
</ul>
</li>
<li><b class="text-gray-800">2. ข้อมูลส่วนบุคคลที่อาจมีการเก็บรวบรวม:</b>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li><b>ข้อมูลบัญชี LINE ของท่าน:</b> ชื่อโปรไฟล์, รูปโปรไฟล์</li>
<li><b>ข้อความสนทนา:</b> ข้อความที่ท่านส่งมายัง LINE Official Account ของโรงพยาบาล</li>
<li><b>ข้อมูลที่ท่านให้เพิ่มเติม:</b> เช่น ชื่อ-นามสกุล,เลขบัตรประจำตัวประชาชน, เบอร์โทรศัพท์, วันเกิด,หมายเลขPassport หรือข้อมูลสุขภาพเบื้องต้น</li>
</ul>
</li>
<li><b class="text-gray-800">3. การคุ้มครองข้อมูลส่วนบุคคล:</b>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>โรงพยาบาลจะเก็บรักษาข้อมูลส่วนบุคคลของท่านที่ได้รับผ่าน LINE Official Account ไว้เป็นความลับตามมาตรฐานการรักษาความปลอดภัยของโรงพยาบาลและกฎหมายคุ้มครองข้อมูลส่วนบุคคล (PDPA)</li>
<li>ข้อมูลของท่านจะถูกใช้งานตามวัตถุประสงค์ที่แจ้งไว้เท่านั้น และจะไม่ถูกเปิดเผยแก่บุคคลที่สามโดยไม่ได้รับความยินยอมจากท่าน เว้นแต่เป็นกรณีที่กฎหมายกำหนด</li>
</ul>
</li>
<li><b class="text-gray-800">4. สิทธิของท่าน:</b>
<ul class="list-disc pl-5 mt-2 space-y-1">
<li>ท่านมีสิทธิที่จะยกเลิกการเป็นเพื่อนกับ LINE Official Account ของโรงพยาบาลได้ตลอดเวลา</li>
<li>ท่านสามารถขอเข้าถึง ขอแก้ไข หรือขอให้ลบข้อมูลส่วนบุคคลของท่านที่เกี่ยวข้องกับการใช้ LINE Official Account นี้ได้ โดยติดต่อ งานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย โทร 077913200 ต่อ 3013</li>
</ul>
</li>
</ul>
<div class="mt-6 p-4 bg-blue-50 rounded-lg text-blue-800 border border-blue-100">
<p class="font-semibold mb-2">โดยการกด "เพิ่มเพื่อน" หรือดำเนินการใช้งาน LINE Official Account KSH STAFF ต่อไป ถือว่าท่านได้อ่านและตกลงให้ความยินยอมตามข้อกำหนดข้างต้นทุกประการ</p>
<p class="text-xs">หากมีข้อสงสัยหรือต้องการสอบถามเพิ่มเติม โปรดติดต่อ งานสุขภาพดิจิทัล โรงพยาบาลเกาะสมุย โทร 077913200 ต่อ 3013 หรือ contact@samuihospital.go.th</p>
</div>
</div>
<div class="bg-gray-50 px-6 py-4 flex justify-end border-t border-gray-200">
<button type="button" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-semibold py-2 px-6 rounded-lg transition duration-200" onclick="document.getElementById('pdpaModal').classList.add('hidden')">ปิดหน้าต่าง</button>
</div>
</div>
</div>
</div>
<script>
// ใส่ LIFF ID ของคุณที่นี่
const LIFF_ID = "YOUR_LIFF_ID_HERE";
async function initializeLiff() {
try {
if (LIFF_ID === "YOUR_LIFF_ID_HERE") {
console.warn("Please configure your LIFF_ID. Bypassing LIFF for testing.");
showRegisterForm("U_TEST_DUMMY_ID", "Tester");
return;
}
await liff.init({ liffId: LIFF_ID });
if (!liff.isLoggedIn()) {
liff.login();
return;
}
const profile = await liff.getProfile();
const userId = profile.userId;
const displayName = profile.displayName;
// ตรวจสอบว่าเคยลงทะเบียนหรือยัง
const response = await fetch('api_login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ line_uid: userId })
});
const data = await response.json();
if (data.status === 'success') {
// ล็อกอินอัตโนมัติสำเร็จ
window.location.href = data.redirect;
} else {
// ยังไม่เคยลงทะเบียน ให้แสดงฟอร์ม
showRegisterForm(userId, displayName);
}
} catch (err) {
console.error('LIFF Error:', err);
Swal.fire('ข้อผิดพลาด', 'ไม่สามารถเชื่อมต่อระบบ Line ได้: ' + err.message, 'error');
document.getElementById('loading').innerHTML = "<div class='text-red-500 font-semibold'>ไม่สามารถใช้งาน LIFF ได้</div>";
}
}
function showRegisterForm(userId, displayName) {
document.getElementById('loading').classList.add('hidden');
document.getElementById('register-form').classList.remove('hidden');
document.getElementById('line_uid').value = userId;
document.getElementById('line_display_name').innerText = displayName;
}
window.onload = function() {
initializeLiff();
<?php if (!empty($error)): ?>
document.getElementById('loading').classList.add('hidden');
document.getElementById('register-form').classList.remove('hidden');
<?php endif; ?>
};
</script>
</body>
</html>
@@ -0,0 +1,38 @@
<?php
session_start();
require_once '../includes/db.php';
if (!isset($_SESSION['cid'])) { header("Location: register.php"); exit; }
$stmt = $pdo->prepare("SELECT * FROM salaries WHERE cid = ? ORDER BY year DESC, month DESC LIMIT 12");
$stmt->execute([$_SESSION['cid']]);
$salaries = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="th">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ข้อมูลเงินเดือน</title>
<link href="https://fonts.googleapis.com/css2?family=Prompt:wght@300;400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="header">
<h2>สลิปเงินเดือน</h2>
</div>
<div class="card" style="margin: 20px;">
<?php if (count($salaries) > 0): ?>
<?php foreach ($salaries as $sal): ?>
<div style="border-bottom: 1px solid #eee; padding: 10px 0; display:flex; justify-content: space-between; align-items:center;">
<div><strong>เดือน/ปี:</strong> <?= str_pad($sal['month'], 2, '0', STR_PAD_LEFT) ?>/<?= $sal['year'] ?></div>
<div><strong style="color: #0072ff;"><?= number_format($sal['net_salary'], 2) ?> ฿</strong></div>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="text-align:center;">ไม่มีข้อมูลเงินเดือน</p>
<?php endif; ?>
<br>
<a href="index.php" class="btn-magic" style="background: #ccc; color: #333;">กลับสู่เมนูหลัก</a>
</div>
</body>
</html>