feat: complete WebGIS implementation with nominal bantuan and cache busting

This commit is contained in:
fananazril
2026-06-11 19:18:25 +07:00
parent 0d59e629bd
commit 0ae2768849
36 changed files with 7687 additions and 0 deletions
+427
View File
@@ -0,0 +1,427 @@
<?php
// ================================================================
// Admin: CRUD Penduduk Miskin
// ================================================================
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/functions.php';
requireLogin();
$pageTitle = 'Penduduk Miskin';
require_once __DIR__ . '/../includes/header.php';
$db = getDB();
$perPage = 15;
$page = max(1, (int)($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
$search = trim($_GET['search'] ?? '');
$filterSt = $_GET['status'] ?? '';
$filterKec = $_GET['kecamatan'] ?? '';
$filterIb = (int)($_GET['ibadah_id'] ?? 0);
$where = ['1=1'];
$params = [];
if ($search) { $where[] = '(pm.kk_nama LIKE ? OR pm.nik LIKE ? OR pm.alamat LIKE ?)'; $q="%$search%"; $params=array_merge($params,[$q,$q,$q]); }
if ($filterSt) { $where[] = 'pm.status_bantuan = ?'; $params[] = $filterSt; }
if ($filterKec){ $where[] = 'pm.kecamatan = ?'; $params[] = $filterKec; }
if ($filterIb) { $where[] = 'pm.rumah_ibadah_id = ?'; $params[] = $filterIb; }
$ws = implode(' AND ', $where);
$stmtCnt = $db->prepare("SELECT COUNT(*) FROM penduduk_miskin pm WHERE $ws");
$stmtCnt->execute($params);
$total = (int)$stmtCnt->fetchColumn();
$totalPages = max(1, ceil($total / $perPage));
$stmt = $db->prepare("
SELECT pm.*, ri.nama AS ibadah_nama, ri.jenis AS ibadah_jenis
FROM penduduk_miskin pm
LEFT JOIN rumah_ibadah ri ON pm.rumah_ibadah_id = ri.id
WHERE $ws ORDER BY pm.created_at DESC LIMIT $perPage OFFSET $offset
");
$stmt->execute($params);
$rows = $stmt->fetchAll();
$kecList = $db->query("SELECT DISTINCT kecamatan FROM penduduk_miskin WHERE kecamatan IS NOT NULL AND kecamatan != '' ORDER BY kecamatan")->fetchAll(PDO::FETCH_COLUMN);
$ibadahList = $db->query("SELECT id, nama, jenis FROM rumah_ibadah ORDER BY nama")->fetchAll();
?>
<!-- ── Page Header ─────────────────────────────────────────── -->
<div class="page-header">
<div class="page-title-block">
<h1>👤 Penduduk Miskin</h1>
<div class="page-subtitle">Total <?= $total ?> KK terdaftar</div>
</div>
<div class="page-actions">
<a href="<?= BASE_URL ?>/?mode=miskin" target="_blank" class="btn btn-ghost btn-sm">
<i class="bi bi-map"></i> Tambah di Peta
</a>
<button class="btn btn-success btn-sm" onclick="openMiskinModal()">
<i class="bi bi-plus-lg"></i> Tambah Manual
</button>
</div>
</div>
<!-- ── Filter ─────────────────────────────────────────────── -->
<div class="card" style="margin-bottom:16px;">
<div class="card-body" style="padding:14px;">
<form method="GET" style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end;">
<div class="search-bar" style="flex:1;min-width:200px;">
<i class="bi bi-search search-ico"></i>
<input type="text" class="form-control" name="search" placeholder="Cari nama, NIK, alamat..." value="<?= htmlspecialchars($search) ?>">
</div>
<div>
<select class="form-control" name="status">
<option value="">Semua Status</option>
<option value="belum" <?= $filterSt==='belum'?'selected':'' ?>>❌ Belum Dibantu</option>
<option value="menunggu" <?= $filterSt==='menunggu'?'selected':'' ?>>⏳ Menunggu</option>
<option value="sudah" <?= $filterSt==='sudah'?'selected':'' ?>>✅ Sudah Dibantu</option>
</select>
</div>
<div>
<select class="form-control" name="kecamatan">
<option value="">Semua Kecamatan</option>
<?php foreach ($kecList as $k): ?>
<option value="<?= htmlspecialchars($k) ?>" <?= $filterKec===$k?'selected':'' ?>><?= htmlspecialchars($k) ?></option>
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-primary btn-sm"><i class="bi bi-search"></i> Filter</button>
<a href="<?= BASE_URL ?>/admin/penduduk_miskin.php" class="btn btn-ghost btn-sm"><i class="bi bi-x"></i> Reset</a>
</form>
</div>
</div>
<!-- ── Table ───────────────────────────────────────────────── -->
<div class="card">
<div class="table-responsive">
<table class="admin-table">
<thead>
<tr>
<th>#</th>
<th>Nama KK</th>
<th>NIK</th>
<th>Anggota</th>
<th>Kecamatan</th>
<th>Status</th>
<th>Rumah Ibadah</th>
<th>Jarak</th>
<th>Bantuan</th>
<th>Nominal</th>
<th>Terdaftar</th>
<th class="td-actions">Aksi</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $i => $row): ?>
<tr>
<td style="color:var(--text3);font-size:11px;"><?= $offset+$i+1 ?></td>
<td style="font-weight:700;"><?= htmlspecialchars($row['kk_nama']) ?></td>
<td style="font-size:11px;color:var(--text3);font-family:monospace;"><?= htmlspecialchars($row['nik']?: '—') ?></td>
<td style="text-align:center;font-weight:700;"><?= $row['jumlah_anggota'] ?></td>
<td style="font-size:12px;color:var(--text2);"><?= htmlspecialchars($row['kecamatan']?: '—') ?></td>
<td><span class="badge-status badge-<?= $row['status_bantuan'] ?>"><?= ucfirst($row['status_bantuan']) ?></span></td>
<td style="font-size:11px;color:var(--text2);"><?= htmlspecialchars($row['ibadah_nama'] ?: '—') ?></td>
<td style="font-size:11px;color:var(--accent);"><?= $row['jarak_ke_ibadah'] ? round($row['jarak_ke_ibadah']).' m' : '—' ?></td>
<td style="font-size:11px;color:var(--text2);"><?= htmlspecialchars($row['jenis_bantuan'] ?: '—') ?></td>
<td style="font-size:11px;color:var(--green);font-family:monospace;"><?= $row['nominal_bantuan'] ? 'Rp '.number_format($row['nominal_bantuan'],0,',','.') : '—' ?></td>
<td style="font-size:11px;color:var(--text3);"><?= formatTanggal($row['created_at']) ?></td>
<td class="td-actions">
<button class="btn btn-ghost btn-sm" onclick='editMiskinRow(<?= htmlspecialchars(json_encode($row)) ?>)' title="Edit">
<i class="bi bi-pencil-fill"></i>
</button>
<?php if ($row['bukti_file']): ?>
<a href="<?= BASE_URL ?>/assets/uploads/<?= htmlspecialchars($row['bukti_file']) ?>" target="_blank" class="btn btn-ghost btn-sm" title="Lihat Bukti">
<i class="bi bi-paperclip"></i>
</a>
<?php endif; ?>
<button class="btn btn-danger btn-sm" onclick="deleteMiskin(<?= $row['id'] ?>, '<?= addslashes($row['kk_nama']) ?>')" title="Hapus">
<i class="bi bi-trash-fill"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
<?php if (empty($rows)): ?>
<tr><td colspan="11" style="text-align:center;padding:32px;color:var(--text3);">
<div style="font-size:28px;margin-bottom:6px;">👤</div>Tidak ada data ditemukan
</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Pagination -->
<?php if ($totalPages > 1): ?>
<div style="padding:14px 18px;border-top:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;">
<div style="font-size:12px;color:var(--text2);">Menampilkan <?= $offset+1 ?><?= min($offset+$perPage,$total) ?> dari <?= $total ?> data</div>
<div class="pagination">
<?php
$base = BASE_URL . '/admin/penduduk_miskin.php?' . http_build_query(array_filter(['search'=>$search,'status'=>$filterSt,'kecamatan'=>$filterKec]));
for ($p = 1; $p <= $totalPages; $p++):
?>
<a href="<?= $base ?>&page=<?= $p ?>" class="page-btn <?= $p===$page?'active':'' ?>"><?= $p ?></a>
<?php endfor; ?>
</div>
</div>
<?php endif; ?>
</div>
<!-- ── Modal Tambah/Edit Penduduk ─────────────────────────── -->
<div class="modal fade" id="modal-miskin" tabindex="-1">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="miskin-modal-title">👤 Tambah Penduduk Miskin</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="m-edit-id">
<div class="section-label">📍 Lokasi</div>
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group">
<label class="form-label">Latitude <span style="color:var(--red)">*</span></label>
<input class="form-control" id="m-lat" type="number" step="any" placeholder="-0.0263">
</div>
<div class="form-group">
<label class="form-label">Longitude <span style="color:var(--red)">*</span></label>
<input class="form-control" id="m-lng" type="number" step="any" placeholder="109.3425">
</div>
</div>
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group"><label class="form-label">Kelurahan</label><input class="form-control" id="m-kelurahan" placeholder="Kelurahan"></div>
<div class="form-group"><label class="form-label">Kecamatan</label><input class="form-control" id="m-kecamatan" placeholder="Kecamatan"></div>
</div>
<div class="form-group" style="margin-bottom:14px;">
<label class="form-label">Alamat Lengkap</label>
<input class="form-control" id="m-alamat" placeholder="Jl. xxx No. xxx">
</div>
<div class="section-label">👨‍👩‍👧 Data Keluarga</div>
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group"><label class="form-label">Nama KK <span style="color:var(--red)">*</span></label><input class="form-control" id="m-kk-nama" placeholder="Nama kepala keluarga"></div>
<div class="form-group"><label class="form-label">NIK KK</label><input class="form-control" id="m-nik" placeholder="16 digit" maxlength="16"></div>
</div>
<div class="form-group" style="margin-bottom:14px;">
<label class="form-label">Jumlah Anggota Keluarga</label>
<input class="form-control" id="m-jumlah" type="number" min="1" value="1" style="max-width:160px;">
</div>
<div class="section-label">👥 Detail Anggota</div>
<div id="m-anggota-container"></div>
<button type="button" class="btn btn-outline-accent btn-sm" onclick="addMAnggota()" style="width:100%;margin-bottom:14px;">
<i class="bi bi-plus-circle"></i> Tambah Anggota
</button>
<div class="section-label">🤝 Status Bantuan</div>
<div class="form-group">
<label class="form-label">Status <span style="color:var(--red)">*</span></label>
<select class="form-control" id="m-status" onchange="toggleMBantuan()">
<option value="belum">❌ Belum Dibantu</option>
<option value="menunggu">⏳ Menunggu Verifikasi</option>
<option value="sudah">✅ Sudah Dibantu</option>
</select>
</div>
<div id="m-bantuan-fields" style="display:none;">
<div class="form-row" style="margin-bottom:12px;">
<div class="form-group"><label class="form-label">Jenis Bantuan</label><input class="form-control" id="m-jenis-bantuan" placeholder="Mis. Sembako"></div>
<div class="form-group"><label class="form-label">Tanggal Bantuan</label><input class="form-control" id="m-tanggal" type="date"></div>
</div>
<div class="form-group">
<label class="form-label">💰 Nominal Bantuan (Rupiah)</label>
<div style="position:relative;">
<span style="position:absolute;left:12px;top:50%;transform:translateY(-50%);font-size:12px;color:var(--text2);font-weight:700;pointer-events:none;">Rp</span>
<input class="form-control" id="m-nominal"
type="text" inputmode="numeric"
placeholder="0"
style="padding-left:36px;font-family:'JetBrains Mono',monospace;"
oninput="formatNominalInput(this)">
</div>
<div style="font-size:10px;color:var(--text3);margin-top:4px;">Kosongkan jika belum ditentukan nominal</div>
</div>
</div>
<div class="section-label">📎 Bukti (Opsional)</div>
<div class="form-group">
<input type="file" class="form-control" id="m-bukti" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx">
<div class="form-hint">JPG, PNG, PDF, DOC maks 10 MB. Biarkan kosong jika tidak diubah.</div>
<div id="m-bukti-info" style="margin-top:6px;font-size:11px;color:var(--text2);"></div>
</div>
<div class="section-label">🏛️ Rumah Ibadah (opsional override)</div>
<div class="form-group">
<label class="form-label">Rumah Ibadah Terdekat (akan dipilih otomatis)</label>
<select class="form-control" id="m-ibadah-id">
<option value="">— Otomatis dari koordinat —</option>
<?php foreach ($ibadahList as $ib): ?>
<option value="<?= $ib['id'] ?>"><?= htmlspecialchars($ib['nama']) ?> (<?= $ib['jenis'] ?>)</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-ghost" data-bs-dismiss="modal"><i class="bi bi-x-lg"></i> Batal</button>
<button type="button" class="btn btn-success" id="btn-save-m"><i class="bi bi-floppy2-fill"></i> Simpan</button>
</div>
</div>
</div>
</div>
<script>
const BASE = '<?= BASE_URL ?>';
let mAIdx = 0;
function formatNominalInput(el) {
let raw = el.value.replace(/[^0-9]/g, '');
el.value = raw.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
}
function openMiskinModal() { fillMModal(null); }
function editMiskinRow(data) { fillMModal(data); }
function fillMModal(data) {
const isEdit = data !== null;
document.getElementById('miskin-modal-title').textContent = isEdit ? '✏️ Edit Penduduk Miskin' : '👤 Tambah Penduduk Miskin';
document.getElementById('m-edit-id').value = isEdit ? data.id : '';
document.getElementById('m-kk-nama').value = isEdit ? data.kk_nama : '';
document.getElementById('m-nik').value = isEdit ? (data.nik||'') : '';
document.getElementById('m-jumlah').value = isEdit ? (data.jumlah_anggota||1) : 1;
document.getElementById('m-lat').value = isEdit ? data.lat : '';
document.getElementById('m-lng').value = isEdit ? data.lng : '';
document.getElementById('m-kelurahan').value = isEdit ? (data.kelurahan||'') : '';
document.getElementById('m-kecamatan').value = isEdit ? (data.kecamatan||'') : '';
document.getElementById('m-alamat').value = isEdit ? (data.alamat||'') : '';
document.getElementById('m-status').value = isEdit ? data.status_bantuan : 'belum';
document.getElementById('m-jenis-bantuan').value = isEdit ? (data.jenis_bantuan||'') : '';
document.getElementById('m-tanggal').value = isEdit ? (data.tanggal_bantuan||'') : '';
// Set nominal bantuan
const nomEl = document.getElementById('m-nominal');
if (nomEl) {
const rawNom = isEdit && data.nominal_bantuan ? parseFloat(data.nominal_bantuan) : '';
nomEl.value = rawNom !== '' ? rawNom.toLocaleString('id-ID', {maximumFractionDigits:0}) : '';
}
document.getElementById('m-ibadah-id').value = isEdit ? (data.rumah_ibadah_id||'') : '';
document.getElementById('m-bukti').value = '';
document.getElementById('m-bukti-info').innerHTML = isEdit && data.bukti_file
? `📎 File saat ini: <a href="${BASE}/assets/uploads/${data.bukti_file}" target="_blank" style="color:var(--accent)">${data.bukti_file}</a>`
: '';
document.getElementById('m-anggota-container').innerHTML = '';
mAIdx = 0;
if (isEdit && data.id) loadMAnggota(data.id);
toggleMBantuan();
new bootstrap.Modal(document.getElementById('modal-miskin')).show();
}
function toggleMBantuan() {
const statusVal = document.getElementById('m-status').value;
document.getElementById('m-bantuan-fields').style.display =
(statusVal === 'sudah' || statusVal === 'menunggu') ? 'block' : 'none';
}
async function loadMAnggota(pid) {
try {
const r = await fetch(`${BASE}/api/anggota_keluarga.php?penduduk_id=${pid}`);
const js = await r.json();
if (js.success) js.data.forEach(a => addMAnggota(a));
} catch{}
}
function addMAnggota(data = null) {
mAIdx++;
const c = document.getElementById('m-anggota-container');
const d = document.createElement('div');
d.className = 'anggota-row'; d.id = `ma-row-${mAIdx}`;
d.innerHTML = `
<div class="anggota-row-header">
<span class="anggota-num">Anggota ${mAIdx}</span>
<button class="btn-remove-anggota" type="button" onclick="document.getElementById('ma-row-${mAIdx}').remove()">✕</button>
</div>
<div class="form-row">
<div class="form-group" style="margin-bottom:0"><label class="form-label">Nama *</label><input class="form-control ma-nama" id="ma-nama-${mAIdx}" value="${data?.nama||''}"></div>
<div class="form-group" style="margin-bottom:0"><label class="form-label">Hubungan</label><input class="form-control" id="ma-hub-${mAIdx}" value="${data?.hubungan||''}"></div>
</div>
<div class="form-row" style="margin-top:8px">
<div class="form-group" style="margin-bottom:0"><label class="form-label">Umur</label><input class="form-control" id="ma-umur-${mAIdx}" type="number" min="0" value="${data?.umur||''}"></div>
<div class="form-group" style="margin-bottom:0"><label class="form-label">Pekerjaan</label><input class="form-control" id="ma-pkj-${mAIdx}" value="${data?.pekerjaan||''}"></div>
</div>
<div class="form-group" style="margin-top:8px;margin-bottom:0">
<label class="form-label">Keterangan</label>
<input class="form-control" id="ma-ket-${mAIdx}" placeholder="Catatan tambahan (opsional)" value="${data?.keterangan||''}">
</div>`;
c.appendChild(d);
}
function collectMAnggota() {
const rows = document.querySelectorAll('.ma-nama');
return Array.from(rows).map(el => {
const idx = el.id.replace('ma-nama-','');
return {
nama: el.value.trim(),
hubungan: document.getElementById(`ma-hub-${idx}`)?.value || '',
umur: document.getElementById(`ma-umur-${idx}`)?.value || '',
pekerjaan: document.getElementById(`ma-pkj-${idx}`)?.value || '',
keterangan: document.getElementById(`ma-ket-${idx}`)?.value || ''
};
}).filter(a => a.nama);
}
document.getElementById('btn-save-m').addEventListener('click', async function() {
const nama = document.getElementById('m-kk-nama').value.trim();
const lat = document.getElementById('m-lat').value;
const lng = document.getElementById('m-lng').value;
if (!nama || !lat || !lng) { Swal.fire('Error','Nama KK, Lat, dan Lng wajib diisi!','error'); return; }
const editId = document.getElementById('m-edit-id').value;
const fd = new FormData();
fd.append('kk_nama', nama);
fd.append('nik', document.getElementById('m-nik').value);
fd.append('jumlah_anggota', document.getElementById('m-jumlah').value||1);
fd.append('lat', lat); fd.append('lng', lng);
fd.append('kelurahan', document.getElementById('m-kelurahan').value);
fd.append('kecamatan', document.getElementById('m-kecamatan').value);
fd.append('alamat', document.getElementById('m-alamat').value);
fd.append('status_bantuan', document.getElementById('m-status').value);
fd.append('jenis_bantuan', document.getElementById('m-jenis-bantuan').value);
fd.append('tanggal_bantuan', document.getElementById('m-tanggal').value);
// Nominal bantuan: hapus titik pemisah ribuan sebelum kirim
const nomRaw = (document.getElementById('m-nominal')?.value || '').replace(/\./g,'');
fd.append('nominal_bantuan', nomRaw);
fd.append('anggota', JSON.stringify(collectMAnggota()));
const buktiInput = document.getElementById('m-bukti');
if (buktiInput.files[0]) fd.append('bukti_file', buktiInput.files[0]);
this.disabled = true; this.textContent = '⏳ Menyimpan...';
try {
const url = editId ? `${BASE}/api/penduduk_miskin.php?id=${editId}` : `${BASE}/api/penduduk_miskin.php`;
const resp = await fetch(url, { method: editId?'PUT':'POST', body: fd });
const json = await resp.json();
if (json.success) { location.reload(); } else { Swal.fire('Error', json.message, 'error'); }
} catch { Swal.fire('Error','Gagal menyimpan','error'); }
finally { this.disabled = false; this.innerHTML = '<i class="bi bi-floppy2-fill"></i> Simpan'; }
});
async function deleteMiskin(id, nama) {
const res = await Swal.fire({
title: 'Hapus Penduduk?', html: `Data <b>${nama}</b> akan dihapus permanen.`,
icon: 'warning', showCancelButton: true, confirmButtonText: 'Ya, Hapus!',
cancelButtonText: 'Batal', confirmButtonColor: '#ef4444',
background: '#161b27', color: '#e8ecf4'
});
if (!res.isConfirmed) return;
try {
const r = await fetch(`${BASE}/api/penduduk_miskin.php?id=${id}`, { method:'DELETE' });
const js = await r.json();
if (js.success) { Swal.fire({title:'Terhapus!',icon:'success',timer:1500,background:'#161b27',color:'#e8ecf4'}).then(()=>location.reload()); }
else Swal.fire('Error', js.message, 'error');
} catch { Swal.fire('Error','Gagal menghapus','error'); }
}
</script>
<?php require_once __DIR__ . '/../includes/footer.php'; ?>