2061 lines
102 KiB
JavaScript
2061 lines
102 KiB
JavaScript
// 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 & 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');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}); |