// =================================================== // app.js - ARSITEKTUR WEBGIS V3 (SPBU + KEMISKINAN TERPADU) // =================================================== const state = { isMarkerMode: false, isRoadMode: false, isLandMode: false, isKemiskinanMode: false, newMarkerType: 'ibadah', isGhostLocked: false, tempRoadCoords: [], tempRoadLayer: null, tempLandCoords: [], tempLandLayer: null, filterSPBU: 'Semua', filterJalan: ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'], filterTanah: ['SHM', 'HGB', 'HGU', 'HP'], garisKemiskinan: 500000, // KUNCI AUTENTIKASI user: null, authMode: 'login' // 'login' atau 'register' }; const dataStore = { spbu: {}, jalan: {}, tanah: {}, ibadah: {}, penduduk: {}, statusDarurat: {}, pelatihan: [] }; const mapLayers = { spbu: {}, jalan: {}, tanah: {}, ibadah: {}, penduduk: {}, radiusIbadah: {} }; const savedLat = parseFloat(localStorage.getItem('webgis_map_lat')); const savedLng = parseFloat(localStorage.getItem('webgis_map_lng')); const savedZoom = parseInt(localStorage.getItem('webgis_map_zoom')); const startLat = !isNaN(savedLat) ? savedLat : -0.0227; const startLng = !isNaN(savedLng) ? savedLng : 109.3366; const startZoom = !isNaN(savedZoom) ? savedZoom : 15; const map = L.map('map', { zoomControl: false }).setView([startLat, startLng], startZoom); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OSM' }).addTo(map); L.control.zoom({ position: 'bottomright' }).addTo(map); function simpanPosisiPeta() { const center = map.getCenter(); localStorage.setItem('webgis_map_lat', center.lat); localStorage.setItem('webgis_map_lng', center.lng); localStorage.setItem('webgis_map_zoom', map.getZoom()); } map.on('moveend', simpanPosisiPeta); map.on('zoomend', simpanPosisiPeta); // ==================================================== // GHOST MARKER (EFEK KURSOR MENGIKUTI MOUSE) // ==================================================== let ghostMarker = null; map.on('mousemove', function(e) { if (state.isKemiskinanMode && !state.isGhostLocked) { let isIbadah = state.newMarkerType === 'ibadah'; let pinColor = isIbadah ? 'var(--gold)' : 'var(--navy)'; let iconHtml = isIbadah ? `
` : `
`; if (!ghostMarker) { ghostMarker = L.marker(e.latlng, { icon: L.divIcon({ className: 'custom-div-icon', html: iconHtml, iconSize: [30, 42], iconAnchor: [15, 42] }), interactive: false }).addTo(map); } else { ghostMarker.setLatLng(e.latlng); ghostMarker.setIcon(L.divIcon({ className: 'custom-div-icon', html: iconHtml, iconSize: [30, 42], iconAnchor: [15, 42] })); } } else if (!state.isKemiskinanMode) { if (ghostMarker) { map.removeLayer(ghostMarker); ghostMarker = null; } } }); // ==================================================== // CORE UTILITIES: GEOCODING, TOAST, FETCH // ==================================================== const safeFetch = async (url, payload = null) => { const options = payload ? { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload), credentials: 'include' } : { credentials: 'include' }; try { const res = await fetch(url, options); if (!res.ok) { const errData = await res.json().catch(() => ({})); throw new Error(errData.error || `HTTP Error: ${res.status}`); } return await res.json(); } catch(err) { throw err; } }; async function dapatkanAlamat(lat, lng) { try { const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`); if (!response.ok) throw new Error(); const data = await response.json(); return data.display_name || "Alamat tidak spesifik"; } catch (error) { return "Gagal melacak alamat dari satelit"; } } function triggerToastNotification(pesan) { const toast = document.getElementById('toast-notification'); toast.innerText = pesan; toast.classList.add('show'); setTimeout(() => { toast.classList.remove('show'); }, 3000); } // ==================================================== // AUTENTIKASI: GATEKEEPER & SINKRONISASI SESI // ==================================================== window.switchAuthTab = (mode) => { state.authMode = mode; document.getElementById('tab-login').classList.toggle('active', mode === 'login'); document.getElementById('tab-register').classList.toggle('active', mode === 'register'); document.getElementById('btn-submit-auth').innerText = mode === 'login' ? 'Login Sistem' : 'Daftar Akun Baru'; document.getElementById('auth-error-msg').innerText = ''; }; window.eksekusiAuth = () => { const user = document.getElementById('auth-username').value; const pass = document.getElementById('auth-password').value; const errMsg = document.getElementById('auth-error-msg'); if(!user || !pass) { errMsg.innerText = "Username dan Password tidak boleh kosong!"; return; } errMsg.innerText = "Memproses request ke server..."; const endpoint = state.authMode === 'login' ? 'otak_auth/api_login.php' : 'otak_auth/api_register.php'; safeFetch(endpoint, { username: user, password: pass }) .then(res => { if(state.authMode === 'register') { triggerToastNotification(res.pesan); switchAuthTab('login'); } else { state.user = res.user; document.getElementById('auth-screen').style.display = 'none'; triggerToastNotification(`Selamat datang, ${res.user}!`); muatDataSemua(); // Booting peta dilanjutkan } }) .catch(err => { errMsg.innerText = err.message; }); }; window.eksekusiLogout = () => { if(!confirm("Anda yakin ingin keluar dari sistem?")) return; safeFetch('otak_auth/api_logout.php') .then(res => { state.user = null; document.getElementById('auth-screen').style.display = 'flex'; document.getElementById('auth-username').value = ''; document.getElementById('auth-password').value = ''; triggerToastNotification("Sesi diakhiri secara aman."); // Bersihkan kanvas peta agar data sensitif tidak tertinggal Object.values(mapLayers.spbu).forEach(m => map.removeLayer(m)); Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m)); Object.values(mapLayers.tanah).forEach(m => map.removeLayer(m)); Object.values(mapLayers.penduduk).forEach(m => map.removeLayer(m)); Object.values(mapLayers.ibadah).forEach(m => map.removeLayer(m)); Object.values(mapLayers.radiusIbadah).forEach(m => map.removeLayer(m)); }) .catch(err => console.error(err)); }; const cekSesiBackend = () => { safeFetch('otak_auth/api_session.php') .then(res => { state.user = res.username; document.getElementById('auth-screen').style.display = 'none'; muatDataSemua(); }) .catch(err => { console.log("Tertahan di gerbang autentikasi: " + err.message); document.getElementById('auth-screen').style.display = 'flex'; }); }; // ==================================================== // MANAJEMEN UI PERSISTENT & ALAT PETA // ==================================================== window.toggleSidebar = () => { const s = document.getElementById('sidebar-container'); const b = document.getElementById('toggle-sidebar'); s.classList.toggle('collapsed'); const isCollapsed = s.classList.contains('collapsed'); b.innerHTML = isCollapsed ? '❯' : '❮'; localStorage.setItem('webgis_sidebar_collapsed', isCollapsed); setTimeout(() => map.invalidateSize(), 300); }; window.switchTab = (tabId) => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); const targetBtn = document.querySelector(`.tab-btn[onclick="switchTab('${tabId}')"]`); const targetContent = document.getElementById(tabId); if (targetBtn && targetContent) { targetBtn.classList.add('active'); targetContent.classList.add('active'); localStorage.setItem('webgis_active_tab', tabId); } }; function pulihkanIngatanUI() { const isCollapsed = localStorage.getItem('webgis_sidebar_collapsed') === 'true'; if (isCollapsed) { const s = document.getElementById('sidebar-container'); const b = document.getElementById('toggle-sidebar'); if (s && b) { s.classList.add('collapsed'); b.innerHTML = '❯'; } } const savedTab = localStorage.getItem('webgis_active_tab'); if (savedTab) switchTab(savedTab); } window.lokasiSaya = () => { triggerToastNotification("Mencari lokasi GPS..."); map.locate({enableHighAccuracy: true}); }; map.on('locationfound', function(e) { map.setView(e.latlng, 16); triggerToastNotification("Lokasi GPS ditemukan."); }); map.on('locationerror', function(e) { triggerToastNotification("Gagal mendeteksi lokasi GPS. Periksa izin browser Anda."); }); window.bukaModalFilterLayer = () => { document.getElementById('modal-filter-layer').style.display = 'flex'; }; window.bukaModalFilterSPBU = () => { document.getElementById('modal-filter-spbu').style.display = 'flex'; }; window.terapkanFilterSPBU = () => { state.filterSPBU = document.querySelector('input[name="rad-spbu"]:checked').value; document.getElementById('modal-filter-spbu').style.display = 'none'; renderSPBU(); }; window.terapkanFilterLayer = () => { state.filterJalan = Array.from(document.querySelectorAll('.cb-jalan:checked')).map(cb => cb.value); state.filterTanah = Array.from(document.querySelectorAll('.cb-tanah:checked')).map(cb => cb.value); document.getElementById('modal-filter-layer').style.display = 'none'; renderJalan(); renderTanah(); }; const setupDropdownColor = (idSelect, colorMap) => { const select = document.getElementById(idSelect); if(select) { select.addEventListener('change', () => { select.style.borderLeftColor = colorMap[select.value] || '#CBD5E0'; }); } }; setupDropdownColor('jalan-status', { 'Jalan Nasional': '#E53E3E', 'Jalan Provinsi': '#3182CE', 'Jalan Kabupaten': '#38A169' }); setupDropdownColor('tanah-status', { 'SHM': '#38A169', 'HGB': '#DD6B20', 'HGU': '#3182CE', 'HP': '#718096' }); const resetAllModes = () => { state.isMarkerMode = false; const btnMarker = document.getElementById('btn-tool-marker'); if(btnMarker) btnMarker.classList.remove('active'); state.isRoadMode = false; const btnJalan = document.getElementById('btn-tool-jalan'); if(btnJalan) btnJalan.classList.remove('active'); state.isLandMode = false; const btnTanah = document.getElementById('btn-tool-tanah'); if(btnTanah) btnTanah.classList.remove('active'); state.isKemiskinanMode = false; const btnKemiskinanMode = document.getElementById('btn-add-kemiskinan'); if(btnKemiskinanMode) btnKemiskinanMode.classList.remove('active'); state.isGhostLocked = false; map.getContainer().style.cursor = ''; if (ghostMarker) { map.removeLayer(ghostMarker); ghostMarker = null; } map.closePopup(); Object.values(mapLayers.spbu).forEach(m => { if(m.dragging) m.dragging.disable(); }); batalGambarJalan(); batalGambarTanah(); const btnKemiskinan = document.getElementById('btn-add-kemiskinan'); if(btnKemiskinan) { btnKemiskinan.innerHTML = `+ Aktifkan Kursor Marker`; } }; window.toggleMarkerMode = () => { const wasActive = state.isMarkerMode; resetAllModes(); if (!wasActive) { state.isMarkerMode = true; document.getElementById('btn-tool-marker').classList.add('active'); map.getContainer().style.cursor = 'crosshair'; Object.values(mapLayers.spbu).forEach(m => { if(m.dragging) m.dragging.enable(); }); } }; window.toggleRoadMode = () => { const wasActive = state.isRoadMode; resetAllModes(); if (!wasActive) { state.isRoadMode = true; document.getElementById('btn-tool-jalan').classList.add('active'); map.getContainer().style.cursor = 'crosshair'; } }; window.toggleLandMode = () => { const wasActive = state.isLandMode; resetAllModes(); if (!wasActive) { state.isLandMode = true; document.getElementById('btn-tool-tanah').classList.add('active'); map.getContainer().style.cursor = 'crosshair'; } }; window.toggleKemiskinanMode = () => { const wasActive = state.isKemiskinanMode; resetAllModes(); if (!wasActive) { state.isKemiskinanMode = true; const btn = document.getElementById('btn-add-kemiskinan'); btn.classList.add('active'); btn.innerHTML = `📍 Mode Penempatan Aktif`; map.getContainer().style.cursor = 'none'; } }; const cbMarkerBaru = document.getElementById('pilihan-marker-baru'); if(cbMarkerBaru) { cbMarkerBaru.addEventListener('change', function(e) { state.newMarkerType = e.target.value; }); } const RightToolbar = L.Control.extend({ options: { position: 'topright' }, onAdd: function() { const c = L.DomUtil.create('div', 'custom-leaflet-toolbar'); c.innerHTML = ` `; L.DomEvent.disableClickPropagation(c); return c; } }); map.addControl(new RightToolbar()); // ==================================================== // EVENT LISTENER ROUTER (KLIK PETA UTAMA) // ==================================================== function bukaGembokForm(prefix) { const nama = document.getElementById(`${prefix}-nama`); const status = document.getElementById(`${prefix}-status`); const telp = document.getElementById(`${prefix}-telp`); if (nama) { nama.readOnly = false; nama.removeAttribute('readonly'); } if (status) { status.disabled = false; status.removeAttribute('disabled'); } if (telp) { telp.readOnly = false; telp.removeAttribute('readonly'); } } function resetFormModal() { document.getElementById('spbu-id').value = ""; document.getElementById('spbu-nama').value = ""; document.getElementById('spbu-telp').value = ""; document.getElementById('spbu-status').value = "Buka 24 Jam"; if(document.getElementById('spbu-alamat')) { document.getElementById('spbu-alamat').value = ""; } bukaGembokForm('spbu'); } map.on('click', async function(e) { if (state.isMarkerMode) { resetFormModal(); document.getElementById('spbu-modal-title').innerText = "Tambah SPBU Baru"; document.getElementById('spbu-lat').value = e.latlng.lat; document.getElementById('spbu-lng').value = e.latlng.lng; const alamatInput = document.getElementById('spbu-alamat'); if(alamatInput) alamatInput.value = "Sedang melacak alamat dari satelit..."; document.getElementById('spbu-action-wrapper').innerHTML = ` `; document.getElementById('modal-spbu').style.display = 'flex'; setTimeout(() => { const input = document.getElementById('spbu-nama'); if(input) input.focus(); }, 100); const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng); if(alamatInput) alamatInput.value = alamatDitemukan; const btnSimpan = document.getElementById('btn-eksekusi-spbu'); if(btnSimpan) { btnSimpan.innerText = "Simpan Lokasi"; btnSimpan.disabled = false; } } else if (state.isRoadMode) { state.tempRoadCoords.push([e.latlng.lat, e.latlng.lng]); if (state.tempRoadLayer) { map.removeLayer(state.tempRoadLayer); } state.tempRoadLayer = L.polyline(state.tempRoadCoords, { color: '#1A365D', weight: 4, dashArray: '5, 8' }).addTo(map); const btn = document.getElementById('btn-selesai-jalan'); if (btn && state.tempRoadCoords.length >= 2) { btn.style.display = 'block'; } } else if (state.isLandMode) { state.tempLandCoords.push([e.latlng.lat, e.latlng.lng]); if (state.tempLandLayer) { map.removeLayer(state.tempLandLayer); } state.tempLandLayer = L.polygon(state.tempLandCoords, { color: '#D69E2E', fillColor: '#D69E2E', fillOpacity: 0.4, weight: 3, dashArray: '5, 8' }).addTo(map); const btn = document.getElementById('btn-selesai-tanah'); if (btn && state.tempLandCoords.length >= 3) { btn.style.display = 'block'; } } else if (state.isKemiskinanMode) { if (state.isGhostLocked) { triggerToastNotification("Selesaikan atau tutup formulir yang sedang terbuka terlebih dahulu."); return; } state.isGhostLocked = true; triggerToastNotification("Menganalisis satelit..."); const isIbadah = state.newMarkerType === 'ibadah'; const tmplId = isIbadah ? 'template-popup-ibadah' : 'template-popup-penduduk'; const tmpl = document.getElementById(tmplId); if(!tmpl) return; const clone = tmpl.content.cloneNode(true); const wrapper = document.createElement('div'); wrapper.appendChild(clone); const popup = L.popup({ closeOnClick: false, autoClose: false }) .setLatLng(e.latlng) .setContent(wrapper) .openOn(map); popup.on('remove', function() { state.isGhostLocked = false; }); const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng); if (isIbadah) { const elAlamatIb = wrapper.querySelector('#pop-ib-alamat'); if (elAlamatIb) elAlamatIb.value = alamatDitemukan; const radInput = wrapper.querySelector('#pop-ib-rad'); const radVal = wrapper.querySelector('#pop-ib-rad-val'); if (radInput && radVal) { radInput.oninput = function() { radVal.innerText = this.value; }; } const btnSimpan = wrapper.querySelector('#btn-pop-simpan'); if(btnSimpan) { btnSimpan.innerText = "Simpan Ibadah"; btnSimpan.disabled = false; btnSimpan.onclick = () => simpanIbadahPopup(e.latlng.lat, e.latlng.lng, popup, wrapper); } } else { const elAlamatPd = wrapper.querySelector('#pop-pd-alamat'); if (elAlamatPd) elAlamatPd.value = alamatDitemukan; const inputTanggungan = wrapper.querySelector('#pop-pd-tanggungan'); if(inputTanggungan) { inputTanggungan.value = 0; inputTanggungan.disabled = true; inputTanggungan.setAttribute('title', 'Dihitung otomatis melalui menu Detail Keluarga'); } const btnSimpan = wrapper.querySelector('#btn-pop-simpan'); if(btnSimpan) { btnSimpan.innerText = "Simpan Penduduk"; btnSimpan.disabled = false; btnSimpan.onclick = () => simpanPendudukPopup(e.latlng.lat, e.latlng.lng, popup, wrapper); } } } }); // ==================================================== // DECISION SUPPORT SYSTEM (DSS) KEMISKINAN (SKOR 0-5) // ==================================================== function kalkulasiDSS(pengeluaran, pmtScore) { if (pengeluaran > state.garisKemiskinan) return "Tidak Miskin"; if (pmtScore >= 4) return "Sangat Miskin"; if (pmtScore >= 2) return "Miskin"; if (pmtScore === 1) return "Rentan Miskin"; return "Tidak Miskin"; } function kalkulasiBantuan(status) { if (status === 'Sangat Miskin') return { beras: 15, minyak: 3, gula: 2, telur: 2, susu: 4, tunai: 300000 }; if (status === 'Miskin') return { beras: 10, minyak: 2, gula: 1, telur: 1, susu: 2, tunai: 150000 }; if (status === 'Rentan Miskin') return { beras: 5, minyak: 1, gula: 1, telur: 0, susu: 0, tunai: 0 }; return { beras: 0, minyak: 0, gula: 0, telur: 0, susu: 0, tunai: 0 }; } // ==================================================== // DECOUPLED RENDERING ENGINE (PENGGAMBAR TUNGGAL) // ==================================================== const agamaToIbadahMap = { 'Islam': 'Masjid', 'Kristen': 'Gereja', 'Katolik': 'Gereja', 'Hindu': 'Pura', 'Buddha': 'Vihara', 'Konghucu': 'Klenteng' }; function renderSPBU() { const cardContainer = document.getElementById('card-container'); if(cardContainer) cardContainer.innerHTML = ''; Object.values(mapLayers.spbu).forEach(m => map.removeLayer(m)); mapLayers.spbu = {}; Object.values(dataStore.spbu).forEach(d => { if (state.filterSPBU !== 'Semua' && d.status_operasional !== state.filterSPBU) return; const lat = parseFloat(d.latitude); const lng = parseFloat(d.longitude); if (isNaN(lat) || isNaN(lng)) return; const isBuka = d.status_operasional === 'Buka 24 Jam'; const customIcon = L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [30, 42], iconAnchor: [15, 42] }); const marker = L.marker([lat, lng], { icon: customIcon, draggable: state.isMarkerMode }).addTo(map); marker.bindTooltip(`${d.nama_spbu}`, { direction: 'top', offset: [0, -35], className: 'spbu-tooltip' }); marker.on('click', () => { if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return; document.getElementById('spbu-id').value = d.id; document.getElementById('spbu-nama').value = d.nama_spbu; document.getElementById('spbu-telp').value = d.no_telp; document.getElementById('spbu-status').value = d.status_operasional; if(document.getElementById('spbu-alamat')) { document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam"; } bukaGembokForm('spbu'); document.getElementById('spbu-modal-title').innerText = "Detail & Kelola SPBU"; document.getElementById('spbu-action-wrapper').innerHTML = ` `; document.getElementById('modal-spbu').style.display = 'flex'; }); marker.on('dragend', async function(e) { const pos = e.target.getLatLng(); triggerToastNotification("⏳ Menganalisis alamat baru..."); const alamatBaru = await dapatkanAlamat(pos.lat, pos.lng); safeFetch('otak_spbu/api_update_coords.php', { id: d.id, lat: pos.lat, lng: pos.lng, alamat: alamatBaru }) .then(res => { dataStore.spbu[d.id].latitude = pos.lat; dataStore.spbu[d.id].longitude = pos.lng; dataStore.spbu[d.id].alamat = alamatBaru; triggerToastNotification("📍 Koordinat & Alamat berhasil digeser!"); renderSPBU(); }).catch(err => { alert("Gagal."); e.target.setLatLng([dataStore.spbu[d.id].latitude, dataStore.spbu[d.id].longitude]); }); }); mapLayers.spbu[d.id] = marker; const alamatRingkas = d.alamat ? (d.alamat.substring(0, 45) + '...') : 'Alamat belum direkam'; if(cardContainer) { cardContainer.innerHTML += `

