457 lines
16 KiB
PHP
457 lines
16 KiB
PHP
@extends('admin.layout')
|
|
|
|
@section('content')
|
|
<link rel="stylesheet" href="{{ asset('css/fitur-admin.css') }}">
|
|
<link rel="stylesheet" href="{{ asset('css/riwayat-admin.css') }}">
|
|
|
|
<div class="dashboard-section">
|
|
<div class="riwayat-header">
|
|
<div>
|
|
<div class="riwayat-label">
|
|
<i class="fas fa-history"></i>
|
|
Riwayat Prediksi
|
|
</div>
|
|
|
|
<h4 class="master-page-title">Riwayat Prediksi Jurusan</h4>
|
|
|
|
<p class="master-page-subtitle">
|
|
Halaman ini menampilkan seluruh hasil rekomendasi siswa beserta informasi pendukung seperti waktu prediksi,
|
|
hasil rekomendasi, tingkat keyakinan, minat utama, dan rata-rata nilai.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container-modern shadow-sm">
|
|
<div class="table-toolbar">
|
|
<div class="search-wrapper-modern">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" id="searchInput" class="search-input-modern" placeholder="Cari nama siswa, email, atau hasil rekomendasi...">
|
|
</div>
|
|
|
|
<div class="per-page-wrapper">
|
|
<span>Tampilkan</span>
|
|
<select id="perPageSelect" class="per-page-select">
|
|
<option value="7" selected>7 data</option>
|
|
<option value="5">5 data</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-modern mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-center" width="70">No</th>
|
|
<th width="170">Waktu</th>
|
|
<th>Data Siswa</th>
|
|
<th>Hasil Rekomendasi</th>
|
|
<th>Ringkasan Input</th>
|
|
<th class="text-center" width="140">Keyakinan</th>
|
|
<th class="text-center" width="130">Aksi</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody id="table-riwayat">
|
|
<tr>
|
|
<td colspan="7" class="loading-state-modern">
|
|
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pagination-container">
|
|
<div class="text-muted small" id="pagination-info">
|
|
Menampilkan 0 sampai 0 dari 0 data
|
|
</div>
|
|
|
|
<div class="d-flex gap-1 flex-wrap" id="pagination-controls"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="detailModal" tabindex="-1" aria-labelledby="detailModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered modal-lg">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header border-bottom-0 px-4 pt-4">
|
|
<div>
|
|
<h5 class="modal-title fw-bold" id="detailModalLabel">Detail Prediksi Jurusan</h5>
|
|
<p class="text-muted small mb-0">Informasi ringkas dari hasil rekomendasi siswa.</p>
|
|
</div>
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<div class="modal-body px-4 pb-4" id="modal-body-content"></div>
|
|
|
|
<div class="modal-footer border-top-0 px-4 pb-4 pt-0">
|
|
<button type="button" class="btn btn-secondary px-4 rounded-pill shadow-none" data-bs-dismiss="modal">
|
|
Tutup
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@push('scripts')
|
|
<script>
|
|
let currentPage = 1;
|
|
let currentSearch = '';
|
|
let perPage = 7;
|
|
let searchTimer = null;
|
|
|
|
function escapeHtml(value) {
|
|
if (value === null || value === undefined || value === '') return '-';
|
|
|
|
return String(value)
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function formatPercent(value) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return '-';
|
|
}
|
|
|
|
const number = Number(value);
|
|
|
|
if (Number.isNaN(number)) {
|
|
return '-';
|
|
}
|
|
|
|
return `${number.toFixed(number % 1 === 0 ? 0 : 2)}%`;
|
|
}
|
|
|
|
function formatRupiah(value) {
|
|
const number = Number(value || 0);
|
|
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
maximumFractionDigits: 0
|
|
}).format(number);
|
|
}
|
|
|
|
function getInitial(name) {
|
|
const cleanName = name || 'S';
|
|
return cleanName.charAt(0).toUpperCase();
|
|
}
|
|
|
|
function showLoading() {
|
|
document.getElementById('table-riwayat').innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="loading-state-modern">
|
|
<i class="fas fa-spinner fa-spin me-2"></i> Memuat data riwayat...
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
|
|
async function fetchRiwayat(page = 1) {
|
|
const token = localStorage.getItem('access_token');
|
|
const tbody = document.getElementById('table-riwayat');
|
|
|
|
if (!token) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
currentPage = page;
|
|
showLoading();
|
|
|
|
const params = new URLSearchParams({
|
|
page: page,
|
|
per_page: perPage
|
|
});
|
|
|
|
if (currentSearch !== '') {
|
|
params.append('search', currentSearch);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/riwayat?${params.toString()}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok || result.success === false) {
|
|
throw new Error(result.message || 'Gagal memuat data riwayat.');
|
|
}
|
|
|
|
const data = result.data || [];
|
|
tbody.innerHTML = '';
|
|
|
|
if (data.length === 0) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="empty-state-modern">
|
|
<i class="fas fa-folder-open"></i>
|
|
Tidak ada riwayat prediksi yang ditemukan.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
|
|
updatePaginationInfo(null);
|
|
renderPagination({ current_page: 1, last_page: 1, total: 0 });
|
|
return;
|
|
}
|
|
|
|
data.forEach((item, index) => {
|
|
const confidence = formatPercent(item.akurasi_prediksi);
|
|
|
|
tbody.innerHTML += `
|
|
<tr>
|
|
<td class="text-center text-muted fw-bold">${(result.from || 1) + index}</td>
|
|
|
|
<td>
|
|
<div class="riwayat-date-main">${escapeHtml(item.tanggal)}</div>
|
|
<div class="riwayat-date-sub">Pukul ${escapeHtml(item.jam)}</div>
|
|
</td>
|
|
|
|
<td>
|
|
<div class="riwayat-user-cell">
|
|
<div class="riwayat-avatar">${escapeHtml(getInitial(item.nama_siswa))}</div>
|
|
<div>
|
|
<div class="riwayat-user-name">${escapeHtml(item.nama_siswa)}</div>
|
|
<div class="riwayat-user-email">${escapeHtml(item.email_siswa)}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
|
|
<td>
|
|
<span class="badge-soft badge-result">${escapeHtml(item.hasil)}</span>
|
|
</td>
|
|
|
|
<td>
|
|
<div class="riwayat-input-summary">
|
|
<span>Minat: ${escapeHtml(item.minat_utama)}</span>
|
|
<span>Rata-rata nilai: ${escapeHtml(item.rata_nilai)}</span>
|
|
</div>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<span class="confidence-badge">
|
|
<i class="fas fa-chart-line"></i>
|
|
${escapeHtml(confidence)}
|
|
</span>
|
|
</td>
|
|
|
|
<td class="text-center">
|
|
<button class="btn-detail" onclick="showDetail(${item.id})">
|
|
<i class="fas fa-file-alt me-1"></i> Detail
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
|
|
updatePaginationInfo(result);
|
|
renderPagination(result);
|
|
|
|
} catch (error) {
|
|
tbody.innerHTML = `
|
|
<tr>
|
|
<td colspan="7" class="empty-state-modern text-danger">
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
Gagal memuat data riwayat.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function updatePaginationInfo(data) {
|
|
const info = document.getElementById('pagination-info');
|
|
|
|
if (!data || !data.total) {
|
|
info.innerText = 'Menampilkan 0 sampai 0 dari 0 data';
|
|
return;
|
|
}
|
|
|
|
info.innerText = `Menampilkan ${data.from || 0} sampai ${data.to || 0} dari ${data.total || 0} data`;
|
|
}
|
|
|
|
function renderPagination(data) {
|
|
const controls = document.getElementById('pagination-controls');
|
|
controls.innerHTML = '';
|
|
|
|
const current = Number(data.current_page || 1);
|
|
const last = Number(data.last_page || 1);
|
|
const total = Number(data.total || 0);
|
|
|
|
if (total === 0) return;
|
|
|
|
const createButton = (label, page, disabled = false, active = false) => {
|
|
const btn = document.createElement('button');
|
|
btn.className = `page-link-custom ${active ? 'active' : ''}`;
|
|
btn.innerHTML = label;
|
|
btn.disabled = disabled;
|
|
|
|
btn.onclick = () => {
|
|
if (!disabled && !active) fetchRiwayat(page);
|
|
};
|
|
|
|
return btn;
|
|
};
|
|
|
|
controls.appendChild(createButton('<i class="fas fa-chevron-left"></i>', current - 1, current === 1));
|
|
|
|
let pages = [];
|
|
|
|
if (last <= 5) {
|
|
for (let i = 1; i <= last; i++) pages.push(i);
|
|
} else {
|
|
pages.push(1);
|
|
|
|
if (current > 3) pages.push('...');
|
|
|
|
const start = Math.max(2, current - 1);
|
|
const end = Math.min(last - 1, current + 1);
|
|
|
|
for (let i = start; i <= end; i++) pages.push(i);
|
|
|
|
if (current < last - 2) pages.push('...');
|
|
|
|
pages.push(last);
|
|
}
|
|
|
|
pages.forEach(page => {
|
|
if (page === '...') {
|
|
controls.appendChild(createButton('...', current, true));
|
|
} else {
|
|
controls.appendChild(createButton(page, page, false, page === current));
|
|
}
|
|
});
|
|
|
|
controls.appendChild(createButton('<i class="fas fa-chevron-right"></i>', current + 1, current === last));
|
|
}
|
|
|
|
function renderObjectGrid(data, className) {
|
|
if (!data) return '';
|
|
|
|
return Object.entries(data).map(([label, value]) => {
|
|
return `
|
|
<div class="${className}">
|
|
<span>${escapeHtml(label)}</span>
|
|
<strong>${escapeHtml(value)}</strong>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function showDetail(id) {
|
|
const token = localStorage.getItem('access_token');
|
|
const modalBody = document.getElementById('modal-body-content');
|
|
|
|
const detailModal = new bootstrap.Modal(document.getElementById('detailModal'));
|
|
detailModal.show();
|
|
|
|
modalBody.innerHTML = `
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-spinner fa-spin text-success fs-3 mb-2"></i>
|
|
<p class="text-muted m-0">Mengambil detail...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/riwayat/${id}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Accept': 'application/json'
|
|
}
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const data = result.data;
|
|
|
|
modalBody.innerHTML = `
|
|
<div class="detail-result-card">
|
|
<small>Hasil Rekomendasi Random Forest</small>
|
|
<h4>${escapeHtml(data.hasil_rekomendasi)}</h4>
|
|
</div>
|
|
|
|
<div class="detail-grid">
|
|
<div class="detail-info-card">
|
|
<span>Nama Siswa</span>
|
|
<strong>${escapeHtml(data.nama_siswa)}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Email Siswa</span>
|
|
<strong>${escapeHtml(data.email_siswa)}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Waktu Prediksi</span>
|
|
<strong>${escapeHtml(data.tanggal)}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Tingkat Keyakinan</span>
|
|
<strong>${escapeHtml(formatPercent(data.akurasi_prediksi))}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Minat Utama</span>
|
|
<strong>${escapeHtml(data.minat_utama)}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Rata-rata Nilai</span>
|
|
<strong>${escapeHtml(data.rata_nilai)}</strong>
|
|
</div>
|
|
|
|
<div class="detail-info-card">
|
|
<span>Kondisi Ekonomi</span>
|
|
<strong>${escapeHtml(data.kondisi_ekonomi)} · ${escapeHtml(formatRupiah(data.gaji_ortu))}</strong>
|
|
</div>
|
|
</div>
|
|
|
|
<h6 class="detail-section-title">Nilai Rapor Ringkas</h6>
|
|
<div class="mini-data-grid">
|
|
${renderObjectGrid(data.nilai_ringkas, 'mini-data-item')}
|
|
</div>
|
|
|
|
<h6 class="detail-section-title">Skor Minat RIASEC</h6>
|
|
<div class="mini-data-grid">
|
|
${renderObjectGrid(data.riasec, 'mini-data-item')}
|
|
</div>
|
|
`;
|
|
} else {
|
|
modalBody.innerHTML = `<div class="alert alert-danger m-0">Gagal memuat detail: ${escapeHtml(result.message)}</div>`;
|
|
}
|
|
} catch (error) {
|
|
modalBody.innerHTML = `<div class="alert alert-danger m-0">Terjadi kesalahan jaringan saat mengambil detail.</div>`;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
fetchRiwayat(1);
|
|
|
|
document.getElementById('searchInput').addEventListener('input', function () {
|
|
clearTimeout(searchTimer);
|
|
|
|
searchTimer = setTimeout(() => {
|
|
currentSearch = this.value.trim();
|
|
fetchRiwayat(1);
|
|
}, 350);
|
|
});
|
|
|
|
document.getElementById('perPageSelect').addEventListener('change', function () {
|
|
perPage = this.value;
|
|
fetchRiwayat(1);
|
|
});
|
|
});
|
|
</script>
|
|
@endpush
|
|
@endsection |