544 lines
19 KiB
PHP
544 lines
19 KiB
PHP
<!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>© {{ 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 30–60 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>
|
||
|