${d.nama_spbu}

${d.status_operasional}

${alamatRingkas}

`; } }); } function renderJalan() { Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m)); mapLayers.jalan = {}; Object.values(dataStore.jalan).forEach(d => { if (!state.filterJalan.includes(d.status_jalan)) return; try { const coords = JSON.parse(d.koordinat); if (!Array.isArray(coords) || coords.length < 2) return; let lineColor = '#38A169', lineWeight = 4; if (d.status_jalan === 'Jalan Nasional') { lineColor = '#E53E3E'; lineWeight = 8; } else if (d.status_jalan === 'Jalan Provinsi') { lineColor = '#3182CE'; lineWeight = 6; } const polyline = L.polyline(coords, { color: lineColor, weight: lineWeight, opacity: 0.8 }).addTo(map); polyline.bindTooltip(`${d.nama_jalan}
${d.jarak_meter} meter`, { direction: 'auto', className: 'spbu-tooltip' }); polyline.on('click', () => { handleRoadClick(d.id); }); mapLayers.jalan[d.id] = polyline; } catch(e) {} }); } function renderTanah() { Object.values(mapLayers.tanah).forEach(m => map.removeLayer(m)); mapLayers.tanah = {}; Object.values(dataStore.tanah).forEach(d => { if (!state.filterTanah.includes(d.status_kepemilikan)) return; try { const coords = JSON.parse(d.koordinat); if (!Array.isArray(coords) || coords.length < 3) return; let fillColor = '#38A169'; if (d.status_kepemilikan === 'HGB') fillColor = '#DD6B20'; else if (d.status_kepemilikan === 'HGU') fillColor = '#3182CE'; else if (d.status_kepemilikan === 'HP') fillColor = '#718096'; const polygon = L.polygon(coords, { color: fillColor, fillColor: fillColor, fillOpacity: 0.4, weight: 2 }).addTo(map); polygon.bindTooltip(`${d.nama_pemilik}
${d.luas_meter} m²`, { direction: 'auto', className: 'spbu-tooltip' }); polygon.on('click', () => { handleLandClick(d.id); }); mapLayers.tanah[d.id] = polygon; } catch(e) {} }); } function renderKemiskinan() { const listContainer = document.getElementById('kemiskinan-card-container'); if(listContainer) listContainer.innerHTML = ''; Object.values(mapLayers.ibadah).forEach(m => map.removeLayer(m)); mapLayers.ibadah = {}; Object.values(mapLayers.radiusIbadah).forEach(m => map.removeLayer(m)); mapLayers.radiusIbadah = {}; Object.values(mapLayers.penduduk).forEach(m => map.removeLayer(m)); mapLayers.penduduk = {}; const naunganCounter = {}; Object.keys(dataStore.ibadah).forEach(id => naunganCounter[id] = 0); const pendudukDiproses = []; Object.values(dataStore.penduduk).forEach(p => { const pLat = parseFloat(p.latitude); const pLng = parseFloat(p.longitude); if (isNaN(pLat) || isNaN(pLng)) return; const pos = L.latLng(pLat, pLng); let adaNaungan = false; let jarakTerdekat = Infinity; let idIbadahTerpilih = null; let teksNaungan = "Tidak berada di bawah wewenang rumah ibadah manapun"; if (p.status_kemiskinan !== 'Tidak Miskin') { const jenisIbadahDicari = agamaToIbadahMap[p.agama]; if (jenisIbadahDicari) { Object.values(dataStore.ibadah).forEach(ib => { if (ib.jenis === jenisIbadahDicari) { const ibLat = parseFloat(ib.latitude); const ibLng = parseFloat(ib.longitude); if (!isNaN(ibLat) && !isNaN(ibLng)) { const jarakM = map.distance(pos, L.latLng(ibLat, ibLng)); if (jarakM <= ib.radius_meter && jarakM < jarakTerdekat) { jarakTerdekat = jarakM; idIbadahTerpilih = ib.id; adaNaungan = true; let namaIb = ib.nama; if (!namaIb.toLowerCase().startsWith(ib.jenis.toLowerCase())) { namaIb = `${ib.jenis} ${ib.nama}`; } teksNaungan = `Bernaung di bawah wewenang: ${namaIb}`; } } } }); } } if (adaNaungan && idIbadahTerpilih) { naunganCounter[idIbadahTerpilih]++; } pendudukDiproses.push({ dataMentah: p, lat: pLat, lng: pLng, bernaung: adaNaungan, teksNaungan: teksNaungan }); }); Object.values(dataStore.ibadah).forEach(d => { const lat = parseFloat(d.latitude); const lng = parseFloat(d.longitude); if (isNaN(lat) || isNaN(lng)) return; let judulIbadah = d.nama; const jenisLower = d.jenis.toLowerCase(); if (!judulIbadah.toLowerCase().startsWith(jenisLower)) { judulIbadah = `${d.jenis} ${d.nama}`; } const circle = L.circle([lat, lng], { color: '#D69E2E', fillColor: '#D69E2E', fillOpacity: 0.15, radius: d.radius_meter, weight: 1 }).addTo(map); mapLayers.radiusIbadah[d.id] = circle; const icon = L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [30, 42], iconAnchor: [15, 42] }); const marker = L.marker([lat, lng], { icon: icon }).addTo(map); marker.bindTooltip(`${judulIbadah}
Radius: ${d.radius_meter}m
Naungan: ${naunganCounter[d.id]} KK`, { direction: 'top', className: 'spbu-tooltip' }); marker.on('click', () => { if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return; const tmpl = document.getElementById('template-popup-read-ibadah'); if(!tmpl) return; const clone = tmpl.content.cloneNode(true); clone.querySelector('#read-ib-nama').innerText = judulIbadah; clone.querySelector('#read-ib-jenis').innerText = `Jenis: ${d.jenis}`; clone.querySelector('#read-ib-radius').innerText = `Radius Jangkauan: ${d.radius_meter} Meter`; clone.querySelector('#read-ib-naungan').innerText = `Keluarga dalam radius: ${naunganCounter[d.id]} KK`; clone.querySelector('#read-ib-alamat').innerText = d.alamat || "Alamat tidak direkam"; const wrapper = document.createElement('div'); wrapper.appendChild(clone); const popup = L.popup({ minWidth: 260 }) .setLatLng([lat, lng]) .setContent(wrapper) .openOn(map); const btnEdit = wrapper.querySelector('#btn-ib-edit'); if(btnEdit) btnEdit.onclick = () => { map.closePopup(popup); bukaFormEditIbadah(d, d.nama); }; const btnHapus = wrapper.querySelector('#btn-ib-hapus'); if(btnHapus) btnHapus.onclick = () => { if(confirm(`Yakin ingin menghapus permanen ${judulIbadah}?`)) { map.closePopup(popup); eksekusiHapusIbadah(d.id); } }; }); mapLayers.ibadah[d.id] = marker; if(listContainer) { listContainer.innerHTML += `

