424 lines
28 KiB
PHP
424 lines
28 KiB
PHP
<?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">​</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'; ?>
|