From c20afe65439af7d79108608d99dde191a5d7ff6f Mon Sep 17 00:00:00 2001 From: ihamdani Date: Wed, 16 Sep 2026 10:38:01 +0700 Subject: [PATCH] first commit --- app.js | 348 ++++++++++++++++++++++++ data.js | 570 +++++++++++++++++++++++++++++++++++++++ index.html | 429 +++++++++++++++++++++++++++++ pages/dashboard.js | 236 ++++++++++++++++ pages/escalasi.js | 195 ++++++++++++++ pages/integration.js | 243 +++++++++++++++++ pages/issues.js | 151 +++++++++++ pages/programs.js | 247 +++++++++++++++++ pages/survey.js | 259 ++++++++++++++++++ styles.css | 629 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 3307 insertions(+) create mode 100644 app.js create mode 100644 data.js create mode 100644 index.html create mode 100644 pages/dashboard.js create mode 100644 pages/escalasi.js create mode 100644 pages/integration.js create mode 100644 pages/issues.js create mode 100644 pages/programs.js create mode 100644 pages/survey.js create mode 100644 styles.css diff --git a/app.js b/app.js new file mode 100644 index 0000000..10a4ee4 --- /dev/null +++ b/app.js @@ -0,0 +1,348 @@ +// ═══════════════════════════════════════════════════════════════════ +// 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(); + } +} + diff --git a/data.js b/data.js new file mode 100644 index 0000000..3676f98 --- /dev/null +++ b/data.js @@ -0,0 +1,570 @@ +// ═══════════════════════════════════════════════════════════════════ +// KSP NATIONAL MONITORING DASHBOARD — DATA LAYER (DUMMY DATA) +// ═══════════════════════════════════════════════════════════════════ + +// ─── PROGRAM DEFINITIONS ──────────────────────────────────────────── +const PROGRAMS = [ + { + id: 'mbg', name: 'Makan Bergizi Gratis', shortName: 'MBG', icon: '🍱', + color: '#22c55e', badge: 'green', + description: 'Pemberian makanan bergizi gratis untuk siswa SD–SMA seluruh Indonesia', + budget: 71000, realisasi: 38420, budgetUnit: 'Miliar Rp', + target: 82500000, capaian: 64300000, targetUnit: 'penerima', + ministry: 'Badan Gizi Nasional', startDate: '2025-01-06', status: 'active', + kpiLabel: ['Sekolah Aktif', 'Porsi/Hari', 'Kabupaten/Kota'], + kpiValue: ['284.220', '17,4 Juta', '514'] + }, + { + id: 'koperasi', name: 'Koperasi Desa Merah Putih', shortName: 'Koperasi', icon: '🏪', + color: '#3b82f6', badge: 'blue', + description: 'Pembentukan dan penguatan koperasi di setiap desa/kelurahan Indonesia', + budget: 40000, realisasi: 18600, budgetUnit: 'Miliar Rp', + target: 80000, capaian: 52400, targetUnit: 'unit koperasi', + ministry: 'KemenKopUKM', startDate: '2025-03-01', status: 'active', + kpiLabel: ['Unit Aktif', 'Total Anggota', 'Modal Tersalur'], + kpiValue: ['52.400', '6,2 Juta', 'Rp 18,6T'] + }, + { + id: 'psn', name: 'Proyek Strategis Nasional', shortName: 'PSN', icon: '🏗️', + color: '#f59e0b', badge: 'amber', + description: 'Percepatan pembangunan infrastruktur strategis nasional lintas sektor', + budget: 185000, realisasi: 112000, budgetUnit: 'Miliar Rp', + target: 218, capaian: 142, targetUnit: 'proyek', + ministry: 'Kemenko Perekonomian', startDate: '2024-10-20', status: 'active', + kpiLabel: ['Proyek Selesai', 'Proyek Berjalan', 'Investasi Masuk'], + kpiValue: ['142', '58', 'Rp 892T'] + }, + { + id: 'hilirisasi', name: 'Hilirisasi Industri', shortName: 'Hilirisasi', icon: '⚙️', + color: '#a855f7', badge: 'purple', + description: 'Transformasi nilai tambah komoditas dalam negeri melalui industrialisasi', + budget: 68000, realisasi: 29800, budgetUnit: 'Miliar Rp', + target: 21, capaian: 11, targetUnit: 'komoditas', + ministry: 'Kemenperin', startDate: '2025-01-01', status: 'active', + kpiLabel: ['Komoditas Aktif', 'Pabrik Baru', 'Nilai Ekspor'], + kpiValue: ['11', '428', 'Rp 142T'] + }, + { + id: 'rumah', name: 'Rumah Rakyat', shortName: 'Rumah', icon: '🏠', + color: '#f97316', badge: 'orange', + description: 'Program 3 juta rumah untuk Masyarakat Berpenghasilan Rendah (MBR)', + budget: 53000, realisasi: 21400, budgetUnit: 'Miliar Rp', + target: 3000000, capaian: 842000, targetUnit: 'unit rumah', + ministry: 'Kemen PUPR', startDate: '2025-02-01', status: 'active', + kpiLabel: ['Unit Selesai', 'Sedang Dibangun', 'KPR Disetujui'], + kpiValue: ['842.000', '1,24 Juta', '620.000'] + } +]; + +// ─── LOCATIONS ─────────────────────────────────────────────────────── +const INITIAL_LOCS = [ + { + id: 0, name: 'Medan', province: 'Sumatra Utara', lat: 3.595, lng: 98.672, + programs: ['mbg', 'koperasi'], + mbg: { schools: 84, meals: 62400, coverage: 94, supplier: 'CV Berkah Pangan Utara', status: 'operational', students: 58200 }, + koperasi: { units: 42, members: 8200, capital: 6.8, activeLoans: 312, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, + issueCount: 1, criticalCount: 0 + }, + { + id: 1, name: 'Pekanbaru', province: 'Riau', lat: 0.507, lng: 101.448, + programs: ['mbg'], + mbg: { schools: 42, meals: 28900, coverage: 88, supplier: 'PT Mitra Gizi Riau', status: 'operational', students: 24600 }, + koperasi: null, psn: null, hilirisasi: null, rumah: null, + issueCount: 0, criticalCount: 0 + }, + { + id: 2, name: 'Palembang', province: 'Sumatra Selatan', lat: -2.990, lng: 104.756, + programs: ['mbg', 'koperasi', 'psn'], + mbg: { schools: 128, meals: 96000, coverage: 91, supplier: 'Koperasi Sriwijaya Pangan', status: 'operational', students: 82000 }, + koperasi: { units: 68, members: 14200, capital: 11.2, activeLoans: 520, status: 'active' }, + psn: { projectName: 'Jalan Tol Palembang–Betung', progress: 72, contractor: 'PT Waskita Karya', value: 4800 }, + hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 3, name: 'Samarinda', province: 'Kalimantan Timur', lat: -0.502, lng: 117.153, + programs: ['koperasi'], + mbg: null, + koperasi: { units: 28, members: 4820, capital: 6.8, activeLoans: 312, status: 'warning' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 2, criticalCount: 0 + }, + { + id: 4, name: 'Balikpapan', province: 'Kalimantan Timur', lat: -1.268, lng: 116.829, + programs: ['koperasi', 'hilirisasi', 'psn'], + mbg: null, + koperasi: { units: 18, members: 2340, capital: 3.2, activeLoans: 148, status: 'active' }, + psn: { projectName: 'Kawasan Industri Kariangau', progress: 58, contractor: 'PT PP (Persero)', value: 12400 }, + hilirisasi: { commodity: 'Batu Bara', stage: 'Coal to Chemical', progress: 45, value: 8200 }, + rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 5, name: 'Makassar', province: 'Sulawesi Selatan', lat: -5.147, lng: 119.432, + programs: ['mbg', 'koperasi', 'psn'], + mbg: { schools: 156, meals: 118000, coverage: 93, supplier: 'PT Sulsel Gizi Prima', status: 'operational', students: 104000 }, + koperasi: { units: 52, members: 6200, capital: 8.4, activeLoans: 480, status: 'active' }, + psn: { projectName: 'Makassar New Port Phase 2', progress: 84, contractor: 'PT Pelindo IV', value: 9600 }, + hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 6, name: 'Surabaya', province: 'Jawa Timur', lat: -7.250, lng: 112.750, + programs: ['mbg', 'koperasi', 'psn', 'rumah'], + mbg: { schools: 218, meals: 182000, coverage: 98, supplier: 'PT Gizi Jatim', status: 'operational', students: 162000 }, + koperasi: { units: 96, members: 8420, capital: 12.4, activeLoans: 640, status: 'active' }, + psn: { projectName: 'LRT Surabaya', progress: 38, contractor: 'PT Adhi Karya', value: 31000 }, + rumah: { units: 12400, completed: 8200, progress: 66, contractor: 'Perum Perumnas', budget: 1240 }, + hilirisasi: null, issueCount: 2, criticalCount: 1 + }, + { + id: 7, name: 'Bandung', province: 'Jawa Barat', lat: -6.914, lng: 107.609, + programs: ['mbg', 'koperasi', 'rumah'], + mbg: { schools: 310, meals: 245000, coverage: 99, supplier: 'Koperasi Jabar Pangan', status: 'operational', students: 220000 }, + koperasi: { units: 128, members: 11200, capital: 18.6, activeLoans: 890, status: 'active' }, + rumah: { units: 18600, completed: 14800, progress: 80, contractor: 'PT Ciputra', budget: 1860 }, + psn: null, hilirisasi: null, issueCount: 0, criticalCount: 0 + }, + { + id: 8, name: 'Semarang', province: 'Jawa Tengah', lat: -6.967, lng: 110.419, + programs: ['mbg', 'koperasi'], + mbg: { schools: 196, meals: 158000, coverage: 97, supplier: 'UD Pangan Maju Jateng', status: 'operational', students: 142000 }, + koperasi: { units: 84, members: 6800, capital: 9.1, activeLoans: 520, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 9, name: 'Yogyakarta', province: 'DI Yogyakarta', lat: -7.797, lng: 110.370, + programs: ['mbg', 'koperasi'], + mbg: { schools: 144, meals: 104000, coverage: 99, supplier: 'Koperasi UGM Pangan', status: 'operational', students: 96000 }, + koperasi: { units: 62, members: 4200, capital: 6.0, activeLoans: 320, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 10, name: 'Denpasar', province: 'Bali', lat: -8.670, lng: 115.213, + programs: ['mbg', 'koperasi', 'psn'], + mbg: { schools: 88, meals: 62000, coverage: 96, supplier: 'UD Boga Bali', status: 'operational', students: 56000 }, + koperasi: { units: 36, members: 3600, capital: 5.2, activeLoans: 240, status: 'active' }, + psn: { projectName: 'Shortcut Mengwitani–Singaraja', progress: 91, contractor: 'PT Hutama Karya', value: 2800 }, + hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 11, name: 'Palu', province: 'Sulawesi Tengah', lat: -0.900, lng: 119.877, + programs: ['koperasi'], + mbg: null, + koperasi: { units: 14, members: 1840, capital: 2.1, activeLoans: 0, status: 'inactive' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 1, criticalCount: 1 + }, + { + id: 12, name: 'Manokwari', province: 'Papua Barat', lat: -0.861, lng: 134.062, + programs: ['mbg'], + mbg: { schools: 28, meals: 3200, coverage: 74, supplier: 'Vendor Tidak Terverifikasi', status: 'under_review', students: 18400 }, + koperasi: null, psn: null, hilirisasi: null, rumah: null, issueCount: 1, criticalCount: 1 + }, + { + id: 13, name: 'Kupang', province: 'NTT', lat: -10.174, lng: 123.607, + programs: ['mbg', 'rumah'], + mbg: { schools: 64, meals: 42000, coverage: 82, supplier: 'PT NTT Sejahtera', status: 'under_review', students: 32000 }, + rumah: { units: 4200, completed: 1800, progress: 43, contractor: 'CV NTT Bangun', budget: 420 }, + koperasi: null, psn: null, hilirisasi: null, issueCount: 2, criticalCount: 0 + }, + { + id: 14, name: 'Jayapura', province: 'Papua', lat: -2.538, lng: 140.718, + programs: ['mbg', 'koperasi', 'psn'], + mbg: { schools: 52, meals: 36000, coverage: 78, supplier: 'Koperasi Papua Sehat', status: 'operational', students: 28000 }, + koperasi: { units: 22, members: 1820, capital: 2.6, activeLoans: 88, status: 'active' }, + psn: { projectName: 'Trans Papua Jalan Nasional', progress: 28, contractor: 'PT Brantas Abipraya', value: 18600 }, + hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 15, name: 'Pontianak', province: 'Kalimantan Barat', lat: -0.026, lng: 109.343, + programs: ['koperasi', 'hilirisasi'], + mbg: null, + koperasi: { units: 32, members: 3120, capital: 4.0, activeLoans: 198, status: 'active' }, + hilirisasi: { commodity: 'Kelapa Sawit', stage: 'Oleochemical', progress: 68, value: 6400 }, + psn: null, rumah: null, issueCount: 1, criticalCount: 0 + }, + { + id: 16, name: 'Ambon', province: 'Maluku', lat: -3.695, lng: 128.181, + programs: ['mbg'], + mbg: { schools: 38, meals: 24500, coverage: 85, supplier: 'PT Maluku Pangan Mandiri', status: 'operational', students: 21000 }, + koperasi: null, psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 17, name: 'Mataram', province: 'NTB', lat: -8.582, lng: 116.117, + programs: ['mbg', 'koperasi'], + mbg: { schools: 76, meals: 54000, coverage: 92, supplier: 'Koperasi Lombok Sejahtera', status: 'operational', students: 49000 }, + koperasi: { units: 44, members: 2850, capital: 3.8, activeLoans: 154, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 18, name: 'Manado', province: 'Sulawesi Utara', lat: 1.475, lng: 124.843, + programs: ['koperasi', 'hilirisasi'], + mbg: null, + koperasi: { units: 24, members: 1950, capital: 2.9, activeLoans: 112, status: 'active' }, + hilirisasi: { commodity: 'Nikel', stage: 'Nickel Matte Processing', progress: 72, value: 14200 }, + psn: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 19, name: 'Padang', province: 'Sumatra Barat', lat: -0.947, lng: 100.417, + programs: ['mbg'], + mbg: { schools: 98, meals: 72000, coverage: 96, supplier: 'PT Minang Gizi Utama', status: 'operational', students: 67000 }, + koperasi: null, psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + }, + { + id: 20, name: 'Solo', province: 'Jawa Tengah', lat: -7.576, lng: 110.824, + programs: ['mbg', 'rumah'], + mbg: { schools: 112, meals: 89000, coverage: 97, supplier: 'CV Solo Gizi Prima', status: 'operational', students: 81200 }, + rumah: { units: 8400, completed: 6200, progress: 74, contractor: 'Perum Perumnas', budget: 840 }, + koperasi: null, psn: null, hilirisasi: null, issueCount: 0, criticalCount: 0 + }, + { + id: 21, name: 'Malang', province: 'Jawa Timur', lat: -7.983, lng: 112.621, + programs: ['mbg', 'koperasi'], + mbg: { schools: 168, meals: 124000, coverage: 95, supplier: 'PT Malang Sejahtera', status: 'operational', students: 112000 }, + koperasi: { units: 72, members: 5600, capital: 7.8, activeLoans: 420, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 1, criticalCount: 0 + }, + { + id: 22, name: 'Banjarmasin', province: 'Kalimantan Selatan', lat: -3.320, lng: 114.592, + programs: ['mbg', 'koperasi', 'hilirisasi'], + mbg: { schools: 88, meals: 64000, coverage: 90, supplier: 'PT Kalsel Pangan', status: 'operational', students: 58000 }, + koperasi: { units: 38, members: 3400, capital: 4.6, activeLoans: 210, status: 'active' }, + hilirisasi: { commodity: 'Batu Bara', stage: 'Coal Gasification', progress: 32, value: 5800 }, + psn: null, rumah: null, issueCount: 1, criticalCount: 1 + }, + { + id: 23, name: 'Bengkulu', province: 'Bengkulu', lat: -3.800, lng: 102.267, + programs: ['mbg', 'rumah'], + mbg: { schools: 54, meals: 38000, coverage: 84, supplier: 'Koperasi Bengkulu Mandiri', status: 'operational', students: 34000 }, + rumah: { units: 2800, completed: 980, progress: 35, contractor: 'CV Bumi Bengkulu', budget: 280 }, + koperasi: null, psn: null, hilirisasi: null, issueCount: 1, criticalCount: 0 + }, + { + id: 24, name: 'Banda Aceh', province: 'Aceh', lat: 5.548, lng: 95.323, + programs: ['mbg', 'koperasi'], + mbg: { schools: 72, meals: 52000, coverage: 88, supplier: 'Koperasi Aceh Sejahtera', status: 'operational', students: 48000 }, + koperasi: { units: 36, members: 2800, capital: 3.4, activeLoans: 162, status: 'active' }, + psn: null, hilirisasi: null, rumah: null, issueCount: 0, criticalCount: 0 + } +]; + +// ─── ISSUES ───────────────────────────────────────────────────────── +const INITIAL_ISSUES = [ + { + id: 'ISS-001', programId: 'mbg', + daerah: 'Manokwari', provinsi: 'Papua Barat', + judul: 'Ketidaksesuaian Laporan Distribusi MBG — Vendor Tidak Terverifikasi', + deskripsi: '3.200 porsi makanan dilaporkan terdistribusi namun tidak ada satu pun konfirmasi pengiriman dari pihak vendor. Data harian sistem SIMBG menunjukkan gap yang signifikan. Vendor yang terdaftar tidak ditemukan di database resmi BGN.', + severity: 'critical', status: 'in_progress', + assignee: 'Tim Verifikasi KSP — Region Timur', + createdAt: '2026-06-15', updatedAt: '2026-06-22', + surveyIds: ['SRV-001'], escalationIds: ['ESC-001'], + timeline: [ + { time: '2026-06-15 08:00', action: 'Issue terdeteksi oleh sistem monitoring SIMBG', actor: 'Sistem Otomatis', type: 'created' }, + { time: '2026-06-16 09:30', action: 'Issue diverifikasi manual oleh analis senior', actor: 'Budi Santoso', type: 'verified' }, + { time: '2026-06-17 14:00', action: 'Tim investigasi lapangan ditugaskan ke Manokwari', actor: 'KSP Div. Pengawasan', type: 'assigned' }, + { time: '2026-06-18 10:00', action: 'Survey lapangan dilaksanakan — temuan kritis', actor: 'Hendra Wijaya', type: 'survey' }, + { time: '2026-06-20 16:00', action: 'Eskalasi ke Level 3 (BGN) diajukan', actor: 'KSP Div. Pengawasan', type: 'escalated' } + ] + }, + { + id: 'ISS-002', programId: 'koperasi', + daerah: 'Palu', provinsi: 'Sulawesi Tengah', + judul: 'Koperasi Fiktif — Modal Disalurkan Tanpa Aktivitas Bisnis 42 Hari', + deskripsi: 'Modal Rp 2,1 Miliar telah disalurkan ke 3 unit koperasi di Palu namun tidak ada aktivitas bisnis yang tercatat selama 42 hari berturut-turut. Alamat fisik koperasi tidak dapat ditemukan saat verifikasi lapangan.', + severity: 'critical', status: 'in_progress', + assignee: 'Tim Investigasi Khusus KSP', + createdAt: '2026-06-10', updatedAt: '2026-06-23', + surveyIds: ['SRV-002'], escalationIds: ['ESC-002'], + timeline: [ + { time: '2026-06-10 07:00', action: 'Anomali zero-activity terdeteksi (42 hari berturut-turut)', actor: 'Sistem Otomatis', type: 'created' }, + { time: '2026-06-11 11:00', action: 'Cross-referensi data NIK anggota dilakukan', actor: 'Analis Senior', type: 'verified' }, + { time: '2026-06-13 09:00', action: 'Tim Investigasi Khusus ditugaskan oleh Deputi KSP', actor: 'Deputi KSP', type: 'assigned' }, + { time: '2026-06-15 08:00', action: 'Survey lapangan: alamat koperasi tidak ditemukan', actor: 'Ratna Dewi', type: 'survey' }, + { time: '2026-06-18 14:00', action: 'Eskalasi L4 ke Kejaksaan Agung & POLRI', actor: 'KSP / Deputi', type: 'escalated' } + ] + }, + { + id: 'ISS-003', programId: 'mbg', + daerah: 'Kupang', provinsi: 'NTT', + judul: 'Lonjakan Penerima MBG 340% Tanpa Dasar Data Kemendikbud', + deskripsi: 'Jumlah penerima MBG di Kupang melonjak 340% dalam 14 hari (dari 4.200 menjadi 18.500) tanpa ada perubahan data jumlah siswa dari Kemendikbud. Tidak ada penambahan sekolah baru yang terverifikasi.', + severity: 'high', status: 'open', + assignee: 'Belum ditugaskan', + createdAt: '2026-06-20', updatedAt: '2026-06-20', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-20 13:00', action: 'Anomali lonjakan data NIK penerima terdeteksi otomatis', actor: 'Sistem Otomatis', type: 'created' } + ] + }, + { + id: 'ISS-004', programId: 'koperasi', + daerah: 'Samarinda', provinsi: 'Kalimantan Timur', + judul: '2 Unit Koperasi Terdaftar di Alamat Fisik Identik', + deskripsi: 'Dua unit koperasi ditemukan terdaftar dengan alamat fisik yang persis sama: Jl. Pattimura No.14, Samarinda. Verifikasi lapangan menunjukkan lokasi tersebut bukan bangunan komersial melainkan rumah tinggal warga biasa.', + severity: 'high', status: 'open', + assignee: 'Belum ditugaskan', + createdAt: '2026-06-18', updatedAt: '2026-06-18', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-18 10:00', action: 'Duplikasi alamat koperasi terdeteksi saat rekonsiliasi data', actor: 'Sistem Otomatis', type: 'created' } + ] + }, + { + id: 'ISS-005', programId: 'mbg', + daerah: 'Surabaya', provinsi: 'Jawa Timur', + judul: 'Keterlambatan Pembayaran Vendor MBG Lebih dari 60 Hari', + deskripsi: 'PT Gizi Jatim selaku vendor utama MBG Surabaya mengalami keterlambatan pembayaran dari pemda lebih dari 60 hari. Vendor mengancam hentikan pengiriman jika tidak ada kepastian pembayaran dalam 7 hari. Berpotensi ganggu 182.000 porsi/hari.', + severity: 'critical', status: 'in_progress', + assignee: 'Tim Koordinasi KSP–Kemenkeu', + createdAt: '2026-06-19', updatedAt: '2026-06-24', + surveyIds: [], escalationIds: ['ESC-003'], + timeline: [ + { time: '2026-06-19 09:00', action: 'Laporan keterlambatan pembayaran masuk dari vendor', actor: 'PT Gizi Jatim', type: 'created' }, + { time: '2026-06-20 14:00', action: 'Verifikasi tagihan vendor oleh tim keuangan daerah', actor: 'Kemenkeu Region Jatim', type: 'verified' }, + { time: '2026-06-22 10:00', action: 'Eskalasi L2 ke Kemenkeu Pusat diajukan', actor: 'KSP Div. Koordinasi', type: 'escalated' } + ] + }, + { + id: 'ISS-006', programId: 'psn', + daerah: 'Surabaya', provinsi: 'Jawa Timur', + judul: 'Progress LRT Surabaya Terlambat 28% dari Jadwal Kuartal 2', + deskripsi: 'Proyek LRT Surabaya baru mencapai progress 38% sementara target Q2-2026 seharusnya 66%. Keterlambatan karena pembebasan lahan belum tuntas di 3 segmen utama. Risiko keterlambatan selesai bertambah 18 bulan.', + severity: 'high', status: 'in_progress', + assignee: 'Tim Monev PSN Jawa Timur', + createdAt: '2026-06-14', updatedAt: '2026-06-21', + surveyIds: ['SRV-003'], escalationIds: [], + timeline: [ + { time: '2026-06-14 11:00', action: 'Review progress Q2 menunjukkan keterlambatan signifikan', actor: 'Tim Monev PSN', type: 'created' }, + { time: '2026-06-16 09:00', action: 'Rapat koordinasi dengan PT Adhi Karya & BPJT', actor: 'KSP + BPJT', type: 'verified' }, + { time: '2026-06-21 14:00', action: 'Site visit dan assessment lahan oleh tim lapangan', actor: 'Tim PSN Jatim', type: 'survey' } + ] + }, + { + id: 'ISS-007', programId: 'rumah', + daerah: 'Kupang', provinsi: 'NTT', + judul: 'Kualitas Konstruksi Rumah Rakyat Tidak Memenuhi Standar SNI', + deskripsi: 'Inspeksi lapangan menemukan 480 unit rumah yang telah "diselesaikan" memiliki kualitas di bawah standar SNI: dinding retak, pondasi tidak memenuhi spesifikasi, material berbeda dari kontrak. Berpotensi membahayakan penghuni.', + severity: 'high', status: 'open', + assignee: 'Belum ditugaskan', + createdAt: '2026-06-21', updatedAt: '2026-06-21', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-21 14:00', action: 'Laporan inspeksi kualitas diterima dari surveyor PUPR', actor: 'Tim Inspeksi PUPR', type: 'created' } + ] + }, + { + id: 'ISS-008', programId: 'koperasi', + daerah: 'Pontianak', provinsi: 'Kalimantan Barat', + judul: 'Anggota Koperasi Terdaftar Ganda di 3 Unit Berbeda', + deskripsi: 'Analisis data menemukan 840 orang yang sama terdaftar sebagai anggota aktif di 3 unit koperasi berbeda di Pontianak secara bersamaan. Melanggar aturan keanggotaan dan menyebabkan triple-counting laporan kinerja.', + severity: 'medium', status: 'open', + assignee: 'Belum ditugaskan', + createdAt: '2026-06-22', updatedAt: '2026-06-22', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-22 09:00', action: 'Duplikasi data anggota terdeteksi dalam rekonsiliasi SIKOPNAS', actor: 'Sistem Otomatis', type: 'created' } + ] + }, + { + id: 'ISS-009', programId: 'mbg', + daerah: 'Malang', provinsi: 'Jawa Timur', + judul: 'Kandungan Gizi Menu MBG di Bawah Standar Minimum BGN', + deskripsi: 'Uji laboratorium terhadap 24 sampel makanan MBG di 8 sekolah Malang menunjukkan kandungan protein rata-rata hanya 42% dari standar minimum BGN. Beberapa sampel mengandung pengawet yang tidak diizinkan.', + severity: 'medium', status: 'in_progress', + assignee: 'BGN Lab & Tim Verifikasi', + createdAt: '2026-06-17', updatedAt: '2026-06-22', + surveyIds: ['SRV-004'], escalationIds: [], + timeline: [ + { time: '2026-06-17 10:00', action: 'Hasil uji lab diterima: kualitas gizi di bawah standar', actor: 'BGN Lab Jatim', type: 'created' }, + { time: '2026-06-18 09:00', action: 'Verifikasi lapangan ke 8 sekolah oleh tim gizi', actor: 'Tim Gizi BGN', type: 'verified' }, + { time: '2026-06-22 14:00', action: 'Surat peringatan resmi dikirimkan ke vendor', actor: 'BGN + KSP', type: 'assigned' } + ] + }, + { + id: 'ISS-010', programId: 'hilirisasi', + daerah: 'Banjarmasin', provinsi: 'Kalimantan Selatan', + judul: 'Proyek Coal Gasification Terhenti — Izin Lingkungan Dicabut KLHK', + deskripsi: 'Proyek hilirisasi batu bara Coal Gasification terhenti karena KLHK mencabut izin lingkungan setelah ditemukan AMDAL awal menggunakan data palsu. Progress terhenti di 32% dan investasi Rp 5,8T terancam mangkrak.', + severity: 'critical', status: 'open', + assignee: 'Belum ditugaskan', + createdAt: '2026-06-23', updatedAt: '2026-06-23', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-23 08:00', action: 'Keputusan pencabutan izin lingkungan KLHK diterima', actor: 'KLHK', type: 'created' } + ] + }, + { + id: 'ISS-011', programId: 'rumah', + daerah: 'Bengkulu', provinsi: 'Bengkulu', + judul: 'Realisasi Rumah Rakyat Hanya 35% — Jauh di Bawah Target Semester 1', + deskripsi: 'Target pembangunan 2.800 unit rumah rakyat di Bengkulu baru terealisasi 980 unit (35%) akhir semester 1. Audit menunjukkan keterlambatan administrasi kontrak sejak awal sebagai penyebab utama, bukan cuaca.', + severity: 'medium', status: 'in_progress', + assignee: 'Tim Monev PUPR Bengkulu', + createdAt: '2026-06-15', updatedAt: '2026-06-20', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-15 09:00', action: 'Evaluasi semester 1: realisasi jauh di bawah target', actor: 'PUPR Bengkulu', type: 'created' }, + { time: '2026-06-18 11:00', action: 'Rapat koordinasi dengan CV Bumi Bengkulu dilaksanakan', actor: 'Tim Monev PUPR', type: 'assigned' } + ] + }, + { + id: 'ISS-012', programId: 'mbg', + daerah: 'Medan', provinsi: 'Sumatra Utara', + judul: 'Gap Data Distribusi 2 Sekolah — 8 Hari Berturut-turut', + deskripsi: '2 sekolah di Medan Kota melaporkan nol distribusi makanan selama 8 hari sekolah berturut-turut, sementara log pengiriman supplier menunjukkan pengiriman berhasil.', + severity: 'medium', status: 'resolved', + assignee: 'Tim Verifikasi KSP Sumatra Utara', + createdAt: '2026-06-05', updatedAt: '2026-06-15', + surveyIds: ['SRV-005'], escalationIds: [], + timeline: [ + { time: '2026-06-05 08:00', action: 'Gap distribusi terdeteksi oleh sistem SIMBG', actor: 'Sistem Otomatis', type: 'created' }, + { time: '2026-06-07 10:00', action: 'Verifikasi dengan kepala sekolah dilakukan', actor: 'Tim KSP Sumut', type: 'verified' }, + { time: '2026-06-10 09:00', action: 'Survey lapangan ke 2 sekolah bermasalah', actor: 'Andi Kusuma', type: 'survey' }, + { time: '2026-06-15 14:00', action: 'RESOLVED: sistem pencatatan sekolah error, data telah diperbaiki', actor: 'Tim IT BGN', type: 'resolved' } + ] + }, + { + id: 'ISS-013', programId: 'koperasi', + daerah: 'Samarinda', provinsi: 'Kalimantan Timur', + judul: '1.240 NIK Anggota Koperasi Tidak Valid dalam DTSEN', + deskripsi: 'Rekonsiliasi data koperasi Samarinda dengan DTSEN menemukan 1.240 NIK anggota tidak valid: 820 NIK tidak terdaftar di Dukcapil dan 420 NIK merupakan duplikasi. Berpotensi menyebabkan penyaluran modal ke penerima yang salah.', + severity: 'high', status: 'in_progress', + assignee: 'Tim Rekonsiliasi Data KemenKopUKM', + createdAt: '2026-06-12', updatedAt: '2026-06-22', + surveyIds: [], escalationIds: [], + timeline: [ + { time: '2026-06-12 11:00', action: 'Rekonsiliasi otomatis SIKOPNAS×DTSEN menemukan anomali NIK', actor: 'Sistem Otomatis', type: 'created' }, + { time: '2026-06-14 09:00', action: 'Tim verifikasi data ditugaskan oleh KemenKopUKM', actor: 'KemenKopUKM', type: 'assigned' }, + { time: '2026-06-22 14:00', action: 'Proses pembersihan data berlangsung (840/1.240 NIK selesai)', actor: 'Tim Rekonsiliasi', type: 'verified' } + ] + } +]; + +// ─── SURVEYS ───────────────────────────────────────────────────────── +const INITIAL_SURVEYS = [ + { + id: 'SRV-001', issueId: 'ISS-001', programId: 'mbg', + namaOfficer: 'Hendra Wijaya', jabatan: 'Analis Senior KSP', + tanggalSurvey: '2026-06-18', daerah: 'Manokwari', provinsi: 'Papua Barat', + status: 'verified', + temuanUtama: 'Sistem pencatatan vendor tidak sinkron dengan SIMBG. Terdapat gap 3 hari tanpa upload data. Gudang vendor ditemukan kosong saat dikunjungi. Tidak ada jejak pengiriman fisik.', + rekomendasi: 'Audit menyeluruh terhadap vendor dan integrasi langsung sistem vendor ke SIMBG. Pertimbangkan penggantian vendor segera. Laporkan ke BGN untuk tindakan administratif.', + skorKeparahan: 1, verifiedBy: 'Deputi Pengawasan KSP', createdAt: '2026-06-18' + }, + { + id: 'SRV-002', issueId: 'ISS-002', programId: 'koperasi', + namaOfficer: 'Ratna Dewi Puspita', jabatan: 'Investigator KSP', + tanggalSurvey: '2026-06-15', daerah: 'Palu', provinsi: 'Sulawesi Tengah', + status: 'verified', + temuanUtama: 'Ketiga alamat koperasi tidak ditemukan. Lokasi 1: warung tutup & kosong. Lokasi 2: tanah kosong berpagar. Lokasi 3: rumah warga yang tidak mengetahui adanya koperasi. Nomor pengurus tidak aktif semua.', + rekomendasi: 'Pembekuan segera pencairan modal. Laporkan ke POLRI dan Kejaksaan untuk tindak pidana penipuan. Cabut izin operasional ketiga koperasi fiktif.', + skorKeparahan: 1, verifiedBy: 'Deputi Pengawasan KSP', createdAt: '2026-06-15' + }, + { + id: 'SRV-003', issueId: 'ISS-006', programId: 'psn', + namaOfficer: 'Dimas Prasetyo', jabatan: 'Inspektor PSN KSP', + tanggalSurvey: '2026-06-21', daerah: 'Surabaya', provinsi: 'Jawa Timur', + status: 'submitted', + temuanUtama: 'Pembebasan lahan Segmen 2 (6,2 km) belum tuntas — 14 bidang masih sengketa. Segmen 3 terhenti total karena dispute tarif ganti rugi. Alat berat idle sejak 45 hari. Kontraktor mengklaim force majeure.', + rekomendasi: 'Percepat mediasi lahan dengan BPN dan Pemda. Kaji ulang klaim force majeure. Pertimbangkan denda keterlambatan. Evaluasi kapasitas kontraktor.', + skorKeparahan: 3, verifiedBy: null, createdAt: '2026-06-21' + }, + { + id: 'SRV-004', issueId: 'ISS-009', programId: 'mbg', + namaOfficer: 'Dr. Siti Rahmawati', jabatan: 'Ahli Gizi Nasional BGN', + tanggalSurvey: '2026-06-18', daerah: 'Malang', provinsi: 'Jawa Timur', + status: 'verified', + temuanUtama: 'Dari 24 sampel: protein rata-rata 10,5g (standar minimum 25g). 8 sampel mengandung Natrium Benzoat >50mg/kg (batas aman). Dapur produksi vendor tidak memiliki sertifikasi HACCP. Bahan baku menggunakan ayam broiler kualitas C.', + rekomendasi: 'Ganti vendor segera. Wajibkan sertifikasi HACCP. Lakukan uji lab random setiap bulan. Pertimbangkan blacklist vendor saat ini dari program MBG.', + skorKeparahan: 2, verifiedBy: 'Kepala BGN Jatim', createdAt: '2026-06-18' + }, + { + id: 'SRV-005', issueId: 'ISS-012', programId: 'mbg', + namaOfficer: 'Andi Kusuma', jabatan: 'Analis KSP Wilayah Sumatra', + tanggalSurvey: '2026-06-10', daerah: 'Medan', provinsi: 'Sumatra Utara', + status: 'verified', + temuanUtama: 'Sistem pencatatan sekolah menggunakan aplikasi lama yang mengalami bug saat update versi baru. Data tidak ter-upload ke SIMBG secara otomatis. Makanan tetap diterima dan terdistribusi kepada siswa secara normal.', + rekomendasi: 'Update sistem pencatatan di semua sekolah. Training ulang operator. Pasang monitoring real-time untuk deteksi dini gap pencatatan.', + skorKeparahan: 4, verifiedBy: 'Tim KSP Sumatra Utara', createdAt: '2026-06-10' + } +]; + +// ─── ESCALATIONS ───────────────────────────────────────────────────── +const INITIAL_ESCALATIONS = [ + { + id: 'ESC-001', issueId: 'ISS-001', programId: 'mbg', + level: 'L3', levelName: 'Eskalasi Kementerian/Lembaga', + assignTo: 'Badan Gizi Nasional (BGN)', + notes: 'Issue memerlukan intervensi langsung BGN untuk verifikasi vendor dan audit rantai pasok. KSP merekomendasikan penghentian sementara vendor hingga investigasi selesai.', + status: 'pending', createdBy: 'KSP Div. Pengawasan', + createdAt: '2026-06-20', resolvedAt: null, + daerah: 'Manokwari', provinsi: 'Papua Barat' + }, + { + id: 'ESC-002', issueId: 'ISS-002', programId: 'koperasi', + level: 'L4', levelName: 'Eskalasi Penegakan Hukum', + assignTo: 'Kejaksaan Agung & POLRI', + notes: 'Temuan lapangan menunjukkan indikasi tindak pidana: pendirian koperasi fiktif untuk menyerap dana pemerintah. Diperlukan penyelidikan pidana oleh aparat penegak hukum.', + status: 'approved', createdBy: 'Deputi KSP', + createdAt: '2026-06-18', resolvedAt: null, + daerah: 'Palu', provinsi: 'Sulawesi Tengah' + }, + { + id: 'ESC-003', issueId: 'ISS-005', programId: 'mbg', + level: 'L2', levelName: 'Eskalasi Internal KSP', + assignTo: 'Deputi Koordinasi KSP + Kemenkeu Pusat', + notes: 'Diperlukan percepatan pencairan pembayaran ke vendor MBG Surabaya untuk mencegah penghentian layanan yang berdampak pada 182.000 penerima manfaat.', + status: 'resolved', createdBy: 'KSP Div. Koordinasi', + createdAt: '2026-06-22', resolvedAt: '2026-06-24', + daerah: 'Surabaya', provinsi: 'Jawa Timur' + } +]; + +// ─── NOTIFICATION FEED ──────────────────────────────────────────────── +const INITIAL_NOTIFICATIONS = [ + { id: 'N1', type: 'critical', msg: 'ISS-001: Vendor MBG Manokwari tidak terverifikasi — perlu tindakan segera', time: '10 menit lalu', read: false }, + { id: 'N2', type: 'critical', msg: 'ISS-002: Koperasi fiktif Palu — eskalasi ke Kejaksaan telah disetujui', time: '1 jam lalu', read: false }, + { id: 'N3', type: 'high', msg: 'ISS-010: Izin lingkungan proyek Banjarmasin dicabut KLHK', time: '3 jam lalu', read: false }, + { id: 'N4', type: 'info', msg: 'SRV-003: Survey PSN Surabaya menunggu verifikasi', time: '5 jam lalu', read: true }, + { id: 'N5', type: 'success', msg: 'ISS-012: Issue distribusi MBG Medan berhasil diselesaikan', time: 'Kemarin', read: true } +]; + +// ─── HELPER UTILITIES ───────────────────────────────────────────────── +function getProgramById(id) { return PROGRAMS.find(p => p.id === id); } +function getIssueById(id) { return APP_STATE.issues.find(i => i.id === id); } +function getSurveyById(id) { return APP_STATE.surveys.find(s => s.id === id); } +function getEscalationById(id) { return APP_STATE.escalations.find(e => e.id === id); } + +const SEVERITY_CONFIG = { + critical: { label: 'Kritis', color: '#ef4444', bg: 'rgba(239,68,68,0.12)', icon: '🔴' }, + high: { label: 'Tinggi', color: '#f97316', bg: 'rgba(249,115,22,0.12)', icon: '🟠' }, + medium: { label: 'Sedang', color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', icon: '🟡' }, + low: { label: 'Rendah', color: '#22c55e', bg: 'rgba(34,197,94,0.12)', icon: '🟢' } +}; +const STATUS_CONFIG = { + open: { label: 'Terbuka', color: '#ef4444', bg: 'rgba(239,68,68,0.10)' }, + in_progress: { label: 'Diproses', color: '#3b82f6', bg: 'rgba(59,130,246,0.10)' }, + resolved: { label: 'Selesai', color: '#22c55e', bg: 'rgba(34,197,94,0.10)' }, + closed: { label: 'Ditutup', color: '#71717a', bg: 'rgba(113,113,122,0.10)' } +}; +const ESCALATION_LEVELS = { + L1: { label: 'L1 — Daerah', color: '#22c55e' }, + L2: { label: 'L2 — KSP Internal', color: '#3b82f6' }, + L3: { label: 'L3 — Kementerian', color: '#f59e0b' }, + L4: { label: 'L4 — Penegakan Hukum', color: '#ef4444' } +}; + +// ─── GLOBAL APP STATE ───────────────────────────────────────────────── +const APP_STATE = { + locs: JSON.parse(JSON.stringify(INITIAL_LOCS)), + issues: JSON.parse(JSON.stringify(INITIAL_ISSUES)), + surveys: JSON.parse(JSON.stringify(INITIAL_SURVEYS)), + escalations: JSON.parse(JSON.stringify(INITIAL_ESCALATIONS)), + notifications: JSON.parse(JSON.stringify(INITIAL_NOTIFICATIONS)), + currentPage: 'dashboard', + currentProgram: 'all', + issueFilter: { program: 'all', severity: 'all', status: 'all', search: '' }, + surveyFilter: { program: 'all', status: 'all' }, + selectedIssueId: null, + mapInstance: null, + mapMarkers: {}, + chartInstances: {} +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..985401f --- /dev/null +++ b/index.html @@ -0,0 +1,429 @@ + + + + + + KSP — Dashboard Pengawasan Program Nasional + + + + + + + + + + + + + + +
+ + + + + +
+ + +
+
+
+
Overview Nasional
+
Pemantauan real-time seluruh program strategis pemerintah
+
+
+
+
+
+ LIVE DATA +
+
+
00:00:00
+
Senin, 1 Jan 2026
+
+
+ + + + +
+
+
+ + +
+ + +
+ +
+ +
+
+
+
+
+ + + + + + + +
+
+
Legenda
+
MBG Aktif
+
Koperasi
+
PSN
+
Hilirisasi
+
Rumah Rakyat
+
Ada Masalah
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + + +
+ + +
+ +
+
+
+ + +
+
+ + + + +
+
+
+ + +
+
+
+ + +
+
+
+ +
+
+
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + diff --git a/pages/dashboard.js b/pages/dashboard.js new file mode 100644 index 0000000..a331f00 --- /dev/null +++ b/pages/dashboard.js @@ -0,0 +1,236 @@ +// ═══════════════════════════════════════════════════════════════════ +// 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'); + } +} diff --git a/pages/escalasi.js b/pages/escalasi.js new file mode 100644 index 0000000..bdc7f53 --- /dev/null +++ b/pages/escalasi.js @@ -0,0 +1,195 @@ +// ═══════════════════════════════════════════════════════════════════ +// ESCALASI PAGE — Escalation pipeline and management +// ═══════════════════════════════════════════════════════════════════ + +function renderEscalasiPage() { + const escs = APP_STATE.escalations; + const active = escs.filter(e => e.status !== 'resolved'); + const resolved = escs.filter(e => e.status === 'resolved'); + + const levelCounts = { L1:0, L2:0, L3:0, L4:0 }; + active.forEach(e => { if (levelCounts[e.level] !== undefined) levelCounts[e.level]++; }); + + document.getElementById('escalasi-body').innerHTML = ` +
+ +
+
+
Pipeline Eskalasi
+
Manajemen eskalasi berjenjang: Daerah → KSP → Kementerian → Penegakan Hukum
+
+ +
+ + +
+ ${['L1','L2','L3','L4'].map((lv, idx) => { + const cfg = ESCALATION_LEVELS[lv]; + const desc = { + L1: 'Tim / Dinas Daerah', + L2: 'KSP Internal', + L3: 'Kementerian / Lembaga', + L4: 'Kejaksaan / POLRI / BPK' + }[lv]; + return ` +
+
${levelCounts[lv] > 0 ? levelCounts[lv] : idx+1}
+
${cfg.label.replace(lv+' — ','')}
+
${desc}
+
+ `; + }).join('')} +
+ + +
+
+
${active.length}
+
Eskalasi Aktif
+
+
+
${escs.filter(e=>e.status==='pending').length}
+
Menunggu Persetujuan
+
+
+
${escs.filter(e=>e.status==='approved').length}
+
Disetujui
+
+
+
${resolved.length}
+
Selesai
+
+
+ + +
⚡ Eskalasi Aktif
+ ${active.length === 0 + ? `
Tidak ada eskalasi aktif
Semua eskalasi telah diselesaikan
` + : active.map(e => renderEscCard(e, false)).join('') + } + + ${resolved.length > 0 ? ` +
+
✅ Riwayat Eskalasi Selesai
+ ${resolved.map(e => renderEscCard(e, true)).join('')} + ` : ''} +
+ `; +} + +function renderEscCard(esc, isResolved) { + const issue = APP_STATE.issues.find(i => i.id === esc.issueId); + const lvCfg = ESCALATION_LEVELS[esc.level]; + const statusMap = { pending: { label: 'Menunggu', color: 'var(--amber)', bg: 'var(--amber-soft)' }, approved: { label: 'Disetujui', color: 'var(--blue)', bg: 'var(--blue-soft)' }, rejected: { label: 'Ditolak', color: 'var(--red)', bg: 'var(--red-soft)' }, resolved: { label: 'Selesai', color: 'var(--green)', bg: 'var(--green-soft)' } }; + const st = statusMap[esc.status] || statusMap.pending; + + return ` +
+
+
+
+ ${esc.id} + ${esc.level} + ${st.label} + ${getProgramBadge(esc.programId)} +
+
${esc.levelName}
+
Ditujukan ke: ${esc.assignTo}
+
+
+
📅 ${formatDate(esc.createdAt)}
+ ${esc.resolvedAt ? `
✅ ${formatDate(esc.resolvedAt)}
` : ''} +
+
+ + ${issue ? ` +
+
ISSUE TERKAIT — ${esc.daerah}, ${esc.provinsi}
+
${issue.id}: ${issue.judul.slice(0,70)}${issue.judul.length>70?'...':''}
+
+ ` : ''} + +
${esc.notes}
+ +
+
Dibuat oleh: ${esc.createdBy}
+ ${!isResolved ? ` +
+ ${esc.status === 'pending' ? ` + + + ` : ''} + ${esc.status === 'approved' ? ` + + ` : ''} +
+ ` : ''} +
+
+ `; +} + +function approveEscalation(id) { + const e = APP_STATE.escalations.find(e => e.id === id); + if (!e) return; + e.status = 'approved'; + showToast('Eskalasi Disetujui', `${id} telah disetujui dan diteruskan`, 'success'); + renderEscalasiPage(); +} + +function rejectEscalation(id) { + const e = APP_STATE.escalations.find(e => e.id === id); + if (!e) return; + e.status = 'rejected'; + showToast('Eskalasi Ditolak', `${id} ditolak`, 'warning'); + renderEscalasiPage(); +} + +function resolveEscalation(id) { + const e = APP_STATE.escalations.find(e => e.id === id); + if (!e) return; + e.status = 'resolved'; + e.resolvedAt = new Date().toISOString().split('T')[0]; + showToast('Eskalasi Diselesaikan', `${id} berhasil diselesaikan`, 'success'); + renderEscalasiPage(); + updateIssuesBadge(); +} + +function openNewEscalasiModal() { + // Simple prompt-based creation + const issueList = APP_STATE.issues + .filter(i => i.status !== 'resolved') + .map(i => `${i.id}: ${i.judul.slice(0,50)}`).join('\n'); + const issueId = prompt(`Masukkan ID issue untuk dieskalasi:\n\nIssue aktif:\n${issueList}`); + if (!issueId) return; + const issue = APP_STATE.issues.find(i => i.id.toUpperCase() === issueId.toUpperCase().trim()); + if (!issue) { showToast('Issue Tidak Ditemukan', `ID ${issueId} tidak ditemukan`, 'error'); return; } + + const levelInput = prompt('Pilih level eskalasi:\nL1 - Daerah\nL2 - KSP Internal\nL3 - Kementerian/Lembaga\nL4 - Penegakan Hukum', 'L2'); + const level = (levelInput || 'L2').toUpperCase().trim(); + if (!ESCALATION_LEVELS[level]) { showToast('Level Tidak Valid', 'Pilih L1, L2, L3, atau L4', 'error'); return; } + + const assignTo = prompt('Ditujukan kepada:', 'KSP Div. Pengawasan'); + const notes = prompt('Catatan eskalasi:', `Eskalasi dari issue ${issue.id}`); + + const id = `ESC-${Date.now().toString().slice(-4)}`; + const now = new Date().toISOString().split('T')[0]; + const ts = new Date().toLocaleTimeString('id-ID') + ' WIB'; + + const newEsc = { + id, issueId: issue.id, programId: issue.programId, + level, levelName: ESCALATION_LEVELS[level].label, + assignTo: assignTo || 'KSP Div. Pengawasan', + notes: notes || `Eskalasi dari issue ${issue.id}`, + status: 'pending', createdBy: 'Admin KSP', + createdAt: now, resolvedAt: null, + daerah: issue.daerah, provinsi: issue.provinsi + }; + APP_STATE.escalations.push(newEsc); + issue.escalationIds.push(id); + issue.timeline.push({ time: ts, action: `Eskalasi ${level} dibuat ke ${assignTo}`, actor: 'Admin KSP', type: 'escalated' }); + issue.status = 'in_progress'; + + showToast('Eskalasi Dibuat', `${id} berhasil dibuat`, 'success'); + renderEscalasiPage(); +} diff --git a/pages/integration.js b/pages/integration.js new file mode 100644 index 0000000..bda1a4b --- /dev/null +++ b/pages/integration.js @@ -0,0 +1,243 @@ +// ═══════════════════════════════════════════════════════════════════ +// INTEGRATION PAGE — Infographics for OSMAP, OSPRO, and OSLOG +// ═══════════════════════════════════════════════════════════════════ + +let _integrationInterval = null; + +function renderIntegrationPage() { + const container = document.getElementById('integration-body'); + + container.innerHTML = ` +
+ +
+
Ekosistem Monitoring KSP
+
+ Sistem Pengawasan Nasional Kantor Staf Presiden didukung oleh tiga platform modular utama yang saling terintegrasi: + OSMAP untuk analisis spasial dan pemetaan, + OSPRO untuk manajemen survei lapangan dan eskalasi, serta + OSLOG untuk pelacakan distribusi logistik dan pergerakan personel. +
+
+ + +
+ + +
+
+
+ 🌐 +
+
OSMAP
+
Map Engine & GIS
+
+
+
+ Engine pemetaan wilayah untuk memvisualisasikan cakupan sebaran program strategis, heatmap laporan issue, dan data batas administratif secara nasional. +
+ +
+ +
Layer Terpasang
+
+
+ 🗺️ Map Tiles (CartoDB Dark) + ✓ Aktif +
+
+ 📍 Titik Program (GeoJSON) + ✓ Sinkron +
+
+ 🔴 Heatmap Wilayah Kritis + ✓ Aktif +
+
+
+
+ +
+
+ + +
+
+
+ +
+
OSPRO
+
Task & Survey Engine
+
+
+
+ Sistem penugasan tim lapangan secara terstruktur dari deteksi anomali hingga pelaporan rekomendasi survei di lokasi bermasalah. +
+ +
+ +
Statistik Tugas
+
+
+
0
+
Tugas Aktif
+
+
+
0
+
Tugas Selesai
+
+
+
+
+ +
+
+ + +
+
+
+ 🚚 +
+
OSLOG
+
Tracking & Logistics
+
+
+
+ Solusi pelacakan real-time untuk logistik program (distribusi menu MBG) dan lokasi terkini GPS petugas survei yang sedang berjalan. +
+ +
+ +
Pelacakan Aktif
+
+
+ 📦 Pengiriman MBG Aktif + 14 Armada +
+
+ 👤 GPS Surveyor Aktif + 8 Personel +
+
+
+
+ +
+
+ +
+ + +
+
📋 Alur Penugasan OSPRO (Survey & Escalation Timeline)
+ +
+ +
+ + +
+
⚠️
+
1. Deteksi Issue
+
Anomali terdeteksi SIMBG / Pengaduan
+
+ + +
+
👤
+
2. Penugasan OSPRO
+
Delegasi tugas ke investigator lapangan
+
+ + +
+
🏃
+
3. Survei Lapangan
+
Verifikasi fisik, foto, & entri form OSPRO
+
+ + +
+
+
4. Verifikasi KSP
+
Validasi laporan & eskalasi tindak lanjut
+
+
+
+ + +
+
+
🚚 Log Aktivitas OSLOG (Simulasi Live Feed GPS)
+
+
+ SINKRONISASI GPS +
+
+ +
+ +
+
+ +
+ `; + + // Update counters initially + document.getElementById('ospro-active-tasks').textContent = APP_STATE.surveys.filter(s => s.status !== 'verified').length; + document.getElementById('ospro-verified-tasks').textContent = APP_STATE.surveys.filter(s => s.status === 'verified').length; + + // Render dummy live activity feed for OSLOG + renderOslogLogs(); + + // Set interval to update GPS log simulation every 3 seconds + if (_integrationInterval) clearInterval(_integrationInterval); + _integrationInterval = setInterval(() => { + if (APP_STATE.currentPage === 'integration') { + renderOslogLogs(); + } + }, 3500); +} + +// OSLOG dummy activity data +const OSLOG_LOGS = [ + { device: 'Truk MBG #08 (Medan)', action: 'Pengiriman 840 porsi menu Makan Bergizi selesai di SD N 2 Medan', time: 'Baru saja', type: 'success' }, + { device: 'Surveyor Hendra (Manokwari)', action: 'GPS terdeteksi di lokasi issue ISS-001 (Kec. Manokwari Timur)', time: '1 menit lalu', type: 'location' }, + { device: 'Armada Koperasi #02 (Palu)', action: 'Pengantaran paket modal Koperasi selesai di Kantor Kelurahan Palu', time: '3 menit lalu', type: 'success' }, + { device: 'Surveyor Ratna (Palu)', action: 'Aktivitas survei OSPRO dimulai untuk issue koperasi fiktif Palu', time: '5 menit lalu', type: 'task' }, + { device: 'Truk MBG #12 (Surabaya)', action: 'Truk bergerak meninggalkan Gudang Utama menuju Rute R-04', time: '8 menit lalu', type: 'transit' }, + { device: 'Surveyor Dimas (Surabaya)', action: 'Akurasi GPS terverifikasi stabil (±3m) di kawasan LRT Surabaya', time: '12 menit lalu', type: 'location' } +]; + +function renderOslogLogs() { + const container = document.getElementById('oslog-feed-container'); + if (!container) return; + + // Randomly rotate one of the logs to make it look "live" + const logsToRender = [...OSLOG_LOGS]; + const shuffled = logsToRender.sort(() => 0.5 - Math.random()).slice(0, 4); + + const getLogIcon = (type) => { + switch (type) { + case 'success': return '✅'; + case 'location': return '📍'; + case 'task': return '📋'; + case 'transit': return '🚚'; + default: return '⚪'; + } + }; + + container.innerHTML = shuffled.map(log => ` +
+
+ ${getLogIcon(log.type)} +
+ ${log.device} +
${log.action}
+
+
+ ${log.time} +
+ `).join(''); +} diff --git a/pages/issues.js b/pages/issues.js new file mode 100644 index 0000000..7d9be92 --- /dev/null +++ b/pages/issues.js @@ -0,0 +1,151 @@ +// ═══════════════════════════════════════════════════════════════════ +// ISSUES PAGE — Kanban + List view for issue management +// ═══════════════════════════════════════════════════════════════════ + +let _issueView = 'kanban'; // 'kanban' | 'list' + +function renderIssuesPage() { + const filtered = getFilteredIssues(); + if (_issueView === 'kanban') renderKanbanView(filtered); + else renderListView(filtered); +} + +function applyIssueFilters() { + renderIssuesPage(); +} + +function switchIssueView(view) { + _issueView = view; + document.getElementById('kanban-view-btn').classList.toggle('active', view === 'kanban'); + document.getElementById('list-view-btn').classList.toggle('active', view === 'list'); + renderIssuesPage(); +} + +function getFilteredIssues() { + const search = (document.getElementById('issues-search')?.value || '').toLowerCase().trim(); + const program = document.getElementById('issues-filter-program')?.value || 'all'; + const severity= document.getElementById('issues-filter-severity')?.value || 'all'; + + return APP_STATE.issues.filter(i => { + const matchSearch = !search || i.judul.toLowerCase().includes(search) || i.daerah.toLowerCase().includes(search) || i.provinsi.toLowerCase().includes(search) || i.id.toLowerCase().includes(search); + const matchProg = program === 'all' || i.programId === program; + const matchSev = severity === 'all' || i.severity === severity; + return matchSearch && matchProg && matchSev; + }); +} + +// ─── KANBAN VIEW ───────────────────────────────────────────────── +function renderKanbanView(issues) { + const cols = [ + { key: 'open', label: 'Terbuka', icon: '🔴', color: 'var(--red)' }, + { key: 'in_progress', label: 'Diproses', icon: '🔵', color: 'var(--blue)' }, + { key: 'resolved', label: 'Selesai', icon: '✅', color: 'var(--green)' } + ]; + document.getElementById('issues-body').innerHTML = ` +
+ ${cols.map(col => { + const colIssues = issues.filter(i => i.status === col.key); + return ` +
+
+
+ ${col.icon} + ${col.label} +
+ ${colIssues.length} +
+
+ ${colIssues.length === 0 + ? `
📭
Tidak ada issue
` + : colIssues.map(i => renderIssueCard(i)).join('') + } +
+
+ `; + }).join('')} +
+ `; +} + +function renderIssueCard(issue) { + const prog = getProgramById(issue.programId); + const survCount = issue.surveyIds.length; + const escCount = issue.escalationIds.length; + return ` +
+
+ ${issue.id} + ${getSevBadge(issue.severity)} +
+
+ ${getProgramBadge(issue.programId)} + 📍 ${issue.daerah} +
+
${issue.judul}
+
${issue.deskripsi}
+ +
+ 👤 ${issue.assignee} +
+
+ `; +} + +// ─── LIST / TABLE VIEW ──────────────────────────────────────────── +function renderListView(issues) { + document.getElementById('issues-body').innerHTML = ` +
+ ${issues.length === 0 + ? `
🔍
Tidak ada issue
Tidak ada issue yang cocok dengan filter yang dipilih
` + : `
+ + + + + + + + + + + + + + + + ${issues.map(i => ` + + + + + + + + + + + + `).join('')} + +
IDJudul IssueProgramDaerahKeparahanStatusDitugaskan keTanggal
${i.id} +
${i.judul}
+
${getProgramBadge(i.programId)} +
${i.daerah}
+
${i.provinsi}
+
${getSevBadge(i.severity)}${getStatBadge(i.status)}${i.assignee}${formatDate(i.createdAt)} +
+ ${i.surveyIds.length > 0 ? `📋` : ''} + ${i.escalationIds.length > 0 ? `🔺` : ''} +
+
+
` + } +
+ `; +} diff --git a/pages/programs.js b/pages/programs.js new file mode 100644 index 0000000..ca0bc9f --- /dev/null +++ b/pages/programs.js @@ -0,0 +1,247 @@ +// ═══════════════════════════════════════════════════════════════════ +// PROGRAMS PAGE — Detail per program with stats and province breakdown +// ═══════════════════════════════════════════════════════════════════ + +let _activeProgTab = 'mbg'; + +function renderProgramsPage() { + renderProgTabs(); + renderProgBody(_activeProgTab); +} + +function renderProgTabs() { + document.getElementById('prog-tabs').innerHTML = PROGRAMS.map(p => ` + + `).join(''); +} + +function switchProgTab(id) { + _activeProgTab = id; + renderProgTabs(); + renderProgBody(id); +} + +function renderProgBody(pid) { + const p = getProgramById(pid); + if (!p) return; + const pct = Math.round((p.capaian / p.target) * 100); + const budgetPct = Math.round((p.realisasi / p.budget) * 100); + const activeIssues = APP_STATE.issues.filter(i => i.programId === pid && i.status !== 'resolved' && i.status !== 'closed'); + const critCount = activeIssues.filter(i => i.severity === 'critical').length; + const highCount = activeIssues.filter(i => i.severity === 'high').length; + + // Location data for this program + const progLocs = APP_STATE.locs.filter(l => l.programs.includes(pid)); + + // Generate province stats + const provinceData = {}; + progLocs.forEach(l => { + const prov = l.province; + if (!provinceData[prov]) provinceData[prov] = { locs: 0, issues: 0 }; + provinceData[prov].locs++; + provinceData[prov].issues += l.issueCount; + }); + + const provinceRows = Object.entries(provinceData) + .sort((a,b) => b[1].issues - a[1].issues) + .slice(0, 10) + .map(([prov, data]) => { + const locList = progLocs.filter(l => l.province === prov); + let prog = 90; // default coverage + if (pid === 'mbg') prog = Math.round(locList.reduce((s,l) => s + (l.mbg?.coverage || 0), 0) / locList.length); + else if (pid === 'koperasi') prog = 75; + return ` + + ${prov} + ${data.locs} titik + +
+
+ ${prog}% +
+ + ${data.issues > 0 ? `${data.issues} issue` : ''} + + + + + `; + }).join(''); + + document.getElementById('prog-bodies').innerHTML = ` +
+
+ +
+
+
${p.icon}
+
+
${p.name}
+
📋 ${p.ministry} · Mulai ${p.startDate}
+
${p.description}
+
+
+
${pct}%
+
Capaian Target
+ 🟢 Aktif +
+
+
+
+
+
+
Realisasi: ${p.capaian.toLocaleString()} ${p.targetUnit}
+
Target: ${p.target.toLocaleString()} ${p.targetUnit}
+
+
+ + +
+ ${p.kpiLabel.map((lbl, i) => ` +
+
${p.kpiValue[i]}
+
${lbl}
+
+ `).join('')} +
+
Rp ${p.realisasi.toLocaleString()}M
+
Realisasi Anggaran
+
+
+
${budgetPct}%
+
Serapan Anggaran
+
+
+
${activeIssues.length}
+
Issue Aktif
+
+
+ + + ${activeIssues.length > 0 ? ` +
+
+
⚠️ Issue Aktif (${activeIssues.length})
+ +
+ ${activeIssues.slice(0,3).map(i => ` +
+
${getSevBadge(i.severity)} ${getStatBadge(i.status)}
+
${i.judul}
+
📍 ${i.daerah}, ${i.provinsi}
+
+ `).join('')} +
` : ''} + + +
+
+ Distribusi per Provinsi (${progLocs.length} titik pemantauan) +
+
+ + + + + + + + + + + ${provinceRows || ''} +
ProvinsiTitik AktifCapaianStatus Issue
Tidak ada data
+
+
+ + +
+ Lokasi Pemantauan (${progLocs.length} titik) +
+ ${progLocs.length === 0 + ? `
📍
Belum ada titik pemantauan
Data lokasi untuk program ${p.shortName} belum tersedia
` + : `
+ ${progLocs.slice(0, 12).map(loc => { + const d = loc[pid]; + const hasIssue = loc.issueCount > 0; + const relIssue = APP_STATE.issues.find(i => i.daerah === loc.name && i.programId === pid && i.status !== 'resolved'); + const onclickAction = (hasIssue && relIssue) + ? `openIssueDetail('${relIssue.id}')` + : `showToast('${loc.name.replace(/'/g,"\\'")}', '${loc.province.replace(/'/g,"\\'")} — kondisi normal', 'info')`; + return ` +
+
+ ${loc.name} + ${hasIssue ? '⚠ Issue' : ''} +
+
${loc.province}
+ ${pid === 'mbg' && d ? ` +
+ Porsi/Hari + ${(d.meals||0).toLocaleString()} +
+
+ ` : ''} + ${pid === 'koperasi' && d ? ` +
+ Anggota + ${(d.members||0).toLocaleString()} +
+ ${d.status} + ` : ''} + ${pid === 'psn' && d ? ` +
+ ${(d.projectName||'—').slice(0,22)}… + ${d.progress}% +
+
+ ` : ''} + ${pid === 'hilirisasi' && d ? ` +
+ ${d.commodity||'—'} + ${d.progress}% +
+
${d.stage||''}
+
+ ` : ''} + ${pid === 'rumah' && d ? ` +
+ Unit Selesai + ${(d.completed||0).toLocaleString()} +
+
Target: ${(d.units||0).toLocaleString()} unit
+
+ ` : ''} +
+ `; + }).join('')} +
` + } +
+
+ `; +} + +function filterIssuesByProgram(pid) { + APP_STATE.issueFilter.program = pid; + navigateTo('issues'); + setTimeout(() => { + const sel = document.getElementById('issues-filter-program'); + if (sel) { sel.value = pid; applyIssueFilters(); } + }, 100); +} + +function filterIssuesByProvince(prov, pid) { + APP_STATE.issueFilter.program = pid; + navigateTo('issues'); + setTimeout(() => { + const sel = document.getElementById('issues-filter-program'); + const search = document.getElementById('issues-search'); + if (sel) sel.value = pid; + if (search) search.value = prov; + applyIssueFilters(); + }, 100); +} diff --git a/pages/survey.js b/pages/survey.js new file mode 100644 index 0000000..f1db31a --- /dev/null +++ b/pages/survey.js @@ -0,0 +1,259 @@ +// ═══════════════════════════════════════════════════════════════════ +// SURVEY PAGE — Lapangan survey management +// ═══════════════════════════════════════════════════════════════════ + +function renderSurveyPage() { + const search = (document.getElementById('survey-search')?.value || '').toLowerCase().trim(); + const statusF = document.getElementById('survey-filter-status')?.value || 'all'; + const programF = document.getElementById('survey-filter-program')?.value || 'all'; + + const filtered = APP_STATE.surveys.filter(s => { + const matchS = !search || s.namaOfficer.toLowerCase().includes(search) || s.daerah.toLowerCase().includes(search) || s.issueId.toLowerCase().includes(search) || s.temuanUtama.toLowerCase().includes(search); + const matchSt = statusF === 'all' || s.status === statusF; + const matchP = programF === 'all' || s.programId === programF; + return matchS && matchSt && matchP; + }); + + const stats = { + total: APP_STATE.surveys.length, + verified: APP_STATE.surveys.filter(s => s.status === 'verified').length, + submitted: APP_STATE.surveys.filter(s => s.status === 'submitted').length, + draft: APP_STATE.surveys.filter(s => s.status === 'draft').length + }; + + document.getElementById('survey-body').innerHTML = ` + +
+
+
${stats.total}
+
Total Survey
+
+
+
${stats.verified}
+
Diverifikasi
+
+
+
${stats.submitted}
+
Menunggu Verifikasi
+
+
+
${stats.draft}
+
Draft
+
+
+ + + ${filtered.length === 0 + ? `
📋
Tidak ada survey
Tidak ada survey yang cocok dengan filter yang dipilih
` + : filtered.map(s => renderSurveyCard(s)).join('') + } + `; +} + +function renderSurveyCard(survey) { + const issue = APP_STATE.issues.find(i => i.id === survey.issueId); + const prog = getProgramById(survey.programId); + const skorColors = ['','var(--red)','var(--orange)','var(--amber)','var(--green)','var(--blue)']; + const skorLabel = ['','Sangat Kritis','Kritis','Perlu Perhatian','Minor','Normal']; + + return ` +
+
+
+
+ ${survey.id} + ${getProgramBadge(survey.programId)} + ${issue ? getSevBadge(issue.severity) : ''} +
+
${survey.namaOfficer}
+
${survey.jabatan}
+
+ ${{draft:'Draft',submitted:'Diajukan',verified:'Diverifikasi'}[survey.status]} +
+
+
+
+
Temuan Utama
+
${survey.temuanUtama}
+
+
+ ${survey.rekomendasi ? ` +
+
💡 Rekomendasi
+
${survey.rekomendasi.slice(0,120)}${survey.rekomendasi.length > 120 ? '...' : ''}
+
+ ` : ''} + +
+ `; +} + +function openSurveyDetail(surveyId) { + const s = APP_STATE.surveys.find(sv => sv.id === surveyId); + if (!s) return; + const issue = APP_STATE.issues.find(i => i.id === s.issueId); + const prog = getProgramById(s.programId); + const skorColors = ['','var(--red)','var(--orange)','var(--amber)','var(--green)','var(--blue)']; + const skorLabel = ['','Sangat Kritis — Tindakan Segera','Kritis','Perlu Perhatian Khusus','Minor — Tetap Perlu Ditangani','Kondisi Normal']; + + document.getElementById('sd-title').textContent = `Survey Lapangan — ${s.id}`; + document.getElementById('sd-body').innerHTML = ` +
+ ${getProgramBadge(s.programId)} + ${{draft:'Draft',submitted:'Diajukan',verified:'Diverifikasi'}[s.status]} + ${issue ? getSevBadge(issue.severity) : ''} +
+ +
+
+
OFFICER SURVEI
+
${s.namaOfficer}
+
${s.jabatan}
+
+
+
DETAIL SURVEI
+
📅 ${formatDate(s.tanggalSurvey)}
+
📍 ${s.daerah}, ${s.provinsi}
+
+
+ + ${issue ? ` +
+
ISSUE TERKAIT
+
${issue.id} — ${issue.judul.slice(0,60)}${issue.judul.length>60?'...':''}
+
Klik untuk lihat detail issue →
+
+ ` : ''} + +
+
📋 Temuan Utama
+
${s.temuanUtama}
+
+ + ${s.rekomendasi ? ` +
+
💡 Rekomendasi Tindak Lanjut
+
${s.rekomendasi}
+
+ ` : ''} + + ${s.skorKeparahan ? ` +
+
Penilaian Keparahan
+
+
${s.skorKeparahan}
+
+
${skorLabel[s.skorKeparahan]}
+
Skala 1 (paling parah) — 5 (normal)
+
+
+
+ ` : ''} + + ${s.verifiedBy ? ` +
+ +
+
Diverifikasi oleh ${s.verifiedBy}
+
Survey ini telah divalidasi dan dapat dijadikan dasar tindak lanjut
+
+
+ ` : ` +
+ +
+
Menunggu Verifikasi
+
Survey belum diverifikasi oleh atasan
+
+ +
+ `} + `; + openModal('survey-detail-modal'); +} + +function verifySurvey(surveyId) { + const s = APP_STATE.surveys.find(sv => sv.id === surveyId); + if (!s) return; + s.status = 'verified'; + s.verifiedBy = 'Admin KSP'; + showToast('Survey Diverifikasi', `${surveyId} berhasil diverifikasi`, 'success'); + closeModal('survey-detail-modal'); + renderSurveyPage(); +} + +// ─── NEW SURVEY MODAL ───────────────────────────────────────────── +function openNewSurveyModal(prefillIssueId) { + // Populate issue select + const issueSelect = document.getElementById('ns-issue'); + const openIssues = APP_STATE.issues.filter(i => i.status !== 'resolved' && i.status !== 'closed'); + issueSelect.innerHTML = '' + + openIssues.map(i => ``).join(''); + + // Set today's date + const today = new Date().toISOString().split('T')[0]; + document.getElementById('ns-tanggal').value = today; + + if (prefillIssueId) { + const issue = APP_STATE.issues.find(i => i.id === prefillIssueId); + if (issue) { + document.getElementById('ns-program').value = issue.programId; + document.getElementById('ns-daerah').value = issue.daerah; + } + } + openModal('new-survey-modal'); +} + +function submitNewSurvey(e) { + e.preventDefault(); + const issueId = document.getElementById('ns-issue').value; + const issue = APP_STATE.issues.find(i => i.id === issueId); + const id = `SRV-${String(APP_STATE.surveys.length + 1).padStart(3,'0')}`; + const now = new Date().toISOString().split('T')[0]; + const ts = new Date().toLocaleTimeString('id-ID') + ' WIB'; + + const newSurvey = { + id, issueId: issueId || null, + programId: document.getElementById('ns-program').value, + namaOfficer: document.getElementById('ns-officer').value, + jabatan: document.getElementById('ns-jabatan').value, + tanggalSurvey:document.getElementById('ns-tanggal').value, + daerah: document.getElementById('ns-daerah').value, + provinsi: issue?.provinsi || '', + status: 'submitted', + temuanUtama: document.getElementById('ns-temuan').value, + rekomendasi: document.getElementById('ns-rekomendasi').value, + skorKeparahan: null, verifiedBy: null, createdAt: now + }; + + APP_STATE.surveys.push(newSurvey); + + // Update issue if linked + if (issue && !issue.surveyIds.includes(id)) { + issue.surveyIds.push(id); + issue.timeline.push({ time: ts, action: `Survey lapangan dibuat oleh ${newSurvey.namaOfficer}`, actor: newSurvey.namaOfficer, type: 'survey' }); + issue.status = 'in_progress'; + } + + document.getElementById('new-survey-form').reset(); + closeModal('new-survey-modal'); + updateIssuesBadge(); + if (APP_STATE.currentPage === 'survey') renderSurveyPage(); + showToast('Survey Dibuat', `${id} berhasil diajukan`, 'success'); +} diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..7ab1aa7 --- /dev/null +++ b/styles.css @@ -0,0 +1,629 @@ +/* ═══════════════════════════════════════════════════════════════════ + KSP MONITORING DASHBOARD — DESIGN SYSTEM (Shadcn-inspired) + ═══════════════════════════════════════════════════════════════════ */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); + +/* ─── CSS VARIABLES ─────────────────────────────────────────────── */ +:root { + --bg: #09090b; + --bg2: #0d0d10; + --card: #18181b; + --card-hover: #1e1e22; + --sidebar: #111113; + --border: #27272a; + --border-mid: #3f3f46; + --text-1: #fafafa; + --text-2: #a1a1aa; + --text-3: #71717a; + --blue: #3b82f6; + --blue-d: #2563eb; + --blue-soft: rgba(59,130,246,0.12); + --green: #22c55e; + --green-soft: rgba(34,197,94,0.12); + --amber: #f59e0b; + --amber-soft: rgba(245,158,11,0.12); + --red: #ef4444; + --red-soft: rgba(239,68,68,0.12); + --purple: #a855f7; + --purple-soft: rgba(168,85,247,0.12); + --orange: #f97316; + --orange-soft: rgba(249,115,22,0.12); + --r: 6px; + --r-sm: 4px; + --r-md: 8px; + --r-lg: 12px; + --r-xl: 16px; + --sh: 0 4px 16px rgba(0,0,0,0.4); + --sh-lg: 0 8px 32px rgba(0,0,0,0.5); + --sidebar-w: 240px; + --topbar-h: 60px; + --ease: cubic-bezier(0.4,0,0.2,1); +} + +/* ─── LIGHT THEME VARIABLES ──────────────────────────────────────── */ +.light-theme { + --bg: #fafafa; + --bg2: #f4f4f5; + --card: #ffffff; + --card-hover: #f4f4f5; + --sidebar: #f4f4f5; + --border: #e4e4e7; + --border-mid: #d4d4d8; + --text-1: #09090b; + --text-2: #27272a; + --text-3: #71717a; + --blue-soft: rgba(59,130,246,0.08); + --green-soft: rgba(34,197,94,0.08); + --amber-soft: rgba(245,158,11,0.08); + --red-soft: rgba(239,68,68,0.08); + --purple-soft: rgba(168,85,247,0.08); + --orange-soft: rgba(249,115,22,0.08); + --sh: 0 4px 16px rgba(0,0,0,0.05); + --sh-lg: 0 8px 32px rgba(0,0,0,0.08); +} + +/* ─── RESET ─────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body { height: 100%; font-family: 'Inter', system-ui, sans-serif; background: var(--bg); color: var(--text-1); font-size: 14px; line-height: 1.5; -webkit-font-smoothing: antialiased; overflow: hidden; } +body, .sidebar, .main-area, .topbar, .card, .kpi-card, .btn-secondary, .form-input, .form-select, .form-textarea, .search-bar, table, th, td, .prog-stat-card, .nav-item { + transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease), color 0.25s var(--ease), box-shadow 0.25s var(--ease); +} +a { text-decoration: none; color: inherit; } +button { font-family: inherit; cursor: pointer; } +input, select, textarea { font-family: inherit; } +ul, ol { list-style: none; } +::-webkit-scrollbar { width: 4px; height: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border-mid); border-radius: 99px; } + +/* ─── APP LAYOUT ─────────────────────────────────────────────────── */ +.app-layout { display: flex; height: 100vh; overflow: hidden; } + +/* ─── SIDEBAR ───────────────────────────────────────────────────── */ +.sidebar { + width: var(--sidebar-w); min-width: var(--sidebar-w); + background: var(--sidebar); border-right: 1px solid var(--border); + display: flex; flex-direction: column; height: 100vh; overflow: hidden; + position: relative; z-index: 10; +} +.sidebar-logo { + padding: 18px 16px 14px; border-bottom: 1px solid var(--border); + display: flex; align-items: center; gap: 10px; +} +.logo-icon { + width: 34px; height: 34px; border-radius: var(--r-md); + background: linear-gradient(135deg, #1d4ed8, #3b82f6); + display: flex; align-items: center; justify-content: center; + font-size: 16px; flex-shrink: 0; + box-shadow: 0 2px 8px rgba(59,130,246,0.4); +} +.logo-text { flex: 1; overflow: hidden; } +.logo-title { font-size: 11px; font-weight: 700; color: var(--text-1); letter-spacing: 0.5px; line-height: 1.2; } +.logo-sub { font-size: 10px; color: var(--text-3); margin-top: 1px; } + +.sidebar-nav { flex: 1; padding: 12px 8px; overflow-y: auto; display: flex; flex-direction: column; gap: 1px; } +.nav-section-label { font-size: 10px; font-weight: 600; color: var(--text-3); letter-spacing: 0.8px; text-transform: uppercase; padding: 8px 8px 4px; margin-top: 4px; } +.nav-item { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; border-radius: var(--r); color: var(--text-2); + font-size: 13px; font-weight: 500; cursor: pointer; + transition: all 0.15s var(--ease); position: relative; + border: 1px solid transparent; +} +.nav-item:hover { background: rgba(255,255,255,0.04); color: var(--text-1); } +.nav-item.active { + background: var(--blue-soft); color: var(--blue); + border-color: rgba(59,130,246,0.2); +} +.nav-item-icon { font-size: 15px; width: 20px; text-align: center; flex-shrink: 0; } +.nav-item-label { flex: 1; } +.nav-badge { + background: var(--red); color: #fff; font-size: 10px; font-weight: 700; + padding: 1px 6px; border-radius: 99px; min-width: 18px; text-align: center; + animation: pulse-badge 2s infinite; +} +@keyframes pulse-badge { + 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } +} + +.sidebar-footer { padding: 12px 8px 16px; border-top: 1px solid var(--border); } +.sidebar-user { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; border-radius: var(--r); + cursor: pointer; transition: background 0.15s; +} +.sidebar-user:hover { background: rgba(255,255,255,0.04); } +.user-avatar { + width: 30px; height: 30px; border-radius: 50%; + background: linear-gradient(135deg, #1e40af, #7c3aed); + display: flex; align-items: center; justify-content: center; + font-size: 12px; font-weight: 700; color: #fff; flex-shrink: 0; +} +.user-info { flex: 1; overflow: hidden; } +.user-name { font-size: 12px; font-weight: 600; color: var(--text-1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.user-role { font-size: 10px; color: var(--text-3); } + +/* ─── MAIN AREA ─────────────────────────────────────────────────── */ +.main-area { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; } + +/* ─── TOPBAR ────────────────────────────────────────────────────── */ +.topbar { + height: var(--topbar-h); min-height: var(--topbar-h); + background: var(--bg2); border-bottom: 1px solid var(--border); + display: flex; align-items: center; justify-content: space-between; + padding: 0 24px; gap: 16px; +} +.topbar-left { display: flex; align-items: center; gap: 12px; } +.page-title { font-size: 15px; font-weight: 600; color: var(--text-1); } +.page-subtitle { font-size: 12px; color: var(--text-3); margin-top: 1px; } +.topbar-right { display: flex; align-items: center; gap: 8px; } + +.clock-widget { text-align: right; } +.clock-time { font-size: 13px; font-weight: 700; color: var(--text-1); font-variant-numeric: tabular-nums; letter-spacing: 0.5px; } +.clock-date { font-size: 10px; color: var(--text-3); } + +.live-pill { + display: flex; align-items: center; gap: 6px; + background: rgba(34,197,94,0.08); border: 1px solid rgba(34,197,94,0.2); + padding: 4px 10px; border-radius: 99px; +} +.live-dot { + width: 6px; height: 6px; border-radius: 50%; background: var(--green); + box-shadow: 0 0 6px var(--green); animation: blink 1.5s ease-in-out infinite; +} +@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } } +.live-text { font-size: 10px; font-weight: 700; color: var(--green); letter-spacing: 0.6px; } + +.notif-btn { + position: relative; width: 36px; height: 36px; border-radius: var(--r); + background: transparent; border: 1px solid var(--border); color: var(--text-2); + display: flex; align-items: center; justify-content: center; font-size: 16px; + transition: all 0.15s; +} +.notif-btn:hover { background: rgba(255,255,255,0.04); border-color: var(--border-mid); color: var(--text-1); } +.notif-dot { + position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; + background: var(--red); border-radius: 50%; border: 1.5px solid var(--bg2); +} + +/* ─── PAGE CONTENT ──────────────────────────────────────────────── */ +.page-content { flex: 1; overflow: hidden; position: relative; } +.page-view { height: 100%; display: none; overflow: hidden; } +.page-view.active { display: flex; flex-direction: column; } + +/* ─── SCROLLABLE CONTAINERS ─────────────────────────────────────── */ +.scroll-area { flex: 1; overflow-y: auto; padding: 20px 24px; } + +/* ─── SECTION HEADER ────────────────────────────────────────────── */ +.section-header { + display: flex; align-items: center; justify-content: space-between; + margin-bottom: 16px; gap: 12px; +} +.section-title { font-size: 15px; font-weight: 600; color: var(--text-1); } +.section-sub { font-size: 12px; color: var(--text-3); margin-top: 2px; } + +/* ─── CARDS ─────────────────────────────────────────────────────── */ +.card { + background: var(--card); border: 1px solid var(--border); + border-radius: var(--r-md); transition: border-color 0.15s; +} +.card:hover { border-color: var(--border-mid); } +.card-p { padding: 16px; } +.card-p-sm { padding: 12px; } + +/* ─── KPI CARDS ─────────────────────────────────────────────────── */ +.kpi-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 20px; } +.kpi-card { + background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); + padding: 16px; cursor: pointer; transition: all 0.2s var(--ease); position: relative; overflow: hidden; +} +.kpi-card::before { + content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; + background: var(--kpi-color, var(--blue)); opacity: 0.6; +} +.kpi-card:hover { border-color: var(--border-mid); background: var(--card-hover); transform: translateY(-1px); box-shadow: var(--sh); } +.kpi-card.active { border-color: var(--kpi-color, var(--blue)); background: var(--card-hover); } +.kpi-label { font-size: 11px; font-weight: 500; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 8px; } +.kpi-value { font-size: 24px; font-weight: 800; color: var(--text-1); line-height: 1; margin-bottom: 6px; letter-spacing: -0.5px; } +.kpi-value.red { color: var(--red); } +.kpi-sub { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 4px; } +.kpi-sub.up { color: var(--green); } +.kpi-sub.down { color: var(--red); } +.kpi-sub.warn { color: var(--amber); } +.kpi-icon { position: absolute; top: 14px; right: 14px; font-size: 20px; opacity: 0.15; } + +/* ─── PROGRAM BADGES ────────────────────────────────────────────── */ +.badge { + display: inline-flex; align-items: center; gap: 4px; + padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; + border: 1px solid transparent; white-space: nowrap; +} +.badge-green { background: var(--green-soft); color: var(--green); border-color: rgba(34,197,94,0.2); } +.badge-blue { background: var(--blue-soft); color: var(--blue); border-color: rgba(59,130,246,0.2); } +.badge-amber { background: var(--amber-soft); color: var(--amber); border-color: rgba(245,158,11,0.2); } +.badge-purple { background: var(--purple-soft); color: var(--purple); border-color: rgba(168,85,247,0.2); } +.badge-orange { background: var(--orange-soft); color: var(--orange); border-color: rgba(249,115,22,0.2); } +.badge-red { background: var(--red-soft); color: var(--red); border-color: rgba(239,68,68,0.2); } +.badge-zinc { background: rgba(113,113,122,0.1); color: var(--text-2); border-color: var(--border); } + +/* ─── SEVERITY + STATUS BADGES ──────────────────────────────────── */ +.sev-critical { background: var(--red-soft); color: var(--red); border: 1px solid rgba(239,68,68,0.25); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.sev-high { background: var(--orange-soft); color: var(--orange); border: 1px solid rgba(249,115,22,0.25); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.sev-medium { background: var(--amber-soft); color: var(--amber); border: 1px solid rgba(245,158,11,0.25); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.sev-low { background: var(--green-soft); color: var(--green); border: 1px solid rgba(34,197,94,0.25); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } + +.stat-open { background: var(--red-soft); color: var(--red); border: 1px solid rgba(239,68,68,0.2); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.stat-in_progress { background: var(--blue-soft); color: var(--blue); border: 1px solid rgba(59,130,246,0.2); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.stat-resolved { background: var(--green-soft); color: var(--green); border: 1px solid rgba(34,197,94,0.2); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.stat-closed { background: rgba(113,113,122,0.1); color: var(--text-3); border: 1px solid var(--border); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } + +/* ─── BUTTONS ───────────────────────────────────────────────────── */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: 6px; + padding: 7px 14px; border-radius: var(--r); font-size: 13px; font-weight: 500; + border: 1px solid transparent; cursor: pointer; transition: all 0.15s var(--ease); + white-space: nowrap; user-select: none; +} +.btn-primary { background: var(--blue); color: #fff; } +.btn-primary:hover { background: var(--blue-d); } +.btn-secondary { background: rgba(255,255,255,0.05); color: var(--text-2); border-color: var(--border); } +.btn-secondary:hover { background: rgba(255,255,255,0.08); border-color: var(--border-mid); color: var(--text-1); } +.btn-destructive { background: var(--red-soft); color: var(--red); border-color: rgba(239,68,68,0.3); } +.btn-destructive:hover { background: rgba(239,68,68,0.2); } +.btn-success { background: var(--green-soft); color: var(--green); border-color: rgba(34,197,94,0.3); } +.btn-success:hover { background: rgba(34,197,94,0.2); } +.btn-sm { padding: 4px 10px; font-size: 12px; } +.btn-xs { padding: 2px 8px; font-size: 11px; border-radius: var(--r-sm); } +.btn-icon { padding: 7px; width: 34px; height: 34px; } + +/* ─── FORMS ─────────────────────────────────────────────────────── */ +.form-group { display: flex; flex-direction: column; gap: 6px; } +.form-label { font-size: 12px; font-weight: 500; color: var(--text-2); } +.form-input, .form-select, .form-textarea { + background: var(--bg); border: 1px solid var(--border); border-radius: var(--r); + color: var(--text-1); font-size: 13px; padding: 8px 12px; font-family: inherit; + transition: border-color 0.15s; outline: none; width: 100%; +} +.form-input:focus, .form-select:focus, .form-textarea:focus { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(59,130,246,0.12); } +.form-input::placeholder { color: var(--text-3); } +.form-select option { background: #1a1a1e; } +.form-textarea { resize: vertical; min-height: 80px; } +.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + +/* ─── SEARCH BAR ────────────────────────────────────────────────── */ +.search-bar { + display: flex; align-items: center; gap: 8px; + background: var(--bg); border: 1px solid var(--border); border-radius: var(--r); + padding: 0 12px; transition: border-color 0.15s; +} +.search-bar:focus-within { border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-soft); } +.search-bar svg { color: var(--text-3); flex-shrink: 0; } +.search-bar input { flex: 1; background: transparent; border: none; outline: none; color: var(--text-1); font-size: 13px; padding: 8px 0; font-family: inherit; } +.search-bar input::placeholder { color: var(--text-3); } + +/* ─── FILTERS ROW ───────────────────────────────────────────────── */ +.filters-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; } +.filter-pill { + padding: 5px 12px; border-radius: 99px; font-size: 12px; font-weight: 500; + border: 1px solid var(--border); background: transparent; color: var(--text-2); + cursor: pointer; transition: all 0.15s; white-space: nowrap; +} +.filter-pill:hover { border-color: var(--border-mid); color: var(--text-1); } +.filter-pill.active { background: var(--blue-soft); border-color: rgba(59,130,246,0.35); color: var(--blue); } +.filter-sep { width: 1px; height: 20px; background: var(--border); margin: 0 4px; } + +/* ─── TABLE ─────────────────────────────────────────────────────── */ +.table-wrap { overflow-x: auto; border-radius: var(--r-md); border: 1px solid var(--border); } +table { width: 100%; border-collapse: collapse; } +thead th { background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--border); padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 600; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; } +tbody tr { border-bottom: 1px solid var(--border); transition: background 0.1s; } +tbody tr:last-child { border-bottom: none; } +tbody tr:hover { background: rgba(255,255,255,0.025); } +tbody td { padding: 12px 14px; font-size: 13px; color: var(--text-1); vertical-align: middle; } +.td-muted { color: var(--text-3); font-size: 12px; } + +/* ─── PROGRESS BAR ──────────────────────────────────────────────── */ +.progress-bar { height: 4px; background: var(--border); border-radius: 99px; overflow: hidden; } +.progress-fill { height: 100%; border-radius: 99px; transition: width 0.6s var(--ease); } +.progress-lg { height: 6px; } +.progress-sm { height: 3px; } + +/* ─── DASHBOARD MAP LAYOUT ──────────────────────────────────────── */ +.dash-body { display: flex; gap: 16px; flex: 1; min-height: 0; padding: 0 24px 20px; } +.dash-map-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; } +.dash-side-col { width: 320px; min-width: 320px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; } +#map { flex: 1; border-radius: var(--r-lg); min-height: 300px; border: 1px solid var(--border); overflow: hidden; } +.map-wrapper { flex: 1; position: relative; border-radius: var(--r-lg); overflow: hidden; border: 1px solid var(--border); } +#leaflet-map { width: 100%; height: 100%; } +.map-controls { + position: absolute; top: 12px; left: 12px; z-index: 400; + display: flex; gap: 6px; flex-wrap: wrap; max-width: calc(100% - 24px); +} +.map-btn { + padding: 5px 10px; border-radius: var(--r); font-size: 11px; font-weight: 600; + background: rgba(9,9,11,0.85); color: var(--text-2); border: 1px solid var(--border); + backdrop-filter: blur(8px); cursor: pointer; transition: all 0.15s; white-space: nowrap; +} +.map-btn:hover { border-color: var(--border-mid); color: var(--text-1); } +.map-btn.active { background: var(--blue-soft); border-color: rgba(59,130,246,0.4); color: var(--blue); } +.map-legend { + position: absolute; bottom: 12px; left: 12px; z-index: 400; + background: rgba(9,9,11,0.88); border: 1px solid var(--border); + border-radius: var(--r-md); padding: 10px 12px; backdrop-filter: blur(8px); +} +.map-legend-title { font-size: 10px; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; } +.legend-row { display: flex; align-items: center; gap: 8px; font-size: 11px; color: var(--text-2); margin-bottom: 4px; } +.legend-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + +/* ─── ALERT ITEMS ───────────────────────────────────────────────── */ +.alert-item { + padding: 12px; border-radius: var(--r); border-left: 3px solid var(--border); + background: rgba(255,255,255,0.02); margin-bottom: 8px; cursor: pointer; + transition: all 0.15s; +} +.alert-item:hover { background: rgba(255,255,255,0.04); } +.alert-item.critical { border-left-color: var(--red); } +.alert-item.high { border-left-color: var(--orange); } +.alert-item.medium { border-left-color: var(--amber); } +.alert-item.low { border-left-color: var(--green); } +.alert-title { font-size: 12px; font-weight: 600; color: var(--text-1); margin-bottom: 4px; line-height: 1.4; } +.alert-meta { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } + +/* ─── ISSUE KANBAN ──────────────────────────────────────────────── */ +.issues-layout { flex: 1; display: flex; flex-direction: column; overflow: hidden; } +.issues-filters { padding: 16px 24px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; flex-wrap: wrap; background: var(--bg2); } +.issues-body { flex: 1; overflow: hidden; } +.kanban-view { display: flex; height: 100%; gap: 0; overflow-x: auto; } +.kanban-col { flex: 1; min-width: 280px; max-width: 400px; display: flex; flex-direction: column; border-right: 1px solid var(--border); overflow: hidden; } +.kanban-col:last-child { border-right: none; } +.kanban-col-header { padding: 14px 16px 12px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; background: rgba(255,255,255,0.01); flex-shrink: 0; } +.kanban-col-title { font-size: 13px; font-weight: 600; color: var(--text-1); display: flex; align-items: center; gap: 8px; } +.col-count { font-size: 11px; color: var(--text-3); background: var(--border); padding: 1px 7px; border-radius: 99px; font-weight: 600; } +.kanban-cards { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 8px; } + +.issue-card { + background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); + padding: 12px; cursor: pointer; transition: all 0.15s var(--ease); + border-top: 2px solid transparent; +} +.issue-card:hover { border-color: var(--border-mid); background: var(--card-hover); transform: translateY(-1px); box-shadow: var(--sh); } +.issue-card.critical-card { border-top-color: var(--red); } +.issue-card.high-card { border-top-color: var(--orange); } +.issue-card.medium-card { border-top-color: var(--amber); } +.issue-card.low-card { border-top-color: var(--green); } +.ic-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; margin-bottom: 8px; } +.ic-id { font-size: 10px; color: var(--text-3); font-family: monospace; font-weight: 600; } +.ic-title { font-size: 12px; font-weight: 600; color: var(--text-1); line-height: 1.4; margin-bottom: 6px; } +.ic-desc { font-size: 11px; color: var(--text-3); line-height: 1.5; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.ic-footer { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 4px; } +.ic-meta { font-size: 10px; color: var(--text-3); } +.ic-assignee { font-size: 10px; color: var(--text-3); max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + +/* ─── LIST VIEW ─────────────────────────────────────────────────── */ +.list-view { padding: 0 24px 20px; overflow-y: auto; height: 100%; } + +/* ─── MODAL ─────────────────────────────────────────────────────── */ +.modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); + z-index: 1000; display: flex; align-items: center; justify-content: center; + opacity: 0; pointer-events: none; transition: opacity 0.2s; +} +.modal-overlay.open { opacity: 1; pointer-events: all; } +.modal-box { + background: var(--card); border: 1px solid var(--border-mid); border-radius: var(--r-xl); + width: 600px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; + box-shadow: var(--sh-lg); transform: scale(0.97) translateY(8px); transition: transform 0.2s; +} +.modal-overlay.open .modal-box { transform: scale(1) translateY(0); } +.modal-header { padding: 18px 20px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-shrink: 0; } +.modal-title { font-size: 15px; font-weight: 700; color: var(--text-1); line-height: 1.4; } +.modal-close { width: 28px; height: 28px; border-radius: var(--r); background: rgba(255,255,255,0.05); border: 1px solid var(--border); color: var(--text-2); display: flex; align-items: center; justify-content: center; font-size: 16px; cursor: pointer; flex-shrink: 0; transition: all 0.15s; } +.modal-close:hover { background: rgba(255,255,255,0.08); color: var(--text-1); } +.modal-body { flex: 1; overflow-y: auto; padding: 18px 20px; } +.modal-footer { padding: 14px 20px; border-top: 1px solid var(--border); display: flex; align-items: center; justify-content: flex-end; gap: 8px; flex-shrink: 0; } + +/* ─── WIDE MODAL ────────────────────────────────────────────────── */ +.modal-box.wide { width: 760px; } + +/* ─── TIMELINE ──────────────────────────────────────────────────── */ +.timeline { display: flex; flex-direction: column; gap: 0; } +.tl-item { display: flex; gap: 12px; position: relative; padding-bottom: 16px; } +.tl-item:last-child { padding-bottom: 0; } +.tl-item:last-child .tl-line { display: none; } +.tl-dot-wrap { display: flex; flex-direction: column; align-items: center; flex-shrink: 0; } +.tl-dot { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; border: 2px solid var(--border); background: var(--card); flex-shrink: 0; } +.tl-dot.created { background: rgba(239,68,68,0.15); border-color: var(--red); } +.tl-dot.assigned { background: rgba(59,130,246,0.15); border-color: var(--blue); } +.tl-dot.verified { background: rgba(245,158,11,0.15); border-color: var(--amber); } +.tl-dot.survey { background: rgba(168,85,247,0.15); border-color: var(--purple); } +.tl-dot.escalated { background: rgba(249,115,22,0.15); border-color: var(--orange); } +.tl-dot.resolved { background: rgba(34,197,94,0.15); border-color: var(--green); } +.tl-line { flex: 1; width: 1px; background: var(--border); margin: 4px 0; min-height: 12px; } +.tl-content { flex: 1; padding-top: 4px; } +.tl-action { font-size: 12px; color: var(--text-1); font-weight: 500; line-height: 1.4; } +.tl-meta { font-size: 11px; color: var(--text-3); margin-top: 3px; display: flex; gap: 6px; } + +/* ─── ESCALATION PIPELINE ───────────────────────────────────────── */ +.esc-pipeline { display: flex; align-items: center; gap: 0; margin-bottom: 20px; overflow-x: auto; padding-bottom: 4px; } +.esc-step { + flex: 1; min-width: 140px; text-align: center; position: relative; + padding: 14px 12px; background: var(--card); border: 1px solid var(--border); + border-right: none; first-child { border-radius: var(--r-md) 0 0 var(--r-md); } +} +.esc-step:first-child { border-radius: var(--r-md) 0 0 var(--r-md); } +.esc-step:last-child { border-radius: 0 var(--r-md) var(--r-md) 0; border-right: 1px solid var(--border); } +.esc-step.active { background: var(--blue-soft); border-color: rgba(59,130,246,0.3); } +.esc-step-num { width: 28px; height: 28px; border-radius: 50%; background: var(--border); color: var(--text-2); font-size: 12px; font-weight: 700; display: flex; align-items: center; justify-content: center; margin: 0 auto 8px; } +.esc-step.active .esc-step-num { background: var(--blue); color: #fff; } +.esc-step-label { font-size: 11px; font-weight: 600; color: var(--text-2); } +.esc-step.active .esc-step-label { color: var(--blue); } + +/* ─── SURVEY CARD ───────────────────────────────────────────────── */ +.survey-card { + background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); + padding: 16px; margin-bottom: 10px; cursor: pointer; transition: all 0.15s; +} +.survey-card:hover { border-color: var(--border-mid); background: var(--card-hover); } +.sc-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 8px; } +.sc-id { font-size: 10px; color: var(--text-3); font-family: monospace; font-weight: 600; } +.sc-officer { font-size: 13px; font-weight: 600; color: var(--text-1); } +.sc-jabatan { font-size: 11px; color: var(--text-3); } +.sc-temuan { font-size: 12px; color: var(--text-2); line-height: 1.5; margin: 8px 0; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } +.sc-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; } +.sc-meta { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 8px; } + +/* ─── SURVEY STATUS BADGE ───────────────────────────────────────── */ +.status-draft { background: rgba(113,113,122,0.1); color: var(--text-3); border: 1px solid var(--border); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.status-submitted { background: var(--blue-soft); color: var(--blue); border: 1px solid rgba(59,130,246,0.2); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } +.status-verified { background: var(--green-soft); color: var(--green); border: 1px solid rgba(34,197,94,0.2); padding: 2px 8px; border-radius: 99px; font-size: 11px; font-weight: 600; } + +/* ─── PROGRAM PAGE ──────────────────────────────────────────────── */ +.program-tabs { display: flex; gap: 4px; padding: 16px 24px 0; background: var(--bg2); border-bottom: 1px solid var(--border); overflow-x: auto; } +.prog-tab { + display: flex; align-items: center; gap: 6px; + padding: 8px 14px; border-radius: var(--r) var(--r) 0 0; + font-size: 13px; font-weight: 500; color: var(--text-2); + cursor: pointer; transition: all 0.15s; border: 1px solid transparent; + border-bottom: none; white-space: nowrap; margin-bottom: -1px; + background: transparent; +} +.prog-tab:hover { color: var(--text-1); background: rgba(255,255,255,0.03); } +.prog-tab.active { background: var(--card); border-color: var(--border); border-bottom-color: var(--card); color: var(--text-1); } +.prog-body { flex: 1; overflow: hidden; display: none; } +.prog-body.active { display: flex; flex-direction: column; } +.prog-scroll { flex: 1; overflow-y: auto; padding: 20px 24px; } + +/* ─── STAT PAIR ─────────────────────────────────────────────────── */ +.stat-pair { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); } +.stat-pair:last-child { border-bottom: none; } +.stat-key { font-size: 12px; color: var(--text-3); } +.stat-val { font-size: 12px; color: var(--text-1); font-weight: 500; text-align: right; } + +/* ─── EMPTY STATE ───────────────────────────────────────────────── */ +.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 48px 24px; text-align: center; } +.empty-icon { font-size: 40px; margin-bottom: 12px; opacity: 0.4; } +.empty-title { font-size: 14px; font-weight: 600; color: var(--text-2); margin-bottom: 6px; } +.empty-desc { font-size: 12px; color: var(--text-3); max-width: 280px; line-height: 1.6; } + +/* ─── CHART CONTAINER ───────────────────────────────────────────── */ +.chart-wrap { position: relative; height: 180px; } + +/* ─── DIVIDER ───────────────────────────────────────────────────── */ +.divider { height: 1px; background: var(--border); margin: 14px 0; } + +/* ─── TOOLTIP ───────────────────────────────────────────────────── */ +.custom-tooltip .leaflet-tooltip { + background: var(--card) !important; border: 1px solid var(--border) !important; + border-radius: var(--r-md) !important; color: var(--text-1) !important; + font-family: 'Inter', sans-serif !important; font-size: 11px !important; + padding: 8px 12px !important; backdrop-filter: blur(8px) !important; box-shadow: var(--sh) !important; +} +.tt-title { font-size: 12px; font-weight: 700; color: var(--text-1); margin-bottom: 4px; } +.tt-sub { font-size: 10px; color: var(--text-3); margin-bottom: 6px; } +.tt-row { display: flex; justify-content: space-between; gap: 16px; font-size: 11px; color: var(--text-2); padding: 2px 0; } +.tt-val { color: var(--text-1); font-weight: 600; } + +/* ─── NOTIFICATION DROPDOWN ─────────────────────────────────────── */ +.notif-dropdown { + position: absolute; top: calc(100% + 8px); right: 0; width: 320px; z-index: 100; + background: var(--card); border: 1px solid var(--border-mid); border-radius: var(--r-lg); + box-shadow: var(--sh-lg); overflow: hidden; +} +.notif-header { padding: 12px 14px; border-bottom: 1px solid var(--border); font-size: 12px; font-weight: 600; color: var(--text-1); } +.notif-item { padding: 10px 14px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.1s; display: flex; gap: 10px; } +.notif-item:last-child { border-bottom: none; } +.notif-item:hover { background: rgba(255,255,255,0.03); } +.notif-item.unread { background: rgba(59,130,246,0.04); } +.notif-dot-indicator { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; margin-top: 4px; } +.notif-msg { font-size: 12px; color: var(--text-2); line-height: 1.4; flex: 1; } +.notif-time { font-size: 10px; color: var(--text-3); margin-top: 3px; } + +/* ─── TOPBAR ACTIONS ────────────────────────────────────────────── */ +.topbar-actions-area { position: relative; } + +/* ─── PROGRAM STAT GRID ─────────────────────────────────────────── */ +.prog-stat-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; } +.prog-stat-card { background: var(--bg); border: 1px solid var(--border); border-radius: var(--r-md); padding: 12px; } +.prog-stat-val { font-size: 20px; font-weight: 800; color: var(--text-1); letter-spacing: -0.5px; margin-bottom: 4px; } +.prog-stat-lbl { font-size: 11px; color: var(--text-3); } + +/* ─── TOAST NOTIFICATION ────────────────────────────────────────── */ +.toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 2000; display: flex; flex-direction: column; gap: 8px; pointer-events: none; } +.toast { + display: flex; align-items: flex-start; gap: 10px; + background: var(--card); border: 1px solid var(--border-mid); border-radius: var(--r-md); + padding: 12px 14px; box-shadow: var(--sh-lg); max-width: 340px; + pointer-events: all; transform: translateX(110%); transition: transform 0.3s var(--ease); +} +.toast.show { transform: translateX(0); } +.toast-icon { font-size: 16px; flex-shrink: 0; } +.toast-body { flex: 1; } +.toast-title { font-size: 13px; font-weight: 600; color: var(--text-1); } +.toast-msg { font-size: 12px; color: var(--text-3); margin-top: 2px; } + +/* ─── LEAFLET OVERRIDES ─────────────────────────────────────────── */ +.leaflet-container { background: var(--bg) !important; } +.leaflet-control-zoom { border: 1px solid var(--border) !important; border-radius: var(--r-md) !important; overflow: hidden; } +.leaflet-control-zoom a { background: rgba(9,9,11,0.9) !important; color: var(--text-2) !important; border-color: var(--border) !important; } +.leaflet-control-zoom a:hover { background: var(--card) !important; color: var(--text-1) !important; } +.leaflet-bar a:first-child { border-radius: var(--r-md) var(--r-md) 0 0 !important; } +.leaflet-bar a:last-child { border-radius: 0 0 var(--r-md) var(--r-md) !important; } + +/* ─── MARKER PULSE ──────────────────────────────────────────────── */ +.marker-pulse { position: relative; } +.marker-pulse::after { + content: ''; position: absolute; inset: -3px; border-radius: 50%; + border: 2px solid currentColor; opacity: 0; animation: pulse-ring 2s ease-out infinite; +} +@keyframes pulse-ring { 0% { inset: -3px; opacity: 0.5; } 100% { inset: -10px; opacity: 0; } } + +/* ─── PROGRAM PROGRESS ──────────────────────────────────────────── */ +.prog-overview-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-bottom: 20px; } +.prog-overview-card { background: var(--card); border: 1px solid var(--border); border-radius: var(--r-md); padding: 14px; transition: all 0.15s; cursor: pointer; } +.prog-overview-card:hover { border-color: var(--border-mid); transform: translateY(-1px); box-shadow: var(--sh); } +.poc-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } +.poc-icon { font-size: 20px; } +.poc-name { font-size: 13px; font-weight: 600; color: var(--text-1); } +.poc-ministry { font-size: 10px; color: var(--text-3); margin-top: 1px; } +.poc-progress { margin-bottom: 8px; } +.poc-pct { font-size: 20px; font-weight: 800; color: var(--text-1); letter-spacing: -0.5px; } +.poc-target { font-size: 11px; color: var(--text-3); margin-bottom: 6px; } +.poc-footer { display: flex; align-items: center; justify-content: space-between; } +.poc-budget { font-size: 11px; color: var(--text-2); } +.poc-issues { font-size: 11px; color: var(--red); font-weight: 600; } + +/* ─── UTILITY ───────────────────────────────────────────────────── */ +.flex { display: flex; } +.flex-col { flex-direction: column; } +.items-center { align-items: center; } +.justify-between { justify-content: space-between; } +.gap-2 { gap: 8px; } +.gap-3 { gap: 12px; } +.gap-4 { gap: 16px; } +.mb-2 { margin-bottom: 8px; } +.mb-3 { margin-bottom: 12px; } +.mb-4 { margin-bottom: 16px; } +.mt-2 { margin-top: 8px; } +.mt-3 { margin-top: 12px; } +.mt-4 { margin-top: 16px; } +.text-sm { font-size: 12px; } +.text-xs { font-size: 11px; } +.font-bold { font-weight: 700; } +.font-semibold { font-weight: 600; } +.text-muted { color: var(--text-3); } +.text-secondary { color: var(--text-2); } +.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.w-full { width: 100%; } +.hidden { display: none !important; } + +/* ─── ANIMATIONS ────────────────────────────────────────────────── */ +@keyframes fadeIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } } +@keyframes slideIn { from { transform: translateX(-10px); opacity: 0; } to { transform: translateX(0); opacity: 1; } } +.fade-in { animation: fadeIn 0.25s var(--ease) forwards; } +.slide-in { animation: slideIn 0.2s var(--ease) forwards; } + +/* ─── VIEW TOGGLE ───────────────────────────────────────────────── */ +.view-toggle { display: flex; border: 1px solid var(--border); border-radius: var(--r); overflow: hidden; } +.view-btn { padding: 5px 12px; font-size: 12px; font-weight: 500; color: var(--text-2); background: transparent; border: none; cursor: pointer; transition: all 0.15s; } +.view-btn.active { background: var(--blue-soft); color: var(--blue); } +.view-btn:hover:not(.active) { color: var(--text-1); background: rgba(255,255,255,0.04); }