TKK_E32230487/resources/views/histori.blade.php

505 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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)">&lsaquo; Prev</button>
<div class="page-numbers" id="pageNumbers"></div>
<button class="page-btn" id="btnNext" onclick="changePage(1)">Next &rsaquo;</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()">&times;</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>