// ═══════════════════════════════════════════════════════════════════ // MAIN APPLICATION — Router, State, Clock, Shared Utilities // ═══════════════════════════════════════════════════════════════════ // ─── INITIALIZATION ────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { initTheme(); initClock(); updateIssuesBadge(); renderNotifications(); navigateTo('dashboard'); // Close dropdowns on outside click document.addEventListener('click', (e) => { if (!e.target.closest('.topbar-actions-area')) { document.getElementById('notif-dropdown').classList.add('hidden'); } }); }); // ─── ROUTER ────────────────────────────────────────────────────── const PAGES = { dashboard: { title: 'Overview Nasional', sub: 'Pemantauan real-time seluruh program strategis pemerintah', render: renderDashboardPage }, programs: { title: 'Detail Program', sub: 'Capaian dan status per program strategis pemerintah', render: renderProgramsPage }, issues: { title: 'Manajemen Issue', sub: 'Pantau, tindak lanjut, dan selesaikan issue di setiap daerah', render: renderIssuesPage }, survey: { title: 'Survey Lapangan', sub: 'Hasil verifikasi dan temuan survei lapangan per daerah', render: renderSurveyPage }, escalasi: { title: 'Eskalasi Issue', sub: 'Pipeline eskalasi berjenjang: Daerah → KSP → Kementerian → APH', render: renderEscalasiPage }, integration: { title: 'Arsitektur Platform Terintegrasi', sub: 'Hubungan sistem pemetaan, manajemen survei, dan tracking logistik KSP', render: renderIntegrationPage } }; function navigateTo(page) { APP_STATE.currentPage = page; // Update nav items document.querySelectorAll('.nav-item[id^="nav-"]').forEach(el => { if (!el.id.startsWith('nav-filter')) el.classList.remove('active'); }); const navEl = document.getElementById(`nav-${page}`); if (navEl) navEl.classList.add('active'); // Update topbar const cfg = PAGES[page]; document.getElementById('topbar-title').textContent = cfg.title; document.getElementById('topbar-sub').textContent = cfg.sub; // Switch view document.querySelectorAll('.page-view').forEach(v => v.classList.remove('active')); document.getElementById(`view-${page}`).classList.add('active'); // Render page cfg.render(); } function setNavProgramFilter(programId, el) { APP_STATE.currentProgram = programId; // Remove active state from all sidebar filter items document.querySelectorAll('[id^="nav-filter-"]').forEach(e => e.classList.remove('active')); if (el) el.classList.add('active'); if (programId === 'all') { // "Semua Program" → kembali ke Overview Nasional navigateTo('dashboard'); } else { // Program spesifik → buka halaman Detail Program dengan tab yang sesuai _activeProgTab = programId; // set tab aktif di programs.js (global scope) navigateTo('programs'); } } // ─── CLOCK ─────────────────────────────────────────────────────── function initClock() { updateClock(); setInterval(updateClock, 1000); } function updateClock() { const now = new Date(); const time = now.toLocaleTimeString('id-ID', { timeZone: 'Asia/Jakarta', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); const jkt = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Jakarta' })); const days = ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu']; const months = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agt','Sep','Okt','Nov','Des']; document.getElementById('clock-time').textContent = time + ' WIB'; document.getElementById('clock-date').textContent = `${days[jkt.getDay()]}, ${jkt.getDate()} ${months[jkt.getMonth()]} ${jkt.getFullYear()}`; } // ─── MODAL CONTROLS ────────────────────────────────────────────── function openModal(id) { document.getElementById(id).classList.add('open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } // ─── NOTIFICATIONS ─────────────────────────────────────────────── function toggleNotifDropdown() { document.getElementById('notif-dropdown').classList.toggle('hidden'); } function updateIssuesBadge() { const openCount = APP_STATE.issues.filter(i => i.status === 'open' || i.status === 'in_progress').length; const badge = document.getElementById('issues-badge'); badge.textContent = openCount; badge.style.display = openCount > 0 ? 'block' : 'none'; } function renderNotifications() { const notifs = APP_STATE.notifications; const unread = notifs.filter(n => !n.read).length; document.getElementById('notif-dot').style.display = unread > 0 ? 'block' : 'none'; const list = document.getElementById('notif-list'); if (!notifs.length) { list.innerHTML = '
Tidak ada notifikasi
'; return; } const typeColor = { critical: 'var(--red)', high: 'var(--orange)', info: 'var(--blue)', success: 'var(--green)' }; list.innerHTML = notifs.map(n => `
${n.msg}
${n.time}
`).join(''); } function markNotifRead(id) { const n = APP_STATE.notifications.find(n => n.id === id); if (n) n.read = true; renderNotifications(); } function markAllRead() { APP_STATE.notifications.forEach(n => n.read = true); renderNotifications(); } // ─── TOAST ─────────────────────────────────────────────────────── function showToast(title, msg, type = 'info') { const icons = { info: 'ℹ️', success: '✅', warning: '⚠️', error: '❌' }; const el = document.createElement('div'); el.className = 'toast'; el.innerHTML = `
${icons[type]}
${title}
${msg ? `
${msg}
` : ''}
`; document.getElementById('toast-container').appendChild(el); requestAnimationFrame(() => el.classList.add('show')); setTimeout(() => { el.classList.remove('show'); setTimeout(() => el.remove(), 300); }, 3500); } // ─── ISSUE HELPERS ──────────────────────────────────────────────── function getSevBadge(sev) { const m = { critical:'Kritis', high:'Tinggi', medium:'Sedang', low:'Rendah' }; return `${m[sev] || sev}`; } function getStatBadge(st) { const m = { open:'Terbuka', in_progress:'Diproses', resolved:'Selesai', closed:'Ditutup' }; return `${m[st] || st}`; } function getProgramBadge(pid) { const p = getProgramById(pid); if (!p) return ''; return `${p.icon} ${p.shortName}`; } function getTlIcon(type) { return { created:'🔴', assigned:'🔵', verified:'🟡', survey:'🟣', escalated:'🟠', resolved:'✅' }[type] || '⚪'; } function formatDate(ds) { if (!ds) return '—'; const d = new Date(ds); return d.toLocaleDateString('id-ID', { day:'numeric', month:'short', year:'numeric' }); } // ─── OPEN ISSUE DETAIL ──────────────────────────────────────────── function openIssueDetail(issueId) { const issue = APP_STATE.issues.find(i => i.id === issueId); if (!issue) return; const prog = getProgramById(issue.programId); document.getElementById('md-issue-id').textContent = issue.id; document.getElementById('md-issue-title').textContent = issue.judul; const surveys = APP_STATE.surveys.filter(s => issue.surveyIds.includes(s.id)); const escalations = APP_STATE.escalations.filter(e => issue.escalationIds.includes(e.id)); document.getElementById('md-issue-body').innerHTML = `
${getProgramBadge(issue.programId)} ${getSevBadge(issue.severity)} ${getStatBadge(issue.status)} 📍 ${issue.daerah}, ${issue.provinsi} 📅 ${formatDate(issue.createdAt)}
${issue.deskripsi}
👤 Ditugaskan ke: ${issue.assignee || 'Belum ditugaskan'}
${escalations.length ? `
Eskalasi
${escalations.map(e => `
${e.levelName} ${e.status==='resolved'?'Selesai':e.status==='approved'?'Disetujui':'Menunggu'}
Ditujukan ke: ${e.assignTo}
${e.notes}
`).join('')}
` : ''} ${surveys.length ? `
Survey Lapangan Terkait
${surveys.map(s => `
${s.namaOfficer} — ${s.tanggalSurvey} ${{draft:'Draft',submitted:'Diajukan',verified:'Diverifikasi'}[s.status]}
${s.temuanUtama}
${s.rekomendasi ? `
💡 ${s.rekomendasi}
` : ''}
`).join('')}
` : ''}
Riwayat Aktivitas
${issue.timeline.map(t => `
${getTlIcon(t.type)}
${t.action}
${t.actor}·${t.time}
`).join('')}
`; const footer = document.getElementById('md-issue-footer'); let actionBtns = ''; if (issue.status !== 'resolved' && issue.status !== 'closed') { actionBtns = ` `; } footer.innerHTML = `${actionBtns}`; openModal('issue-detail-modal'); } function issueAction(issueId, action) { const issue = APP_STATE.issues.find(i => i.id === issueId); if (!issue) return; const ts = new Date().toLocaleTimeString('id-ID') + ' WIB'; if (action === 'assign_survey') { closeModal('issue-detail-modal'); openNewSurveyModal(issueId); return; } if (action === 'escalate') { const levels = ['L1','L2','L3','L4']; const lastEsc = APP_STATE.escalations.filter(e => issue.escalationIds.includes(e.id)).pop(); const nextLevel = lastEsc ? levels[Math.min(levels.indexOf(lastEsc.level)+1, 3)] : 'L2'; const newEsc = { id: `ESC-${Date.now().toString().slice(-4)}`, issueId: issue.id, programId: issue.programId, level: nextLevel, levelName: ESCALATION_LEVELS[nextLevel].label, assignTo: 'Tim KSP Pengawasan', notes: `Eskalasi dari issue ${issue.id}`, status: 'pending', createdBy: 'Admin KSP', createdAt: new Date().toISOString().split('T')[0], resolvedAt: null, daerah: issue.daerah, provinsi: issue.provinsi }; APP_STATE.escalations.push(newEsc); issue.escalationIds.push(newEsc.id); issue.timeline.push({ time: ts, action: `Eskalasi ke ${nextLevel} (${ESCALATION_LEVELS[nextLevel].label}) diajukan`, actor: 'Admin KSP', type: 'escalated' }); issue.status = 'in_progress'; showToast('Eskalasi Dibuat', `Issue ${issue.id} dieksalasi ke ${nextLevel}`, 'success'); } if (action === 'resolve') { issue.status = 'resolved'; issue.updatedAt = new Date().toISOString().split('T')[0]; issue.timeline.push({ time: ts, action: 'Issue dinyatakan selesai oleh Admin KSP', actor: 'Admin KSP', type: 'resolved' }); // Update location issue count const loc = APP_STATE.locs.find(l => l.name === issue.daerah); if (loc && loc.issueCount > 0) { loc.issueCount--; if (issue.severity === 'critical' && loc.criticalCount > 0) loc.criticalCount--; } showToast('Issue Diselesaikan', `${issue.id} berhasil ditandai selesai`, 'success'); } closeModal('issue-detail-modal'); updateIssuesBadge(); if (APP_STATE.currentPage === 'issues') renderIssuesPage(); if (APP_STATE.currentPage === 'escalasi') renderEscalasiPage(); } // ─── NEW ISSUE FORM ─────────────────────────────────────────────── function openNewIssueModal() { openModal('new-issue-modal'); } function submitNewIssue(e) { e.preventDefault(); const id = `ISS-${String(APP_STATE.issues.length + 1).padStart(3, '0')}`; const now = new Date().toISOString().split('T')[0]; const ts = new Date().toLocaleTimeString('id-ID') + ' WIB'; const issue = { id, programId: document.getElementById('ni-program').value, daerah: document.getElementById('ni-daerah').value, provinsi: document.getElementById('ni-provinsi').value, judul: document.getElementById('ni-judul').value, deskripsi: document.getElementById('ni-deskripsi').value, severity: document.getElementById('ni-severity').value, status: 'open', assignee: document.getElementById('ni-assignee').value || 'Belum ditugaskan', createdAt: now, updatedAt: now, surveyIds: [], escalationIds: [], timeline: [{ time: ts, action: 'Issue baru dibuat oleh Admin KSP', actor: 'Admin KSP', type: 'created' }] }; APP_STATE.issues.push(issue); APP_STATE.notifications.unshift({ id: `N${Date.now()}`, type: issue.severity === 'critical' ? 'critical' : 'high', msg: `Issue baru: ${issue.judul}`, time: 'Baru saja', read: false }); document.getElementById('new-issue-form').reset(); closeModal('new-issue-modal'); updateIssuesBadge(); renderNotifications(); if (APP_STATE.currentPage === 'issues') renderIssuesPage(); showToast('Issue Ditambahkan', `${id} berhasil dibuat`, 'success'); } // ─── THEME TOGGLE ───────────────────────────────────────────────── function initTheme() { const saved = localStorage.getItem('ksp-theme'); if (saved === 'light') { document.body.classList.add('light-theme'); const btn = document.getElementById('theme-toggle-btn'); if (btn) btn.textContent = '🌙'; } } function toggleTheme() { const isLight = document.body.classList.toggle('light-theme'); const btn = document.getElementById('theme-toggle-btn'); if (btn) { btn.textContent = isLight ? '🌙' : '☀️'; } localStorage.setItem('ksp-theme', isLight ? 'light' : 'dark'); showToast('Tema Diubah', `Berhasil beralih ke Mode ${isLight ? 'Terang' : 'Gelap'}`, 'success'); // Reload map tiles if dashboard map is active if (typeof updateMapTileLayers === 'function' && APP_STATE.mapInstance) { updateMapTileLayers(); } }