368 lines
12 KiB
PHP
368 lines
12 KiB
PHP
@extends('admin.layout')
|
|
|
|
@section('content')
|
|
<link rel="stylesheet" href="{{ asset('css/dashboard-admin.css') }}">
|
|
|
|
<div class="dashboard-modern">
|
|
<section class="dashboard-hero">
|
|
<div class="dashboard-hero-content">
|
|
<div>
|
|
<div class="hero-label">
|
|
<i class="fas fa-chart-pie"></i>
|
|
Dashboard Admin
|
|
</div>
|
|
|
|
<h2 class="hero-title">Overview Statistik Aplikasi</h2>
|
|
|
|
<p class="hero-subtitle">
|
|
Pantau jumlah siswa, rekomendasi yang sudah diproses, rata-rata tingkat keyakinan prediksi,
|
|
serta distribusi jurusan yang paling banyak muncul pada sistem rekomendasi jurusan kuliah SMAN 4 Jember.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="hero-date-card">
|
|
<span>Terakhir diperbarui</span>
|
|
<strong id="last-updated">Memuat...</strong>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="modern-stats-grid">
|
|
<div class="modern-stat-card">
|
|
<div class="stat-card-content">
|
|
<div>
|
|
<p class="stat-label">Total Siswa</p>
|
|
<h3 class="stat-value" id="stat-total-siswa">0</h3>
|
|
<p class="stat-note">Jumlah akun siswa yang terdaftar.</p>
|
|
</div>
|
|
|
|
<div class="modern-stat-icon icon-blue">
|
|
<i class="fas fa-user-graduate"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modern-stat-card">
|
|
<div class="stat-card-content">
|
|
<div>
|
|
<p class="stat-label">Rekomendasi Diproses</p>
|
|
<h3 class="stat-value" id="stat-total-rekomendasi">0</h3>
|
|
<p class="stat-note">Jumlah riwayat rekomendasi yang tersimpan.</p>
|
|
</div>
|
|
|
|
<div class="modern-stat-icon icon-green">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modern-stat-card">
|
|
<div class="stat-card-content">
|
|
<div>
|
|
<p class="stat-label">Rata-rata Keyakinan Prediksi</p>
|
|
<h3 class="stat-value" id="stat-rata-akurasi">0%</h3>
|
|
<p class="stat-note" id="akurasi-status">Menunggu data keyakinan prediksi.</p>
|
|
</div>
|
|
|
|
<div class="accuracy-ring" id="accuracy-ring">
|
|
<div class="accuracy-ring-inner" id="accuracy-ring-text">0%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="dashboard-card dashboard-chart-full">
|
|
<div class="card-header-modern">
|
|
<div>
|
|
<h5 class="card-title-modern">Distribusi Rekomendasi Jurusan</h5>
|
|
<p class="card-subtitle-modern">
|
|
Grafik ini menampilkan jumlah hasil rekomendasi berdasarkan jurusan yang muncul dari riwayat prediksi siswa.
|
|
Detail siswa dan hasil lengkap dapat dilihat pada menu <b>Riwayat Prediksi</b>.
|
|
</p>
|
|
</div>
|
|
|
|
<span class="chart-badge">
|
|
<i class="fas fa-chart-column me-1"></i>
|
|
Grafik Jurusan
|
|
</span>
|
|
</div>
|
|
|
|
<div class="chart-wrapper-modern">
|
|
<canvas id="distribusiChart" style="display: none;"></canvas>
|
|
|
|
<div id="chart-placeholder" class="dashboard-placeholder">
|
|
<div>
|
|
<i class="fas fa-chart-pie"></i>
|
|
<p>Memuat grafik distribusi...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
function getAppBaseUrl() {
|
|
const origin = window.location.origin;
|
|
const segments = window.location.pathname.split('/').filter(Boolean);
|
|
const adminIndex = segments.indexOf('admin');
|
|
|
|
if (adminIndex > 0) {
|
|
return origin + '/' + segments.slice(0, adminIndex).join('/');
|
|
}
|
|
|
|
return origin;
|
|
}
|
|
|
|
const APP_BASE_URL = getAppBaseUrl();
|
|
const API_DASHBOARD_URL = `${APP_BASE_URL}/api/admin/dashboard-stats`;
|
|
const LOGIN_URL = `${APP_BASE_URL}/login`;
|
|
|
|
let distribusiChartInstance = null;
|
|
|
|
function getToken() {
|
|
return localStorage.getItem('access_token') ||
|
|
localStorage.getItem('token') ||
|
|
sessionStorage.getItem('access_token') ||
|
|
sessionStorage.getItem('token') ||
|
|
'';
|
|
}
|
|
|
|
async function parseJsonResponse(response) {
|
|
const text = await response.text();
|
|
|
|
try {
|
|
return text ? JSON.parse(text) : {};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
message: text || response.statusText || 'Respon server tidak valid.'
|
|
};
|
|
}
|
|
}
|
|
|
|
function setText(id, value) {
|
|
const element = document.getElementById(id);
|
|
|
|
if (element) {
|
|
element.innerText = value;
|
|
}
|
|
}
|
|
|
|
function formatNumber(value) {
|
|
const number = Number(value || 0);
|
|
return new Intl.NumberFormat('id-ID').format(number);
|
|
}
|
|
|
|
function formatPercent(value) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return '0%';
|
|
}
|
|
|
|
const number = Number(value);
|
|
|
|
if (Number.isNaN(number)) {
|
|
return '0%';
|
|
}
|
|
|
|
return `${number.toFixed(number % 1 === 0 ? 0 : 2)}%`;
|
|
}
|
|
|
|
function updateAccuracyRing(value) {
|
|
const ring = document.getElementById('accuracy-ring');
|
|
const ringText = document.getElementById('accuracy-ring-text');
|
|
|
|
const number = Number(value || 0);
|
|
const degree = Math.max(0, Math.min(100, number)) * 3.6;
|
|
|
|
if (ring) {
|
|
ring.style.background = `conic-gradient(#2D7A47 ${degree}deg, #e2e8f0 ${degree}deg)`;
|
|
}
|
|
|
|
if (ringText) {
|
|
ringText.innerText = formatPercent(number);
|
|
}
|
|
}
|
|
|
|
function updateLastUpdated() {
|
|
const now = new Date();
|
|
|
|
setText('last-updated', now.toLocaleString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}));
|
|
}
|
|
|
|
function renderDistribusiChart(distribusi) {
|
|
const placeholder = document.getElementById('chart-placeholder');
|
|
const canvas = document.getElementById('distribusiChart');
|
|
|
|
if (!placeholder || !canvas) return;
|
|
|
|
if (!distribusi || distribusi.length === 0) {
|
|
placeholder.style.display = 'flex';
|
|
canvas.style.display = 'none';
|
|
placeholder.querySelector('p').innerText = 'Belum ada data riwayat rekomendasi.';
|
|
return;
|
|
}
|
|
|
|
placeholder.style.display = 'none';
|
|
canvas.style.display = 'block';
|
|
|
|
const labels = distribusi.map(item => item.jurusan || 'Tidak diketahui');
|
|
const counts = distribusi.map(item => Number(item.total || 0));
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
if (distribusiChartInstance) {
|
|
distribusiChartInstance.destroy();
|
|
}
|
|
|
|
const gradient = ctx.createLinearGradient(0, 0, 0, 360);
|
|
gradient.addColorStop(0, 'rgba(45, 122, 71, 0.95)');
|
|
gradient.addColorStop(1, 'rgba(45, 122, 71, 0.55)');
|
|
|
|
distribusiChartInstance = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Jumlah Rekomendasi',
|
|
data: counts,
|
|
backgroundColor: gradient,
|
|
borderColor: 'rgba(45, 122, 71, 1)',
|
|
borderWidth: 1,
|
|
borderRadius: 14,
|
|
borderSkipped: false,
|
|
maxBarThickness: 74
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
layout: {
|
|
padding: {
|
|
top: 12,
|
|
right: 12,
|
|
bottom: 0,
|
|
left: 4
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
backgroundColor: '#0f172a',
|
|
titleColor: '#ffffff',
|
|
bodyColor: '#e2e8f0',
|
|
padding: 12,
|
|
cornerRadius: 12,
|
|
callbacks: {
|
|
label: function(context) {
|
|
return ` ${context.raw} rekomendasi`;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
color: '#64748b',
|
|
font: {
|
|
size: 11,
|
|
weight: '700'
|
|
},
|
|
maxRotation: 0,
|
|
minRotation: 0
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: '#edf2f7'
|
|
},
|
|
ticks: {
|
|
color: '#64748b',
|
|
precision: 0,
|
|
stepSize: 1,
|
|
font: {
|
|
size: 11,
|
|
weight: '700'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadDashboardStats() {
|
|
const token = getToken();
|
|
|
|
if (!token) {
|
|
window.location.href = LOGIN_URL;
|
|
return;
|
|
}
|
|
|
|
updateLastUpdated();
|
|
|
|
try {
|
|
const response = await fetch(API_DASHBOARD_URL, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await parseJsonResponse(response);
|
|
|
|
if (response.status === 401 || response.status === 403) {
|
|
window.location.href = LOGIN_URL;
|
|
return;
|
|
}
|
|
|
|
if (!response.ok || result.success === false) {
|
|
throw new Error(result.message || 'Gagal memuat data dashboard.');
|
|
}
|
|
|
|
const data = result.data || {};
|
|
const rataAkurasi = data.rata_rata_akurasi;
|
|
|
|
setText('stat-total-siswa', formatNumber(data.total_siswa));
|
|
setText('stat-total-rekomendasi', formatNumber(data.rekomendasi_diproses));
|
|
setText('stat-rata-akurasi', formatPercent(rataAkurasi));
|
|
setText('akurasi-status', data.akurasi_status || 'Data keyakinan prediksi berhasil dimuat.');
|
|
|
|
updateAccuracyRing(rataAkurasi || 0);
|
|
renderDistribusiChart(data.distribusi_jurusan || []);
|
|
} catch (error) {
|
|
console.error('Gagal memuat data dashboard:', error);
|
|
|
|
setText('akurasi-status', error.message || 'Gagal memuat data dashboard.');
|
|
updateAccuracyRing(0);
|
|
|
|
const placeholder = document.getElementById('chart-placeholder');
|
|
const canvas = document.getElementById('distribusiChart');
|
|
|
|
if (placeholder) {
|
|
placeholder.style.display = 'flex';
|
|
placeholder.querySelector('p').innerText = 'Gagal memuat grafik distribusi.';
|
|
}
|
|
|
|
if (canvas) {
|
|
canvas.style.display = 'none';
|
|
}
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadDashboardStats);
|
|
</script>
|
|
@endsection |