Files
2026-06-13 11:24:58 +07:00

210 lines
13 KiB
PHP

<?php
/**
* partials/analisis_body.php — Konten halaman Analisis Spasial (Fase E).
* Di-include oleh admin/analisis.php & pimpinan/analisis.php (butuh $pdo).
* Header sudah memuat Leaflet CSS + APP_BASE. Chart.js & Leaflet JS dimuat di bawah.
*/
$wilOpt = $pdo->query("SELECT id, nama FROM wilayah ORDER BY nama")->fetchAll();
$jenisOpt = $pdo->query("SELECT id, kode FROM jenis_bantuan ORDER BY id")->fetchAll();
?>
<div class="page-header">
<div class="breadcrumb"><span>Analisis</span><span class="sep">/</span><span>Spasial</span></div>
<h1>📈 Analisis Spasial</h1>
<p>Filter multi-kriteria, zona prioritas, proximity, cakupan bantuan, dan tren (KF-19 s/d 23).</p>
</div>
<div class="an-tabs">
<button class="an-tab active" data-tab="filter" onclick="switchTab('filter')"><i class="fas fa-filter"></i> Filter</button>
<button class="an-tab" data-tab="zona" onclick="switchTab('zona')"><i class="fas fa-ranking-star"></i> Zona Prioritas</button>
<button class="an-tab" data-tab="proximity" onclick="switchTab('proximity')"><i class="fas fa-route"></i> Proximity</button>
<button class="an-tab" data-tab="cakupan" onclick="switchTab('cakupan')"><i class="fas fa-hand-holding-heart"></i> Cakupan Bantuan</button>
<button class="an-tab" data-tab="tren" onclick="switchTab('tren')"><i class="fas fa-chart-line"></i> Tren</button>
</div>
<!-- shared map (dipindah antar tab) -->
<div id="mapHolder" style="display:none;"><div id="anMap" style="height:460px;border-radius:12px;"></div></div>
<!-- TAB FILTER -->
<div class="an-pane" id="pane-filter">
<div class="an-grid">
<div class="card an-filter">
<h3>Kriteria</h3>
<label>Kelurahan</label>
<select id="fl-wilayah" class="form-control"><option value="">Semua</option>
<?php foreach ($wilOpt as $w): ?><option value="<?= $w['id'] ?>"><?= htmlspecialchars($w['nama']) ?></option><?php endforeach; ?>
</select>
<label>Sumber Air</label>
<select id="fl-air" class="form-control"><option value="">Semua</option><option>SUMUR_BOR</option><option>PDAM</option><option>SUNGAI</option><option>HUJAN</option><option>TIDAK_ADA</option></select>
<label>Material Dinding</label>
<select id="fl-dinding" class="form-control"><option value="">Semua</option><option>PERMANEN</option><option>SEMI_PERMANEN</option><option>TIDAK_PERMANEN</option></select>
<label>Status Kepemilikan</label>
<select id="fl-kepemilikan" class="form-control"><option value="">Semua</option><option>MILIK</option><option>SEWA</option><option>NUMPANG</option><option>LAINNYA</option></select>
<label>Status Bantuan</label>
<div style="display:flex;gap:6px;">
<select id="fl-bantuan" class="form-control"><option value="">—</option>
<?php foreach ($jenisOpt as $j): ?><option value="<?= $j['id'] ?>"><?= htmlspecialchars($j['kode']) ?></option><?php endforeach; ?>
</select>
<select id="fl-bmode" class="form-control" style="max-width:130px;"><option value="menerima">Menerima</option><option value="tidak">Tidak terima</option></select>
</div>
<label>Verifikasi</label>
<select id="fl-verif" class="form-control"><option value="">Semua</option><option value="belum">Belum</option><option value="terverifikasi">Terverifikasi</option><option value="perlu_tinjauan">Perlu Tinjauan</option></select>
<button class="btn btn-primary" style="width:100%;margin-top:12px;" onclick="runFilter()"><i class="fas fa-search"></i> Terapkan Filter</button>
<button class="btn btn-ghost" style="width:100%;margin-top:6px;" onclick="resetFilter()">Reset</button>
<div id="fl-count" class="an-count">—</div>
</div>
<div class="card" id="filterMapBox"><div class="an-maptarget"></div></div>
</div>
</div>
<!-- TAB ZONA -->
<div class="an-pane" id="pane-zona" style="display:none;">
<div class="an-grid">
<div class="card" id="zonaMapBox"><div class="an-maptarget"></div></div>
<div class="card">
<h3>Ranking Prioritas</h3>
<table class="data-table"><thead><tr><th>#</th><th>Kelurahan</th><th>% Miskin</th><th>Cakupan</th><th>Skor</th><th>Level</th></tr></thead>
<tbody id="zonaTable"></tbody></table>
</div>
</div>
</div>
<!-- TAB PROXIMITY -->
<div class="an-pane" id="pane-proximity" style="display:none;">
<div class="card" style="margin-bottom:14px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label style="margin:0;">Radius layanan (meter):</label>
<input type="number" id="px-radius" class="form-control" value="1000" style="max-width:140px;">
<button class="btn btn-primary" onclick="runProximity()"><i class="fas fa-route"></i> Hitung</button>
<div id="px-summary" class="an-count" style="margin:0;"></div>
</div>
<div class="card" id="proxMapBox"><div class="an-maptarget"></div></div>
</div>
<!-- TAB CAKUPAN -->
<div class="an-pane" id="pane-cakupan" style="display:none;">
<div class="an-grid">
<div class="card"><h3>Cakupan Keseluruhan</h3><canvas id="chartPie" height="220"></canvas><div id="cak-summary" class="an-count"></div></div>
<div class="card"><h3>Penerima per Jenis Bantuan</h3><canvas id="chartBar" height="220"></canvas></div>
</div>
<div class="card" style="margin-top:14px;">
<h3>Cakupan per Kelurahan</h3>
<table class="data-table"><thead><tr><th>Kelurahan</th><th>Miskin</th><th>Dibantu</th><th>Cakupan</th></tr></thead><tbody id="cakTable"></tbody></table>
</div>
</div>
<!-- TAB TREN -->
<div class="an-pane" id="pane-tren" style="display:none;">
<div class="card"><h3>Tren 12 Bulan Terakhir</h3><canvas id="chartTren" height="110"></canvas></div>
</div>
<style>
.an-tabs{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px;}
.an-tab{background:#fff;border:1px solid #E5E7EB;border-radius:10px;padding:9px 14px;font-size:0.85rem;font-weight:600;cursor:pointer;color:#374151;}
.an-tab.active{background:#2563EB;color:#fff;border-color:#2563EB;}
.an-grid{display:grid;grid-template-columns:1fr 1.6fr;gap:16px;}
.an-filter label{display:block;font-size:0.78rem;font-weight:600;margin:8px 0 3px;}
.an-filter h3,.card h3{font-size:0.95rem;margin-bottom:10px;}
.an-count{margin-top:12px;padding:10px;background:#F0F9FF;border-radius:8px;font-size:0.88rem;font-weight:600;color:#1D4ED8;text-align:center;}
.an-maptarget{min-height:460px;}
@media(max-width:900px){.an-grid{grid-template-columns:1fr;}}
</style>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
const ANAPI = APP_BASE + '/api';
let anMap, anLayer, loaded={}, charts={};
function initAnMap(){
if(anMap) return;
anMap = L.map('anMap').setView([-0.0500,109.3450],13);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',{subdomains:'abcd',maxZoom:19}).addTo(anMap);
}
function moveMapTo(boxId){
const box=document.querySelector('#'+boxId+' .an-maptarget');
const holder=document.getElementById('anMap');
box.appendChild(holder); holder.style.height='460px';
initAnMap(); setTimeout(()=>anMap.invalidateSize(),120);
}
function clearLayer(){ if(anLayer){anMap.removeLayer(anLayer);anLayer=null;} }
function switchTab(t){
document.querySelectorAll('.an-tab').forEach(b=>b.classList.toggle('active',b.dataset.tab===t));
document.querySelectorAll('.an-pane').forEach(p=>p.style.display='none');
document.getElementById('pane-'+t).style.display='';
if(t==='filter'){ moveMapTo('filterMapBox'); if(!loaded.filter){runFilter();loaded.filter=1;} }
if(t==='zona'){ moveMapTo('zonaMapBox'); if(!loaded.zona){loadZona();loaded.zona=1;} }
if(t==='proximity'){ moveMapTo('proxMapBox'); if(!loaded.prox){runProximity();loaded.prox=1;} }
if(t==='cakupan' && !loaded.cak){loadCakupan();loaded.cak=1;}
if(t==='tren' && !loaded.tren){loadTren();loaded.tren=1;}
}
// ---- Filter ----
async function runFilter(){
const q=new URLSearchParams();
const add=(k,id)=>{const v=document.getElementById(id).value;if(v)q.set(k,v);};
add('wilayah_id','fl-wilayah'); add('sumber_air','fl-air'); add('material_dinding','fl-dinding');
add('status_kepemilikan','fl-kepemilikan'); add('verif','fl-verif');
const bj=document.getElementById('fl-bantuan').value;
if(bj){ q.set('bantuan_jenis',bj); q.set('bantuan_mode',document.getElementById('fl-bmode').value); }
const d=(await (await fetch(`${ANAPI}/filter.php?`+q.toString())).json()).data;
clearLayer();
anLayer=L.geoJSON(d,{pointToLayer:(f,ll)=>L.circleMarker(ll,{radius:8,color:'#EF4444',fillColor:'#EF4444',fillOpacity:.7,weight:2}),
onEachFeature:(f,l)=>{const p=f.properties;l.bindPopup(`<b>${p.nama_kk}</b><br>${p.kelurahan||'-'}<br>Air: ${p.sumber_air||'-'} · Dinding: ${p.material_dinding||'-'}<br>Bantuan: ${p.bantuan_aktif||'Belum ada'}`);}}).addTo(anMap);
document.getElementById('fl-count').innerHTML=`Ditemukan <b>${d.jumlah}</b> rumah tangga`;
if(d.features.length){try{anMap.fitBounds(anLayer.getBounds(),{padding:[40,40]});}catch(e){}}
}
function resetFilter(){ ['fl-wilayah','fl-air','fl-dinding','fl-kepemilikan','fl-bantuan','fl-verif'].forEach(i=>document.getElementById(i).value=''); document.getElementById('fl-bmode').value='menerima'; runFilter(); }
// ---- Zona ----
const lvlColor={Tinggi:'#EF4444',Sedang:'#FB923C',Rendah:'#10B981'};
async function loadZona(){
const d=(await (await fetch(`${ANAPI}/zona_prioritas.php`)).json()).data;
clearLayer();
anLayer=L.geoJSON(d,{style:f=>({fillColor:lvlColor[f.properties.level]||'#9CA3AF',fillOpacity:.55,color:'#374151',weight:1.5}),
onEachFeature:(f,l)=>{const p=f.properties;l.bindPopup(`<b>#${p.rank} ${p.nama}</b><br>Skor: ${p.skor} (${p.level})<br>% miskin: ${p.persentase_miskin}%<br>Cakupan: ${p.cakupan_bantuan}%`);l.bindTooltip(p.nama,{sticky:true});}}).addTo(anMap);
try{anMap.fitBounds(anLayer.getBounds(),{padding:[40,40]});}catch(e){}
document.getElementById('zonaTable').innerHTML=d.features.map(f=>{const p=f.properties;
return `<tr><td>${p.rank}</td><td>${p.nama}</td><td>${p.persentase_miskin}%</td><td>${p.cakupan_bantuan}%</td><td><b>${p.skor}</b></td><td><span class="badge" style="background:${lvlColor[p.level]}22;color:${lvlColor[p.level]}">${p.level}</span></td></tr>`;}).join('');
}
// ---- Proximity ----
async function runProximity(){
const radius=document.getElementById('px-radius').value||1000;
const d=(await (await fetch(`${ANAPI}/proximity.php?radius=${radius}`)).json()).data;
clearLayer();
anLayer=L.geoJSON(d,{pointToLayer:(f,ll)=>{const iso=f.properties.terisolasi;return L.circleMarker(ll,{radius:8,color:iso?'#EF4444':'#10B981',fillColor:iso?'#EF4444':'#10B981',fillOpacity:.7,weight:2});},
onEachFeature:(f,l)=>{const p=f.properties;l.bindPopup(`<b>${p.nama_kk}</b><br>Terdekat: ${p.fasilitas||'-'} (${p.fasilitas_jenis||'-'})<br>Jarak: ${p.jarak_km} km<br>${p.terisolasi?'<span style="color:#EF4444">⚠ Terisolasi</span>':'<span style="color:#10B981">Terjangkau</span>'}`);}}).addTo(anMap);
try{anMap.fitBounds(anLayer.getBounds(),{padding:[40,40]});}catch(e){}
document.getElementById('px-summary').innerHTML=`Terisolasi: <b>${d.jumlah_terisolasi}</b> / ${d.total} (radius ${d.radius_m} m)`;
}
// ---- Cakupan ----
async function loadCakupan(){
const d=(await (await fetch(`${ANAPI}/cakupan_bantuan.php`)).json()).data;
document.getElementById('cak-summary').innerHTML=`Cakupan: <b>${d.cakupan_persen}%</b> · ${d.dibantu} dibantu, ${d.belum_dibantu} belum`;
charts.pie&&charts.pie.destroy();
charts.pie=new Chart(document.getElementById('chartPie'),{type:'doughnut',
data:{labels:['Dibantu','Belum dibantu'],datasets:[{data:[d.dibantu,d.belum_dibantu],backgroundColor:['#10B981','#E5E7EB']}]},
options:{plugins:{legend:{position:'bottom'}}}});
charts.bar&&charts.bar.destroy();
charts.bar=new Chart(document.getElementById('chartBar'),{type:'bar',
data:{labels:d.per_jenis.map(j=>j.kode),datasets:[{label:'Penerima',data:d.per_jenis.map(j=>j.penerima),backgroundColor:'#2563EB'}]},
options:{plugins:{legend:{display:false}},scales:{y:{beginAtZero:true,ticks:{precision:0}}}}});
document.getElementById('cakTable').innerHTML=d.per_wilayah.map(w=>`<tr><td>${w.nama}</td><td>${w.miskin}</td><td>${w.dibantu}</td><td>${w.cakupan}%</td></tr>`).join('');
}
// ---- Tren ----
async function loadTren(){
const d=(await (await fetch(`${ANAPI}/tren.php`)).json()).data;
charts.tren&&charts.tren.destroy();
charts.tren=new Chart(document.getElementById('chartTren'),{type:'line',
data:{labels:d.labels,datasets:[
{label:'Kumulatif Warga',data:d.kumulatif,borderColor:'#2563EB',backgroundColor:'#2563EB22',fill:true,tension:.3},
{label:'Warga Baru',data:d.warga_baru,borderColor:'#F59E0B',tension:.3},
{label:'Bantuan Baru',data:d.bantuan_baru,borderColor:'#10B981',tension:.3}
]},options:{scales:{y:{beginAtZero:true,ticks:{precision:0}}}}});
}
document.addEventListener('DOMContentLoaded',()=>switchTab('filter'));
</script>