🕌 ${judulIbadah}

${d.jenis} | ${d.radius_meter}m | ${naunganCounter[d.id]} KK

`; } }); pendudukDiproses.forEach(item => { const p = item.dataMentah; let isDarurat = dataStore.statusDarurat[p.id] === true; let pinColor = item.bernaung ? '#3182CE' : '#805AD5'; let markerClass = isDarurat ? 'marker-wrapper marker-darurat' : 'marker-wrapper'; const icon = L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [30, 42], iconAnchor: [15, 42] }); const marker = L.marker([item.lat, item.lng], { icon: icon }).addTo(map); marker.bindTooltip(`KK: ${p.nama_kk}
PMT: ${p.pmt_score} | ${p.status_kemiskinan}`, { direction: 'top', className: 'spbu-tooltip' }); marker.on('click', () => { if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return; const tmpl = document.getElementById('template-popup-read-penduduk'); if(!tmpl) return; const clone = tmpl.content.cloneNode(true); clone.querySelector('#read-pd-nama').innerText = `Keluarga ${p.nama_kk}`; clone.querySelector('#read-pd-alamat').innerText = p.alamat || "Alamat tidak direkam"; const elAgama = clone.querySelector('#read-pd-agama'); if(elAgama) elAgama.innerText = `Agama: ${p.agama || '-'}`; const elAnggota = clone.querySelector('#read-pd-anggota'); if(elAnggota) { const totalJiwa = parseInt(p.real_tanggungan) || 0; elAnggota.innerText = `Anggota/Tanggungan: ${totalJiwa} Jiwa`; } const badge = clone.querySelector('#read-pd-badge'); badge.innerText = p.status_kemiskinan; if (p.status_kemiskinan === 'Sangat Miskin') badge.className = 'pop-badge badge-sangat-miskin'; else if (p.status_kemiskinan === 'Miskin') badge.className = 'pop-badge badge-miskin'; else if (p.status_kemiskinan === 'Rentan Miskin') badge.className = 'pop-badge badge-rentan'; else badge.className = 'pop-badge badge-aman'; const elNaungan = clone.querySelector('#read-pd-naungan'); if(elNaungan) { elNaungan.innerText = item.teksNaungan; elNaungan.className = item.bernaung ? 'pop-naungan naungan-aktif' : 'pop-naungan naungan-pasif'; } const accContent = clone.querySelector('#acc-pd-content'); const btnMusibah = document.createElement('button'); btnMusibah.className = 'btn btn-danger'; btnMusibah.innerText = 'Lapor Darurat Musibah'; btnMusibah.style.width = '100%'; btnMusibah.style.marginBottom = '8px'; btnMusibah.style.padding = '12px'; const btnDetail = document.createElement('button'); btnDetail.className = 'btn btn-warning'; btnDetail.innerText = 'Detail Anggota Keluarga'; btnDetail.style.width = '100%'; btnDetail.style.marginTop = '0px'; btnDetail.style.marginBottom = '15px'; btnDetail.style.padding = '12px'; const popActionMain = clone.querySelector('.pop-action-main'); if(popActionMain) { if(!item.bernaung) { popActionMain.innerHTML = ''; } else { popActionMain.insertBefore(btnMusibah, clone.querySelector('#btn-pd-bantuan')); btnMusibah.onclick = () => { bukaFormMusibah(p); }; const btnBantuan = clone.querySelector('#btn-pd-bantuan'); if(btnBantuan) btnBantuan.onclick = () => { bukaFormLogBantuan(p); }; } popActionMain.appendChild(btnDetail); btnDetail.onclick = () => { bukaFormDetailKeluarga(p, item.bernaung); }; } const btnOpsi = clone.querySelector('#btn-pd-opsi'); const wrapper = document.createElement('div'); wrapper.appendChild(clone); const popup = L.popup({ minWidth: 260 }) .setLatLng([item.lat, item.lng]) .setContent(wrapper) .openOn(map); if(btnOpsi && accContent) { btnOpsi.onclick = () => { accContent.classList.toggle('show'); btnOpsi.innerText = accContent.classList.contains('show') ? 'Tutup Opsi' : 'Opsi Lainnya'; popup.update(); }; } // Integrasi Tombol Riwayat const btnRiwayatPelatihan = wrapper.querySelector('#btn-pd-riwayat-pelatihan'); if(btnRiwayatPelatihan) { btnRiwayatPelatihan.onclick = () => { bukaFormRiwayatPelatihan(p); }; } const btnRiwayatMusibah = wrapper.querySelector('#btn-pd-riwayat-musibah'); if(btnRiwayatMusibah) { btnRiwayatMusibah.onclick = () => { bukaFormRiwayatMusibah(p); }; } const btnRiwayatBantuan = wrapper.querySelector('#btn-pd-riwayat-bantuan'); if(btnRiwayatBantuan) { btnRiwayatBantuan.onclick = () => { bukaFormRiwayatBantuan(p); }; } const btnEdit = wrapper.querySelector('#btn-pd-edit'); if(btnEdit) { btnEdit.onclick = () => { map.closePopup(popup); bukaFormEditPenduduk(p); }; } const btnHapus = wrapper.querySelector('#btn-pd-hapus'); if(btnHapus) { btnHapus.onclick = () => { if(confirm(`Anda yakin ingin menghapus permanen data keluarga ${p.nama_kk}?`)) { map.closePopup(popup); eksekusiHapusPenduduk(p.id); } }; } }); mapLayers.penduduk[p.id] = marker; let cls = 'card-penduduk-rentan'; if(p.status_kemiskinan === 'Sangat Miskin' || p.status_kemiskinan === 'Miskin') cls = 'card-penduduk-miskin'; if(p.status_kemiskinan === 'Tidak Miskin') cls = 'card-penduduk-aman'; if(listContainer) { listContainer.innerHTML += `

