feat(povertymap): make verification tables responsive, add pagination, and support hash routing

This commit is contained in:
cygouw
2026-06-12 16:03:35 +07:00
parent 61cd292163
commit 91fa872970
2 changed files with 164 additions and 33 deletions
+98 -28
View File
@@ -11,7 +11,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" /> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" <link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap"
rel="stylesheet"> rel="stylesheet">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css?v=1.6">
</head> </head>
<body> <body>
@@ -433,6 +433,9 @@
await fetch(API.auth + '?action=logout', { method: 'POST', credentials: 'include' }); await fetch(API.auth + '?action=logout', { method: 'POST', credentials: 'include' });
currentUser = null; currentUser = null;
if (location.hash) {
history.replaceState(null, null, ' ');
}
document.getElementById('login-screen').classList.remove('hidden'); document.getElementById('login-screen').classList.remove('hidden');
document.getElementById('app').classList.add('hidden'); document.getElementById('app').classList.add('hidden');
document.getElementById('login-user').value = ''; document.getElementById('login-user').value = '';
@@ -473,9 +476,12 @@
buildNav(); buildNav();
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Load data dulu, baru render dashboard sekali (mencegah double render) // Load data dulu
await loadAll(); await loadAll();
switchPage('dashboard'); const hashPage = location.hash.replace('#', '');
const validPages = getNavItems(currentUser.role).map(item => item.id);
const targetPage = validPages.includes(hashPage) ? hashPage : 'dashboard';
switchPage(targetPage);
} }
// -- NAV // -- NAV
@@ -543,7 +549,22 @@
else cancelAddMode(); else cancelAddMode();
renderPage(page); renderPage(page);
window.scrollTo(0, 0); window.scrollTo(0, 0);
// Update URL hash
if (location.hash !== '#' + page) {
location.hash = page;
} }
}
// Global listener for SPA back/forward browser buttons
window.addEventListener('hashchange', () => {
if (!currentUser) return;
const hashPage = location.hash.replace('#', '');
const validPages = getNavItems(currentUser.role).map(item => item.id);
if (validPages.includes(hashPage) && currentPage !== hashPage) {
switchPage(hashPage);
}
});
// -- RENDER PAGES --------------------------- // -- RENDER PAGES ---------------------------
function renderPage(page) { function renderPage(page) {
@@ -1409,6 +1430,8 @@
// -- VERIFY (Table layout) ------------------ // -- VERIFY (Table layout) ------------------
let _verifyData = { ibadah: [], warga: [] }; let _verifyData = { ibadah: [], warga: [] };
let _verifyTab = 'ibadah'; // active tab: 'ibadah' | 'warga' let _verifyTab = 'ibadah'; // active tab: 'ibadah' | 'warga'
let _verifyPage = 1;
let _verifyPerPage = 10;
async function renderVerify(area) { async function renderVerify(area) {
area.innerHTML = '<div class="empty-state"><div class="spin"></div><p style="margin-top:12px">Memuat data pending...</p></div>'; area.innerHTML = '<div class="empty-state"><div class="spin"></div><p style="margin-top:12px">Memuat data pending...</p></div>';
@@ -1422,6 +1445,9 @@
// Set default tab ke yang ada datanya // Set default tab ke yang ada datanya
if (!_verifyData.ibadah.length && _verifyData.warga.length) _verifyTab = 'warga'; if (!_verifyData.ibadah.length && _verifyData.warga.length) _verifyTab = 'warga';
else _verifyTab = 'ibadah'; else _verifyTab = 'ibadah';
_verifyPage = 1;
if (!_verifyData.ibadah.length && !_verifyData.warga.length) { if (!_verifyData.ibadah.length && !_verifyData.warga.length) {
area.innerHTML = '<div class="empty-state"><div class="e-icon"><i class="fa-solid fa-circle-check" style="color:var(--green)"></i></div><p>Semua data sudah diverifikasi!<br>Tidak ada data pending.</p></div>'; area.innerHTML = '<div class="empty-state"><div class="e-icon"><i class="fa-solid fa-circle-check" style="color:var(--green)"></i></div><p>Semua data sudah diverifikasi!<br>Tidak ada data pending.</p></div>';
return; return;
@@ -1439,7 +1465,8 @@
</select> </select>
<span class="verify-count-badge" id="vt-count"></span> <span class="verify-count-badge" id="vt-count"></span>
</div> </div>
<div id="vt-table-section"></div>`; <div id="vt-table-section"></div>
<div class="daftar-pagination" id="vt-pagination"></div>`;
renderVerifyTabs(); renderVerifyTabs();
renderVerifyTables(); renderVerifyTables();
} catch (e) { area.innerHTML = '<div class="empty-state"><p>Gagal memuat data. Coba refresh.</p></div>'; } } catch (e) { area.innerHTML = '<div class="empty-state"><p>Gagal memuat data. Coba refresh.</p></div>'; }
@@ -1447,6 +1474,7 @@
function switchVerifyTab(tab) { function switchVerifyTab(tab) {
_verifyTab = tab; _verifyTab = tab;
_verifyPage = 1;
renderVerifyTabs(); renderVerifyTabs();
renderVerifyTables(); renderVerifyTables();
} }
@@ -1475,21 +1503,43 @@
: new Date(a.created_at) - new Date(b.created_at); : new Date(a.created_at) - new Date(b.created_at);
const sec = document.getElementById('vt-table-section'); const sec = document.getElementById('vt-table-section');
const pgEl = document.getElementById('vt-pagination');
if (!sec) return; if (!sec) return;
if (_verifyTab === 'ibadah') { let filtered = [];
const isIb = (_verifyTab === 'ibadah');
if (isIb) {
const matchIb = ib => !q || [ib.nama, ib.surveyor_nama, ib.kecamatan, ib.pic, ib.alamat, ib.jenis] const matchIb = ib => !q || [ib.nama, ib.surveyor_nama, ib.kecamatan, ib.pic, ib.alamat, ib.jenis]
.some(v => (v || '').toLowerCase().includes(q)); .some(v => (v || '').toLowerCase().includes(q));
const pi = [..._verifyData.ibadah].filter(matchIb).sort(sorter); filtered = [..._verifyData.ibadah].filter(matchIb).sort(sorter);
const countEl = document.getElementById('vt-count'); } else {
if (countEl) countEl.textContent = pi.length + ' pending'; const matchW = w => !q || [w.nama_kk, w.surveyor_nama, w.kecamatan, w.nik, w.keterangan, w.alamat]
.some(v => (v || '').toLowerCase().includes(q));
filtered = [..._verifyData.warga].filter(matchW).sort(sorter);
}
if (!pi.length) { const countEl = document.getElementById('vt-count');
if (countEl) countEl.textContent = filtered.length + ' pending';
if (!filtered.length) {
sec.innerHTML = '<div class="empty-state" style="padding:40px 0"><p>Tidak ada data yang cocok dengan pencarian.</p></div>'; sec.innerHTML = '<div class="empty-state" style="padding:40px 0"><p>Tidak ada data yang cocok dengan pencarian.</p></div>';
if (pgEl) pgEl.innerHTML = '';
return; return;
} }
// Pagination calculation
const total = filtered.length;
const totalPages = Math.max(1, Math.ceil(total / _verifyPerPage));
if (_verifyPage > totalPages) _verifyPage = totalPages;
if (_verifyPage < 1) _verifyPage = 1;
const start = (_verifyPage - 1) * _verifyPerPage;
const pageData = filtered.slice(start, start + _verifyPerPage);
if (isIb) {
sec.innerHTML = ` sec.innerHTML = `
<div class="verify-table-wrap"> <div class="verify-table-wrap verify-ibadah-table">
<table> <table>
<thead><tr> <thead><tr>
<th style="width:52px">Foto</th> <th style="width:52px">Foto</th>
@@ -1503,7 +1553,7 @@
<th style="text-align:right">Aksi</th> <th style="text-align:right">Aksi</th>
</tr></thead> </tr></thead>
<tbody> <tbody>
${pi.map(ib => ` ${pageData.map(ib => `
<tr> <tr>
<td> <td>
${ib.foto_path ${ib.foto_path
@@ -1512,7 +1562,7 @@
</td> </td>
<td> <td>
<div class="vt-name"><i class="${ICONS[ib.jenis]}" style="margin-right:5px;color:var(--amber)"></i>${h(ib.nama)}</div> <div class="vt-name"><i class="${ICONS[ib.jenis]}" style="margin-right:5px;color:var(--amber)"></i>${h(ib.nama)}</div>
${ib.alamat ? `<div class="vt-sub">${h(ib.alamat.substring(0, 45))}${ib.alamat.length > 45 ? ' ' : ''}</div>` : ''} ${ib.alamat ? `<div class="vt-sub">${h(ib.alamat.substring(0, 45))}${ib.alamat.length > 45 ? '...' : ''}</div>` : ''}
</td> </td>
<td><span class="vt-tag amber"><i class="${ICONS[ib.jenis]}"></i> ${h(ib.jenis)}</span></td> <td><span class="vt-tag amber"><i class="${ICONS[ib.jenis]}"></i> ${h(ib.jenis)}</span></td>
<td><div class="vt-sub">${ib.kecamatan ? 'Kec. ' + h(ib.kecamatan) : ' '}</div></td> <td><div class="vt-sub">${ib.kecamatan ? 'Kec. ' + h(ib.kecamatan) : ' '}</div></td>
@@ -1534,21 +1584,9 @@
</tbody> </tbody>
</table> </table>
</div>`; </div>`;
} else { } else {
// Tab Warga Miskin
const matchW = w => !q || [w.nama_kk, w.surveyor_nama, w.kecamatan, w.nik, w.keterangan, w.alamat]
.some(v => (v || '').toLowerCase().includes(q));
const pw = [..._verifyData.warga].filter(matchW).sort(sorter);
const countEl = document.getElementById('vt-count');
if (countEl) countEl.textContent = pw.length + ' pending';
if (!pw.length) {
sec.innerHTML = '<div class="empty-state" style="padding:40px 0"><p>Tidak ada data yang cocok dengan pencarian.</p></div>';
return;
}
sec.innerHTML = ` sec.innerHTML = `
<div class="verify-table-wrap"> <div class="verify-table-wrap verify-warga-table">
<table> <table>
<thead><tr> <thead><tr>
<th style="width:52px">Foto</th> <th style="width:52px">Foto</th>
@@ -1562,7 +1600,7 @@
<th style="text-align:right">Aksi</th> <th style="text-align:right">Aksi</th>
</tr></thead> </tr></thead>
<tbody> <tbody>
${pw.map(w => ` ${pageData.map(w => `
<tr> <tr>
<td> <td>
${w.foto_path ${w.foto_path
@@ -1571,8 +1609,8 @@
</td> </td>
<td> <td>
<div class="vt-name"><i class="fa-solid fa-user" style="margin-right:5px;color:var(--blue)"></i>${h(w.nama_kk)}</div> <div class="vt-name"><i class="fa-solid fa-user" style="margin-right:5px;color:var(--blue)"></i>${h(w.nama_kk)}</div>
${w.alamat ? `<div class="vt-sub">${h(w.alamat.substring(0, 45))}${w.alamat.length > 45 ? ' ' : ''}</div>` : ''} ${w.alamat ? `<div class="vt-sub">${h(w.alamat.substring(0, 45))}${w.alamat.length > 45 ? '...' : ''}</div>` : ''}
${w.keterangan ? `<div class="vt-sub" style="color:var(--text3);font-style:italic">${h(w.keterangan.substring(0, 40))}${w.keterangan.length > 40 ? ' ' : ''}</div>` : ''} ${w.keterangan ? `<div class="vt-sub" style="color:var(--text3);font-style:italic">${h(w.keterangan.substring(0, 40))}${w.keterangan.length > 40 ? '...' : ''}</div>` : ''}
</td> </td>
<td> <td>
<div class="vt-col-nik">${h(w.nik || ' ')}</div> <div class="vt-col-nik">${h(w.nik || ' ')}</div>
@@ -1601,6 +1639,38 @@
</table> </table>
</div>`; </div>`;
} }
// Render Pagination Buttons
if (pgEl) {
if (totalPages <= 1) { pgEl.innerHTML = ''; return; }
let pgHtml = `<button ${_verifyPage <= 1 ? 'disabled' : ''} onclick="verifyGoPage(${_verifyPage - 1})"><i class="fa-solid fa-chevron-left" style="font-size:10px"></i></button>`;
let pages = [];
if (totalPages <= 7) {
for (let i = 1; i <= totalPages; i++) pages.push(i);
} else {
pages.push(1);
if (_verifyPage > 3) pages.push('...');
for (let i = Math.max(2, _verifyPage - 1); i <= Math.min(totalPages - 1, _verifyPage + 1); i++) pages.push(i);
if (_verifyPage < totalPages - 2) pages.push('...');
pages.push(totalPages);
}
pages.forEach(p => {
if (p === '...') { pgHtml += `<span class="pg-info">...</span>`; }
else { pgHtml += `<button class="${p === _verifyPage ? 'active' : ''}" onclick="verifyGoPage(${p})">${p}</button>`; }
});
pgHtml += `<button ${_verifyPage >= totalPages ? 'disabled' : ''} onclick="verifyGoPage(${_verifyPage + 1})"><i class="fa-solid fa-chevron-right" style="font-size:10px"></i></button>`;
pgEl.innerHTML = pgHtml;
}
}
function verifyGoPage(p) {
_verifyPage = p;
renderVerifyTables();
const sec = document.getElementById('vt-tabs');
if (sec) sec.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} }
function openDetailModal(type, id) { function openDetailModal(type, id) {
+63 -2
View File
@@ -1627,7 +1627,8 @@ a {
background: #fff; background: #fff;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius); border-radius: var(--radius);
overflow: hidden; overflow-x: auto;
scrollbar-width: thin;
margin-bottom: 24px; margin-bottom: 24px;
} }
@@ -1636,6 +1637,64 @@ a {
border-collapse: collapse; border-collapse: collapse;
} }
/* Column sizes for verification tables to prevent squeezing and cutting off */
.verify-table-wrap th:nth-child(1),
.verify-table-wrap td:nth-child(1) {
width: 52px;
min-width: 52px;
}
.verify-table-wrap th:nth-child(4),
.verify-table-wrap td:nth-child(4) {
min-width: 110px; /* Kecamatan (both tables) */
}
.verify-table-wrap th:nth-child(7),
.verify-table-wrap td:nth-child(7) {
min-width: 100px; /* Surveyor (both tables) */
}
.verify-table-wrap th:nth-child(8),
.verify-table-wrap td:nth-child(8) {
min-width: 110px; /* Tanggal (both tables) */
}
.verify-table-wrap th:last-child,
.verify-table-wrap td:last-child {
width: 180px;
min-width: 180px;
text-align: right; /* Aksi (both tables) */
}
/* -- Ibadah table specific columns -- */
.verify-ibadah-table th:nth-child(3),
.verify-ibadah-table td:nth-child(3) {
min-width: 90px; /* Jenis */
}
.verify-ibadah-table th:nth-child(5),
.verify-ibadah-table td:nth-child(5) {
min-width: 120px; /* PIC & WA */
}
.verify-ibadah-table th:nth-child(6),
.verify-ibadah-table td:nth-child(6) {
width: 70px;
min-width: 70px; /* Radius (short, e.g. "500 m") */
}
/* -- Warga table specific columns -- */
.verify-warga-table th:nth-child(3),
.verify-warga-table td:nth-child(3) {
min-width: 130px; /* NIK KK (16 digits) */
}
.verify-warga-table th:nth-child(5),
.verify-warga-table td:nth-child(5) {
min-width: 80px; /* Jml Anggota (short, e.g. "2 jiwa") */
text-align: center;
}
.verify-warga-table th:nth-child(5) {
text-align: center;
}
.verify-warga-table th:nth-child(6),
.verify-warga-table td:nth-child(6) {
min-width: 140px; /* Ibadah Terdekat (longer text) */
}
.verify-table-wrap thead tr { .verify-table-wrap thead tr {
background: var(--border2); background: var(--border2);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
@@ -1649,7 +1708,7 @@ a {
letter-spacing: .5px; letter-spacing: .5px;
color: var(--text2); color: var(--text2);
text-align: left; text-align: left;
white-space: nowrap; white-space: normal;
} }
.verify-table-wrap tbody tr { .verify-table-wrap tbody tr {
@@ -1670,6 +1729,8 @@ a {
font-size: 13px; font-size: 13px;
color: var(--text); color: var(--text);
vertical-align: middle; vertical-align: middle;
word-break: break-word;
overflow-wrap: break-word;
} }
.vt-thumb { .vt-thumb {