feat: add activity log admin page and nav links
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
||||
@@ -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">
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user