// Globals let map; let layerRI, layerPM, layerRadius; let riData = []; let pmData = []; let pickingMode = null; // 'ri' or 'pm' let tempMarker = null; let currentView = 'dashboard'; let searchTerm = ''; const rumahIbadahIconConfig = { Masjid: { icon: 'fa-mosque', className: 'mosque' }, Gereja: { icon: 'fa-church', className: 'church' }, Pura: { icon: 'fa-gopuram', className: 'temple' }, Vihara: { icon: 'fa-vihara', className: 'vihara' }, Klenteng: { icon: 'fa-torii-gate', className: 'klenteng' }, Lainnya: { icon: 'fa-place-of-worship', className: 'other' } }; function getRumahIbadahIcon(jenis) { const config = rumahIbadahIconConfig[jenis] || rumahIbadahIconConfig.Lainnya; return L.divIcon({ className: 'ri-div-icon', html: `
`, iconSize: [36, 46], iconAnchor: [18, 43], popupAnchor: [0, -43] }); } // PM Icons are now circleMarkers generated inline in renderPM document.addEventListener('DOMContentLoaded', async () => { await initMap(); initUI(); loadData(); }); async function initMap() { map = L.map('map', { zoomControl: false, preferCanvas: true }).setView([-0.060547, 109.344902], 13); window.map = map; L.control.zoom({ position: 'bottomright' }).addTo(map); const tileOptions = { keepBuffer: 6, updateWhenIdle: false, updateWhenZooming: false, updateInterval: 100 }; const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { ...tileOptions, attribution: '© OpenStreetMap contributors' }); const satelit = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { ...tileOptions, attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' }); const darkMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { ...tileOptions, attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 20 }); osm.addTo(map); const baseMaps = { "OSM Standard": osm, "Citra Satelit": satelit, "Dark Mode": darkMap }; layerRadius = L.featureGroup().addTo(map); layerRI = L.featureGroup().addTo(map); layerPM = L.featureGroup().addTo(map); const overlays = { "Radius Layanan": layerRadius, "Rumah Ibadah": layerRI, "Penduduk Miskin": layerPM }; L.control.layers(baseMaps, overlays, { position: 'topright' }).addTo(map); // Map Click for Picking Location map.on('click', async (e) => { if (!pickingMode) return; const lat = e.latlng.lat; const lng = e.latlng.lng; if (tempMarker) map.removeLayer(tempMarker); tempMarker = L.marker([lat, lng]).addTo(map); if (pickingMode === 'ri') { document.getElementById('ri-lat').value = lat.toFixed(6); document.getElementById('ri-lng').value = lng.toFixed(6); document.getElementById('ri-alamat').value = 'Mencari alamat...'; document.getElementById('ri-alamat').value = await reverseGeocode(lat, lng); document.getElementById('modal-ri').classList.add('show'); } else if (pickingMode === 'pm') { document.getElementById('pm-lat').value = lat.toFixed(6); document.getElementById('pm-lng').value = lng.toFixed(6); document.getElementById('pm-alamat').value = 'Mencari alamat...'; document.getElementById('pm-alamat').value = await reverseGeocode(lat, lng); document.getElementById('modal-pm').classList.add('show'); } pickingMode = null; if(tempMarker) map.removeLayer(tempMarker); }); } function initUI() { // Sidebar Toggle document.getElementById('toggle-sidebar').addEventListener('click', () => { const sidebar = document.getElementById('sidebar'); if (window.innerWidth <= 768) { sidebar.classList.toggle('open'); } else { sidebar.classList.toggle('collapsed'); } }); document.querySelectorAll('.menu-item[data-view]').forEach(item => { item.addEventListener('click', () => switchView(item.dataset.view)); }); // Menus const openAddRI = () => { switchView('map'); document.getElementById('form-ri').reset(); document.getElementById('ri-id').value = ''; document.getElementById('modal-ri-title').innerText = 'Tambah Rumah Ibadah'; document.getElementById('modal-ri').classList.add('show'); }; document.getElementById('menu-add-ri').addEventListener('click', openAddRI); document.getElementById('fab-add-ri')?.addEventListener('click', openAddRI); const openAddPM = () => { switchView('map'); document.getElementById('form-pm').reset(); document.getElementById('pm-id').value = ''; document.getElementById('anggota-container').innerHTML = ''; document.getElementById('bantuan-fields').style.display = 'none'; document.getElementById('modal-pm-title').innerText = 'Tambah Penduduk Miskin'; document.getElementById('modal-pm').classList.add('show'); }; document.getElementById('menu-add-pm').addEventListener('click', openAddPM); document.getElementById('fab-add-pm')?.addEventListener('click', openAddPM); document.getElementById('dashboard-add-ri')?.addEventListener('click', openAddRI); document.getElementById('dashboard-add-pm')?.addEventListener('click', openAddPM); document.getElementById('dashboard-open-map')?.addEventListener('click', () => switchView('map')); // Filters document.getElementById('filter-status').addEventListener('change', renderMapLayers); document.getElementById('filter-jenis').addEventListener('change', renderMapLayers); document.getElementById('filter-kebutuhan').addEventListener('change', renderPM); // Search document.getElementById('search-input').addEventListener('input', () => { searchTerm = getSearchTerm(); renderMapLayers(); }); document.getElementById('search-input').addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); applyMapSearch(true); } }); document.getElementById('search-button').addEventListener('click', () => applyMapSearch(true)); // Location document.getElementById('btn-my-location').addEventListener('click', () => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( (position) => { const lat = position.coords.latitude; const lng = position.coords.longitude; map.flyTo([lat, lng], 16); L.marker([lat, lng]).addTo(map).bindPopup('Lokasi Anda').openPopup(); }, (err) => alert('Gagal mendapatkan lokasi: ' + err.message) ); } else { alert('Geolocation tidak didukung browser ini.'); } }); switchView(currentView); } function switchView(view) { currentView = view; const mainContent = document.querySelector('.main-content'); document.querySelectorAll('.view-panel').forEach(panel => { panel.classList.toggle('active', panel.id === `${view}-view`); }); document.querySelectorAll('.menu-item[data-view]').forEach(item => { item.classList.toggle('active', item.dataset.view === view); }); mainContent?.classList.toggle('map-mode', view === 'map'); mainContent?.classList.toggle('dashboard-mode', view !== 'map'); document.querySelector('.search-bar')?.classList.toggle('hidden', view !== 'map'); document.querySelector('.map-header-controls')?.classList.toggle('hidden', view !== 'map'); if (window.innerWidth <= 768) { document.getElementById('sidebar').classList.remove('open'); } if (view === 'map' && map) { refreshMapSize(); } } function refreshMapSize() { [0, 100, 300].forEach(delay => { setTimeout(() => map.invalidateSize({ pan: false }), delay); }); } function getSearchTerm() { return document.getElementById('search-input').value.trim().toLowerCase(); } function textIncludes(value, term) { return String(value || '').toLowerCase().includes(term); } function matchesRI(ri) { if (!searchTerm) return true; return [ ri.nama, ri.jenis, ri.kontak, ri.alamat ].some(value => textIncludes(value, searchTerm)); } function matchesPM(pm) { if (!searchTerm) return true; const anggotaMatch = Array.isArray(pm.anggota) && pm.anggota.some(anggota => [ anggota.nama, anggota.hubungan, anggota.pekerjaan ].some(value => textIncludes(value, searchTerm))); return anggotaMatch || [ pm.kk_nama, pm.kk_nik, pm.alamat, pm.status_bantuan, pm.jenis_bantuan, pm.kategori_kebutuhan, pm.deskripsi_kebutuhan, pm.tindak_lanjut, pm.kondisi_rumah, pm.rumah_ibadah_nama ].some(value => textIncludes(value, searchTerm)); } function getFilteredRI() { const filterJenis = document.getElementById('filter-jenis').value; return riData.filter(ri => { if (filterJenis !== 'all' && ri.jenis !== filterJenis) return false; return matchesRI(ri); }); } function getFilteredPM() { const filterStatus = document.getElementById('filter-status').value; const filterKebutuhan = document.getElementById('filter-kebutuhan').value; return pmData.filter(pm => { if (filterStatus !== 'all' && pm.status_bantuan !== filterStatus) return false; if (filterKebutuhan !== 'all' && pm.kategori_kebutuhan !== filterKebutuhan) return false; return matchesPM(pm); }); } function renderMapLayers() { renderRI(); renderPM(); } function applyMapSearch(focusResult = false) { searchTerm = getSearchTerm(); renderMapLayers(); if (focusResult) { switchView('map'); focusSearchResults(); } } function focusSearchResults() { const results = [ ...getFilteredRI().map(item => [parseFloat(item.lat), parseFloat(item.lng)]), ...getFilteredPM().map(item => [parseFloat(item.lat), parseFloat(item.lng)]) ].filter(([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng)); if (!searchTerm || results.length === 0) { alert(searchTerm ? 'Data tidak ditemukan di peta.' : 'Masukkan kata kunci pencarian.'); return; } if (results.length === 1) { map.flyTo(results[0], 16); return; } map.fitBounds(L.latLngBounds(results), { padding: [40, 40], maxZoom: 16 }); } function pickLocation(mode) { switchView('map'); pickingMode = mode; closeModal('modal-' + mode); } function closeModal(id) { document.getElementById(id).classList.remove('show'); } async function reverseGeocode(lat, lng) { try { const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`); const data = await res.json(); return data.display_name || 'Alamat tidak ditemukan'; } catch (e) { return 'Gagal memuat alamat'; } } function toggleBantuanFields() { const status = document.getElementById('pm-status').value; document.getElementById('bantuan-fields').style.display = status === 'Sudah dibantu' ? 'block' : 'none'; } function addAnggotaField(data = null) { const container = document.getElementById('anggota-container'); const id = Date.now() + Math.floor(Math.random() * 100); const html = `
`; container.insertAdjacentHTML('beforeend', html); } // Data Fetching and Rendering async function loadData() { await loadStats(); await fetchRI(); await fetchPM(); } async function loadStats() { try { const res = await fetch('api/dashboard_stats.php'); const json = await res.json(); if(json.success) { document.getElementById('stat-ri').innerText = json.data.total_rumah_ibadah; document.getElementById('stat-pm').innerText = json.data.total_penduduk; document.getElementById('stat-sudah').innerText = json.data.sudah_dibantu; document.getElementById('stat-belum').innerText = json.data.belum_dibantu; document.getElementById('stat-menunggu').innerText = json.data.menunggu_verifikasi || 0; document.getElementById('stat-prioritas').innerText = json.data.prioritas_tinggi || 0; document.getElementById('stat-luar-radius').innerText = json.data.luar_radius || 0; document.getElementById('summary-sudah').innerText = json.data.sudah_dibantu; document.getElementById('summary-belum').innerText = json.data.belum_dibantu; document.getElementById('summary-menunggu').innerText = json.data.menunggu_verifikasi || 0; renderDashboardLists(json.data); } } catch(e) { console.error(e); } } function renderDashboardLists(stats) { const priorityList = document.getElementById('priority-list'); const needList = document.getElementById('need-list'); if (priorityList) { const items = stats.top_prioritas || []; priorityList.innerHTML = items.length ? items.map(item => `
${item.skor_prioritas || 0}
${item.kk_nama}

