505 lines
24 KiB
PHP
505 lines
24 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="Access Log - Biometric Door Activity History">
|
||
<title>Histori - Dashboard</title>
|
||
<link rel="stylesheet" href="{{ asset('css/style.css') }}">
|
||
<link rel="stylesheet" href="{{ asset('css/dashboard.css') }}">
|
||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"
|
||
rel="stylesheet">
|
||
<script>if(localStorage.getItem('theme')==='light')document.documentElement.classList.add('light');</script>
|
||
</head>
|
||
|
||
<body>
|
||
<!-- Mobile Top Bar -->
|
||
<div class="mobile-topbar">
|
||
<div class="mobile-topbar-logo">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||
</svg>
|
||
</div>
|
||
<span class="mobile-topbar-title">DASHBOARD</span>
|
||
<span class="mobile-topbar-page">Histori Akses</span>
|
||
<button class="mobile-theme-btn" id="mobileThemeBtn" title="Ganti Tema">
|
||
<svg id="mobileThemeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="5"/>
|
||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Mobile Bottom Navigation -->
|
||
<nav class="mobile-bottom-nav">
|
||
<a href="/dashboard" class="mobile-nav-item">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||
<polyline points="9 22 9 12 15 12 15 22" />
|
||
</svg>
|
||
Dashboard
|
||
</a>
|
||
<a href="/input-data" class="mobile-nav-item">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||
<line x1="9" y1="3" x2="9" y2="21" />
|
||
</svg>
|
||
Input Data
|
||
</a>
|
||
<a href="/histori" class="mobile-nav-item active">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||
<line x1="8" y1="9" x2="16" y2="9" />
|
||
<line x1="8" y1="13" x2="14" y2="13" />
|
||
<line x1="8" y1="17" x2="12" y2="17" />
|
||
</svg>
|
||
Histori
|
||
</a>
|
||
<form method="POST" action="{{ route('logout') }}" style="flex:1;display:contents">
|
||
@csrf
|
||
<button type="submit" class="mobile-nav-item logout-nav">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4" />
|
||
<polyline points="16 17 21 12 16 7" />
|
||
<line x1="21" y1="12" x2="9" y2="12" />
|
||
</svg>
|
||
Logout
|
||
</button>
|
||
</form>
|
||
</nav>
|
||
|
||
<div class="dashboard-layout">
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="sidebar-logo">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
||
</svg>
|
||
</div>
|
||
<span class="sidebar-title">DASHBOARD</span>
|
||
</div>
|
||
|
||
<nav class="sidebar-nav">
|
||
<a href="/dashboard" class="nav-item">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2z" />
|
||
<polyline points="9 22 9 12 15 12 15 22" />
|
||
</svg>
|
||
<span>Dashboard</span>
|
||
</a>
|
||
<a href="/input-data" class="nav-item">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||
<line x1="9" y1="3" x2="9" y2="21" />
|
||
</svg>
|
||
<span>Input Data</span>
|
||
</a>
|
||
<a href="/histori" class="nav-item active">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||
<line x1="8" y1="9" x2="16" y2="9" />
|
||
<line x1="8" y1="13" x2="14" y2="13" />
|
||
<line x1="8" y1="17" x2="12" y2="17" />
|
||
</svg>
|
||
<span>Histori</span>
|
||
</a>
|
||
</nav>
|
||
|
||
<div class="sidebar-footer">
|
||
<button class="theme-toggle-btn" id="themeToggleBtn" title="Ganti Tema">
|
||
<svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="5"/>
|
||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||
<line x1="12" y1="21" x2="12" y2="23"/>
|
||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
||
<line x1="1" y1="12" x2="3" y2="12"/>
|
||
<line x1="21" y1="12" x2="23" y2="12"/>
|
||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||
</svg>
|
||
<span id="themeLabel">Mode Terang</span>
|
||
<div class="theme-pill"></div>
|
||
</button>
|
||
<form method="POST" action="{{ route('logout') }}">
|
||
@csrf
|
||
<button type="submit" class="nav-item logout"
|
||
style="background:none;border:none;width:100%;text-align:left;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<polyline points="12 6 12 12 16 14" />
|
||
</svg>
|
||
<span>Logout</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content -->
|
||
<main class="main-content">
|
||
<div class="content-wrapper">
|
||
<div class="page-header">
|
||
<h1 class="page-title">ACCESS LOG</h1>
|
||
<p class="page-subtitle">Riwayat aktivitas akses pintu biometric</p>
|
||
</div>
|
||
|
||
<!-- Summary Cards -->
|
||
<div class="summary-cards">
|
||
<div class="summary-card summary-total">
|
||
<div class="summary-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" />
|
||
<polyline points="14 2 14 8 20 8" />
|
||
<line x1="16" y1="13" x2="8" y2="13" />
|
||
<line x1="16" y1="17" x2="8" y2="17" />
|
||
<polyline points="10 9 9 9 8 9" />
|
||
</svg>
|
||
</div>
|
||
<div class="summary-info">
|
||
<span class="summary-value" id="summaryTotal">{{ $totalLogs }}</span>
|
||
<span class="summary-label">Total Akses</span>
|
||
</div>
|
||
</div>
|
||
<div class="summary-card summary-success">
|
||
<div class="summary-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
|
||
<polyline points="22 4 12 14.01 9 11.01" />
|
||
</svg>
|
||
</div>
|
||
<div class="summary-info">
|
||
<span class="summary-value" id="summarySuccess">{{ $successLogs }}</span>
|
||
<span class="summary-label">Sukses</span>
|
||
</div>
|
||
</div>
|
||
<div class="summary-card summary-failed">
|
||
<div class="summary-icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<circle cx="12" cy="12" r="10" />
|
||
<line x1="15" y1="9" x2="9" y2="15" />
|
||
<line x1="9" y1="9" x2="15" y2="15" />
|
||
</svg>
|
||
</div>
|
||
<div class="summary-info">
|
||
<span class="summary-value" id="summaryFailed">{{ $failedLogs }}</span>
|
||
<span class="summary-label">Gagal</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Filter Bar -->
|
||
<div class="filter-bar">
|
||
<div class="filter-group">
|
||
<label class="filter-label">Status</label>
|
||
<select class="filter-select" id="filterStatus">
|
||
<option value="all">Semua</option>
|
||
<option value="success">Sukses</option>
|
||
<option value="failed">Gagal</option>
|
||
</select>
|
||
</div>
|
||
<div class="filter-group filter-search-group">
|
||
<label class="filter-label">Cari Nama</label>
|
||
<div class="filter-search-wrapper">
|
||
<svg class="filter-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||
stroke-width="2">
|
||
<circle cx="11" cy="11" r="8" />
|
||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||
</svg>
|
||
<input type="text" class="filter-search" id="searchName" placeholder="Ketik nama user...">
|
||
</div>
|
||
</div>
|
||
<div class="filter-actions">
|
||
<button class="btn-clear-logs" id="clearLogsBtn" title="Hapus semua log">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="15"
|
||
height="15">
|
||
<polyline points="3 6 5 6 21 6" />
|
||
<path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" />
|
||
</svg>
|
||
Hapus Semua
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log Table -->
|
||
<div class="log-card">
|
||
<div class="log-card-header">Log Histori</div>
|
||
<div class="table-scroll-wrapper">
|
||
<table class="log-table" id="logTable">
|
||
<thead>
|
||
<tr>
|
||
<th>No</th>
|
||
<th>Nama User</th>
|
||
<th>Metode</th>
|
||
<th>Status</th>
|
||
<th>Waktu Akses</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="logTableBody">
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="empty-state" id="logEmptyState" style="display:none;">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="48"
|
||
height="48">
|
||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||
<line x1="8" y1="9" x2="16" y2="9" />
|
||
<line x1="8" y1="13" x2="14" y2="13" />
|
||
<line x1="8" y1="17" x2="12" y2="17" />
|
||
</svg>
|
||
<p>Belum ada riwayat akses pintu</p>
|
||
</div>
|
||
|
||
<!-- Pagination -->
|
||
<div class="pagination-bar" id="paginationBar" style="display:none;">
|
||
<span class="pagination-info" id="paginationInfo"></span>
|
||
<div class="pagination-controls">
|
||
<button class="page-btn" id="btnPrev" onclick="changePage(-1)">‹ Prev</button>
|
||
<div class="page-numbers" id="pageNumbers"></div>
|
||
<button class="page-btn" id="btnNext" onclick="changePage(1)">Next ›</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Clear Logs Confirmation Modal -->
|
||
<div class="modal-overlay" id="clearLogsModal" style="display:none;">
|
||
<div class="modal-card modal-card-sm">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title">Hapus Semua Log</h2>
|
||
<button class="modal-close" onclick="closeClearLogsModal()">×</button>
|
||
</div>
|
||
<p style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 24px;">
|
||
Apakah Anda yakin ingin menghapus <strong style="color:var(--text-primary)">semua riwayat akses
|
||
pintu</strong>? Tindakan ini tidak dapat dibatalkan.
|
||
</p>
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn-cancel" onclick="closeClearLogsModal()">Batal</button>
|
||
<button type="button" class="btn-danger" id="confirmClearLogsBtn">HAPUS SEMUA</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="{{ asset('js/dashboard.js') }}" defer></script>
|
||
|
||
<script>
|
||
// ===== THEME TOGGLE =====
|
||
(function() {
|
||
const saved = localStorage.getItem('theme');
|
||
if (saved === 'light') document.documentElement.classList.add('light');
|
||
})();
|
||
|
||
function updateThemeUI() {
|
||
const isLight = document.documentElement.classList.contains('light');
|
||
const moonSvg = `<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>`;
|
||
const sunSvg = `<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>`;
|
||
const icon = document.getElementById('themeIcon');
|
||
const label = document.getElementById('themeLabel');
|
||
const mobileIcon = document.getElementById('mobileThemeIcon');
|
||
if (icon) icon.innerHTML = isLight ? moonSvg : sunSvg;
|
||
if (mobileIcon) mobileIcon.innerHTML = isLight ? moonSvg : sunSvg;
|
||
if (label) label.textContent = isLight ? 'Mode Gelap' : 'Mode Terang';
|
||
}
|
||
|
||
function toggleTheme() {
|
||
const html = document.documentElement;
|
||
html.classList.toggle('light');
|
||
localStorage.setItem('theme', html.classList.contains('light') ? 'light' : 'dark');
|
||
updateThemeUI();
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
updateThemeUI();
|
||
const btn = document.getElementById('themeToggleBtn');
|
||
const mobileBtn = document.getElementById('mobileThemeBtn');
|
||
if (btn) btn.addEventListener('click', toggleTheme);
|
||
if (mobileBtn) mobileBtn.addEventListener('click', toggleTheme);
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
|
||
localStorage.removeItem('accessLogs');
|
||
|
||
const PER_PAGE = 50;
|
||
let currentPage = 1;
|
||
let totalPages = 1;
|
||
let searchTimer = null;
|
||
|
||
// ── Hapus semua log ──────────────────────────────────────────────
|
||
document.getElementById("confirmClearLogsBtn")
|
||
.addEventListener("click", async function () {
|
||
try {
|
||
await fetch("/api/logs", { method: "DELETE" });
|
||
location.reload();
|
||
} catch (error) {
|
||
console.error("Delete error:", error);
|
||
}
|
||
});
|
||
|
||
// ── Render tabel ─────────────────────────────────────────────────
|
||
function renderTable(logs, page, perPage) {
|
||
const table = document.getElementById("logTableBody");
|
||
table.innerHTML = "";
|
||
|
||
if (logs.length === 0) {
|
||
table.innerHTML = `<tr><td colspan="5" style="text-align:center;color:#888;padding:30px;">Tidak ada data yang sesuai filter</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
const offset = (page - 1) * perPage;
|
||
logs.forEach((log, index) => {
|
||
const statusLower = log.status.toLowerCase();
|
||
const isSuccess = ["success", "sukses", "open"].includes(statusLower);
|
||
const statusColor = isSuccess ? "#3fb950" : "#f85149";
|
||
const statusDisplay = isSuccess ? "Sukses" : "Gagal";
|
||
const name = log.user?.name || log.user_profiles?.name || "Unknown";
|
||
const time = new Date(log.access_time ?? log.created_at).toLocaleString("id-ID");
|
||
|
||
table.innerHTML += `
|
||
<tr>
|
||
<td>${offset + index + 1}</td>
|
||
<td>${name}</td>
|
||
<td>${log.method}</td>
|
||
<td style="color:${statusColor}">${statusDisplay}</td>
|
||
<td>${time}</td>
|
||
</tr>`;
|
||
});
|
||
}
|
||
|
||
// ── Render pagination ─────────────────────────────────────────────
|
||
function renderPagination(total, page, perPage, pages) {
|
||
const bar = document.getElementById("paginationBar");
|
||
const info = document.getElementById("paginationInfo");
|
||
const nums = document.getElementById("pageNumbers");
|
||
|
||
if (pages <= 1) { bar.style.display = "none"; return; }
|
||
bar.style.display = "flex";
|
||
|
||
const from = (page - 1) * perPage + 1;
|
||
const to = Math.min(page * perPage, total);
|
||
info.textContent = `Menampilkan ${from}–${to} dari ${total} data`;
|
||
|
||
// Tombol prev/next
|
||
document.getElementById("btnPrev").disabled = page <= 1;
|
||
document.getElementById("btnNext").disabled = page >= pages;
|
||
|
||
// Nomor halaman (tampilkan maks 5 angka)
|
||
nums.innerHTML = "";
|
||
let start = Math.max(1, page - 2);
|
||
let end = Math.min(pages, start + 4);
|
||
if (end - start < 4) start = Math.max(1, end - 4);
|
||
|
||
for (let i = start; i <= end; i++) {
|
||
const btn = document.createElement("button");
|
||
btn.className = "page-btn" + (i === page ? " page-btn-active" : "");
|
||
btn.textContent = i;
|
||
btn.onclick = () => goToPage(i);
|
||
nums.appendChild(btn);
|
||
}
|
||
}
|
||
|
||
// ── Ambil data dari server ────────────────────────────────────────
|
||
async function loadLogs(resetPage = false) {
|
||
if (resetPage) currentPage = 1;
|
||
|
||
const status = document.getElementById("filterStatus").value;
|
||
const name = document.getElementById("searchName").value.trim();
|
||
|
||
const params = new URLSearchParams({
|
||
page: currentPage,
|
||
per_page: PER_PAGE,
|
||
status: status,
|
||
name: name
|
||
});
|
||
|
||
try {
|
||
const [logsRes, summaryRes] = await Promise.all([
|
||
fetch(`/api/logs?${params}`),
|
||
fetch("/api/logs/summary")
|
||
]);
|
||
|
||
// Cek jika response bukan 200
|
||
if (!logsRes.ok) {
|
||
const errText = await logsRes.text();
|
||
console.error("API /logs error:", logsRes.status, errText);
|
||
document.getElementById("logTableBody").innerHTML =
|
||
`<tr><td colspan="5" style="text-align:center;color:#f85149;padding:30px;">
|
||
Error ${logsRes.status}: Gagal memuat data log
|
||
</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
const result = await logsRes.json();
|
||
const summary = await summaryRes.json();
|
||
|
||
// Update summary cards
|
||
document.getElementById("summaryTotal").textContent = summary.total ?? "-";
|
||
document.getElementById("summarySuccess").textContent = summary.success ?? "-";
|
||
document.getElementById("summaryFailed").textContent = summary.failed ?? "-";
|
||
|
||
// Validasi format response
|
||
if (!Array.isArray(result.data)) {
|
||
console.error("Format API tidak terduga:", result);
|
||
document.getElementById("logTableBody").innerHTML =
|
||
`<tr><td colspan="5" style="text-align:center;color:#f85149;padding:30px;">
|
||
Format response tidak valid. Cek console untuk detail.
|
||
</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
totalPages = result.total_pages || 1;
|
||
|
||
renderTable(result.data, result.page, result.per_page);
|
||
renderPagination(result.total, result.page, result.per_page, result.total_pages);
|
||
|
||
} catch (err) {
|
||
console.error("loadLogs exception:", err);
|
||
document.getElementById("logTableBody").innerHTML =
|
||
`<tr><td colspan="5" style="text-align:center;color:#f85149;padding:30px;">
|
||
Koneksi ke server gagal. Periksa jaringan Anda.
|
||
</td></tr>`;
|
||
}
|
||
}
|
||
|
||
// ── Navigasi halaman ──────────────────────────────────────────────
|
||
function changePage(delta) {
|
||
const next = currentPage + delta;
|
||
if (next < 1 || next > totalPages) return;
|
||
currentPage = next;
|
||
loadLogs();
|
||
}
|
||
|
||
function goToPage(page) {
|
||
if (page < 1 || page > totalPages) return;
|
||
currentPage = page;
|
||
loadLogs();
|
||
}
|
||
|
||
// ── Event listener filter (reset ke halaman 1 setiap filter berubah)
|
||
document.getElementById("filterStatus").addEventListener("change", () => loadLogs(true));
|
||
document.getElementById("searchName").addEventListener("input", () => {
|
||
clearTimeout(searchTimer);
|
||
searchTimer = setTimeout(() => loadLogs(true), 400);
|
||
});
|
||
|
||
// ── Auto-refresh setiap 5 detik (tetap di halaman yang sama) ─────
|
||
setInterval(() => loadLogs(), 5000);
|
||
|
||
loadLogs();
|
||
|
||
</script>
|
||
|
||
</body>
|
||
|
||
</html> |