🏠 Kel. ${p.nama_kk}

PMT: ${p.pmt_score}/5 | Rp${p.pengeluaran}

`; } }); } // ==================================================== // GEOMETRY BUILDER HELPERS (Jalan & Tanah Selesai) // ==================================================== window.lihatSpbuDariCard = function(id) { if (!dataStore.spbu[id]) return; const d = dataStore.spbu[id]; document.getElementById('spbu-id').value = d.id; document.getElementById('spbu-nama').value = d.nama_spbu; document.getElementById('spbu-telp').value = d.no_telp; document.getElementById('spbu-status').value = d.status_operasional; if(document.getElementById('spbu-alamat')) { document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam"; } document.getElementById('spbu-nama').readOnly = true; document.getElementById('spbu-telp').readOnly = true; document.getElementById('spbu-status').disabled = true; document.getElementById('spbu-modal-title').innerText = "Informasi SPBU (Read-Only)"; document.getElementById('spbu-action-wrapper').innerHTML = ` `; document.getElementById('modal-spbu').style.display = 'flex'; }; window.selesaiGambarJalan = function() { if (state.tempRoadCoords.length < 2) return; let totalJarak = 0; for (let i = 0; i < state.tempRoadCoords.length - 1; i++) { totalJarak += map.distance(L.latLng(state.tempRoadCoords[i]), L.latLng(state.tempRoadCoords[i+1])); } document.getElementById('jalan-id').value = ""; document.getElementById('jalan-nama').value = ""; document.getElementById('jalan-status').value = "Jalan Kabupaten"; document.getElementById('jalan-status').dispatchEvent(new Event('change')); document.getElementById('jalan-jarak').value = Math.round(totalJarak); document.getElementById('jalan-koordinat').value = JSON.stringify(state.tempRoadCoords); bukaGembokForm('jalan'); document.getElementById('jalan-action-wrapper').innerHTML = ` `; document.getElementById('modal-jalan').style.display = 'flex'; document.getElementById('btn-selesai-jalan').style.display = 'none'; }; window.batalGambarJalan = function() { state.tempRoadCoords = []; if (state.tempRoadLayer) map.removeLayer(state.tempRoadLayer); state.tempRoadLayer = null; const btn = document.getElementById('btn-selesai-jalan'); if (btn) btn.style.display = 'none'; if(document.getElementById('modal-jalan')) { document.getElementById('modal-jalan').style.display = 'none'; } }; window.handleRoadClick = function(id) { if (state.isRoadMode || state.isMarkerMode || state.isLandMode || !dataStore.jalan[id]) return; const d = dataStore.jalan[id]; document.getElementById('jalan-id').value = d.id; document.getElementById('jalan-nama').value = d.nama_jalan; document.getElementById('jalan-status').value = d.status_jalan; document.getElementById('jalan-jarak').value = d.jarak_meter; document.getElementById('jalan-status').dispatchEvent(new Event('change')); bukaGembokForm('jalan'); document.getElementById('jalan-modal-title').innerText = "Detail & Kelola Jalan"; document.getElementById('jalan-action-wrapper').innerHTML = ` `; document.getElementById('modal-jalan').style.display = 'flex'; }; window.selesaiGambarTanah = function() { if (state.tempLandCoords.length < 3) return; let area = 0, R = 6378137; const ll = state.tempLandCoords.map(c => L.latLng(c[0], c[1])); for (let i = 0; i < ll.length; i++) { let p1 = ll[i], p2 = ll[(i + 1) % ll.length]; area += (p2.lng - p1.lng) * Math.PI / 180 * (2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180)); } document.getElementById('tanah-id').value = ""; document.getElementById('tanah-nama').value = ""; document.getElementById('tanah-status').value = "SHM"; document.getElementById('tanah-status').dispatchEvent(new Event('change')); document.getElementById('tanah-luas').value = Math.round(Math.abs(area * R * R / 2.0)); document.getElementById('tanah-koordinat').value = JSON.stringify(state.tempLandCoords); bukaGembokForm('tanah'); document.getElementById('tanah-action-wrapper').innerHTML = ` `; document.getElementById('modal-tanah').style.display = 'flex'; document.getElementById('btn-selesai-tanah').style.display = 'none'; }; window.batalGambarTanah = function() { state.tempLandCoords = []; if (state.tempLandLayer) map.removeLayer(state.tempLandLayer); state.tempLandLayer = null; const btn = document.getElementById('btn-selesai-tanah'); if (btn) btn.style.display = 'none'; if(document.getElementById('modal-tanah')) { document.getElementById('modal-tanah').style.display = 'none'; } }; window.handleLandClick = function(id) { if (state.isRoadMode || state.isMarkerMode || state.isLandMode || !dataStore.tanah[id]) return; const d = dataStore.tanah[id]; document.getElementById('tanah-id').value = d.id; document.getElementById('tanah-nama').value = d.nama_pemilik; document.getElementById('tanah-status').value = d.status_kepemilikan; document.getElementById('tanah-luas').value = d.luas_meter; document.getElementById('tanah-status').dispatchEvent(new Event('change')); bukaGembokForm('tanah'); document.getElementById('tanah-modal-title').innerText = "Detail & Kelola Tanah"; document.getElementById('tanah-action-wrapper').innerHTML = ` `; document.getElementById('modal-tanah').style.display = 'flex'; }; // ==================================================== // CRUD PENYIMPANAN // ==================================================== window.eksekusiUpdateGaris = () => { const val = document.getElementById('input-garis-kemiskinan').value; if(!val || val <= 0) return alert("Masukkan nilai rupiah yang valid!"); if(!confirm(`PERINGATAN: Mengubah garis kemiskinan menjadi Rp${val} akan mengkalibrasi ulang status miskin/tidak miskin seluruh penduduk di database saat ini.\n\nLanjutkan?`)) return; triggerToastNotification("⏳ Mengkalibrasi ulang seluruh data penduduk..."); safeFetch('otak_kemiskinan/api_update_gariskemiskinan.php', { nilai: parseInt(val) }) .then(res => { triggerToastNotification("✅ " + res.pesan); document.getElementById('input-garis-kemiskinan').value = ""; muatDataSemua(); }) .catch(err => alert("Gagal memperbarui: " + err)); }; window.eksekusiSimpanBaru = () => { const payload = { nama: document.getElementById('spbu-nama').value, status: document.getElementById('spbu-status').value, telp: document.getElementById('spbu-telp').value, alamat: document.getElementById('spbu-alamat') ? document.getElementById('spbu-alamat').value : "", lat: document.getElementById('spbu-lat').value, lng: document.getElementById('spbu-lng').value }; safeFetch('otak_spbu/api_simpan_spbu.php', payload) .then(() => { document.getElementById('modal-spbu').style.display='none'; triggerToastNotification("✅ SPBU ditambahkan!"); muatDataSemua(); }) .catch(err => alert(`Gagal menyimpan data.\nAlasan: ${err.message}`)); }; window.eksekusiUpdate = (id) => { const payload = { id: id, nama: document.getElementById('spbu-nama').value, status: document.getElementById('spbu-status').value, telp: document.getElementById('spbu-telp').value }; safeFetch('otak_spbu/api_update_spbu.php', payload) .then(() => { document.getElementById('modal-spbu').style.display='none'; triggerToastNotification("✅ SPBU diperbarui!"); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiHapus = (id) => { if(!confirm("Hapus SPBU ini?")) return; safeFetch('otak_spbu/api_hapus_spbu.php', { id: id }) .then(() => { document.getElementById('modal-spbu').style.display='none'; triggerToastNotification("🗑️ SPBU dihapus."); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiSimpanJalan = () => { const payload = { nama: document.getElementById('jalan-nama').value, status: document.getElementById('jalan-status').value, jarak: document.getElementById('jalan-jarak').value, koordinat: document.getElementById('jalan-koordinat').value }; safeFetch('otak_jalan/api_simpan_jalan.php', payload) .then(() => { batalGambarJalan(); triggerToastNotification("✅ Jalan ditambahkan!"); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiUpdateJalan = (id) => { const payload = { id: id, nama: document.getElementById('jalan-nama').value, status: document.getElementById('jalan-status').value }; safeFetch('otak_jalan/api_update_jalan.php', payload) .then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("✅ Jalan diperbarui!"); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiHapusJalan = (id) => { if(!confirm("Hapus jalur ini?")) return; safeFetch('otak_jalan/api_hapus_jalan.php', { id: id }) .then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("🗑️ Jalan dihapus."); muatDataSemua(); }) .catch(err => alert(err)); }; // --- INJEKSI FUNGSI TANAH YANG HILANG --- window.eksekusiSimpanTanah = () => { const payload = { nama: document.getElementById('tanah-nama').value, status: document.getElementById('tanah-status').value, luas: document.getElementById('tanah-luas').value, koordinat: document.getElementById('tanah-koordinat').value }; safeFetch('otak_tanah/api_simpan_tanah.php', payload) .then(() => { batalGambarTanah(); triggerToastNotification("✅ Area Tanah ditambahkan!"); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiUpdateTanah = (id) => { const payload = { id: id, nama: document.getElementById('tanah-nama').value, status: document.getElementById('tanah-status').value }; safeFetch('otak_tanah/api_update_tanah.php', payload) .then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("✅ Area Tanah diperbarui!"); muatDataSemua(); }) .catch(err => alert(err)); }; window.eksekusiHapusTanah = (id) => { if(!confirm("Hapus area tanah ini?")) return; safeFetch('otak_tanah/api_hapus_tanah.php', { id: id }) .then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("🗑️ Area Tanah dihapus."); muatDataSemua(); }) .catch(err => alert(err)); }; // ---------------------------------------- window.simpanPendudukPopup = (lat, lng, popupRef, wrapper) => { const payload = { nama: wrapper.querySelector('#pop-pd-kk').value, agama: wrapper.querySelector('#pop-pd-agama').value, tanggungan: 0, pengeluaran: parseInt(wrapper.querySelector('#pop-pd-pengeluaran').value) || 0, lat: lat, lng: lng, alamat: wrapper.querySelector('#pop-pd-alamat').value, pmt_lantai: parseInt(wrapper.querySelector('#pop-pmt-lantai').value), pmt_dinding: parseInt(wrapper.querySelector('#pop-pmt-dinding').value), pmt_sanitasi: parseInt(wrapper.querySelector('#pop-pmt-sanitasi').value), pmt_penerangan: parseInt(wrapper.querySelector('#pop-pmt-listrik').value), pmt_air: parseInt(wrapper.querySelector('#pop-pmt-air').value) }; if (!payload.nama) return alert("Nama Kepala Keluarga wajib diisi!"); payload.pmt_score = payload.pmt_lantai + payload.pmt_dinding + payload.pmt_sanitasi + payload.pmt_penerangan + payload.pmt_air; payload.status = kalkulasiDSS(payload.pengeluaran, payload.pmt_score); safeFetch('otak_kemiskinan/api_simpan_penduduk.php', payload) .then(() => { map.closePopup(popupRef); resetAllModes(); triggerToastNotification(`DSS Dieksekusi: ${payload.status}`); muatDataSemua(); }) .catch(err => alert("Gagal menyimpan data: " + err)); }; window.bukaFormEditPenduduk = (p) => { state.isGhostLocked = true; const tmpl = document.getElementById('template-popup-penduduk'); if(!tmpl) return; const clone = tmpl.content.cloneNode(true); const wrapper = document.createElement('div'); wrapper.appendChild(clone); const popup = L.popup({ closeOnClick: false, autoClose: false }) .setLatLng([parseFloat(p.latitude), parseFloat(p.longitude)]) .setContent(wrapper) .openOn(map); popup.on('remove', function() { state.isGhostLocked = false; }); wrapper.querySelector('#pop-pd-kk').value = p.nama_kk; wrapper.querySelector('#pop-pd-agama').value = p.agama; const inputTanggungan = wrapper.querySelector('#pop-pd-tanggungan'); if(inputTanggungan) { inputTanggungan.value = p.real_tanggungan || 0; inputTanggungan.disabled = true; } wrapper.querySelector('#pop-pd-pengeluaran').value = p.pengeluaran; wrapper.querySelector('#pop-pmt-lantai').value = p.pmt_lantai; wrapper.querySelector('#pop-pmt-dinding').value = p.pmt_dinding; wrapper.querySelector('#pop-pmt-sanitasi').value = p.pmt_sanitasi; wrapper.querySelector('#pop-pmt-listrik').value = p.pmt_penerangan; wrapper.querySelector('#pop-pmt-air').value = p.pmt_air; wrapper.querySelector('#pop-pd-alamat').value = p.alamat; const btnSimpan = wrapper.querySelector('#btn-pop-simpan'); if(btnSimpan) { btnSimpan.innerText = "Update Data Keluarga"; btnSimpan.disabled = false; btnSimpan.onclick = () => updatePendudukPopup(p.id, popup, wrapper); } }; window.updatePendudukPopup = (id, popupRef, wrapper) => { const payload = { id: id, nama: wrapper.querySelector('#pop-pd-kk').value, agama: wrapper.querySelector('#pop-pd-agama').value, tanggungan: 0, pengeluaran: parseInt(wrapper.querySelector('#pop-pd-pengeluaran').value) || 0, pmt_lantai: parseInt(wrapper.querySelector('#pop-pmt-lantai').value), pmt_dinding: parseInt(wrapper.querySelector('#pop-pmt-dinding').value), pmt_sanitasi: parseInt(wrapper.querySelector('#pop-pmt-sanitasi').value), pmt_penerangan: parseInt(wrapper.querySelector('#pop-pmt-listrik').value), pmt_air: parseInt(wrapper.querySelector('#pop-pmt-air').value) }; if (!payload.nama) return alert("Nama Kepala Keluarga wajib diisi!"); payload.pmt_score = payload.pmt_lantai + payload.pmt_dinding + payload.pmt_sanitasi + payload.pmt_penerangan + payload.pmt_air; payload.status = kalkulasiDSS(payload.pengeluaran, payload.pmt_score); safeFetch('otak_kemiskinan/api_update_penduduk.php', payload) .then(() => { map.closePopup(popupRef); resetAllModes(); triggerToastNotification(`Data Diperbarui: ${payload.status}`); muatDataSemua(); }) .catch(err => alert("Gagal mengupdate data: " + err)); }; window.eksekusiHapusPenduduk = (id) => { safeFetch('otak_kemiskinan/api_hapus_penduduk.php', { id: id }) .then(() => { triggerToastNotification("🗑️ Data Penduduk dihapus permanen."); muatDataSemua(); }) .catch(err => alert(err)); }; // --- LOG BANTUAN --- window.bukaFormLogBantuan = (p) => { document.getElementById('log-modal-title').innerText = "Catat Penyaluran Bantuan"; document.getElementById('log-nama-penerima').innerText = `Keluarga ${p.nama_kk}`; document.getElementById('log-id-penduduk').value = p.id; const rekomendasi = kalkulasiBantuan(p.status_kemiskinan); document.getElementById('log-beras').value = rekomendasi.beras; document.getElementById('log-minyak').value = rekomendasi.minyak; document.getElementById('log-gula').value = rekomendasi.gula; document.getElementById('log-telur').value = rekomendasi.telur; document.getElementById('log-susu').value = rekomendasi.susu; document.getElementById('log-tunai').value = rekomendasi.tunai; document.getElementById('log-catatan').value = ""; document.getElementById('modal-log-bantuan').style.display = 'flex'; }; window.eksekusiSimpanLogBantuan = () => { const id = document.getElementById('log-id-penduduk').value; const payload = { id_penduduk: id, beras: parseInt(document.getElementById('log-beras').value) || 0, minyak: parseInt(document.getElementById('log-minyak').value) || 0, gula: parseInt(document.getElementById('log-gula').value) || 0, telur: parseInt(document.getElementById('log-telur').value) || 0, susu: parseInt(document.getElementById('log-susu').value) || 0, tunai: parseInt(document.getElementById('log-tunai').value) || 0, catatan: document.getElementById('log-catatan').value }; safeFetch('otak_kemiskinan/api_simpan_logbantuan.php', payload) .then(() => { document.getElementById('modal-log-bantuan').style.display = 'none'; triggerToastNotification("✅ Log bantuan berhasil dicatat."); if(dataStore.statusDarurat[id]) { dataStore.statusDarurat[id] = false; triggerToastNotification("Penanganan Bantuan menghentikan status Darurat keluarga ini."); renderKemiskinan(); } }) .catch(err => alert("Gagal menyimpan log bantuan: " + err)); }; // --- LOGIKA RIWAYAT --- window.bukaFormRiwayatBantuan = (p) => { document.getElementById('riwayat-nama-penerima').innerText = p.nama_kk; document.getElementById('modal-riwayat-bantuan').style.display = 'flex'; const tbody = document.getElementById('riwayat-tbody'); tbody.innerHTML = `Mengambil data dari satelit...`; fetch(`otak_kemiskinan/api_tampil_logbantuan.php?id=${p.id}`) .then(res => res.json()) .then(data => { if(data.error) throw new Error(data.error); if(data.length === 0) { tbody.innerHTML = `Belum ada riwayat penerimaan logistik.`; return; } let html = ''; data.forEach(log => { const tgl = log.created_at || log.waktu || log.tanggal || '-'; html += ` ${tgl} ${log.beras_kg} ${log.minyak_l} ${log.gula_kg} ${log.telur_kg} ${log.susu_kaleng} Rp${parseInt(log.tunai_rp).toLocaleString('id-ID')} ${log.catatan || '-'} `; }); tbody.innerHTML = html; }) .catch(err => { tbody.innerHTML = `Gagal memuat histori: ${err.message}`; }); }; window.bukaFormRiwayatPelatihan = (p) => { document.getElementById('riwayat-pelatihan-nama-penerima').innerText = p.nama_kk; document.getElementById('modal-riwayat-pelatihan').style.display = 'flex'; const tbody = document.getElementById('riwayat-pelatihan-tbody'); tbody.innerHTML = `Mengambil data dari satelit...`; fetch(`otak_kemiskinan/api_tampil_logpelatihan.php?id=${p.id}`) .then(res => res.json()) .then(data => { if(data.error) throw new Error(data.error); if(data.length === 0) { tbody.innerHTML = `Belum ada anggota keluarga yang mengikuti pelatihan.`; return; } let html = ''; data.forEach(log => { const tgl = log.tanggal_daftar || '-'; html += ` ${tgl} ${log.nik} ${log.nama_lengkap} ${log.nama_pelatihan} ${log.instansi_penyelenggara} `; }); tbody.innerHTML = html; }) .catch(err => { tbody.innerHTML = `Gagal memuat histori: ${err.message}`; }); }; window.bukaFormRiwayatMusibah = (p) => { document.getElementById('riwayat-musibah-nama-penerima').innerText = p.nama_kk; document.getElementById('modal-riwayat-musibah').style.display = 'flex'; const tbody = document.getElementById('riwayat-musibah-tbody'); tbody.innerHTML = `Mengambil data dari satelit...`; fetch(`otak_kemiskinan/api_tampil_musibah.php?id=${p.id}`) .then(res => res.json()) .then(data => { if(data.error) throw new Error(data.error); if(data.length === 0) { tbody.innerHTML = `Tidak ada riwayat pelaporan musibah.`; return; } let html = ''; data.forEach(log => { const tgl = log.tanggal_lapor || '-'; const statusColor = log.status_penanganan === 'Sudah Ditangani' ? 'var(--green-safe)' : 'var(--red-danger)'; html += ` ${tgl} ${log.jenis_musibah} ${log.deskripsi || '-'} ${log.status_penanganan} `; }); tbody.innerHTML = html; }) .catch(err => { tbody.innerHTML = `Gagal memuat histori: ${err.message}`; }); }; // --- LOGIKA MUSIBAH & PELATIHAN --- window.bukaFormMusibah = (p) => { document.getElementById('musibah-id-penduduk').value = p.id; document.getElementById('musibah-nama').innerText = p.nama_kk; document.getElementById('musibah-tgl').value = new Date().toISOString().split('T')[0]; document.getElementById('musibah-deskripsi').value = ""; document.getElementById('modal-musibah').style.display = 'flex'; }; window.simpanMusibah = () => { const id = document.getElementById('musibah-id-penduduk').value; const payload = { id_penduduk: id, tanggal: document.getElementById('musibah-tgl').value, jenis: document.getElementById('musibah-jenis').value, deskripsi: document.getElementById('musibah-deskripsi').value, status: document.getElementById('musibah-status').value }; if(!payload.tanggal || !payload.jenis) return alert("Tanggal dan Jenis Musibah wajib diisi!"); safeFetch('otak_kemiskinan/api_simpan_musibah.php', payload) .then(res => { triggerToastNotification("🚨 " + res.pesan); dataStore.statusDarurat[id] = true; document.getElementById('modal-musibah').style.display = 'none'; renderKemiskinan(); }) .catch(err => alert("Gagal mengirim laporan musibah: " + err)); }; window.bukaModalPelatihan = () => { document.getElementById('latih-nama').value = ''; document.getElementById('latih-instansi').value = ''; document.getElementById('latih-mulai').value = ''; document.getElementById('latih-selesai').value = ''; document.getElementById('latih-deskripsi').value = ''; document.getElementById('modal-master-pelatihan').style.display = 'flex'; }; window.eksekusiSimpanPelatihan = () => { const payload = { nama_pelatihan: document.getElementById('latih-nama').value, instansi: document.getElementById('latih-instansi').value, tgl_mulai: document.getElementById('latih-mulai').value, tgl_selesai: document.getElementById('latih-selesai').value, deskripsi: document.getElementById('latih-deskripsi').value }; if(!payload.nama_pelatihan || !payload.instansi || !payload.tgl_mulai || !payload.tgl_selesai) { return alert("Data krusial wajib diisi!"); } if(new Date(payload.tgl_mulai) > new Date(payload.tgl_selesai)) { return alert("Logika waktu tidak valid."); } safeFetch('otak_kemiskinan/api_simpan_pelatihan.php', payload) .then(res => { document.getElementById('modal-master-pelatihan').style.display = 'none'; triggerToastNotification("✅ " + res.pesan); muatDataSemua(); }) .catch(err => alert("Gagal menyimpan program pelatihan: " + err)); }; // --- LOGIKA ANGGOTA KELUARGA --- window.bukaFormDetailKeluarga = (p, isBernaung) => { document.getElementById('detail-keluarga-nama').innerText = p.nama_kk; document.getElementById('detail-id-penduduk').value = p.id; document.getElementById('detail-is-bernaung').value = isBernaung; document.getElementById('modal-detail-keluarga').style.display = 'flex'; resetFormAnggota(); muatDaftarAnggota(p.id, isBernaung); }; window.muatDaftarAnggota = (idPenduduk, isBernaung) => { const tbody = document.getElementById('tbody-anggota-keluarga'); tbody.innerHTML = `Memuat data anggota keluarga dari satelit...`; fetch(`otak_kemiskinan/api_tampil_anggota.php?id=${idPenduduk}`) .then(res => res.json()) .then(data => { if(data.error) throw new Error(data.error); if(data.length === 0) { tbody.innerHTML = `Belum ada rincian anggota keluarga yang diinput.`; return; } let html = ''; data.forEach(m => { const thnLahir = new Date(m.tanggal_lahir).getFullYear(); const umur = new Date().getFullYear() - thnLahir; const eduValid = !['Tidak Sekolah', 'SD'].includes(m.pendidikan_terakhir); const jobless = !m.pekerjaan || m.pekerjaan.trim() === ''; const ageValid = umur >= 17 && umur <= 55; let pelatihanHtml = ''; if(!isBernaung) { pelatihanHtml = `Tidak Bernaung`; } else if (eduValid && jobless && ageValid) { let opsi = ``; dataStore.pelatihan.forEach(pel => { opsi += ``; }); pelatihanHtml = `
`; } else { pelatihanHtml = `Tidak Memenuhi Syarat`; } html += ` ${m.nik} ${m.nama_lengkap} ${umur} Thn ${m.pendidikan_terakhir} ${m.pekerjaan || 'Menganggur'} ${pelatihanHtml} `; }); tbody.innerHTML = html; }) .catch(err => { tbody.innerHTML = `Gagal memuat: ${err.message}`; }); }; window.resetFormAnggota = () => { document.getElementById('anggota-nik').value = ''; document.getElementById('anggota-nama').value = ''; document.getElementById('anggota-lahir').value = ''; document.getElementById('anggota-pendidikan').value = 'Tidak Sekolah'; document.getElementById('anggota-pekerjaan').value = ''; const h4 = document.getElementById('modal-detail-keluarga').querySelector('h4'); if(h4) h4.innerText = "Tambah Anggota Baru"; const btnContainer = document.getElementById('anggota-pekerjaan').parentElement.nextElementSibling; if(btnContainer) { btnContainer.innerHTML = ``; } }; window.bukaFormEditAnggota = (id, nik, nama, lahir, pendidikan, pekerjaan) => { document.getElementById('anggota-nik').value = nik; document.getElementById('anggota-nama').value = nama; document.getElementById('anggota-lahir').value = lahir; document.getElementById('anggota-pendidikan').value = pendidikan; document.getElementById('anggota-pekerjaan').value = pekerjaan; const h4 = document.getElementById('modal-detail-keluarga').querySelector('h4'); if(h4) h4.innerText = `Edit Informasi Anggota (ID: ${id})`; const btnContainer = document.getElementById('anggota-pekerjaan').parentElement.nextElementSibling; if(btnContainer) { btnContainer.innerHTML = ` `; } }; window.eksekusiUpdateAnggota = (id) => { const idPenduduk = document.getElementById('detail-id-penduduk').value; const isBernaung = document.getElementById('detail-is-bernaung').value === 'true'; const payload = { id: id, nik: document.getElementById('anggota-nik').value, nama: document.getElementById('anggota-nama').value, lahir: document.getElementById('anggota-lahir').value, pendidikan: document.getElementById('anggota-pendidikan').value, pekerjaan: document.getElementById('anggota-pekerjaan').value }; if(!payload.nik || !payload.nama || !payload.lahir) return alert("Wajib diisi!"); safeFetch('otak_kemiskinan/api_update_anggota.php', payload) .then(res => { triggerToastNotification("✅ " + res.pesan); resetFormAnggota(); muatDaftarAnggota(idPenduduk, isBernaung); muatDataSemua(); }) .catch(err => alert("Gagal mengupdate: " + err)); }; window.eksekusiSimpanAnggota = () => { const idPenduduk = document.getElementById('detail-id-penduduk').value; const isBernaung = document.getElementById('detail-is-bernaung').value === 'true'; const payload = { id_penduduk: idPenduduk, nik: document.getElementById('anggota-nik').value, nama: document.getElementById('anggota-nama').value, lahir: document.getElementById('anggota-lahir').value, pendidikan: document.getElementById('anggota-pendidikan').value, pekerjaan: document.getElementById('anggota-pekerjaan').value }; if(!payload.nik || !payload.nama || !payload.lahir) return alert("Wajib diisi!"); safeFetch('otak_kemiskinan/api_simpan_anggota.php', payload) .then(res => { triggerToastNotification("✅ " + res.pesan); resetFormAnggota(); muatDaftarAnggota(idPenduduk, isBernaung); muatDataSemua(); }) .catch(err => alert("Gagal menambah: " + err)); }; window.eksekusiHapusAnggota = (idAnggota) => { if(!confirm("Hapus permanen?")) return; const idPenduduk = document.getElementById('detail-id-penduduk').value; const isBernaung = document.getElementById('detail-is-bernaung').value === 'true'; safeFetch('otak_kemiskinan/api_hapus_anggota.php', { id: idAnggota }) .then(() => { triggerToastNotification("🗑️ Dihapus."); muatDaftarAnggota(idPenduduk, isBernaung); muatDataSemua(); }) .catch(err => alert(err)); }; window.daftarPelatihanAnggota = (idAnggota) => { const idPel = document.getElementById(`sel-pel-${idAnggota}`).value; if(!idPel) return alert("Pilih program!"); if(!confirm("Daftarkan anggota ini?")) return; safeFetch('otak_kemiskinan/api_simpan_logpelatihan.php', { id_anggota: idAnggota, id_pelatihan: idPel }) .then(res => { triggerToastNotification("✅ " + res.pesan); }) .catch(err => alert(err)); }; // --- CRUD IBADAH --- window.simpanIbadahPopup = (lat, lng, popupRef, wrapper) => { const payload = { nama: wrapper.querySelector('#pop-ib-nama').value, jenis: wrapper.querySelector('#pop-ib-jenis').value, radius_meter: parseInt(wrapper.querySelector('#pop-ib-rad').value), latitude: lat, longitude: lng, alamat: wrapper.querySelector('#pop-ib-alamat').value }; if(!payload.nama) return alert("Wajib diisi!"); safeFetch('otak_kemiskinan/api_simpan_ibadah.php', payload) .then(() => { map.closePopup(popupRef); resetAllModes(); triggerToastNotification("✅ Berhasil!"); muatDataSemua(); }) .catch(err => alert("Gagal: " + err)); }; window.bukaFormEditIbadah = (d, judul) => { state.isGhostLocked = true; const tmpl = document.getElementById('template-popup-ibadah'); if(!tmpl) return; const clone = tmpl.content.cloneNode(true); const wrapper = document.createElement('div'); wrapper.appendChild(clone); const popup = L.popup({ closeOnClick: false, autoClose: false }) .setLatLng([parseFloat(d.latitude), parseFloat(d.longitude)]) .setContent(wrapper) .openOn(map); popup.on('remove', function() { state.isGhostLocked = false; }); wrapper.querySelector('#pop-ib-nama').value = judul; wrapper.querySelector('#pop-ib-jenis').value = d.jenis; const radInput = wrapper.querySelector('#pop-ib-rad'); const radVal = wrapper.querySelector('#pop-ib-rad-val'); if(radInput) { radInput.value = d.radius_meter; if(radVal) radVal.innerText = d.radius_meter; radInput.oninput = function() { radVal.innerText = this.value; }; } wrapper.querySelector('#pop-ib-alamat').value = d.alamat; const btnSimpan = wrapper.querySelector('#btn-pop-simpan'); if(btnSimpan) { btnSimpan.innerText = "Update"; btnSimpan.disabled = false; btnSimpan.onclick = () => updateIbadahPopup(d.id, popup, wrapper); } }; window.updateIbadahPopup = (id, popupRef, wrapper) => { const payload = { id: id, nama: wrapper.querySelector('#pop-ib-nama').value, jenis: wrapper.querySelector('#pop-ib-jenis').value, radius: parseInt(wrapper.querySelector('#pop-ib-rad').value) }; safeFetch('otak_kemiskinan/api_update_ibadah.php', payload) .then(() => { map.closePopup(popupRef); resetAllModes(); triggerToastNotification("✅ Diperbarui."); muatDataSemua(); }) .catch(err => alert("Gagal: " + err)); }; window.eksekusiHapusIbadah = (id) => { safeFetch('otak_kemiskinan/api_hapus_ibadah.php', { id: id }) .then(() => { triggerToastNotification("🗑️ Dihapus."); muatDataSemua(); }) .catch(err => alert(err)); }; // ==================================================== // LIFECYCLE AWAL (PROMISE ALL & GATEKEEPER) // ==================================================== window.muatDataSemua = () => { Promise.all([ safeFetch('otak_spbu/api_tampil_spbu.php') .then(d => { dataStore.spbu = {}; d.forEach(x => dataStore.spbu[x.id] = x); renderSPBU(); }) .catch(e => console.warn("API SPBU Offline")), safeFetch('otak_jalan/api_tampil_jalan.php') .then(d => { dataStore.jalan = {}; d.forEach(x => dataStore.jalan[x.id] = x); renderJalan(); }) .catch(e => console.warn("API Jalan Offline")), safeFetch('otak_tanah/api_tampil_tanah.php') .then(d => { dataStore.tanah = {}; d.forEach(x => dataStore.tanah[x.id] = x); renderTanah(); }) .catch(e => console.warn("API Tanah Offline")), safeFetch('otak_kemiskinan/api_tampil_ibadah.php') .then(d => { dataStore.ibadah = {}; d.forEach(x => dataStore.ibadah[x.id] = x); }) .catch(e => console.warn("API Ibadah Offline")), safeFetch('otak_kemiskinan/api_tampil_penduduk.php') .then(d => { dataStore.penduduk = {}; d.forEach(x => dataStore.penduduk[x.id] = x); }) .catch(e => console.warn("API Penduduk Offline")), safeFetch('otak_kemiskinan/api_tampil_pelatihan.php') .then(d => { dataStore.pelatihan = Array.isArray(d) ? d : []; }) .catch(e => console.warn("API Pelatihan Offline")), safeFetch('otak_kemiskinan/api_tampil_gariskemiskinan.php') .then(d => { if(d && d.nilai_rupiah) { state.garisKemiskinan = parseInt(d.nilai_rupiah); const inputGaris = document.getElementById('input-garis-kemiskinan'); if(inputGaris) inputGaris.placeholder = `Saat ini: Rp${state.garisKemiskinan}`; } }) .catch(e => console.warn("API Garis Offline")), safeFetch('otak_kemiskinan/api_tampil_darurat.php') .then(d => { dataStore.statusDarurat = {}; if(Array.isArray(d)) { d.forEach(id => dataStore.statusDarurat[id] = true); } }) .catch(e => console.warn("API Darurat Offline")) ]).then(() => { renderKemiskinan(); console.log("✅ Sistem WebGIS V3 Siap dan Data Tersinkronisasi Penuh."); }); }; setTimeout(() => { map.invalidateSize(); pulihkanIngatanUI(); cekSesiBackend(); }, 500);