1535c75138
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
178 lines
10 KiB
PHP
178 lines
10 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id" data-theme="light">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
<title>Log Aktivitas - WebGIS</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/daisyui@4/dist/full.min.css" rel="stylesheet">
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&display=swap" rel="stylesheet">
|
|
<style>
|
|
* { font-family: 'Plus Jakarta Sans', sans-serif; }
|
|
body { font-weight: 500; background-color: #f8fafc; }
|
|
h1, h2, h3, .btn { font-weight: 700 !important; letter-spacing: -0.01em; }
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen">
|
|
<div class="navbar bg-base-100 border-b border-base-200 shadow-sm sticky top-0 z-[1000]">
|
|
<div class="flex-1">
|
|
<a href="{{ url('/') }}" class="btn btn-ghost text-lg font-bold gap-2 text-primary">
|
|
<i class="fa-solid fa-house-chimney-crack"></i> WebGIS Pemetaan Kemiskinan
|
|
</a>
|
|
</div>
|
|
<div class="flex-none">
|
|
<ul class="menu menu-horizontal px-1 gap-1">
|
|
<li><a href="{{ url('/map') }}" class="font-medium"><i class="fa-solid fa-map"></i> Peta</a></li>
|
|
<li><a href="{{ url('/data') }}" class="font-medium"><i class="fa-solid fa-table-list"></i> Data</a></li>
|
|
<li><a href="{{ url('/dashboard') }}" class="font-medium"><i class="fa-solid fa-chart-bar"></i> Dashboard</a></li>
|
|
<li><a href="{{ url('/admin/users') }}" class="font-medium"><i class="fa-solid fa-users-gear"></i> Admin</a></li>
|
|
<li><a href="{{ url('/admin/logs') }}" class="font-semibold bg-primary/10 text-primary rounded-lg"><i class="fa-solid fa-clock-rotate-left"></i> Log</a></li>
|
|
</ul>
|
|
<div class="dropdown dropdown-end ml-2">
|
|
<div tabindex="0" role="button" class="btn btn-ghost gap-2">
|
|
<div class="avatar placeholder"><div class="bg-primary text-primary-content rounded-full w-8"><span class="text-xs font-bold">{{ substr(auth()->user()->name, 0, 1) }}</span></div></div>
|
|
<span class="hidden sm:inline text-sm font-semibold">{{ auth()->user()->name }}</span>
|
|
</div>
|
|
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-lg border border-base-200 mt-2">
|
|
<li class="menu-title"><span class="text-xs text-base-content/50 capitalize">{{ str_replace('_', ' ', auth()->user()->getRoleNames()->first() ?? 'user') }}</span></li>
|
|
<li><form method="POST" action="{{ route('logout') }}">@csrf<button type="submit" class="text-error w-full text-left flex items-center gap-2"><i class="fa-solid fa-right-from-bracket"></i> Keluar</button></form></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main class="max-w-screen-xl mx-auto px-4 py-8">
|
|
<div class="mb-8">
|
|
<h1 class="text-3xl font-bold flex items-center gap-3"><i class="fa-solid fa-clock-rotate-left text-primary"></i> Log Aktivitas</h1>
|
|
<p class="text-base-content/50 mt-1">200 aktivitas terakhir pengguna sistem.</p>
|
|
</div>
|
|
|
|
<!-- Stats cards -->
|
|
<div class="mt-2 grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-white rounded-xl border border-base-200 p-4 shadow-sm">
|
|
<p class="text-xs text-base-content/50 uppercase font-bold">Login Hari Ini</p>
|
|
<p id="statLoginsToday" class="text-3xl font-bold text-primary mt-1">-</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-base-200 p-4 shadow-sm">
|
|
<p class="text-xs text-base-content/50 uppercase font-bold">Login Minggu Ini</p>
|
|
<p id="statLoginsWeek" class="text-3xl font-bold text-secondary mt-1">-</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-base-200 p-4 shadow-sm">
|
|
<p class="text-xs text-base-content/50 uppercase font-bold">Aksi Hari Ini</p>
|
|
<p id="statActionsToday" class="text-3xl font-bold text-accent mt-1">-</p>
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-base-200 p-4 shadow-sm">
|
|
<p class="text-xs text-base-content/50 uppercase font-bold">Login Bulan Ini</p>
|
|
<p id="statLoginsMonth" class="text-3xl font-bold text-info mt-1">-</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity chart + top users -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
<div class="md:col-span-2 bg-white rounded-xl border border-base-200 p-4 shadow-sm">
|
|
<p class="text-xs font-bold uppercase opacity-50 mb-3">Aktivitas 14 Hari Terakhir</p>
|
|
<canvas id="chartDaily" height="80"></canvas>
|
|
</div>
|
|
<div class="bg-white rounded-xl border border-base-200 p-4 shadow-sm overflow-x-auto">
|
|
<p class="text-xs font-bold uppercase opacity-50 mb-3">Top Pengguna</p>
|
|
<table class="table table-xs w-full">
|
|
<thead><tr><th>Pengguna</th><th>Aksi</th></tr></thead>
|
|
<tbody id="bodyTopUsers"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white rounded-2xl shadow-sm border border-base-200 overflow-x-auto">
|
|
<table class="table table-zebra table-sm w-full">
|
|
<thead>
|
|
<tr class="text-xs font-bold uppercase opacity-50">
|
|
<th>Waktu</th><th>Pengguna</th><th>Aksi</th><th>Deskripsi</th><th>IP</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="bodyLogs"></tbody>
|
|
</table>
|
|
</div>
|
|
</main>
|
|
|
|
<div class="toast toast-end toast-bottom z-[9999]" id="toastBox"></div>
|
|
|
|
<script>
|
|
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
function esc(s) { return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); }
|
|
|
|
const actionBadge = {
|
|
create: 'badge-success', update: 'badge-warning', delete: 'badge-error',
|
|
bulk_delete: 'badge-error', import: 'badge-info', backup: 'badge-neutral',
|
|
login: 'badge-ghost', logout: 'badge-ghost',
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { loadStats(); loadLogs(); });
|
|
|
|
let chartDaily = null;
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const res = await fetch('{{ url('/api/admin/stats') }}', { headers: { 'X-CSRF-TOKEN': csrfToken } });
|
|
const s = await res.json();
|
|
|
|
document.getElementById('statLoginsToday').textContent = s.logins_today;
|
|
document.getElementById('statLoginsWeek').textContent = s.logins_week;
|
|
document.getElementById('statLoginsMonth').textContent = s.logins_month;
|
|
document.getElementById('statActionsToday').textContent = s.actions_today;
|
|
|
|
document.getElementById('bodyTopUsers').innerHTML = s.top_users.length
|
|
? s.top_users.map(u => `<tr><td class="font-semibold text-sm">${esc(u.user_name)}</td><td>${esc(String(u.total))}</td></tr>`).join('')
|
|
: '<tr><td colspan="2" class="text-center opacity-30 italic py-4">Belum ada data</td></tr>';
|
|
|
|
const labels = Object.keys(s.daily);
|
|
const values = Object.values(s.daily);
|
|
const ctx = document.getElementById('chartDaily').getContext('2d');
|
|
if (chartDaily) chartDaily.destroy();
|
|
chartDaily = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels,
|
|
datasets: [{ label: 'Aktivitas', data: values, backgroundColor: '#6366f1', borderRadius: 4 }],
|
|
},
|
|
options: {
|
|
plugins: { legend: { display: false } },
|
|
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } },
|
|
},
|
|
});
|
|
} catch (e) {
|
|
console.error('Gagal memuat statistik', e);
|
|
}
|
|
}
|
|
|
|
async function loadLogs() {
|
|
try {
|
|
const res = await fetch('{{ url('/api/admin/logs') }}', { headers: { 'X-CSRF-TOKEN': csrfToken } });
|
|
const logs = await res.json();
|
|
document.getElementById('bodyLogs').innerHTML = logs.length ? logs.map(l => `
|
|
<tr>
|
|
<td class="text-xs whitespace-nowrap opacity-70">${esc(l.created_at || '-')}</td>
|
|
<td class="font-semibold text-sm">${esc(l.user)}</td>
|
|
<td><span class="badge badge-sm ${actionBadge[l.action] || 'badge-ghost'} badge-outline">${esc(l.action)}</span></td>
|
|
<td class="text-sm">${esc(l.description)}</td>
|
|
<td class="font-mono text-xs opacity-50">${esc(l.ip || '-')}</td>
|
|
</tr>`).join('') : '<tr><td colspan="5" class="text-center py-10 opacity-30 italic">Belum ada aktivitas</td></tr>';
|
|
} catch (e) {
|
|
showToast('Gagal memuat log', 'error');
|
|
}
|
|
}
|
|
|
|
function showToast(msg, type = 'info') {
|
|
const t = document.createElement('div');
|
|
const cls = type === 'success' ? 'alert-success' : type === 'error' ? 'alert-error' : 'alert-info';
|
|
t.className = `alert ${cls} shadow-lg py-3 text-xs font-bold`;
|
|
t.innerHTML = `<span><i class="fa-solid fa-circle-info mr-2"></i>${esc(msg)}</span>`;
|
|
document.getElementById('toastBox').appendChild(t);
|
|
setTimeout(() => t.remove(), 3000);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|