feat: add activity log admin page and nav links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
GuavaPopper
2026-06-04 14:28:00 +07:00
parent c4eb884495
commit 27eca4f041
7 changed files with 150 additions and 0 deletions
@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Models\ActivityLog;
class ActivityLogController extends Controller
{
public function index()
{
$logs = ActivityLog::latest()->limit(200)->get()->map(fn($l) => [
'id' => $l->id,
'user' => $l->user_name ?? 'Sistem',
'action' => $l->action,
'description' => $l->description,
'ip' => $l->ip_address,
'created_at' => $l->created_at?->format('d/m/Y H:i:s'),
]);
return response()->json($logs);
}
}
+105
View File
@@ -0,0 +1,105 @@
<!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>
<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>
<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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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', loadLogs);
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>
+5
View File
@@ -49,6 +49,11 @@
<i class="fa-solid fa-users-gear"></i> Admin
</a>
</li>
<li>
<a href="{{ url('/admin/logs') }}" class="font-medium">
<i class="fa-solid fa-clock-rotate-left"></i> Log
</a>
</li>
</ul>
<!-- User dropdown -->
<div class="dropdown dropdown-end ml-2">
+5
View File
@@ -52,6 +52,11 @@
<i class="fa-solid fa-users-gear"></i> Admin
</a>
</li>
<li>
<a href="{{ url('/admin/logs') }}" class="font-medium">
<i class="fa-solid fa-clock-rotate-left"></i> Log
</a>
</li>
@endif
</ul>
<!-- User dropdown -->
+5
View File
@@ -47,6 +47,11 @@
<i class="fa-solid fa-users-gear"></i> Admin
</a>
</li>
<li>
<a href="{{ url('/admin/logs') }}" class="font-medium">
<i class="fa-solid fa-clock-rotate-left"></i> Log
</a>
</li>
@endif
</ul>
<!-- User dropdown -->
+5
View File
@@ -99,6 +99,11 @@
<i class="fa-solid fa-users-gear"></i> Admin
</a>
</li>
<li>
<a href="{{ url('/admin/logs') }}" class="font-medium">
<i class="fa-solid fa-clock-rotate-left"></i> Log
</a>
</li>
@endif
</ul>
<!-- User dropdown -->
+4
View File
@@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\ActivityLogController;
use App\Http\Controllers\AdminUserController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\BackupController;
@@ -52,6 +53,7 @@ Route::middleware('auth')->group(function () {
// Admin routes — hanya administrator
Route::middleware('role:administrator')->group(function () {
Route::get('/admin/users', fn() => view('admin.users'))->name('admin.users');
Route::get('/admin/logs', fn() => view('admin.logs'))->name('admin.logs');
Route::prefix('api/admin')->group(function () {
Route::get('/users', [AdminUserController::class, 'index']);
@@ -67,6 +69,8 @@ Route::middleware('auth')->group(function () {
Route::post('/import/miskin', [ImportController::class, 'miskin']);
Route::get('/backup', [BackupController::class, 'download'])->name('admin.backup');
Route::get('/logs', [ActivityLogController::class, 'index']);
});
});
});