// ═══════════════════════════════════════════════════════════════════
// DASHBOARD PAGE — National Overview with Map + KPIs
// ═══════════════════════════════════════════════════════════════════
let _mapInit = false;
let _mapFilter = 'all';
function renderDashboardPage() {
renderKPIStrip();
renderDashSidePanel();
initLeafletMap();
}
// ─── KPI STRIP ───────────────────────────────────────────────────
function renderKPIStrip() {
const issues = APP_STATE.issues;
const locs = APP_STATE.locs;
const openIssues = issues.filter(i => i.status === 'open').length;
const inProg = issues.filter(i => i.status === 'in_progress').length;
const criticalIssues= issues.filter(i => i.severity === 'critical' && i.status !== 'resolved').length;
const totalLocs = locs.length;
const totalMeals = locs.reduce((s,l) => s + (l.mbg?.meals || 0), 0);
const totalKop = locs.reduce((s,l) => s + (l.koperasi?.units || 0), 0);
const openEsc = APP_STATE.escalations.filter(e => e.status !== 'resolved').length;
document.getElementById('kpi-strip').innerHTML = `
🗂️
Program Aktif
5
↑ Semua berjalan normal
📍
Titik Pemantauan
${totalLocs}
Kota/Kabupaten terpantau
⚠️
Issue Terbuka
${openIssues + inProg}
↑ ${criticalIssues} kritis · ${inProg} diproses
🍱
Porsi MBG / Hari
${(totalMeals/1000000).toFixed(1)}M
↑ 64,3% dari target nasional
🔺
Eskalasi Aktif
${openEsc}
Butuh tindak lanjut segera
`;
}
// ─── SIDE PANEL ───────────────────────────────────────────────────
function renderDashSidePanel() {
const flaggedIssues = APP_STATE.issues
.filter(i => i.status !== 'resolved' && i.status !== 'closed')
.sort((a,b) => { const o = {critical:0,high:1,medium:2,low:3}; return o[a.severity]-o[b.severity]; })
.slice(0,8);
const progRows = PROGRAMS.map(p => {
const pct = Math.round((p.capaian / p.target) * 100);
const issCount = APP_STATE.issues.filter(i => i.programId === p.id && i.status !== 'resolved').length;
return `
${pct}%
${issCount > 0 ? `
${issCount} issue
` : '
✓ Normal
'}
`;
}).join('');
document.getElementById('dash-side-panel').innerHTML = `
Capaian Program
${progRows}
⚠️ Issue Aktif (${flaggedIssues.length})
${flaggedIssues.length === 0
? `
`
: flaggedIssues.map(i => `
${getProgramBadge(i.programId)}
${getSevBadge(i.severity)}
${i.judul}
📍 ${i.daerah}, ${i.provinsi}
${getStatBadge(i.status)}
`).join('')
}
`;
}
let _tileGroup = null;
function initLeafletMap() {
if (_mapInit) { renderMapMarkers(); return; }
_mapInit = true;
const map = L.map('leaflet-map', { zoomControl: false, attributionControl: false }).setView([-2.5, 118], 5);
_tileGroup = L.layerGroup().addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
APP_STATE.mapInstance = map;
updateMapTileLayers();
renderMapMarkers();
}
function updateMapTileLayers() {
const map = APP_STATE.mapInstance;
if (!map || !_tileGroup) return;
_tileGroup.clearLayers();
const isLight = document.body.classList.contains('light-theme');
if (isLight) {
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', { maxZoom:14, subdomains:'abcd' }).addTo(_tileGroup);
} else {
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}{r}.png', { maxZoom:14, subdomains:'abcd' }).addTo(_tileGroup);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png', { maxZoom:14, subdomains:'abcd', opacity:0.5 }).addTo(_tileGroup);
}
}
function applyMapFilter(f, el) {
_mapFilter = f;
document.querySelectorAll('.map-btn').forEach(b => b.classList.remove('active'));
el.classList.add('active');
renderMapMarkers();
}
const PROG_COLORS = { mbg:'#22c55e', koperasi:'#3b82f6', psn:'#f59e0b', hilirisasi:'#a855f7', rumah:'#f97316' };
function getLocColor(loc) {
if (loc.criticalCount > 0 || loc.issueCount > 1) return '#ef4444';
if (loc.issueCount > 0) return '#f97316';
if (loc.programs.length > 1) return '#e2e8f0';
return PROG_COLORS[loc.programs[0]] || '#8FAABE';
}
function renderMapMarkers() {
const map = APP_STATE.mapInstance;
if (!map) return;
Object.values(APP_STATE.mapMarkers).forEach(m => map.removeLayer(m));
APP_STATE.mapMarkers = {};
const filtered = APP_STATE.locs.filter(loc => {
if (_mapFilter === 'all') return true;
if (_mapFilter === 'issues') return loc.issueCount > 0;
return loc.programs.includes(_mapFilter);
});
filtered.forEach(loc => {
const color = getLocColor(loc);
const hasIssue = loc.issueCount > 0;
const size = loc.programs.length > 1 ? 14 : 11;
const label = hasIssue ? '⚠' : loc.programs.length > 1 ? '★' : loc.programs[0]?.charAt(0).toUpperCase();
const icon = L.divIcon({
className: '',
html: `${label}
`,
iconSize: [size*2, size*2],
iconAnchor: [size, size]
});
const progInfo = loc.programs.map(pid => {
const d = loc[pid];
if (!d) return '';
if (pid === 'mbg') return `🍱 Porsi/Hari${(d.meals||0).toLocaleString()}
`;
if (pid === 'koperasi') return `🏪 Anggota${(d.members||0).toLocaleString()}
`;
if (pid === 'psn') return `🏗️ Progress PSN${d.progress}%
`;
if (pid === 'hilirisasi') return `⚙️ ${d.commodity}${d.progress}%
`;
if (pid === 'rumah') return `🏠 Unit Selesai${(d.completed||0).toLocaleString()}
`;
return '';
}).join('');
const m = L.marker([loc.lat, loc.lng], { icon });
m.bindTooltip(`
${loc.name}
${loc.province}
${progInfo}
${loc.issueCount > 0 ? `⚠ Issue${loc.issueCount} aktif
` : ''}
`, { className:'custom-tooltip', direction:'top', offset:[0,-4] });
m.on('click', () => openLocIssues(loc));
m.addTo(map);
APP_STATE.mapMarkers[loc.id] = m;
});
}
function openLocIssues(loc) {
const locIssues = APP_STATE.issues.filter(i => i.daerah === loc.name && i.status !== 'resolved');
if (locIssues.length > 0) {
openIssueDetail(locIssues[0].id);
} else {
showToast(loc.name, `${loc.province} — ${loc.programs.length} program aktif, tidak ada issue`, 'info');
}
}