TKK_E32230908/resources/views/admin/dashboard.blade.php

544 lines
19 KiB
PHP
Raw Permalink 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">
<title>Dashboard - Pustaka Nawasena</title>
<link rel="icon" href="{{ asset('images/pustaka.jpeg') }}" type="image/png">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
:root {
--primary-color: #0d47a1;
--secondary-color: #ffb300;
--iot-online: #2ecc71;
--glass-bg: rgba(255, 255, 255, 0.85);
}
/* --- LOGIKA MENGHILANGKAN SCROLLBAR --- */
body::-webkit-scrollbar {
display: none;
}
body {
font-family: 'Inter', sans-serif;
background: linear-gradient(rgba(0, 0, 0, 0.3), rgba(0, 0, 0, 0.3)),
url('{{ asset("images/utama.jpg") }}') no-repeat center center fixed;
background-size: cover;
min-height: 100vh;
backdrop-filter: blur(5px);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.glass {
background: var(--glass-bg);
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.4);
padding: 25px;
transition: all 0.3s ease;
}
.glass:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.25);
}
.navbar-custom {
background: rgba(13, 71, 161, 0.95);
backdrop-filter: blur(10px);
border-bottom: 4px solid var(--secondary-color);
padding: 12px 0;
}
.datetime-widget {
background: rgba(0, 0, 0, 0.4);
padding: 8px 20px;
border-radius: 50px;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
align-items: center;
gap: 15px;
font-size: 0.85rem;
}
.time-text { font-weight: 800; color: var(--secondary-color); font-family: monospace; font-size: 1.1rem; }
.date-text { font-weight: 400; border-left: 1px solid rgba(255,255,255,0.3); padding-left: 15px; }
.text-shadow-strong {
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.iot-status-pill {
display: inline-flex;
align-items: center;
gap: 10px;
background: white;
padding: 8px 18px;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 800;
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
letter-spacing: 0.5px;
}
.dot-pulse {
width: 12px; height: 12px;
background-color: var(--iot-online);
border-radius: 50%;
animation: pulse-green 2s infinite;
}
@keyframes pulse-green {
0% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(46, 204, 113, 0); }
100% { box-shadow: 0 0 0 0 rgba(46, 204, 113, 0); }
}
.stat-box {
border-left: 6px solid var(--primary-color);
padding: 20px;
border-radius: 15px;
background-color: white;
height: 100%;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
/* --- STYLE BADGE NOTIFIKASI --- */
.badge-floating {
position: absolute;
top: -10px;
right: -10px;
background-color: #dc3545;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 800;
border: 2px solid white;
z-index: 99;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.logo-circle {
width: 45px; height: 45px;
background-color: white;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
overflow: hidden; margin-right: 12px;
}
.logo-circle img { width: 30px; height: 30px; object-fit: contain; }
.menu-link:hover .glass {
background: white;
border-color: var(--primary-color);
}
/* Judul Utama Kotak Feedback */
.title-feedback {
color: #ffffff; /* Biru Navy khas profesional */
font-weight: 800; /* Sangat Tebal */
text-transform: uppercase;
letter-spacing: 1px;
font-size: 1.1rem;
}
.avatar-initial {
width: 35px;
height: 35px;
background: linear-gradient(135deg, #004d91, #007bff);
color: white;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
font-weight: 800;
font-size: 0.85rem;
margin-right: 12px;
}
.book-title-small {
font-size: 0.85rem;
color: #004d91; /* Judul buku di tabel juga biru */
font-weight: 700;
}
.comment-text {
font-size: 0.8rem;
color: #000000; /* Teks ulasan hitam pekat agar mudah dibaca */
font-style: italic;
max-width: 250px;
line-height: 1.4;
font-weight: 500;
}
.rating-badge {
background: #fffcf0;
color: #b45309;
padding: 4px 10px;
border-radius: 8px;
font-weight: 700;
font-size: 0.8rem;
display: inline-flex;
align-items: center;
gap: 5px;
border: 1px solid #fef3c7;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-custom sticky-top shadow-lg">
<!-- Mengubah container menjadi container-fluid agar mepet kanan-kiri, ditambah padding samping px-4 atau px-lg-5 -->
<div class="container-fluid px-3 px-lg-5">
<!-- SISI KIRI: Brand / Logo -->
<a class="navbar-brand fw-bold text-white d-flex align-items-center" href="{{ route('admin.dashboard') }}">
<div class="logo-circle">
<img src="{{ asset('images/kartu.png') }}" alt="Logo">
</div>
<div style="line-height: 1.1;">
<span class="fs-5 d-block">PUSTAKA NAWASENA</span>
<span class="text-light opacity-75" style="font-size: 0.65rem; font-weight: 300;">Bakorwil III Malang</span>
</div>
</a>
<button class="navbar-toggler border-0 text-white" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<i class="bi bi-grid-3x3-gap-fill"></i>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<!-- SISI TENGAH/KANAN: Menggunakan ms-auto & w-100 untuk membagi ruang -->
<div class="d-flex w-100 flex-column flex-lg-row align-items-center justify-content-between mt-3 mt-lg-0">
<!-- Widget Waktu didorong ke kanan menggunakan ms-lg-auto di layar lebar -->
<div class="d-none d-lg-flex ms-lg-auto me-4">
<div class="datetime-widget">
<i class="bi bi-calendar-check-fill text-warning"></i>
<span id="realtime-date" class="date-text">Memuat Tanggal...</span>
<span id="realtime-clock" class="time-text">00:00:00</span>
</div>
</div>
<!-- SISI PALING KANAN: Menu Navigasi & Tombol Keluar -->
<ul class="navbar-nav align-items-center">
<li class="nav-item">
<a class="nav-link text-white" href="{{ route('profile') }}">
<i class="bi bi-person-circle me-1"></i> Profil
</a>
</ul>
</div>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">@csrf</form>
</div>
</div>
</nav>
<main class="py-5">
<div class="container container-xl">
<div class="row align-items-center mb-5">
<div class="col-lg-8 text-center text-lg-start">
<h2 class="fw-800 text-white text-shadow-strong mb-1">Panel Dashboard Utama</h2>
<p class="text-light text-shadow-strong opacity-90">Manajemen Perpustakaan Nawasena Bakorwil III Malang.</p>
</div>
<div class="col-lg-4 text-center text-lg-end">
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="stat-box d-flex align-items-center justify-content-between" style="border-left-color: #6f42c1;">
<div>
<h6 class="text-secondary fw-bold small text-uppercase mb-1">Koleksi Terfavorit</h6>
<h4 class="fw-bold text-dark">{{ $mostBorrowedBook->koleksi->title ?? 'Belum ada data' }}</h4>
</div>
<div class="text-end">
<span class="badge bg-primary rounded-pill px-3">Terpinjam {{ $mostBorrowedBook->total ?? 0 }}x</span>
</div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-box shadow-sm">
<h6 class="text-muted small fw-bold">TOTAL BUKU</h6>
<h3 class="fw-bold text-primary mb-0">{{ number_format($totalBuku ?? 0) }}</h3>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-box shadow-sm" style="border-left-color: #17a2b8;">
<h6 class="text-muted small fw-bold">ANGGOTA</h6>
<h3 class="fw-bold text-info mb-0">{{ number_format($totalAnggota ?? 0) }}</h3>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-box shadow-sm" style="border-left-color: #ffc107;">
<h6 class="text-muted small fw-bold">DIPINJAM</h6>
<h3 class="fw-bold text-warning mb-0">{{ number_format($totalPeminjaman ?? 0) }}</h3>
</div>
</div>
<div class="col-md-3 col-6">
<div class="stat-box shadow-sm" style="border-left-color: #dc3545;">
<h6 class="text-muted small fw-bold">KEMBALI</h6>
<h3 class="fw-bold text-danger mb-0">{{ number_format($totalPengembalian ?? 0) }}</h3>
</div>
</div>
</div>
<div class="glass mb-5">
<h5 class="fw-bold mb-4 text-primary"><i class="bi bi-graph-up-arrow me-2"></i> Analisis Minat Baca</h5>
<div style="height: 300px;"><canvas id="chartMinat"></canvas></div>
</div>
<div class="feedback-card mb-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h5 class="fw-bold text-white mb-0">
<i class="bi bi-chat-left-heart-fill text-primary me-2"></i>
Ulasan Anggota
</h5>
<div class="text-white">
Feedback dan penilaian dari anggota terhadap layanan perpustakaan
</div>
</div>
<span class="badge bg-light text-primary rounded-pill px-3 py-2 border shadow-sm small"></span>
</div>
<div class="table-responsive">
<table class="table table-hover table-feedback align-middle border-0">
<thead>
<tr>
<th class="text-center" width="50">No</th>
<th>Anggota</th>
<th>Koleksi Buku</th>
<th class="text-center">Rating</th>
<th>Ulasan</th>
<th class="text-end">Tanggal</th>
</tr>
</thead>
<tbody id="papan-ulasan">
@forelse($ratings ?? [] as $index => $rating)
<tr>
<td class="text-center text-muted small fw-bold">{{ $index + 1 }}</td>
<td>
<div class="d-flex align-items-center">
<div class="avatar-initial shadow-sm">
{{ strtoupper(substr($rating->user->name ?? 'A', 0, 1)) }}
</div>
<div>
<div class="fw-bold text-dark small">{{ $rating->user->name ?? 'Anonim' }}</div>
<div class="text-muted" style="font-size: 0.7rem;">Member Pustaka</div>
</div>
</div>
</td>
<td>
<div class="book-title-small" title="{{ $rating->transaction->koleksi->title ?? '-' }}">
<i class="bi bi-book me-1 text-muted"></i> {{ Str::limit($rating->transaction->koleksi->title ?? '-', 30) }}
</div>
</td>
<td class="text-center">
<div class="rating-badge border border-warning-subtle">
<i class="bi bi-star-fill text-warning"></i> {{ $rating->rating }}.0
</div>
</td>
<td>
{{-- Pastikan nama kolom 'comment' atau 'feedback' sesuai DB --}}
<div class="comment-text">
"{{ $rating->comment ?? ($rating->feedback ?? 'Tidak ada ulasan') }}"
</div>
</td>
<td class="text-end">
<div class="text-muted small fw-600">
{{ $rating->created_at ? $rating->created_at->format('d/m/Y') : '-' }}
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-5 text-muted small">
<i class="bi bi-chat-dots d-block fs-2 opacity-25"></i>
Belum ada feedback dari anggota.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
<div class="text-center mb-4">
<h3 class="fw-bold fs-4 text-white text-shadow-strong">
<i class="bi bi-grid-fill text-warning me-2"></i> Navigasi Akses Cepat
</h3>
<p class="text-light text-shadow-strong small">Kelola data dengan satu klik</p>
</div>
<div class="row g-3 g-lg-4 text-center justify-content-center">
@livewire('admin.notification-badges')
</div>
</div>
</main>
<footer class="py-4 text-center text-white-50" style="background: rgba(0,0,0,0.9); border-top: 2px solid var(--secondary-color);">
<div class="container">
<small>&copy; {{ date('Y') }} Pustaka Nawasena - Bakorwil III Malang</small>
</div>
</footer>
<script>
// =========================
// 1. CLOCK (AMAN - ringan)
// =========================
function updateClock() {
const now = new Date();
const options = {
weekday: 'long',
day: 'numeric',
month: 'long',
year: 'numeric'
};
const dateEl = document.getElementById('realtime-date');
const clockEl = document.getElementById('realtime-clock');
if (dateEl) {
dateEl.textContent = now.toLocaleDateString('id-ID', options);
}
if (clockEl) {
clockEl.textContent = now.toLocaleTimeString('id-ID', { hour12: false });
}
}
updateClock();
setInterval(updateClock, 1000);
// =========================
// 2. CHART INIT
// =========================
const ctx = document.getElementById('chartMinat');
let myChart = null;
if (ctx) {
myChart = new Chart(ctx.getContext('2d'), {
type: 'line',
data: {
labels: @json($bulanLabel ?? []),
datasets: [{
label: 'Frekuensi Peminjaman',
data: @json($chartDataFull ?? []),
borderColor: '#0d47a1',
backgroundColor: 'rgba(13, 71, 161, 0.1)',
tension: 0.4,
fill: true,
pointRadius: 3,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
y: {
beginAtZero: true,
grid: { color: 'rgba(0,0,0,0.05)' }
},
x: {
grid: { display: false }
}
}
}
});
}
// =========================
// 3. FETCH API (FIX BESAR)
// =========================
let isFetching = false; // ❗ anti spam request
async function fetchDashboardData() {
if (isFetching) return;
isFetching = true;
try {
const response = await fetch("{{ route('admin.dashboard.api') }}", {
method: "GET",
headers: {
"X-Requested-With": "XMLHttpRequest",
"Accept": "application/json"
}
});
if (!response.ok) {
console.warn("API error:", response.status);
return;
}
const data = await response.json();
// =========================
// 4. UPDATE UI (SAFE CHECK)
// =========================
const setText = (id, value) => {
const el = document.getElementById(id);
if (el) el.textContent = value ?? 0;
};
setText('stat-judul', data.jumlahJudul);
setText('stat-peminjaman', data.totalPeminjaman);
setText('stat-pengembalian', data.totalPengembalian);
setText('stat-pending-loans', data.pendingLoans);
// =========================
// 5. UPDATE CHART (SAFE)
// =========================
if (myChart && data.chartDataFull) {
myChart.data.datasets[0].data = data.chartDataFull;
myChart.update('none'); // no animation → lebih ringan
}
} catch (error) {
console.error("Fetch error:", error);
} finally {
isFetching = false;
}
}
// =========================
// 6. AUTO REFRESH (DIHENINGKAN)
// =========================
// ❌ JANGAN 5 DETIK (ini biang lemot)
// setInterval(fetchDashboardData, 5000);
// ✅ GANTI 3060 DETIK (AMAN SERVER)
setInterval(fetchDashboardData, 30000);
// optional: load awal
fetchDashboardData();
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>