// ================================================================ // MAP.JS — WebGIS Peta Interaktif (terhubung ke PHP/MySQL API) // Variabel global BASE_URL, MAP_LAT, MAP_LNG, MAP_ZOOM // diinjeksikan dari index.php via PHP // ================================================================ // ── INIT MAP ──────────────────────────────────────────────────── const map = L.map('map', { preferCanvas: false, zoomControl: false }) .setView([MAP_LAT, MAP_LNG], MAP_ZOOM); // Basemaps const BASEMAPS = { osm: L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }), satellite: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: '© Esri World Imagery', maxZoom: 19 }), dark: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© CartoDB', maxZoom: 19 }) }; const BASEMAP_KEYS = ['osm', 'satellite', 'dark']; const BASEMAP_ICONS = { osm: '🗺️', satellite: '🛰️', dark: '🌑' }; let basemapIdx = 0; BASEMAPS.osm.addTo(map); // Layer groups const ibadahLayer = L.layerGroup().addTo(map); const miskinLayer = L.layerGroup().addTo(map); let heatLayer = null; let routeControl = null; let myMarker = null; // State let mode = null; // 'ibadah' | 'miskin' | null let tempMarker = null; let pendingLatLng = null; let pendingGeo = null; // reverse geocode result let allIbadah = []; // Array of ibadah objects with marker & circle let allMiskin = []; // Array of miskin objects with marker let anggotaCount = 0; // ── JENIS METADATA ────────────────────────────────────────────── const JENIS_COLOR = { Masjid: '#10b981', Musholla: '#34d399', Gereja: '#3b82f6', Katolik: '#60a5fa', Pura: '#f59e0b', Vihara: '#fbbf24', Klenteng: '#ef4444', Lainnya: '#8b5cf6' }; const JENIS_EMOJI = { Masjid: '🕌', Musholla: '🕌', Gereja: '⛪', Katolik: '⛪', Pura: '🛕', Vihara: '🛕', Klenteng: '⛩️', Lainnya: '🙏' }; const STATUS_COLOR = { sudah: '#22c55e', menunggu: '#f59e0b', belum: '#ef4444', darurat: '#a855f7' }; const STATUS_LABEL = { sudah: 'Sudah Dibantu', menunggu: 'Menunggu Verif.', belum: 'Belum Dibantu', darurat: 'Bantuan Darurat (Musibah)' }; // ── HELPERS ────────────────────────────────────────────────────── function fmtRadius(v) { v = parseInt(v); return v >= 1000 ? (v/1000).toFixed(1)+' km' : v+' m'; } function truncate(s, n) { s = (s || ''); return s.length > n ? s.slice(0,n)+'\u2026' : s; } function fmtRupiah(n) { if (!n && n !== 0) return '\u2014'; return 'Rp\u00a0' + parseFloat(n).toLocaleString('id-ID', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); } window.formatNominalInput = function(el) { let raw = el.value.replace(/[^0-9]/g, ''); el.value = raw.replace(/\B(?=(\d{3})+(?!\d))/g, '.'); }; // ── TOAST ──────────────────────────────────────────────────────── function showToast(msg, color = '#5b7fff') { const t = document.getElementById('toast'); t.innerHTML = msg; t.style.display = 'block'; t.style.borderLeftColor = color; clearTimeout(t._tid); t._tid = setTimeout(() => t.style.display = 'none', 3500); } // ── ICONS ──────────────────────────────────────────────────────── function createIbadahIcon(jenis) { const c = JENIS_COLOR[jenis] || '#5b7fff'; const em = JENIS_EMOJI[jenis] || '🏛️'; return L.divIcon({ html: `
${em}
`, iconSize:[36,36], iconAnchor:[18,36], popupAnchor:[0,-42], className:'' }); } function createMiskinIcon(status) { const c = STATUS_COLOR[status] || '#ef4444'; const glow = c.replace('#','') === '22c55e' ? 'rgba(34,197,94,0.55)' : c === '#f59e0b' ? 'rgba(245,158,11,0.55)' : status === 'darurat' ? 'rgba(168,85,247,0.55)' : 'rgba(239,68,68,0.55)'; return L.divIcon({ html: `
`, iconSize:[20,20], iconAnchor:[10,10], popupAnchor:[0,-13], className:'' }); } function createTempIcon(type) { if (type === 'ibadah') return L.divIcon({ html: `
📍
`, iconSize:[36,36], iconAnchor:[18,36], className:'' }); return L.divIcon({ html: `
`, iconSize:[20,20], iconAnchor:[10,10], className:'' }); } function createMyIcon() { return L.divIcon({ html: `
`, iconSize:[24,24], iconAnchor:[12,12], popupAnchor:[0,-16], className:'' }); } // ── LOAD DATA ──────────────────────────────────────────────────── async function loadIbadah() { try { const q = buildIbadahQuery(); const r = await fetch(`${BASE_URL}/api/rumah_ibadah.php${q}`); const js = await r.json(); if (!js.success) return; ibadahLayer.clearLayers(); allIbadah = []; js.data.forEach(d => addIbadahToMap(d)); updateStats(); renderIbadahList(); } catch(e) { console.error('loadIbadah:', e); } } async function loadMiskin() { try { const q = buildMiskinQuery(); const r = await fetch(`${BASE_URL}/api/penduduk_miskin.php${q}`); const js = await r.json(); if (!js.success) return; miskinLayer.clearLayers(); allMiskin = []; js.data.forEach(d => addMiskinToMap(d)); updateStats(); renderMiskinList(); rebuildHeatmap(); } catch(e) { console.error('loadMiskin:', e); } } function buildIbadahQuery() { const jenis = document.getElementById('filter-jenis').value; const kec = document.getElementById('filter-kecamatan')?.value || ''; const kel = document.getElementById('filter-kelurahan')?.value || ''; const search = document.getElementById('global-search').value.trim(); const p = []; if (jenis) p.push('jenis=' + encodeURIComponent(jenis)); if (kec) p.push('kecamatan=' + encodeURIComponent(kec)); if (kel) p.push('kelurahan=' + encodeURIComponent(kel)); if (search) p.push('search=' + encodeURIComponent(search)); return p.length ? '?' + p.join('&') : ''; } function buildMiskinQuery() { const status = document.getElementById('filter-status').value; const kec = document.getElementById('filter-kecamatan')?.value || ''; const kel = document.getElementById('filter-kelurahan')?.value || ''; const search = document.getElementById('global-search').value.trim(); const p = []; if (status) p.push('status=' + encodeURIComponent(status)); if (kec) p.push('kecamatan=' + encodeURIComponent(kec)); if (kel) p.push('kelurahan=' + encodeURIComponent(kel)); if (search) p.push('search=' + encodeURIComponent(search)); return p.length ? '?' + p.join('&') : ''; } // ── LOAD KECAMATAN & KELURAHAN OPTIONS ───────────────────────────────── let _allWilayah = []; // cache [{kecamatan, kelurahan}] async function loadKecamatanOptions() { try { const r = await fetch(`${BASE_URL}/api/penduduk_miskin.php`); const js = await r.json(); if (!js.success) return; // Cache semua data wilayah _allWilayah = js.data.map(d => ({ kecamatan: d.kecamatan || '', kelurahan: d.kelurahan || '' })); const selKec = document.getElementById('filter-kecamatan'); if (!selKec) return; // Populate kecamatan const kecs = [...new Set(_allWilayah.map(d => d.kecamatan).filter(Boolean))].sort(); const curKec = selKec.value; selKec.innerHTML = ''; kecs.forEach(k => { const opt = document.createElement('option'); opt.value = k; opt.textContent = k; if (k === curKec) opt.selected = true; selKec.appendChild(opt); }); // Populate kelurahan (cascade dari kecamatan terpilih) _updateKelurahanOptions(); } catch(e) { /* silent */ } } function _updateKelurahanOptions() { const selKel = document.getElementById('filter-kelurahan'); if (!selKel) return; const kec = document.getElementById('filter-kecamatan')?.value || ''; const kels = [...new Set( _allWilayah .filter(d => !kec || d.kecamatan === kec) .map(d => d.kelurahan) .filter(Boolean) )].sort(); const cur = selKel.value; selKel.innerHTML = ''; kels.forEach(k => { const opt = document.createElement('option'); opt.value = k; opt.textContent = k; if (k === cur) opt.selected = true; selKel.appendChild(opt); }); } function addIbadahToMap(d) { const lat = parseFloat(d.lat); const lng = parseFloat(d.lng); const r = parseInt(d.radius); const c = JENIS_COLOR[d.jenis] || '#5b7fff'; const marker = L.marker([lat, lng], { icon: createIbadahIcon(d.jenis) }); const circle = L.circle([lat, lng], { radius: r, color: c, fillColor: c, fillOpacity: 0.07, weight: 2, dashArray: '6,4', opacity: 0.8 }); const obj = { ...d, lat, lng, radius: r, marker, circle }; marker.bindPopup(() => buildIbadahPopup(obj)); marker.addTo(ibadahLayer); circle.addTo(ibadahLayer); allIbadah.push(obj); } function addMiskinToMap(d) { const lat = parseFloat(d.lat); const lng = parseFloat(d.lng); const marker = L.marker([lat, lng], { icon: createMiskinIcon(d.status_bantuan) }); const obj = { ...d, lat, lng, marker }; marker.bindPopup(() => buildMiskinPopup(obj)); marker.addTo(miskinLayer); allMiskin.push(obj); } // ── POPUPS ─────────────────────────────────────────────────────── function buildIbadahPopup(d) { const em = JENIS_EMOJI[d.jenis] || '🏛️'; return ` ${d.kontak ? `` : ''} ${d.alamat ? `` : ''} `; } function buildMiskinPopup(d) { const sc = STATUS_COLOR[d.status_bantuan] || '#ef4444'; const sl = STATUS_LABEL[d.status_bantuan] || d.status_bantuan; const ib = allIbadah.find(x => x.id == d.rumah_ibadah_id); return ` ${d.jumlah_anggota ? `` : ''} ${ib ? `` : ''} ${d.jarak_ke_ibadah ? `` : ''} ${d.jenis_bantuan ? `` : ''} ${d.nominal_bantuan ? `` : ''} ${d.bukti_file ? `` : ''} ${d.alamat ? `` : ''} `; } // ── STATS ──────────────────────────────────────────────────────── function updateStats() { document.getElementById('stat-ibadah').textContent = allIbadah.length; document.getElementById('stat-miskin').textContent = allMiskin.length; document.getElementById('stat-sudah').textContent = allMiskin.filter(m => m.status_bantuan === 'sudah').length; document.getElementById('stat-belum').textContent = allMiskin.filter(m => m.status_bantuan === 'belum').length; const elDarurat = document.getElementById('stat-darurat'); if (elDarurat) elDarurat.textContent = allMiskin.filter(m => m.status_bantuan === 'darurat').length; document.getElementById('count-ibadah').textContent = allIbadah.length; document.getElementById('count-miskin').textContent = allMiskin.length; const totalNominal = allMiskin.reduce((acc, m) => { return acc + ((m.status_bantuan === 'sudah' || m.status_bantuan === 'darurat') && m.nominal_bantuan ? parseFloat(m.nominal_bantuan) : 0); }, 0); const elNominal = document.getElementById('stat-nominal'); if (elNominal) elNominal.textContent = fmtRupiah(totalNominal); } // ── HEATMAP ────────────────────────────────────────────────────── function rebuildHeatmap() { if (!heatLayer || !map.hasLayer(heatLayer)) return; heatLayer.setLatLngs(allMiskin.map(m => [m.lat, m.lng, 0.9])); } function toggleHeatmap() { const btn = document.getElementById('btn-heatmap'); if (heatLayer && map.hasLayer(heatLayer)) { map.removeLayer(heatLayer); btn.classList.remove('active'); showToast('🔥 Heatmap dinonaktifkan', '#f59e0b'); } else { const pts = allMiskin.map(m => [m.lat, m.lng, 0.9]); if (!heatLayer) heatLayer = L.heatLayer(pts, { radius:32, blur:22, maxZoom:17 }); else heatLayer.setLatLngs(pts); heatLayer.addTo(map); btn.classList.add('active'); showToast('🔥 Heatmap aktif', '#f59e0b'); } } // ── BASEMAP ────────────────────────────────────────────────────── function toggleBasemap() { map.removeLayer(BASEMAPS[BASEMAP_KEYS[basemapIdx]]); basemapIdx = (basemapIdx + 1) % BASEMAP_KEYS.length; BASEMAPS[BASEMAP_KEYS[basemapIdx]].addTo(map); document.getElementById('btn-layers').textContent = BASEMAP_ICONS[BASEMAP_KEYS[basemapIdx]]; showToast(`🗺️ Basemap: ${BASEMAP_KEYS[basemapIdx].toUpperCase()}`, '#5b7fff'); } // ── SIDEBAR & TABS ─────────────────────────────────────────────── function switchTab(tab) { document.getElementById('list-ibadah').style.display = tab === 'ibadah' ? 'block' : 'none'; document.getElementById('list-miskin').style.display = tab === 'miskin' ? 'block' : 'none'; document.getElementById('tab-ibadah').className = 'tab-btn' + (tab === 'ibadah' ? ' active' : ''); document.getElementById('tab-miskin').className = 'tab-btn' + (tab === 'miskin' ? ' active' : ''); } function toggleSidebar() { document.getElementById('sidebar').classList.toggle('mobile-open'); } // ── SEARCH & FILTER ────────────────────────────────────────────── let searchTimer = null; document.getElementById('global-search').addEventListener('input', function () { clearTimeout(searchTimer); searchTimer = setTimeout(() => { loadIbadah(); loadMiskin(); }, 350); }); document.getElementById('filter-status').addEventListener('change', loadMiskin); document.getElementById('filter-jenis').addEventListener('change', loadIbadah); document.getElementById('filter-kecamatan')?.addEventListener('change', () => { _updateKelurahanOptions(); // cascade: update kelurahan sesuai kecamatan loadIbadah(); loadMiskin(); }); document.getElementById('filter-kelurahan')?.addEventListener('change', () => { loadIbadah(); loadMiskin(); }); // ── RENDER LISTS ───────────────────────────────────────────────── function renderIbadahList() { const el = document.getElementById('list-ibadah'); if (!allIbadah.length) { el.innerHTML = `
🏛️
Belum ada data rumah ibadah
`; return; } el.innerHTML = allIbadah.map(ib => `
${JENIS_EMOJI[ib.jenis] || '🏛️'}
${ib.nama}
${ib.jenis}${ib.kecamatan ? ' · '+ib.kecamatan : ''}
📏 ${fmtRadius(ib.radius)} 👥 ${ib.jumlah_binaan || 0} binaan
${fmtRadius(ib.radius)}
`).join(''); } function renderMiskinList() { const el = document.getElementById('list-miskin'); if (!allMiskin.length) { el.innerHTML = `
👤
Belum ada data penduduk
`; return; } el.innerHTML = allMiskin.map(m => { const ib = allIbadah.find(x => x.id == m.rumah_ibadah_id); const sc = STATUS_COLOR[m.status_bantuan] || '#ef4444'; return `
👤
${m.kk_nama}
${m.kecamatan || m.kelurahan || truncate(m.alamat||'',40) || '—'}
${STATUS_LABEL[m.status_bantuan]||m.status_bantuan} ${ib ? `📌 ${truncate(ib.nama,18)}` : ''}
`; }).join(''); } // ── FOCUS MARKER ───────────────────────────────────────────────── function focusMarker(type, id) { if (type === 'ibadah') { const ib = allIbadah.find(x => x.id == id); if (!ib) return; map.setView([ib.lat, ib.lng], Math.max(map.getZoom(), 16)); ib.marker.openPopup(); } else { const m = allMiskin.find(x => x.id == id); if (!m) return; map.setView([m.lat, m.lng], Math.max(map.getZoom(), 17)); m.marker.openPopup(); } } // ── LIVE RADIUS ────────────────────────────────────────────────── window.liveRadius = function (id, val) { const ib = allIbadah.find(x => x.id == id); if (!ib) return; ib.radius = parseInt(val); if (ib.circle) ib.circle.setRadius(ib.radius); const disp = document.getElementById('rdisplay-' + id); if (disp) disp.textContent = fmtRadius(ib.radius); }; // ── MODE / MAP CLICK ───────────────────────────────────────────── function setMode(m) { if (mode === m) { mode = null; _resetMode(); return; } mode = m; _resetMode(); if (m === 'ibadah') { document.getElementById('map').classList.add('adding-ibadah'); showToast('🏛️ Klik peta untuk menentukan lokasi Rumah Ibadah', '#5b7fff'); } else { document.getElementById('map').classList.add('adding-miskin'); showToast('👤 Klik peta untuk menentukan lokasi Penduduk Miskin', '#22c55e'); } _updateToolbar(); } function _resetMode() { document.getElementById('map').classList.remove('adding-ibadah', 'adding-miskin'); } function _updateToolbar() { document.getElementById('toggle-ibadah').className = 'toggle-btn' + (mode === 'ibadah' ? ' active-ibadah' : ''); document.getElementById('toggle-miskin').className = 'toggle-btn' + (mode === 'miskin' ? ' active-miskin' : ''); } map.on('click', async (e) => { if (!mode) return; const { lat, lng } = e.latlng; pendingLatLng = { lat, lng }; pendingGeo = null; if (tempMarker) map.removeLayer(tempMarker); tempMarker = L.marker([lat, lng], { icon: createTempIcon(mode) }).addTo(map); if (mode === 'ibadah') _openModalIbadah(lat, lng, null); else _openModalMiskin(lat, lng, null); // Reverse geocode async try { const gr = await fetch(`${BASE_URL}/api/geocode.php?lat=${lat}&lng=${lng}`); pendingGeo = await gr.json(); _fillGeoFields(mode); } catch { /* no-op */ } }); function _fillGeoFields(type) { if (!pendingGeo) return; const addr = pendingGeo.display_name || ''; if (type === 'ibadah') { const el = document.getElementById('ibadah-addr-display'); if (el) el.textContent = addr ? '📍 ' + addr : '—'; const kel = document.getElementById('ibadah-kelurahan'); const kec = document.getElementById('ibadah-kecamatan'); if (kel && !kel.value && pendingGeo.kelurahan) kel.value = pendingGeo.kelurahan; if (kec && !kec.value && pendingGeo.kecamatan) kec.value = pendingGeo.kecamatan; } else { const el = document.getElementById('miskin-addr-display'); if (el) el.textContent = addr ? '📍 ' + addr : '—'; const kel = document.getElementById('miskin-kelurahan'); const kec = document.getElementById('miskin-kecamatan'); if (kel && !kel.value && pendingGeo.kelurahan) kel.value = pendingGeo.kelurahan; if (kec && !kec.value && pendingGeo.kecamatan) kec.value = pendingGeo.kecamatan; } } // ── MODAL IBADAH ───────────────────────────────────────────────── function _openModalIbadah(lat, lng, data) { const isEdit = data !== null; document.getElementById('modal-ibadah-title').textContent = isEdit ? '✏️ Edit Rumah Ibadah' : '🏛️ Tambah Rumah Ibadah'; document.getElementById('ibadah-edit-id').value = isEdit ? data.id : ''; document.getElementById('ibadah-nama').value = isEdit ? data.nama : ''; document.getElementById('ibadah-jenis').value = isEdit ? data.jenis : 'Masjid'; document.getElementById('ibadah-kontak').value = isEdit ? (data.kontak||'') : ''; document.getElementById('ibadah-kelurahan').value = isEdit ? (data.kelurahan||'') : ''; document.getElementById('ibadah-kecamatan').value = isEdit ? (data.kecamatan||'') : ''; const rad = isEdit ? data.radius : 300; document.getElementById('ibadah-radius').value = rad; document.getElementById('ibadah-radius-val').textContent = fmtRadius(rad); document.getElementById('ibadah-coord-display').textContent = `Lat: ${lat.toFixed(6)} Lng: ${lng.toFixed(6)}`; const addrEl = document.getElementById('ibadah-addr-display'); if (isEdit) addrEl.textContent = '📍 ' + (data.alamat || `${lat.toFixed(5)}, ${lng.toFixed(5)}`); else addrEl.innerHTML = '🔄 Memuat alamat...'; document.getElementById('modal-ibadah').classList.add('open'); setTimeout(() => document.getElementById('ibadah-nama').focus(), 120); } window.openEditIbadah = function (id) { const ib = allIbadah.find(x => x.id == id); if (!ib) return; map.closePopup(); pendingLatLng = { lat: ib.lat, lng: ib.lng }; _openModalIbadah(ib.lat, ib.lng, ib); }; window.closeModalIbadah = function () { document.getElementById('modal-ibadah').classList.remove('open'); if (tempMarker && !document.getElementById('modal-miskin').classList.contains('open')) { map.removeLayer(tempMarker); tempMarker = null; } if (!document.getElementById('modal-miskin').classList.contains('open')) { pendingLatLng = null; pendingGeo = null; mode = null; _resetMode(); _updateToolbar(); } }; document.getElementById('ibadah-radius').addEventListener('input', function () { document.getElementById('ibadah-radius-val').textContent = fmtRadius(parseInt(this.value)); // Live preview on existing circle const eid = document.getElementById('ibadah-edit-id').value; if (eid) { const ib = allIbadah.find(x => x.id == eid); if (ib?.circle) ib.circle.setRadius(parseInt(this.value)); } }); document.getElementById('btn-save-ibadah').addEventListener('click', _saveIbadah); async function _saveIbadah() { if (!pendingLatLng) { showToast('❌ Pilih lokasi di peta terlebih dahulu', '#ef4444'); return; } const { lat, lng } = pendingLatLng; const editId = document.getElementById('ibadah-edit-id').value; const nama = document.getElementById('ibadah-nama').value.trim(); if (!nama) { showToast('❌ Nama rumah ibadah wajib diisi!', '#ef4444'); return; } const payload = { nama, jenis: document.getElementById('ibadah-jenis').value, kontak: document.getElementById('ibadah-kontak').value, kelurahan: document.getElementById('ibadah-kelurahan').value, kecamatan: document.getElementById('ibadah-kecamatan').value, alamat: pendingGeo?.display_name || '', lat, lng, radius: parseInt(document.getElementById('ibadah-radius').value) }; const btn = document.getElementById('btn-save-ibadah'); btn.disabled = true; btn.textContent = '⏳ Menyimpan...'; try { const url = editId ? `${BASE_URL}/api/rumah_ibadah.php?id=${editId}` : `${BASE_URL}/api/rumah_ibadah.php`; const resp = await fetch(url, { method: editId?'PUT':'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) }); const json = await resp.json(); if (json.success) { window.closeModalIbadah(); await Promise.all([loadIbadah(), loadMiskin()]); showToast(`✅ ${json.message}`, '#22c55e'); } else { showToast('❌ ' + json.message, '#ef4444'); } } catch { showToast('❌ Gagal menyimpan data', '#ef4444'); } finally { btn.disabled = false; btn.innerHTML = ' Simpan'; } } // ── MODAL MISKIN ───────────────────────────────────────────────── function _openModalMiskin(lat, lng, data) { const isEdit = data !== null; document.getElementById('modal-miskin-title').textContent = isEdit ? '✏️ Edit Penduduk Miskin' : '👤 Tambah Penduduk Miskin'; document.getElementById('miskin-edit-id').value = isEdit ? data.id : ''; document.getElementById('miskin-kk-nama').value = isEdit ? data.kk_nama : ''; document.getElementById('miskin-nik').value = isEdit ? (data.nik||'') : ''; document.getElementById('miskin-jumlah').value = isEdit ? (data.jumlah_anggota||1) : 1; document.getElementById('miskin-kelurahan').value = isEdit ? (data.kelurahan||'') : ''; document.getElementById('miskin-kecamatan').value = isEdit ? (data.kecamatan||'') : ''; document.getElementById('miskin-status').value = isEdit ? data.status_bantuan : 'belum'; document.getElementById('miskin-jenis-bantuan').value = isEdit ? (data.jenis_bantuan||'') : ''; document.getElementById('miskin-tanggal').value = isEdit ? (data.tanggal_bantuan||'') : ''; // Nominal bantuan const nomEl = document.getElementById('miskin-nominal'); if (nomEl) { const rawNom = isEdit && data.nominal_bantuan ? parseFloat(data.nominal_bantuan) : ''; nomEl.value = rawNom !== '' ? rawNom.toLocaleString('id-ID', {maximumFractionDigits:0}) : ''; } // Keterangan menunggu const ketEl = document.getElementById('miskin-keterangan'); if (ketEl) ketEl.value = isEdit ? (data.keterangan||'') : ''; document.getElementById('miskin-coord-display').textContent = `Lat: ${lat.toFixed(6)} Lng: ${lng.toFixed(6)}`; const addrEl = document.getElementById('miskin-addr-display'); if (isEdit) addrEl.textContent = '📍 ' + (data.alamat || `${lat.toFixed(5)}, ${lng.toFixed(5)}`); else addrEl.innerHTML = '🔄 Memuat alamat...'; _toggleBantuanFields(); // Reset anggota anggotaCount = 0; document.getElementById('anggota-container').innerHTML = ''; if (isEdit && data.id) _loadAnggota(data.id); // Bukti preview const prev = document.getElementById('miskin-bukti-preview'); prev.innerHTML = isEdit && data.bukti_file ? `
📎 File: ${data.bukti_file}
` : ''; document.getElementById('miskin-bukti').value = ''; document.getElementById('modal-miskin').classList.add('open'); setTimeout(() => document.getElementById('miskin-kk-nama').focus(), 120); } async function _loadAnggota(pid) { try { const r = await fetch(`${BASE_URL}/api/anggota_keluarga.php?penduduk_id=${pid}`); const js = await r.json(); if (js.success) js.data.forEach(a => _addAnggotaRow(a)); } catch { /* silent */ } } window.closeModalMiskin = function () { document.getElementById('modal-miskin').classList.remove('open'); if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; } pendingLatLng = null; pendingGeo = null; mode = null; _resetMode(); _updateToolbar(); }; window.openEditMiskin = function (id) { const m = allMiskin.find(x => x.id == id); if (!m) return; map.closePopup(); pendingLatLng = { lat: m.lat, lng: m.lng }; _openModalMiskin(m.lat, m.lng, m); }; function _toggleBantuanFields() { const st = document.getElementById('miskin-status').value; const showBantuan = (st === 'sudah' || st === 'menunggu' || st === 'darurat'); document.getElementById('bantuan-fields').style.display = showBantuan ? 'block' : 'none'; const ketWrap = document.getElementById('keterangan-menunggu-wrap'); if (ketWrap) ketWrap.style.display = (st === 'menunggu') ? 'block' : 'none'; } document.getElementById('miskin-status').addEventListener('change', _toggleBantuanFields); // Anggota rows function _addAnggotaRow(data) { anggotaCount++; const idx = anggotaCount; const container = document.getElementById('anggota-container'); const div = document.createElement('div'); div.className = 'anggota-row'; div.id = `anggota-row-${idx}`; div.innerHTML = `
👤 Anggota ${idx}
`; container.appendChild(div); } window.addAnggotaRow = () => _addAnggotaRow(null); window.removeAnggotaRow = (idx) => { const r = document.getElementById(`anggota-row-${idx}`); if (r) r.remove(); }; document.getElementById('btn-add-anggota').addEventListener('click', () => _addAnggotaRow(null)); function _collectAnggota() { const rows = document.querySelectorAll('.anggota-row'); const res = []; rows.forEach(row => { const idx = row.id.replace('anggota-row-', ''); const nama = document.getElementById(`ak-nama-${idx}`)?.value?.trim(); if (!nama) return; res.push({ nama, hubungan: document.getElementById(`ak-hub-${idx}`)?.value || '', umur: document.getElementById(`ak-umur-${idx}`)?.value || '', pekerjaan: document.getElementById(`ak-pkj-${idx}`)?.value || '', keterangan: document.getElementById(`ak-ket-${idx}`)?.value || '' }); }); return res; } document.getElementById('btn-save-miskin').addEventListener('click', _saveMiskin); async function _saveMiskin() { if (!pendingLatLng) { showToast('❌ Pilih lokasi di peta terlebih dahulu', '#ef4444'); return; } const { lat, lng } = pendingLatLng; const editId = document.getElementById('miskin-edit-id').value; const nama = document.getElementById('miskin-kk-nama').value.trim(); if (!nama) { showToast('❌ Nama Kepala Keluarga wajib diisi!', '#ef4444'); return; } const btn = document.getElementById('btn-save-miskin'); btn.disabled = true; btn.textContent = '⏳ Menyimpan...'; const fd = new FormData(); fd.append('kk_nama', nama); fd.append('nik', document.getElementById('miskin-nik').value); fd.append('jumlah_anggota', document.getElementById('miskin-jumlah').value || 1); fd.append('kelurahan', document.getElementById('miskin-kelurahan').value); fd.append('kecamatan', document.getElementById('miskin-kecamatan').value); fd.append('alamat', pendingGeo?.display_name || ''); fd.append('lat', lat); fd.append('lng', lng); fd.append('status_bantuan', document.getElementById('miskin-status').value); fd.append('jenis_bantuan', document.getElementById('miskin-jenis-bantuan').value); fd.append('tanggal_bantuan', document.getElementById('miskin-tanggal').value); // Nominal bantuan: hapus titik pemisah ribuan sebelum kirim const nomRaw = (document.getElementById('miskin-nominal')?.value || '').replace(/\./g,''); fd.append('nominal_bantuan', nomRaw); fd.append('anggota', JSON.stringify(_collectAnggota())); const buktiInput = document.getElementById('miskin-bukti'); if (buktiInput.files[0]) fd.append('bukti_file', buktiInput.files[0]); try { const url = editId ? `${BASE_URL}/api/penduduk_miskin.php?id=${editId}` : `${BASE_URL}/api/penduduk_miskin.php`; const resp = await fetch(url, { method: 'POST', body: fd }); const json = await resp.json(); if (json.success) { window.closeModalMiskin(); await loadMiskin(); showToast(`✅ ${json.message}`, '#22c55e'); } else { showToast('❌ ' + json.message, '#ef4444'); } } catch { showToast('❌ Gagal menyimpan data', '#ef4444'); } finally { btn.disabled = false; btn.innerHTML = ' Simpan'; } } // ── DELETE ─────────────────────────────────────────────────────── window.confirmDeleteIbadah = async function (id) { if (!confirm('Hapus rumah ibadah ini? Warga yang terkait akan di-reset.')) return; try { const r = await fetch(`${BASE_URL}/api/rumah_ibadah.php?id=${id}`, { method:'DELETE' }); const js = await r.json(); if (js.success) { map.closePopup(); await Promise.all([loadIbadah(), loadMiskin()]); showToast('🗑️ Data dihapus', '#f59e0b'); } else showToast('❌ ' + js.message, '#ef4444'); } catch { showToast('❌ Gagal menghapus', '#ef4444'); } }; window.confirmDeleteMiskin = async function (id) { if (!confirm('Hapus data penduduk ini?')) return; try { const r = await fetch(`${BASE_URL}/api/penduduk_miskin.php?id=${id}`, { method:'DELETE' }); const js = await r.json(); if (js.success) { map.closePopup(); await loadMiskin(); showToast('🗑️ Data dihapus', '#f59e0b'); } else showToast('❌ ' + js.message, '#ef4444'); } catch { showToast('❌ Gagal menghapus', '#ef4444'); } }; // ── GEOLOCATION ────────────────────────────────────────────────── function doLocate() { if (!navigator.geolocation) { showToast('❌ Browser tidak mendukung Geolocation', '#ef4444'); return; } showToast('📡 Mendeteksi lokasi...', '#f59e0b'); navigator.geolocation.getCurrentPosition(async (pos) => { const lat = pos.coords.latitude, lng = pos.coords.longitude; if (myMarker) map.removeLayer(myMarker); myMarker = L.marker([lat, lng], { icon: createMyIcon() }).addTo(map); let addr = `${lat.toFixed(5)}, ${lng.toFixed(5)}`; try { const gr = await fetch(`${BASE_URL}/api/geocode.php?lat=${lat}&lng=${lng}`); const gj = await gr.json(); addr = gj.display_name || addr; } catch { /* silent */ } myMarker.bindPopup(`📍 Lokasi Saya `).openPopup(); map.setView([lat, lng], 16); const lb = document.getElementById('loc-bar'); lb.style.display = 'flex'; document.getElementById('loc-bar-addr').textContent = truncate(addr, 60); document.getElementById('loc-bar-coord').textContent = `${lat.toFixed(5)}, ${lng.toFixed(5)} · ±${Math.round(pos.coords.accuracy)}m`; showToast('✅ Lokasi ditemukan!', '#22c55e'); }, (err) => { const msgs = {1:'Izin ditolak',2:'Posisi tidak tersedia',3:'Timeout'}; showToast('❌ ' + (msgs[err.code]||'Gagal mendapatkan lokasi'), '#ef4444'); }, { enableHighAccuracy: true, timeout: 10000 }); } // ── ROUTING ────────────────────────────────────────────────────── window.routeTo = function (fromLat, fromLng, destLat, destLng) { if (routeControl) { try { map.removeControl(routeControl); } catch{} routeControl = null; } map.closePopup(); routeControl = L.Routing.control({ waypoints: [L.latLng(fromLat, fromLng), L.latLng(destLat, destLng)], lineOptions: { styles: [{ color: '#5b7fff', weight: 4, opacity: 0.85 }] }, createMarker: () => null, show: false, addWaypoints: false, draggableWaypoints: false }).addTo(map); showToast('🗺️ Rute ditampilkan', '#5b7fff'); }; // ── REFRESH ────────────────────────────────────────────────────── async function refreshAll() { showToast('🔄 Memperbarui data...', '#f59e0b'); await Promise.all([loadIbadah(), loadMiskin()]); showToast('✅ Data diperbarui', '#22c55e'); } // ── KEYBOARD ───────────────────────────────────────────────────── document.addEventListener('keydown', e => { if (e.key === 'Escape') { if (document.getElementById('modal-ibadah').classList.contains('open')) window.closeModalIbadah(); else if (document.getElementById('modal-miskin').classList.contains('open')) window.closeModalMiskin(); else { mode = null; _resetMode(); _updateToolbar(); } } }); // ── UPLOAD PREVIEW ─────────────────────────────────────────────── document.getElementById('miskin-bukti')?.addEventListener('change', function () { const prev = document.getElementById('miskin-bukti-preview'); if (!this.files[0]) { prev.innerHTML = ''; return; } const f = this.files[0]; if (f.type.startsWith('image/')) { const reader = new FileReader(); reader.onload = (ev) => { prev.innerHTML = `
`; }; reader.readAsDataURL(f); } else { prev.innerHTML = `
📎 ${f.name} (${(f.size/1024).toFixed(1)} KB)
`; } }); // ── NOTIFICATION SYSTEM ───────────────────────────────────────── let notifLastCheck = new Date().toISOString().replace('T',' ').substring(0,19); let notifCount = 0; let notifPanelOpen = false; function toggleNotifPanel() { notifPanelOpen = !notifPanelOpen; const panel = document.getElementById('notif-panel'); if (panel) panel.style.display = notifPanelOpen ? 'block' : 'none'; if (notifPanelOpen) { // Clear badge when panel opened notifCount = 0; const badge = document.getElementById('notif-badge'); if (badge) badge.style.display = 'none'; } } async function checkNotifications() { try { const r = await fetch(`${BASE_URL}/api/notify.php?since=${encodeURIComponent(notifLastCheck)}`); const js = await r.json(); if (!js.success) return; notifLastCheck = js.server_time; const list = document.getElementById('notif-list'); const badge = document.getElementById('notif-badge'); let html = ''; if (js.new_miskin > 0 || js.new_ibadah > 0) { notifCount += (js.new_miskin + js.new_ibadah); if (js.new_miskin > 0) { html += `
👤 +${js.new_miskin} data penduduk baru
`; showToast(`🔔 ${js.new_miskin} data penduduk baru ditambahkan`, '#22c55e'); await loadMiskin(); } if (js.new_ibadah > 0) { html += `
🏛️ +${js.new_ibadah} rumah ibadah baru
`; showToast(`🔔 ${js.new_ibadah} rumah ibadah baru ditambahkan`, '#5b7fff'); await loadIbadah(); } if (list) list.innerHTML = html + (list.innerHTML || ''); if (badge) { badge.style.display = 'flex'; badge.textContent = notifCount > 9 ? '9+' : notifCount; } } if (js.belum_dibantu > 0 && list && !list.innerHTML.includes('belum-info')) { list.innerHTML += `
📊 ${js.belum_dibantu} warga belum dibantu
`; } if (!list?.innerHTML.trim()) { if (list) list.innerHTML = '
Tidak ada notifikasi baru
'; } } catch(e) { /* silent */ } } // Poll setiap 60 detik setInterval(checkNotifications, 60000); // ── INIT ───────────────────────────────────────────────────────── setTimeout(() => map.invalidateSize(), 150); refreshAll().then(() => { loadKecamatanOptions(); });