Files
D1041231027-WebGIS/03/assets/js/app.js
T
2026-06-10 18:54:25 +07:00

2061 lines
102 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = '<option value="">-- Pilih Rumah Ibadah --</option>';
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: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22V11c0-3.31 3.58-6 8-6s8 2.69 8 6v11"/><path d="M2 22h20"/><path d="M12 5V3"/><path d="M8 22v-4a4 4 0 0 1 8 0v4"/></svg>`,
church: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M18 22V11l-6-6-6 6v11"/><path d="M2 22h20"/><line x1="12" y1="5" x2="12" y2="2"/><line x1="10" y1="3.5" x2="14" y2="3.5"/></svg>`,
temple: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 22h18"/><path d="M6 18V9l6-6 6 6v9"/><path d="M4 14h16"/><path d="M4 18h16"/></svg>`,
star: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>`,
wheel: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="3"/><line x1="12" y1="2" x2="12" y2="9"/><line x1="12" y1="15" x2="12" y2="22"/><line x1="2" y1="12" x2="9" y2="12"/><line x1="15" y1="12" x2="22" y2="12"/></svg>`,
building: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M9 22V12h6v10"/></svg>`,
home: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`,
users: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
pin: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 10c0 7-9 13-9 13S3 17 3 10a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
check: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>`,
x: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
info: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`,
edit: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`,
trash: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`,
gift: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><path d="M12 22V7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>`,
};
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('<svg', `<svg width="${w}" height="${h}"`) || '';
}
/* ══════════════════════════════════════════════════
INIT MAP
══════════════════════════════════════════════════ */
function initMap() {
map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);
ibadahLayerGroup = L.layerGroup().addTo(map);
wargaLayerGroup = L.layerGroup().addTo(map);
map.on('click', onMapClick);
loadAllData();
}
/* ══════════════════════════════════════════════════
LOAD DATA
══════════════════════════════════════════════════ */
async function loadAllData() {
const role = currentUser ? currentUser.role : 'guest';
const promises = [loadIbadah()];
if (['admin', 'petugas', 'koordinator', 'kontributor', 'guest'].includes(role)) {
promises.push(loadWarga());
} else {
wargaData = [];
renderWargaMarkers();
renderWargaList();
}
try {
await Promise.all(promises);
loadStats();
// Terbang otomatis ke rumah ibadah koordinator saat login/muat data
if (currentUser?.role === 'koordinator' && currentUser.rumah_ibadah_id) {
flyToIbadah(currentUser.rumah_ibadah_id);
}
} catch (e) {
console.error('Error in loadAllData:', e);
}
}
async function loadIbadah() {
try {
const res = await fetch('api/rumah_ibadah.php');
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch (e) { throw new Error('Invalid JSON response'); }
if (!res.ok || !json.success) {
throw new Error(json.message || 'HTTP ' + res.status);
}
ibadahData = json.data || [];
renderIbadahMarkers();
renderIbadahList();
} catch (err) {
console.error('loadIbadah error:', err);
showToast(err.message || 'Gagal memuat data rumah ibadah', 'error');
}
}
async function loadWarga() {
try {
const res = await fetch('api/rumah_warga.php');
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch (e) { throw new Error('Invalid JSON response'); }
if (!res.ok || !json.success) {
throw new Error(json.message || 'HTTP ' + res.status);
}
wargaData = json.data || [];
renderWargaMarkers();
renderWargaList();
} catch (err) {
console.error('loadWarga error:', err);
showToast(err.message || 'Gagal memuat data rumah warga', 'error');
}
}
async function loadStats() {
try {
const res = await fetch('api/statistik.php');
const text = await res.text();
let json;
try { json = JSON.parse(text); } catch (e) { throw new Error('Invalid JSON response'); }
if (!res.ok || !json.success) {
showToast(json.message || 'Gagal memuat statistik', 'error');
return;
}
const d = json.data;
const set = (id, val) => { 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: `<div style="width:38px;height:38px;border-radius:10px;background:#fff;border:1.5px solid #e2e8f0;display:flex;align-items:center;justify-content:center;color:#64748b;box-shadow:0 2px 8px rgba(0,0,0,0.12);cursor:pointer;">${svgIcon(cfg.svgKey, 18, 18)}</div>`,
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 ? `<div style="position:absolute;bottom:0;left:50%;transform:translate(-50%, 50%);width:14px;height:14px;border-radius:50%;border:1.5px dashed ${markerColor};opacity:0.5;animation:pulse-ring 2.5s infinite;"></div>` : '';
htmlContent = `
<div style="position:relative; width:28px; height:28px; display:flex; justify-content:center; align-items:flex-end; cursor:pointer;">
${pulse}
<svg viewBox="0 0 24 24" width="28" height="28" fill="${markerColor}" stroke="#ffffff" stroke-width="1.5" style="filter: drop-shadow(0px 3px 4px rgba(0,0,0,0.3)); z-index:2; position:relative;">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3.5" fill="#ffffff" stroke="none"></circle>
</svg>
</div>
`;
} else {
const size = 13;
const shadow = '0 2px 6px rgba(0,0,0,0.2)';
const pulse = isKontributor ? `<div style="position:absolute;top:-3px;left:-3px;width:${size + 6}px;height:${size + 6}px;border-radius:50%;border:1.5px dashed ${markerColor};opacity:0.45;animation:pulse-ring 2.5s ease-out infinite;"></div>` : '';
htmlContent = `
<div style="position:relative;width:${size}px;height:${size}px;">
${pulse}
<div style="width:${size}px;height:${size}px;border-radius:50%;background:${markerColor};border:2px solid ${markerColor};box-shadow:${shadow};cursor:pointer;transition:all 0.25s;"></div>
</div>
`;
}
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 = `<div class="info-title-icon">${svgIcon(cfg.svgKey, 14, 14)}</div><span>${d.nama}</span>`;
body.innerHTML = `
<div class="address-box"><span class="addr-icon">${svgIcon('pin', 11, 11)}</span><span id="info-address">Memuat alamat...</span></div>
<div class="info-row"><span class="info-key">Jenis</span><span class="info-val">${cfg.label}</span></div>
${d.penanggung_jawab ? `<div class="info-row"><span class="info-key">Penanggung Jwb</span><span class="info-val">${d.penanggung_jawab}</span></div>` : ''}
${d.kontak ? `<div class="info-row"><span class="info-key">Kontak</span><span class="info-val">${d.kontak}</span></div>` : ''}
${d.kelurahan ? `<div class="info-row"><span class="info-key">Kelurahan</span><span class="info-val">${d.kelurahan}</span></div>` : ''}
${d.kecamatan ? `<div class="info-row"><span class="info-key">Kecamatan</span><span class="info-val">${d.kecamatan}</span></div>` : ''}
${d.kota ? `<div class="info-row"><span class="info-key">Kota</span><span class="info-val">${d.kota}</span></div>` : ''}
${d.keterangan ? `<div class="info-row"><span class="info-key">Ket.</span><span class="info-val">${d.keterangan}</span></div>` : ''}
<div class="info-coords">${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}</div>
${(currentUser && currentUser.role === 'admin') ? `
<div style="display:flex;gap:6px;margin-top:10px">
<button class="btn btn-primary btn-sm" style="flex:1" onclick="App.editIbadah(${d.id})">${svgIcon('edit', 12, 12)} Edit</button>
<button class="btn btn-danger btn-sm" onclick="App.deleteIbadah(${d.id})">${svgIcon('trash', 12, 12)}</button>
</div>` : ''}`;
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 = `<div class="info-title-icon">${svgIcon('home', 14, 14)}</div><span>${title}</span>`;
let htmlContent = `
<div class="address-box"><span class="addr-icon">${svgIcon('pin', 11, 11)}</span><span id="info-address">Memuat alamat...</span></div>
<div class="info-row"><span class="info-key">Kategori</span><span class="info-val"><span class="badge ${cat ? cat.badgeCls : 'badge-miskin'}">${cat ? cat.label : 'Kurang Mampu'}</span></span></div>
<div class="info-row"><span class="info-key">Status Bansos</span><span class="info-val"><span class="badge ${sts.cls}">${sts.label}</span></span></div>
<div class="info-coords">${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}</div>
<div style="margin-top: 15px; padding: 12px; border-radius: 8px; background: rgba(99, 102, 241, 0.08); border: 1px solid rgba(99, 102, 241, 0.15); display: flex; flex-direction: column; gap: 6px;">
<div style="display: flex; align-items: center; gap: 8px; font-weight: 600; color: #4f46e5; font-size: 12px;">
<span>🔒</span> Privasi Terjaga
</div>
<div style="font-size: 11px; color: #4b5563; line-height: 1.4;">
Detail informasi pribadi warga disembunyikan untuk menjaga privasi penerima manfaat.
</div>
</div>
`;
if (isKontributor) {
htmlContent += `
<button class="btn-laporkan" onclick="FiturBaru.Laporan.bukaModalLaporan(${d.id}, '${d.nama_kepala_keluarga ? d.nama_kepala_keluarga.replace(/'/g, "\\'") : 'Penerima Manfaat'}')">
<svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
Laporkan Data Tidak Sesuai
</button>
`;
}
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 = `<div class="info-title-icon">${svgIcon('home', 14, 14)}</div><span>${d.nama_kepala_keluarga}</span>`;
let htmlContent = `
<div class="address-box"><span class="addr-icon">${svgIcon('pin', 11, 11)}</span><span id="info-address">Memuat alamat...</span></div>
`;
if (!isKoor) {
htmlContent += `
${d.no_kk ? `<div class="info-row"><span class="info-key">No. KK</span><span class="info-val" style="font-family:var(--mono);font-size:10px">${d.no_kk}</span></div>` : ''}
${d.nik ? `<div class="info-row"><span class="info-key">NIK</span><span class="info-val" style="font-family:var(--mono);font-size:10px">${d.nik}</span></div>` : ''}
<div class="info-row"><span class="info-key">Tanggungan</span><span class="info-val">${d.jumlah_tanggungan} jiwa</span></div>
<div class="info-row"><span class="info-key">Penghasilan</span><span class="info-val">${formatRupiah(d.penghasilan_bulanan)}/bln</span></div>
${d.pekerjaan ? `<div class="info-row"><span class="info-key">Pekerjaan</span><span class="info-val">${d.pekerjaan}</span></div>` : ''}
<div class="info-row"><span class="info-key">Kategori</span><span class="info-val"><span class="badge ${cat.badgeCls}">${cat.label}</span></span></div>
`;
}
htmlContent += `
<div class="info-row"><span class="info-key">Bansos</span><span class="info-val"><span class="badge ${sts.cls}">${sts.label}</span></span></div>
<div class="info-row"><span class="info-key">Rekomendasi</span><span class="info-val"><span class="badge ${rek.cls}">${rek.label}</span></span></div>
${d.dikelola_oleh_ibadah_id ? (() => {
const ibadah = ibadahData.find(i => parseInt(i.id) === parseInt(d.dikelola_oleh_ibadah_id));
return ibadah ? `<div style="margin-top:8px;padding:10px;background:var(--bg-tertiary);border-radius:6px;border:1px solid var(--border);"><div style="font-size:12px;color:var(--text-muted);margin-bottom:4px">Penanggung Bantuan:</div><div style="font-weight:bold;font-size:12px;color:var(--primary);display:flex;align-items:center;gap:4px">${ibadah.nama}</div></div>` : '';
})() : ''}
${!isKoor ? `<div class="rek-box ${rek.boxCls}"><div class="rek-box-title">${rek.label}</div><div class="rek-box-desc">${rek.desc}</div></div>` : ''}
${d.kelurahan ? `<div class="info-row"><span class="info-key">Kelurahan</span><span class="info-val">${d.kelurahan}</span></div>` : ''}
${d.kecamatan ? `<div class="info-row"><span class="info-key">Kecamatan</span><span class="info-val">${d.kecamatan}</span></div>` : ''}
${d.kota ? `<div class="info-row"><span class="info-key">Kota</span><span class="info-val">${d.kota}</span></div>` : ''}
${!isKoor && d.keterangan ? `<div class="info-row"><span class="info-key">Ket.</span><span class="info-val">${d.keterangan}</span></div>` : ''}
<div class="info-coords">${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}</div>
`;
if (!isKoor) {
htmlContent += `
<div class="info-section-title">${svgIcon('users', 11, 11)} Anggota Keluarga</div>
<div id="anggota-info-list"><div class="loading"><div class="spinner"></div></div></div>
`;
}
if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'petugas' || currentUser.role === 'koordinator')) {
htmlContent += `
<div style="display:flex;gap:6px;margin-top:10px">
<button class="btn btn-primary btn-sm" style="flex:1" onclick="App.editWarga(${d.id})">${svgIcon('edit', 12, 12)} Edit</button>
${currentUser.role === 'admin' ? `<button class="btn btn-danger btn-sm" onclick="App.deleteWarga(${d.id})">${svgIcon('trash', 12, 12)}</button>` : ''}
</div>
`;
}
if (currentUser && (currentUser.role === 'kontributor' || currentUser.role === 'koordinator')) {
htmlContent += `
<button class="btn-laporkan" onclick="FiturBaru.Laporan.bukaModalLaporan(${d.id}, '${d.nama_kepala_keluarga ? d.nama_kepala_keluarga.replace(/'/g, "\\'") : 'Penerima Manfaat'}')">
<svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
Laporkan Data Tidak Sesuai
</button>
`;
}
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 = `<div style="text-align:center;color:var(--text-muted);font-size:11px;padding:10px">Belum ada data anggota</div>`; return; }
const PEND = { tidak_sekolah: 'Tdk Sekolah', sd: 'SD', smp: 'SMP', sma: 'SMA', d3: 'D3', s1: 'S1', s2: 'S2', s3: 'S3' };
container.innerHTML = `<div class="anggota-list">${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 `<div class="anggota-item">
<div class="anggota-dot" style="background:${dotColor}"></div>
<div class="anggota-info">
<div class="anggota-name">${a.nama} <span style="font-size:9px;color:var(--text-muted)">(${usia})</span></div>
<div class="anggota-meta">
${a.jenis_kelamin === 'laki_laki' ? '♂' : '♀'} · ${PEND[a.pendidikan] || a.pendidikan}
${a.pekerjaan ? ' · ' + a.pekerjaan : ''}
${a.kondisi_kesehatan !== 'sehat' ? ` · <span style="color:var(--warning)">${a.kondisi_kesehatan.replace(/_/g, ' ')}</span>` : ''}
${parseInt(a.dapat_bekerja) ? '' : ' · <span style="color:var(--danger)">Tdk bisa kerja</span>'}
</div>
</div>
</div>`;
}).join('')}</div>`;
} 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 = `<div class="mode-dot ibadah"></div> Klik peta untuk menentukan lokasi Rumah Ibadah`;
ind.classList.add('show');
map.getContainer().style.cursor = 'crosshair';
} else if (mode === 'warga') {
ind.innerHTML = `<div class="mode-dot warga"></div> 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: `<div style="width:38px;height:38px;border-radius:10px;background:#fff;border:1.5px dashed #6366f1;display:flex;align-items:center;justify-content:center;color:#818cf8;">${svgIcon('mosque', 18, 18)}</div>`, 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: `<div style="width:14px;height:14px;border-radius:50%;background:#6366f1;border:2px dashed #818cf8;opacity:0.85;"></div>`, 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 + ' <span style="color:#f59e0b">(Alamat detail tidak didapatkan, silahkan isi manual)</span>';
}
}
});
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 `<div class="anggota-form-item" id="arow-${index}" data-arow="${index}">
<div class="anggota-form-header">
<div class="anggota-form-title">Anggota ${typeof index === 'number' ? index + 1 : ''}</div>
<button class="btn btn-xs btn-ghost" onclick="App.removeAnggotaRow(${index})" style="color:var(--danger)">Hapus</button>
</div>
<div class="anggota-form-body">
<div class="form-group" style="grid-column:1/-1">
<label class="form-label">Nama <span>*</span></label>
<input type="text" class="form-control" name="nama" value="${data.nama || ''}" placeholder="Nama lengkap" data-arow="${index}">
</div>
<div class="form-group">
<label class="form-label">NIK</label>
<input type="text" class="form-control" name="nik" value="${data.nik || ''}" maxlength="16" data-arow="${index}">
</div>
<div class="form-group">
<label class="form-label">Tgl Lahir</label>
<input type="date" class="form-control" name="tanggal_lahir" value="${data.tanggal_lahir || ''}" data-arow="${index}" oninput="App.previewRek()">
</div>
<div class="form-group">
<label class="form-label">Jenis Kelamin</label>
<select class="form-control" name="jenis_kelamin" data-arow="${index}">
<option value="laki_laki" ${(data.jenis_kelamin || 'laki_laki') === 'laki_laki' ? 'selected' : ''}>Laki-laki</option>
<option value="perempuan" ${data.jenis_kelamin === 'perempuan' ? 'selected' : ''}>Perempuan</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Pendidikan</label>
<select class="form-control" name="pendidikan" data-arow="${index}">
${['tidak_sekolah', 'sd', 'smp', 'sma', 'd3', 's1', 's2', 's3'].map(p => `<option value="${p}" ${(data.pendidikan || 'sd') === p ? 'selected' : ''}>${p.replace('_', ' ').toUpperCase()}</option>`).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">Pekerjaan</label>
<input type="text" class="form-control" name="pekerjaan" value="${data.pekerjaan || ''}" data-arow="${index}">
</div>
<div class="form-group">
<label class="form-label">Status Bekerja</label>
<select class="form-control" name="status_bekerja" data-arow="${index}" oninput="App.previewRek()">
<option value="bekerja" ${(data.status_bekerja || '') === 'bekerja' ? 'selected' : ''}>Bekerja</option>
<option value="tidak_bekerja" ${(data.status_bekerja || 'tidak_bekerja') === 'tidak_bekerja' ? 'selected' : ''}>Tidak Bekerja</option>
<option value="pelajar" ${(data.status_bekerja || '') === 'pelajar' ? 'selected' : ''}>Pelajar/Mahasiswa</option>
<option value="pensiunan" ${(data.status_bekerja || '') === 'pensiunan' ? 'selected' : ''}>Pensiunan</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Kondisi Kesehatan</label>
<select class="form-control" name="kondisi_kesehatan" data-arow="${index}" oninput="App.previewRek()">
<option value="sehat" ${(data.kondisi_kesehatan || 'sehat') === 'sehat' ? 'selected' : ''}>Sehat</option>
<option value="sakit_ringan" ${(data.kondisi_kesehatan || '') === 'sakit_ringan' ? 'selected' : ''}>Sakit Ringan</option>
<option value="sakit_berat" ${(data.kondisi_kesehatan || '') === 'sakit_berat' ? 'selected' : ''}>Sakit Berat</option>
<option value="disabilitas" ${(data.kondisi_kesehatan || '') === 'disabilitas' ? 'selected' : ''}>Disabilitas</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Dapat Bekerja</label>
<select class="form-control" name="dapat_bekerja" data-arow="${index}" oninput="App.previewRek()">
<option value="1" ${parseInt(data.dapat_bekerja ?? 1) === 1 ? 'selected' : ''}>Ya</option>
<option value="0" ${parseInt(data.dapat_bekerja ?? 1) === 0 ? 'selected' : ''}>Tidak</option>
</select>
</div>
</div>
</div>`;
}
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 = `<div style="font-weight:700;color:var(--text-primary);margin-bottom:2px">Prediksi Bantuan: <span class="badge ${cfg.cls}">${cfg.label}</span></div><div style="font-size:10px;color:var(--text-muted);margin-top:4px">${cfg.desc}</div>`;
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 = `<div style="font-weight:700;color:var(--text-primary);margin-bottom:6px">Prediksi Kategori: <span class="fb-badge ${katCls}">${katLabel}</span></div>` + 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 = '<option value="">-- Tidak Ada / Default Radius --</option>';
let closestIbadahId = null;
let minDistance = Infinity;
if (typeof ibadahData !== 'undefined' && Array.isArray(ibadahData)) {
ibadahData.forEach(ib => {
optionsHtml += `<option value="${ib.id}">${ib.nama}</option>`;
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 + ' <span style="color:#f59e0b">(Alamat detail tidak didapatkan, silahkan isi manual)</span>';
}
}
});
// 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 = '<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2" style="margin-right:4px"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg> Simpan &amp; 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 += `<div style="margin-bottom:12px">
<div style="font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;padding:0 2px">Rumah Ibadah Saya</div>
<div class="data-item" style="border:1.5px solid #6366f1;background:rgba(99,102,241,0.06)" onclick="App.flyToIbadah(${myIbadah.id})">
<div class="item-icon" style="color:#6366f1">${svgIcon(cfg.svgKey, 13, 13)}</div>
<div class="item-info">
<div class="item-name" style="font-weight:600">${myIbadah.nama}</div>
<div class="item-sub">${myIbadah.kelurahan || ''} ${myIbadah.kecamatan ? '· ' + myIbadah.kecamatan : ''}</div>
</div>
<span class="item-badge" style="background:rgba(99,102,241,0.15);color:#6366f1">${cfg.label}</span>
</div>
</div>
<div style="font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px;padding:0 2px">Semua Rumah Ibadah</div>`;
}
}
if (!filtered.length) {
container.innerHTML = html + `<div class="empty-state">${svgIcon('building', 26, 26)}<div>Tidak ada data</div></div>`;
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 `<div class="data-item"${styleAttr} onclick="App.flyToIbadah(${d.id})">
<div class="item-icon">${svgIcon(cfg.svgKey, 13, 13)}</div>
<div class="item-info">
<div class="item-name">${d.nama}</div>
<div class="item-sub">${d.kelurahan || ''} ${d.kecamatan ? '· ' + d.kecamatan : ''}</div>
</div>
<span class="item-badge" style="background:rgba(99,102,241,0.1);color:#6366f1">${cfg.label}</span>
</div>`;
}).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 = `<div class="empty-state">${svgIcon('home', 26, 26)}<div>Tidak ada data</div></div>`; 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' : ''}`
: `<span class="badge ${sts.cls}">${sts.label}</span>`;
return `<div class="data-item" onclick="App.flyToWarga(${d.id})">
<div class="item-icon">${svgIcon('home', 13, 13)}</div>
<div class="item-info">
<div class="item-name">${d.nama_kepala_keluarga}</div>
<div class="item-sub">${subInfo}</div>
</div>
<span class="item-badge ${rek.cls}">${rek.label}</span>
</div>`;
}).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 = `<span class="toast-icon">${icons[type] || icons.info}</span><span>${msg}</span>`;
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');
}
});
});
}
});