// --- Fitur Pemetaan Kemiskinan --- // Emoji per jenis ibadah const IBADAH_EMOJI_MAP = { 'Masjid': '🕌', 'Gereja': '⛪', 'Pura': '🛕', 'Vihara': '🪷', 'Kelenteng': '🏮', }; // Emoji Bubble Icon builder function makeIbadahIcon(jenis) { const emot = IBADAH_EMOJI_MAP[jenis] || '🕌'; return L.divIcon({ className: '', html: `
${emot}
`, iconSize: [38, 38], iconAnchor: [19, 38], popupAnchor: [0, -40] }); } function makeMiskinIcon(inRadius) { const cls = inRadius ? 'miskin-in' : 'miskin-out'; return L.divIcon({ className: '', html: `
🏠
`, iconSize: [38, 38], iconAnchor: [19, 38], popupAnchor: [0, -40] }); } let ibadahDataList = []; let miskinMarkerList = []; // Simpan referensi marker untuk update warna // Helper: cek apakah koordinat (lat, lng) berada dalam radius ibadah pengelola yang login function isInMyIbadahRadius(lat, lng) { if (!window.currentUser || window.currentUser.role !== 'pengelola') return true; // admin selalu bisa const myIbadahId = window.currentUser.ibadah_id; if (!myIbadahId) return false; const ib = ibadahDataList.find(i => i.id == myIbadahId); if (!ib) return false; const dist = L.latLng(lat, lng).distanceTo(L.latLng(ib.lat, ib.lng)); return dist <= ib.radius; } window.isInMyIbadahRadius = isInMyIbadahRadius; let isResizing = false; let resizingCircle = null; let resizingIbadah = null; map.on('mousemove', function(e) { if (isResizing && resizingCircle && resizingIbadah) { const center = resizingCircle.getLatLng(); const newRadius = center.distanceTo(e.latlng); resizingCircle.setRadius(newRadius); resizingIbadah.radius = newRadius; // Update data radius di marker juga rumahIbadahLayer.eachLayer(function(layer) { if (layer.ibadahData && layer.ibadahData.id === resizingIbadah.id && layer instanceof L.Marker) { layer.ibadahData.radius = newRadius; } }); updateSemuaWarnaMiskin(); } }); map.on('mouseup', function(e) { if (isResizing) { isResizing = false; map.dragging.enable(); if (resizingIbadah) { updateIbadah(resizingIbadah.id, resizingIbadah.nama, resizingIbadah.jenis, resizingIbadah.alamat, resizingIbadah.radius, resizingIbadah.lat, resizingIbadah.lng); } resizingCircle = null; resizingIbadah = null; } }); // --- Rumah Ibadah --- function loadRumahIbadah() { rumahIbadahLayer.clearLayers(); ibadahDataList = []; fetch('api/rumah_ibadah/read.php') .then(res => res.json()) .then(data => { if (data.status === 'success' && data.data) { ibadahDataList = data.data; data.data.forEach(item => { addIbadahMarker(item); }); updateSemuaWarnaMiskin(); } if (window.refreshActivePanel) window.refreshActivePanel(); }); } function addIbadahMarker(item) { const lat = parseFloat(item.lat); const lng = parseFloat(item.lng); const radius = parseFloat(item.radius) || 100; // fallback if invalid if (isNaN(lat) || isNaN(lng)) return; const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin'); const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: isAdmin }); // Lingkaran radius const circle = L.circle([lat, lng], { radius: radius, color: '#ff7800', weight: 2, fillColor: '#ff7800', fillOpacity: 0.2 }); circle.on('mousemove', function(e) { if (!isAdmin || isResizing) return; const center = circle.getLatLng(); const radius = circle.getRadius(); const dist = center.distanceTo(e.latlng); const metersPerPixel = map.distance(map.containerPointToLatLng([0,0]), map.containerPointToLatLng([0,1])); const tolerance = Math.max(radius * 0.1, metersPerPixel * 15); // toleransi 15px atau 10% if (Math.abs(dist - radius) <= tolerance) { if (circle._path) circle._path.style.cursor = 'ew-resize'; circle.nearEdge = true; } else { if (circle._path) circle._path.style.cursor = 'pointer'; circle.nearEdge = false; } }); circle.on('mousedown', function(e) { if (isAdmin && circle.nearEdge) { map.dragging.disable(); isResizing = true; resizingCircle = circle; resizingIbadah = item; L.DomEvent.stopPropagation(e); } }); marker.ibadahData = item; marker.circleLayer = circle; const d = item; const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌'; let actionButtons = ''; if (isAdmin) { actionButtons = `
`; } const popupContent = `

${emot} ${d.nama}

Jenis: ${d.jenis || 'Masjid'}

Alamat: ${d.alamat}

Radius: ${d.radius} m

${actionButtons}
`; marker.bindPopup(popupContent); marker.on('dragend', function(e) { const newPos = marker.getLatLng(); circle.setLatLng(newPos); item.lat = newPos.lat; item.lng = newPos.lng; // Update ke DB updateIbadah(item.id, item.nama, item.jenis, item.alamat, item.radius, newPos.lat, newPos.lng); }); rumahIbadahLayer.addLayer(circle); rumahIbadahLayer.addLayer(marker); } // Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta) map.on('contextmenu', function(e) { const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); if (!isAdminOrPengelola) return; const canAddHere = isInMyIbadahRadius(e.latlng.lat, e.latlng.lng); if (!canAddHere) { // Pengelola: lokasi di luar radius ibadahnya const warnContent = ` `; L.popup().setLatLng(e.latlng).setContent(warnContent).openOn(map); return; } const popupContent = ` `; L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map); }); // Map Click -> Form Add Rumah Ibadah map.on('click', function(e) { if (window.currentAddMode === 'rumah_ibadah') { const lat = e.latlng.lat; const lng = e.latlng.lng; // Reverse Geocoding fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`) .then(res => res.json()) .then(geoData => { const alamat = geoData.display_name || 'Alamat tidak ditemukan'; const bodyHTML = `
`; openModal("Tambah Rumah Ibadah", bodyHTML, function() { window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius'); }); window.deactivateAddMode(); }) .catch(err => { // Tetap buka modal meskipun gagal mendapatkan alamat const alamat = ''; const bodyHTML = `
`; openModal("Tambah Rumah Ibadah", bodyHTML, function() { window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius'); }); window.deactivateAddMode(); }); } }); window.saveNewIbadah = function(lat, lng, namaId, jenisId, alamatId, radiusId) { const nama = document.getElementById(namaId).value; const jenis = document.getElementById(jenisId).value; const alamat = document.getElementById(alamatId).value; const radius = document.getElementById(radiusId).value; if (!nama) { alert("Nama Rumah Ibadah harus diisi!"); return; } fetch('api/rumah_ibadah/create.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nama, jenis, alamat, radius, lat, lng }) }) .then(res => res.json()) .then(data => { if(data.status === 'success') { closeModal(); loadRumahIbadah(); } else { alert(data.message); } }); }; window.openEditIbadahModal = function(id) { let d = null; rumahIbadahLayer.eachLayer(function(layer) { if (layer.ibadahData && layer.ibadahData.id == id) d = layer.ibadahData; }); if (!d) return; const jenisOpts = ['Masjid','Gereja','Pura','Vihara','Kelenteng'].map(j => `` ).join(''); const bodyHTML = `
`; map.closePopup(); openModal("Edit Rumah Ibadah", bodyHTML, function() { window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahJenis', 'editIbadahAlamat', 'editIbadahRadius'); }); }; window.previewRadius = function(id, newRadius) { rumahIbadahLayer.eachLayer(function(layer) { // layer ini bisa marker atau circle, kita cek yang punya circleLayer (marker) if (layer.ibadahData && layer.ibadahData.id === id && layer.circleLayer) { layer.circleLayer.setRadius(parseFloat(newRadius)); layer.ibadahData.radius = parseFloat(newRadius); updateSemuaWarnaMiskin(); // Update warna miskin real-time } }); }; window.saveEditIbadah = function(id, lat, lng, namaId, jenisId, alamatId, radiusId) { const nama = document.getElementById(namaId).value; const jenis = document.getElementById(jenisId).value; const alamat = document.getElementById(alamatId).value; const radius = document.getElementById(radiusId).value; updateIbadah(id, nama, jenis, alamat, radius, lat, lng); }; function updateIbadah(id, nama, jenis, alamat, radius, lat, lng) { fetch('api/rumah_ibadah/update.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, nama, jenis, alamat, radius, lat, lng }) }).then(res => res.json()).then(data => { if(data.status === 'success') { if (typeof closeModal === 'function') closeModal(); map.closePopup(); loadRumahIbadah(); } }); } window.deleteIbadah = function(id) { openConfirmModal("Yakin hapus rumah ibadah ini?", function() { fetch('api/rumah_ibadah/delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }).then(res => res.json()).then(data => { if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); } }); }); }; // --- Penduduk Miskin --- function loadPendudukMiskin() { pendudukMiskinLayer.clearLayers(); miskinMarkerList = []; fetch('api/penduduk_miskin/read.php') .then(res => res.json()) .then(data => { if (data.status === 'success' && data.data) { data.data.forEach(item => { addMiskinMarker(item); }); updateSemuaWarnaMiskin(); } if (window.refreshActivePanel) window.refreshActivePanel(); }); } function addMiskinMarker(item) { const lat = parseFloat(item.lat); const lng = parseFloat(item.lng); if (isNaN(lat) || isNaN(lng)) return; // Pengelola hanya bisa manage marker yang ada di dalam radius ibadahnya const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola')); const canManage = isAdminOrPengelola && isInMyIbadahRadius(lat, lng); const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: canManage }); marker.miskinData = item; const d = item; let actionButtons = ''; if (canManage) { actionButtons = ` `; } let photoLinksHtml = ''; if (d.foto_rumah) { photoLinksHtml += `
Foto Rumah:
Foto Rumah ❌ Foto tidak dapat dimuat
`; } if (d.foto_kk) { photoLinksHtml += `
Foto KK:
Foto KK ❌ Foto tidak dapat dimuat
`; } const popupContent = `

🏠 ${d.nama}

Bantuan: ${d.kategori_bantuan}

Jiwa: ${d.jumlah_jiwa || '-'}

${photoLinksHtml}
${actionButtons}
`; marker.bindPopup(popupContent); marker.on('dragend', function(e) { const newPos = marker.getLatLng(); item.lat = newPos.lat; item.lng = newPos.lng; updateSemuaWarnaMiskin(); // Update warna segera // Simpan ke DB fetch('api/penduduk_miskin/update.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(item) }); }); miskinMarkerList.push(marker); pendudukMiskinLayer.addLayer(marker); } window.formAddMiskin = function(lat, lng) { map.closePopup(); const bodyHTML = `
`; openModal("Tambah Penduduk Miskin", bodyHTML, function() { window.saveNewMiskin(lat, lng); }); }; window.saveNewMiskin = function(lat, lng) { const nama = document.getElementById('miskinNama').value; const kategori_bantuan = document.getElementById('miskinKategori').value; const jumlah_jiwa = document.getElementById('miskinJiwa').value; const fotoRumahInput = document.getElementById('miskinFotoRumah'); const fotoKKInput = document.getElementById('miskinFotoKK'); if (!nama) { alert('Nama kepala keluarga harus diisi!'); return; } const formData = new FormData(); formData.append('nama', nama); formData.append('kategori_bantuan', kategori_bantuan); formData.append('jumlah_jiwa', jumlah_jiwa); formData.append('lat', lat); formData.append('lng', lng); if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]); if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]); fetch('api/penduduk_miskin/create.php', { method: 'POST', body: formData }) .then(res => res.json()) .then(data => { if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } else { alert(data.message); } }); }; window.openEditMiskinModal = function(id) { let d = null; pendudukMiskinLayer.eachLayer(function(layer) { if (layer.miskinData && layer.miskinData.id == id) d = layer.miskinData; }); if (!d) return; const bodyHTML = `
${d.foto_rumah ? `
Foto Rumah ❌ File gambar rusak atau tidak ditemukan
Upload baru untuk mengganti
` : ''}
${d.foto_kk ? `
Foto KK ❌ File gambar rusak atau tidak ditemukan
Upload baru untuk mengganti
` : ''}
`; map.closePopup(); openModal("Edit Penduduk Miskin", bodyHTML, function() { window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori', 'editMiskinJiwa'); }); }; window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) { const nama = document.getElementById(namaId).value; const kategori_bantuan = document.getElementById(kategoriId).value; const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1; const fotoRumahInput = document.getElementById('editMiskinFotoRumah'); const fotoKKInput = document.getElementById('editMiskinFotoKK'); const formData = new FormData(); formData.append('id', id); formData.append('nama', nama); formData.append('kategori_bantuan', kategori_bantuan); formData.append('jumlah_jiwa', jumlah_jiwa); formData.append('lat', lat); formData.append('lng', lng); if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]); if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]); fetch('api/penduduk_miskin/update.php', { method: 'POST', body: formData }).then(res => res.json()).then(data => { if(data.status === 'success') { closeModal(); loadPendudukMiskin(); } }); }; window.deleteMiskin = function(id) { openConfirmModal("Yakin hapus penduduk miskin ini?", function() { fetch('api/penduduk_miskin/delete.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }).then(res => res.json()).then(data => { if(data.status === 'success') { map.closePopup(); loadPendudukMiskin(); } }); }); }; // Logika dinamis warna: Merah jika di dalam salah satu radius Rumah Ibadah, Hijau jika di luar function updateSemuaWarnaMiskin() { miskinMarkerList.forEach(marker => { let inRadius = false; const pLatlng = L.latLng(marker.miskinData.lat, marker.miskinData.lng); for (let i = 0; i < ibadahDataList.length; i++) { const ibadah = ibadahDataList[i]; const iLatlng = L.latLng(ibadah.lat, ibadah.lng); const dist = pLatlng.distanceTo(iLatlng); // meter if (dist <= ibadah.radius) { inRadius = true; break; // Jika sudah masuk satu radius, langsung merah } } if (inRadius) { marker.setIcon(makeMiskinIcon(true)); } else { marker.setIcon(makeMiskinIcon(false)); } }); } // ========================== // ===== Import Feature ===== // ========================== let parsedImportData = []; window.openImportMiskinModal = function() { parsedImportData = []; // Reset data const bodyHTML = `
Format CSV yang didukung:
Header minimal berisi: nama, kategori_bantuan (atau kategori), jumlah_jiwa, lat, lng.
Pemisah: Koma (,), Titik Koma (;), atau Tab.
`; map.closePopup(); openModal("Impor Penduduk Miskin", bodyHTML, function() { if (parsedImportData.length === 0) { showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning'); return; } // Kirim data ke API bulk_create.php fetch('api/penduduk_miskin/bulk_create.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(parsedImportData) }) .then(res => res.json()) .then(data => { if (data.status === 'success') { closeModal(); loadPendudukMiskin(); showToast(data.message, 'success'); } else { showToast(data.message || 'Gagal mengimpor data.', 'error'); } }) .catch(err => { console.error(err); showToast('Terjadi kesalahan saat menghubungi server.', 'error'); }); }); // Register file input listener document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange); }; function parseCSV(text) { const lines = text.split(/\r?\n/); if (lines.length < 2) return []; const firstLine = lines[0]; let delimiter = ','; if (firstLine.includes(';')) delimiter = ';'; else if (firstLine.includes('\t')) delimiter = '\t'; const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase()); const results = []; for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; let fields = []; let currentField = ''; let insideQuote = false; for (let j = 0; j < line.length; j++) { const char = line[j]; if (char === '"' || char === "'") { insideQuote = !insideQuote; } else if (char === delimiter && !insideQuote) { fields.push(currentField.trim()); currentField = ''; } else { currentField += char; } } fields.push(currentField.trim()); fields = fields.map(f => f.replace(/^["']|["']$/g, '')); const row = {}; headers.forEach((header, index) => { row[header] = fields[index] || ''; }); results.push(row); } return results; } function handleImportFileChange(e) { const file = e.target.files[0]; const previewContainer = document.getElementById('importPreviewContainer'); const previewCount = document.getElementById('importPreviewCount'); const previewTableBody = document.getElementById('importPreviewTableBody'); const errorMsg = document.getElementById('importErrorMsg'); if (!file) { previewContainer.style.display = 'none'; parsedImportData = []; return; } errorMsg.style.display = 'none'; previewContainer.style.display = 'none'; previewTableBody.innerHTML = ''; parsedImportData = []; const reader = new FileReader(); reader.onload = function(event) { try { const text = event.target.result; let rawData = []; if (file.name.endsWith('.csv')) { rawData = parseCSV(text); } else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) { const json = JSON.parse(text); if (json.type === 'FeatureCollection' && Array.isArray(json.features)) { rawData = json.features.map(f => { const props = f.properties || {}; const coords = f.geometry && f.geometry.coordinates; return { nama: props.nama || props.name || props.penduduk || '', kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan', jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1, lat: coords ? coords[1] : (props.lat || props.latitude), lng: coords ? coords[0] : (props.lng || props.longitude) }; }); } else if (Array.isArray(json)) { rawData = json; } else { throw new Error("Format JSON tidak didukung."); } } if (rawData.length === 0) { errorMsg.textContent = "File kosong atau format tidak didukung."; errorMsg.style.display = 'block'; return; } const validRows = []; let invalidCount = 0; rawData.forEach(row => { const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || ''; const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan'; const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1; const lat = parseFloat(row.lat || row.latitude || row.y); const lng = parseFloat(row.lng || row.longitude || row.long || row.x); const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== ''; if (isValid) { validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng }); } else { invalidCount++; } }); parsedImportData = validRows; if (validRows.length === 0) { errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama_kk/nama, lat, dan lng terisi)."; errorMsg.style.display = 'block'; return; } validRows.forEach(row => { const tr = document.createElement('tr'); tr.style.borderBottom = '1px solid #e2e8f0'; tr.innerHTML = ` ${row.nama} ${row.kategori_bantuan} ${row.jumlah_jiwa} ${row.lat.toFixed(5)}, ${row.lng.toFixed(5)} `; previewTableBody.appendChild(tr); }); previewCount.textContent = validRows.length; previewContainer.style.display = 'block'; const statusEl = document.getElementById('importPreviewStatus'); if (statusEl) { if (invalidCount > 0) { statusEl.innerHTML = ` ${invalidCount} baris tidak valid diabaikan`; } else { statusEl.innerHTML = ` Semua data valid`; } } } catch (err) { console.error(err); errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai."; errorMsg.style.display = 'block'; } }; reader.readAsText(file); } // Initial Load loadRumahIbadah(); loadPendudukMiskin();