Fix: Convert 03PovertyMapping from submodule to regular folder
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
/* ============================================================
|
||||
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 = '<div style="text-align:center;padding:40px;color:#9ba4b5;"><i class="fas fa-chart-line" style="font-size:24px;margin-bottom:10px;display:block;"></i>Belum ada data pendataan 12 bulan terakhir</div>';
|
||||
}
|
||||
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 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user