// ================================================================
// 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 = ``;
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
? ``
: '';
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 = `
`;
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(); });