213 lines
8.9 KiB
PHP
213 lines
8.9 KiB
PHP
@php($showRecapTitle = $showRecapTitle ?? true)
|
||
<style>
|
||
.recap-scroll-area {
|
||
max-height: 55vh;
|
||
overflow-y: auto;
|
||
scroll-behavior: smooth;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #b8c7db transparent;
|
||
}
|
||
.recap-scroll-area::-webkit-scrollbar {
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
.recap-scroll-area::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
border-radius: 999px;
|
||
}
|
||
.recap-scroll-area::-webkit-scrollbar-thumb {
|
||
background: rgba(148, 163, 184, 0.72);
|
||
border-radius: 999px;
|
||
border: 2px solid transparent;
|
||
background-clip: padding-box;
|
||
}
|
||
.recap-scroll-area::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(100, 116, 139, 0.86);
|
||
border: 2px solid transparent;
|
||
background-clip: padding-box;
|
||
}
|
||
</style>
|
||
<div class="d-flex flex-column flex-md-row align-items-md-center gap-2 mb-3">
|
||
@if ($showRecapTitle)
|
||
<div>
|
||
<h4 class="mb-0">Rekap Dokumen</h4>
|
||
<small class="text-muted">Ringkasan jumlah file per Unit dan Kategori</small>
|
||
</div>
|
||
@endif
|
||
<div class="{{ $showRecapTitle ? 'ms-md-auto' : '' }} d-flex gap-2 align-items-center">
|
||
<div class="input-group input-group-sm" style="max-width:320px;">
|
||
<span class="input-group-text bg-white border-end-0">
|
||
<i class="fa fa-search text-muted"></i>
|
||
</span>
|
||
<input type="search" id="recapSearch" class="form-control border-start-0" placeholder="Cari unit atau folder" oninput="debouncedRecapSearch(this.value)">
|
||
</div>
|
||
<select id="recapPerPage" class="form-select form-select-sm" style="width:auto;" onchange="changePerPage(this.value)">
|
||
<option value="5">5</option>
|
||
<option value="10" selected>10</option>
|
||
<option value="20">20</option>
|
||
<option value="50">50</option>
|
||
</select>
|
||
<button class="btn btn-outline-secondary btn-sm d-flex align-items-center gap-1" onclick="fetchRecap()">
|
||
<i class="fa fa-rotate"></i>
|
||
<span>Refresh</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="table-responsive recap-scroll-area">
|
||
<table class="table table-sm table-hover align-middle">
|
||
<thead class="table-light shadow-sm">
|
||
<tr>
|
||
<th style="width:5%;" class="text-center">#</th>
|
||
<th style="width:30%;">Unit / Akreditasi</th>
|
||
<th style="width:20%;">Kategori</th>
|
||
<th style="width:15%;" class="text-center">Jumlah File</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="recapBody">
|
||
<tr>
|
||
<td colspan="4" class="text-center text-muted py-4">Memuat data...</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div class="small text-muted mt-2" id="recapSummary">Memuat ringkasan rekap...</div>
|
||
<div class="d-flex flex-column flex-md-row align-items-center justify-content-between gap-2 mt-3" id="recapPagination"></div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => fetchRecap());
|
||
|
||
let recapDebounce;
|
||
const recapState = { page:1, perPage:10, keyword:'', lastPage:1, totalUnits:0, grandTotalFiles:0, currentPageTotalFiles:0 };
|
||
let recapRequestController = null;
|
||
let recapRequestToken = 0;
|
||
|
||
function debouncedRecapSearch(val){
|
||
clearTimeout(recapDebounce);
|
||
recapDebounce = setTimeout(() => {
|
||
recapState.keyword = val;
|
||
recapState.page = 1;
|
||
fetchRecap();
|
||
}, 250);
|
||
}
|
||
|
||
function changePerPage(val){
|
||
recapState.perPage = parseInt(val) || 10;
|
||
recapState.page = 1;
|
||
fetchRecap();
|
||
}
|
||
|
||
function fetchRecap(){
|
||
const tbody = document.getElementById('recapBody');
|
||
const pager = document.getElementById('recapPagination');
|
||
const summaryEl = document.getElementById('recapSummary');
|
||
if(!tbody) return;
|
||
const requestToken = ++recapRequestToken;
|
||
if (recapRequestController) {
|
||
recapRequestController.abort();
|
||
}
|
||
recapRequestController = new AbortController();
|
||
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Memuat data...</td></tr>`;
|
||
if(pager) pager.innerHTML = '';
|
||
if(summaryEl) summaryEl.textContent = 'Memuat ringkasan rekap...';
|
||
|
||
const params = new URLSearchParams({
|
||
page: recapState.page,
|
||
per_page: recapState.perPage,
|
||
keyword: recapState.keyword || ''
|
||
});
|
||
fetch('/data/recap?' + params.toString(), {
|
||
signal: recapRequestController.signal,
|
||
})
|
||
.then(res => res.json())
|
||
.then(json => {
|
||
if (requestToken !== recapRequestToken) return;
|
||
const rows = json?.data || [];
|
||
recapState.page = json?.pagination?.current_page || recapState.page;
|
||
recapState.lastPage = json?.pagination?.last_page || 1;
|
||
recapState.totalUnits = json?.summary?.total_units || 0;
|
||
recapState.grandTotalFiles = json?.summary?.grand_total_files || 0;
|
||
recapState.currentPageTotalFiles = json?.summary?.current_page_total_files || 0;
|
||
if(!rows.length){
|
||
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-muted py-4">Tidak ada data</td></tr>`;
|
||
if(summaryEl) summaryEl.textContent = recapState.grandTotalFiles
|
||
? `Total file hasil filter: ${recapState.grandTotalFiles}`
|
||
: 'Tidak ada data rekap';
|
||
return;
|
||
}
|
||
const html = rows.map((row, idx) => {
|
||
const rowNumber = ((recapState.page - 1) * recapState.perPage) + idx + 1;
|
||
const folderRows = (row.data || []).map((f, i) => `
|
||
<tr>
|
||
${i === 0 ? `<td rowspan="${row.data.length}" class="text-center align-middle fw-semibold">${rowNumber}</td>` : ''}
|
||
${i === 0 ? `<td rowspan="${row.data.length}" class="fw-semibold">${row.unit || '-'}</td>` : ''}
|
||
<td>${f.folder || '-'}</td>
|
||
<td class="text-center fw-bold">${f.count || 0}</td>
|
||
</tr>
|
||
`).join('');
|
||
return folderRows;
|
||
}).join('');
|
||
tbody.innerHTML = html + `
|
||
<tr class="table-light">
|
||
<td colspan="3" class="text-end fw-semibold">Subtotal Halaman Ini</td>
|
||
<td class="text-center fw-bold">${recapState.currentPageTotalFiles}</td>
|
||
</tr>
|
||
`;
|
||
if(summaryEl) {
|
||
const from = recapState.totalUnits === 0 ? 0 : ((recapState.page - 1) * recapState.perPage) + 1;
|
||
const to = Math.min(((recapState.page - 1) * recapState.perPage) + rows.length, recapState.totalUnits);
|
||
summaryEl.textContent = `Menampilkan ${from}-${to} dari ${recapState.totalUnits} unit rekap. Total file hasil filter: ${recapState.grandTotalFiles}.`;
|
||
}
|
||
renderRecapPagination();
|
||
})
|
||
.catch(err => {
|
||
if (err?.name === 'AbortError') return;
|
||
console.error(err);
|
||
tbody.innerHTML = `<tr><td colspan="4" class="text-center text-danger py-4">Gagal memuat data</td></tr>`;
|
||
const summaryEl = document.getElementById('recapSummary');
|
||
if(summaryEl) summaryEl.textContent = 'Gagal memuat ringkasan rekap';
|
||
})
|
||
.finally(() => {
|
||
if (requestToken === recapRequestToken) {
|
||
recapRequestController = null;
|
||
}
|
||
});
|
||
}
|
||
|
||
function renderRecapPagination(){
|
||
const pager = document.getElementById('recapPagination');
|
||
if(!pager) return;
|
||
if(recapState.lastPage <= 1){
|
||
pager.innerHTML = '';
|
||
return;
|
||
}
|
||
const maxButtons = 5;
|
||
let start = Math.max(1, recapState.page - Math.floor(maxButtons/2));
|
||
let end = Math.min(recapState.lastPage, start + maxButtons - 1);
|
||
start = Math.max(1, end - maxButtons + 1);
|
||
|
||
let buttons = '';
|
||
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="prev" ${recapState.page === 1 ? 'disabled' : ''}>‹</button>`;
|
||
for(let i=start; i<=end; i++){
|
||
buttons += `<button class="btn btn-sm ${i === recapState.page ? 'btn-primary' : 'btn-outline-secondary'}" data-page="${i}">${i}</button>`;
|
||
}
|
||
buttons += `<button class="btn btn-outline-secondary btn-sm" data-page="next" ${recapState.page === recapState.lastPage ? 'disabled' : ''}>›</button>`;
|
||
|
||
pager.innerHTML = `
|
||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||
<div class="btn-group" role="group">${buttons}</div>
|
||
<span class="small text-muted">Halaman ${recapState.page} dari ${recapState.lastPage}</span>
|
||
</div>
|
||
`;
|
||
|
||
pager.querySelectorAll('button[data-page]').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const page = btn.getAttribute('data-page');
|
||
if(page === 'prev' && recapState.page > 1) recapState.page--;
|
||
else if(page === 'next' && recapState.page < recapState.lastPage) recapState.page++;
|
||
else if(!isNaN(parseInt(page))) recapState.page = parseInt(page);
|
||
fetchRecap();
|
||
});
|
||
});
|
||
}
|
||
</script>
|