Files
WebGIS-Poverty-MAP/assets/js/map.js
T

944 lines
45 KiB
JavaScript

// ================================================================
// 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: '© <a href="https://www.openstreetmap.org/copyright" style="color:#5b7fff">OpenStreetMap</a>',
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' };
const STATUS_LABEL = { sudah: 'Sudah Dibantu', menunggu: 'Menunggu Verif.', belum: 'Belum Dibantu' };
// ── 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: `<div style="background:${c};width:36px;height:36px;
border-radius:50% 50% 50% 0;transform:rotate(-45deg);border:3px solid #fff;
box-shadow:0 4px 12px rgba(0,0,0,0.45);display:flex;align-items:center;justify-content:center;">
<span style="transform:rotate(45deg);font-size:16px;">${em}</span></div>`,
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)' : 'rgba(239,68,68,0.55)';
return L.divIcon({
html: `<div style="background:${c};width:20px;height:20px;border-radius:50%;
border:3px solid #fff;box-shadow:0 2px 8px ${glow};"></div>`,
iconSize:[20,20], iconAnchor:[10,10], popupAnchor:[0,-13], className:''
});
}
function createTempIcon(type) {
if (type === 'ibadah') return L.divIcon({
html: `<div style="background:linear-gradient(135deg,#5b7fff,#7c5bff);opacity:0.6;
width:36px;height:36px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
border:3px dashed #fff;display:flex;align-items:center;justify-content:center;">
<span style="transform:rotate(45deg);font-size:16px;">📍</span></div>`,
iconSize:[36,36], iconAnchor:[18,36], className:''
});
return L.divIcon({
html: `<div style="background:#8890b0;width:20px;height:20px;border-radius:50%;
border:3px dashed #fff;opacity:0.6;"></div>`,
iconSize:[20,20], iconAnchor:[10,10], className:''
});
}
function createMyIcon() {
return L.divIcon({
html: `<div style="background:#f59e0b;width:24px;height:24px;border-radius:50%;
border:3px solid #fff;box-shadow:0 2px 12px rgba(245,158,11,0.7);
animation:mypulse 1.8s infinite ease-in-out;"></div>
<style>@keyframes mypulse{0%,100%{box-shadow:0 0 0 0 rgba(245,158,11,.6)}50%{box-shadow:0 0 0 8px rgba(245,158,11,0)}}</style>`,
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 = '<option value="">Semua Kecamatan</option>';
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 = '<option value="">Semua Kelurahan</option>';
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 `
<div class="popup-title">${em} ${d.nama}</div>
<div class="popup-divider"></div>
<div class="popup-row"><span>Jenis</span><strong>${d.jenis}</strong></div>
${d.kontak ? `<div class="popup-row"><span>Kontak</span><strong>${d.kontak}</strong></div>` : ''}
<div class="popup-row"><span>Radius</span><strong style="color:var(--accent)">${fmtRadius(d.radius)}</strong></div>
<div class="popup-row"><span>Binaan</span><strong>${d.jumlah_binaan || 0} warga</strong></div>
${d.alamat ? `<div class="popup-addr">📍 ${d.alamat}</div>` : ''}
<div class="popup-coord">${d.lat.toFixed(6)}, ${d.lng.toFixed(6)}</div>
<div class="popup-actions">
<button class="popup-btn edit" onclick="openEditIbadah(${d.id})">✏️ Edit</button>
<button class="popup-btn danger" onclick="confirmDeleteIbadah(${d.id})">🗑️ Hapus</button>
</div>`;
}
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 `
<div class="popup-title">👤 ${d.kk_nama}</div>
<div class="popup-divider"></div>
<div class="popup-row">
<span style="background:${sc}22;color:${sc};padding:2px 8px;border-radius:4px;font-weight:700;font-size:11px;">${sl}</span>
</div>
${d.jumlah_anggota ? `<div class="popup-row"><span>Anggota</span><strong>${d.jumlah_anggota} orang</strong></div>` : ''}
${ib ? `<div class="popup-row"><span>Ibadah</span><strong style="color:var(--accent)">${JENIS_EMOJI[ib.jenis]||''} ${ib.nama}</strong></div>` : ''}
${d.jarak_ke_ibadah ? `<div class="popup-row"><span>Jarak</span><strong>${Math.round(d.jarak_ke_ibadah)} m</strong></div>` : ''}
${d.jenis_bantuan ? `<div class="popup-row"><span>Bantuan</span><strong>${d.jenis_bantuan}</strong></div>` : ''}
${d.nominal_bantuan ? `<div class="popup-row"><span>Nominal</span><strong style="color:var(--green);font-family:'JetBrains Mono',monospace;">${fmtRupiah(d.nominal_bantuan)}</strong></div>` : ''}
${d.bukti_file ? `<div class="popup-row"><a href="${BASE_URL}/assets/uploads/${d.bukti_file}" target="_blank" style="color:var(--accent);font-size:11px;">📎 Lihat Bukti</a></div>` : ''}
${d.alamat ? `<div class="popup-addr">📍 ${truncate(d.alamat, 80)}</div>` : ''}
<div class="popup-coord">${d.lat.toFixed(6)}, ${d.lng.toFixed(6)}</div>
<div class="popup-actions">
<button class="popup-btn edit" onclick="openEditMiskin(${d.id})">✏️ Edit</button>
<button class="popup-btn danger" onclick="confirmDeleteMiskin(${d.id})">🗑️ Hapus</button>
${ib ? `<button class="popup-btn" style="background:rgba(91,127,255,.15);color:var(--accent)" onclick="routeTo(${d.lat},${d.lng},${ib.lat},${ib.lng})">🗺️ Rute</button>` : ''}
</div>`;
}
// ── 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;
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.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 = `<div class="empty-state"><span class="empty-icon">🏛️</span><div>Belum ada data rumah ibadah</div></div>`;
return;
}
el.innerHTML = allIbadah.map(ib => `
<div class="item-card" id="card-ibadah-${ib.id}" onclick="focusMarker('ibadah',${ib.id})">
<div class="item-icon">${JENIS_EMOJI[ib.jenis] || '🏛️'}</div>
<div class="item-info">
<div class="item-name">${ib.nama}</div>
<div class="item-addr">${ib.jenis}${ib.kecamatan ? ' · '+ib.kecamatan : ''}</div>
<div class="item-meta">
<span class="nearest-tag">📏 ${fmtRadius(ib.radius)}</span>
<span class="nearest-tag">👥 ${ib.jumlah_binaan || 0} binaan</span>
</div>
<div class="item-radius-row" onclick="event.stopPropagation()">
<input type="range" min="100" max="5000" step="50" value="${ib.radius}"
oninput="liveRadius(${ib.id}, this.value)" title="Geser untuk ubah radius">
<span class="radius-val" id="rdisplay-${ib.id}">${fmtRadius(ib.radius)}</span>
</div>
</div>
<div class="item-actions">
<button class="btn-icon edit" onclick="event.stopPropagation();openEditIbadah(${ib.id})" title="Edit">✏️</button>
<button class="btn-icon delete" onclick="event.stopPropagation();confirmDeleteIbadah(${ib.id})" title="Hapus">✕</button>
</div>
</div>`).join('');
}
function renderMiskinList() {
const el = document.getElementById('list-miskin');
if (!allMiskin.length) {
el.innerHTML = `<div class="empty-state"><span class="empty-icon">👤</span><div>Belum ada data penduduk</div></div>`;
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 `
<div class="item-card" id="card-miskin-${m.id}" onclick="focusMarker('miskin',${m.id})">
<div class="item-icon">👤</div>
<div class="item-info">
<div class="item-name">${m.kk_nama}</div>
<div class="item-addr">${m.kecamatan || m.kelurahan || truncate(m.alamat||'',40) || '—'}</div>
<div class="item-meta">
<span class="badge-status" style="background:${sc}22;color:${sc}">${STATUS_LABEL[m.status_bantuan]||m.status_bantuan}</span>
${ib ? `<span class="nearest-tag">📌 ${truncate(ib.nama,18)}</span>` : ''}
</div>
</div>
<div class="item-actions">
<button class="btn-icon edit" onclick="event.stopPropagation();openEditMiskin(${m.id})" title="Edit">✏️</button>
<button class="btn-icon delete" onclick="event.stopPropagation();confirmDeleteMiskin(${m.id})" title="Hapus">✕</button>
</div>
</div>`;
}).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 = '<span class="addr-loading">🔄 Memuat alamat...</span>';
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 = '<i class="bi bi-floppy"></i> 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 = '<span class="addr-loading">🔄 Memuat alamat...</span>';
_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
? `<div style="font-size:11px;color:var(--text2)">📎 File: <a href="${BASE_URL}/assets/uploads/${data.bukti_file}" target="_blank" style="color:var(--accent)">${data.bukti_file}</a></div>`
: '';
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');
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 = `
<div class="anggota-row-header">
<span class="anggota-num">👤 Anggota ${idx}</span>
<button class="btn-remove-anggota" onclick="removeAnggotaRow(${idx})" type="button">✕</button>
</div>
<div class="form-row">
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Nama *</label>
<input class="form-control" id="ak-nama-${idx}" placeholder="Nama lengkap" value="${data?.nama||''}">
</div>
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Hubungan</label>
<input class="form-control" id="ak-hub-${idx}" placeholder="Istri/Anak/Dll" value="${data?.hubungan||''}">
</div>
</div>
<div class="form-row" style="margin-top:8px">
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Umur</label>
<input class="form-control" id="ak-umur-${idx}" type="number" min="0" placeholder="Tahun" value="${data?.umur||''}">
</div>
<div class="form-group" style="margin-bottom:0">
<label class="form-label">Pekerjaan</label>
<input class="form-control" id="ak-pkj-${idx}" placeholder="Pekerjaan" value="${data?.pekerjaan||''}">
</div>
</div>
<div class="form-group" style="margin-top:8px;margin-bottom:0">
<label class="form-label">Keterangan</label>
<input class="form-control" id="ak-ket-${idx}" placeholder="Catatan tambahan (opsional)" value="${data?.keterangan||''}">
</div>`;
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: editId?'PUT':'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 = '<i class="bi bi-floppy"></i> 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(`<b>📍 Lokasi Saya</b><div class="popup-addr">${addr}</div>
<div class="popup-coord">±${Math.round(pos.coords.accuracy)}m · ${lat.toFixed(6)}, ${lng.toFixed(6)}</div>`).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 = `<div class="upload-preview"><img src="${ev.target.result}" style="max-height:100px;border-radius:6px;margin-top:6px;"></div>`;
};
reader.readAsDataURL(f);
} else {
prev.innerHTML = `<div style="font-size:11px;color:var(--accent);margin-top:6px;">📎 ${f.name} (${(f.size/1024).toFixed(1)} KB)</div>`;
}
});
// ── 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 += `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);"><span style="color:#22c55e;">👤 +${js.new_miskin}</span> data penduduk baru</div>`;
showToast(`🔔 ${js.new_miskin} data penduduk baru ditambahkan`, '#22c55e');
await loadMiskin();
}
if (js.new_ibadah > 0) {
html += `<div style="padding:8px 0;border-bottom:1px solid rgba(255,255,255,.05);"><span style="color:#5b7fff;">🏛️ +${js.new_ibadah}</span> rumah ibadah baru</div>`;
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 += `<div id="belum-info" style="padding:8px 0;font-size:11px;color:var(--text3);">📊 ${js.belum_dibantu} warga belum dibantu</div>`;
}
if (!list?.innerHTML.trim()) {
if (list) list.innerHTML = '<div style="color:var(--text3);text-align:center;padding:16px;">Tidak ada notifikasi baru</div>';
}
} catch(e) { /* silent */ }
}
// Poll setiap 60 detik
setInterval(checkNotifications, 60000);
// ── INIT ─────────────────────────────────────────────────────────
setTimeout(() => map.invalidateSize(), 150);
refreshAll().then(() => { loadKecamatanOptions(); });