// modules/ibadah.js — Manajemen Rumah Ibadah dengan Radius Buffer & Reverse Geocoding (function () { const layerGroup = L.layerGroup(); window._ibadahLayer = layerGroup; // Data list: [{id, lat, lng, nama, jenis, radius}] — dibaca oleh penduduk.js window._ibadahList = []; let allMarkers = {}; // { id: { marker, circle } } let tempMarker = null; let _previewCircle = null; let active = false; let bufferVisible = true; // ── Icon & Color Factory ────────────────────────────────────────── const JENIS_ICON = { 'Masjid' : '🕌', 'Mushola' : '🕌', 'Gereja' : '⛪', 'Pura' : '🛕', 'Vihara' : '🏯', 'Klenteng' : '🏮', 'Lainnya' : '🕍', }; // Warna border circle per jenis (sesuai PRD F2) const JENIS_COLOR = { 'Masjid' : { stroke: '#38bdf8', fill: '#0ea5e9' }, // biru 'Mushola' : { stroke: '#67e8f9', fill: '#06b6d4' }, // cyan 'Gereja' : { stroke: '#4ade80', fill: '#16a34a' }, // hijau 'Pura' : { stroke: '#f97316', fill: '#ea580c' }, // oranye 'Vihara' : { stroke: '#facc15', fill: '#ca8a04' }, // kuning 'Klenteng' : { stroke: '#f43f5e', fill: '#be123c' }, // merah 'Lainnya' : { stroke: '#a78bfa', fill: '#7c3aed' }, // ungu }; const OPERATOR_HIGHLIGHT = { stroke: '#f59e0b', fill: '#fbbf24', ring: 'rgba(245,158,11,0.22)', shadow: 'rgba(180,83,9,0.35)' }; function isOwnOperatorIbadah(id) { const appUser = window.APP_USER || {}; if (!appUser.isOp || appUser.ibadahId === null || appUser.ibadahId === undefined) return false; return parseInt(appUser.ibadahId) === parseInt(id); } // ── KK list helper (called on popupopen & after setPopupContent) ── function _refreshKkList(id) { const bodyEl = document.getElementById('kkListBody_' + id); const countEl = document.getElementById('kkCount_' + id); if (!bodyEl) return; const list = (window._pendudukAll || window._pendudukList || []).filter(p => p.ibadah && parseInt(p.ibadah.id) === parseInt(id)); // Update live count (overrides stale DB value from initial load) if (countEl) { const totalJiwa = list.reduce((s, p) => s + (parseInt(p.jumlah_jiwa) || 1), 0); countEl.innerHTML = `${list.length} KK  ·  ${totalJiwa} jiwa`; } if (list.length === 0) { bodyEl.innerHTML = 'Belum ada warga binaan.'; return; } const MAX = 8; let html = list.slice(0, MAX).map(p => `
${escapeHTML(p.nama_kk || '(Anonim)')} ${p.jumlah_jiwa || 1} jiwa
`).join(''); if (list.length > MAX) { html += `
+${list.length - MAX} lainnya
`; } bodyEl.innerHTML = html; } function createIbadahIcon(jenis, ownOperatorIbadah = false) { const emoji = JENIS_ICON[jenis] || '🕍'; const size = ownOperatorIbadah ? 46 : 36; const border = ownOperatorIbadah ? `4px solid ${OPERATOR_HIGHLIGHT.stroke}` : '2px solid rgba(56,189,248,0.8)'; const background = ownOperatorIbadah ? 'linear-gradient(135deg, #fffbeb, #fef3c7)' : 'linear-gradient(135deg, #ffffff, #f0f9ff)'; const shadow = ownOperatorIbadah ? `0 0 0 8px ${OPERATOR_HIGHLIGHT.ring}, 0 8px 22px ${OPERATOR_HIGHLIGHT.shadow}` : '0 3px 12px rgba(14,165,233,0.45)'; const fontSize = ownOperatorIbadah ? 18 : 16; return L.divIcon({ className: '', html: `
${emoji}
`, iconSize: [size, size], iconAnchor: ownOperatorIbadah ? [12, 46] : [8, 36], popupAnchor: ownOperatorIbadah ? [10, -48] : [10, -38] }); } const iconTemp = L.divIcon({ className: '', html: `
`, iconSize: [32, 32], iconAnchor: [16, 16], }); // ── Tambah ke peta ──────────────────────────────────────────────── function addToMap(data) { const lat = parseFloat(data.lat); const lng = parseFloat(data.lng); const radius = parseInt(data.radius_m) || 500; const jenis = data.jenis || 'Lainnya'; const emoji = JENIS_ICON[jenis] || '🕍'; const ownOperatorIbadah = isOwnOperatorIbadah(data.id); // Lingkaran radius — warna berbeda per jenis const clr = JENIS_COLOR[jenis] || JENIS_COLOR['Lainnya']; const circle = L.circle([lat, lng], { radius: radius, color: ownOperatorIbadah ? OPERATOR_HIGHLIGHT.stroke : clr.stroke, weight: ownOperatorIbadah ? 3 : 1.5, opacity: ownOperatorIbadah ? 1 : 0.8, fillColor: ownOperatorIbadah ? OPERATOR_HIGHLIGHT.fill : clr.fill, fillOpacity: ownOperatorIbadah ? 0.12 : 0.07, }); const isAdmin = window.APP_USER && window.APP_USER.isAdmin; const marker = L.marker([lat, lng], { icon: createIbadahIcon(jenis, ownOperatorIbadah), draggable: isAdmin, zIndexOffset: ownOperatorIbadah ? 1000 : 100 }); marker._ibadahId = data.id; marker._ibadahNama = data.nama; marker._ibadahJenis = jenis; marker._ibadahRadius = radius; marker._circle = circle; // Drag: update posisi + recalculate penduduk marker.on('dragend', function (e) { const { lat: newLat, lng: newLng } = e.target.getLatLng(); circle.setLatLng([newLat, newLng]); // Update _ibadahList (parseInt to guard against string/number type mismatch) const entry = window._ibadahList.find(x => parseInt(x.id) === parseInt(data.id)); if (entry) { entry.lat = newLat; entry.lng = newLng; } const fd = new FormData(); fd.append('id', data.id); fd.append('lat', newLat.toFixed(8)); fd.append('lng', newLng.toFixed(8)); fetch('api/ibadah/update_posisi.php', { method: 'POST', body: appendCsrf(fd) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { showToast(`Posisi "${escapeHTML(data.nama)}" diperbarui.`); // Recalculate semua penduduk if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } } else throw new Error(j.message); }) .catch(err => showToast(err.message || 'Gagal update posisi.', 'error')); }); // Popup info marker.bindPopup(buildInfoPopup(data), { maxWidth: 340 }); // KK list: populate on popup open and after inline edits marker.on('popupopen', function () { _refreshKkList(data.id); }); if (bufferVisible) circle.addTo(layerGroup); marker.addTo(layerGroup); // Simpan data lengkap agar bisa di-update nanti allMarkers[data.id] = { marker, circle, data: { ...data, radius_m: radius } }; // Update global list untuk penduduk.js updateIbadahList(); } function buildInfoPopup(data) { const isAdmin = window.APP_USER && window.APP_USER.isAdmin; const ownOperatorIbadah = isOwnOperatorIbadah(data.id); const jenis = data.jenis || 'Lainnya'; const emoji = JENIS_ICON[jenis] || '🕍'; const r = parseInt(data.radius_m) || 500; const alamatHtml = data.alamat ? `
Alamat
${escapeHTML(data.alamat)}
` : ''; const kontakValue = data.kontak ? escapeHTML(data.kontak) : ''; const kontakHtml = (isAdmin || data.kontak) ? `
Kontak Pengurus
${isAdmin ? `
` : `
${kontakValue || '—'}
`}
` : ''; return `
${emoji}
${escapeHTML(data.nama)}
#${data.id} · ${escapeHTML(jenis)}
${ownOperatorIbadah ? `
Rumah ibadah Anda
Akun operator ini terhubung ke rumah ibadah tersebut.
` : ''} ${alamatHtml} ${kontakHtml} ${isAdmin ? `
Radius Wilayah
${r}m
100m5000m
` : `
Radius Wilayah
${r}m
`}
Warga Binaan (terverifikasi)
${parseInt(data.total_kk) || 0} KK  ·  ${parseInt(data.total_jiwa) || 0} jiwa
${parseInt(data.jiwa_terjangkau) > 0 || parseInt(data.jiwa_blankspot) > 0 ? `
● ${parseInt(data.jiwa_terjangkau)||0} terjangkau  ·  ● ${parseInt(data.jiwa_blankspot)||0} blank spot
` : ''}
Koordinat
${parseFloat(data.lat).toFixed(6)}, ${parseFloat(data.lng).toFixed(6)}
${(window.APP_USER && window.APP_USER.loggedIn) ? `
Daftar KK Binaan (klik untuk fokus)
Memuat...
` : `
Data detail warga hanya tersedia untuk petugas yang login.
Masuk →
`} ${(window.APP_USER && window.APP_USER.isAdmin) ? `
Export CSV Print PDF
` : ''} ${isAdmin ? `` : ''}
`; } // ── Update global list ──────────────────────────────────────────── function updateIbadahList() { window._ibadahList = Object.values(allMarkers).map(entry => { const m = entry.marker; const ll = m.getLatLng(); return { id: m._ibadahId, lat: ll.lat, lng: ll.lng, nama: m._ibadahNama, jenis: m._ibadahJenis, radius: m._ibadahRadius }; }); if (typeof window._statsUpdate === 'function') window._statsUpdate(); } // ── Load data ───────────────────────────────────────────────────── function loadData() { layerGroup.clearLayers(); allMarkers = {}; window._ibadahList = []; fetch('api/ibadah/ambil.php?_=' + Date.now(), { cache: 'no-store' }) .then(r => r.json()) .then(j => { if (j.status !== 'success') throw new Error(j.message); j.data.forEach(addToMap); updateIbadahList(); refreshList(j.data); // Setelah load ibadah, recalculate penduduk if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } }) .catch(err => { showToast('Gagal memuat data ibadah.', 'error'); console.error(err); }); } // ── Refresh panel list ──────────────────────────────────────────── function refreshList(data) { if (typeof window.dlRefreshList !== 'function') return; const items = (data || []).map(d => ({ id: d.id, name: d.nama, badge: d.jenis, badgeColor: '#a855f7', dotColor: '#a855f7', })); window.dlRefreshList('Ibadah', items); } // ── Hapus ───────────────────────────────────────────────────────── window._ibadahHapus = function (id, btnEl) { // Step 1: cek operator terkait (confirmed=0) const fd0 = new FormData(); fd0.append('id', id); fd0.append('confirmed', '0'); fetch('api/ibadah/hapus.php', { method: 'POST', body: appendCsrf(fd0) }) .then(r => r.json()) .then(j => { if (j.status === 'warning') { showDeleteConfirm(j.message + '\n\nLanjutkan hapus?').then(ok => { if (ok) _doHapusIbadah(id, btnEl); }); } else if (j.status === 'success') { _applyHapusIbadah(id); showToast('Rumah ibadah berhasil dihapus.'); } else { showToast(j.message, 'error'); } }) .catch(() => showToast('Gagal menghubungi server.', 'error')); }; function _doHapusIbadah(id, btnEl) { showDeleteConfirm('Yakin ingin menghapus permanen rumah ibadah ini?').then(ok => { if (!ok) return; btnEl.disabled = true; btnEl.innerHTML = ' Menghapus...'; lucide.createIcons(); const fd = new FormData(); fd.append('id', id); fd.append('confirmed', '1'); fetch('api/ibadah/hapus.php', { method: 'POST', body: appendCsrf(fd) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { map.closePopup(); _applyHapusIbadah(id); showToast('Rumah ibadah berhasil dihapus.'); if (typeof window._pendudukRecalcAll === 'function') window._pendudukRecalcAll(); } else { showToast(j.message, 'error'); btnEl.disabled = false; btnEl.innerHTML = ' Hapus Rumah Ibadah'; lucide.createIcons(); } }); }); } function _applyHapusIbadah(id) { if (allMarkers[id]) { layerGroup.removeLayer(allMarkers[id].marker); layerGroup.removeLayer(allMarkers[id].circle); delete allMarkers[id]; } updateIbadahList(); refreshList(Object.values(allMarkers).map(e => ({ id: e.marker._ibadahId, nama: e.marker._ibadahNama, jenis: e.marker._ibadahJenis, }))); } // ── Simpan perubahan kontak (dari popup info) ───────────────────── window._ibadahSimpanKontak = function (id) { const inputEl = document.getElementById('edit_kontak_' + id); const statusEl = document.getElementById('kontak_status_' + id); if (!inputEl) return; const kontak = inputEl.value.trim(); statusEl.innerHTML = ' Menyimpan...'; statusEl.style.color = 'var(--c-muted)'; lucide.createIcons(); const fd = new FormData(); fd.append('id', id); fd.append('kontak', kontak); fetch('api/ibadah/update_kontak.php', { method: 'POST', body: appendCsrf(fd) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { if (allMarkers[id]) { allMarkers[id].data.kontak = kontak; allMarkers[id].marker.setPopupContent(buildInfoPopup(allMarkers[id].data)); _refreshKkList(id); } showToast(`Kontak berhasil diperbarui.`); } else throw new Error(j.message); }) .catch(err => { statusEl.innerHTML = ' ' + escapeHTML(err.message); statusEl.style.color = 'var(--c-danger)'; lucide.createIcons(); }); }; // ── Simpan perubahan radius (dari popup info) ───────────────────── window._ibadahSimpanRadius = function (id) { const inputEl = document.getElementById('edit_radius_' + id); const statusEl = document.getElementById('radius_status_' + id); if (!inputEl) return; const radius = Math.max(100, Math.min(5000, parseInt(inputEl.value) || 500)); inputEl.value = radius; // normalize statusEl.innerHTML = ' Menyimpan...'; statusEl.style.color = 'var(--c-muted)'; lucide.createIcons(); const fd = new FormData(); fd.append('id', id); fd.append('radius_m', radius); fetch('api/ibadah/update_radius.php', { method: 'POST', body: appendCsrf(fd) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { if (allMarkers[id]) { allMarkers[id].circle.setRadius(radius); allMarkers[id].marker._ibadahRadius = radius; // Update objek data yang disimpan allMarkers[id].data.radius_m = radius; // Update isi popup agar saat dibuka lagi nilainya sudah yang baru allMarkers[id].marker.setPopupContent(buildInfoPopup(allMarkers[id].data)); _refreshKkList(id); } // Update _ibadahList const entry = window._ibadahList.find(x => parseInt(x.id) === id); if (entry) entry.radius = radius; // Recalculate semua penduduk dengan radius baru if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } // Alert jika ada warga baru yang jadi blank spot akibat radius diperkecil setTimeout(() => { const newBlankCount = (window._pendudukAll || window._pendudukList || []) .filter(p => !p.ibadah).length; if (newBlankCount > 0) { showToast( `⚠ Radius diperkecil — ${newBlankCount} warga kini menjadi Blank Spot.`, 'error' ); } if (typeof window._updateBlankSpotCounter === 'function') { window._updateBlankSpotCounter(); } }, 200); showToast(`Radius berhasil diubah ke ${radius}m.`); } else throw new Error(j.message); }) .catch(err => { statusEl.innerHTML = ' ' + escapeHTML(err.message); statusEl.style.color = 'var(--c-danger)'; lucide.createIcons(); }); }; // ── Klik peta: Form tambah ibadah ───────────────────────────────── function onMapClick(e) { if (typeof activeTool === 'undefined' || activeTool !== 'ibadah') return; const lat = e.latlng.lat.toFixed(8); const lng = e.latlng.lng.toFixed(8); if (tempMarker) map.removeLayer(tempMarker); tempMarker = L.marker([lat, lng], { icon: iconTemp }).addTo(map); // Buka popup dengan alamat "Loading..." const popup = L.popup({ maxWidth: 340, closeOnClick: false }) .setLatLng(e.latlng) .setContent(buildFormPopup(lat, lng, 'Mengambil alamat...')) .openOn(map); // Reverse Geocoding via Nominatim fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json&accept-language=id`, { headers: { 'Accept-Language': 'id' } }) .then(r => r.json()) .then(geo => { const alamat = geo.display_name || ''; // Update field alamat di form const el = document.getElementById('f_ibadah_alamat'); if (el) el.value = alamat; }) .catch(() => { const el = document.getElementById('f_ibadah_alamat'); if (el) el.value = ''; }); setTimeout(() => { const el = document.getElementById('f_ibadah_nama'); if (el) el.focus(); }, 200); // Tampilkan preview circle radius saat popup terbuka if (_previewCircle) { layerGroup.removeLayer(_previewCircle); _previewCircle = null; } _previewCircle = L.circle([lat, lng], { radius: 500, color: '#38bdf8', weight: 1.5, opacity: 0.6, fillColor: '#0ea5e9', fillOpacity: 0.05, dashArray: '6,4' }).addTo(layerGroup); // Hapus preview circle & temp marker saat popup ditutup const _markerThisClick = tempMarker; const _circleThisClick = _previewCircle; map.once('popupclose', function () { if (_circleThisClick) { try { layerGroup.removeLayer(_circleThisClick); } catch(_){} } if (_previewCircle === _circleThisClick) _previewCircle = null; if (_markerThisClick) { try { map.removeLayer(_markerThisClick); } catch(_){} } if (tempMarker === _markerThisClick) tempMarker = null; }); } function buildFormPopup(lat, lng, alamatPlaceholder) { return `
Tambah Rumah Ibadah
${lat}, ${lng}
500m
100m5000m
`; } // ── Live preview radius (form tambah baru) ──────────────────────── window._ibadahPreviewRadius = function (val) { const r = Math.max(100, Math.min(5000, parseInt(val) || 500)); if (_previewCircle) _previewCircle.setRadius(r); }; // ── Live preview radius (edit marker yg sudah ada) ──────────────── let _recalcDebounce = null; window._ibadahPreviewRadiusById = function (id, val) { const r = Math.max(100, Math.min(5000, parseInt(val) || 500)); const targetId = parseInt(id); // 1. Update visual circle & marker property if (allMarkers[targetId]) { allMarkers[targetId].circle.setRadius(r); allMarkers[targetId].marker._ibadahRadius = r; } // 2. Paksa sinkronisasi window._ibadahList agar penduduk.js melihat radius terbaru if (window._ibadahList) { const entry = window._ibadahList.find(x => parseInt(x.id) === targetId); if (entry) { entry.radius = r; } else { // Jika tidak ketemu (jarang terjadi), panggil sync full updateIbadahList(); } } // 3. Recalculate warna penduduk (debounce agar ringan) clearTimeout(_recalcDebounce); _recalcDebounce = setTimeout(() => { console.log(`[Ibadah] Recalculating proximity for Radius: ${r}m`); if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } }, 50); // Dipercepat ke 50ms untuk feel yang lebih instan }; // ── Simpan ──────────────────────────────────────────────────────── window._ibadahSimpan = function () { const nama = document.getElementById('f_ibadah_nama').value.trim(); const jenis = document.getElementById('f_ibadah_jenis').value; const radius = Math.max(100, Math.min(5000, parseInt(document.getElementById('f_ibadah_radius')?.value) || 500)); const alamat = document.getElementById('f_ibadah_alamat').value.trim(); const kontak = document.getElementById('f_ibadah_kontak')?.value.trim() || ''; const lat = document.getElementById('f_ibadah_lat').value; const lng = document.getElementById('f_ibadah_lng').value; const status = document.getElementById('ibadahStatus'); const btn = document.getElementById('btnSimpanIbadah'); if (!nama) { status.textContent = '⚠ Nama wajib diisi.'; status.className = 'form-status error'; document.getElementById('f_ibadah_nama').focus(); return; } btn.disabled = true; btn.innerHTML = ' Menyimpan...'; lucide.createIcons(); const fd = new FormData(); fd.append('nama', nama); fd.append('jenis', jenis); fd.append('alamat', alamat); fd.append('kontak', kontak); fd.append('lat', lat); fd.append('lng', lng); fd.append('radius_m', radius); fetch('api/ibadah/simpan.php', { method: 'POST', body: appendCsrf(fd) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { map.closePopup(); if (_previewCircle) { layerGroup.removeLayer(_previewCircle); _previewCircle = null; } addToMap(j.data); updateIbadahList(); refreshList(Object.values(allMarkers).map(e => ({ id: e.marker._ibadahId, nama: e.marker._ibadahNama, jenis: e.marker._ibadahJenis, }))); showToast(`"${escapeHTML(nama)}" berhasil ditambahkan!`); // Recalculate penduduk setelah ibadah baru ditambah if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } } else throw new Error(j.message); }) .catch(err => { status.innerHTML = ' ' + escapeHTML(err.message); status.className = 'form-status error'; btn.disabled = false; btn.innerHTML = ' Simpan Rumah Ibadah'; lucide.createIcons(); }); }; // ── Recalculate All proximity (Admin only, with race-condition guard) ── let _recalcInProgress = false; window._recalcAll = function () { if (_recalcInProgress) { showToast('Proses hitung ulang sedang berjalan...', 'error'); return; } showDeleteConfirm( 'Proses ini dapat mengubah assignment warga ke rumah ibadah/operator berdasarkan radius terkini. Lanjutkan?', { type: 'warn', iconName: 'refresh-cw', title: 'Konfirmasi Hitung Ulang Proximity', btnLabel: 'Ya, Hitung Ulang' } ).then(confirmed => { if (!confirmed) return; _recalcInProgress = true; const allBtns = document.querySelectorAll( '#btnToolIbadah, #btnToolPenduduk, #btnRecalcAll, .btn-hapus, .btn-save' ); allBtns.forEach(b => { b.disabled = true; }); const btn = document.getElementById('btnRecalcAll'); const icon = btn ? btn.querySelector('.tb-icon') : null; if (btn) { if (icon) icon.textContent = '⏳'; btn.style.color = 'var(--c-muted)'; } showToast('Menghitung ulang proximity...'); fetch('api/ibadah/recalculate.php', { method: 'POST', body: appendCsrf(new FormData()) }) .then(r => r.json()) .then(j => { if (j.status === 'success') { showToast(`✓ ${j.message}`); if (typeof window._pendudukReload === 'function') { window._pendudukReload(); } else if (typeof window._pendudukRecalcAll === 'function') { window._pendudukRecalcAll(); } } else { showToast(j.message || 'Gagal menghitung ulang.', 'error'); } }) .catch(() => showToast('Gagal menghubungi server.', 'error')) .finally(() => { _recalcInProgress = false; allBtns.forEach(b => { b.disabled = false; }); if (btn) { if (icon) icon.textContent = '🔄'; btn.style.color = 'var(--c-warn)'; } }); }); }; window._ibadahReload = loadData; // ── Init ───────────────────────────────────────────────────────── window.initIbadah = function () { if (active) { loadData(); return; } active = true; layerGroup.addTo(map); map.on('click', onMapClick); window._dlFocusFns['Ibadah'] = function (id) { const entry = allMarkers[id]; if (!entry) return; map.setView(entry.marker.getLatLng(), Math.max(map.getZoom(), 17), { animate: true }); setTimeout(() => entry.marker.openPopup(), 350); }; loadData(); }; // ── Toggle visibilitas layer ────────────────────────────────────── window._ibadahToggleLayer = function (visible) { if (visible) { if (!map.hasLayer(layerGroup)) map.addLayer(layerGroup); } else { map.removeLayer(layerGroup); if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } } }; window._setBufferVisible = function (visible) { bufferVisible = visible; Object.values(allMarkers).forEach(function (entry) { if (visible) { if (!layerGroup.hasLayer(entry.circle)) entry.circle.addTo(layerGroup); } else { if (layerGroup.hasLayer(entry.circle)) layerGroup.removeLayer(entry.circle); } }); }; })();