${item.kategori_kebutuhan || 'Kebutuhan belum diisi'} - ${item.tindak_lanjut || 'Belum disurvei'}

`).join('') : '

Belum ada data prioritas.

'; } if (needList) { const items = stats.kebutuhan_terbanyak || []; needList.innerHTML = items.length ? items.map(item => `
${item.kategori}

${item.total} keluarga membutuhkan kategori ini

`).join('') : '

Belum ada data kebutuhan.

'; } } async function fetchRI() { try { const res = await fetch('api/rumah_ibadah_read.php'); const json = await res.json(); if(json.success) { riData = json.data; renderRI(); } } catch(e) { console.error(e); } } async function fetchPM() { try { const res = await fetch('api/penduduk_read.php'); const json = await res.json(); if(json.success) { pmData = json.data; renderPM(); } } catch(e) { console.error(e); } } function renderRI() { layerRI.clearLayers(); layerRadius.clearLayers(); getFilteredRI().forEach(ri => { const lat = parseFloat(ri.lat); const lng = parseFloat(ri.lng); const rad = parseInt(ri.radius) || 500; // Circle L.circle([lat, lng], { color: '#4f46e5', fillColor: '#4f46e5', fillOpacity: 0.1, radius: rad, interactive: false }).addTo(layerRadius); // Marker const marker = L.marker([lat, lng], { icon: getRumahIbadahIcon(ri.jenis) }).addTo(layerRI); marker.feature = ri; let popup = ` `; marker.bindPopup(popup); }); layerRadius.eachLayer(circle => circle.bringToBack()); } function renderPM() { layerPM.clearLayers(); getFilteredPM().forEach(pm => { const lat = parseFloat(pm.lat); const lng = parseFloat(pm.lng); const anggotaCount = pm.anggota ? pm.anggota.length : 0; let badgeClass = 'badge-danger'; if(pm.status_bantuan === 'Sudah dibantu') badgeClass = 'badge-success'; else if(pm.status_bantuan === 'Menunggu verifikasi') badgeClass = 'badge-warning'; const isBlankSpot = pm.is_blank_spot == 1; const pmColor = isBlankSpot ? '#ef4444' : '#10b981'; // Merah (Blank Spot), Hijau (Terjangkau) const riwayatHtml = Array.isArray(pm.riwayat_bantuan) && pm.riwayat_bantuan.length ? pm.riwayat_bantuan.slice(0, 3).map(r => `${r.tanggal_bantuan}: ${r.jenis_bantuan}`).join('
') : '-'; const buktiHtml = pm.bukti_file ? `Lihat bukti` : '-'; const marker = L.circleMarker([lat, lng], { radius: 7, fillColor: pmColor, color: '#ffffff', weight: 1.5, opacity: 1, fillOpacity: 0.9 }).addTo(layerPM); marker.feature = pm; let popup = ` `; marker.bindPopup(popup); }); layerPM.bringToFront(); } // CRUD Operations async function saveRI() { const id = document.getElementById('ri-id').value; const data = { nama: document.getElementById('ri-nama').value, jenis: document.getElementById('ri-jenis').value, kontak: document.getElementById('ri-kontak').value, lat: document.getElementById('ri-lat').value, lng: document.getElementById('ri-lng').value, alamat: document.getElementById('ri-alamat').value, radius: document.getElementById('ri-radius').value }; if(!data.nama || !data.lat) { alert("Isi form dengan lengkap"); return; } const url = id ? 'api/rumah_ibadah_update.php' : 'api/rumah_ibadah_create.php'; if(id) data.id = id; try { const res = await fetch(url, { method: 'POST', body: JSON.stringify(data) }); const json = await res.json(); if(json.success) { closeModal('modal-ri'); await fetchRI(); await fetchPM(); await loadStats(); } else { alert(json.message); } } catch(e) { alert('Error saving data'); } } async function savePM() { const id = document.getElementById('pm-id').value; const data = new FormData(); const anggota = []; data.append('kk_nama', document.getElementById('pm-nama').value); data.append('kk_nik', document.getElementById('pm-nik').value); data.append('lat', document.getElementById('pm-lat').value); data.append('lng', document.getElementById('pm-lng').value); data.append('alamat', document.getElementById('pm-alamat').value); data.append('kategori_kebutuhan', document.getElementById('pm-kategori-kebutuhan').value); data.append('deskripsi_kebutuhan', document.getElementById('pm-deskripsi-kebutuhan').value); data.append('kondisi_rumah', document.getElementById('pm-kondisi-rumah').value); data.append('penghasilan', document.getElementById('pm-penghasilan').value); data.append('status_bantuan', document.getElementById('pm-status').value); data.append('jenis_bantuan', document.getElementById('pm-jenis-bantuan').value); data.append('tanggal_bantuan', document.getElementById('pm-tanggal-bantuan').value); data.append('tindak_lanjut', document.getElementById('pm-tindak-lanjut').value); data.append('catatan_survei', document.getElementById('pm-catatan-survei').value); data.append('existing_bukti_file', document.getElementById('pm-existing-bukti-file').value); const buktiFile = document.getElementById('pm-bukti-file').files[0]; if (buktiFile) data.append('bukti_file', buktiFile); if(!data.get('kk_nama') || !data.get('lat')) { alert("Isi form dengan lengkap"); return; } if (data.get('penghasilan') !== '' && Number(data.get('penghasilan')) < 0) { alert('Penghasilan tidak boleh bernilai negatif.'); document.getElementById('pm-penghasilan').focus(); return; } const items = document.querySelectorAll('.dynamic-item'); items.forEach(el => { anggota.push({ nama: el.querySelector('.a-nama').value, hubungan: el.querySelector('.a-hubungan').value, umur: el.querySelector('.a-umur').value, pekerjaan: el.querySelector('.a-pekerjaan').value }); }); data.append('anggota', JSON.stringify(anggota)); const url = id ? 'api/penduduk_update.php' : 'api/penduduk_create.php'; if(id) data.append('id', id); try { const res = await fetch(url, { method: 'POST', body: data }); const json = await res.json(); if(json.success) { closeModal('modal-pm'); await fetchPM(); await loadStats(); if(json.ibadah_id) { const ri = riData.find(r => r.id == json.ibadah_id); if(ri) alert(`Warga otomatis dimasukkan ke binaan: ${ri.nama}`); } } else { alert(json.message); } } catch(e) { alert('Error saving data'); } } function editRI(data) { document.getElementById('ri-id').value = data.id; document.getElementById('ri-nama').value = data.nama; document.getElementById('ri-jenis').value = data.jenis; document.getElementById('ri-kontak').value = data.kontak; document.getElementById('ri-lat').value = data.lat; document.getElementById('ri-lng').value = data.lng; document.getElementById('ri-alamat').value = data.alamat; document.getElementById('ri-radius').value = data.radius; document.getElementById('modal-ri-title').innerText = 'Edit Rumah Ibadah'; document.getElementById('modal-ri').classList.add('show'); } function editPM(data) { document.getElementById('pm-id').value = data.id; document.getElementById('pm-nama').value = data.kk_nama; document.getElementById('pm-nik').value = data.kk_nik; document.getElementById('pm-lat').value = data.lat; document.getElementById('pm-lng').value = data.lng; document.getElementById('pm-alamat').value = data.alamat; document.getElementById('pm-kategori-kebutuhan').value = data.kategori_kebutuhan || ''; document.getElementById('pm-deskripsi-kebutuhan').value = data.deskripsi_kebutuhan || ''; document.getElementById('pm-kondisi-rumah').value = data.kondisi_rumah || ''; document.getElementById('pm-penghasilan').value = data.penghasilan || ''; document.getElementById('pm-status').value = data.status_bantuan; document.getElementById('pm-jenis-bantuan').value = data.jenis_bantuan; document.getElementById('pm-tanggal-bantuan').value = data.tanggal_bantuan; document.getElementById('pm-tindak-lanjut').value = data.tindak_lanjut || 'Belum disurvei'; document.getElementById('pm-catatan-survei').value = data.catatan_survei || ''; document.getElementById('pm-existing-bukti-file').value = data.bukti_file || ''; document.getElementById('pm-bukti-file').value = ''; toggleBantuanFields(); document.getElementById('anggota-container').innerHTML = ''; if(data.anggota && data.anggota.length > 0) { data.anggota.forEach(a => addAnggotaField(a)); } document.getElementById('modal-pm-title').innerText = 'Edit Penduduk Miskin'; document.getElementById('modal-pm').classList.add('show'); } function setBantuanAlert(message = '', type = 'error') { const alertBox = document.getElementById('bantuan-alert'); if (!alertBox) return; alertBox.textContent = message; alertBox.hidden = !message; alertBox.classList.toggle('success', type === 'success'); } function openBantuanModal(id) { const penerima = pmData.find(pm => String(pm.id) === String(id)); const form = document.getElementById('form-bantuan'); form.reset(); setBantuanAlert(); document.getElementById('bantuan-penduduk-id').value = id; document.getElementById('bantuan-penerima').value = penerima ? `Keluarga ${penerima.kk_nama}` : `ID ${id}`; document.getElementById('bantuan-jenis').value = penerima?.kategori_kebutuhan || ''; document.getElementById('bantuan-tanggal').value = new Date().toISOString().slice(0, 10); document.getElementById('bantuan-pemberi').value = 'Admin'; document.getElementById('bantuan-bukti-file').value = ''; document.getElementById('btn-save-bantuan').disabled = false; document.getElementById('modal-bantuan').classList.add('show'); } async function saveBantuan() { const id = document.getElementById('bantuan-penduduk-id').value; const jenis = document.getElementById('bantuan-jenis').value.trim(); const tanggal = document.getElementById('bantuan-tanggal').value; const nilai = document.getElementById('bantuan-nilai').value; const pemberi = document.getElementById('bantuan-pemberi').value.trim() || 'Admin'; const catatan = document.getElementById('bantuan-catatan').value.trim(); const buktiFile = document.getElementById('bantuan-bukti-file').files[0]; const saveButton = document.getElementById('btn-save-bantuan'); if (!id || !jenis || !tanggal) { setBantuanAlert('Keluarga penerima, jenis bantuan, dan tanggal wajib diisi.'); return; } const formData = new FormData(); formData.append('penduduk_id', id); formData.append('jenis_bantuan', jenis); formData.append('tanggal_bantuan', tanggal); formData.append('nilai_bantuan', nilai || ''); formData.append('pemberi_bantuan', pemberi); formData.append('catatan', catatan || ''); if (buktiFile) formData.append('bukti_file', buktiFile); try { saveButton.disabled = true; saveButton.innerText = 'Menyimpan...'; const res = await fetch('api/bantuan_create.php', { method: 'POST', body: formData }); const json = await res.json(); if (json.success) { closeModal('modal-bantuan'); await fetchPM(); await loadStats(); renderMapLayers(); } else { setBantuanAlert(json.message || 'Gagal menambah bantuan.'); } } catch(e) { setBantuanAlert('Terjadi kesalahan saat menyimpan bantuan.'); } finally { saveButton.disabled = false; saveButton.innerText = 'Simpan Bantuan'; } } async function deleteRI(id) { if(!confirm('Hapus rumah ibadah ini?')) return; try { const res = await fetch('api/rumah_ibadah_delete.php', { method:'POST', body:JSON.stringify({id}) }); const json = await res.json(); if(json.success) { fetchRI(); fetchPM(); loadStats(); } } catch(e) { alert('Error'); } } async function deletePM(id) { if(!confirm('Hapus penduduk miskin ini?')) return; try { const res = await fetch('api/penduduk_delete.php', { method:'POST', body:JSON.stringify({id}) }); const json = await res.json(); if(json.success) { fetchPM(); loadStats(); } } catch(e) { alert('Error'); } }