// ═══════════════════════════════════════════════════════════════════ // 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 `
${p.icon}
${p.shortName}
${pct}%
${issCount > 0 ? `
${issCount} issue
` : '
✓ Normal
'}
`; }).join(''); document.getElementById('dash-side-panel').innerHTML = `
Capaian Program
${progRows}
⚠️ Issue Aktif (${flaggedIssues.length})
${flaggedIssues.length === 0 ? `
Tidak ada issue aktif
` : 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'); } }