// assets/js/app.js — WebGIS Bansos (Versi Lengkap — Fixed) const App = (() => { /* ══════════════════════════════════════════════════ STATE ══════════════════════════════════════════════════ */ let map; let activeRadiusCircle = null, activeRadiusIbadahId = null; let radiusMeters = 500; let addMode = null, tempMarker = null; let ibadahMarkers = {}, wargaMarkers = {}; let ibadahData = [], wargaData = []; let ibadahLayerGroup, wargaLayerGroup; let anggotaRows = []; let currentUser = null; async function checkAuthStatus() { try { const res = await fetch('api/auth.php?action=status'); const json = await res.json(); if (json.success && json.logged_in) { currentUser = json.user; updateUserUI(); // Auto-switch to overview for admin/petugas if they are on default tab const activeTab = document.querySelector('.tab-btn.active')?.dataset.tab; if (['admin', 'petugas'].includes(currentUser.role) && activeTab === 'ibadah') { const overviewBtn = document.querySelector('.tab-btn[data-tab="overview"]'); if (overviewBtn) overviewBtn.click(); } } else { currentUser = null; updateUserUI(); } applyRoleUI(); if (window.FiturBaru && FiturBaru.Kebutuhan && typeof FiturBaru.Kebutuhan.load === 'function') { FiturBaru.Kebutuhan.load(); } if (window.FiturBaru && FiturBaru.Laporan && typeof FiturBaru.Laporan.loadList === 'function' && currentUser) { FiturBaru.Laporan.loadList(); } // Force refresh Kebutuhan tab if it's currently showing when auth status changes if (window.FiturBaru && FiturBaru.Kebutuhan && typeof FiturBaru.Kebutuhan.refreshIfActive === 'function') { FiturBaru.Kebutuhan.refreshIfActive(); } } catch (e) { console.error('Error checking auth status:', e); currentUser = null; updateUserUI(); applyRoleUI(); } } function updateUserUI() { const avatar = document.getElementById('user-avatar'); const nameEl = document.getElementById('user-display-name'); const roleEl = document.getElementById('user-display-role'); const btnToggle = document.getElementById('btn-auth-toggle'); if (currentUser) { if (avatar) { avatar.textContent = currentUser.username.charAt(0).toUpperCase(); avatar.className = 'user-avatar'; } if (nameEl) nameEl.textContent = currentUser.username; if (roleEl) { roleEl.textContent = currentUser.role.toUpperCase(); roleEl.className = 'user-role-badge badge-' + currentUser.role; } if (btnToggle) btnToggle.textContent = 'Logout'; } else { if (avatar) { avatar.textContent = 'G'; avatar.className = 'user-avatar avatar-guest'; } if (nameEl) nameEl.textContent = 'Guest Mode'; if (roleEl) { roleEl.textContent = 'Pengunjung'; roleEl.className = 'user-role-badge badge-guest'; } if (btnToggle) btnToggle.textContent = 'Login'; } } function applyRoleUI() { const role = currentUser ? currentUser.role : 'guest'; // Tab buttons const btnOverview = document.querySelector('.tab-btn[data-tab="overview"]'); const btnIbadah = document.getElementById('tab-btn-ibadah'); const btnWarga = document.getElementById('tab-btn-warga'); const btnKebutuhan = document.getElementById('tab-btn-kebutuhan'); const btnLaporan = document.getElementById('tab-btn-laporan'); if (btnOverview) btnOverview.style.display = (role !== 'guest' && role !== 'kontributor') ? '' : 'none'; if (btnIbadah) btnIbadah.style.display = ['admin', 'petugas', 'koordinator', 'kontributor', 'guest'].includes(role) ? '' : 'none'; if (btnWarga) btnWarga.style.display = ['admin', 'petugas', 'koordinator'].includes(role) ? '' : 'none'; if (btnLaporan) btnLaporan.style.display = role !== 'guest' ? '' : 'none'; // Kebutuhan tab: explicit rule so changes to guest handling don't remove kontributor access const kebutuhanAllowed = ['admin', 'koordinator', 'kontributor', 'guest']; if (btnKebutuhan) btnKebutuhan.style.display = kebutuhanAllowed.includes(role) ? '' : 'none'; // If currently selected tab is hidden, switch to a visible one const activeTabBtn = document.querySelector('.tab-btn.active'); if (activeTabBtn && activeTabBtn.style.display === 'none') { const fallbackTab = (role === 'guest' || role === 'kontributor') ? 'ibadah' : 'overview'; const fallbackBtn = document.querySelector(`.tab-btn[data-tab="${fallbackTab}"]`); if (fallbackBtn) fallbackBtn.click(); } // Add buttons const btnAddIbadah = document.getElementById('btn-add-ibadah'); const btnAddWarga = document.getElementById('btn-add-warga'); if (btnAddIbadah) btnAddIbadah.style.display = role === 'admin' ? '' : 'none'; if (btnAddWarga) btnAddWarga.style.display = ['admin', 'koordinator'].includes(role) ? '' : 'none'; // Advanced Features inside Overview const advancedCards = document.querySelectorAll('.ekon-card'); advancedCards.forEach(card => { const onclickAttr = card.getAttribute('onclick') || ''; if (onclickAttr.includes('Redistribusi')) { card.style.display = role === 'admin' ? 'flex' : 'none'; } else if (onclickAttr.includes('Verifikasi')) { card.style.display = ['admin', 'petugas'].includes(role) ? 'flex' : 'none'; } else if (onclickAttr.includes('Kemandirian')) { card.style.display = ['admin', 'petugas'].includes(role) ? 'flex' : 'none'; } }); // Hide admin-only Overview sections for koordinator & petugas (Ekonomi & Kependudukan, Kontrol Radius) if (['koordinator', 'petugas'].includes(role)) { document.querySelectorAll('.section-title').forEach(el => { const txt = (el.textContent || '').trim(); if (txt.includes('Ekonomi & Kependudukan') || txt.includes('Kontrol Radius')) { el.style.display = 'none'; const next = el.nextElementSibling; if (next) next.style.display = 'none'; } }); } else { // ensure visibility for non-koordinator/petugas roles document.querySelectorAll('.section-title').forEach(el => { const txt = (el.textContent || '').trim(); if (txt.includes('Ekonomi & Kependudukan') || txt.includes('Kontrol Radius')) { el.style.display = ''; const next = el.nextElementSibling; if (next) next.style.display = ''; } }); } // Hide Verifikasi Konfigurasi for non-admins const verifTabKonfig = document.querySelector('[data-fbtab="konfig"]'); if (verifTabKonfig) { verifTabKonfig.style.display = role === 'admin' ? '' : 'none'; if (role !== 'admin' && verifTabKonfig.classList.contains('active')) { const verifTabList = document.querySelector('[data-fbtab="list"]'); if (verifTabList) window.switchFbTab(verifTabList, 'verifikasi'); } } } function showAuthModal(intendedAction) { sessionStorage.setItem('intendedAction', JSON.stringify(intendedAction)); document.getElementById('modal-login').classList.add('show'); } async function handleRegister() { const username = document.getElementById('register-username').value.trim(); const email = document.getElementById('register-email').value.trim(); const password = document.getElementById('register-password').value.trim(); const phone = document.getElementById('register-phone')?.value.trim() || ''; const role = document.getElementById('register-role').value; const rumahIbadahSelect = document.getElementById('register-rumah-ibadah'); const rumahIbadahId = rumahIbadahSelect && rumahIbadahSelect.value ? parseInt(rumahIbadahSelect.value) : null; const errorEl = document.getElementById('register-error-msg'); if (!username || !email || !password || !phone || !role) { if (errorEl) { errorEl.textContent = 'Semua field wajib diisi.'; errorEl.style.display = 'block'; } return; } if (password.length < 6) { if (errorEl) { errorEl.textContent = 'Password minimal 6 karakter.'; errorEl.style.display = 'block'; } return; } const payload = { username, email, password, no_telp: phone, role }; if (role === 'koordinator' && rumahIbadahId) payload.rumah_ibadah_id = rumahIbadahId; try { const res = await fetch('api/auth.php?action=register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { if (errorEl) errorEl.style.display = 'none'; showToast(json.message, 'success'); // Auto login after registration document.getElementById('login-username').value = username; document.getElementById('login-password').value = password; await handleLogin(); } else { if (errorEl) { errorEl.textContent = json.message || 'Registrasi gagal.'; errorEl.style.display = 'block'; } } } catch (e) { if (errorEl) { errorEl.textContent = 'Kesalahan jaringan.'; errorEl.style.display = 'block'; } } } // Populate rumah ibadah dropdown for koordinator registration async function loadRumahIbadahOptions() { const select = document.getElementById('register-rumah-ibadah'); if (!select) return; try { const res = await fetch('api/rumah_ibadah.php'); const json = await res.json(); if (!json.success) return; const data = json.data || []; // Clear existing options except placeholder select.innerHTML = ''; data.forEach(item => { const opt = document.createElement('option'); opt.value = item.id; opt.textContent = item.nama; select.appendChild(opt); }); } catch (e) { console.error('Failed load rumah ibadah options', e); } } // Toggle register/login forms function toggleToRegister() { document.getElementById('login-form-container').style.display = 'none'; document.getElementById('register-form-container').style.display = 'block'; document.getElementById('btn-login-submit').style.display = 'none'; document.getElementById('btn-register-submit').style.display = 'inline-block'; loadRumahIbadahOptions(); } function toggleToLogin() { document.getElementById('login-form-container').style.display = 'block'; document.getElementById('register-form-container').style.display = 'none'; document.getElementById('btn-login-submit').style.display = 'inline-block'; document.getElementById('btn-register-submit').style.display = 'none'; } // Show/hide rumah ibadah selector based on role function handleRoleChange() { const role = document.getElementById('register-role').value; const group = document.getElementById('register-rumah-ibadah-group'); if (group) group.style.display = role === 'koordinator' ? 'block' : 'none'; } // Event listeners registration document.addEventListener('DOMContentLoaded', () => { const linkReg = document.getElementById('link-show-register'); if (linkReg) linkReg.addEventListener('click', e => { e.preventDefault(); toggleToRegister(); }); const linkLogin = document.getElementById('link-show-login'); if (linkLogin) linkLogin.addEventListener('click', e => { e.preventDefault(); toggleToLogin(); }); const roleSelect = document.getElementById('register-role'); if (roleSelect) roleSelect.addEventListener('change', handleRoleChange); const btnReg = document.getElementById('btn-register-submit'); if (btnReg) btnReg.addEventListener('click', e => { e.preventDefault(); handleRegister(); }); }); function checkAuth(intendedAction, callback) { if (currentUser) { callback(); } else { showAuthModal(intendedAction); } } async function handleLogin() { const userVal = document.getElementById('login-username').value.trim(); const passVal = document.getElementById('login-password').value.trim(); const errorEl = document.getElementById('login-error-msg'); if (!userVal || !passVal) { if (errorEl) { errorEl.textContent = 'Username dan password wajib diisi!'; errorEl.style.display = 'block'; } return; } try { const res = await fetch('api/auth.php?action=login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: userVal, password: passVal }) }); const json = await res.json(); if (json.success) { if (errorEl) errorEl.style.display = 'none'; showToast(json.message, 'success'); document.getElementById('modal-login').classList.remove('show'); document.getElementById('login-username').value = ''; document.getElementById('login-password').value = ''; await checkAuthStatus(); await loadAllData(); const intended = sessionStorage.getItem('intendedAction'); if (intended) { sessionStorage.removeItem('intendedAction'); const action = JSON.parse(intended); executeIntendedAction(action); } } else { if (errorEl) { errorEl.textContent = json.message || 'Login gagal!'; errorEl.style.display = 'block'; } } } catch (e) { if (errorEl) { errorEl.textContent = 'Terjadi kesalahan server saat login.'; errorEl.style.display = 'block'; } } } async function handleLogout() { // Show custom logout confirmation modal (browser confirm() often gets blocked) document.getElementById('modal-logout-confirm')?.classList.add('show'); } async function doLogout() { document.getElementById('modal-logout-confirm')?.classList.remove('show'); try { const res = await fetch('api/auth.php?action=logout', { method: 'POST' }); const json = await res.json(); if (json.success) { showToast(json.message, 'info'); currentUser = null; updateUserUI(); applyRoleUI(); clearRadius(); // Bersihkan radius dan data highlight saat logout await loadAllData(); if (window.FiturBaru && FiturBaru.Kebutuhan && typeof FiturBaru.Kebutuhan.load === 'function') { FiturBaru.Kebutuhan.load(); } // Force refresh Kebutuhan tab if it's currently showing if (window.FiturBaru && FiturBaru.Kebutuhan && typeof FiturBaru.Kebutuhan.refreshIfActive === 'function') { FiturBaru.Kebutuhan.refreshIfActive(); } // Switch to ibadah tab for guest const overviewBtn = document.querySelector('.tab-btn[data-tab="overview"]'); if (overviewBtn) overviewBtn.style.display = 'none'; const wargaBtn = document.getElementById('tab-btn-warga'); if (wargaBtn) wargaBtn.style.display = 'none'; const ibadahBtn = document.querySelector('.tab-btn[data-tab="ibadah"]'); if (ibadahBtn) ibadahBtn.click(); } } catch (e) { showToast('Gagal logout', 'error'); } } function executeIntendedAction(action) { if (!action) return; if (action.type === 'add_ibadah') { setAddMode('ibadah'); showToast('Klik peta untuk menentukan lokasi Rumah Ibadah', 'info'); } else if (action.type === 'add_warga') { setAddMode('warga'); showToast('Klik peta untuk menentukan lokasi Rumah Warga', 'info'); } else if (action.type === 'edit_ibadah') { editIbadah(action.id); } else if (action.type === 'edit_warga') { editWarga(action.id); } else if (action.type === 'delete_ibadah') { deleteIbadah(action.id); } else if (action.type === 'delete_warga') { deleteWarga(action.id); } else if (action.type === 'donate') { if (window.FiturBaru && FiturBaru.Kebutuhan && typeof FiturBaru.Kebutuhan.openDonationModal === 'function') { FiturBaru.Kebutuhan.openDonationModal(action.kebutuhan_id, action.sisa, action.jenis, action.ibadah_nama); } } } /* ══════════════════════════════════════════════════ SVG LIBRARY ══════════════════════════════════════════════════ */ const SVG = { mosque: ``, church: ``, temple: ``, star: ``, wheel: ``, building: ``, home: ``, users: ``, pin: ``, check: ``, x: ``, info: ``, edit: ``, trash: ``, gift: ``, }; const JENIS_CONFIG = { masjid: { svgKey: 'mosque', label: 'Masjid' }, gereja: { svgKey: 'church', label: 'Gereja' }, klenteng: { svgKey: 'temple', label: 'Klenteng' }, pura: { svgKey: 'star', label: 'Pura' }, vihara: { svgKey: 'wheel', label: 'Vihara' }, lainnya: { svgKey: 'building', label: 'Lainnya' }, }; const KATEGORI_CONFIG = { sangat_miskin: { label: 'Sangat Kurang Mampu', color: '#f87171', badgeCls: 'badge-sangat-miskin' }, miskin: { label: 'Kurang Mampu', color: '#6366f1', badgeCls: 'badge-miskin' }, rentan_miskin: { label: 'Rentan Kurang Mampu', color: '#fbbf24', badgeCls: 'badge-rentan-miskin' }, }; const STATUS_CONFIG = { belum_terima: { label: 'Belum Terima', cls: 'badge-belum-terima' }, sudah_terima: { label: 'Sudah Terima', cls: 'badge-sudah-terima' }, sedang_proses: { label: 'Sedang Proses', cls: 'badge-sedang-proses' }, }; const REK_CONFIG = { bantuan_rutin: { label: 'Bantuan Rutin', cls: 'badge-bantuan-rutin', boxCls: 'rutin', desc: 'Keluarga ini memerlukan bantuan sosial rutin (sembako, tunai, dll) karena tidak memiliki anggota produktif yang mampu bekerja.' }, bantuan_pelatihan: { label: 'Bantuan Pelatihan', cls: 'badge-bantuan-pelatihan', boxCls: 'pelatihan', desc: 'Terdapat anggota keluarga usia produktif yang belum bekerja tetap. Direkomendasikan pelatihan keterampilan atau program pemberdayaan.' }, keduanya: { label: 'Rutin + Pelatihan', cls: 'badge-keduanya', boxCls: 'keduanya', desc: 'Keluarga ini memerlukan bantuan rutin sekaligus ada potensi pemberdayaan melalui pelatihan kerja.' }, tidak_ada: { label: 'Tidak Diperlukan', cls: 'badge-tidak-ada', boxCls: 'tidak-ada', desc: 'Kondisi keluarga sudah cukup mandiri. Tidak ada rekomendasi bantuan khusus saat ini.' }, }; /* ══════════════════════════════════════════════════ HELPER FUNCTIONS ══════════════════════════════════════════════════ */ function panToPoint(lat, lng, minZoom = 15) { const cur = map.getZoom(); const z = Math.max(cur, minZoom); if (cur === z) map.panTo([lat, lng], { animate: true, duration: 0.5 }); else map.flyTo([lat, lng], z, { duration: 0.6 }); } function haversine(lat1, lng1, lat2, lng2) { const R = 6371000; const dLat = (lat2 - lat1) * Math.PI / 180; const dLng = (lng2 - lng1) * Math.PI / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } function hitungUsia(tglLahir) { if (!tglLahir) return null; const lahir = new Date(tglLahir); const today = new Date(); let usia = today.getFullYear() - lahir.getFullYear(); const m = today.getMonth() - lahir.getMonth(); if (m < 0 || (m === 0 && today.getDate() < lahir.getDate())) usia--; return usia; } function rekomendasiLocal(anggotaList, penghasilan, statusPekerjaan) { let adaProd = false, adaProdBisa = false, semuaProdTakBisa = true; anggotaList.forEach(a => { const usia = hitungUsia(a.tanggal_lahir); if (usia !== null && usia >= 18 && usia <= 55) { adaProd = true; const bisaKerja = parseInt(a.dapat_bekerja) === 1 && ['sehat', 'sakit_ringan'].includes(a.kondisi_kesehatan); if (bisaKerja) semuaProdTakBisa = false; if (bisaKerja && a.status_bekerja !== 'bekerja') adaProdBisa = true; } }); const perluRutin = !adaProd || (adaProd && semuaProdTakBisa) || parseFloat(penghasilan) < 500000 || statusPekerjaan === 'tidak_bekerja'; const perluPelatihan = adaProdBisa; if (perluRutin && perluPelatihan) return 'keduanya'; if (perluRutin) return 'bantuan_rutin'; if (perluPelatihan) return 'bantuan_pelatihan'; return 'tidak_ada'; } function formatRupiah(n) { return 'Rp ' + parseInt(n || 0).toLocaleString('id-ID'); } function svgIcon(key, w = 14, h = 14) { return SVG[key]?.replace(' { const el = document.getElementById(id); if (el) el.textContent = val; }; set('stat-ibadah', d.total_ibadah || 0); set('stat-warga', d.total_warga || 0); set('stat-jiwa', d.total_jiwa || 0); set('stat-bansos', d.status_bansos?.sudah_terima || 0); const rutin = (d.rekomendasi_bantuan?.bantuan_rutin || 0) + (d.rekomendasi_bantuan?.keduanya || 0); const pelatihan = (d.rekomendasi_bantuan?.bantuan_pelatihan || 0) + (d.rekomendasi_bantuan?.keduanya || 0); set('stat-rutin', rutin); set('stat-pelatihan', pelatihan); // Field baru dari statistik.php const avgPh = parseInt(d.rata_penghasilan || 0); set('stat-avg-penghasilan', 'Rp ' + avgPh.toLocaleString('id-ID')); set('stat-anggota-db', (d.total_anggota_db || 0) + ' jiwa'); } catch { /* silent */ } } /* ══════════════════════════════════════════════════ MARKERS IBADAH ══════════════════════════════════════════════════ */ function renderIbadahMarkers() { ibadahLayerGroup.clearLayers(); ibadahMarkers = {}; ibadahData.forEach(d => { const cfg = JENIS_CONFIG[d.jenis] || JENIS_CONFIG.lainnya; const icon = L.divIcon({ className: '', html: `
${svgIcon(cfg.svgKey, 18, 18)}
`, iconSize: [38, 38], iconAnchor: [19, 19], }); const marker = L.marker([d.latitude, d.longitude], { icon, draggable: true }) .on('click', () => { if (!addMode) onIbadahClick(d, marker); }) .on('dragend', async e => { const { lat, lng } = e.target.getLatLng(); d.latitude = lat; d.longitude = lng; const res = await fetch('api/rumah_ibadah.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: d.id, nama: d.nama, jenis: d.jenis, penanggung_jawab: d.penanggung_jawab, kontak: d.kontak, alamat: d.alamat, kelurahan: d.kelurahan, kecamatan: d.kecamatan, kota: d.kota, keterangan: d.keterangan, kapasitas_kk: d.kapasitas_kk ?? 20, radius_primer: d.radius_primer ?? 500, anggaran_bulanan: d.anggaran_bulanan ?? 0, latitude: lat, longitude: lng }) }); const json = await res.json(); if (json.success) { showToast('Koordinat diperbarui', 'success'); await loadIbadah(); } else { showToast('Gagal menyimpan koordinat', 'error'); } if (activeRadiusIbadahId == d.id) { activeRadiusCircle.setLatLng([lat, lng]); refreshRadiusIfActive(); } }); ibadahLayerGroup.addLayer(marker); ibadahMarkers[d.id] = marker; }); } /* ══════════════════════════════════════════════════ MARKERS WARGA ══════════════════════════════════════════════════ */ function renderWargaMarkers(highlightIds = new Set()) { wargaLayerGroup.clearLayers(); wargaMarkers = {}; const isKontributor = currentUser && currentUser.role === 'kontributor'; const canDrag = currentUser && ['admin', 'petugas'].includes(currentUser.role); const visibleWarga = getWargaForRole(); visibleWarga.forEach(d => { const inRadius = highlightIds.has(parseInt(d.id)); const cfg = KATEGORI_CONFIG[d.kategori_kemiskinan] || KATEGORI_CONFIG.miskin; let markerColor; if (inRadius) { markerColor = (d.status_bantuan === 'sudah_terima') ? '#10b981' : '#ef4444'; } else { markerColor = cfg.color; } let htmlContent = ''; let iconSize = [13, 13]; let iconAnchor = [6.5, 6.5]; if (inRadius) { iconSize = [28, 28]; iconAnchor = [14, 28]; const pulse = isKontributor ? `
` : ''; htmlContent = `
${pulse}
`; } else { const size = 13; const shadow = '0 2px 6px rgba(0,0,0,0.2)'; const pulse = isKontributor ? `
` : ''; htmlContent = `
${pulse}
`; } const icon = L.divIcon({ className: '', html: htmlContent, iconSize: iconSize, iconAnchor: iconAnchor, }); const marker = L.marker([d.latitude, d.longitude], { icon, draggable: canDrag }) .on('click', () => { if (!addMode) onWargaClick(d); }); // Only attach drag handler for roles that can edit if (canDrag) { marker.on('dragend', async e => { const { lat, lng } = e.target.getLatLng(); d.latitude = lat; d.longitude = lng; const resW = await fetch('api/rumah_warga.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: d.id, no_kk: d.no_kk, nama_kepala_keluarga: d.nama_kepala_keluarga, nik: d.nik, alamat: d.alamat, kelurahan: d.kelurahan, kecamatan: d.kecamatan, kota: d.kota, jumlah_tanggungan: d.jumlah_tanggungan, penghasilan_bulanan: d.penghasilan_bulanan, pekerjaan: d.pekerjaan, status_pekerjaan: d.status_pekerjaan, luas_rumah: d.luas_rumah, jenis_lantai: d.jenis_lantai, jenis_dinding: d.jenis_dinding, sumber_air: d.sumber_air, status_tempat_tinggal: d.status_tempat_tinggal, kategori_kemiskinan: d.is_kategori_manual ? d.kategori_kemiskinan : 'sistem', status_bantuan: d.status_bantuan, rekomendasi_bantuan: d.is_rekomendasi_manual ? d.rekomendasi_bantuan : 'sistem', keterangan: d.keterangan, latitude: lat, longitude: lng }) }); const jsonW = await resW.json(); if (jsonW.success) { showToast('Koordinat diperbarui', 'success'); await loadWarga(); } else { showToast('Gagal menyimpan koordinat', 'error'); } refreshRadiusIfActive(); }); } // Tooltip for kontributor: location indicator only if (isKontributor) { marker.bindTooltip('📍 Lokasi Penerima Manfaat', { direction: 'top', offset: [0, -8], opacity: 0.9, className: 'kontributor-tooltip' }); } wargaLayerGroup.addLayer(marker); wargaMarkers[d.id] = marker; }); } /* ══════════════════════════════════════════════════ CLICK HANDLERS ══════════════════════════════════════════════════ */ function onIbadahClick(d, marker) { if (addMode) return; drawRadius(d, marker); showInfoIbadah(d); panToPoint(d.latitude, d.longitude, 15); } async function onWargaClick(d) { if (addMode) return; await showInfoWarga(d); panToPoint(d.latitude, d.longitude, 16); } /* ══════════════════════════════════════════════════ RADIUS ══════════════════════════════════════════════════ */ function drawRadius(ibadah, marker) { if (activeRadiusCircle) map.removeLayer(activeRadiusCircle); activeRadiusCircle = L.circle([ibadah.latitude, ibadah.longitude], { radius: radiusMeters, color: '#6366f1', fillColor: '#6366f1', fillOpacity: 0.07, weight: 1.5, dashArray: '6 4', }).addTo(map); activeRadiusIbadahId = ibadah.id; refreshRadiusData(ibadah); } function getWargaInRadius(ibadahId, lat, lng, r) { return getWargaForRole().filter(w => { const kelolaId = w.dikelola_oleh_ibadah_id ? parseInt(w.dikelola_oleh_ibadah_id) : null; if (kelolaId) { // Jika ada penugasan eksplisit, tampilkan jika dia milik ibadah ini return kelolaId === parseInt(ibadahId); } else { // Jika belum ada penugasan, cek radius Haversine return haversine(lat, lng, parseFloat(w.latitude), parseFloat(w.longitude)) <= r; } }); } function getWargaForRole() { if (currentUser?.role === 'koordinator' && currentUser.rumah_ibadah_id) { const myId = parseInt(currentUser.rumah_ibadah_id); const myIbadah = ibadahData.find(x => parseInt(x.id) === myId); if (myIbadah) { return wargaData.filter(w => { const kelolaId = w.dikelola_oleh_ibadah_id ? parseInt(w.dikelola_oleh_ibadah_id) : null; if (kelolaId) { return kelolaId === myId; } else { return haversine(parseFloat(myIbadah.latitude), parseFloat(myIbadah.longitude), parseFloat(w.latitude), parseFloat(w.longitude)) <= radiusMeters; } }); } return []; } return wargaData; } function refreshRadiusData(ibadah) { const inRadius = getWargaInRadius(ibadah.id, ibadah.latitude, ibadah.longitude, radiusMeters); const ids = new Set(inRadius.map(m => parseInt(m.id))); renderWargaMarkers(ids); showRadiusInfo(ibadah.nama, radiusMeters, inRadius.length); if (currentUser?.role === 'koordinator') { renderWargaList(); } } function refreshRadiusIfActive() { if (currentUser?.role === 'koordinator' && currentUser.rumah_ibadah_id) { if (activeRadiusCircle) { activeRadiusCircle.setRadius(radiusMeters); } const ibadah = ibadahData.find(d => parseInt(d.id) === parseInt(currentUser.rumah_ibadah_id)); if (ibadah) { refreshRadiusData(ibadah); } return; } if (!activeRadiusCircle || !activeRadiusIbadahId) return; const ibadah = ibadahData.find(d => parseInt(d.id) === parseInt(activeRadiusIbadahId)); if (!ibadah) return; activeRadiusCircle.setRadius(radiusMeters); refreshRadiusData(ibadah); } function clearRadius() { if (activeRadiusCircle) { map.removeLayer(activeRadiusCircle); activeRadiusCircle = null; } activeRadiusIbadahId = null; renderWargaMarkers(new Set()); hideRadiusInfo(); } function showRadiusInfo(nama, r, count) { const bar = document.getElementById('radius-info'); bar.querySelector('.ri-val').textContent = r >= 1000 ? (r / 1000).toFixed(1) + ' km' : r + ' m'; bar.querySelector('.ri-count').textContent = count + ' rumah'; bar.classList.add('show'); } function hideRadiusInfo() { document.getElementById('radius-info').classList.remove('show'); } /* ══════════════════════════════════════════════════ RADIUS CONTROL ══════════════════════════════════════════════════ */ function initRadiusControl() { const slider = document.getElementById('radius-slider'); const display = document.getElementById('radius-display'); const input = document.getElementById('radius-input'); const updateGrad = () => { const pct = (radiusMeters - slider.min) / (slider.max - slider.min) * 100; slider.style.background = `linear-gradient(to right,#6366f1 0%,#6366f1 ${pct}%,#e2e8f0 ${pct}%)`; }; slider.addEventListener('input', () => { radiusMeters = parseInt(slider.value); display.textContent = radiusMeters >= 1000 ? (radiusMeters / 1000).toFixed(1) + ' km' : radiusMeters + ' m'; input.value = radiusMeters; updateGrad(); refreshRadiusIfActive(); }); input.addEventListener('change', () => { radiusMeters = Math.max(50, Math.min(5000, parseInt(input.value) || 500)); slider.value = radiusMeters; slider.dispatchEvent(new Event('input')); }); document.querySelectorAll('.preset-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); radiusMeters = parseInt(btn.dataset.r); slider.value = radiusMeters; slider.dispatchEvent(new Event('input')); }); }); updateGrad(); } /* ══════════════════════════════════════════════════ INFO PANEL ══════════════════════════════════════════════════ */ function showInfoIbadah(d) { const cfg = JENIS_CONFIG[d.jenis] || JENIS_CONFIG.lainnya; const panel = document.getElementById('info-panel'); const header = document.getElementById('info-panel-title'); const body = document.getElementById('info-panel-body'); header.innerHTML = `
${svgIcon(cfg.svgKey, 14, 14)}
${d.nama}`; body.innerHTML = `
${svgIcon('pin', 11, 11)}Memuat alamat...
Jenis${cfg.label}
${d.penanggung_jawab ? `
Penanggung Jwb${d.penanggung_jawab}
` : ''} ${d.kontak ? `
Kontak${d.kontak}
` : ''} ${d.kelurahan ? `
Kelurahan${d.kelurahan}
` : ''} ${d.kecamatan ? `
Kecamatan${d.kecamatan}
` : ''} ${d.kota ? `
Kota${d.kota}
` : ''} ${d.keterangan ? `
Ket.${d.keterangan}
` : ''}
${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}
${(currentUser && currentUser.role === 'admin') ? `
` : ''}`; panel.classList.add('show'); reverseGeocode(d.latitude, d.longitude, addr => { const el = document.getElementById('info-address'); if (el) el.textContent = addr; }); } async function showInfoWarga(d) { const isKoor = currentUser && currentUser.role === 'koordinator'; const isKontributor = currentUser && currentUser.role === 'kontributor'; const isGuest = !currentUser; const cat = d.kategori_kemiskinan ? (KATEGORI_CONFIG[d.kategori_kemiskinan] || KATEGORI_CONFIG.miskin) : null; const sts = STATUS_CONFIG[d.status_bantuan] || STATUS_CONFIG.belum_terima; const rek = REK_CONFIG[d.rekomendasi_bantuan] || REK_CONFIG.tidak_ada; const panel = document.getElementById('info-panel'); const header = document.getElementById('info-panel-title'); const body = document.getElementById('info-panel-body'); if (isKontributor || isGuest) { const title = isGuest ? 'Lokasi Penerima Manfaat' : 'Rumah Penerima Manfaat'; header.innerHTML = `
${svgIcon('home', 14, 14)}
${title}`; let htmlContent = `
${svgIcon('pin', 11, 11)}Memuat alamat...
Kategori${cat ? cat.label : 'Kurang Mampu'}
Status Bansos${sts.label}
${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}
🔒 Privasi Terjaga
Detail informasi pribadi warga disembunyikan untuk menjaga privasi penerima manfaat.
`; if (isKontributor) { htmlContent += ` `; } body.innerHTML = htmlContent; panel.classList.add('show'); reverseGeocode(d.latitude, d.longitude, addr => { const el = document.getElementById('info-address'); if (el) el.textContent = addr; }); return; } header.innerHTML = `
${svgIcon('home', 14, 14)}
${d.nama_kepala_keluarga}`; let htmlContent = `
${svgIcon('pin', 11, 11)}Memuat alamat...
`; if (!isKoor) { htmlContent += ` ${d.no_kk ? `
No. KK${d.no_kk}
` : ''} ${d.nik ? `
NIK${d.nik}
` : ''}
Tanggungan${d.jumlah_tanggungan} jiwa
Penghasilan${formatRupiah(d.penghasilan_bulanan)}/bln
${d.pekerjaan ? `
Pekerjaan${d.pekerjaan}
` : ''}
Kategori${cat.label}
`; } htmlContent += `
Bansos${sts.label}
Rekomendasi${rek.label}
${d.dikelola_oleh_ibadah_id ? (() => { const ibadah = ibadahData.find(i => parseInt(i.id) === parseInt(d.dikelola_oleh_ibadah_id)); return ibadah ? `
Penanggung Bantuan:
${ibadah.nama}
` : ''; })() : ''} ${!isKoor ? `
${rek.label}
${rek.desc}
` : ''} ${d.kelurahan ? `
Kelurahan${d.kelurahan}
` : ''} ${d.kecamatan ? `
Kecamatan${d.kecamatan}
` : ''} ${d.kota ? `
Kota${d.kota}
` : ''} ${!isKoor && d.keterangan ? `
Ket.${d.keterangan}
` : ''}
${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}
`; if (!isKoor) { htmlContent += `
${svgIcon('users', 11, 11)} Anggota Keluarga
`; } if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'petugas' || currentUser.role === 'koordinator')) { htmlContent += `
${currentUser.role === 'admin' ? `` : ''}
`; } if (currentUser && (currentUser.role === 'kontributor' || currentUser.role === 'koordinator')) { htmlContent += ` `; } body.innerHTML = htmlContent; panel.classList.add('show'); reverseGeocode(d.latitude, d.longitude, addr => { const el = document.getElementById('info-address'); if (el) el.textContent = addr; }); if (!isKoor) { try { const res = await fetch(`api/anggota_keluarga.php?rumah_warga_id=${d.id}`); const json = await res.json(); const container = document.getElementById('anggota-info-list'); if (!container) return; if (!json.data?.length) { container.innerHTML = `
Belum ada data anggota
`; return; } const PEND = { tidak_sekolah: 'Tdk Sekolah', sd: 'SD', smp: 'SMP', sma: 'SMA', d3: 'D3', s1: 'S1', s2: 'S2', s3: 'S3' }; container.innerHTML = `
${json.data.map(a => { const usia = a.usia !== null ? `${a.usia} th` : '–'; const prod = a.usia >= 18 && a.usia <= 55; const bisaKerja = parseInt(a.dapat_bekerja) === 1 && ['sehat', 'sakit_ringan'].includes(a.kondisi_kesehatan); const dotColor = prod && bisaKerja ? '#10b981' : (prod ? '#fbbf24' : '#94a3b8'); return `
${a.nama} (${usia})
${a.jenis_kelamin === 'laki_laki' ? '♂' : '♀'} · ${PEND[a.pendidikan] || a.pendidikan} ${a.pekerjaan ? ' · ' + a.pekerjaan : ''} ${a.kondisi_kesehatan !== 'sehat' ? ` · ${a.kondisi_kesehatan.replace(/_/g, ' ')}` : ''} ${parseInt(a.dapat_bekerja) ? '' : ' · Tdk bisa kerja'}
`; }).join('')}
`; } catch { /* silent */ } } } /* ══════════════════════════════════════════════════ REVERSE GEOCODING ══════════════════════════════════════════════════ */ async function reverseGeocode(lat, lng, cb) { try { const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&accept-language=id`); const json = await res.json(); cb(json.display_name || `${lat}, ${lng}`, json.address || {}); } catch { cb(`${lat}, ${lng}`, {}); } } /* ══════════════════════════════════════════════════ ADD MODE ══════════════════════════════════════════════════ */ function setAddMode(mode) { addMode = mode; if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } const ind = document.getElementById('mode-indicator'); if (mode === 'ibadah') { ind.innerHTML = `
Klik peta untuk menentukan lokasi Rumah Ibadah`; ind.classList.add('show'); map.getContainer().style.cursor = 'crosshair'; } else if (mode === 'warga') { ind.innerHTML = `
Klik peta untuk menentukan lokasi Rumah Warga`; ind.classList.add('show'); map.getContainer().style.cursor = 'crosshair'; } else { ind.classList.remove('show'); map.getContainer().style.cursor = ''; } } function cancelAddMode() { setAddMode(null); if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } } function onMapClick(e) { if (!addMode) return; const { lat, lng } = e.latlng; if (tempMarker) map.removeLayer(tempMarker); if (addMode === 'ibadah') { document.getElementById('ibadah-lat').value = lat; document.getElementById('ibadah-lng').value = lng; updateCoordDisplay('ibadah', lat, lng); tempMarker = L.marker([lat, lng], { icon: L.divIcon({ className: '', html: `
${svgIcon('mosque', 18, 18)}
`, iconSize: [38, 38], iconAnchor: [19, 19] }) }).addTo(map); // reverseGeocode is called inside openIbadahModal — no need to call again here // Setelah memilih titik untuk rumah ibadah, buka modal pre-filled dan keluar dari add mode openIbadahModal(null, lat, lng); cancelAddMode(); } else if (addMode === 'warga') { document.getElementById('warga-lat').value = lat; document.getElementById('warga-lng').value = lng; updateCoordDisplay('warga', lat, lng); tempMarker = L.marker([lat, lng], { icon: L.divIcon({ className: '', html: `
`, iconSize: [14, 14], iconAnchor: [7, 7] }) }).addTo(map); // reverseGeocode is called inside openWargaModal — no need to call again here // Setelah memilih titik untuk rumah warga, buka modal pre-filled dan keluar dari add mode openWargaModal(null, lat, lng); cancelAddMode(); } } function updateCoordDisplay(type, lat, lng) { const el = document.getElementById(`${type}-coord-display`); if (el) el.textContent = lat && lng ? `${parseFloat(lat).toFixed(6)}, ${parseFloat(lng).toFixed(6)}` : 'Belum dipilih — klik di peta'; } /* ══════════════════════════════════════════════════ MODAL IBADAH ══════════════════════════════════════════════════ */ function openIbadahModal(data = null, lat = null, lng = null) { document.getElementById('modal-ibadah-title').textContent = data ? 'Edit Rumah Ibadah' : 'Tambah Rumah Ibadah'; document.getElementById('ibadah-id').value = data?.id || ''; document.getElementById('ibadah-nama').value = data?.nama || ''; document.getElementById('ibadah-jenis').value = data?.jenis || 'masjid'; document.getElementById('ibadah-pj').value = data?.penanggung_jawab || ''; document.getElementById('ibadah-kontak').value = data?.kontak || ''; document.getElementById('ibadah-alamat').value = data?.alamat || ''; document.getElementById('ibadah-kelurahan').value = data?.kelurahan || ''; document.getElementById('ibadah-kecamatan').value = data?.kecamatan || ''; document.getElementById('ibadah-kota').value = data?.kota || ''; document.getElementById('ibadah-ket').value = data?.keterangan || ''; document.getElementById('ibadah-kapasitas').value = data?.kapasitas_kk || 20; document.getElementById('ibadah-radius').value = data?.radius_primer || 500; document.getElementById('ibadah-anggaran').value = data?.anggaran_bulanan || 0; const latVal = data ? data.latitude : (lat || ''); const lngVal = data ? data.longitude : (lng || ''); document.getElementById('ibadah-lat').value = latVal; document.getElementById('ibadah-lng').value = lngVal; updateCoordDisplay('ibadah', latVal, lngVal); if (latVal && lngVal) reverseGeocode(latVal, lngVal, (addr, details) => { const h = document.getElementById('ibadah-addr-hint'); if (h) h.textContent = addr; if (!data) { if (details && Object.keys(details).length > 0) { const kel = details.village || details.suburb || details.neighbourhood || ''; const kec = details.county || details.city_district || details.town || ''; const kot = details.city || details.municipality || details.regency || details.county || ''; if (kel && !document.getElementById('ibadah-kelurahan').value) document.getElementById('ibadah-kelurahan').value = kel; if (kec && !document.getElementById('ibadah-kecamatan').value) document.getElementById('ibadah-kecamatan').value = kec; if (kot && !document.getElementById('ibadah-kota').value) document.getElementById('ibadah-kota').value = kot; } else { if (h) h.innerHTML = addr + ' (Alamat detail tidak didapatkan, silahkan isi manual)'; } } }); document.getElementById('modal-ibadah').classList.add('show'); } async function saveIbadah() { const id = document.getElementById('ibadah-id').value; const nama = document.getElementById('ibadah-nama').value.trim(); const lat = document.getElementById('ibadah-lat').value; const lng = document.getElementById('ibadah-lng').value; if (!nama) { showToast('Nama wajib diisi', 'error'); return; } if (!lat || !lng) { showToast('Pilih lokasi di peta terlebih dahulu', 'error'); return; } const payload = { id, nama, jenis: document.getElementById('ibadah-jenis').value, penanggung_jawab: document.getElementById('ibadah-pj').value, kontak: document.getElementById('ibadah-kontak').value, alamat: document.getElementById('ibadah-alamat').value, kelurahan: document.getElementById('ibadah-kelurahan').value, kecamatan: document.getElementById('ibadah-kecamatan').value, kota: document.getElementById('ibadah-kota').value, keterangan: document.getElementById('ibadah-ket').value, kapasitas_kk: document.getElementById('ibadah-kapasitas').value, radius_primer: document.getElementById('ibadah-radius').value, anggaran_bulanan: document.getElementById('ibadah-anggaran').value, latitude: lat, longitude: lng, }; try { const res = await fetch('api/rumah_ibadah.php', { method: id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { showToast(json.message, 'success'); closeModal('modal-ibadah'); cancelAddMode(); await loadIbadah(); loadStats(); refreshRadiusIfActive(); } else showToast(json.message, 'error'); } catch { showToast('Terjadi kesalahan', 'error'); } } /* ══════════════════════════════════════════════════ MODAL WARGA — ANGGOTA ROWS ══════════════════════════════════════════════════ */ function buildAnggotaRow(data = {}, index = 0) { return `
Anggota ${typeof index === 'number' ? index + 1 : ''}
`; } function collectAnggotaRows() { const container = document.getElementById('anggota-form-container'); if (!container) return []; const items = container.querySelectorAll('[data-arow]'); const result = [], seen = new Set(); items.forEach(el => { const idx = el.dataset.arow; if (seen.has(idx)) return; seen.add(idx); const row = { dapat_bekerja: 1 }; const inputs = container.querySelectorAll(`[name][data-arow="${idx}"]`); inputs.forEach(i => { row[i.name] = i.tagName === 'SELECT' ? i.value : i.value.trim(); }); if (row.nama) result.push(row); }); return result; } function kategoriLocal(penghasilan, tanggungan, tinggal, air) { let p = tanggungan > 0 ? penghasilan / tanggungan : penghasilan; let skor = 0; if (p < 300000) skor += 40; else if (p < 600000) skor += 20; if (tinggal === 'numpang' || tinggal === 'sewa') skor += 20; if (air === 'sungai' || air === 'hujan') skor += 10; if (skor >= 50) return 'sangat_miskin'; if (skor >= 20) return 'miskin'; return 'rentan_miskin'; } function previewRek() { const anggota = collectAnggotaRows(); const penghasilan = parseFloat(document.getElementById('warga-penghasilan')?.value || 0); const tanggungan = parseInt(document.getElementById('warga-tanggungan')?.value || 1); const tinggal = document.getElementById('warga-tempat')?.value || 'milik_sendiri'; const air = document.getElementById('warga-air')?.value || 'sumur'; const stsPekerjaan = document.getElementById('warga-stspekerjaan')?.value || 'tidak_bekerja'; const isKatSistem = document.getElementById('warga-kategori')?.value === 'sistem'; const rek = rekomendasiLocal(anggota, penghasilan, stsPekerjaan); const cfg = REK_CONFIG[rek] || REK_CONFIG.tidak_ada; const box = document.getElementById('rek-preview-box'); if (box) { let html = `
Prediksi Bantuan: ${cfg.label}
${cfg.desc}
`; if (isKatSistem) { const kat = kategoriLocal(penghasilan, tanggungan, tinggal, air); let katLabel = ''; if (kat === 'sangat_miskin') katLabel = 'Sangat Kurang Mampu'; else if (kat === 'miskin') katLabel = 'Kurang Mampu'; else if (kat === 'rentan_miskin') katLabel = 'Rentan Kurang Mampu'; let katCls = 'badge-miskin'; if (kat === 'sangat_miskin') katCls = 'badge-sangat-miskin'; else if (kat === 'rentan_miskin') katCls = 'badge-rentan-miskin'; html = `
Prediksi Kategori: ${katLabel}
` + html; } box.innerHTML = html; } } /* ══════════════════════════════════════════════════ MODAL WARGA FIX: openWargaModal sekarang reset sub-tab dengan cara yang aman (tidak bergantung pada querySelector global yang bisa bentrok dengan elemen lain) ══════════════════════════════════════════════════ */ async function openWargaModal(data = null, lat = null, lng = null) { const isKoor = currentUser && currentUser.role === 'koordinator'; document.getElementById('modal-warga-title').textContent = data ? 'Edit Rumah Warga' : 'Tambah Rumah Warga'; // Reset sub-tab modal warga — pakai scope modal biar aman const modalEl = document.getElementById('modal-warga'); modalEl.querySelectorAll('.modal-tab-btn').forEach(b => b.classList.remove('active')); modalEl.querySelectorAll('.modal-tab-panel').forEach(p => p.classList.remove('active')); const firstTabBtn = modalEl.querySelector('.modal-tab-btn[data-mtab="data-pokok"]'); const firstTabPanel = document.getElementById('mtab-data-pokok'); if (firstTabBtn) firstTabBtn.classList.add('active'); if (firstTabPanel) firstTabPanel.classList.add('active'); // Hide tabs if coordinator const tabRumahBtn = modalEl.querySelector('.modal-tab-btn[data-mtab="rumah"]'); const tabAnggotaBtn = modalEl.querySelector('.modal-tab-btn[data-mtab="anggota"]'); if (tabRumahBtn) tabRumahBtn.style.display = isKoor ? 'none' : ''; if (tabAnggotaBtn) tabAnggotaBtn.style.display = isKoor ? 'none' : ''; const toggleField = (id, show) => { const el = document.getElementById(id); if (el) { const fg = el.closest('.form-group'); if (fg) fg.style.display = show ? '' : 'none'; } }; const disableField = (id, disable) => { const el = document.getElementById(id); if (el) el.disabled = disable; }; const fieldsToToggle = [ 'warga-nokk', 'warga-nik', 'warga-tanggungan', 'warga-penghasilan', 'warga-pekerjaan', 'warga-stspekerjaan', 'warga-kategori', 'warga-rekomendasi', 'warga-ket', 'warga-coord-display' ]; fieldsToToggle.forEach(fId => toggleField(fId, !isKoor)); const fieldsToDisable = [ 'warga-nama', 'warga-alamat', 'warga-kelurahan', 'warga-kecamatan', 'warga-kota' ]; fieldsToDisable.forEach(fId => disableField(fId, isKoor)); const isAdmin = currentUser && currentUser.role === 'admin'; disableField('warga-dikelola-oleh', !isAdmin); // Also hide prediction/preview box if coordinator const previewBox = document.getElementById('rek-preview-box'); if (previewBox) previewBox.style.display = isKoor ? 'none' : ''; // Hide empty form-rows (rows where all child form-groups are hidden) for clean layout if (isKoor) { const dataPokokPanel = document.getElementById('mtab-data-pokok'); if (dataPokokPanel) { dataPokokPanel.querySelectorAll('.form-row').forEach(row => { const groups = row.querySelectorAll('.form-group'); const allHidden = Array.from(groups).every(g => g.style.display === 'none'); row.style.display = allHidden ? 'none' : ''; }); } } else { // Restore all form-rows for non-coordinator modalEl.querySelectorAll('.form-row').forEach(row => { row.style.display = ''; }); } document.getElementById('warga-id').value = data?.id || ''; document.getElementById('warga-nokk').value = data?.no_kk || ''; document.getElementById('warga-nama').value = data?.nama_kepala_keluarga || ''; document.getElementById('warga-nik').value = data?.nik || ''; document.getElementById('warga-tanggungan').value = data?.jumlah_tanggungan || 1; document.getElementById('warga-penghasilan').value = data?.penghasilan_bulanan || 0; document.getElementById('warga-pekerjaan').value = data?.pekerjaan || ''; document.getElementById('warga-stspekerjaan').value = data?.status_pekerjaan || 'tidak_bekerja'; document.getElementById('warga-alamat').value = data?.alamat || ''; document.getElementById('warga-kelurahan').value = data?.kelurahan || ''; document.getElementById('warga-kecamatan').value = data?.kecamatan || ''; document.getElementById('warga-kota').value = data?.kota || ''; // Set manual kategori if active if (data && parseInt(data.is_kategori_manual) === 1) { document.getElementById('warga-kategori').value = data.kategori_kemiskinan || 'miskin'; } else { document.getElementById('warga-kategori').value = 'sistem'; } document.getElementById('warga-stsbantuan').value = data?.status_bantuan || 'belum_terima'; // Set manual recommendation if active if (data && parseInt(data.is_rekomendasi_manual) === 1) { document.getElementById('warga-rekomendasi').value = data.rekomendasi_bantuan || 'sistem'; } else { document.getElementById('warga-rekomendasi').value = 'sistem'; } document.getElementById('warga-luas').value = data?.luas_rumah || ''; document.getElementById('warga-lantai').value = data?.jenis_lantai || 'semen'; document.getElementById('warga-dinding').value = data?.jenis_dinding || 'tembok'; document.getElementById('warga-air').value = data?.sumber_air || 'sumur'; document.getElementById('warga-tempat').value = data?.status_tempat_tinggal || 'milik_sendiri'; document.getElementById('warga-ket').value = data?.keterangan || ''; const latVal = data ? data.latitude : (lat || ''); const lngVal = data ? data.longitude : (lng || ''); document.getElementById('warga-lat').value = latVal; document.getElementById('warga-lng').value = lngVal; updateCoordDisplay('warga', latVal, lngVal); // Populate dropdown Penanggung Bantuan const selectPj = document.getElementById('warga-dikelola-oleh'); if (selectPj) { let optionsHtml = ''; let closestIbadahId = null; let minDistance = Infinity; if (typeof ibadahData !== 'undefined' && Array.isArray(ibadahData)) { ibadahData.forEach(ib => { optionsHtml += ``; if (!data && latVal && lngVal && typeof haversine === 'function') { const dist = haversine(parseFloat(latVal), parseFloat(lngVal), parseFloat(ib.latitude), parseFloat(ib.longitude)); if (dist < minDistance) { minDistance = dist; closestIbadahId = ib.id; } } }); } selectPj.innerHTML = optionsHtml; if (data && data.dikelola_oleh_ibadah_id) { selectPj.value = data.dikelola_oleh_ibadah_id; } else if (!data && closestIbadahId) { selectPj.value = closestIbadahId; } else { selectPj.value = ''; } } if (latVal && lngVal) reverseGeocode(latVal, lngVal, (addr, details) => { const h = document.getElementById('warga-addr-hint'); if (h) h.textContent = addr; if (!data) { if (details && Object.keys(details).length > 0) { const kel = details.village || details.suburb || details.neighbourhood || ''; const kec = details.county || details.city_district || details.town || ''; const kot = details.city || details.municipality || details.regency || details.county || ''; if (kel && !document.getElementById('warga-kelurahan').value) document.getElementById('warga-kelurahan').value = kel; if (kec && !document.getElementById('warga-kecamatan').value) document.getElementById('warga-kecamatan').value = kec; if (kot && !document.getElementById('warga-kota').value) document.getElementById('warga-kota').value = kot; } else { if (h) h.innerHTML = addr + ' (Alamat detail tidak didapatkan, silahkan isi manual)'; } } }); // Load anggota existing anggotaRows = []; const container = document.getElementById('anggota-form-container'); container.innerHTML = ''; if (data?.id && !isKoor) { try { const res = await fetch(`api/anggota_keluarga.php?rumah_warga_id=${data.id}`); const json = await res.json(); if (json.data?.length) { json.data.forEach((a, i) => { anggotaRows.push(i); container.insertAdjacentHTML('beforeend', buildAnggotaRow(a, i)); }); } } catch { /* silent */ } } previewRek(); // Adjust footer buttons based on mode (Add vs Edit) const btnVerifyDirect = document.getElementById('btn-verify-warga-direct'); const btnSave = document.getElementById('btn-save-warga'); if (data?.id && !isKoor) { if (btnVerifyDirect) btnVerifyDirect.style.display = 'inline-flex'; if (btnSave) btnSave.innerHTML = ' Simpan & Verifikasi'; } else { if (btnVerifyDirect) btnVerifyDirect.style.display = 'none'; if (btnSave) { if (isKoor) { btnSave.innerHTML = 'Simpan'; } else { btnSave.innerHTML = 'Simpan'; } } } // Buka modal setelah semua data terisi modalEl.classList.add('show'); if (document.getElementById('modal-premium-verifikasi')?.classList.contains('show')) { document.body.classList.add('side-by-side-modals'); } } async function saveWarga() { const isKoor = currentUser && currentUser.role === 'koordinator'; const id = document.getElementById('warga-id').value; const nokk = document.getElementById('warga-nokk').value.trim(); const nama = document.getElementById('warga-nama').value.trim(); const lat = document.getElementById('warga-lat').value; const lng = document.getElementById('warga-lng').value; if (!isKoor) { if (!nama) { showToast('Nama Kepala Keluarga wajib diisi', 'error'); return; } if (!lat || !lng) { showToast('Pilih lokasi di peta terlebih dahulu', 'error'); return; } if (!nokk || !/^\d{16}$/.test(nokk)) { showToast('No. KK harus 16 digit angka', 'error'); return; } } else { if (!id) { showToast('ID Warga wajib terisi', 'error'); return; } } const payload = isKoor ? { id, status_bantuan: document.getElementById('warga-stsbantuan').value } : { id, no_kk: nokk, nama_kepala_keluarga: nama, nik: document.getElementById('warga-nik').value, jumlah_tanggungan: document.getElementById('warga-tanggungan').value, penghasilan_bulanan: document.getElementById('warga-penghasilan').value, pekerjaan: document.getElementById('warga-pekerjaan').value, status_pekerjaan: document.getElementById('warga-stspekerjaan').value, alamat: document.getElementById('warga-alamat').value, kelurahan: document.getElementById('warga-kelurahan').value, kecamatan: document.getElementById('warga-kecamatan').value, kota: document.getElementById('warga-kota').value, kategori_kemiskinan: document.getElementById('warga-kategori').value, status_bantuan: document.getElementById('warga-stsbantuan').value, rekomendasi_bantuan: document.getElementById('warga-rekomendasi').value, luas_rumah: document.getElementById('warga-luas').value, jenis_lantai: document.getElementById('warga-lantai').value, jenis_dinding: document.getElementById('warga-dinding').value, sumber_air: document.getElementById('warga-air').value, status_tempat_tinggal: document.getElementById('warga-tempat').value, keterangan: document.getElementById('warga-ket').value, dikelola_oleh_ibadah_id: document.getElementById('warga-dikelola-oleh') ? document.getElementById('warga-dikelola-oleh').value : '', latitude: lat, longitude: lng, anggota: collectAnggotaRows(), }; try { const res = await fetch('api/rumah_warga.php', { method: id ? 'PUT' : 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const json = await res.json(); if (json.success) { const rekLabel = json.rekomendasi ? (REK_CONFIG[json.rekomendasi]?.label || json.rekomendasi) : ''; // Auto-verify pasca-simpan (baru maupun edit) const targetId = id || json.id; if (targetId) { try { await fetch('api/verifikasi.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ warga_id: parseInt(targetId), petugas: currentUser ? (currentUser.role === 'koordinator' ? `Koordinator (${currentUser.username})` : currentUser.username) : 'Sistem', tanggal: new Date().toLocaleDateString('en-CA'), catatan: id ? 'Verifikasi otomatis pasca-edit data warga' : 'Verifikasi otomatis pendaftaran warga baru' }) }); } catch (e) { console.error('Auto-verify error:', e); } } showToast(`${json.message}${rekLabel ? ' · Rek: ' + rekLabel : ''}`, 'success'); try { closeModal('modal-warga'); } catch (e) { } try { cancelAddMode(); } catch (e) { } try { await loadWarga(); } catch (e) { } try { loadStats(); } catch (e) { } try { refreshRadiusIfActive(); } catch (e) { } if (window.FiturBaru && FiturBaru.Verifikasi) { try { FiturBaru.Verifikasi.load(document.getElementById('filter-verif-status')?.value || ''); } catch (e) { } } } else { showToast(json.message, 'error'); } } catch (err) { console.error('SaveWarga Error:', err); showToast('Terjadi kesalahan', 'error'); } } /* ══════════════════════════════════════════════════ EDIT / DELETE ══════════════════════════════════════════════════ */ function editIbadah(id) { checkAuth({ type: 'edit_ibadah', id }, () => { if (currentUser.role !== 'admin') { showToast('Hanya Admin yang berhak merubah data rumah ibadah', 'error'); return; } const d = ibadahData.find(x => parseInt(x.id) === parseInt(id)); if (d) { document.getElementById('info-panel').classList.remove('show'); openIbadahModal(d); } }); } async function editWarga(id) { checkAuth({ type: 'edit_warga', id }, async () => { if (!['admin', 'petugas', 'koordinator'].includes(currentUser.role)) { showToast('Anda tidak memiliki izin untuk mengedit data warga', 'error'); return; } const d = wargaData.find(x => parseInt(x.id) === parseInt(id)); if (d) { document.getElementById('info-panel').classList.remove('show'); await openWargaModal(d); } }); } async function deleteIbadah(id) { checkAuth({ type: 'delete_ibadah', id }, async () => { if (currentUser.role !== 'admin') { showToast('Hanya Admin yang berhak menghapus data rumah ibadah', 'error'); return; } if (!confirm('Hapus data rumah ibadah ini?')) return; try { const res = await fetch(`api/rumah_ibadah.php?id=${id}`, { method: 'DELETE' }); const json = await res.json(); if (json.success) { showToast(json.message, 'success'); document.getElementById('info-panel').classList.remove('show'); if (parseInt(activeRadiusIbadahId) === parseInt(id)) clearRadius(); await loadIbadah(); loadStats(); } else showToast(json.message, 'error'); } catch { showToast('Terjadi kesalahan', 'error'); } }); } async function deleteWarga(id) { checkAuth({ type: 'delete_warga', id }, async () => { if (currentUser.role !== 'admin') { showToast('Hanya Admin yang berhak menghapus data warga', 'error'); return; } if (!confirm('Hapus data rumah warga ini?')) return; try { const res = await fetch(`api/rumah_warga.php?id=${id}`, { method: 'DELETE' }); const json = await res.json(); if (json.success) { showToast(json.message, 'success'); document.getElementById('info-panel').classList.remove('show'); await loadWarga(); loadStats(); refreshRadiusIfActive(); } else showToast(json.message, 'error'); } catch { showToast('Terjadi kesalahan', 'error'); } }); } /* ══════════════════════════════════════════════════ DATA LISTS ══════════════════════════════════════════════════ */ function renderIbadahList() { const search = document.getElementById('search-ibadah')?.value || ''; const jenis = document.getElementById('filter-ibadah-jenis')?.value || ''; const container = document.getElementById('ibadah-list'); if (!container) return; const filtered = ibadahData.filter(d => { const matchSearch = !search || d.nama.toLowerCase().includes(search.toLowerCase()); const matchJenis = !jenis || d.jenis === jenis; return matchSearch && matchJenis; }); const isKoor = currentUser?.role === 'koordinator'; let html = ''; if (isKoor && currentUser.rumah_ibadah_id) { const myIbadah = ibadahData.find(x => parseInt(x.id) === parseInt(currentUser.rumah_ibadah_id)); if (myIbadah) { const cfg = JENIS_CONFIG[myIbadah.jenis] || JENIS_CONFIG.lainnya; html += `
Rumah Ibadah Saya
${svgIcon(cfg.svgKey, 13, 13)}
${myIbadah.nama}
${myIbadah.kelurahan || '–'} ${myIbadah.kecamatan ? '· ' + myIbadah.kecamatan : ''}
${cfg.label}
Semua Rumah Ibadah
`; } } if (!filtered.length) { container.innerHTML = html + `
${svgIcon('building', 26, 26)}
Tidak ada data
`; return; } const listHTML = filtered.map(d => { const cfg = JENIS_CONFIG[d.jenis] || JENIS_CONFIG.lainnya; const isMyIbadah = isKoor && parseInt(d.id) === parseInt(currentUser.rumah_ibadah_id); const styleAttr = isMyIbadah ? ` style="border:1.5px solid #6366f1;background:rgba(99,102,241,0.06)"` : ''; return `
${svgIcon(cfg.svgKey, 13, 13)}
${d.nama}
${d.kelurahan || '–'} ${d.kecamatan ? '· ' + d.kecamatan : ''}
${cfg.label}
`; }).join(''); container.innerHTML = html + listHTML; } function renderWargaList() { const search = document.getElementById('search-warga')?.value || ''; const kategori = document.getElementById('filter-warga-kategori')?.value || ''; const status = document.getElementById('filter-warga-status')?.value || ''; const rekomendasi = document.getElementById('filter-warga-rekomendasi')?.value || ''; const container = document.getElementById('warga-list'); if (!container) return; const visibleWarga = getWargaForRole(); const filtered = visibleWarga.filter(d => { const matchSearch = !search || d.nama_kepala_keluarga.toLowerCase().includes(search.toLowerCase()) || (d.no_kk || '').includes(search) || (d.nik || '').includes(search); const matchKat = !kategori || (d.kategori_kemiskinan && d.kategori_kemiskinan === kategori); const matchSts = !status || (d.status_bantuan && d.status_bantuan === status); const matchRek = !rekomendasi || (d.rekomendasi_bantuan && d.rekomendasi_bantuan === rekomendasi); return matchSearch && matchKat && matchSts && matchRek; }); if (!filtered.length) { container.innerHTML = `
${svgIcon('home', 26, 26)}
Tidak ada data
`; return; } container.innerHTML = filtered.map(d => { const rek = REK_CONFIG[d.rekomendasi_bantuan] || REK_CONFIG.tidak_ada; const cat = d.kategori_kemiskinan ? (KATEGORI_CONFIG[d.kategori_kemiskinan] || KATEGORI_CONFIG.miskin) : null; const sts = STATUS_CONFIG[d.status_bantuan] || STATUS_CONFIG.belum_terima; const subInfo = cat ? `${cat.label}${d.jumlah_tanggungan != null ? ' · ' + d.jumlah_tanggungan + ' jiwa' : ''}` : `${sts.label}`; return `
${svgIcon('home', 13, 13)}
${d.nama_kepala_keluarga}
${subInfo}
${rek.label}
`; }).join(''); } function flyToIbadah(id) { const d = ibadahData.find(x => parseInt(x.id) === parseInt(id)); if (!d) return; panToPoint(d.latitude, d.longitude, 16); setTimeout(() => { const m = ibadahMarkers[id]; if (m) { drawRadius(d, m); showInfoIbadah(d); } }, 600); } function flyToWarga(id) { const d = wargaData.find(x => parseInt(x.id) === parseInt(id)); if (!d) return; panToPoint(d.latitude, d.longitude, 17); setTimeout(() => showInfoWarga(d), 600); } /* ══════════════════════════════════════════════════ MODAL HELPERS ══════════════════════════════════════════════════ */ function closeModal(id) { document.getElementById(id).classList.remove('show'); if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } if (addMode) cancelAddMode(); if (id === 'modal-warga') { document.body.classList.remove('side-by-side-modals'); } } /* ══════════════════════════════════════════════════ TOAST ══════════════════════════════════════════════════ */ function showToast(msg, type = 'info', duration = 3500) { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: svgIcon('check', 14, 14), error: svgIcon('x', 14, 14), info: svgIcon('info', 14, 14) }; toast.innerHTML = `${icons[type] || icons.info}${msg}`; container.appendChild(toast); setTimeout(() => { toast.style.cssText += ';opacity:0;transform:translateX(16px);transition:all 0.25s'; setTimeout(() => toast.remove(), 250); }, duration); } /* ══════════════════════════════════════════════════ TABS ══════════════════════════════════════════════════ */ function initTabs() { // Sidebar tabs document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); btn.classList.add('active'); document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); if (btn.dataset.tab === 'laporan' && window.FiturBaru && FiturBaru.Laporan) { FiturBaru.Laporan.loadList(); } }); }); // Modal sub-tabs — scope ke dalam masing-masing modal document.querySelectorAll('.modal-overlay').forEach(modalOverlay => { modalOverlay.querySelectorAll('.modal-tab-btn').forEach(btn => { btn.addEventListener('click', () => { modalOverlay.querySelectorAll('.modal-tab-btn').forEach(b => b.classList.remove('active')); modalOverlay.querySelectorAll('.modal-tab-panel').forEach(p => p.classList.remove('active')); btn.classList.add('active'); const panelId = 'mtab-' + btn.dataset.mtab; const panel = document.getElementById(panelId); if (panel) panel.classList.add('active'); }); }); }); } /* ══════════════════════════════════════════════════ SEARCH & FILTER ══════════════════════════════════════════════════ */ function initSearchFilter() { document.getElementById('search-ibadah')?.addEventListener('input', renderIbadahList); document.getElementById('filter-ibadah-jenis')?.addEventListener('change', renderIbadahList); document.getElementById('filter-ibadah-reset')?.addEventListener('click', () => { document.getElementById('search-ibadah').value = ''; document.getElementById('filter-ibadah-jenis').value = ''; renderIbadahList(); }); document.getElementById('search-warga')?.addEventListener('input', renderWargaList); document.getElementById('filter-warga-kategori')?.addEventListener('change', renderWargaList); document.getElementById('filter-warga-status')?.addEventListener('change', renderWargaList); document.getElementById('filter-warga-rekomendasi')?.addEventListener('change', renderWargaList); document.getElementById('filter-warga-reset')?.addEventListener('click', () => { document.getElementById('search-warga').value = ''; document.getElementById('filter-warga-kategori').value = ''; document.getElementById('filter-warga-status').value = ''; document.getElementById('filter-warga-rekomendasi').value = ''; renderWargaList(); }); document.getElementById('warga-penghasilan')?.addEventListener('input', previewRek); document.getElementById('warga-stspekerjaan')?.addEventListener('change', previewRek); } /* ══════════════════════════════════════════════════ ANGGOTA ROWS ══════════════════════════════════════════════════ */ function addAnggotaRow() { const container = document.getElementById('anggota-form-container'); const idx = Date.now(); anggotaRows.push(idx); container.insertAdjacentHTML('beforeend', buildAnggotaRow({}, idx)); previewRek(); } function removeAnggotaRow(idx) { const el = document.getElementById(`arow-${idx}`); if (el) el.remove(); anggotaRows = anggotaRows.filter(r => r !== idx); previewRek(); } async function verifyWargaNoChanges() { const wargaId = document.getElementById('warga-id').value; if (!wargaId) return; try { const today = new Date().toLocaleDateString('en-CA'); const res = await fetch('api/verifikasi.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ warga_id: parseInt(wargaId), petugas: 'Admin', tanggal: today, catatan: 'Verifikasi langsung (data sesuai, tidak ada perubahan)' }) }); const json = await res.json(); if (json.success) { showToast('Verifikasi berhasil dicatat (Data Sesuai)', 'success'); closeModal('modal-warga'); await loadWarga(); loadStats(); if (window.FiturBaru && FiturBaru.Verifikasi) { FiturBaru.Verifikasi.load(document.getElementById('filter-verif-status')?.value || ''); } } else { showToast(json.message || 'Gagal verifikasi', 'error'); } } catch (e) { showToast('Gagal memproses verifikasi', 'error'); } } /* ══════════════════════════════════════════════════ PUBLIC API ══════════════════════════════════════════════════ */ return { init() { initMap(); initTabs(); initRadiusControl(); initSearchFilter(); checkAuthStatus(); // Bind login submit button document.getElementById('btn-login-submit')?.addEventListener('click', handleLogin); // Support enter key on login modal document.getElementById('login-password')?.addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); }); // Bind login/logout toggle button document.getElementById('btn-auth-toggle')?.addEventListener('click', () => { if (currentUser) { handleLogout(); } else { showAuthModal({ type: 'login' }); } }); // Bind logout confirmation button document.getElementById('btn-confirm-logout')?.addEventListener('click', doLogout); // Tombol tambah document.getElementById('btn-add-ibadah')?.addEventListener('click', () => { checkAuth({ type: 'add_ibadah' }, () => { setAddMode('ibadah'); showToast('Klik peta untuk menentukan lokasi Rumah Ibadah', 'info'); }); }); document.getElementById('btn-add-warga')?.addEventListener('click', () => { checkAuth({ type: 'add_warga' }, () => { setAddMode('warga'); showToast('Klik peta untuk menentukan lokasi Rumah Warga', 'info'); }); }); document.getElementById('btn-add-anggota-row')?.addEventListener('click', addAnggotaRow); document.getElementById('btn-save-ibadah')?.addEventListener('click', saveIbadah); document.getElementById('btn-save-warga')?.addEventListener('click', saveWarga); document.getElementById('tool-fit')?.addEventListener('click', () => { const all = [...ibadahData, ...wargaData]; if (!all.length) return; map.fitBounds(L.latLngBounds(all.map(d => [d.latitude, d.longitude])), { padding: [40, 40] }); }); document.getElementById('tool-clear-radius')?.addEventListener('click', clearRadius); document.getElementById('info-close')?.addEventListener('click', () => document.getElementById('info-panel').classList.remove('show')); document.getElementById('radius-info-close')?.addEventListener('click', clearRadius); document.querySelectorAll('[data-modal-close]').forEach(btn => { btn.addEventListener('click', () => closeModal(btn.dataset.modalClose)); }); document.addEventListener('keydown', e => { if (e.key === 'Escape') { cancelAddMode(); document.getElementById('info-panel').classList.remove('show'); document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('show')); } }); }, // Exposed untuk onclick di HTML editIbadah, deleteIbadah, editWarga, deleteWarga, flyToIbadah, flyToWarga, removeAnggotaRow, previewRek, verifyWargaNoChanges, // Exposed helper functions getCurrentUser() { return currentUser; }, checkAuth, showAuthModal, doLogout, handleAuthToggle() { if (currentUser) { handleLogout(); } else { showAuthModal({ type: 'login' }); } }, getIbadahData() { return ibadahData; }, getWargaData() { return wargaData; }, loadWarga, haversine }; })(); // Expose App globally window.App = App; document.addEventListener('DOMContentLoaded', () => { App.init(); // Mobile menu toggle const mobileMenuBtn = document.getElementById('mobile-menu-btn'); const sidebar = document.getElementById('sidebar'); let overlay = document.querySelector('.sidebar-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.className = 'sidebar-overlay'; document.body.appendChild(overlay); } if (mobileMenuBtn && sidebar) { mobileMenuBtn.addEventListener('click', () => { sidebar.classList.toggle('show'); overlay.classList.toggle('show'); }); overlay.addEventListener('click', () => { sidebar.classList.remove('show'); overlay.classList.remove('show'); }); // Close sidebar on tab click in mobile document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { if (window.innerWidth <= 768) { sidebar.classList.remove('show'); overlay.classList.remove('show'); } }); }); } });