Files
webgis_Ikbal/webgis-poverty-pontianak/assets/js/features/dashboard.js
T
2026-06-11 16:37:05 +07:00

469 lines
24 KiB
JavaScript
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.
// ===== 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 = `
<div class="db-loading">
<div class="db-spinner"></div>
<span>Memuat data dashboard terintegrasi…</span>
</div>`;
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 = `<div class="db-error">❌ Gagal memuat data: ${err.message}</div>`;
});
}
// ── 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 = `
<div class="db-tab-bar" id="dbTabBar">
${tabs.map(t => `
<button class="db-tab-btn${t.id === activeTab ? ' active' : ''}"
data-tab="${t.id}">${t.label}</button>
`).join('')}
</div>
<div id="dbTabContent" class="db-tab-content"></div>`;
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 = `
<div class="db-kpi-grid">
<div class="db-kpi-card kpi-ibadah">
<div class="db-kpi-icon">🕌</div>
<div class="db-kpi-val">${d.total_ibadah}</div>
<div class="db-kpi-label">Rumah Ibadah</div>
</div>
<div class="db-kpi-card kpi-miskin">
<div class="db-kpi-icon">🏠</div>
<div class="db-kpi-val">${d.total_miskin}</div>
<div class="db-kpi-label">KK Miskin</div>
</div>
<div class="db-kpi-card kpi-jiwa">
<div class="db-kpi-icon">👥</div>
<div class="db-kpi-val">${d.total_jiwa}</div>
<div class="db-kpi-label">Total Jiwa</div>
</div>
<div class="db-kpi-card kpi-log">
<div class="db-kpi-icon">📋</div>
<div class="db-kpi-val">${d.total_log}</div>
<div class="db-kpi-label">Log Bantuan</div>
</div>
</div>
${d.total_miskin > 0 ? `
<div class="db-section">
<div class="db-section-title">📊 Cakupan Penanganan</div>
<div class="db-coverage-row">
<div class="db-coverage-labels">
<span style="color:#22c55e">✅ Terurus: <strong>${d.terurus}</strong></span>
<span style="color:#ef4444">⚠️ Tidak Terurus: <strong>${d.tidak_terurus}</strong></span>
<span style="color:${pctColor}; font-weight:700">${d.pct_terurus}%</span>
</div>
<div class="db-progress-bar">
<div class="db-progress-fill" style="width:${d.pct_terurus}%; background:${pctColor};"></div>
</div>
</div>
</div>` : ''}
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">📦 Kategori Bantuan</div>
<canvas id="chartKategori" height="180"></canvas>
</div>
<div class="db-chart-card">
<div class="db-chart-title">🎁 Tipe Bantuan</div>
<canvas id="chartTipe" height="180"></canvas>
</div>
<div class="db-chart-card db-chart-wide">
<div class="db-chart-title">📅 Aktivitas Bantuan (6 Bulan)</div>
<canvas id="chartBulan" height="140"></canvas>
</div>
</div>`;
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 = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#3b82f6">
<div class="db-kpi-icon">🛣️</div>
<div class="db-kpi-val">${d.total_jalan}</div>
<div class="db-kpi-label">Ruas Jalan</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#0ea5e9">
<div class="db-kpi-icon">📏</div>
<div class="db-kpi-val">${panjangKm} km</div>
<div class="db-kpi-label">Total Panjang</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#8b5cf6">
<div class="db-kpi-icon">🗂️</div>
<div class="db-kpi-val">${d.total_parsil}</div>
<div class="db-kpi-label">Persil Tanah</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#6d28d9">
<div class="db-kpi-icon">📐</div>
<div class="db-kpi-val">${luasHa} ha</div>
<div class="db-kpi-label">Total Luas Persil</div>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">🛣️ Jalan per Status</div>
<canvas id="chartStatusJalan" height="180"></canvas>
</div>
<div class="db-chart-card">
<div class="db-chart-title">📋 Persil per Kepemilikan</div>
<canvas id="chartKepemilikan" height="180"></canvas>
</div>
</div>
${Object.keys(d.per_status).length > 0 ? `
<div class="db-section">
<div class="db-section-title">🛣️ Rincian Status Jalan</div>
<div class="db-table-wrap">
<table class="db-table">
<thead><tr><th>Status</th><th>Jumlah Ruas</th></tr></thead>
<tbody>
${Object.entries(d.per_status).map(([k,v]) => `
<tr><td>${esc(k)}</td><td><span class="db-badge">${v}</span></td></tr>
`).join('')}
</tbody>
</table>
</div>
</div>` : '<div class="db-empty">Belum ada data jalan tersimpan di database.</div>'}`;
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 = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#f97316">
<div class="db-kpi-icon">⛽</div>
<div class="db-kpi-val">${d.total_spbu}</div>
<div class="db-kpi-label">Total SPBU</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#22c55e">
<div class="db-kpi-icon">🕐</div>
<div class="db-kpi-val">${d.buka_24_jam}</div>
<div class="db-kpi-label">Buka 24 Jam</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#ef4444">
<div class="db-kpi-icon">🕙</div>
<div class="db-kpi-val">${d.tidak_24_jam}</div>
<div class="db-kpi-label">Tidak 24 Jam</div>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card">
<div class="db-chart-title">⏰ Status Operasional</div>
<canvas id="chartSpbuOps" height="180"></canvas>
</div>
</div>
${d.list.length > 0 ? `
<div class="db-section">
<div class="db-section-title">📍 Daftar SPBU</div>
<div class="db-table-wrap">
<table class="db-table">
<thead>
<tr>
<th>#</th>
<th>Nama SPBU</th>
<th>Nomor WA</th>
<th>Operasional</th>
<th>Koordinat</th>
</tr>
</thead>
<tbody>
${d.list.map((s, i) => `
<tr>
<td>${i+1}</td>
<td><strong>${esc(s.nama_spbu)}</strong></td>
<td>${esc(s.nomor_wa)}</td>
<td>${s.buka_24_jam == 1
? '<span class="db-badge" style="background:#22c55e">24 Jam</span>'
: '<span class="db-badge" style="background:#f59e0b">Terbatas</span>'}</td>
<td style="font-size:11px;color:#888">${parseFloat(s.latitude).toFixed(5)}, ${parseFloat(s.longitude).toFixed(5)}</td>
</tr>`).join('')}
</tbody>
</table>
</div>
</div>` : '<div class="db-empty">Belum ada data SPBU tersimpan di database.</div>'}`;
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 = `
<div class="db-kpi-grid">
<div class="db-kpi-card" style="--kpi-color:#0891b2">
<div class="db-kpi-icon">🗺️</div>
<div class="db-kpi-val">${d.total_kecamatan}</div>
<div class="db-kpi-label">Kecamatan</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#06b6d4">
<div class="db-kpi-icon">📊</div>
<div class="db-kpi-val">${d.rata_density.toLocaleString('id-ID')}</div>
<div class="db-kpi-label">Rata-rata Kepadatan (jiwa/km²)</div>
</div>
<div class="db-kpi-card" style="--kpi-color:#0e7490">
<div class="db-kpi-icon">🏆</div>
<div class="db-kpi-val" style="font-size:14px">${esc(d.terpadat)}</div>
<div class="db-kpi-label">Kecamatan Terpadat</div>
</div>
</div>
<div class="db-section">
<div class="db-section-title">👥 Kepadatan Penduduk per Kecamatan (jiwa/km²)</div>
${d.kecamatan.map(k => {
const pct = ((k.density / maxDensity) * 100).toFixed(1);
const color = k.density >= 8 ? '#ef4444' : k.density >= 5 ? '#f59e0b' : '#22c55e';
return `
<div style="margin-bottom:10px">
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:3px">
<span>${esc(k.nama)}</span>
<strong style="color:${color}">${k.density.toLocaleString('id-ID')} jiwa/km²</strong>
</div>
<div class="db-progress-bar">
<div class="db-progress-fill" style="width:${pct}%; background:${color};"></div>
</div>
</div>`;
}).join('')}
<div style="margin-top:8px; font-size:11px; color:#888; display:flex; gap:16px">
<span>🟢 &lt;5 Rendah</span>
<span>🟡 58 Sedang</span>
<span>🔴 ≥8 Tinggi</span>
</div>
</div>
<div class="db-charts-grid">
<div class="db-chart-card db-chart-wide">
<div class="db-chart-title">📊 Kepadatan per Kecamatan</div>
<canvas id="chartDensity" height="160"></canvas>
</div>
</div>`;
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
})();