// ===== Dashboard Feature =====
// Dashboard informatif untuk Admin dan Pengelola Rumah Ibadah
(function () {
'use strict';
// ── State ──────────────────────────────────────────────────────────────
let dashboardData = null;
let chartInstances = {};
// ── Modal Element ──────────────────────────────────────────────────────
const modal = document.getElementById('dashboardModal');
if (!modal) return;
// ── Open / Close ───────────────────────────────────────────────────────
window.openDashboard = function () {
modal.classList.add('show');
loadDashboard();
};
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 = `
`;
fetch('api/dashboard/stats.php')
.then(r => r.json())
.then(data => {
if (data.status !== 'success') throw new Error(data.message);
dashboardData = data;
renderDashboard(data);
})
.catch(err => {
body.innerHTML = `❌ Gagal memuat data: ${err.message}
`;
});
}
// ── Render ─────────────────────────────────────────────────────────────
function renderDashboard(data) {
const { stats, distribusi, ibadah_list, ibadah_performa, recent_logs, role } = data;
const isPengelola = role === 'pengelola';
const ibadah = ibadah_list?.[0] ?? null;
const body = document.getElementById('dashboardBody');
// ── Header info ──────────────────────────────────────────────────
let headerHtml = '';
if (isPengelola && ibadah) {
const emoji = (window.IBADAH_EMOJI || {})[ibadah.jenis] || '🕌';
headerHtml = `
`;
}
// ── KPI Cards ────────────────────────────────────────────────────
const pctTerurus = stats.pct_terurus;
const pctColor = pctTerurus >= 70 ? '#22c55e' : pctTerurus >= 40 ? '#f59e0b' : '#ef4444';
let kpiCards = `
${isPengelola ? '' : `
🕌
${stats.total_ibadah}
Rumah Ibadah
`}
🏠
${stats.total_miskin}
KK Miskin${isPengelola ? ' (radius)' : ''}
👥
${stats.total_jiwa}
Total Jiwa
📋
${stats.total_log}
Log Bantuan
`;
// ── Coverage bar ──────────────────────────────────────────────────
const coverageHtml = stats.total_miskin > 0 ? `
📊 Cakupan Penanganan
✅ Terurus: ${stats.terurus}
⚠️ Tidak Terurus: ${stats.tidak_terurus}
${pctTerurus}%
` : '';
// ── Charts area ───────────────────────────────────────────────────
const chartsHtml = `
${!isPengelola ? `
` : ''}
📅 Aktivitas Bantuan (6 Bulan)
`;
// ── Recent logs ───────────────────────────────────────────────────
let logsHtml = `🕐 Bantuan Terbaru
`;
if (recent_logs.length === 0) {
logsHtml += `
Belum ada log bantuan
`;
} else {
logsHtml += `
`;
recent_logs.forEach(log => {
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' });
const emoji = (window.IBADAH_EMOJI || {})[log.jenis_ibadah] || '🕌';
logsHtml += `
${esc(log.tipe_bantuan)}
→ ${esc(log.nama_kk)}
${emoji} ${esc(log.nama_ibadah)} · ${tgl}${log.keterangan ? ` · ${esc(log.keterangan)}` : ''}
`;
});
logsHtml += `
`;
}
logsHtml += `
`;
// ── Ibadah performa table (admin only) ────────────────────────────
let perfHtml = '';
if (!isPengelola && ibadah_performa.length > 0) {
perfHtml = `
🏆 Performa Rumah Ibadah
| # |
Nama |
Jenis |
Total Log |
Penerima Unik |
${ibadah_performa.map((ib, i) => `
| ${i + 1} |
${esc(ib.nama)} |
${esc(ib.jenis)} |
${ib.total_log} |
${ib.total_penerima} |
`).join('')}
`;
}
// ── Assemble ──────────────────────────────────────────────────────
body.innerHTML = `
${headerHtml}
${kpiCards}
${coverageHtml}
${chartsHtml}
${logsHtml}
${perfHtml}
`;
// ── Draw Charts ───────────────────────────────────────────────────
setTimeout(() => {
drawPieChart('chartKategori', distribusi.kategori, ['#6366f1', '#f59e0b', '#22c55e', '#ef4444', '#06b6d4']);
drawPieChart('chartTipe', distribusi.tipe_bantuan, ['#4f46e5', '#16a34a', '#dc2626', '#d97706', '#0891b2', '#7c3aed']);
if (!isPengelola) drawPieChart('chartJenis', distribusi.jenis_ibadah, ['#f97316', '#6366f1', '#22c55e', '#ec4899', '#14b8a6']);
drawBarChart('chartBulan', distribusi.per_bulan);
}, 50);
}
// ── Chart helpers ──────────────────────────────────────────────────────
function drawPieChart(canvasId, dataObj, colors) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const entries = Object.entries(dataObj || {});
if (entries.length === 0) {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
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 ctx = canvas.getContext('2d');
const cx = canvas.width / 2;
const cy = canvas.height * 0.45;
const r = Math.min(cx, cy) * 0.75;
ctx.clearRect(0, 0, canvas.width, canvas.height);
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;
});
// Center text
ctx.fillStyle = '#333';
ctx.font = 'bold 16px Inter';
ctx.textAlign = 'center';
ctx.fillText(total, cx, cy + 5);
// Legend
const legY = cy + r + 16;
const legItemW = 90;
const perRow = Math.floor(canvas.width / legItemW);
labels.forEach((lbl, i) => {
const row = Math.floor(i / perRow);
const col = i % perRow;
const lx = (canvas.width - Math.min(labels.length, perRow) * legItemW) / 2 + col * legItemW;
const ly = legY + row * 18;
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 > 10 ? lbl.slice(0, 9) + '…' : 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;
const H = canvas.height - padT - padB;
const values = perBulan.map(d => parseInt(d.cnt));
const maxVal = Math.max(...values, 1);
const barW = W / perBulan.length * 0.6;
const gap = W / perBulan.length;
// Grid lines
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);
}
// Bars
perBulan.forEach((d, i) => {
const bh = (parseInt(d.cnt) / maxVal) * H;
const bx = padL + i * gap + (gap - barW) / 2;
const 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();
ctx.roundRect ? ctx.roundRect(bx, by, barW, bh, [4, 4, 0, 0]) : ctx.rect(bx, by, barW, bh);
ctx.fill();
// Value label
ctx.fillStyle = '#4f46e5';
ctx.font = 'bold 10px Inter';
ctx.textAlign = 'center';
ctx.fillText(d.cnt, bx + barW / 2, by - 4);
// Month label
const monthLabel = d.bulan ? d.bulan.slice(5) + '/' + d.bulan.slice(2, 4) : '';
ctx.fillStyle = '#6b7280';
ctx.font = '9px Inter';
ctx.fillText(monthLabel, bx + barW / 2, padT + H + 14);
});
}
// ── Utility ───────────────────────────────────────────────────────────
function esc(str) {
if (!str) return '';
return String(str).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"');
}
})();