2097 lines
82 KiB
JavaScript
2097 lines
82 KiB
JavaScript
// ===================================================
|
|
// 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,
|
|
|
|
};
|
|
|
|
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 ?
|
|
`<div class="marker-wrapper" style="opacity:0.7; transform:scale(0.9)"><div class="marker-ibadah"></div><i class="marker-ibadah-dot"></i></div>` :
|
|
`<div class="marker-wrapper" style="opacity:0.7; transform:scale(0.9)"><div class="marker-pin" style="background-color: ${pinColor}; border-radius: 50%;"></div><i class="marker-dot"></i></div>`;
|
|
|
|
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.eksekusiLogout = () => {
|
|
if(!confirm("Anda yakin ingin keluar dari sistem?")) return;
|
|
|
|
safeFetch('otak_auth/api_logout.php')
|
|
.then(res => {
|
|
state.user = null;
|
|
window.location.href = 'login.php';
|
|
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;
|
|
window.USER_ROLE = res.role;
|
|
if(window.USER_ROLE !== 'admin') {
|
|
document.body.classList.add('role-user');
|
|
} else {
|
|
if(typeof RightToolbar !== 'undefined') map.addControl(new RightToolbar());
|
|
}
|
|
if (sessionStorage.getItem('just_logged_in') === 'true') {
|
|
Swal.fire({
|
|
title: 'Login Berhasil!',
|
|
html: `Selamat <b>${res.username}</b> masuk ke WebGIS-Pontianak sebagai <b>${res.role.toUpperCase()}</b>`,
|
|
icon: 'success',
|
|
timer: 3000,
|
|
showConfirmButton: false,
|
|
toast: true,
|
|
position: 'top-end'
|
|
});
|
|
sessionStorage.removeItem('just_logged_in');
|
|
}
|
|
|
|
muatDataSemua();
|
|
})
|
|
.catch(err => {
|
|
console.log("Tertahan di gerbang autentikasi: " + err.message);
|
|
window.location.href = 'login.php';
|
|
});
|
|
};
|
|
|
|
// ====================================================
|
|
// 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 = `<span class="icon">+</span> 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 = `<span class="icon">📍</span> 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 = `
|
|
<button class="tool-btn" id="btn-tool-marker" title="SPBU" onclick="toggleMarkerMode()"><i class="fa-solid fa-gas-pump"></i></button>
|
|
<button class="tool-btn" id="btn-tool-jalan" title="Jalan" onclick="toggleRoadMode()"><i class="fa-solid fa-road"></i></button>
|
|
<button class="tool-btn" id="btn-tool-tanah" title="Tanah" onclick="toggleLandMode()"><i class="fa-solid fa-draw-polygon"></i></button>
|
|
`;
|
|
L.DomEvent.disableClickPropagation(c);
|
|
return c;
|
|
}
|
|
});
|
|
|
|
|
|
// ====================================================
|
|
// 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 = `
|
|
<button class="btn btn-save admin-only" id="btn-eksekusi-spbu" onclick="eksekusiSimpanBaru()" disabled>Tunggu Alamat...</button>
|
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-spbu').style.display='none'">Batal</button>
|
|
`;
|
|
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: `<div class="marker-wrapper"><div class="marker-pin" style="background-color: ${isBuka ? '#28a745' : '#dc3545'};"></div><i class="marker-dot"></i></div>`,
|
|
iconSize: [30, 42],
|
|
iconAnchor: [15, 42]
|
|
});
|
|
|
|
const marker = L.marker([lat, lng], {
|
|
icon: customIcon,
|
|
draggable: state.isMarkerMode
|
|
}).addTo(map);
|
|
|
|
marker.bindTooltip(`<b>${d.nama_spbu}</b>`, {
|
|
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 = `
|
|
<button class="btn btn-warning admin-only" onclick="eksekusiUpdate(${d.id})">Simpan Perubahan</button>
|
|
<button class="btn btn-danger admin-only" onclick="eksekusiHapus(${d.id})">Hapus Marker</button>
|
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-spbu').style.display='none'">Tutup</button>
|
|
`;
|
|
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 += `
|
|
<div class="spbu-card ${isBuka ? 'status-buka' : 'status-tutup'}" onclick="lihatSpbuDariCard(${d.id})">
|
|
<h4>${d.nama_spbu}</h4>
|
|
<p style="font-weight:bold; margin-bottom:4px;">${d.status_operasional}</p>
|
|
<p style="font-size:0.75rem; color:#718096; line-height:1.2;">${alamatRingkas}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
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(`<b>${d.nama_jalan}</b><br>${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(`<b>${d.nama_pemilik}</b><br>${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,
|
|
interactive: false
|
|
}).addTo(map);
|
|
|
|
mapLayers.radiusIbadah[d.id] = circle;
|
|
|
|
const icon = L.divIcon({
|
|
className: 'custom-div-icon',
|
|
html: `<div class="marker-wrapper"><div class="marker-ibadah"></div><i class="marker-ibadah-dot"></i></div>`,
|
|
iconSize: [30, 42],
|
|
iconAnchor: [15, 42]
|
|
});
|
|
|
|
const marker = L.marker([lat, lng], { icon: icon }).addTo(map);
|
|
|
|
marker.bindTooltip(`<b>${judulIbadah}</b><br>Radius: ${d.radius_meter}m<br>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 += `
|
|
<div class="spbu-card card-ibadah">
|
|
<h4>🕌 ${judulIbadah}</h4>
|
|
<p>${d.jenis} | ${d.radius_meter}m | ${naunganCounter[d.id]} KK</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
|
|
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: `<div class="${markerClass}"><div class="marker-pin" style="background-color: ${pinColor}; border-radius: 50%;"></div><i class="marker-dot"></i></div>`,
|
|
iconSize: [30, 42],
|
|
iconAnchor: [15, 42]
|
|
});
|
|
|
|
const marker = L.marker([item.lat, item.lng], { icon: icon }).addTo(map);
|
|
|
|
marker.bindTooltip(`<b>KK: ${p.nama_kk}</b><br>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 += `
|
|
<div class="spbu-card ${cls}">
|
|
<h4>🏠 Kel. ${p.nama_kk}</h4>
|
|
<p>PMT: ${p.pmt_score}/5 | Rp${p.pengeluaran}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
});
|
|
}
|
|
|
|
// ====================================================
|
|
// 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 = `
|
|
<button class="btn btn-cancel" style="width:100%" onclick="document.getElementById('modal-spbu').style.display='none'">Kembali</button>
|
|
`;
|
|
|
|
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 = `
|
|
<button class="btn btn-save admin-only" onclick="eksekusiSimpanJalan()">Simpan Jalan</button>
|
|
<button class="btn btn-cancel" onclick="batalGambarJalan()">Batal</button>
|
|
`;
|
|
|
|
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 = `
|
|
<button class="btn btn-warning admin-only" onclick="eksekusiUpdateJalan(${id})">Simpan Perubahan</button>
|
|
<button class="btn btn-danger admin-only" onclick="eksekusiHapusJalan(${id})">Hapus Jalan</button>
|
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-jalan').style.display='none'">Tutup</button>
|
|
`;
|
|
|
|
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 = `
|
|
<button class="btn btn-save admin-only" onclick="eksekusiSimpanTanah()">Simpan Area</button>
|
|
<button class="btn btn-cancel" onclick="batalGambarTanah()">Batal</button>
|
|
`;
|
|
|
|
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 = `
|
|
<button class="btn btn-warning admin-only" onclick="eksekusiUpdateTanah(${id})">Simpan Perubahan</button>
|
|
<button class="btn btn-danger admin-only" onclick="eksekusiHapusTanah(${id})">Hapus Tanah</button>
|
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-tanah').style.display='none'">Tutup</button>
|
|
`;
|
|
|
|
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 = `<tr><td colspan="8" style="text-align:center; padding: 20px;">Mengambil data dari satelit...</td></tr>`;
|
|
|
|
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 = `<tr><td colspan="8" style="text-align:center; padding: 20px;">Belum ada riwayat penerimaan logistik.</td></tr>`;
|
|
return;
|
|
}
|
|
let html = '';
|
|
data.forEach(log => {
|
|
const tgl = log.created_at || log.waktu || log.tanggal || '-';
|
|
html += `
|
|
<tr>
|
|
<td><b>${tgl}</b></td>
|
|
<td>${log.beras_kg}</td>
|
|
<td>${log.minyak_l}</td>
|
|
<td>${log.gula_kg}</td>
|
|
<td>${log.telur_kg}</td>
|
|
<td>${log.susu_kaleng}</td>
|
|
<td style="color:var(--green-safe); font-weight:bold;">Rp${parseInt(log.tunai_rp).toLocaleString('id-ID')}</td>
|
|
<td>${log.catatan || '-'}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
tbody.innerHTML = `<tr><td colspan="8" style="text-align:center; color:red; padding: 20px;">Gagal memuat histori: ${err.message}</td></tr>`;
|
|
});
|
|
};
|
|
|
|
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 = `<tr><td colspan="5" style="text-align:center; padding: 20px;">Mengambil data dari satelit...</td></tr>`;
|
|
|
|
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 = `<tr><td colspan="5" style="text-align:center; padding: 20px;">Belum ada anggota keluarga yang mengikuti pelatihan.</td></tr>`;
|
|
return;
|
|
}
|
|
let html = '';
|
|
data.forEach(log => {
|
|
const tgl = log.tanggal_daftar || '-';
|
|
html += `
|
|
<tr>
|
|
<td><b>${tgl}</b></td>
|
|
<td>${log.nik}</td>
|
|
<td>${log.nama_lengkap}</td>
|
|
<td style="color:var(--navy); font-weight:bold;">${log.nama_pelatihan}</td>
|
|
<td>${log.instansi_penyelenggara}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
tbody.innerHTML = `<tr><td colspan="5" style="text-align:center; color:red; padding: 20px;">Gagal memuat histori: ${err.message}</td></tr>`;
|
|
});
|
|
};
|
|
|
|
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 = `<tr><td colspan="4" style="text-align:center; padding: 20px;">Mengambil data dari satelit...</td></tr>`;
|
|
|
|
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 = `<tr><td colspan="4" style="text-align:center; padding: 20px;">Tidak ada riwayat pelaporan musibah.</td></tr>`;
|
|
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 += `
|
|
<tr>
|
|
<td><b>${tgl}</b></td>
|
|
<td>${log.jenis_musibah}</td>
|
|
<td>${log.deskripsi || '-'}</td>
|
|
<td style="color:${statusColor}; font-weight:bold;">${log.status_penanganan}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
tbody.innerHTML = `<tr><td colspan="4" style="text-align:center; color:red; padding: 20px;">Gagal memuat histori: ${err.message}</td></tr>`;
|
|
});
|
|
};
|
|
|
|
// --- 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 = `<tr><td colspan="7" style="text-align:center; padding:20px;">Memuat data anggota keluarga dari satelit...</td></tr>`;
|
|
|
|
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 = `<tr><td colspan="7" style="text-align:center; padding:20px;">Belum ada rincian anggota keluarga yang diinput.</td></tr>`;
|
|
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 = `<span style="color:var(--red-danger); font-size:0.75rem; font-weight:bold;">Tidak Bernaung</span>`;
|
|
} else if (eduValid && jobless && ageValid) {
|
|
let opsi = `<option value="">-- Pilih Pelatihan --</option>`;
|
|
dataStore.pelatihan.forEach(pel => {
|
|
opsi += `<option value="${pel.id}">${pel.nama_pelatihan}</option>`;
|
|
});
|
|
pelatihanHtml = `
|
|
<div style="display:flex; gap:4px; align-items:center;">
|
|
<select id="sel-pel-${m.id}" style="padding:4px; max-width:120px; font-size:0.75rem; border-radius:4px;">${opsi}</select>
|
|
<button class="btn btn-save admin-only" style="padding:4px 8px; font-size:0.75rem;" onclick="daftarPelatihanAnggota(${m.id})">Daftar</button>
|
|
</div>
|
|
`;
|
|
} else {
|
|
pelatihanHtml = `<span style="color:#718096; font-size:0.75rem;">Tidak Memenuhi Syarat</span>`;
|
|
}
|
|
|
|
html += `
|
|
<tr>
|
|
<td>${m.nik}</td>
|
|
<td>${m.nama_lengkap}</td>
|
|
<td>${umur} Thn</td>
|
|
<td>${m.pendidikan_terakhir}</td>
|
|
<td>${m.pekerjaan || '<i style="color:#718096;">Menganggur</i>'}</td>
|
|
<td>${pelatihanHtml}</td>
|
|
<td>
|
|
<button class="btn btn-warning admin-only" style="padding:4px 8px; font-size:0.75rem; margin-right:4px;" onclick="bukaFormEditAnggota(${m.id}, '${m.nik}', '${m.nama_lengkap}', '${m.tanggal_lahir}', '${m.pendidikan_terakhir}', '${m.pekerjaan || ''}')">Edit</button>
|
|
<button class="btn btn-danger admin-only" style="padding:4px 8px; font-size:0.75rem;" onclick="eksekusiHapusAnggota(${m.id})">Hapus</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
tbody.innerHTML = html;
|
|
})
|
|
.catch(err => {
|
|
tbody.innerHTML = `<tr><td colspan="7" style="text-align:center; color:red;">Gagal memuat: ${err.message}</td></tr>`;
|
|
});
|
|
};
|
|
|
|
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 = `<button class="btn btn-success admin-only" style="width:100%; padding: 10px;" onclick="eksekusiSimpanAnggota()">Tambah Anggota</button>`;
|
|
}
|
|
};
|
|
|
|
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 = `
|
|
<button class="btn btn-warning admin-only" style="flex:1; padding: 10px;" onclick="eksekusiUpdateAnggota(${id})">Update Data</button>
|
|
<button class="btn btn-cancel" style="padding: 10px;" onclick="resetFormAnggota()">Batal Edit</button>
|
|
`;
|
|
}
|
|
};
|
|
|
|
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); |