944 lines
45 KiB
JavaScript
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(); });
|