feat(povertymap): make verification tables responsive, add pagination, and support hash routing
This commit is contained in:
+98
-28
@@ -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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user