// ===================================================
// app.js - ARSITEKTUR WEBGIS V3 (SPBU + KEMISKINAN TERPADU)
// ===================================================
const state = {
isMarkerMode: false,
isRoadMode: false,
isLandMode: false,
isKemiskinanMode: false,
newMarkerType: 'ibadah',
isGhostLocked: false,
tempRoadCoords: [], tempRoadLayer: null,
tempLandCoords: [], tempLandLayer: null,
filterSPBU: 'Semua',
filterJalan: ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'],
filterTanah: ['SHM', 'HGB', 'HGU', 'HP'],
garisKemiskinan: 500000,
// KUNCI AUTENTIKASI
user: null,
authMode: 'login' // 'login' atau 'register'
};
const dataStore = {
spbu: {},
jalan: {},
tanah: {},
ibadah: {},
penduduk: {},
statusDarurat: {},
pelatihan: []
};
const mapLayers = {
spbu: {},
jalan: {},
tanah: {},
ibadah: {},
penduduk: {},
radiusIbadah: {}
};
const savedLat = parseFloat(localStorage.getItem('webgis_map_lat'));
const savedLng = parseFloat(localStorage.getItem('webgis_map_lng'));
const savedZoom = parseInt(localStorage.getItem('webgis_map_zoom'));
const startLat = !isNaN(savedLat) ? savedLat : -0.0227;
const startLng = !isNaN(savedLng) ? savedLng : 109.3366;
const startZoom = !isNaN(savedZoom) ? savedZoom : 15;
const map = L.map('map', { zoomControl: false }).setView([startLat, startLng], startZoom);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OSM'
}).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
function simpanPosisiPeta() {
const center = map.getCenter();
localStorage.setItem('webgis_map_lat', center.lat);
localStorage.setItem('webgis_map_lng', center.lng);
localStorage.setItem('webgis_map_zoom', map.getZoom());
}
map.on('moveend', simpanPosisiPeta);
map.on('zoomend', simpanPosisiPeta);
// ====================================================
// GHOST MARKER (EFEK KURSOR MENGIKUTI MOUSE)
// ====================================================
let ghostMarker = null;
map.on('mousemove', function(e) {
if (state.isKemiskinanMode && !state.isGhostLocked) {
let isIbadah = state.newMarkerType === 'ibadah';
let pinColor = isIbadah ? 'var(--gold)' : 'var(--navy)';
let iconHtml = isIbadah ?
`
` :
``;
if (!ghostMarker) {
ghostMarker = L.marker(e.latlng, {
icon: L.divIcon({
className: 'custom-div-icon',
html: iconHtml,
iconSize: [30, 42],
iconAnchor: [15, 42]
}),
interactive: false
}).addTo(map);
} else {
ghostMarker.setLatLng(e.latlng);
ghostMarker.setIcon(L.divIcon({
className: 'custom-div-icon',
html: iconHtml,
iconSize: [30, 42],
iconAnchor: [15, 42]
}));
}
} else if (!state.isKemiskinanMode) {
if (ghostMarker) {
map.removeLayer(ghostMarker);
ghostMarker = null;
}
}
});
// ====================================================
// CORE UTILITIES: GEOCODING, TOAST, FETCH
// ====================================================
const safeFetch = async (url, payload = null) => {
const options = payload ? {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
credentials: 'include'
} : {
credentials: 'include'
};
try {
const res = await fetch(url, options);
if (!res.ok) {
const errData = await res.json().catch(() => ({}));
throw new Error(errData.error || `HTTP Error: ${res.status}`);
}
return await res.json();
} catch(err) {
throw err;
}
};
async function dapatkanAlamat(lat, lng) {
try {
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`);
if (!response.ok) throw new Error();
const data = await response.json();
return data.display_name || "Alamat tidak spesifik";
} catch (error) {
return "Gagal melacak alamat dari satelit";
}
}
function triggerToastNotification(pesan) {
const toast = document.getElementById('toast-notification');
toast.innerText = pesan;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// ====================================================
// AUTENTIKASI: GATEKEEPER & SINKRONISASI SESI
// ====================================================
window.switchAuthTab = (mode) => {
state.authMode = mode;
document.getElementById('tab-login').classList.toggle('active', mode === 'login');
document.getElementById('tab-register').classList.toggle('active', mode === 'register');
document.getElementById('btn-submit-auth').innerText = mode === 'login' ? 'Login Sistem' : 'Daftar Akun Baru';
document.getElementById('auth-error-msg').innerText = '';
};
window.eksekusiAuth = () => {
const user = document.getElementById('auth-username').value;
const pass = document.getElementById('auth-password').value;
const errMsg = document.getElementById('auth-error-msg');
if(!user || !pass) {
errMsg.innerText = "Username dan Password tidak boleh kosong!";
return;
}
errMsg.innerText = "Memproses request ke server...";
const endpoint = state.authMode === 'login' ? 'otak_auth/api_login.php' : 'otak_auth/api_register.php';
safeFetch(endpoint, { username: user, password: pass })
.then(res => {
if(state.authMode === 'register') {
triggerToastNotification(res.pesan);
switchAuthTab('login');
} else {
state.user = res.user;
document.getElementById('auth-screen').style.display = 'none';
triggerToastNotification(`Selamat datang, ${res.user}!`);
muatDataSemua(); // Booting peta dilanjutkan
}
})
.catch(err => {
errMsg.innerText = err.message;
});
};
window.eksekusiLogout = () => {
if(!confirm("Anda yakin ingin keluar dari sistem?")) return;
safeFetch('otak_auth/api_logout.php')
.then(res => {
state.user = null;
document.getElementById('auth-screen').style.display = 'flex';
document.getElementById('auth-username').value = '';
document.getElementById('auth-password').value = '';
triggerToastNotification("Sesi diakhiri secara aman.");
// Bersihkan kanvas peta agar data sensitif tidak tertinggal
Object.values(mapLayers.spbu).forEach(m => map.removeLayer(m));
Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m));
Object.values(mapLayers.tanah).forEach(m => map.removeLayer(m));
Object.values(mapLayers.penduduk).forEach(m => map.removeLayer(m));
Object.values(mapLayers.ibadah).forEach(m => map.removeLayer(m));
Object.values(mapLayers.radiusIbadah).forEach(m => map.removeLayer(m));
})
.catch(err => console.error(err));
};
const cekSesiBackend = () => {
safeFetch('otak_auth/api_session.php')
.then(res => {
state.user = res.username;
document.getElementById('auth-screen').style.display = 'none';
muatDataSemua();
})
.catch(err => {
console.log("Tertahan di gerbang autentikasi: " + err.message);
document.getElementById('auth-screen').style.display = 'flex';
});
};
// ====================================================
// MANAJEMEN UI PERSISTENT & ALAT PETA
// ====================================================
window.toggleSidebar = () => {
const s = document.getElementById('sidebar-container');
const b = document.getElementById('toggle-sidebar');
s.classList.toggle('collapsed');
const isCollapsed = s.classList.contains('collapsed');
b.innerHTML = isCollapsed ? '❯' : '❮';
localStorage.setItem('webgis_sidebar_collapsed', isCollapsed);
setTimeout(() => map.invalidateSize(), 300);
};
window.switchTab = (tabId) => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
const targetBtn = document.querySelector(`.tab-btn[onclick="switchTab('${tabId}')"]`);
const targetContent = document.getElementById(tabId);
if (targetBtn && targetContent) {
targetBtn.classList.add('active');
targetContent.classList.add('active');
localStorage.setItem('webgis_active_tab', tabId);
}
};
function pulihkanIngatanUI() {
const isCollapsed = localStorage.getItem('webgis_sidebar_collapsed') === 'true';
if (isCollapsed) {
const s = document.getElementById('sidebar-container');
const b = document.getElementById('toggle-sidebar');
if (s && b) {
s.classList.add('collapsed');
b.innerHTML = '❯';
}
}
const savedTab = localStorage.getItem('webgis_active_tab');
if (savedTab) switchTab(savedTab);
}
window.lokasiSaya = () => {
triggerToastNotification("Mencari lokasi GPS...");
map.locate({enableHighAccuracy: true});
};
map.on('locationfound', function(e) {
map.setView(e.latlng, 16);
triggerToastNotification("Lokasi GPS ditemukan.");
});
map.on('locationerror', function(e) {
triggerToastNotification("Gagal mendeteksi lokasi GPS. Periksa izin browser Anda.");
});
window.bukaModalFilterLayer = () => {
document.getElementById('modal-filter-layer').style.display = 'flex';
};
window.bukaModalFilterSPBU = () => {
document.getElementById('modal-filter-spbu').style.display = 'flex';
};
window.terapkanFilterSPBU = () => {
state.filterSPBU = document.querySelector('input[name="rad-spbu"]:checked').value;
document.getElementById('modal-filter-spbu').style.display = 'none';
renderSPBU();
};
window.terapkanFilterLayer = () => {
state.filterJalan = Array.from(document.querySelectorAll('.cb-jalan:checked')).map(cb => cb.value);
state.filterTanah = Array.from(document.querySelectorAll('.cb-tanah:checked')).map(cb => cb.value);
document.getElementById('modal-filter-layer').style.display = 'none';
renderJalan();
renderTanah();
};
const setupDropdownColor = (idSelect, colorMap) => {
const select = document.getElementById(idSelect);
if(select) {
select.addEventListener('change', () => {
select.style.borderLeftColor = colorMap[select.value] || '#CBD5E0';
});
}
};
setupDropdownColor('jalan-status', {
'Jalan Nasional': '#E53E3E',
'Jalan Provinsi': '#3182CE',
'Jalan Kabupaten': '#38A169'
});
setupDropdownColor('tanah-status', {
'SHM': '#38A169',
'HGB': '#DD6B20',
'HGU': '#3182CE',
'HP': '#718096'
});
const resetAllModes = () => {
state.isMarkerMode = false;
const btnMarker = document.getElementById('btn-tool-marker');
if(btnMarker) btnMarker.classList.remove('active');
state.isRoadMode = false;
const btnJalan = document.getElementById('btn-tool-jalan');
if(btnJalan) btnJalan.classList.remove('active');
state.isLandMode = false;
const btnTanah = document.getElementById('btn-tool-tanah');
if(btnTanah) btnTanah.classList.remove('active');
state.isKemiskinanMode = false;
const btnKemiskinanMode = document.getElementById('btn-add-kemiskinan');
if(btnKemiskinanMode) btnKemiskinanMode.classList.remove('active');
state.isGhostLocked = false;
map.getContainer().style.cursor = '';
if (ghostMarker) {
map.removeLayer(ghostMarker);
ghostMarker = null;
}
map.closePopup();
Object.values(mapLayers.spbu).forEach(m => {
if(m.dragging) m.dragging.disable();
});
batalGambarJalan();
batalGambarTanah();
const btnKemiskinan = document.getElementById('btn-add-kemiskinan');
if(btnKemiskinan) {
btnKemiskinan.innerHTML = `+ Aktifkan Kursor Marker`;
}
};
window.toggleMarkerMode = () => {
const wasActive = state.isMarkerMode;
resetAllModes();
if (!wasActive) {
state.isMarkerMode = true;
document.getElementById('btn-tool-marker').classList.add('active');
map.getContainer().style.cursor = 'crosshair';
Object.values(mapLayers.spbu).forEach(m => {
if(m.dragging) m.dragging.enable();
});
}
};
window.toggleRoadMode = () => {
const wasActive = state.isRoadMode;
resetAllModes();
if (!wasActive) {
state.isRoadMode = true;
document.getElementById('btn-tool-jalan').classList.add('active');
map.getContainer().style.cursor = 'crosshair';
}
};
window.toggleLandMode = () => {
const wasActive = state.isLandMode;
resetAllModes();
if (!wasActive) {
state.isLandMode = true;
document.getElementById('btn-tool-tanah').classList.add('active');
map.getContainer().style.cursor = 'crosshair';
}
};
window.toggleKemiskinanMode = () => {
const wasActive = state.isKemiskinanMode;
resetAllModes();
if (!wasActive) {
state.isKemiskinanMode = true;
const btn = document.getElementById('btn-add-kemiskinan');
btn.classList.add('active');
btn.innerHTML = `📍 Mode Penempatan Aktif`;
map.getContainer().style.cursor = 'none';
}
};
const cbMarkerBaru = document.getElementById('pilihan-marker-baru');
if(cbMarkerBaru) {
cbMarkerBaru.addEventListener('change', function(e) {
state.newMarkerType = e.target.value;
});
}
const RightToolbar = L.Control.extend({
options: { position: 'topright' },
onAdd: function() {
const c = L.DomUtil.create('div', 'custom-leaflet-toolbar');
c.innerHTML = `
`;
L.DomEvent.disableClickPropagation(c);
return c;
}
});
map.addControl(new RightToolbar());
// ====================================================
// EVENT LISTENER ROUTER (KLIK PETA UTAMA)
// ====================================================
function bukaGembokForm(prefix) {
const nama = document.getElementById(`${prefix}-nama`);
const status = document.getElementById(`${prefix}-status`);
const telp = document.getElementById(`${prefix}-telp`);
if (nama) {
nama.readOnly = false;
nama.removeAttribute('readonly');
}
if (status) {
status.disabled = false;
status.removeAttribute('disabled');
}
if (telp) {
telp.readOnly = false;
telp.removeAttribute('readonly');
}
}
function resetFormModal() {
document.getElementById('spbu-id').value = "";
document.getElementById('spbu-nama').value = "";
document.getElementById('spbu-telp').value = "";
document.getElementById('spbu-status').value = "Buka 24 Jam";
if(document.getElementById('spbu-alamat')) {
document.getElementById('spbu-alamat').value = "";
}
bukaGembokForm('spbu');
}
map.on('click', async function(e) {
if (state.isMarkerMode) {
resetFormModal();
document.getElementById('spbu-modal-title').innerText = "Tambah SPBU Baru";
document.getElementById('spbu-lat').value = e.latlng.lat;
document.getElementById('spbu-lng').value = e.latlng.lng;
const alamatInput = document.getElementById('spbu-alamat');
if(alamatInput) alamatInput.value = "Sedang melacak alamat dari satelit...";
document.getElementById('spbu-action-wrapper').innerHTML = `
`;
document.getElementById('modal-spbu').style.display = 'flex';
setTimeout(() => {
const input = document.getElementById('spbu-nama');
if(input) input.focus();
}, 100);
const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng);
if(alamatInput) alamatInput.value = alamatDitemukan;
const btnSimpan = document.getElementById('btn-eksekusi-spbu');
if(btnSimpan) {
btnSimpan.innerText = "Simpan Lokasi";
btnSimpan.disabled = false;
}
}
else if (state.isRoadMode) {
state.tempRoadCoords.push([e.latlng.lat, e.latlng.lng]);
if (state.tempRoadLayer) {
map.removeLayer(state.tempRoadLayer);
}
state.tempRoadLayer = L.polyline(state.tempRoadCoords, {
color: '#1A365D',
weight: 4,
dashArray: '5, 8'
}).addTo(map);
const btn = document.getElementById('btn-selesai-jalan');
if (btn && state.tempRoadCoords.length >= 2) {
btn.style.display = 'block';
}
}
else if (state.isLandMode) {
state.tempLandCoords.push([e.latlng.lat, e.latlng.lng]);
if (state.tempLandLayer) {
map.removeLayer(state.tempLandLayer);
}
state.tempLandLayer = L.polygon(state.tempLandCoords, {
color: '#D69E2E',
fillColor: '#D69E2E',
fillOpacity: 0.4,
weight: 3,
dashArray: '5, 8'
}).addTo(map);
const btn = document.getElementById('btn-selesai-tanah');
if (btn && state.tempLandCoords.length >= 3) {
btn.style.display = 'block';
}
}
else if (state.isKemiskinanMode) {
if (state.isGhostLocked) {
triggerToastNotification("Selesaikan atau tutup formulir yang sedang terbuka terlebih dahulu.");
return;
}
state.isGhostLocked = true;
triggerToastNotification("Menganalisis satelit...");
const isIbadah = state.newMarkerType === 'ibadah';
const tmplId = isIbadah ? 'template-popup-ibadah' : 'template-popup-penduduk';
const tmpl = document.getElementById(tmplId);
if(!tmpl) return;
const clone = tmpl.content.cloneNode(true);
const wrapper = document.createElement('div');
wrapper.appendChild(clone);
const popup = L.popup({ closeOnClick: false, autoClose: false })
.setLatLng(e.latlng)
.setContent(wrapper)
.openOn(map);
popup.on('remove', function() {
state.isGhostLocked = false;
});
const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng);
if (isIbadah) {
const elAlamatIb = wrapper.querySelector('#pop-ib-alamat');
if (elAlamatIb) elAlamatIb.value = alamatDitemukan;
const radInput = wrapper.querySelector('#pop-ib-rad');
const radVal = wrapper.querySelector('#pop-ib-rad-val');
if (radInput && radVal) {
radInput.oninput = function() {
radVal.innerText = this.value;
};
}
const btnSimpan = wrapper.querySelector('#btn-pop-simpan');
if(btnSimpan) {
btnSimpan.innerText = "Simpan Ibadah";
btnSimpan.disabled = false;
btnSimpan.onclick = () => simpanIbadahPopup(e.latlng.lat, e.latlng.lng, popup, wrapper);
}
} else {
const elAlamatPd = wrapper.querySelector('#pop-pd-alamat');
if (elAlamatPd) elAlamatPd.value = alamatDitemukan;
const inputTanggungan = wrapper.querySelector('#pop-pd-tanggungan');
if(inputTanggungan) {
inputTanggungan.value = 0;
inputTanggungan.disabled = true;
inputTanggungan.setAttribute('title', 'Dihitung otomatis melalui menu Detail Keluarga');
}
const btnSimpan = wrapper.querySelector('#btn-pop-simpan');
if(btnSimpan) {
btnSimpan.innerText = "Simpan Penduduk";
btnSimpan.disabled = false;
btnSimpan.onclick = () => simpanPendudukPopup(e.latlng.lat, e.latlng.lng, popup, wrapper);
}
}
}
});
// ====================================================
// DECISION SUPPORT SYSTEM (DSS) KEMISKINAN (SKOR 0-5)
// ====================================================
function kalkulasiDSS(pengeluaran, pmtScore) {
if (pengeluaran > state.garisKemiskinan) return "Tidak Miskin";
if (pmtScore >= 4) return "Sangat Miskin";
if (pmtScore >= 2) return "Miskin";
if (pmtScore === 1) return "Rentan Miskin";
return "Tidak Miskin";
}
function kalkulasiBantuan(status) {
if (status === 'Sangat Miskin') return { beras: 15, minyak: 3, gula: 2, telur: 2, susu: 4, tunai: 300000 };
if (status === 'Miskin') return { beras: 10, minyak: 2, gula: 1, telur: 1, susu: 2, tunai: 150000 };
if (status === 'Rentan Miskin') return { beras: 5, minyak: 1, gula: 1, telur: 0, susu: 0, tunai: 0 };
return { beras: 0, minyak: 0, gula: 0, telur: 0, susu: 0, tunai: 0 };
}
// ====================================================
// DECOUPLED RENDERING ENGINE (PENGGAMBAR TUNGGAL)
// ====================================================
const agamaToIbadahMap = {
'Islam': 'Masjid',
'Kristen': 'Gereja',
'Katolik': 'Gereja',
'Hindu': 'Pura',
'Buddha': 'Vihara',
'Konghucu': 'Klenteng'
};
function renderSPBU() {
const cardContainer = document.getElementById('card-container');
if(cardContainer) cardContainer.innerHTML = '';
Object.values(mapLayers.spbu).forEach(m => map.removeLayer(m));
mapLayers.spbu = {};
Object.values(dataStore.spbu).forEach(d => {
if (state.filterSPBU !== 'Semua' && d.status_operasional !== state.filterSPBU) return;
const lat = parseFloat(d.latitude);
const lng = parseFloat(d.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const isBuka = d.status_operasional === 'Buka 24 Jam';
const customIcon = L.divIcon({
className: 'custom-div-icon',
html: ``,
iconSize: [30, 42],
iconAnchor: [15, 42]
});
const marker = L.marker([lat, lng], {
icon: customIcon,
draggable: state.isMarkerMode
}).addTo(map);
marker.bindTooltip(`${d.nama_spbu}`, {
direction: 'top',
offset: [0, -35],
className: 'spbu-tooltip'
});
marker.on('click', () => {
if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return;
document.getElementById('spbu-id').value = d.id;
document.getElementById('spbu-nama').value = d.nama_spbu;
document.getElementById('spbu-telp').value = d.no_telp;
document.getElementById('spbu-status').value = d.status_operasional;
if(document.getElementById('spbu-alamat')) {
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
}
bukaGembokForm('spbu');
document.getElementById('spbu-modal-title').innerText = "Detail & Kelola SPBU";
document.getElementById('spbu-action-wrapper').innerHTML = `
`;
document.getElementById('modal-spbu').style.display = 'flex';
});
marker.on('dragend', async function(e) {
const pos = e.target.getLatLng();
triggerToastNotification("⏳ Menganalisis alamat baru...");
const alamatBaru = await dapatkanAlamat(pos.lat, pos.lng);
safeFetch('otak_spbu/api_update_coords.php', {
id: d.id,
lat: pos.lat,
lng: pos.lng,
alamat: alamatBaru
})
.then(res => {
dataStore.spbu[d.id].latitude = pos.lat;
dataStore.spbu[d.id].longitude = pos.lng;
dataStore.spbu[d.id].alamat = alamatBaru;
triggerToastNotification("📍 Koordinat & Alamat berhasil digeser!");
renderSPBU();
}).catch(err => {
alert("Gagal.");
e.target.setLatLng([dataStore.spbu[d.id].latitude, dataStore.spbu[d.id].longitude]);
});
});
mapLayers.spbu[d.id] = marker;
const alamatRingkas = d.alamat ? (d.alamat.substring(0, 45) + '...') : 'Alamat belum direkam';
if(cardContainer) {
cardContainer.innerHTML += `
${d.nama_spbu}
${d.status_operasional}
${alamatRingkas}
`;
}
});
}
function renderJalan() {
Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m));
mapLayers.jalan = {};
Object.values(dataStore.jalan).forEach(d => {
if (!state.filterJalan.includes(d.status_jalan)) return;
try {
const coords = JSON.parse(d.koordinat);
if (!Array.isArray(coords) || coords.length < 2) return;
let lineColor = '#38A169', lineWeight = 4;
if (d.status_jalan === 'Jalan Nasional') {
lineColor = '#E53E3E';
lineWeight = 8;
} else if (d.status_jalan === 'Jalan Provinsi') {
lineColor = '#3182CE';
lineWeight = 6;
}
const polyline = L.polyline(coords, {
color: lineColor,
weight: lineWeight,
opacity: 0.8
}).addTo(map);
polyline.bindTooltip(`${d.nama_jalan}
${d.jarak_meter} meter`, {
direction: 'auto',
className: 'spbu-tooltip'
});
polyline.on('click', () => { handleRoadClick(d.id); });
mapLayers.jalan[d.id] = polyline;
} catch(e) {}
});
}
function renderTanah() {
Object.values(mapLayers.tanah).forEach(m => map.removeLayer(m));
mapLayers.tanah = {};
Object.values(dataStore.tanah).forEach(d => {
if (!state.filterTanah.includes(d.status_kepemilikan)) return;
try {
const coords = JSON.parse(d.koordinat);
if (!Array.isArray(coords) || coords.length < 3) return;
let fillColor = '#38A169';
if (d.status_kepemilikan === 'HGB') fillColor = '#DD6B20';
else if (d.status_kepemilikan === 'HGU') fillColor = '#3182CE';
else if (d.status_kepemilikan === 'HP') fillColor = '#718096';
const polygon = L.polygon(coords, {
color: fillColor,
fillColor: fillColor,
fillOpacity: 0.4,
weight: 2
}).addTo(map);
polygon.bindTooltip(`${d.nama_pemilik}
${d.luas_meter} m²`, {
direction: 'auto',
className: 'spbu-tooltip'
});
polygon.on('click', () => { handleLandClick(d.id); });
mapLayers.tanah[d.id] = polygon;
} catch(e) {}
});
}
function renderKemiskinan() {
const listContainer = document.getElementById('kemiskinan-card-container');
if(listContainer) listContainer.innerHTML = '';
Object.values(mapLayers.ibadah).forEach(m => map.removeLayer(m));
mapLayers.ibadah = {};
Object.values(mapLayers.radiusIbadah).forEach(m => map.removeLayer(m));
mapLayers.radiusIbadah = {};
Object.values(mapLayers.penduduk).forEach(m => map.removeLayer(m));
mapLayers.penduduk = {};
const naunganCounter = {};
Object.keys(dataStore.ibadah).forEach(id => naunganCounter[id] = 0);
const pendudukDiproses = [];
Object.values(dataStore.penduduk).forEach(p => {
const pLat = parseFloat(p.latitude);
const pLng = parseFloat(p.longitude);
if (isNaN(pLat) || isNaN(pLng)) return;
const pos = L.latLng(pLat, pLng);
let adaNaungan = false;
let jarakTerdekat = Infinity;
let idIbadahTerpilih = null;
let teksNaungan = "Tidak berada di bawah wewenang rumah ibadah manapun";
if (p.status_kemiskinan !== 'Tidak Miskin') {
const jenisIbadahDicari = agamaToIbadahMap[p.agama];
if (jenisIbadahDicari) {
Object.values(dataStore.ibadah).forEach(ib => {
if (ib.jenis === jenisIbadahDicari) {
const ibLat = parseFloat(ib.latitude);
const ibLng = parseFloat(ib.longitude);
if (!isNaN(ibLat) && !isNaN(ibLng)) {
const jarakM = map.distance(pos, L.latLng(ibLat, ibLng));
if (jarakM <= ib.radius_meter && jarakM < jarakTerdekat) {
jarakTerdekat = jarakM;
idIbadahTerpilih = ib.id;
adaNaungan = true;
let namaIb = ib.nama;
if (!namaIb.toLowerCase().startsWith(ib.jenis.toLowerCase())) {
namaIb = `${ib.jenis} ${ib.nama}`;
}
teksNaungan = `Bernaung di bawah wewenang: ${namaIb}`;
}
}
}
});
}
}
if (adaNaungan && idIbadahTerpilih) {
naunganCounter[idIbadahTerpilih]++;
}
pendudukDiproses.push({
dataMentah: p,
lat: pLat,
lng: pLng,
bernaung: adaNaungan,
teksNaungan: teksNaungan
});
});
Object.values(dataStore.ibadah).forEach(d => {
const lat = parseFloat(d.latitude);
const lng = parseFloat(d.longitude);
if (isNaN(lat) || isNaN(lng)) return;
let judulIbadah = d.nama;
const jenisLower = d.jenis.toLowerCase();
if (!judulIbadah.toLowerCase().startsWith(jenisLower)) {
judulIbadah = `${d.jenis} ${d.nama}`;
}
const circle = L.circle([lat, lng], {
color: '#D69E2E',
fillColor: '#D69E2E',
fillOpacity: 0.15,
radius: d.radius_meter,
weight: 1
}).addTo(map);
mapLayers.radiusIbadah[d.id] = circle;
const icon = L.divIcon({
className: 'custom-div-icon',
html: ``,
iconSize: [30, 42],
iconAnchor: [15, 42]
});
const marker = L.marker([lat, lng], { icon: icon }).addTo(map);
marker.bindTooltip(`${judulIbadah}
Radius: ${d.radius_meter}m
Naungan: ${naunganCounter[d.id]} KK`, {
direction: 'top',
className: 'spbu-tooltip'
});
marker.on('click', () => {
if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return;
const tmpl = document.getElementById('template-popup-read-ibadah');
if(!tmpl) return;
const clone = tmpl.content.cloneNode(true);
clone.querySelector('#read-ib-nama').innerText = judulIbadah;
clone.querySelector('#read-ib-jenis').innerText = `Jenis: ${d.jenis}`;
clone.querySelector('#read-ib-radius').innerText = `Radius Jangkauan: ${d.radius_meter} Meter`;
clone.querySelector('#read-ib-naungan').innerText = `Keluarga dalam radius: ${naunganCounter[d.id]} KK`;
clone.querySelector('#read-ib-alamat').innerText = d.alamat || "Alamat tidak direkam";
const wrapper = document.createElement('div');
wrapper.appendChild(clone);
const popup = L.popup({ minWidth: 260 })
.setLatLng([lat, lng])
.setContent(wrapper)
.openOn(map);
const btnEdit = wrapper.querySelector('#btn-ib-edit');
if(btnEdit) btnEdit.onclick = () => {
map.closePopup(popup);
bukaFormEditIbadah(d, d.nama);
};
const btnHapus = wrapper.querySelector('#btn-ib-hapus');
if(btnHapus) btnHapus.onclick = () => {
if(confirm(`Yakin ingin menghapus permanen ${judulIbadah}?`)) {
map.closePopup(popup);
eksekusiHapusIbadah(d.id);
}
};
});
mapLayers.ibadah[d.id] = marker;
if(listContainer) {
listContainer.innerHTML += `
🕌 ${judulIbadah}
${d.jenis} | ${d.radius_meter}m | ${naunganCounter[d.id]} KK
`;
}
});
pendudukDiproses.forEach(item => {
const p = item.dataMentah;
let isDarurat = dataStore.statusDarurat[p.id] === true;
let pinColor = item.bernaung ? '#3182CE' : '#805AD5';
let markerClass = isDarurat ? 'marker-wrapper marker-darurat' : 'marker-wrapper';
const icon = L.divIcon({
className: 'custom-div-icon',
html: ``,
iconSize: [30, 42],
iconAnchor: [15, 42]
});
const marker = L.marker([item.lat, item.lng], { icon: icon }).addTo(map);
marker.bindTooltip(`KK: ${p.nama_kk}
PMT: ${p.pmt_score} | ${p.status_kemiskinan}`, {
direction: 'top',
className: 'spbu-tooltip'
});
marker.on('click', () => {
if (state.isMarkerMode || state.isRoadMode || state.isLandMode || state.isKemiskinanMode) return;
const tmpl = document.getElementById('template-popup-read-penduduk');
if(!tmpl) return;
const clone = tmpl.content.cloneNode(true);
clone.querySelector('#read-pd-nama').innerText = `Keluarga ${p.nama_kk}`;
clone.querySelector('#read-pd-alamat').innerText = p.alamat || "Alamat tidak direkam";
const elAgama = clone.querySelector('#read-pd-agama');
if(elAgama) elAgama.innerText = `Agama: ${p.agama || '-'}`;
const elAnggota = clone.querySelector('#read-pd-anggota');
if(elAnggota) {
const totalJiwa = parseInt(p.real_tanggungan) || 0;
elAnggota.innerText = `Anggota/Tanggungan: ${totalJiwa} Jiwa`;
}
const badge = clone.querySelector('#read-pd-badge');
badge.innerText = p.status_kemiskinan;
if (p.status_kemiskinan === 'Sangat Miskin') badge.className = 'pop-badge badge-sangat-miskin';
else if (p.status_kemiskinan === 'Miskin') badge.className = 'pop-badge badge-miskin';
else if (p.status_kemiskinan === 'Rentan Miskin') badge.className = 'pop-badge badge-rentan';
else badge.className = 'pop-badge badge-aman';
const elNaungan = clone.querySelector('#read-pd-naungan');
if(elNaungan) {
elNaungan.innerText = item.teksNaungan;
elNaungan.className = item.bernaung ? 'pop-naungan naungan-aktif' : 'pop-naungan naungan-pasif';
}
const accContent = clone.querySelector('#acc-pd-content');
const btnMusibah = document.createElement('button');
btnMusibah.className = 'btn btn-danger';
btnMusibah.innerText = 'Lapor Darurat Musibah';
btnMusibah.style.width = '100%';
btnMusibah.style.marginBottom = '8px';
btnMusibah.style.padding = '12px';
const btnDetail = document.createElement('button');
btnDetail.className = 'btn btn-warning';
btnDetail.innerText = 'Detail Anggota Keluarga';
btnDetail.style.width = '100%';
btnDetail.style.marginTop = '0px';
btnDetail.style.marginBottom = '15px';
btnDetail.style.padding = '12px';
const popActionMain = clone.querySelector('.pop-action-main');
if(popActionMain) {
if(!item.bernaung) {
popActionMain.innerHTML = '';
} else {
popActionMain.insertBefore(btnMusibah, clone.querySelector('#btn-pd-bantuan'));
btnMusibah.onclick = () => { bukaFormMusibah(p); };
const btnBantuan = clone.querySelector('#btn-pd-bantuan');
if(btnBantuan) btnBantuan.onclick = () => { bukaFormLogBantuan(p); };
}
popActionMain.appendChild(btnDetail);
btnDetail.onclick = () => { bukaFormDetailKeluarga(p, item.bernaung); };
}
const btnOpsi = clone.querySelector('#btn-pd-opsi');
const wrapper = document.createElement('div');
wrapper.appendChild(clone);
const popup = L.popup({ minWidth: 260 })
.setLatLng([item.lat, item.lng])
.setContent(wrapper)
.openOn(map);
if(btnOpsi && accContent) {
btnOpsi.onclick = () => {
accContent.classList.toggle('show');
btnOpsi.innerText = accContent.classList.contains('show') ? 'Tutup Opsi' : 'Opsi Lainnya';
popup.update();
};
}
// Integrasi Tombol Riwayat
const btnRiwayatPelatihan = wrapper.querySelector('#btn-pd-riwayat-pelatihan');
if(btnRiwayatPelatihan) {
btnRiwayatPelatihan.onclick = () => { bukaFormRiwayatPelatihan(p); };
}
const btnRiwayatMusibah = wrapper.querySelector('#btn-pd-riwayat-musibah');
if(btnRiwayatMusibah) {
btnRiwayatMusibah.onclick = () => { bukaFormRiwayatMusibah(p); };
}
const btnRiwayatBantuan = wrapper.querySelector('#btn-pd-riwayat-bantuan');
if(btnRiwayatBantuan) {
btnRiwayatBantuan.onclick = () => { bukaFormRiwayatBantuan(p); };
}
const btnEdit = wrapper.querySelector('#btn-pd-edit');
if(btnEdit) {
btnEdit.onclick = () => {
map.closePopup(popup);
bukaFormEditPenduduk(p);
};
}
const btnHapus = wrapper.querySelector('#btn-pd-hapus');
if(btnHapus) {
btnHapus.onclick = () => {
if(confirm(`Anda yakin ingin menghapus permanen data keluarga ${p.nama_kk}?`)) {
map.closePopup(popup);
eksekusiHapusPenduduk(p.id);
}
};
}
});
mapLayers.penduduk[p.id] = marker;
let cls = 'card-penduduk-rentan';
if(p.status_kemiskinan === 'Sangat Miskin' || p.status_kemiskinan === 'Miskin') cls = 'card-penduduk-miskin';
if(p.status_kemiskinan === 'Tidak Miskin') cls = 'card-penduduk-aman';
if(listContainer) {
listContainer.innerHTML += `
🏠 Kel. ${p.nama_kk}
PMT: ${p.pmt_score}/5 | Rp${p.pengeluaran}
`;
}
});
}
// ====================================================
// GEOMETRY BUILDER HELPERS (Jalan & Tanah Selesai)
// ====================================================
window.lihatSpbuDariCard = function(id) {
if (!dataStore.spbu[id]) return;
const d = dataStore.spbu[id];
document.getElementById('spbu-id').value = d.id;
document.getElementById('spbu-nama').value = d.nama_spbu;
document.getElementById('spbu-telp').value = d.no_telp;
document.getElementById('spbu-status').value = d.status_operasional;
if(document.getElementById('spbu-alamat')) {
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
}
document.getElementById('spbu-nama').readOnly = true;
document.getElementById('spbu-telp').readOnly = true;
document.getElementById('spbu-status').disabled = true;
document.getElementById('spbu-modal-title').innerText = "Informasi SPBU (Read-Only)";
document.getElementById('spbu-action-wrapper').innerHTML = `
`;
document.getElementById('modal-spbu').style.display = 'flex';
};
window.selesaiGambarJalan = function() {
if (state.tempRoadCoords.length < 2) return;
let totalJarak = 0;
for (let i = 0; i < state.tempRoadCoords.length - 1; i++) {
totalJarak += map.distance(L.latLng(state.tempRoadCoords[i]), L.latLng(state.tempRoadCoords[i+1]));
}
document.getElementById('jalan-id').value = "";
document.getElementById('jalan-nama').value = "";
document.getElementById('jalan-status').value = "Jalan Kabupaten";
document.getElementById('jalan-status').dispatchEvent(new Event('change'));
document.getElementById('jalan-jarak').value = Math.round(totalJarak);
document.getElementById('jalan-koordinat').value = JSON.stringify(state.tempRoadCoords);
bukaGembokForm('jalan');
document.getElementById('jalan-action-wrapper').innerHTML = `
`;
document.getElementById('modal-jalan').style.display = 'flex';
document.getElementById('btn-selesai-jalan').style.display = 'none';
};
window.batalGambarJalan = function() {
state.tempRoadCoords = [];
if (state.tempRoadLayer) map.removeLayer(state.tempRoadLayer);
state.tempRoadLayer = null;
const btn = document.getElementById('btn-selesai-jalan');
if (btn) btn.style.display = 'none';
if(document.getElementById('modal-jalan')) {
document.getElementById('modal-jalan').style.display = 'none';
}
};
window.handleRoadClick = function(id) {
if (state.isRoadMode || state.isMarkerMode || state.isLandMode || !dataStore.jalan[id]) return;
const d = dataStore.jalan[id];
document.getElementById('jalan-id').value = d.id;
document.getElementById('jalan-nama').value = d.nama_jalan;
document.getElementById('jalan-status').value = d.status_jalan;
document.getElementById('jalan-jarak').value = d.jarak_meter;
document.getElementById('jalan-status').dispatchEvent(new Event('change'));
bukaGembokForm('jalan');
document.getElementById('jalan-modal-title').innerText = "Detail & Kelola Jalan";
document.getElementById('jalan-action-wrapper').innerHTML = `
`;
document.getElementById('modal-jalan').style.display = 'flex';
};
window.selesaiGambarTanah = function() {
if (state.tempLandCoords.length < 3) return;
let area = 0, R = 6378137;
const ll = state.tempLandCoords.map(c => L.latLng(c[0], c[1]));
for (let i = 0; i < ll.length; i++) {
let p1 = ll[i], p2 = ll[(i + 1) % ll.length];
area += (p2.lng - p1.lng) * Math.PI / 180 * (2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180));
}
document.getElementById('tanah-id').value = "";
document.getElementById('tanah-nama').value = "";
document.getElementById('tanah-status').value = "SHM";
document.getElementById('tanah-status').dispatchEvent(new Event('change'));
document.getElementById('tanah-luas').value = Math.round(Math.abs(area * R * R / 2.0));
document.getElementById('tanah-koordinat').value = JSON.stringify(state.tempLandCoords);
bukaGembokForm('tanah');
document.getElementById('tanah-action-wrapper').innerHTML = `
`;
document.getElementById('modal-tanah').style.display = 'flex';
document.getElementById('btn-selesai-tanah').style.display = 'none';
};
window.batalGambarTanah = function() {
state.tempLandCoords = [];
if (state.tempLandLayer) map.removeLayer(state.tempLandLayer);
state.tempLandLayer = null;
const btn = document.getElementById('btn-selesai-tanah');
if (btn) btn.style.display = 'none';
if(document.getElementById('modal-tanah')) {
document.getElementById('modal-tanah').style.display = 'none';
}
};
window.handleLandClick = function(id) {
if (state.isRoadMode || state.isMarkerMode || state.isLandMode || !dataStore.tanah[id]) return;
const d = dataStore.tanah[id];
document.getElementById('tanah-id').value = d.id;
document.getElementById('tanah-nama').value = d.nama_pemilik;
document.getElementById('tanah-status').value = d.status_kepemilikan;
document.getElementById('tanah-luas').value = d.luas_meter;
document.getElementById('tanah-status').dispatchEvent(new Event('change'));
bukaGembokForm('tanah');
document.getElementById('tanah-modal-title').innerText = "Detail & Kelola Tanah";
document.getElementById('tanah-action-wrapper').innerHTML = `
`;
document.getElementById('modal-tanah').style.display = 'flex';
};
// ====================================================
// CRUD PENYIMPANAN
// ====================================================
window.eksekusiUpdateGaris = () => {
const val = document.getElementById('input-garis-kemiskinan').value;
if(!val || val <= 0) return alert("Masukkan nilai rupiah yang valid!");
if(!confirm(`PERINGATAN: Mengubah garis kemiskinan menjadi Rp${val} akan mengkalibrasi ulang status miskin/tidak miskin seluruh penduduk di database saat ini.\n\nLanjutkan?`)) return;
triggerToastNotification("⏳ Mengkalibrasi ulang seluruh data penduduk...");
safeFetch('otak_kemiskinan/api_update_gariskemiskinan.php', { nilai: parseInt(val) })
.then(res => {
triggerToastNotification("✅ " + res.pesan);
document.getElementById('input-garis-kemiskinan').value = "";
muatDataSemua();
})
.catch(err => alert("Gagal memperbarui: " + err));
};
window.eksekusiSimpanBaru = () => {
const payload = {
nama: document.getElementById('spbu-nama').value,
status: document.getElementById('spbu-status').value,
telp: document.getElementById('spbu-telp').value,
alamat: document.getElementById('spbu-alamat') ? document.getElementById('spbu-alamat').value : "",
lat: document.getElementById('spbu-lat').value,
lng: document.getElementById('spbu-lng').value
};
safeFetch('otak_spbu/api_simpan_spbu.php', payload)
.then(() => {
document.getElementById('modal-spbu').style.display='none';
triggerToastNotification("✅ SPBU ditambahkan!");
muatDataSemua();
})
.catch(err => alert(`Gagal menyimpan data.\nAlasan: ${err.message}`));
};
window.eksekusiUpdate = (id) => {
const payload = {
id: id,
nama: document.getElementById('spbu-nama').value,
status: document.getElementById('spbu-status').value,
telp: document.getElementById('spbu-telp').value
};
safeFetch('otak_spbu/api_update_spbu.php', payload)
.then(() => {
document.getElementById('modal-spbu').style.display='none';
triggerToastNotification("✅ SPBU diperbarui!");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiHapus = (id) => {
if(!confirm("Hapus SPBU ini?")) return;
safeFetch('otak_spbu/api_hapus_spbu.php', { id: id })
.then(() => {
document.getElementById('modal-spbu').style.display='none';
triggerToastNotification("🗑️ SPBU dihapus.");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiSimpanJalan = () => {
const payload = {
nama: document.getElementById('jalan-nama').value,
status: document.getElementById('jalan-status').value,
jarak: document.getElementById('jalan-jarak').value,
koordinat: document.getElementById('jalan-koordinat').value
};
safeFetch('otak_jalan/api_simpan_jalan.php', payload)
.then(() => {
batalGambarJalan();
triggerToastNotification("✅ Jalan ditambahkan!");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiUpdateJalan = (id) => {
const payload = {
id: id,
nama: document.getElementById('jalan-nama').value,
status: document.getElementById('jalan-status').value
};
safeFetch('otak_jalan/api_update_jalan.php', payload)
.then(() => {
document.getElementById('modal-jalan').style.display='none';
triggerToastNotification("✅ Jalan diperbarui!");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiHapusJalan = (id) => {
if(!confirm("Hapus jalur ini?")) return;
safeFetch('otak_jalan/api_hapus_jalan.php', { id: id })
.then(() => {
document.getElementById('modal-jalan').style.display='none';
triggerToastNotification("🗑️ Jalan dihapus.");
muatDataSemua();
})
.catch(err => alert(err));
};
// --- INJEKSI FUNGSI TANAH YANG HILANG ---
window.eksekusiSimpanTanah = () => {
const payload = {
nama: document.getElementById('tanah-nama').value,
status: document.getElementById('tanah-status').value,
luas: document.getElementById('tanah-luas').value,
koordinat: document.getElementById('tanah-koordinat').value
};
safeFetch('otak_tanah/api_simpan_tanah.php', payload)
.then(() => {
batalGambarTanah();
triggerToastNotification("✅ Area Tanah ditambahkan!");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiUpdateTanah = (id) => {
const payload = {
id: id,
nama: document.getElementById('tanah-nama').value,
status: document.getElementById('tanah-status').value
};
safeFetch('otak_tanah/api_update_tanah.php', payload)
.then(() => {
document.getElementById('modal-tanah').style.display='none';
triggerToastNotification("✅ Area Tanah diperbarui!");
muatDataSemua();
})
.catch(err => alert(err));
};
window.eksekusiHapusTanah = (id) => {
if(!confirm("Hapus area tanah ini?")) return;
safeFetch('otak_tanah/api_hapus_tanah.php', { id: id })
.then(() => {
document.getElementById('modal-tanah').style.display='none';
triggerToastNotification("🗑️ Area Tanah dihapus.");
muatDataSemua();
})
.catch(err => alert(err));
};
// ----------------------------------------
window.simpanPendudukPopup = (lat, lng, popupRef, wrapper) => {
const payload = {
nama: wrapper.querySelector('#pop-pd-kk').value,
agama: wrapper.querySelector('#pop-pd-agama').value,
tanggungan: 0,
pengeluaran: parseInt(wrapper.querySelector('#pop-pd-pengeluaran').value) || 0,
lat: lat,
lng: lng,
alamat: wrapper.querySelector('#pop-pd-alamat').value,
pmt_lantai: parseInt(wrapper.querySelector('#pop-pmt-lantai').value),
pmt_dinding: parseInt(wrapper.querySelector('#pop-pmt-dinding').value),
pmt_sanitasi: parseInt(wrapper.querySelector('#pop-pmt-sanitasi').value),
pmt_penerangan: parseInt(wrapper.querySelector('#pop-pmt-listrik').value),
pmt_air: parseInt(wrapper.querySelector('#pop-pmt-air').value)
};
if (!payload.nama) return alert("Nama Kepala Keluarga wajib diisi!");
payload.pmt_score = payload.pmt_lantai + payload.pmt_dinding + payload.pmt_sanitasi + payload.pmt_penerangan + payload.pmt_air;
payload.status = kalkulasiDSS(payload.pengeluaran, payload.pmt_score);
safeFetch('otak_kemiskinan/api_simpan_penduduk.php', payload)
.then(() => {
map.closePopup(popupRef);
resetAllModes();
triggerToastNotification(`DSS Dieksekusi: ${payload.status}`);
muatDataSemua();
})
.catch(err => alert("Gagal menyimpan data: " + err));
};
window.bukaFormEditPenduduk = (p) => {
state.isGhostLocked = true;
const tmpl = document.getElementById('template-popup-penduduk');
if(!tmpl) return;
const clone = tmpl.content.cloneNode(true);
const wrapper = document.createElement('div');
wrapper.appendChild(clone);
const popup = L.popup({ closeOnClick: false, autoClose: false })
.setLatLng([parseFloat(p.latitude), parseFloat(p.longitude)])
.setContent(wrapper)
.openOn(map);
popup.on('remove', function() {
state.isGhostLocked = false;
});
wrapper.querySelector('#pop-pd-kk').value = p.nama_kk;
wrapper.querySelector('#pop-pd-agama').value = p.agama;
const inputTanggungan = wrapper.querySelector('#pop-pd-tanggungan');
if(inputTanggungan) {
inputTanggungan.value = p.real_tanggungan || 0;
inputTanggungan.disabled = true;
}
wrapper.querySelector('#pop-pd-pengeluaran').value = p.pengeluaran;
wrapper.querySelector('#pop-pmt-lantai').value = p.pmt_lantai;
wrapper.querySelector('#pop-pmt-dinding').value = p.pmt_dinding;
wrapper.querySelector('#pop-pmt-sanitasi').value = p.pmt_sanitasi;
wrapper.querySelector('#pop-pmt-listrik').value = p.pmt_penerangan;
wrapper.querySelector('#pop-pmt-air').value = p.pmt_air;
wrapper.querySelector('#pop-pd-alamat').value = p.alamat;
const btnSimpan = wrapper.querySelector('#btn-pop-simpan');
if(btnSimpan) {
btnSimpan.innerText = "Update Data Keluarga";
btnSimpan.disabled = false;
btnSimpan.onclick = () => updatePendudukPopup(p.id, popup, wrapper);
}
};
window.updatePendudukPopup = (id, popupRef, wrapper) => {
const payload = {
id: id,
nama: wrapper.querySelector('#pop-pd-kk').value,
agama: wrapper.querySelector('#pop-pd-agama').value,
tanggungan: 0,
pengeluaran: parseInt(wrapper.querySelector('#pop-pd-pengeluaran').value) || 0,
pmt_lantai: parseInt(wrapper.querySelector('#pop-pmt-lantai').value),
pmt_dinding: parseInt(wrapper.querySelector('#pop-pmt-dinding').value),
pmt_sanitasi: parseInt(wrapper.querySelector('#pop-pmt-sanitasi').value),
pmt_penerangan: parseInt(wrapper.querySelector('#pop-pmt-listrik').value),
pmt_air: parseInt(wrapper.querySelector('#pop-pmt-air').value)
};
if (!payload.nama) return alert("Nama Kepala Keluarga wajib diisi!");
payload.pmt_score = payload.pmt_lantai + payload.pmt_dinding + payload.pmt_sanitasi + payload.pmt_penerangan + payload.pmt_air;
payload.status = kalkulasiDSS(payload.pengeluaran, payload.pmt_score);
safeFetch('otak_kemiskinan/api_update_penduduk.php', payload)
.then(() => {
map.closePopup(popupRef);
resetAllModes();
triggerToastNotification(`Data Diperbarui: ${payload.status}`);
muatDataSemua();
})
.catch(err => alert("Gagal mengupdate data: " + err));
};
window.eksekusiHapusPenduduk = (id) => {
safeFetch('otak_kemiskinan/api_hapus_penduduk.php', { id: id })
.then(() => {
triggerToastNotification("🗑️ Data Penduduk dihapus permanen.");
muatDataSemua();
})
.catch(err => alert(err));
};
// --- LOG BANTUAN ---
window.bukaFormLogBantuan = (p) => {
document.getElementById('log-modal-title').innerText = "Catat Penyaluran Bantuan";
document.getElementById('log-nama-penerima').innerText = `Keluarga ${p.nama_kk}`;
document.getElementById('log-id-penduduk').value = p.id;
const rekomendasi = kalkulasiBantuan(p.status_kemiskinan);
document.getElementById('log-beras').value = rekomendasi.beras;
document.getElementById('log-minyak').value = rekomendasi.minyak;
document.getElementById('log-gula').value = rekomendasi.gula;
document.getElementById('log-telur').value = rekomendasi.telur;
document.getElementById('log-susu').value = rekomendasi.susu;
document.getElementById('log-tunai').value = rekomendasi.tunai;
document.getElementById('log-catatan').value = "";
document.getElementById('modal-log-bantuan').style.display = 'flex';
};
window.eksekusiSimpanLogBantuan = () => {
const id = document.getElementById('log-id-penduduk').value;
const payload = {
id_penduduk: id,
beras: parseInt(document.getElementById('log-beras').value) || 0,
minyak: parseInt(document.getElementById('log-minyak').value) || 0,
gula: parseInt(document.getElementById('log-gula').value) || 0,
telur: parseInt(document.getElementById('log-telur').value) || 0,
susu: parseInt(document.getElementById('log-susu').value) || 0,
tunai: parseInt(document.getElementById('log-tunai').value) || 0,
catatan: document.getElementById('log-catatan').value
};
safeFetch('otak_kemiskinan/api_simpan_logbantuan.php', payload)
.then(() => {
document.getElementById('modal-log-bantuan').style.display = 'none';
triggerToastNotification("✅ Log bantuan berhasil dicatat.");
if(dataStore.statusDarurat[id]) {
dataStore.statusDarurat[id] = false;
triggerToastNotification("Penanganan Bantuan menghentikan status Darurat keluarga ini.");
renderKemiskinan();
}
})
.catch(err => alert("Gagal menyimpan log bantuan: " + err));
};
// --- LOGIKA RIWAYAT ---
window.bukaFormRiwayatBantuan = (p) => {
document.getElementById('riwayat-nama-penerima').innerText = p.nama_kk;
document.getElementById('modal-riwayat-bantuan').style.display = 'flex';
const tbody = document.getElementById('riwayat-tbody');
tbody.innerHTML = `| Mengambil data dari satelit... |
`;
fetch(`otak_kemiskinan/api_tampil_logbantuan.php?id=${p.id}`)
.then(res => res.json())
.then(data => {
if(data.error) throw new Error(data.error);
if(data.length === 0) {
tbody.innerHTML = `| Belum ada riwayat penerimaan logistik. |
`;
return;
}
let html = '';
data.forEach(log => {
const tgl = log.created_at || log.waktu || log.tanggal || '-';
html += `
| ${tgl} |
${log.beras_kg} |
${log.minyak_l} |
${log.gula_kg} |
${log.telur_kg} |
${log.susu_kaleng} |
Rp${parseInt(log.tunai_rp).toLocaleString('id-ID')} |
${log.catatan || '-'} |
`;
});
tbody.innerHTML = html;
})
.catch(err => {
tbody.innerHTML = `| Gagal memuat histori: ${err.message} |
`;
});
};
window.bukaFormRiwayatPelatihan = (p) => {
document.getElementById('riwayat-pelatihan-nama-penerima').innerText = p.nama_kk;
document.getElementById('modal-riwayat-pelatihan').style.display = 'flex';
const tbody = document.getElementById('riwayat-pelatihan-tbody');
tbody.innerHTML = `| Mengambil data dari satelit... |
`;
fetch(`otak_kemiskinan/api_tampil_logpelatihan.php?id=${p.id}`)
.then(res => res.json())
.then(data => {
if(data.error) throw new Error(data.error);
if(data.length === 0) {
tbody.innerHTML = `| Belum ada anggota keluarga yang mengikuti pelatihan. |
`;
return;
}
let html = '';
data.forEach(log => {
const tgl = log.tanggal_daftar || '-';
html += `
| ${tgl} |
${log.nik} |
${log.nama_lengkap} |
${log.nama_pelatihan} |
${log.instansi_penyelenggara} |
`;
});
tbody.innerHTML = html;
})
.catch(err => {
tbody.innerHTML = `| Gagal memuat histori: ${err.message} |
`;
});
};
window.bukaFormRiwayatMusibah = (p) => {
document.getElementById('riwayat-musibah-nama-penerima').innerText = p.nama_kk;
document.getElementById('modal-riwayat-musibah').style.display = 'flex';
const tbody = document.getElementById('riwayat-musibah-tbody');
tbody.innerHTML = `| Mengambil data dari satelit... |
`;
fetch(`otak_kemiskinan/api_tampil_musibah.php?id=${p.id}`)
.then(res => res.json())
.then(data => {
if(data.error) throw new Error(data.error);
if(data.length === 0) {
tbody.innerHTML = `| Tidak ada riwayat pelaporan musibah. |
`;
return;
}
let html = '';
data.forEach(log => {
const tgl = log.tanggal_lapor || '-';
const statusColor = log.status_penanganan === 'Sudah Ditangani' ? 'var(--green-safe)' : 'var(--red-danger)';
html += `
| ${tgl} |
${log.jenis_musibah} |
${log.deskripsi || '-'} |
${log.status_penanganan} |
`;
});
tbody.innerHTML = html;
})
.catch(err => {
tbody.innerHTML = `| Gagal memuat histori: ${err.message} |
`;
});
};
// --- LOGIKA MUSIBAH & PELATIHAN ---
window.bukaFormMusibah = (p) => {
document.getElementById('musibah-id-penduduk').value = p.id;
document.getElementById('musibah-nama').innerText = p.nama_kk;
document.getElementById('musibah-tgl').value = new Date().toISOString().split('T')[0];
document.getElementById('musibah-deskripsi').value = "";
document.getElementById('modal-musibah').style.display = 'flex';
};
window.simpanMusibah = () => {
const id = document.getElementById('musibah-id-penduduk').value;
const payload = {
id_penduduk: id,
tanggal: document.getElementById('musibah-tgl').value,
jenis: document.getElementById('musibah-jenis').value,
deskripsi: document.getElementById('musibah-deskripsi').value,
status: document.getElementById('musibah-status').value
};
if(!payload.tanggal || !payload.jenis) return alert("Tanggal dan Jenis Musibah wajib diisi!");
safeFetch('otak_kemiskinan/api_simpan_musibah.php', payload)
.then(res => {
triggerToastNotification("🚨 " + res.pesan);
dataStore.statusDarurat[id] = true;
document.getElementById('modal-musibah').style.display = 'none';
renderKemiskinan();
})
.catch(err => alert("Gagal mengirim laporan musibah: " + err));
};
window.bukaModalPelatihan = () => {
document.getElementById('latih-nama').value = '';
document.getElementById('latih-instansi').value = '';
document.getElementById('latih-mulai').value = '';
document.getElementById('latih-selesai').value = '';
document.getElementById('latih-deskripsi').value = '';
document.getElementById('modal-master-pelatihan').style.display = 'flex';
};
window.eksekusiSimpanPelatihan = () => {
const payload = {
nama_pelatihan: document.getElementById('latih-nama').value,
instansi: document.getElementById('latih-instansi').value,
tgl_mulai: document.getElementById('latih-mulai').value,
tgl_selesai: document.getElementById('latih-selesai').value,
deskripsi: document.getElementById('latih-deskripsi').value
};
if(!payload.nama_pelatihan || !payload.instansi || !payload.tgl_mulai || !payload.tgl_selesai) {
return alert("Data krusial wajib diisi!");
}
if(new Date(payload.tgl_mulai) > new Date(payload.tgl_selesai)) {
return alert("Logika waktu tidak valid.");
}
safeFetch('otak_kemiskinan/api_simpan_pelatihan.php', payload)
.then(res => {
document.getElementById('modal-master-pelatihan').style.display = 'none';
triggerToastNotification("✅ " + res.pesan);
muatDataSemua();
})
.catch(err => alert("Gagal menyimpan program pelatihan: " + err));
};
// --- LOGIKA ANGGOTA KELUARGA ---
window.bukaFormDetailKeluarga = (p, isBernaung) => {
document.getElementById('detail-keluarga-nama').innerText = p.nama_kk;
document.getElementById('detail-id-penduduk').value = p.id;
document.getElementById('detail-is-bernaung').value = isBernaung;
document.getElementById('modal-detail-keluarga').style.display = 'flex';
resetFormAnggota();
muatDaftarAnggota(p.id, isBernaung);
};
window.muatDaftarAnggota = (idPenduduk, isBernaung) => {
const tbody = document.getElementById('tbody-anggota-keluarga');
tbody.innerHTML = `| Memuat data anggota keluarga dari satelit... |
`;
fetch(`otak_kemiskinan/api_tampil_anggota.php?id=${idPenduduk}`)
.then(res => res.json())
.then(data => {
if(data.error) throw new Error(data.error);
if(data.length === 0) {
tbody.innerHTML = `| Belum ada rincian anggota keluarga yang diinput. |
`;
return;
}
let html = '';
data.forEach(m => {
const thnLahir = new Date(m.tanggal_lahir).getFullYear();
const umur = new Date().getFullYear() - thnLahir;
const eduValid = !['Tidak Sekolah', 'SD'].includes(m.pendidikan_terakhir);
const jobless = !m.pekerjaan || m.pekerjaan.trim() === '';
const ageValid = umur >= 17 && umur <= 55;
let pelatihanHtml = '';
if(!isBernaung) {
pelatihanHtml = `Tidak Bernaung`;
} else if (eduValid && jobless && ageValid) {
let opsi = ``;
dataStore.pelatihan.forEach(pel => {
opsi += ``;
});
pelatihanHtml = `
`;
} else {
pelatihanHtml = `Tidak Memenuhi Syarat`;
}
html += `
| ${m.nik} |
${m.nama_lengkap} |
${umur} Thn |
${m.pendidikan_terakhir} |
${m.pekerjaan || 'Menganggur'} |
${pelatihanHtml} |
|
`;
});
tbody.innerHTML = html;
})
.catch(err => {
tbody.innerHTML = `| Gagal memuat: ${err.message} |
`;
});
};
window.resetFormAnggota = () => {
document.getElementById('anggota-nik').value = '';
document.getElementById('anggota-nama').value = '';
document.getElementById('anggota-lahir').value = '';
document.getElementById('anggota-pendidikan').value = 'Tidak Sekolah';
document.getElementById('anggota-pekerjaan').value = '';
const h4 = document.getElementById('modal-detail-keluarga').querySelector('h4');
if(h4) h4.innerText = "Tambah Anggota Baru";
const btnContainer = document.getElementById('anggota-pekerjaan').parentElement.nextElementSibling;
if(btnContainer) {
btnContainer.innerHTML = ``;
}
};
window.bukaFormEditAnggota = (id, nik, nama, lahir, pendidikan, pekerjaan) => {
document.getElementById('anggota-nik').value = nik;
document.getElementById('anggota-nama').value = nama;
document.getElementById('anggota-lahir').value = lahir;
document.getElementById('anggota-pendidikan').value = pendidikan;
document.getElementById('anggota-pekerjaan').value = pekerjaan;
const h4 = document.getElementById('modal-detail-keluarga').querySelector('h4');
if(h4) h4.innerText = `Edit Informasi Anggota (ID: ${id})`;
const btnContainer = document.getElementById('anggota-pekerjaan').parentElement.nextElementSibling;
if(btnContainer) {
btnContainer.innerHTML = `
`;
}
};
window.eksekusiUpdateAnggota = (id) => {
const idPenduduk = document.getElementById('detail-id-penduduk').value;
const isBernaung = document.getElementById('detail-is-bernaung').value === 'true';
const payload = {
id: id,
nik: document.getElementById('anggota-nik').value,
nama: document.getElementById('anggota-nama').value,
lahir: document.getElementById('anggota-lahir').value,
pendidikan: document.getElementById('anggota-pendidikan').value,
pekerjaan: document.getElementById('anggota-pekerjaan').value
};
if(!payload.nik || !payload.nama || !payload.lahir) return alert("Wajib diisi!");
safeFetch('otak_kemiskinan/api_update_anggota.php', payload)
.then(res => {
triggerToastNotification("✅ " + res.pesan);
resetFormAnggota();
muatDaftarAnggota(idPenduduk, isBernaung);
muatDataSemua();
})
.catch(err => alert("Gagal mengupdate: " + err));
};
window.eksekusiSimpanAnggota = () => {
const idPenduduk = document.getElementById('detail-id-penduduk').value;
const isBernaung = document.getElementById('detail-is-bernaung').value === 'true';
const payload = {
id_penduduk: idPenduduk,
nik: document.getElementById('anggota-nik').value,
nama: document.getElementById('anggota-nama').value,
lahir: document.getElementById('anggota-lahir').value,
pendidikan: document.getElementById('anggota-pendidikan').value,
pekerjaan: document.getElementById('anggota-pekerjaan').value
};
if(!payload.nik || !payload.nama || !payload.lahir) return alert("Wajib diisi!");
safeFetch('otak_kemiskinan/api_simpan_anggota.php', payload)
.then(res => {
triggerToastNotification("✅ " + res.pesan);
resetFormAnggota();
muatDaftarAnggota(idPenduduk, isBernaung);
muatDataSemua();
})
.catch(err => alert("Gagal menambah: " + err));
};
window.eksekusiHapusAnggota = (idAnggota) => {
if(!confirm("Hapus permanen?")) return;
const idPenduduk = document.getElementById('detail-id-penduduk').value;
const isBernaung = document.getElementById('detail-is-bernaung').value === 'true';
safeFetch('otak_kemiskinan/api_hapus_anggota.php', { id: idAnggota })
.then(() => {
triggerToastNotification("🗑️ Dihapus.");
muatDaftarAnggota(idPenduduk, isBernaung);
muatDataSemua();
})
.catch(err => alert(err));
};
window.daftarPelatihanAnggota = (idAnggota) => {
const idPel = document.getElementById(`sel-pel-${idAnggota}`).value;
if(!idPel) return alert("Pilih program!");
if(!confirm("Daftarkan anggota ini?")) return;
safeFetch('otak_kemiskinan/api_simpan_logpelatihan.php', { id_anggota: idAnggota, id_pelatihan: idPel })
.then(res => {
triggerToastNotification("✅ " + res.pesan);
})
.catch(err => alert(err));
};
// --- CRUD IBADAH ---
window.simpanIbadahPopup = (lat, lng, popupRef, wrapper) => {
const payload = {
nama: wrapper.querySelector('#pop-ib-nama').value,
jenis: wrapper.querySelector('#pop-ib-jenis').value,
radius_meter: parseInt(wrapper.querySelector('#pop-ib-rad').value),
latitude: lat,
longitude: lng,
alamat: wrapper.querySelector('#pop-ib-alamat').value
};
if(!payload.nama) return alert("Wajib diisi!");
safeFetch('otak_kemiskinan/api_simpan_ibadah.php', payload)
.then(() => {
map.closePopup(popupRef);
resetAllModes();
triggerToastNotification("✅ Berhasil!");
muatDataSemua();
})
.catch(err => alert("Gagal: " + err));
};
window.bukaFormEditIbadah = (d, judul) => {
state.isGhostLocked = true;
const tmpl = document.getElementById('template-popup-ibadah');
if(!tmpl) return;
const clone = tmpl.content.cloneNode(true);
const wrapper = document.createElement('div');
wrapper.appendChild(clone);
const popup = L.popup({ closeOnClick: false, autoClose: false })
.setLatLng([parseFloat(d.latitude), parseFloat(d.longitude)])
.setContent(wrapper)
.openOn(map);
popup.on('remove', function() {
state.isGhostLocked = false;
});
wrapper.querySelector('#pop-ib-nama').value = judul;
wrapper.querySelector('#pop-ib-jenis').value = d.jenis;
const radInput = wrapper.querySelector('#pop-ib-rad');
const radVal = wrapper.querySelector('#pop-ib-rad-val');
if(radInput) {
radInput.value = d.radius_meter;
if(radVal) radVal.innerText = d.radius_meter;
radInput.oninput = function() { radVal.innerText = this.value; };
}
wrapper.querySelector('#pop-ib-alamat').value = d.alamat;
const btnSimpan = wrapper.querySelector('#btn-pop-simpan');
if(btnSimpan) {
btnSimpan.innerText = "Update";
btnSimpan.disabled = false;
btnSimpan.onclick = () => updateIbadahPopup(d.id, popup, wrapper);
}
};
window.updateIbadahPopup = (id, popupRef, wrapper) => {
const payload = {
id: id,
nama: wrapper.querySelector('#pop-ib-nama').value,
jenis: wrapper.querySelector('#pop-ib-jenis').value,
radius: parseInt(wrapper.querySelector('#pop-ib-rad').value)
};
safeFetch('otak_kemiskinan/api_update_ibadah.php', payload)
.then(() => {
map.closePopup(popupRef);
resetAllModes();
triggerToastNotification("✅ Diperbarui.");
muatDataSemua();
})
.catch(err => alert("Gagal: " + err));
};
window.eksekusiHapusIbadah = (id) => {
safeFetch('otak_kemiskinan/api_hapus_ibadah.php', { id: id })
.then(() => {
triggerToastNotification("🗑️ Dihapus.");
muatDataSemua();
})
.catch(err => alert(err));
};
// ====================================================
// LIFECYCLE AWAL (PROMISE ALL & GATEKEEPER)
// ====================================================
window.muatDataSemua = () => {
Promise.all([
safeFetch('otak_spbu/api_tampil_spbu.php')
.then(d => { dataStore.spbu = {}; d.forEach(x => dataStore.spbu[x.id] = x); renderSPBU(); })
.catch(e => console.warn("API SPBU Offline")),
safeFetch('otak_jalan/api_tampil_jalan.php')
.then(d => { dataStore.jalan = {}; d.forEach(x => dataStore.jalan[x.id] = x); renderJalan(); })
.catch(e => console.warn("API Jalan Offline")),
safeFetch('otak_tanah/api_tampil_tanah.php')
.then(d => { dataStore.tanah = {}; d.forEach(x => dataStore.tanah[x.id] = x); renderTanah(); })
.catch(e => console.warn("API Tanah Offline")),
safeFetch('otak_kemiskinan/api_tampil_ibadah.php')
.then(d => { dataStore.ibadah = {}; d.forEach(x => dataStore.ibadah[x.id] = x); })
.catch(e => console.warn("API Ibadah Offline")),
safeFetch('otak_kemiskinan/api_tampil_penduduk.php')
.then(d => { dataStore.penduduk = {}; d.forEach(x => dataStore.penduduk[x.id] = x); })
.catch(e => console.warn("API Penduduk Offline")),
safeFetch('otak_kemiskinan/api_tampil_pelatihan.php')
.then(d => { dataStore.pelatihan = Array.isArray(d) ? d : []; })
.catch(e => console.warn("API Pelatihan Offline")),
safeFetch('otak_kemiskinan/api_tampil_gariskemiskinan.php')
.then(d => {
if(d && d.nilai_rupiah) {
state.garisKemiskinan = parseInt(d.nilai_rupiah);
const inputGaris = document.getElementById('input-garis-kemiskinan');
if(inputGaris) inputGaris.placeholder = `Saat ini: Rp${state.garisKemiskinan}`;
}
})
.catch(e => console.warn("API Garis Offline")),
safeFetch('otak_kemiskinan/api_tampil_darurat.php')
.then(d => {
dataStore.statusDarurat = {};
if(Array.isArray(d)) {
d.forEach(id => dataStore.statusDarurat[id] = true);
}
})
.catch(e => console.warn("API Darurat Offline"))
]).then(() => {
renderKemiskinan();
console.log("✅ Sistem WebGIS V3 Siap dan Data Tersinkronisasi Penuh.");
});
};
setTimeout(() => {
map.invalidateSize();
pulihkanIngatanUI();
cekSesiBackend();
}, 500);