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,423 @@
<?php
require_once 'includes/auth.php';
// เช็คสิทธิ์ว่าเป็นแอดมินหรือไม่
if (!isAdmin()) {
header("Location: index.php");
exit();
}
$pageTitle = 'ตั้งค่าผู้ใช้งาน | ระบบจัดการข้อมูลยานพาหนะ โรงพยาบาลเกาะสมุย';
$bodyClass = 'bg-slate-50 dark:bg-slate-900 overflow-hidden';
require_once 'includes/header.php';
?>
<div class="flex h-screen overflow-hidden" x-data="userManagement()">
<?php require_once 'includes/sidebar.php'; ?>
<!-- Main Content -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden page-transition">
<?php require_once 'includes/navbar.php'; ?>
<!-- Content Area -->
<div class="flex-1 overflow-auto p-4 sm:p-6 lg:p-8 custom-scrollbar relative">
<!-- Background decoration -->
<div class="absolute top-[-10%] right-[-5%] w-[40vw] h-[40vw] rounded-full bg-primary-300/10 dark:bg-primary-900/10 blur-[100px] pointer-events-none"></div>
<div class="absolute bottom-[-10%] left-[-5%] w-[30vw] h-[30vw] rounded-full bg-teal-300/10 dark:bg-teal-900/10 blur-[80px] pointer-events-none"></div>
<div class="max-w-7xl mx-auto relative z-10">
<div class="flex flex-col sm:flex-row sm:items-center justify-between mb-8 gap-4">
<div>
<h1 class="text-3xl font-bold text-slate-800 dark:text-white drop-shadow-sm flex items-center gap-3">
<div class="bg-gradient-to-br from-primary-500 to-primary-600 text-white p-2 rounded-xl shadow-lg shadow-primary-500/30">
<i class="fa-solid fa-users-gear text-xl w-6 text-center"></i>
</div>
ตั้งค่าผู้ใช้งาน
</h1>
<p class="text-slate-600 dark:text-slate-400 mt-2 font-medium">จัดการบัญชีผู้ใช้งานระบบและกำหนดสิทธิ์การเข้าถึง</p>
</div>
<button @click="openModal()" class="btn-gradient px-5 py-2.5 rounded-xl flex items-center space-x-2 shadow-lg shadow-primary-500/30 hover:shadow-primary-500/50 hover:-translate-y-0.5 transition-all w-full sm:w-auto justify-center">
<i class="fa-solid fa-plus text-sm"></i>
<span>เพิ่มผู้ใช้งาน</span>
</button>
</div>
<!-- Filter Section -->
<div class="glass-card p-4 mb-6 relative z-20">
<div class="flex flex-col md:flex-row gap-4">
<div class="flex-1 relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i class="fa-solid fa-search text-slate-400"></i>
</div>
<input type="text" x-model="searchQuery" class="glass-input block w-full pl-10 p-2.5" placeholder="ค้นหาจาก ชื่อ-สกุล หรือ เลขบัตรประชาชน...">
</div>
</div>
</div>
<!-- User List -->
<div class="bg-white dark:bg-slate-800 rounded-2xl shadow-sm border border-slate-200 dark:border-slate-700 overflow-hidden">
<!-- Loading State -->
<div x-show="isLoading" class="p-10 flex justify-center items-center">
<i class="fa-solid fa-spinner fa-spin text-3xl text-primary-500"></i>
</div>
<!-- Table -->
<div class="overflow-x-auto" x-show="!isLoading" style="display: none;">
<table class="w-full text-sm text-left text-slate-600 dark:text-slate-400">
<thead class="text-xs text-slate-700 uppercase bg-slate-50 dark:bg-slate-700/50 dark:text-slate-300">
<tr>
<th scope="col" class="px-6 py-4 rounded-tl-2xl">เลขบัตรประชาชน / Username</th>
<th scope="col" class="px-6 py-4">ชื่อ-สกุล</th>
<th scope="col" class="px-6 py-4">สิทธิ์การใช้งาน</th>
<th scope="col" class="px-6 py-4">วันที่เพิ่ม</th>
<th scope="col" class="px-6 py-4 text-right rounded-tr-2xl">จัดการ</th>
</tr>
</thead>
<tbody>
<template x-for="user in filteredUsers" :key="user.id">
<tr class="bg-white border-b dark:bg-slate-800 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 font-medium text-slate-900 whitespace-nowrap dark:text-white" x-text="user.id_card || user.username"></td>
<td class="px-6 py-4" x-text="user.full_name"></td>
<td class="px-6 py-4">
<span class="px-2.5 py-1 text-xs font-semibold rounded-full"
:class="user.role === 'admin' ? 'bg-primary-100 text-primary-800 dark:bg-primary-900/30 dark:text-primary-300' : 'bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-300'"
x-text="user.role === 'admin' ? 'ผู้ดูแลระบบ' : 'ผู้ใช้ทั่วไป'"></span>
</td>
<td class="px-6 py-4" x-text="formatDate(user.created_at)"></td>
<td class="px-6 py-4 text-right">
<button @click="openModal(user)" class="text-primary-600 dark:text-primary-400 hover:underline mr-3"><i class="fa-solid fa-edit"></i></button>
<button @click="deleteUser(user.id)" class="text-danger hover:underline"><i class="fa-solid fa-trash"></i></button>
</td>
</tr>
</template>
<!-- Empty state -->
<tr x-show="filteredUsers.length === 0">
<td colspan="5" class="px-6 py-10 text-center text-slate-500">
ไม่พบข้อมูลผู้ใช้งาน
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- User Modal -->
<div x-show="showModal" style="display: none;" class="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">
<div x-show="showModal" x-transition.opacity class="fixed inset-0 bg-slate-900/60 dark:bg-slate-900/80 backdrop-blur-sm transition-opacity" @click="closeModal()"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div x-show="showModal"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
class="relative z-10 inline-block align-bottom bg-white dark:bg-slate-800 rounded-[20px] text-left overflow-hidden shadow-2xl transform transition-all sm:my-8 sm:align-middle sm:max-w-md w-full border border-slate-200 dark:border-slate-700/50">
<div class="px-6 py-5 border-b border-slate-100 dark:border-slate-700/50 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/50">
<h3 class="text-lg leading-6 font-bold text-slate-900 dark:text-white" id="modal-title" x-text="isEdit ? 'แก้ไขผู้ใช้งาน' : 'เพิ่มผู้ใช้งานใหม่'"></h3>
<button @click="closeModal()" class="text-slate-400 hover:text-slate-500 focus:outline-none bg-slate-100 hover:bg-slate-200 dark:bg-slate-700 dark:hover:bg-slate-600 rounded-full p-2 transition-colors">
<i class="fa-solid fa-xmark w-4 h-4 flex items-center justify-center"></i>
</button>
</div>
<div class="px-6 py-5">
<form @submit.prevent="saveUser">
<div x-show="errorMsg" class="mb-4 bg-danger/10 border border-danger/30 text-danger text-sm rounded-lg p-3 text-center" style="display: none;" x-text="errorMsg"></div>
<div class="space-y-4">
<div x-show="!isEdit" class="relative">
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1"><i class="fa-solid fa-hospital-user text-primary-500 mr-1"></i> ค้นหาจากระบบ HIS (hosoffice)</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
<i class="fa-solid fa-search text-slate-400"></i>
</div>
<input type="text" x-model="hrSearchQuery" @input.debounce.500ms="searchHrPerson" class="glass-input block w-full pl-10 p-2.5 text-sm" placeholder="พิมพ์ชื่อหรือนามสกุล (อย่างน้อย 2 ตัวอักษร)...">
</div>
<!-- Autocomplete Dropdown -->
<div x-show="hrSearchResults.length > 0" @click.away="hrSearchResults = []" style="display: none;" class="absolute z-10 w-full mt-1 bg-white dark:bg-slate-800 rounded-md shadow-lg shadow-slate-200/50 dark:shadow-slate-900/50 border border-slate-200 dark:border-slate-700 max-h-80 overflow-auto">
<ul class="py-1 text-sm text-slate-700 dark:text-slate-200">
<template x-for="person in hrSearchResults">
<li>
<button type="button" @click="selectHrPerson(person)" class="w-full text-left px-4 py-3 hover:bg-primary-50 dark:hover:bg-slate-700 transition-colors border-b border-slate-100 dark:border-slate-700/50 last:border-0 flex items-start gap-3">
<div class="flex-shrink-0 mt-0.5">
<img x-show="person.has_image" :src="'api/hr_image.php?cid=' + person.cid" class="w-10 h-10 rounded-full object-cover border border-slate-200 dark:border-slate-600">
<div x-show="!person.has_image" class="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 flex items-center justify-center text-slate-500 dark:text-slate-400">
<i class="fa-solid fa-user"></i>
</div>
</div>
<div class="flex-1 min-w-0">
<div class="font-bold text-primary-700 dark:text-primary-400 flex items-center gap-2">
<span x-text="person.full_name"></span>
<span class="text-xs text-slate-500 font-normal ml-2" x-text="person.cid"></span>
</div>
<div class="text-xs text-slate-500 dark:text-slate-400 mt-1 flex flex-col gap-0.5">
<span x-show="person.position" x-text="'ตำแหน่ง: ' + person.position"></span>
<span x-show="person.department" x-text="'หน่วยงาน: ' + person.department"></span>
</div>
</div>
</button>
</li>
</template>
</ul>
</div>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">เลขบัตรประชาชน (13 หลัก) <span class="text-danger">*</span></label>
<input type="text" x-model="form.id_card" required maxlength="13" class="glass-input block w-full p-2.5 text-sm" placeholder="เลข 13 หลัก">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">ชื่อ-สกุล <span class="text-danger">*</span></label>
<input type="text" x-model="form.full_name" required class="glass-input block w-full p-2.5 text-sm" placeholder="เช่น สมชาย ใจดี">
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">สิทธิ์การใช้งาน</label>
<select x-model="form.role" class="glass-input block w-full p-2.5 text-sm">
<option value="user">ผู้ใช้ทั่วไป</option>
<option value="admin">ผู้ดูแลระบบ</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
รหัสผ่าน <span class="text-danger" x-show="!isEdit">*</span>
<span class="text-xs text-slate-400 font-normal ml-2" x-show="isEdit">(เว้นว่างไว้หากไม่ต้องการเปลี่ยน)</span>
</label>
<input type="password" x-model="form.password" :required="!isEdit" class="glass-input block w-full p-2.5 text-sm" placeholder="รหัสผ่าน">
<p class="text-[10px] text-slate-500 mt-1">ต้องมีความยาวไม่น้อยกว่า 6 ตัวอักษร</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
ยืนยันรหัสผ่าน <span class="text-danger" x-show="!isEdit">*</span>
</label>
<input type="password" x-model="form.confirm_password" :required="!isEdit || form.password !== ''" class="glass-input block w-full p-2.5 text-sm" placeholder="ยืนยันรหัสผ่าน">
</div>
</div>
<div class="mt-6 sm:flex sm:flex-row-reverse">
<button type="submit" class="w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2 bg-primary-600 text-base font-medium text-white hover:bg-primary-700 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm shadow-primary-600/30">
<i class="fa-solid fa-save mr-2 mt-0.5"></i> บันทึก
</button>
<button type="button" @click="closeModal()" class="mt-3 w-full inline-flex justify-center rounded-xl border border-slate-300 dark:border-slate-600 shadow-sm px-4 py-2 bg-white dark:bg-slate-700 text-base font-medium text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-600 focus:outline-none sm:mt-0 sm:w-auto sm:text-sm">
ยกเลิก
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function userManagement() {
return {
isExpanded: true,
toggleSidebar() { this.isExpanded = !this.isExpanded; },
users: [],
searchQuery: '',
isLoading: false,
showModal: false,
isEdit: false,
errorMsg: '',
hrSearchQuery: '',
hrSearchResults: [],
form: {
id: null,
id_card: '',
full_name: '',
role: 'user',
password: '',
confirm_password: ''
},
init() {
this.fetchUsers();
},
fetchUsers() {
this.isLoading = true;
fetch('api/users.php')
.then(res => res.json())
.then(data => {
if (data.users) {
this.users = data.users;
}
this.isLoading = false;
})
.catch(err => {
console.error(err);
this.isLoading = false;
});
},
get filteredUsers() {
if (this.searchQuery === '') return this.users;
const q = this.searchQuery.toLowerCase();
return this.users.filter(u =>
(u.full_name && u.full_name.toLowerCase().includes(q)) ||
(u.id_card && u.id_card.includes(q)) ||
(u.username && u.username.toLowerCase().includes(q))
);
},
openModal(user = null) {
this.errorMsg = '';
this.hrSearchQuery = '';
this.hrSearchResults = [];
if (user) {
this.isEdit = true;
this.form = {
id: user.id,
id_card: user.id_card || '',
full_name: user.full_name || '',
role: user.role || 'user',
password: '',
confirm_password: ''
};
} else {
this.isEdit = false;
this.form = {
id: null,
id_card: '',
full_name: '',
role: 'user',
password: '',
confirm_password: ''
};
}
this.showModal = true;
},
closeModal() {
this.showModal = false;
},
validatePassword(pw) {
return pw.length >= 6;
},
saveUser() {
this.errorMsg = '';
if (this.form.id_card.length !== 13) {
this.errorMsg = 'เลขบัตรประชาชนต้องมี 13 หลัก';
return;
}
if (this.form.password !== this.form.confirm_password) {
this.errorMsg = 'รหัสผ่านไม่ตรงกัน';
return;
}
if (this.form.password !== '' || !this.isEdit) {
if (!this.validatePassword(this.form.password)) {
this.errorMsg = 'รหัสผ่านต้องมีความยาวไม่น้อยกว่า 6 ตัวอักษร';
return;
}
}
const method = this.isEdit ? 'PUT' : 'POST';
fetch('api/users.php', {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.form)
})
.then(res => res.json())
.then(data => {
if (data.error) {
this.errorMsg = data.error;
} else {
this.closeModal();
this.fetchUsers();
Swal.fire({
icon: 'success',
title: 'สำเร็จ',
text: 'บันทึกข้อมูลผู้ใช้งานเรียบร้อยแล้ว',
timer: 1500,
showConfirmButton: false
});
}
})
.catch(err => {
this.errorMsg = 'เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์';
console.error(err);
});
},
searchHrPerson() {
if (this.hrSearchQuery.length < 2) {
this.hrSearchResults = [];
return;
}
fetch('api/hr_person_search.php?q=' + encodeURIComponent(this.hrSearchQuery))
.then(res => res.json())
.then(data => {
if (data.success) {
this.hrSearchResults = data.data;
} else {
this.hrSearchResults = [];
}
})
.catch(err => {
console.error('Error fetching HR data:', err);
this.hrSearchResults = [];
});
},
selectHrPerson(person) {
this.form.id_card = person.cid;
this.form.full_name = person.full_name;
this.hrSearchQuery = '';
this.hrSearchResults = [];
},
deleteUser(id) {
Swal.fire({
title: 'ยืนยันการลบ?',
text: "คุณไม่สามารถกู้คืนข้อมูลได้หลังจากลบ!",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#ef4444',
cancelButtonColor: '#64748b',
confirmButtonText: 'ลบข้อมูล',
cancelButtonText: 'ยกเลิก',
background: document.documentElement.classList.contains('dark') ? '#1e293b' : '#ffffff',
color: document.documentElement.classList.contains('dark') ? '#f1f5f9' : '#0f172a'
}).then((result) => {
if (result.isConfirmed) {
fetch('api/users.php?id=' + id, { method: 'DELETE' })
.then(res => res.json())
.then(data => {
if(data.error) {
Swal.fire('ข้อผิดพลาด!', data.error, 'error');
} else {
this.fetchUsers();
Swal.fire('ลบแล้ว!', 'ข้อมูลผู้ใช้งานถูกลบเรียบร้อย', 'success');
}
});
}
});
},
formatDate(dateStr) {
if(!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleDateString('th-TH', { year: 'numeric', month: 'short', day: 'numeric' });
}
}
}
</script>
<?php require_once 'includes/footer.php'; ?>