// ===== Dashboard Feature (Terintegrasi) ===== // Menghubungkan: webgis-poverty-pontianak, jalan, penduduk, spbu (function () { 'use strict'; // ── State ────────────────────────────────────────────────────────────── let dashboardData = null; let activeTab = 'kemiskinan'; // ── Modal Element ────────────────────────────────────────────────────── const modal = document.getElementById('dashboardModal'); if (!modal) return; // ── Open / Close ─────────────────────────────────────────────────────── window.openDashboard = function () { modal.classList.add('show'); if (!dashboardData) loadDashboard(); else renderTab(activeTab); }; window.closeDashboard = function () { modal.classList.remove('show'); }; document.getElementById('closeDashboardModal')?.addEventListener('click', window.closeDashboard); modal.addEventListener('click', function (e) { if (e.target === modal) window.closeDashboard(); }); // ── Load Data ────────────────────────────────────────────────────────── function loadDashboard() { const body = document.getElementById('dashboardBody'); body.innerHTML = `
Memuat data dashboard terintegrasi…
`; fetch('api/dashboard/stats_extended.php') .then(r => r.json()) .then(data => { if (data.status !== 'success') throw new Error(data.message || 'Gagal memuat'); dashboardData = data; renderShell(); renderTab(activeTab); }) .catch(err => { body.innerHTML = `
❌ Gagal memuat data: ${err.message}
`; }); } // ── Shell (tab bar + container) ──────────────────────────────────────── function renderShell() { const body = document.getElementById('dashboardBody'); const tabs = [ { id: 'kemiskinan', label: '🏠 Kemiskinan' }, { id: 'jalan', label: '🛣️ Jalan' }, { id: 'spbu', label: '⛽ SPBU' }, { id: 'penduduk', label: '👥 Penduduk' }, ]; body.innerHTML = `
${tabs.map(t => ` `).join('')}
`; document.getElementById('dbTabBar').addEventListener('click', function (e) { const btn = e.target.closest('[data-tab]'); if (!btn) return; activeTab = btn.dataset.tab; document.querySelectorAll('.db-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === activeTab)); renderTab(activeTab); }); } // ── Route to tab ─────────────────────────────────────────────────────── function renderTab(tab) { const content = document.getElementById('dbTabContent'); if (!content || !dashboardData) return; if (tab === 'kemiskinan') renderKemiskinan(content, dashboardData.poverty); else if (tab === 'jalan') renderJalan(content, dashboardData.jalan); else if (tab === 'spbu') renderSpbu(content, dashboardData.spbu); else if (tab === 'penduduk') renderPenduduk(content, dashboardData.penduduk); } // ══════════════════════════════════════════════════════════════════════ // Tab 1: Kemiskinan // ══════════════════════════════════════════════════════════════════════ function renderKemiskinan(el, d) { const pctColor = d.pct_terurus >= 70 ? '#22c55e' : d.pct_terurus >= 40 ? '#f59e0b' : '#ef4444'; el.innerHTML = `
🕌
${d.total_ibadah}
Rumah Ibadah
🏠
${d.total_miskin}
KK Miskin
👥
${d.total_jiwa}
Total Jiwa
📋
${d.total_log}
Log Bantuan
${d.total_miskin > 0 ? `
📊 Cakupan Penanganan
✅ Terurus: ${d.terurus} ⚠️ Tidak Terurus: ${d.tidak_terurus} ${d.pct_terurus}%
` : ''}
📦 Kategori Bantuan
🎁 Tipe Bantuan
📅 Aktivitas Bantuan (6 Bulan)
`; setTimeout(() => { drawPieChart('chartKategori', d.distribusi.kategori, ['#6366f1','#f59e0b','#22c55e','#ef4444','#06b6d4']); drawPieChart('chartTipe', d.distribusi.tipe_bantuan, ['#4f46e5','#16a34a','#dc2626','#d97706','#0891b2','#7c3aed']); drawBarChart('chartBulan', d.distribusi.per_bulan); }, 50); } // ══════════════════════════════════════════════════════════════════════ // Tab 2: Jalan // ══════════════════════════════════════════════════════════════════════ function renderJalan(el, d) { const panjangKm = (d.total_panjang_m / 1000).toFixed(2); const luasHa = (d.total_luas_m2 / 10000).toFixed(2); el.innerHTML = `
🛣️
${d.total_jalan}
Ruas Jalan
📏
${panjangKm} km
Total Panjang
🗂️
${d.total_parsil}
Persil Tanah
📐
${luasHa} ha
Total Luas Persil
🛣️ Jalan per Status
📋 Persil per Kepemilikan
${Object.keys(d.per_status).length > 0 ? `
🛣️ Rincian Status Jalan
${Object.entries(d.per_status).map(([k,v]) => ` `).join('')}
StatusJumlah Ruas
${esc(k)}${v}
` : '
Belum ada data jalan tersimpan di database.
'}`; setTimeout(() => { drawPieChart('chartStatusJalan', d.per_status, ['#3b82f6','#0ea5e9','#38bdf8']); drawPieChart('chartKepemilikan', d.per_kepemilikan, ['#8b5cf6','#a78bfa','#c4b5fd','#7c3aed']); }, 50); } // ══════════════════════════════════════════════════════════════════════ // Tab 3: SPBU // ══════════════════════════════════════════════════════════════════════ function renderSpbu(el, d) { el.innerHTML = `
${d.total_spbu}
Total SPBU
🕐
${d.buka_24_jam}
Buka 24 Jam
🕙
${d.tidak_24_jam}
Tidak 24 Jam
⏰ Status Operasional
${d.list.length > 0 ? `
📍 Daftar SPBU
${d.list.map((s, i) => ` `).join('')}
# Nama SPBU Nomor WA Operasional Koordinat
${i+1} ${esc(s.nama_spbu)} ${esc(s.nomor_wa)} ${s.buka_24_jam == 1 ? '24 Jam' : 'Terbatas'} ${parseFloat(s.latitude).toFixed(5)}, ${parseFloat(s.longitude).toFixed(5)}
` : '
Belum ada data SPBU tersimpan di database.
'}`; setTimeout(() => { drawPieChart('chartSpbuOps', { 'Buka 24 Jam': d.buka_24_jam, 'Tidak 24 Jam': d.tidak_24_jam }, ['#22c55e','#ef4444']); }, 50); } // ══════════════════════════════════════════════════════════════════════ // Tab 4: Penduduk (Choropleth Kecamatan) // ══════════════════════════════════════════════════════════════════════ function renderPenduduk(el, d) { const maxDensity = Math.max(...d.kecamatan.map(k => k.density)); el.innerHTML = `
🗺️
${d.total_kecamatan}
Kecamatan
📊
${d.rata_density.toLocaleString('id-ID')}
Rata-rata Kepadatan (jiwa/km²)
🏆
${esc(d.terpadat)}
Kecamatan Terpadat
👥 Kepadatan Penduduk per Kecamatan (jiwa/km²)
${d.kecamatan.map(k => { const pct = ((k.density / maxDensity) * 100).toFixed(1); const color = k.density >= 8 ? '#ef4444' : k.density >= 5 ? '#f59e0b' : '#22c55e'; return `
${esc(k.nama)} ${k.density.toLocaleString('id-ID')} jiwa/km²
`; }).join('')}
🟢 <5 Rendah 🟡 5–8 Sedang 🔴 ≥8 Tinggi
📊 Kepadatan per Kecamatan
`; setTimeout(() => { drawDensityBar('chartDensity', d.kecamatan); }, 50); } // ══════════════════════════════════════════════════════════════════════ // Chart Helpers // ══════════════════════════════════════════════════════════════════════ function drawPieChart(canvasId, dataObj, colors) { const canvas = document.getElementById(canvasId); if (!canvas) return; const entries = Object.entries(dataObj || {}).filter(([,v]) => v > 0); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); if (entries.length === 0) { ctx.fillStyle = '#aaa'; ctx.font = '13px Inter'; ctx.textAlign = 'center'; ctx.fillText('Belum ada data', canvas.width/2, canvas.height/2); return; } const labels = entries.map(([k]) => k); const values = entries.map(([,v]) => v); const total = values.reduce((a,b) => a+b, 0); const cx = canvas.width/2, cy = canvas.height * 0.42; const r = Math.min(cx, cy) * 0.72; let startAngle = -Math.PI/2; values.forEach((v,i) => { const slice = (v/total)*2*Math.PI; ctx.beginPath(); ctx.moveTo(cx, cy); ctx.arc(cx, cy, r, startAngle, startAngle+slice); ctx.closePath(); ctx.fillStyle = colors[i % colors.length]; ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke(); startAngle += slice; }); ctx.fillStyle='#333'; ctx.font='bold 15px Inter'; ctx.textAlign='center'; ctx.fillText(total, cx, cy+5); const legY = cy+r+14, legItemW = 92, perRow = Math.floor(canvas.width/legItemW); labels.forEach((lbl,i) => { const row=Math.floor(i/perRow), col=i%perRow; const lx=(canvas.width - Math.min(labels.length,perRow)*legItemW)/2 + col*legItemW; const ly=legY+row*17; ctx.fillStyle=colors[i%colors.length]; ctx.fillRect(lx,ly,10,10); ctx.fillStyle='#555'; ctx.font='10px Inter'; ctx.textAlign='left'; const shortLbl = lbl.length>11 ? lbl.slice(0,10)+'…' : lbl; ctx.fillText(`${shortLbl} (${values[i]})`, lx+13, ly+9); }); } function drawBarChart(canvasId, perBulan) { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); if (!perBulan || perBulan.length === 0) { ctx.fillStyle='#aaa'; ctx.font='13px Inter'; ctx.textAlign='center'; ctx.fillText('Belum ada data bantuan', canvas.width/2, canvas.height/2); return; } const padL=36, padR=12, padT=12, padB=28; const W=canvas.width-padL-padR, H=canvas.height-padT-padB; const values=perBulan.map(d=>parseInt(d.cnt)), maxVal=Math.max(...values,1); const barW=W/perBulan.length*0.6, gap=W/perBulan.length; ctx.strokeStyle='#e5e7eb'; ctx.lineWidth=1; for (let i=0;i<=4;i++){ const y=padT+H-(i/4)*H; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+W,y); ctx.stroke(); ctx.fillStyle='#9ca3af'; ctx.font='9px Inter'; ctx.textAlign='right'; ctx.fillText(Math.round((i/4)*maxVal), padL-4, y+3); } perBulan.forEach((d,i)=>{ const bh=(parseInt(d.cnt)/maxVal)*H, bx=padL+i*gap+(gap-barW)/2, by=padT+H-bh; const grad=ctx.createLinearGradient(0,by,0,padT+H); grad.addColorStop(0,'#6366f1'); grad.addColorStop(1,'#a5b4fc'); ctx.fillStyle=grad; ctx.beginPath(); if(ctx.roundRect) ctx.roundRect(bx,by,barW,bh,[4,4,0,0]); else ctx.rect(bx,by,barW,bh); ctx.fill(); ctx.fillStyle='#4f46e5'; ctx.font='bold 10px Inter'; ctx.textAlign='center'; ctx.fillText(d.cnt, bx+barW/2, by-4); const ml=d.bulan ? d.bulan.slice(5)+'/'+d.bulan.slice(2,4) : ''; ctx.fillStyle='#6b7280'; ctx.font='9px Inter'; ctx.fillText(ml, bx+barW/2, padT+H+14); }); } function drawDensityBar(canvasId, kecamatan) { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.height); const padL=130, padR=50, padT=12, padB=12; const W=canvas.width-padL-padR, H=canvas.height-padT-padB; const maxVal=Math.max(...kecamatan.map(k=>k.density),1); const barH=Math.floor(H/kecamatan.length)-6; kecamatan.forEach((k,i) => { const bw=(k.density/maxVal)*W; const by=padT+i*(barH+6); const color=k.density>=8?'#ef4444':k.density>=5?'#f59e0b':'#22c55e'; // Label kecamatan ctx.fillStyle='#374151'; ctx.font='11px Inter'; ctx.textAlign='right'; ctx.fillText(k.nama, padL-6, by+barH/2+4); // Bar ctx.fillStyle=color+'33'; ctx.fillRect(padL, by, W, barH); const grad=ctx.createLinearGradient(padL,0,padL+bw,0); grad.addColorStop(0,color); grad.addColorStop(1,color+'99'); ctx.fillStyle=grad; if(ctx.roundRect) ctx.roundRect(padL,by,bw,barH,[0,4,4,0]); else ctx.fillRect(padL,by,bw,barH); ctx.fill(); // Value ctx.fillStyle=color; ctx.font='bold 11px Inter'; ctx.textAlign='left'; ctx.fillText(k.density.toFixed(3), padL+bw+5, by+barH/2+4); }); } // ── Utility ─────────────────────────────────────────────────────────── function esc(str) { if (!str) return ''; return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } })();