Files
2026-09-16 23:20:08 +07:00

197 lines
12 KiB
PHP

<?php
require_once 'includes/auth.php';
// Check if user is admin
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="logManagement()">
<?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-[0%] right-[10%] w-[30vw] h-[30vw] rounded-full bg-blue-300/10 dark:bg-blue-900/10 blur-[100px] pointer-events-none"></div>
<div class="max-w-7xl mx-auto relative z-10">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-8">
<div>
<h1 class="text-2xl font-bold text-slate-800 dark:text-white flex items-center gap-2 drop-shadow-sm">
<i class="fa-solid fa-list-check text-primary-600 dark:text-primary-400"></i>
ประวัติการใช้งานระบบ (Activity Logs)
</h1>
<p class="text-slate-500 dark:text-slate-400 mt-1">แสดงประวัติการใช้งานระบบ และการกระทำต่างๆ ของผู้ใช้</p>
</div>
</div>
<!-- Search and Filter -->
<div class="glass-card p-4 mb-6 rounded-2xl flex flex-col sm:flex-row gap-4">
<div class="relative flex-1">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center 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 py-2.5 rounded-xl text-sm" placeholder="ค้นหาจากชื่อผู้ใช้, การกระทำ, หรือ IP Address...">
</div>
<div class="w-full sm:w-48">
<input type="date" x-model="dateFilter" class="glass-input block w-full py-2.5 px-3 rounded-xl text-sm text-slate-700 dark:text-slate-300">
</div>
</div>
<!-- Table -->
<div class="glass-card rounded-2xl overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-700/50">
<thead class="bg-slate-50/50 dark:bg-slate-800/50">
<tr>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">วัน/เวลา</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">ผู้ใช้งาน</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">การกระทำ</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">ข้อมูลอ้างอิง</th>
<th scope="col" class="px-6 py-4 text-left text-xs font-bold text-slate-500 dark:text-slate-400 uppercase tracking-wider">IP Address</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-700/50 bg-white/30 dark:bg-slate-800/30">
<template x-if="isLoading">
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500">
<i class="fa-solid fa-spinner fa-spin mr-2"></i> กำลังโหลดข้อมูล...
</td>
</tr>
</template>
<template x-if="!isLoading && filteredLogs.length === 0">
<tr>
<td colspan="5" class="px-6 py-8 text-center text-slate-500">
<div class="flex flex-col items-center justify-center">
<i class="fa-solid fa-inbox text-4xl mb-3 text-slate-300 dark:text-slate-600"></i>
<p>ไม่พบข้อมูลประวัติการใช้งาน</p>
</div>
</td>
</tr>
</template>
<template x-for="log in filteredLogs" :key="log.id">
<tr class="hover:bg-primary-50/50 dark:hover:bg-slate-700/50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400" x-text="formatDate(log.created_at)"></td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="flex-shrink-0 h-8 w-8 rounded-full bg-primary-100 dark:bg-primary-900/50 flex items-center justify-center text-primary-600 dark:text-primary-400">
<i class="fa-solid fa-user text-xs"></i>
</div>
<div class="ml-3">
<div class="text-sm font-medium text-slate-900 dark:text-white" x-text="log.user_name || 'System / Unknown'"></div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2.5 py-1 inline-flex text-xs leading-5 font-semibold rounded-full"
:class="getActionBadgeClass(log.action)"
x-text="log.action">
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
<span x-show="log.table_name" class="font-mono text-xs bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded" x-text="log.table_name"></span>
<span x-show="log.record_id" class="ml-1 text-xs" x-text="'#' + log.record_id"></span>
<span x-show="!log.table_name">-</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400 font-mono" x-text="log.ip_address || '-'"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function logManagement() {
return {
isExpanded: true,
toggleSidebar() { this.isExpanded = !this.isExpanded; },
logs: [],
searchQuery: '',
dateFilter: '',
isLoading: false,
init() {
this.fetchLogs();
},
fetchLogs() {
this.isLoading = true;
fetch('api/logs.php')
.then(res => res.json())
.then(data => {
if (data.logs) {
this.logs = data.logs;
}
this.isLoading = false;
})
.catch(err => {
console.error(err);
this.isLoading = false;
});
},
get filteredLogs() {
let result = this.logs;
if (this.dateFilter) {
result = result.filter(log => {
if(!log.created_at) return false;
return log.created_at.startsWith(this.dateFilter);
});
}
if (this.searchQuery !== '') {
const q = this.searchQuery.toLowerCase();
result = result.filter(log =>
(log.user_name && log.user_name.toLowerCase().includes(q)) ||
(log.action && log.action.toLowerCase().includes(q)) ||
(log.ip_address && log.ip_address.includes(q))
);
}
return result;
},
formatDate(dateStr) {
if(!dateStr) return '-';
const d = new Date(dateStr);
return d.toLocaleString('th-TH', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute:'2-digit'
});
},
getActionBadgeClass(action) {
const act = action.toLowerCase();
if (act.includes('login')) return 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/30 dark:text-emerald-400';
if (act.includes('logout')) return 'bg-slate-100 text-slate-800 dark:bg-slate-800 dark:text-slate-300';
if (act.includes('create')) return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
if (act.includes('delete')) return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
if (act.includes('update')) return 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400';
return 'bg-primary-100 text-primary-800 dark:bg-primary-900/30 dark:text-primary-400';
}
}
}
</script>
<?php require_once 'includes/footer.php'; ?>