// ===== 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}%
` : ''}
📅 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
${Object.keys(d.per_status).length > 0 ? `
🛣️ Rincian Status Jalan
| Status | Jumlah Ruas |
${Object.entries(d.per_status).map(([k,v]) => `
| ${esc(k)} | ${v} |
`).join('')}
` : '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
${d.list.length > 0 ? `
📍 Daftar SPBU
| # |
Nama SPBU |
Nomor WA |
Operasional |
Koordinat |
${d.list.map((s, i) => `
| ${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)} |
`).join('')}
` : '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,'"');
}
})();