Files
UAS_WEBGIS_IhyaUlumuddinHar…/project_final/admin/data_warga.php
T
2026-06-13 11:24:58 +07:00

325 lines
22 KiB
PHP
Raw 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.
<?php
require_once __DIR__ . '/../config/db.php';
require_once __DIR__ . '/../config/auth_check.php';
requireAnyRole(['operator']);
$pdo = Database::getConnection();
$rows = $pdo->query("
SELECT w.*, wl.nama AS wilayah_nama,
(SELECT GROUP_CONCAT(jb.kode SEPARATOR ',')
FROM bantuan_sosial bs JOIN jenis_bantuan jb ON jb.id=bs.jenis_bantuan_id
WHERE bs.warga_id=w.id AND bs.is_aktif=1) AS bantuan_aktif,
(SELECT COUNT(*) FROM foto_rumah f WHERE f.warga_id=w.id) AS jumlah_foto
FROM warga_miskin w
LEFT JOIN wilayah wl ON wl.id=w.wilayah_id
ORDER BY w.id DESC
")->fetchAll();
$wilayahList = $pdo->query("SELECT id, nama FROM wilayah ORDER BY nama")->fetchAll();
$jenisList = $pdo->query("SELECT id, kode, nama FROM jenis_bantuan ORDER BY id")->fetchAll();
$pageTitle = 'Data Rumah Tangga Miskin'; $activeNav = 'warga';
$extraHead = '<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">';
require_once __DIR__ . '/partials/header.php';
function verifBadge($s) {
return match ($s) {
'terverifikasi' => '<span class="badge badge-success">✔ Terverifikasi</span>',
'perlu_tinjauan' => '<span class="badge badge-warning">⚠ Perlu Tinjauan</span>',
default => '<span class="badge" style="background:#F3F4F6;color:#6B7280;">○ Belum</span>',
};
}
?>
<div class="page-header">
<div class="breadcrumb"><span>Operator</span><span class="sep">/</span><span>Rumah Tangga Miskin</span></div>
<h1>👥 Data Rumah Tangga Miskin</h1>
<p>Kelola data lengkap rumah tangga, status bantuan, foto kondisi rumah, dan verifikasi (KF-12 s/d 18).</p>
</div>
<div class="page-toolbar" style="flex-wrap:wrap;gap:10px;">
<div class="search-box"><i class="fas fa-search search-icon"></i><input type="text" id="searchInput" placeholder="Cari nama / NIK..." oninput="filterTable('searchInput','wargaTable')"></div>
<select id="filterVerif" class="form-control" style="max-width:180px;" onchange="applyVerifFilter()">
<option value="">Semua status</option>
<option value="belum">Belum</option>
<option value="terverifikasi">Terverifikasi</option>
<option value="perlu_tinjauan">Perlu Tinjauan</option>
</select>
<button class="btn btn-ghost" onclick="bulkVerify()"><i class="fas fa-check-double"></i> Verifikasi Terpilih</button>
<a class="btn btn-ghost" href="<?= app_url('api/warga_export.php?format=csv') ?>"><i class="fas fa-file-csv"></i> Export CSV</a>
<a class="btn btn-ghost" href="<?= app_url('api/warga_export.php?format=print') ?>" target="_blank"><i class="fas fa-print"></i> Cetak/PDF</a>
<button class="btn btn-ghost" onclick="openModal('modalImport')"><i class="fas fa-file-import"></i> Import CSV</button>
<button class="btn btn-primary" onclick="openTambah()"><i class="fas fa-plus"></i> Tambah Data</button>
</div>
<div class="table-wrap">
<table class="data-table" id="wargaTable">
<thead><tr>
<th><input type="checkbox" id="checkAll" onclick="toggleAll(this)"></th>
<th>Nama KK</th><th>NIK</th><th>Kelurahan</th><th>Penghasilan</th>
<th>Jiwa</th><th>Bantuan</th><th>Foto</th><th>Verifikasi</th><th>Aksi</th>
</tr></thead>
<tbody>
<?php foreach ($rows as $r): ?>
<tr data-verif="<?= $r['status_verifikasi'] ?>">
<td><input type="checkbox" class="rowCheck" value="<?= $r['id'] ?>"></td>
<td><strong><?= htmlspecialchars($r['nama_kk']) ?></strong></td>
<td style="font-size:0.82rem;color:var(--text-muted)"><?= htmlspecialchars($r['nik_kk'] ?: '-') ?></td>
<td style="font-size:0.85rem"><?= htmlspecialchars($r['wilayah_nama'] ?: '-') ?></td>
<td>Rp <?= number_format($r['penghasilan'],0,',','.') ?></td>
<td><?= $r['jumlah_jiwa'] ?: ($r['jumlah_tanggungan'] ?: '-') ?></td>
<td style="font-size:0.78rem"><?= $r['bantuan_aktif'] ? '<span class="badge badge-info">'.str_replace(',','</span> <span class="badge badge-info">',htmlspecialchars($r['bantuan_aktif'])).'</span>' : '<span style="color:#9CA3AF">—</span>' ?></td>
<td><?= $r['jumlah_foto'] > 0 ? '📷 '.$r['jumlah_foto'] : '<span style="color:#9CA3AF">—</span>' ?></td>
<td><?= verifBadge($r['status_verifikasi']) ?></td>
<td><div class="actions">
<button class="btn btn-ghost btn-sm btn-icon" title="Kelola (bantuan & foto)" onclick="openDetail(<?= $r['id'] ?>,'<?= addslashes($r['nama_kk']) ?>')"><i class="fas fa-folder-open"></i></button>
<button class="btn btn-ghost btn-sm btn-icon" title="Edit" onclick='editRow(<?= json_encode($r, JSON_HEX_APOS|JSON_HEX_QUOT) ?>)'><i class="fas fa-edit"></i></button>
<button class="btn btn-danger btn-sm btn-icon" title="Hapus" onclick="hapus(<?= $r['id'] ?>,'<?= addslashes($r['nama_kk']) ?>')"><i class="fas fa-trash"></i></button>
</div></td>
</tr>
<?php endforeach; ?>
<?php if (!$rows): ?><tr><td colspan="10" style="text-align:center;padding:40px;color:var(--text-muted);">Belum ada data.</td></tr><?php endif; ?>
</tbody>
</table>
</div>
<!-- ===== Modal Tambah/Edit ===== -->
<div class="modal-overlay" id="modalForm"><div class="modal" style="max-width:680px;">
<div class="modal-header"><span class="modal-title" id="formTitle">Tambah Data</span><button class="modal-close" onclick="closeModal('modalForm')">×</button></div>
<div style="max-height:70vh;overflow-y:auto;padding-right:6px;">
<input type="hidden" id="fId">
<h4 style="margin:4px 0 8px;font-size:0.85rem;color:#2563EB;">Identitas</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Nama KK *</label><input id="fNama" class="form-control" required></div>
<div class="form-group"><label class="form-label">NIK KK</label><input id="fNik" class="form-control" maxlength="16" placeholder="16 digit"></div>
</div>
<div class="form-group"><label class="form-label">Alamat</label><input id="fAlamat" class="form-control"></div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Kelurahan</label>
<select id="fWilayah" class="form-control"><option value="">- pilih -</option>
<?php foreach ($wilayahList as $w): ?><option value="<?= $w['id'] ?>"><?= htmlspecialchars($w['nama']) ?></option><?php endforeach; ?>
</select></div>
<div class="form-group"><label class="form-label">RT</label><input id="fRt" class="form-control"></div>
<div class="form-group"><label class="form-label">RW</label><input id="fRw" class="form-control"></div>
</div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Demografi & Ekonomi</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Jumlah Jiwa</label><input id="fJiwa" type="number" min="0" class="form-control"></div>
<div class="form-group"><label class="form-label">Jumlah Tanggungan</label><input id="fTanggungan" type="number" min="0" class="form-control"></div>
<div class="form-group"><label class="form-label">Pekerjaan KK</label><input id="fPekerjaan" class="form-control"></div>
<div class="form-group"><label class="form-label">Penghasilan/bln (Rp)</label><input id="fPenghasilan" type="number" min="0" class="form-control"></div>
</div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Kondisi Rumah</h4>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div class="form-group"><label class="form-label">Sumber Air</label>
<select id="fAir" class="form-control"><option value="">-</option><option>SUMUR_BOR</option><option>PDAM</option><option>SUNGAI</option><option>HUJAN</option><option>TIDAK_ADA</option></select></div>
<div class="form-group"><label class="form-label">Listrik</label>
<select id="fListrik" class="form-control"><option value="">-</option><option>PLN</option><option>NON_PLN</option><option>TIDAK_ADA</option></select></div>
<div class="form-group"><label class="form-label">Material Dinding</label>
<select id="fDinding" class="form-control"><option value="">-</option><option>PERMANEN</option><option>SEMI_PERMANEN</option><option>TIDAK_PERMANEN</option></select></div>
<div class="form-group"><label class="form-label">Status Kepemilikan</label>
<select id="fKepemilikan" class="form-control"><option value="">-</option><option>MILIK</option><option>SEWA</option><option>NUMPANG</option><option>LAINNYA</option></select></div>
</div>
<div class="form-group"><label class="form-label">Status Verifikasi</label>
<select id="fVerif" class="form-control"><option value="belum">Belum</option><option value="terverifikasi">Terverifikasi</option><option value="perlu_tinjauan">Perlu Tinjauan</option></select></div>
<h4 style="margin:10px 0 8px;font-size:0.85rem;color:#2563EB;">Lokasi *</h4>
<div id="miniMap" style="height:220px;border-radius:8px;border:1px solid #E5E7EB;margin-bottom:10px;z-index:1;"></div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<input id="fLat" class="form-control" placeholder="Latitude" readonly style="background:#F9FAFB;">
<input id="fLng" class="form-control" placeholder="Longitude" readonly style="background:#F9FAFB;">
</div>
</div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalForm')">Batal</button><button class="btn btn-primary" onclick="simpan()"><i class="fas fa-save"></i> Simpan</button></div>
</div></div>
<!-- ===== Modal Detail: Bantuan & Foto ===== -->
<div class="modal-overlay" id="modalDetail"><div class="modal" style="max-width:640px;">
<div class="modal-header"><span class="modal-title" id="detailTitle">Kelola</span><button class="modal-close" onclick="closeModal('modalDetail')">×</button></div>
<div style="max-height:70vh;overflow-y:auto;">
<h4 style="font-size:0.9rem;margin-bottom:8px;">💳 Status Bantuan</h4>
<div id="bantuanList" style="margin-bottom:10px;"></div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:8px;align-items:end;background:#F9FAFB;padding:10px;border-radius:8px;">
<div><label class="form-label" style="font-size:0.75rem">Jenis</label>
<select id="bJenis" class="form-control">
<?php foreach ($jenisList as $j): ?><option value="<?= $j['id'] ?>"><?= htmlspecialchars($j['kode']) ?></option><?php endforeach; ?>
</select></div>
<div><label class="form-label" style="font-size:0.75rem">Mulai</label><input id="bMulai" type="date" class="form-control"></div>
<div><label class="form-label" style="font-size:0.75rem">Berakhir</label><input id="bBerakhir" type="date" class="form-control"></div>
<button class="btn btn-primary btn-sm" onclick="addBantuan()"><i class="fas fa-plus"></i></button>
</div>
<h4 style="font-size:0.9rem;margin:16px 0 8px;">📷 Foto Kondisi Rumah (maks 5)</h4>
<div id="fotoGallery" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;"></div>
<input type="file" id="fotoInput" accept="image/*" multiple style="font-size:0.85rem;">
<button class="btn btn-primary btn-sm" onclick="uploadFoto()"><i class="fas fa-upload"></i> Unggah</button>
</div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalDetail')">Tutup</button></div>
</div></div>
<!-- ===== Modal Import ===== -->
<div class="modal-overlay" id="modalImport"><div class="modal">
<div class="modal-header"><span class="modal-title">📥 Import CSV</span><button class="modal-close" onclick="closeModal('modalImport')">×</button></div>
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:10px;">Format kolom (Lampiran A): <code>nik_kk, nama_kk, kode_kelurahan, latitude, longitude, jumlah_jiwa, sumber_air, status_listrik, material_dinding, pekerjaan_kk</code></p>
<a href="<?= app_url('api/warga_export.php?format=template') ?>" class="btn btn-ghost btn-sm" style="margin-bottom:12px;"><i class="fas fa-download"></i> Unduh Template CSV</a>
<div class="form-group"><input type="file" id="importFile" accept=".csv" class="form-control"></div>
<div id="importResult" style="font-size:0.85rem;"></div>
<div class="modal-footer"><button class="btn btn-ghost" onclick="closeModal('modalImport')">Batal</button><button class="btn btn-primary" onclick="doImport()"><i class="fas fa-file-import"></i> Proses</button></div>
</div></div>
<?php $extraScript = <<<'JS'
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API = APP_BASE + '/api/warga_miskin.php';
const API_BANTUAN = APP_BASE + '/api/bantuan.php';
const API_FOTO = APP_BASE + '/api/foto.php';
let miniMap=null, marker=null, currentDetailId=null;
function ensureMap(lat=-0.0500, lng=109.3450){
setTimeout(()=>{
if(!miniMap){
miniMap=L.map('miniMap').setView([lat,lng],13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{subdomains:'abcd'}).addTo(miniMap);
miniMap.on('click',e=>setMarker(e.latlng.lat,e.latlng.lng));
} else { miniMap.invalidateSize(); miniMap.setView([lat,lng],13); }
},120);
}
function setMarker(lat,lng){
if(!marker) marker=L.marker([lat,lng]).addTo(miniMap); else marker.setLatLng([lat,lng]);
document.getElementById('fLat').value=lat.toFixed(6);
document.getElementById('fLng').value=lng.toFixed(6);
}
function clearForm(){
['fId','fNama','fNik','fAlamat','fRt','fRw','fJiwa','fTanggungan','fPekerjaan','fPenghasilan','fLat','fLng'].forEach(i=>document.getElementById(i).value='');
['fWilayah','fAir','fListrik','fDinding','fKepemilikan'].forEach(i=>document.getElementById(i).value='');
document.getElementById('fVerif').value='belum';
if(marker&&miniMap){miniMap.removeLayer(marker);marker=null;}
}
function openTambah(){ clearForm(); document.getElementById('formTitle').textContent='Tambah Data'; openModal('modalForm'); ensureMap(); }
function editRow(r){
clearForm();
document.getElementById('formTitle').textContent='Edit: '+r.nama_kk;
document.getElementById('fId').value=r.id;
document.getElementById('fNama').value=r.nama_kk||'';
document.getElementById('fNik').value=r.nik_kk||'';
document.getElementById('fAlamat').value=r.alamat||'';
document.getElementById('fWilayah').value=r.wilayah_id||'';
document.getElementById('fRt').value=r.rt||''; document.getElementById('fRw').value=r.rw||'';
document.getElementById('fJiwa').value=r.jumlah_jiwa||''; document.getElementById('fTanggungan').value=r.jumlah_tanggungan||'';
document.getElementById('fPekerjaan').value=r.pekerjaan_kk||''; document.getElementById('fPenghasilan').value=r.penghasilan||'';
document.getElementById('fAir').value=r.sumber_air||''; document.getElementById('fListrik').value=r.status_listrik||'';
document.getElementById('fDinding').value=r.material_dinding||''; document.getElementById('fKepemilikan').value=r.status_kepemilikan||'';
document.getElementById('fVerif').value=r.status_verifikasi||'belum';
openModal('modalForm');
ensureMap();
}
function payload(){
return {
nama_kk:document.getElementById('fNama').value.trim(),
nik_kk:document.getElementById('fNik').value.trim(),
alamat:document.getElementById('fAlamat').value.trim(),
wilayah_id:document.getElementById('fWilayah').value||null,
rt:document.getElementById('fRt').value.trim(), rw:document.getElementById('fRw').value.trim(),
jumlah_jiwa:document.getElementById('fJiwa').value||null,
jumlah_tanggungan:parseInt(document.getElementById('fTanggungan').value)||0,
pekerjaan_kk:document.getElementById('fPekerjaan').value.trim(),
penghasilan:parseFloat(document.getElementById('fPenghasilan').value)||0,
sumber_air:document.getElementById('fAir').value,
status_listrik:document.getElementById('fListrik').value,
material_dinding:document.getElementById('fDinding').value,
status_kepemilikan:document.getElementById('fKepemilikan').value,
status_verifikasi:document.getElementById('fVerif').value
};
}
async function simpan(){
const id=document.getElementById('fId').value;
const p=payload();
if(!p.nama_kk) return showToast('Nama KK wajib!','error');
const lat=parseFloat(document.getElementById('fLat').value), lng=parseFloat(document.getElementById('fLng').value);
if(id){
const res=await fetch(API+'?id='+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
const d=await res.json();
if(d.status==='success'){ showToast('Diperbarui!'); setTimeout(()=>location.reload(),800);} else showToast(d.message,'error');
} else {
if(isNaN(lat)||isNaN(lng)) return showToast('Pilih lokasi di peta!','error');
p.geometry={type:'Point',coordinates:[lng,lat]};
const res=await fetch(API,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(p)});
const d=await res.json();
if(d.status==='success'){ showToast('Data disimpan!'); setTimeout(()=>location.reload(),800);} else showToast(d.message,'error');
}
}
async function hapus(id,nama){ if(!confirm(`Hapus data "${nama}"?`))return;
const res=await fetch(API+'?id='+id,{method:'DELETE'}); const d=await res.json();
if(d.status==='success'){showToast('Dihapus!');setTimeout(()=>location.reload(),700);}else showToast(d.message,'error'); }
// ---- Verifikasi ----
function applyVerifFilter(){
const v=document.getElementById('filterVerif').value;
document.querySelectorAll('#wargaTable tbody tr').forEach(tr=>{
tr.style.display=(!v||tr.dataset.verif===v)?'':'none';
});
}
function toggleAll(c){ document.querySelectorAll('.rowCheck').forEach(x=>x.checked=c.checked); }
async function bulkVerify(){
const ids=[...document.querySelectorAll('.rowCheck:checked')].map(x=>x.value);
if(!ids.length) return showToast('Pilih minimal satu baris','error');
if(!confirm(`Tandai ${ids.length} data sebagai Terverifikasi?`))return;
for(const id of ids){ await fetch(API+'?id='+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status_verifikasi:'terverifikasi'})}); }
showToast('Verifikasi massal selesai!'); setTimeout(()=>location.reload(),800);
}
// ---- Detail: bantuan & foto ----
async function openDetail(id,nama){
currentDetailId=id;
document.getElementById('detailTitle').textContent='Kelola: '+nama;
openModal('modalDetail');
loadBantuan(); loadFoto();
}
async function loadBantuan(){
const res=await fetch(API_BANTUAN+'?warga_id='+currentDetailId); const d=await res.json();
const box=document.getElementById('bantuanList');
if(!d.data||!d.data.length){ box.innerHTML='<p style="color:#9CA3AF;font-size:0.85rem">Belum ada bantuan.</p>'; return; }
box.innerHTML=d.data.map(b=>`<div style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;border:1px solid #E5E7EB;border-radius:8px;margin-bottom:6px;">
<div><strong>${b.kode}</strong> <span style="font-size:0.78rem;color:#6B7280">${b.nama}</span>
<div style="font-size:0.72rem;color:#9CA3AF">${b.tgl_mulai||'-'} s/d ${b.tgl_berakhir||'-'}</div></div>
<button class="btn btn-danger btn-sm btn-icon" onclick="delBantuan(${b.id})"><i class="fas fa-trash"></i></button></div>`).join('');
}
async function addBantuan(){
const body={warga_id:currentDetailId,jenis_bantuan_id:document.getElementById('bJenis').value,tgl_mulai:document.getElementById('bMulai').value,tgl_berakhir:document.getElementById('bBerakhir').value};
const res=await fetch(API_BANTUAN,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const d=await res.json(); if(d.status==='success'){showToast('Bantuan ditambah');loadBantuan();}else showToast(d.message,'error');
}
async function delBantuan(id){ const res=await fetch(API_BANTUAN+'?id='+id,{method:'DELETE'}); const d=await res.json(); if(d.status==='success'){showToast('Dihapus');loadBantuan();} }
async function loadFoto(){
const res=await fetch(API_FOTO+'?warga_id='+currentDetailId); const d=await res.json();
const g=document.getElementById('fotoGallery');
if(!d.data||!d.data.length){ g.innerHTML='<p style="color:#9CA3AF;font-size:0.85rem">Belum ada foto.</p>'; return; }
g.innerHTML=d.data.map(f=>`<div style="position:relative;"><img src="${f.url}" style="width:90px;height:90px;object-fit:cover;border-radius:8px;border:1px solid #E5E7EB;">
<button onclick="delFoto(${f.id})" style="position:absolute;top:-6px;right:-6px;background:#EF4444;color:#fff;border:none;border-radius:50%;width:22px;height:22px;cursor:pointer;">×</button></div>`).join('');
}
async function uploadFoto(){
const inp=document.getElementById('fotoInput'); if(!inp.files.length) return showToast('Pilih foto dulu','error');
const fd=new FormData(); fd.append('warga_id',currentDetailId);
for(const f of inp.files) fd.append('foto[]',f);
const res=await fetch(API_FOTO,{method:'POST',body:fd}); const d=await res.json();
if(d.status==='success'){showToast(d.message);inp.value='';loadFoto();}else showToast(d.message,'error');
}
async function delFoto(id){ const res=await fetch(API_FOTO+'?id='+id,{method:'DELETE'}); const d=await res.json(); if(d.status==='success'){showToast('Foto dihapus');loadFoto();} }
// ---- Import ----
async function doImport(){
const inp=document.getElementById('importFile'); if(!inp.files.length) return showToast('Pilih file CSV','error');
const fd=new FormData(); fd.append('file',inp.files[0]);
document.getElementById('importResult').innerHTML='Memproses...';
const res=await fetch(APP_BASE+'/api/warga_import.php',{method:'POST',body:fd}); const d=await res.json();
if(d.status==='success'){
document.getElementById('importResult').innerHTML=`<div style="color:#059669">✔ ${d.data.berhasil} baris diimpor.</div>`+
(d.data.gagal?`<div style="color:#DC2626">${d.data.gagal} gagal:<br>${d.data.errors.join('<br>')}</div>`:'');
setTimeout(()=>location.reload(),1800);
} else { document.getElementById('importResult').innerHTML=`<div style="color:#DC2626">${d.message}</div>`; }
}
</script>
JS; ?>
<?php require_once __DIR__ . '/partials/footer.php'; ?>