/* ============================================================ dashboard.js — Chart.js analytics dashboard ============================================================ */ 'use strict'; let charts = {}; async function openDashboard() { openModal('dashboardModal'); // Always fetch fresh data when opening await renderDashboard(); } function destroyChart(id) { if (charts[id]) { charts[id].destroy(); delete charts[id]; } } async function renderDashboard() { showLoading(true); // Destroy all existing charts before re-rendering Object.keys(charts).forEach(key => destroyChart(key)); try { const [overview, trend, poverty, aidStat, age] = await Promise.all([ ApiStats.overview(), ApiStats.trend(), ApiStats.povertyChart(), ApiStats.aidChart(), ApiStats.ageDistribution(), ]); showLoading(false); if (poverty && poverty.ok) renderPovertyChart(poverty.data.data); if (trend && trend.ok) renderTrendChart(trend.data.data); if (age && age.ok) renderAgeChart(age.data.data); if (aidStat && aidStat.ok) renderAidChart(aidStat.data.data); } catch (err) { showLoading(false); console.error('Dashboard render error:', err); } } function chartDefaults() { return { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { font: { family: "'DM Sans', sans-serif", size: 10 }, color: '#5a6478', padding: 12, usePointStyle: true, pointStyleWidth: 8, boxHeight: 8, }, }, tooltip: { backgroundColor: '#0f1623', titleFont: { family: "'DM Sans', sans-serif", size: 11, weight: '700' }, bodyFont: { family: "'DM Sans', sans-serif", size: 11 }, padding: 10, cornerRadius: 8, }, }, scales: { x: { grid: { color: 'rgba(0,0,0,0.03)' }, ticks: { font: { family: "'DM Sans', sans-serif", size: 9 }, color: '#9ba4b5' }, }, y: { grid: { color: 'rgba(0,0,0,0.03)' }, ticks: { font: { family: "'DM Sans', sans-serif", size: 9 }, color: '#9ba4b5', precision: 0 }, beginAtZero: true, }, }, }; } // ---- Poverty distribution (doughnut) ----------------------- function renderPovertyChart(data) { destroyChart('poverty'); const bd = data?.breakdown || []; if (!bd.length) return; const labels = bd.map(r => POVERTY_LABELS[r.poverty_status] || r.poverty_status); const values = bd.map(r => parseInt(r.count)); const colors = bd.map(r => POVERTY_COLORS[r.poverty_status] || '#9ba4b5'); const ctx = document.getElementById('chartPoverty'); if (!ctx) return; charts.poverty = new Chart(ctx, { type: 'doughnut', data: { labels, datasets: [{ data: values, backgroundColor: colors, borderWidth: 2, borderColor: '#fff', hoverOffset: 8, }] }, options: { ...chartDefaults(), cutout: '62%', }, }); } // ---- Trend line chart (12 months) -------------------------- function renderTrendChart(data) { destroyChart('trend'); const rows = data?.trend || []; if (!rows.length) { const ctx = document.getElementById('chartTrend'); if (ctx && ctx.parentElement) { ctx.parentElement.innerHTML = '
Belum ada data pendataan 12 bulan terakhir
'; } return; } // Format bulan (contoh: "2025-01" -> "Jan 2025") const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; const labels = rows.map(r => { const [year, month] = r.month.split('-'); return `${months[parseInt(month) - 1]} ${year}`; }); const ctx = document.getElementById('chartTrend'); if (!ctx) return; charts.trend = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Rumah Baru', data: rows.map(r => r.new_households), backgroundColor: '#46b4f4', borderWidth: 0, borderRadius: 4, }, { label: 'Dibantu', data: rows.map(r => r.aided), backgroundColor: '#7d5ce8', borderWidth: 0, borderRadius: 4, } ], }, options: { ...chartDefaults(), plugins: { ...chartDefaults().plugins, legend: { position: 'bottom', labels: { font: { family: "'DM Sans', sans-serif", size: 10 }, color: '#5a6478', usePointStyle: true, pointStyleWidth: 8, boxHeight: 8, }, }, }, scales: { x: { grid: { display: false }, ticks: { font: { size: 9 }, maxRotation: 35, autoSkip: true, maxTicksLimit: 8 }, }, y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)' }, ticks: { stepSize: 1, precision: 0, }, } }, }, }); } // ---- Age distribution of DEPENDENTS (bar) ------------------ function renderAgeChart(data) { destroyChart('age'); const dist = data?.age_distribution || {}; // Only dependent age groups const labels = ['Anak (<12)', 'Remaja (12-17)', 'Pemuda (18-30)', 'Dewasa (31-59)', 'Lansia (60+)']; const values = [ dist.anak || 0, dist.remaja || 0, dist.pemuda || 0, dist.dewasa || 0, dist.lansia || 0 ]; const colors = ['#7c3aed','#3a56d4','#0b9e73','#d97706','#d63230']; // Skip if all zero if (values.every(v => v === 0)) return; const ctx = document.getElementById('chartAge'); if (!ctx) return; charts.age = new Chart(ctx, { type: 'bar', data: { labels, datasets: [{ label: 'Jumlah Tanggungan', data: values, backgroundColor: colors.map(c => c + 'cc'), borderColor: colors, borderWidth: 1.5, borderRadius: 5, }], }, options: { ...chartDefaults(), plugins: { ...chartDefaults().plugins, legend: { display: false }, title: { display: true, font: { family: "'DM Sans', sans-serif", size: 11 }, color: '#5a6478', padding: { bottom: 10 }, }, }, }, }); } // ---- Aid distribution (pie) -------------------------------- function renderAidChart(data) { destroyChart('aid'); const byType = data?.by_type || []; if (!byType.length) return; const colors = ['#3a56d4','#0b9e73','#d97706','#7c3aed','#d63230','#0e7f6e','#b45309']; const ctx = document.getElementById('chartAid'); if (!ctx) return; charts.aid = new Chart(ctx, { type: 'pie', data: { labels: byType.map(r => AID_LABELS[r.aid_type] || r.aid_type), datasets: [{ data: byType.map(r => parseInt(r.cnt)), backgroundColor: byType.map((_, i) => colors[i % colors.length] + 'cc'), borderColor: byType.map((_, i) => colors[i % colors.length]), borderWidth: 1.5, }], }, options: { ...chartDefaults(), plugins: { ...chartDefaults().plugins, title: { display: true, text: 'Total: ' + (data?.summary?.total_distributions || 0) + ' distribusi', font: { family: "'DM Sans', sans-serif", size: 10 }, color: '#9ba4b5', padding: { bottom: 8 }, }, }, }, }); }