/* ════════════════════════════════════════ GLOBAL HELPERS ════════════════════════════════════════ */ const ge = id => document.getElementById(id); const gv = id => (document.getElementById(id)?.value ?? '').trim(); const sv = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ''; }; const rp = n => n > 0 ? 'Rp ' + parseInt(n).toLocaleString('id-ID') : '—'; const cap = s => s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '—'; const bdg = (val, _cls) => { const v = val || ''; const css = v.replace(/_/g,'-').toLowerCase(); return `${v.replace(/_/g,' ').toUpperCase()}`; }; /* ════════════════════════════════════════ ROLE CONFIG 1 = Admin, 2 = Pengurus RI, 3 = Pimpinan, 4 = Masyarakat ════════════════════════════════════════ */ const ROLE_MENUS = { 1: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat','log'], 2: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat'], 3: ['dashboard','peta','penduduk','keluarga','laporan'], 4: ['dashboard','peta'], 99: ['dashboard','peta'], }; const ROLE_LABELS = { 1: { icon:'⚙️', label:'Administrator', color:'#e74c3c' }, 2: { icon:'🕌', label:'Pengurus Rumah Ibadah', color:'#27ae60' }, 3: { icon:'🏛️', label:'Pimpinan Daerah', color:'#2980b9' }, 4: { icon:'👤', label:'Masyarakat', color:'#8e44ad' }, }; /* ════════════════════════════════════════ APP CONTROLLER ════════════════════════════════════════ */ const App = (() => { let _user = null, _autoTimer = null; const currentUser = () => _user; const userRole = () => _user?.id_role ?? 99; const canEdit = () => [1,2].includes(userRole()); /* ── Login ── */ async function doLogin() { const username = gv('login-user'), password = gv('login-pass'); const errEl = ge('login-err'); const btnMasuk = ge('btn-masuk-submit'); errEl.classList.add('hidden'); if (btnMasuk) { btnMasuk.disabled = true; btnMasuk.textContent = 'Memproses...'; } try { const res = await fetch('api/index.php?r=auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }), credentials: 'same-origin', }); let json; const text = await res.text(); try { json = JSON.parse(text); } catch { throw new Error(`Respons server tidak valid (bukan JSON). Cek error PHP di server.`); } if (!json.success) throw new Error(json.message || 'Login gagal'); _user = json.data; setUserUI(_user); closeModal('modal-login'); ge('login-pass').value = ''; toast(`Selamat datang, ${_user.nama_lengkap}! 👋`, 'success'); // Arahkan ke panel default sesuai role const defaultPanel = { 1:'dashboard', 2:'dashboard', 3:'dashboard', 4:'dashboard' }; switchPanel(defaultPanel[_user.id_role] || 'dashboard'); startAutoRefresh(); } catch(e) { errEl.textContent = e.message || 'Username atau password salah'; errEl.classList.remove('hidden'); } finally { if (btnMasuk) { btnMasuk.disabled = false; btnMasuk.textContent = 'Masuk →'; } } } async function logout() { try { await fetch('api/index.php?r=auth/logout', { method:'DELETE', credentials:'same-origin' }); } catch {} stopAutoRefresh(); _user = null; setUserUI(null); toast('Anda telah logout', 'info'); switchPanel('dashboard'); } /* ── Apply UI per role ── */ function setUserUI(u) { if (u) { const info = ROLE_LABELS[u.id_role] || { icon:'👤', label: u.nama_role, color:'#555' }; ge('user-name-el').textContent = u.nama_lengkap; ge('user-avatar-el').textContent = info.icon; ge('user-role-el').textContent = info.label.toUpperCase(); ge('user-info').classList.remove('hidden'); ge('btn-login').classList.add('hidden'); applyRoleUI(u.id_role); // Tampilkan role banner showRoleBanner(u.id_role, u.nama_lengkap, info); } else { ge('user-info').classList.add('hidden'); ge('btn-login').classList.remove('hidden'); applyRoleUI(99); hideRoleBanner(); } } function showRoleBanner(role, nama, info) { let banner = ge('role-banner'); if (!banner) { banner = document.createElement('div'); banner.id = 'role-banner'; banner.style.cssText = ` position:fixed; top:0; left:0; right:0; z-index:9999; padding:6px 16px; font-size:12px; font-weight:600; display:flex; align-items:center; gap:8px; transition: all 0.3s; `; document.body.prepend(banner); } banner.style.background = info.color; banner.style.color = '#fff'; banner.innerHTML = ` ${info.icon} Login sebagai: ${nama}  |  Role: ${info.label}  |  ${roleDescription(role)} `; // Geser konten ke bawah sedikit document.body.style.paddingTop = '30px'; } function hideRoleBanner() { const banner = ge('role-banner'); if (banner) banner.remove(); document.body.style.paddingTop = ''; } function roleDescription(role) { return { 1: 'Akses penuh — kelola semua data & pengguna', 2: 'Kelola penduduk, bantuan & verifikasi laporan', 3: 'Read-only — monitoring & dashboard', 4: 'Hanya bisa melihat peta & kirim laporan', }[role] || ''; } function applyRoleUI(role) { const allowed = ROLE_MENUS[role] || ROLE_MENUS[99]; // Sembunyikan/tampilkan nav menu document.querySelectorAll('.nav-btn[data-panel]').forEach(btn => { const panel = btn.dataset.panel; btn.style.display = allowed.includes(panel) ? '' : 'none'; }); // Sembunyikan tombol aksi edit/tambah/salur untuk non-editor const canEditRole = [1,2].includes(role); document.querySelectorAll('[id^="btn-tambah"],[id^="btn-salur"],[id^="btn-edit"]') .forEach(el => el.style.display = canEditRole ? '' : 'none'); // Khusus Admin: tampilkan menu log const logBtn = document.querySelector('[data-panel="log"]'); if (logBtn) logBtn.style.display = role === 1 ? '' : 'none'; // Kirim Laporan hanya untuk masyarakat & umum const kirimBtn = ge('nav-kirim-laporan'); if (kirimBtn) kirimBtn.style.display = [4,99].includes(role) ? '' : 'none'; } /* ── Panel routing ── */ let _cur = 'dashboard'; function switchPanel(name) { const role = userRole(); const allowed = ROLE_MENUS[role] || ROLE_MENUS[99]; // Jika panel tidak boleh diakses role ini, redirect ke dashboard if (!allowed.includes(name)) { toast('Anda tidak punya akses ke halaman ini', 'error'); name = 'dashboard'; } document.querySelectorAll('.panel').forEach(p => p.classList.remove('active')); document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active')); ge(`panel-${name}`)?.classList.add('active'); document.querySelector(`[data-panel="${name}"]`)?.classList.add('active'); _cur = name; if (name === 'peta') setTimeout(() => MapApp.init(), 80); else loadPanel(name); } function loadCurrentPanel() { loadPanel(_cur); } function loadPanel(n) { ({ dashboard: Dashboard.load, penduduk: Penduduk.load, keluarga: Keluarga.load, bantuan: Bantuan.load, laporan: LaporanPanel.load, ri: RI.load, prioritas: PrioritasPanel.load, chat: Chat.init, log: LogPanel.load })[n]?.(); } /* ── Auto refresh ── */ function startAutoRefresh() { stopAutoRefresh(); _autoTimer = setInterval(() => { if (_cur === 'dashboard') Dashboard.load(); if (_cur === 'laporan') LaporanPanel.load(); updateHeaderStats(); }, 30000); } function stopAutoRefresh() { clearInterval(_autoTimer); } /* ── Modals ── */ function showModal(id) { ge(id)?.classList.remove('hidden'); } function closeModal(id) { ge(id)?.classList.add('hidden'); } /* ── Toast ── */ function toast(msg, type='info', dur=3800) { const icons = { success:'✅', error:'❌', info:'ℹ️', warning:'⚠️' }; const el = document.createElement('div'); el.className = `toast-item ${type}`; el.innerHTML = `${icons[type]||'ℹ️'}${msg}`; ge('toast-container').appendChild(el); setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, dur); } /* ── Header stats ── */ async function updateHeaderStats() { try { const d = await Api.get('/dashboard'); const s = d.statistik; ge('hp-miskin-n').textContent = s.total_miskin; ge('hp-rentan-n').textContent = s.total_rentan; ge('hp-mampu-n').textContent = s.total_mampu; ge('hp-lap-n').textContent = s.laporan_pending; const nb = ge('nb-laporan'); s.laporan_pending > 0 ? (nb.textContent = s.laporan_pending, nb.classList.remove('hidden')) : nb.classList.add('hidden'); } catch {} } /* ── Events ── */ ge('btn-toggle-sidebar').addEventListener('click', () => { ge('sidebar').classList.toggle('collapsed'); }); document.addEventListener('click', e => { if (e.target.classList.contains('modal-overlay')) e.target.classList.add('hidden'); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden')); }); /* ── Init ── */ (async function init() { try { const res = await fetch('api/index.php?r=auth/me', { credentials:'same-origin' }); const text = await res.text(); const json = JSON.parse(text); if (json.success) { _user = json.data; setUserUI(_user); startAutoRefresh(); } } catch {} Dashboard.load(); updateHeaderStats(); })(); return { currentUser, userRole, canEdit, doLogin, logout, switchPanel, showModal, closeModal, toast, updateHeaderStats }; })();