mengubah sistem menjadi lebih terfokus ke poverty

This commit is contained in:
Abimanyu Ridho
2026-06-11 01:50:37 +07:00
parent 739e360018
commit 468541b1c8
46 changed files with 5753 additions and 2805 deletions
+337
View File
@@ -0,0 +1,337 @@
let allData = [];
let allRI = [];
let filteredData = [];
let sortField = 'nama';
let sortDir = 'asc';
const PAGE_SIZE = 20;
let currentPage = 1;
document.addEventListener('auth-ready', function (e) {
const user = e.detail;
document.body.classList.add('auth-loaded', 'role-' + user.role);
document.getElementById('user-display-name').textContent = user.username;
const roleMap = { 'admin': 'Admin', 'petugas_lapangan': 'Rumah Ibadah', 'guest': 'Guest' };
document.getElementById('user-display-role').textContent = roleMap[user.role] || user.role;
// Hide admin-nav for non-admin
document.querySelectorAll('.admin-nav').forEach(el => {
if (user.role !== 'admin') el.style.display = 'none';
});
loadData(user);
});
async function loadData(user) {
try {
const [pmRes, riRes] = await Promise.all([
fetch('api/penduduk_miskin.php'),
fetch('api/rumah_ibadah.php')
]);
const [pmData, riData] = await Promise.all([pmRes.json(), riRes.json()]);
allData = pmData.data || [];
allRI = riData.data || [];
// For petugas_lapangan: filter to only show residents in their radius
if (user && user.role === 'petugas_lapangan') {
const myRiId = String(user.rumah_ibadah_id);
allData = allData.filter(p => p.status_proximity === 'dalam_radius' && String(p.rumah_ibadah_id) === myRiId);
}
// Populate RI filter
const riFilter = document.getElementById('filter-ri');
allRI.forEach(r => {
const opt = document.createElement('option');
opt.value = r.id;
opt.textContent = r.nama;
riFilter.appendChild(opt);
});
updateSummary();
applyFilters();
} catch (err) {
document.getElementById('pm-tbody').innerHTML = `<tr><td colspan="7" style="text-align:center;padding:40px;color:#ef4444">Gagal memuat data: ${esc(err.message)}</td></tr>`;
}
}
function updateSummary() {
const total = allData.length;
const jiwa = allData.reduce((s, p) => s + (parseInt(p.jumlah_anggota, 10) || 0), 0);
const dalam = allData.filter(p => p.status_proximity === 'dalam_radius').length;
const luar = allData.filter(p => p.status_proximity !== 'dalam_radius').length;
const withBantuan = allData.filter(p => p.total_bantuan > 0).length;
document.getElementById('sum-total').textContent = total;
document.getElementById('sum-jiwa').textContent = jiwa;
document.getElementById('sum-dalam').textContent = dalam;
document.getElementById('sum-luar').textContent = luar;
document.getElementById('sum-bantuan').textContent = withBantuan;
}
function applyFilters() {
const query = document.getElementById('search-input').value.toLowerCase().trim();
const proximity = document.getElementById('filter-proximity').value;
const riId = document.getElementById('filter-ri').value;
const bantuan = document.getElementById('filter-bantuan').value;
filteredData = allData.filter(p => {
const matchQ = !query || p.nama.toLowerCase().includes(query) || (p.nik && p.nik.includes(query)) || (p.alamat && p.alamat.toLowerCase().includes(query)) || (p.nama_rumah_ibadah && p.nama_rumah_ibadah.toLowerCase().includes(query));
const matchProx = !proximity || p.status_proximity === proximity;
const matchRI = !riId || String(p.rumah_ibadah_id) === riId;
const matchBantuan = !bantuan || (bantuan === 'pernah' ? p.total_bantuan > 0 : p.total_bantuan === 0);
return matchQ && matchProx && matchRI && matchBantuan;
});
// Sort
filteredData.sort((a, b) => {
let va = a[sortField], vb = b[sortField];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va < vb) return sortDir === 'asc' ? -1 : 1;
if (va > vb) return sortDir === 'asc' ? 1 : -1;
return 0;
});
currentPage = 1;
document.getElementById('filter-count').textContent = `${filteredData.length} data`;
renderTable();
}
function sortBy(field) {
if (sortField === field) {
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
} else {
sortField = field;
sortDir = 'asc';
}
// Update sort icons
document.querySelectorAll('.pm-table thead th').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc');
});
const ths = document.querySelectorAll('.pm-table thead th');
const fieldIdx = { 'nama': 0, 'nik': 1, 'jumlah_anggota': 2, 'total_bantuan': 5 };
if (fieldIdx[field] !== undefined) {
ths[fieldIdx[field]].classList.add(sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
}
applyFilters();
}
function renderTable() {
const tbody = document.getElementById('pm-tbody');
const total = filteredData.length;
if (total === 0) {
tbody.innerHTML = `
<tr><td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">🔍</div>
<h3>Tidak ada data</h3>
<p>Coba ubah kriteria pencarian atau filter.</p>
</div>
</td></tr>`;
document.getElementById('pagination').style.display = 'none';
return;
}
const totalPages = Math.ceil(total / PAGE_SIZE);
if (currentPage > totalPages) currentPage = totalPages;
const start = (currentPage - 1) * PAGE_SIZE;
const slice = filteredData.slice(start, start + PAGE_SIZE);
tbody.innerHTML = slice.map((p, idx) => {
const inR = p.status_proximity === 'dalam_radius';
const hasPemb = p.bantuan_pemberdayaan > 0;
const hasKons = p.bantuan_konsumtif > 0;
return `
<tr onclick="openDrawer(${p.id})">
<td>
<div class="cell-name">${esc(p.nama)}</div>
${p.alamat ? `<div class="cell-sub">📍 ${esc(p.alamat.substring(0, 50))}${p.alamat.length > 50 ? '…' : ''}</div>` : ''}
</td>
<td><span style="font-family:monospace;font-size:11px">${p.nik ? esc(p.nik) : '<span style="color:#c4c4c4">—</span>'}</span></td>
<td><strong>${p.jumlah_anggota}</strong> jiwa</td>
<td><span class="badge ${inR ? 'badge-dalam' : 'badge-luar'}">${inR ? '✅ Dalam' : '❌ Luar'}</span></td>
<td>
${p.nama_rumah_ibadah ? `<span style="font-size:12px">⛪ ${esc(p.nama_rumah_ibadah)}</span>` : '<span style="color:#c4c4c4;font-size:11px">—</span>'}
</td>
<td>
<div class="aid-pills">
${p.total_bantuan === 0 ? '<span style="color:#c4c4c4;font-size:11px">Belum ada</span>' : ''}
${hasPemb ? `<span class="badge badge-pemberdayaan">📚 ${p.bantuan_pemberdayaan}</span>` : ''}
${hasKons ? `<span class="badge badge-konsumtif">🛍️ ${p.bantuan_konsumtif}</span>` : ''}
</div>
</td>
<td onclick="event.stopPropagation()">
<div style="display:flex;gap:4px">
<button class="btn btn-ghost btn-sm" style="padding:5px 8px;font-size:11px" onclick="openDrawer(${p.id})">👁 Detail</button>
</div>
</td>
</tr>`;
}).join('');
// Pagination
const pag = document.getElementById('pagination');
const pagInfo = document.getElementById('pagination-info');
const pagBtns = document.getElementById('pagination-btns');
pag.style.display = 'flex';
pagInfo.textContent = `Menampilkan ${start + 1}${Math.min(start + PAGE_SIZE, total)} dari ${total} data`;
// Build page buttons
let btns = '';
btns += `<button class="pg-btn" onclick="goPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}></button>`;
const rangeStart = Math.max(1, currentPage - 2);
const rangeEnd = Math.min(totalPages, currentPage + 2);
if (rangeStart > 1) btns += `<button class="pg-btn" onclick="goPage(1)">1</button>${rangeStart > 2 ? '<span style="padding:0 4px;color:#94a3b8">…</span>' : ''}`;
for (let i = rangeStart; i <= rangeEnd; i++) {
btns += `<button class="pg-btn ${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
}
if (rangeEnd < totalPages) btns += `${rangeEnd < totalPages - 1 ? '<span style="padding:0 4px;color:#94a3b8">…</span>' : ''}<button class="pg-btn" onclick="goPage(${totalPages})">${totalPages}</button>`;
btns += `<button class="pg-btn" onclick="goPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}></button>`;
pagBtns.innerHTML = btns;
}
function goPage(page) {
const totalPages = Math.ceil(filteredData.length / PAGE_SIZE);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderTable();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
async function openDrawer(id) {
document.getElementById('detail-overlay').classList.add('open');
document.getElementById('drawer-body').innerHTML = `
<div class="loading-spinner-wrap">
<span class="spinner" style="width:20px;height:20px;border-width:3px;"></span>
<p style="margin-top:12px">Memuat detail…</p>
</div>`;
try {
const [pmRes, logRes] = await Promise.all([
fetch(`api/penduduk_miskin.php?id=${id}`),
fetch(`api/log_bantuan.php?penduduk_miskin_id=${id}`)
]);
const [pmData, logData] = await Promise.all([pmRes.json(), logRes.json()]);
if (pmData.status !== 'success') throw new Error(pmData.message);
renderDrawer(pmData.data, logData.data || []);
} catch (err) {
document.getElementById('drawer-body').innerHTML = `<p style="color:#ef4444">Gagal memuat: ${esc(err.message)}</p>`;
}
}
function renderDrawer(p, logs) {
document.getElementById('drawer-title').textContent = p.nama;
const inR = p.status_proximity === 'dalam_radius';
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
const isPetugas = window.currentUser && window.currentUser.role === 'petugas_lapangan';
const isResidentInMyRadius = isPetugas && inR && String(p.rumah_ibadah_id) === String(window.currentUser.rumah_ibadah_id);
const canEdit = isAdmin || isResidentInMyRadius;
document.getElementById('drawer-body').innerHTML = `
<!-- Proximity status -->
<div class="proximity-card ${inR ? 'dalam' : 'luar'}">
${inR ? `✅ Dalam radius jangkauan <strong>${esc(p.nama_rumah_ibadah || 'rumah ibadah')}</strong>` : '❌ Di luar radius semua rumah ibadah'}
</div>
<!-- Basic info -->
<div class="section-heading">Informasi Pribadi</div>
<div>
<div class="info-row"><span class="info-label">Nama Lengkap</span><span class="info-val">${esc(p.nama)}</span></div>
<div class="info-row"><span class="info-label">NIK</span><span class="info-val" style="font-family:monospace">${p.nik ? esc(p.nik) : '—'}</span></div>
<div class="info-row"><span class="info-label">No. WhatsApp</span><span class="info-val">${p.no_wa ? esc(p.no_wa) : '—'}</span></div>
<div class="info-row"><span class="info-label">Jumlah Anggota</span><span class="info-val">${p.jumlah_anggota} jiwa</span></div>
<div class="info-row"><span class="info-label">Alamat</span><span class="info-val">${p.alamat ? esc(p.alamat) : '—'}</span></div>
<div class="info-row"><span class="info-label">Koordinat</span><span class="info-val" style="font-family:monospace;font-size:11px">${p.latitude.toFixed(6)}, ${p.longitude.toFixed(6)}</span></div>
<div class="info-row"><span class="info-label">Terdata Sejak</span><span class="info-val">${new Date(p.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })}</span></div>
</div>
<!-- Bantuan summary -->
<div class="section-heading">Ringkasan Bantuan</div>
<div>
${p.ringkasan_bantuan && p.ringkasan_bantuan.length ? p.ringkasan_bantuan.map(rb => `
<div class="info-row">
<span class="info-label">${rb.tipe_bantuan === 'Pemberdayaan' ? '📚 Pemberdayaan' : '🛍️ Konsumtif'}</span>
<span class="info-val">${rb.jumlah} kali</span>
</div>
`).join('') : '<div style="color:#94a3b8;font-size:13px;padding:8px 0">Belum pernah menerima bantuan.</div>'}
</div>
<!-- Action buttons -->
${canEdit ? `
<div style="margin-top:16px;display:flex;gap:8px">
<button class="btn btn-primary btn-sm" onclick="openLogBantuanFromDrawer(${p.id}, '${esc(p.nama)}')">📋 Catat Bantuan</button>
<button class="btn btn-ghost btn-sm" onclick="window.location.href='?page=map'">✏️ Edit di Peta</button>
</div>
` : ''}
<!-- Log history -->
${logs.length ? `
<div class="section-heading">Riwayat Bantuan (${logs.length})</div>
<div class="log-list">
${logs.map(l => `
<div class="log-entry">
<div class="log-type-dot ${l.tipe_bantuan === 'Pemberdayaan' ? 'pem' : 'kon'}"></div>
<div class="log-entry-info">
<div class="log-entry-name">${l.tipe_bantuan === 'Pemberdayaan' ? '📚' : '🛍️'} ${esc(l.tipe_bantuan)}${l.sub_kategori ? ' — ' + esc(l.sub_kategori) : ''}</div>
<div class="log-entry-detail">Dari: ${esc(l.nama_rumah_ibadah)}${l.nilai_bantuan ? ' · Rp' + new Intl.NumberFormat('id-ID').format(l.nilai_bantuan) : ''}${l.keterangan ? ' · ' + esc(l.keterangan) : ''}</div>
</div>
<div class="log-entry-date">${l.tanggal_bantuan}</div>
</div>
`).join('')}
</div>
` : ''}
`;
}
function closeDrawer(e) {
if (e && e.target !== document.getElementById('detail-overlay')) return;
document.getElementById('detail-overlay').classList.remove('open');
}
function openLogBantuanFromDrawer(pmId, pmNama) {
// Redirect to peta with log bantuan opened
window.location.href = '?page=map';
}
function exportCSV() {
if (!filteredData.length) return toast('Tidak ada data untuk diekspor', 'error');
const headers = ['Nama', 'NIK', 'No WA', 'Alamat', 'Jumlah Anggota', 'Status Proximity', 'Rumah Ibadah', 'Total Bantuan', 'Bantuan Pemberdayaan', 'Bantuan Konsumtif', 'Latitude', 'Longitude'];
const rows = filteredData.map(p => [
p.nama, p.nik || '', p.no_wa || '', p.alamat || '',
p.jumlah_anggota, p.status_proximity, p.nama_rumah_ibadah || '',
p.total_bantuan, p.bantuan_pemberdayaan, p.bantuan_konsumtif,
p.latitude, p.longitude
]);
const csv = [headers, ...rows].map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n');
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `penduduk_miskin_${new Date().toISOString().slice(0, 10)}.csv`;
a.click();
URL.revokeObjectURL(url);
toast(`${filteredData.length} data berhasil diekspor`, 'success');
}
function esc(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function toast(msg, type = 'info') {
const t = document.createElement('div');
t.className = `toast ${type}`;
t.textContent = msg;
document.getElementById('toast-container').appendChild(t);
setTimeout(() => t.remove(), 3500);
}
async function handleLogout() {
if (!confirm('Apakah Anda yakin ingin keluar?')) return;
try {
const response = await fetch('api/logout.php');
const data = await response.json();
if (data.status === 'success') window.location.href = '?page=login';
} catch { alert('Gagal logout.'); }
}