Initial commit

This commit is contained in:
envicy
2026-06-11 16:37:05 +07:00
commit 503e9421a9
45 changed files with 13370 additions and 0 deletions
@@ -0,0 +1,468 @@
// ===== 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;');
}
})();