Files
2026-06-11 01:50:37 +07:00

337 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.'); }
}