commit 313d2fd5a709120ef6a8edd75442a7717be1e68d Author: NelsonDavey17 Date: Wed Jun 3 02:09:03 2026 +0700 1st commit: version 1.0 WebGis-Pemetaan-Kemiskinan diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a3cb42 --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +πŸ—ΊοΈ WebGIS Pemetaan Kemiskinan dan Distribusi Bantuan (Kota Pontianak)Sebuah Sistem Informasi Geografis (GIS) berbasis web yang dirancang dengan arsitektur Single Page Application (SPA) untuk memetakan demografi kemiskinan, mengkalkulasi kelayakan bantuan secara otomatis, mengelola respons darurat musibah, dan memfasilitasi pendataan talenta untuk program pemberdayaan warga.Sistem ini dibangun dengan kontrol otorisasi tingkat lanjut (RBAC & Geofencing) serta menggunakan algoritma Sistem Pendukung Keputusan (SPK) terintegrasi.✨ Fitur Utama (Core Features)Gatekeeper Authentication: Sistem layar kunci persisten dengan enkripsi kata sandi menggunakan standar BCRYPT dan validasi sesi via API (Anti Client-Side Amnesia).Pemetaan Spasial & Geofencing: Visualisasi interaktif titik infrastruktur dan batas wewenang radius operasional rumah ibadah menggunakan Leaflet.js dan Reverse Geocoding via Nominatim API.SPK Kemiskinan Otomatis: Menghitung Tingkat Kemiskinan (Sangat Miskin, Miskin, Rentan) secara dinamis menggunakan parameter Garis Kemiskinan Regional dan observasi aset fisik (Proxy Means Testing / PMT).Distribusi Bantuan Cerdas (Smart Autofill): Memberikan rekomendasi takaran logistik pro-rata berdasarkan status keparahan kemiskinan.Manajemen Krisis (Visual Alert): Peringatan dini berbasis geospasial menggunakan rendering animasi kelap-kelip merah pada titik lokasi keluarga yang mengalami musibah darurat.Resolusi Musibah Otomatis (Auto-Resolve): Menerapkan Database Transactions di mana penyaluran logistik akan secara otomatis memadamkan status darurat warga.Filter Pelatihan Algoritmik: Guard clause otomatis untuk menyeleksi kandidat pelatihan berdasarkan batas usia produktif, tingkat pendidikan, dan status pengangguran dari tabel Anggota Keluarga.πŸ› οΈ Stack Teknologi (Tech Stack)Frontend:HTML5 & CSS3 (Native)JavaScript (ES6+ / Vanilla JS)Leaflet.js (Pustaka Pemetaan Klien)Fetch API (Asynchronous HTTP Requests)Backend:PHP 8.x (API Endpoints & Sesi)PDO (PHP Data Objects) untuk interaksi basis dataMySQL / MariaDB (Database Relasional)βš™οΈ Persyaratan Sistem (Prerequisites)Untuk menjalankan proyek ini di mesin lokal, Anda membutuhkan:Web Server lokal seperti XAMPP, MAMP, atau Laragon.PHP Engine (Minimal versi 7.4, direkomendasikan versi 8.x).MySQL Database.Koneksi internet stabil (diperlukan untuk memuat aset basemap OpenStreetMap dan fitur Reverse Geocoding).πŸš€ Panduan Instalasi (Installation Guide)Ikuti langkah-langkah berikut untuk menjalankan sistem di lingkungan pengembangan lokal (localhost):Clone RepositoriBuka terminal/CMD dan jalankan perintah berikut di dalam direktori publik web server Anda (misal: C:\xampp\htdocs\):git clone [https://github.com/username-anda/nama-repositori-anda.git](https://github.com/username-anda/nama-repositori-anda.git) +Konfigurasi DatabaseBuka phpMyAdmin (biasanya di http://localhost/phpmyadmin).Buat sebuah database baru dengan nama webgis_kemiskinan (atau sesuai nama database Anda).Import file SQL yang telah disediakan di repositori ini (misal: database_auth.sql atau file dump lengkap sistem Anda) ke dalam database yang baru dibuat.Konfigurasi Koneksi PHPBuka file koneksi.php di dalam root direktori proyek, dan pastikan kredensial basis data sesuai dengan konfigurasi lokal Anda:$host = "localhost"; +$user = "root"; // Username default XAMPP +$pass = ""; // Kosongkan jika XAMPP default +$dbname = "webgis_kemiskinan"; // Sesuaikan dengan nama database Anda +Jalankan AplikasiBuka web browser dan akses direktori proyek Anda:http://localhost/nama-folder-proyek/ +πŸ”’ Akses Pengujian (Dummy Credentials)Gunakan kredensial berikut untuk melewati layar kunci (Gatekeeper) pada saat pengujian sistem:Username: admin (contoh)Password: 123456 (contoh)(Catatan: Anda dapat membuat akun baru melalui tab "Daftar" di layar awal sistem).πŸ“‚ Struktur Direktori (Directory Structure)πŸ“¦ root-directory + ┣ πŸ“‚ components # Komponen UI modular (Sidebar, Modals) + ┣ πŸ“‚ otak_auth # API Backend untuk Login, Register, Sesi & Logout + ┣ πŸ“‚ otak_kemiskinan # API Backend untuk logika CRUD & SPK warga, anggota, pelatihan + ┣ πŸ“‚ otak_jalan # API Backend manajemen data garis (Polyline) + ┣ πŸ“‚ otak_tanah # API Backend manajemen persil tanah (Polygon) + ┣ πŸ“‚ otak_spbu # API Backend manajemen SPBU + ┣ πŸ“œ app.js # Otak Utama Sistem (Logika Spasial & State Management) + ┣ πŸ“œ style.css # Desain Kosmetik UI dan Animasi GPU + ┣ πŸ“œ koneksi.php # Jembatan penghubung ke Database MySQL + β”— πŸ“œ index.php # Entry point aplikasi +πŸ“ LisensiProyek ini dikembangkan sebagai bagian dari penelitian akademik/tugas perkuliahan Universitas Tanjungpura (2026). Segala bentuk distribusi dan modifikasi mengacu pada kebijakan hak cipta yang berlaku.Dibangun dengan presisi logika dan ketajaman rasional. \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000..3b5b935 --- /dev/null +++ b/app.js @@ -0,0 +1,2118 @@ +// =================================================== +// 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); \ No newline at end of file diff --git a/components/modals.php b/components/modals.php new file mode 100644 index 0000000..fbecf56 --- /dev/null +++ b/components/modals.php @@ -0,0 +1,691 @@ + + + + + + Document + + +
Peringatan: Data SPBU berhasil dihapus!
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+

WebGIS Pontianak

+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + + +

+
+
+ + + \ No newline at end of file diff --git a/components/sidebar.php b/components/sidebar.php new file mode 100644 index 0000000..190e8ed --- /dev/null +++ b/components/sidebar.php @@ -0,0 +1,71 @@ + + + + + + Document + + + + + \ No newline at end of file diff --git a/database.sql b/database.sql new file mode 100644 index 0000000..278c24e --- /dev/null +++ b/database.sql @@ -0,0 +1,128 @@ +CREATE TABLE spbu_markers ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_spbu VARCHAR(100) NOT NULL, + status_operasional ENUM('Buka 24 Jam', 'Tidak 24 Jam') NOT NULL, + no_telp VARCHAR(20) NOT NULL, + latitude DECIMAL(10, 8) NOT NULL, + longitude DECIMAL(11, 8) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE jalan_polylines ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_jalan VARCHAR(100) NOT NULL, + status_jalan ENUM('Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten') NOT NULL, + jarak_meter INT NOT NULL, + koordinat JSON NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE tanah_polygons ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_pemilik VARCHAR(100) NOT NULL, + status_kepemilikan ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL, + luas_meter INT NOT NULL, + koordinat JSON NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +ALTER TABLE spbu_markers +ADD COLUMN alamat TEXT AFTER no_telp; +CREATE TABLE rumah_ibadah ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama VARCHAR(150) NOT NULL, + jenis ENUM('Masjid', 'Gereja', 'Vihara', 'Pura', 'Klenteng') NOT NULL, + radius_meter INT NOT NULL, + alamat TEXT, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE rumah_penduduk ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_kk VARCHAR(100) NOT NULL, + agama VARCHAR(50), + jml_tanggungan INT DEFAULT 0, + pengeluaran INT NOT NULL, + pmt_lantai INT DEFAULT 0, + pmt_dinding INT DEFAULT 0, + pmt_sanitasi INT DEFAULT 0, + pmt_penerangan INT DEFAULT 0, + pmt_air INT DEFAULT 0, + pmt_score INT NOT NULL, + status_kemiskinan ENUM('Sangat Miskin', 'Miskin', 'Rentan Miskin', 'Tidak Miskin') NOT NULL, + alamat TEXT, + latitude DOUBLE NOT NULL, + longitude DOUBLE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE log_bantuan ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_penduduk INT NOT NULL, + beras_kg INT DEFAULT 0, + minyak_l INT DEFAULT 0, + gula_kg INT DEFAULT 0, + telur_kg INT DEFAULT 0, + susu_kaleng INT DEFAULT 0, + tunai_rp INT DEFAULT 0, + catatan TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (id_penduduk) REFERENCES rumah_penduduk(id) ON DELETE CASCADE +); +CREATE TABLE IF NOT EXISTS garis_kemiskinan ( + id INT AUTO_INCREMENT PRIMARY KEY, + tahun YEAR NOT NULL, + nilai_rupiah INT NOT NULL, + is_active BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); +INSERT INTO garis_kemiskinan (tahun, nilai_rupiah, is_active) +VALUES (2026, 700000, TRUE); + +CREATE TABLE master_pelatihan ( + id INT AUTO_INCREMENT PRIMARY KEY, + nama_pelatihan VARCHAR(255) NOT NULL, + instansi_penyelenggara VARCHAR(255) NOT NULL, + tanggal_mulai DATE NOT NULL, + tanggal_selesai DATE NOT NULL, + deskripsi TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE anggota_keluarga ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_penduduk INT NOT NULL, + nik VARCHAR(16) NOT NULL UNIQUE, + nama_lengkap VARCHAR(255) NOT NULL, + tanggal_lahir DATE NOT NULL, + pendidikan_terakhir ENUM('Tidak Sekolah', 'SD', 'SMP', 'SMA/SMK', 'D3', 'S1', 'S2', 'S3') NOT NULL, + pekerjaan VARCHAR(255) DEFAULT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (id_penduduk) REFERENCES rumah_penduduk(id) ON DELETE CASCADE +); + +CREATE TABLE log_pelatihan ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_anggota INT NOT NULL, + id_pelatihan INT NOT NULL, + tanggal_daftar TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (id_anggota) REFERENCES anggota_keluarga(id) ON DELETE CASCADE, + FOREIGN KEY (id_pelatihan) REFERENCES master_pelatihan(id) ON DELETE CASCADE +); +CREATE TABLE laporan_musibah ( + id INT AUTO_INCREMENT PRIMARY KEY, + id_penduduk INT NOT NULL, + tanggal_lapor DATE NOT NULL, + jenis_musibah ENUM('Kecelakaan', 'Kematian', 'Sakit Keras', 'Kebakaran', 'Bencana Alam', 'Lainnya') NOT NULL, + deskripsi TEXT, + status_penanganan ENUM('Belum Ditangani', 'Sudah Ditangani') DEFAULT 'Belum Ditangani', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (id_penduduk) REFERENCES rumah_penduduk(id) ON DELETE CASCADE +); +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(50) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, + role VARCHAR(20) DEFAULT 'admin', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..5cae125 --- /dev/null +++ b/index.php @@ -0,0 +1,25 @@ + + + + + + WebGIS Pontianak V2 + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/koneksi.php b/koneksi.php new file mode 100644 index 0000000..b4d71c3 --- /dev/null +++ b/koneksi.php @@ -0,0 +1,20 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +} catch(PDOException $e) { + // Jika koneksi gagal, hentikan eksekusi dan kirim pesan error format JSON + die(json_encode(["error" => "Koneksi Database Gagal: " . $e->getMessage()])); +} +?> \ No newline at end of file diff --git a/otak_auth/api_login.php b/otak_auth/api_login.php new file mode 100644 index 0000000..771e181 --- /dev/null +++ b/otak_auth/api_login.php @@ -0,0 +1,36 @@ +username) && !empty($data->password)) { + try { + $stmt = $conn->prepare("SELECT id, username, password, role FROM users WHERE username = :username LIMIT 1"); + $stmt->bindValue(':username', $data->username); + $stmt->execute(); + + if($stmt->rowCount() > 0) { + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + // Verifikasi hash password + if(password_verify($data->password, $user['password'])) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + echo json_encode(["pesan" => "Login Berhasil!", "user" => $user['username']]); + } else { + http_response_code(401); echo json_encode(["error" => "Password salah."]); + } + } else { + http_response_code(404); echo json_encode(["error" => "Username tidak ditemukan."]); + } + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "Username dan Password wajib diisi."]); +} +?> \ No newline at end of file diff --git a/otak_auth/api_logout.php b/otak_auth/api_logout.php new file mode 100644 index 0000000..a0327cf --- /dev/null +++ b/otak_auth/api_logout.php @@ -0,0 +1,7 @@ + "Anda telah berhasil keluar."]); +?> \ No newline at end of file diff --git a/otak_auth/api_register.php b/otak_auth/api_register.php new file mode 100644 index 0000000..18b7488 --- /dev/null +++ b/otak_auth/api_register.php @@ -0,0 +1,34 @@ +username) && !empty($data->password)) { + try { + // Cek apakah username sudah ada + $stmt = $conn->prepare("SELECT id FROM users WHERE username = :username"); + $stmt->bindValue(':username', $data->username); + $stmt->execute(); + if($stmt->rowCount() > 0) { + http_response_code(400); die(json_encode(["error" => "Username sudah terdaftar, gunakan yang lain!"])); + } + + // Enkripsi password menggunakan BCRYPT + $query = "INSERT INTO users (username, password) VALUES (:username, :password)"; + $stmt = $conn->prepare($query); + $stmt->bindValue(':username', htmlspecialchars(strip_tags($data->username))); + $stmt->bindValue(':password', password_hash($data->password, PASSWORD_BCRYPT)); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Registrasi berhasil, silakan login!"]); + } + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "Username dan Password wajib diisi."]); +} +?> \ No newline at end of file diff --git a/otak_auth/api_session.php b/otak_auth/api_session.php new file mode 100644 index 0000000..17d5f5f --- /dev/null +++ b/otak_auth/api_session.php @@ -0,0 +1,11 @@ + "aktif", "username" => $_SESSION['username']]); +} else { + http_response_code(401); + echo json_encode(["status" => "tidak_aktif", "error" => "Sesi kedaluwarsa atau belum login."]); +} +?> \ No newline at end of file diff --git a/otak_jalan/api_hapus_jalan.php b/otak_jalan/api_hapus_jalan.php new file mode 100644 index 0000000..9e56568 --- /dev/null +++ b/otak_jalan/api_hapus_jalan.php @@ -0,0 +1,21 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM jalan_polylines WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Jalan berhasil dihapus."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_jalan/api_simpan_jalan.php b/otak_jalan/api_simpan_jalan.php new file mode 100644 index 0000000..c3a8f9a --- /dev/null +++ b/otak_jalan/api_simpan_jalan.php @@ -0,0 +1,30 @@ +nama) && !empty($data->status) && !empty($data->koordinat)) { + try { + $query = "INSERT INTO jalan_polylines (nama_jalan, status_jalan, jarak_meter, koordinat) + VALUES (:nama, :status, :jarak, :koordinat)"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':jarak', $data->jarak, PDO::PARAM_INT); + $stmt->bindValue(':koordinat', $data->koordinat); // Sudah dalam bentuk string JSON dari JS + + if($stmt->execute()) { + echo json_encode(["pesan" => "Jalan berhasil ditambahkan."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_jalan/api_tampil_jalan.php b/otak_jalan/api_tampil_jalan.php new file mode 100644 index 0000000..aae7c6c --- /dev/null +++ b/otak_jalan/api_tampil_jalan.php @@ -0,0 +1,13 @@ +query("SELECT * FROM jalan_polylines ORDER BY created_at DESC"); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_jalan/api_update_jalan.php b/otak_jalan/api_update_jalan.php new file mode 100644 index 0000000..01797e2 --- /dev/null +++ b/otak_jalan/api_update_jalan.php @@ -0,0 +1,25 @@ +id) && !empty($data->nama) && !empty($data->status)) { + try { + $query = "UPDATE jalan_polylines SET nama_jalan = :nama, status_jalan = :status WHERE id = :id"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data Jalan berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_hapus_anggota.php b/otak_kemiskinan/api_hapus_anggota.php new file mode 100644 index 0000000..c85f964 --- /dev/null +++ b/otak_kemiskinan/api_hapus_anggota.php @@ -0,0 +1,16 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM anggota_keluarga WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + if($stmt->execute()) echo json_encode(["pesan" => "Anggota dihapus."]); + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_hapus_ibadah.php b/otak_kemiskinan/api_hapus_ibadah.php new file mode 100644 index 0000000..7e1360d --- /dev/null +++ b/otak_kemiskinan/api_hapus_ibadah.php @@ -0,0 +1,23 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM rumah_ibadah WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + if($stmt->execute()) { + echo json_encode(["pesan" => "Rumah Ibadah berhasil dihapus secara permanen."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "ID tidak ditemukan."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_hapus_penduduk.php b/otak_kemiskinan/api_hapus_penduduk.php new file mode 100644 index 0000000..66bc8b2 --- /dev/null +++ b/otak_kemiskinan/api_hapus_penduduk.php @@ -0,0 +1,24 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM rumah_penduduk WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data keluarga berhasil dihapus secara permanen."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "ID tidak ditemukan."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_anggota.php b/otak_kemiskinan/api_simpan_anggota.php new file mode 100644 index 0000000..a62b784 --- /dev/null +++ b/otak_kemiskinan/api_simpan_anggota.php @@ -0,0 +1,32 @@ +id_penduduk) && !empty($data->nik) && !empty($data->nama)) { + try { + $query = "INSERT INTO anggota_keluarga (id_penduduk, nik, nama_lengkap, tanggal_lahir, pendidikan_terakhir, pekerjaan) + VALUES (:id_penduduk, :nik, :nama, :lahir, :pendidikan, :pekerjaan)"; + $stmt = $conn->prepare($query); + + $pekerjaan = (!empty(trim($data->pekerjaan))) ? htmlspecialchars(strip_tags($data->pekerjaan)) : NULL; + + $stmt->bindValue(':id_penduduk', $data->id_penduduk, PDO::PARAM_INT); + $stmt->bindValue(':nik', htmlspecialchars(strip_tags($data->nik))); + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':lahir', $data->lahir); + $stmt->bindValue(':pendidikan', $data->pendidikan); + $stmt->bindValue(':pekerjaan', $pekerjaan); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Anggota keluarga berhasil ditambahkan."]); + } + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => "Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "Data NIK dan Nama wajib diisi."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_ibadah.php b/otak_kemiskinan/api_simpan_ibadah.php new file mode 100644 index 0000000..2be361f --- /dev/null +++ b/otak_kemiskinan/api_simpan_ibadah.php @@ -0,0 +1,33 @@ +nama) && isset($data->latitude) && isset($data->longitude)) { + try { + $query = "INSERT INTO rumah_ibadah (nama, jenis, radius_meter, alamat, latitude, longitude) + VALUES (:nama, :jenis, :radius, :alamat, :lat, :lng)"; + + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':jenis', htmlspecialchars(strip_tags($data->jenis))); + $stmt->bindValue(':radius', $data->radius_meter, PDO::PARAM_INT); + $stmt->bindValue(':alamat', htmlspecialchars(strip_tags($data->alamat))); + $stmt->bindValue(':lat', $data->latitude); + $stmt->bindValue(':lng', $data->longitude); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Pusat radius ibadah berhasil diarsipkan."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data rumah ibadah tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_logbantuan.php b/otak_kemiskinan/api_simpan_logbantuan.php new file mode 100644 index 0000000..ac44718 --- /dev/null +++ b/otak_kemiskinan/api_simpan_logbantuan.php @@ -0,0 +1,40 @@ +id_penduduk)) { + try { + $conn->beginTransaction(); + + // 1. Catat Logistik Bantuan + $query = "INSERT INTO log_bantuan (id_penduduk, beras_kg, minyak_l, gula_kg, telur_kg, susu_kaleng, tunai_rp, catatan) + VALUES (:id, :beras, :minyak, :gula, :telur, :susu, :tunai, :catatan)"; + $stmt = $conn->prepare($query); + $stmt->bindValue(':id', $data->id_penduduk, PDO::PARAM_INT); + $stmt->bindValue(':beras', $data->beras, PDO::PARAM_INT); + $stmt->bindValue(':minyak', $data->minyak, PDO::PARAM_INT); + $stmt->bindValue(':gula', $data->gula, PDO::PARAM_INT); + $stmt->bindValue(':telur', $data->telur, PDO::PARAM_INT); + $stmt->bindValue(':susu', $data->susu, PDO::PARAM_INT); + $stmt->bindValue(':tunai', $data->tunai, PDO::PARAM_INT); + $stmt->bindValue(':catatan', htmlspecialchars(strip_tags($data->catatan))); + $stmt->execute(); + + // 2. KUNCI BABAK 4: Padamkan status darurat jika keluarga ini sedang terkena musibah + $updateMusibah = $conn->prepare("UPDATE laporan_musibah SET status_penanganan = 'Sudah Ditangani' WHERE id_penduduk = :id AND status_penanganan = 'Belum Ditangani'"); + $updateMusibah->bindValue(':id', $data->id_penduduk, PDO::PARAM_INT); + $updateMusibah->execute(); + + $conn->commit(); + echo json_encode(["pesan" => "Bantuan disalurkan. Status darurat keluarga ini telah dipadamkan secara otomatis."]); + } catch(PDOException $e) { + $conn->rollBack(); + http_response_code(500); echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "ID Keluarga Penerima tidak valid."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_logpelatihan.php b/otak_kemiskinan/api_simpan_logpelatihan.php new file mode 100644 index 0000000..c81a6d9 --- /dev/null +++ b/otak_kemiskinan/api_simpan_logpelatihan.php @@ -0,0 +1,17 @@ +id_anggota) && !empty($data->id_pelatihan)) { + try { + $stmt = $conn->prepare("INSERT INTO log_pelatihan (id_anggota, id_pelatihan) VALUES (:ida, :idp)"); + $stmt->bindValue(':ida', $data->id_anggota, PDO::PARAM_INT); + $stmt->bindValue(':idp', $data->id_pelatihan, PDO::PARAM_INT); + if($stmt->execute()) echo json_encode(["pesan" => "Berhasil didaftarkan ke program pelatihan!"]); + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_musibah.php b/otak_kemiskinan/api_simpan_musibah.php new file mode 100644 index 0000000..0838f68 --- /dev/null +++ b/otak_kemiskinan/api_simpan_musibah.php @@ -0,0 +1,29 @@ +id_penduduk) && !empty($data->tanggal) && !empty($data->jenis)) { + try { + $query = "INSERT INTO laporan_musibah (id_penduduk, tanggal_lapor, jenis_musibah, deskripsi, status_penanganan) + VALUES (:id, :tgl, :jenis, :deskripsi, :status)"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':id', $data->id_penduduk, PDO::PARAM_INT); + $stmt->bindValue(':tgl', $data->tanggal); + $stmt->bindValue(':jenis', htmlspecialchars(strip_tags($data->jenis))); + $stmt->bindValue(':deskripsi', htmlspecialchars(strip_tags($data->deskripsi))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Laporan darurat musibah berhasil dikirim. Menunggu penanganan!"]); + } + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "Data tanggal dan jenis musibah wajib diisi."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_pelatihan.php b/otak_kemiskinan/api_simpan_pelatihan.php new file mode 100644 index 0000000..53754b4 --- /dev/null +++ b/otak_kemiskinan/api_simpan_pelatihan.php @@ -0,0 +1,31 @@ +nama_pelatihan) && !empty($data->instansi) && !empty($data->tgl_mulai) && !empty($data->tgl_selesai)) { + try { + $query = "INSERT INTO master_pelatihan (nama_pelatihan, instansi_penyelenggara, tanggal_mulai, tanggal_selesai, deskripsi) + VALUES (:nama, :instansi, :mulai, :selesai, :deskripsi)"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama_pelatihan))); + $stmt->bindValue(':instansi', htmlspecialchars(strip_tags($data->instansi))); + $stmt->bindValue(':mulai', $data->tgl_mulai); + $stmt->bindValue(':selesai', $data->tgl_selesai); + $stmt->bindValue(':deskripsi', htmlspecialchars(strip_tags($data->deskripsi))); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Program pelatihan berhasil ditambahkan ke dalam Master Data."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data krusial (Nama, Instansi, Tanggal) tidak boleh kosong."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_simpan_penduduk.php b/otak_kemiskinan/api_simpan_penduduk.php new file mode 100644 index 0000000..ba38576 --- /dev/null +++ b/otak_kemiskinan/api_simpan_penduduk.php @@ -0,0 +1,49 @@ +nama) && isset($data->lat) && isset($data->lng)) { + try { + $query = "INSERT INTO rumah_penduduk + (nama_kk, agama, jml_tanggungan, pengeluaran, pmt_lantai, pmt_dinding, pmt_sanitasi, pmt_penerangan, pmt_air, pmt_score, status_kemiskinan, alamat, latitude, longitude) + VALUES + (:nama, :agama, :tanggungan, :pengeluaran, :lantai, :dinding, :sanitasi, :penerangan, :air, :score, :status, :alamat, :lat, :lng)"; + + $stmt = $conn->prepare($query); + + // Membersihkan input dari potensi XSS Attack + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':agama', htmlspecialchars(strip_tags($data->agama))); + $stmt->bindValue(':tanggungan', $data->tanggungan, PDO::PARAM_INT); + $stmt->bindValue(':pengeluaran', $data->pengeluaran, PDO::PARAM_INT); + + // Penilaian 5 Kriteria PMT Biner + $stmt->bindValue(':lantai', $data->pmt_lantai, PDO::PARAM_INT); + $stmt->bindValue(':dinding', $data->pmt_dinding, PDO::PARAM_INT); + $stmt->bindValue(':sanitasi', $data->pmt_sanitasi, PDO::PARAM_INT); + $stmt->bindValue(':penerangan', $data->pmt_penerangan, PDO::PARAM_INT); + $stmt->bindValue(':air', $data->pmt_air, PDO::PARAM_INT); + + // Hasil Analisis DSS + $stmt->bindValue(':score', $data->pmt_score, PDO::PARAM_INT); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':alamat', htmlspecialchars(strip_tags($data->alamat))); + $stmt->bindValue(':lat', $data->lat); + $stmt->bindValue(':lng', $data->lng); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data kemiskinan berhasil diarsipkan."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data krusial tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_anggota.php b/otak_kemiskinan/api_tampil_anggota.php new file mode 100644 index 0000000..3b4da33 --- /dev/null +++ b/otak_kemiskinan/api_tampil_anggota.php @@ -0,0 +1,19 @@ + 0) { + try { + $stmt = $conn->prepare("SELECT * FROM anggota_keluarga WHERE id_penduduk = :id ORDER BY id ASC"); + $stmt->bindValue(':id', $id_penduduk, PDO::PARAM_INT); + $stmt->execute(); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "ID tidak valid"]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_darurat.php b/otak_kemiskinan/api_tampil_darurat.php new file mode 100644 index 0000000..fd3fe6e --- /dev/null +++ b/otak_kemiskinan/api_tampil_darurat.php @@ -0,0 +1,14 @@ +query("SELECT id_penduduk FROM laporan_musibah WHERE status_penanganan = 'Belum Ditangani'"); + $data = $stmt->fetchAll(PDO::FETCH_COLUMN); // Mengembalikan array [id1, id2, ...] + + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_gariskemiskinan.php b/otak_kemiskinan/api_tampil_gariskemiskinan.php new file mode 100644 index 0000000..ae22b5a --- /dev/null +++ b/otak_kemiskinan/api_tampil_gariskemiskinan.php @@ -0,0 +1,20 @@ +query("SELECT nilai_rupiah FROM garis_kemiskinan WHERE is_active = TRUE LIMIT 1"); + $data = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($data) { + echo json_encode($data); + } else { + // Fallback darurat jika tabel kosong + echo json_encode(["nilai_rupiah" => 700000]); + } +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_ibadah.php b/otak_kemiskinan/api_tampil_ibadah.php new file mode 100644 index 0000000..e493eec --- /dev/null +++ b/otak_kemiskinan/api_tampil_ibadah.php @@ -0,0 +1,13 @@ +query("SELECT * FROM rumah_ibadah ORDER BY created_at DESC"); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data ibadah: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_logbantuan.php b/otak_kemiskinan/api_tampil_logbantuan.php new file mode 100644 index 0000000..ae2cef8 --- /dev/null +++ b/otak_kemiskinan/api_tampil_logbantuan.php @@ -0,0 +1,25 @@ + 0) { + try { + // Asumsi tabel Anda memiliki kolom created_at atau minimal id yang Auto Increment + $stmt = $conn->prepare("SELECT * FROM log_bantuan WHERE id_penduduk = :id ORDER BY id DESC"); + $stmt->bindValue(':id', $id_penduduk, PDO::PARAM_INT); + $stmt->execute(); + + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Parameter ID Penduduk tidak ditemukan."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_logpelatihan.php b/otak_kemiskinan/api_tampil_logpelatihan.php new file mode 100644 index 0000000..ac75177 --- /dev/null +++ b/otak_kemiskinan/api_tampil_logpelatihan.php @@ -0,0 +1,31 @@ + 0) { + try { + // Melakukan JOIN untuk merangkai nama anggota, NIK, dan nama pelatihan + $query = "SELECT lp.tanggal_daftar, mp.nama_pelatihan, mp.instansi_penyelenggara, ak.nama_lengkap, ak.nik + FROM log_pelatihan lp + JOIN anggota_keluarga ak ON lp.id_anggota = ak.id + JOIN master_pelatihan mp ON lp.id_pelatihan = mp.id + WHERE ak.id_penduduk = :id + ORDER BY lp.tanggal_daftar DESC"; + + $stmt = $conn->prepare($query); + $stmt->bindValue(':id', $id_penduduk, PDO::PARAM_INT); + $stmt->execute(); + + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Parameter ID Penduduk tidak valid."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_musibah.php b/otak_kemiskinan/api_tampil_musibah.php new file mode 100644 index 0000000..817235e --- /dev/null +++ b/otak_kemiskinan/api_tampil_musibah.php @@ -0,0 +1,23 @@ + 0) { + try { + $stmt = $conn->prepare("SELECT * FROM laporan_musibah WHERE id_penduduk = :id ORDER BY tanggal_lapor DESC, id DESC"); + $stmt->bindValue(':id', $id_penduduk, PDO::PARAM_INT); + $stmt->execute(); + + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Parameter ID Penduduk tidak valid."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_pelatihan.php b/otak_kemiskinan/api_tampil_pelatihan.php new file mode 100644 index 0000000..7e671e6 --- /dev/null +++ b/otak_kemiskinan/api_tampil_pelatihan.php @@ -0,0 +1,12 @@ +query("SELECT * FROM master_pelatihan ORDER BY id DESC"); + echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC)); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_tampil_penduduk.php b/otak_kemiskinan/api_tampil_penduduk.php new file mode 100644 index 0000000..f01f2ec --- /dev/null +++ b/otak_kemiskinan/api_tampil_penduduk.php @@ -0,0 +1,21 @@ +query($query); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data kemiskinan: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_update_anggota.php b/otak_kemiskinan/api_update_anggota.php new file mode 100644 index 0000000..f2b8086 --- /dev/null +++ b/otak_kemiskinan/api_update_anggota.php @@ -0,0 +1,37 @@ +id) && !empty($data->nik) && !empty($data->nama)) { + try { + $query = "UPDATE anggota_keluarga SET + nik = :nik, + nama_lengkap = :nama, + tanggal_lahir = :lahir, + pendidikan_terakhir = :pendidikan, + pekerjaan = :pekerjaan + WHERE id = :id"; + $stmt = $conn->prepare($query); + + $pekerjaan = (!empty(trim($data->pekerjaan))) ? htmlspecialchars(strip_tags($data->pekerjaan)) : NULL; + + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + $stmt->bindValue(':nik', htmlspecialchars(strip_tags($data->nik))); + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':lahir', $data->lahir); + $stmt->bindValue(':pendidikan', $data->pendidikan); + $stmt->bindValue(':pekerjaan', $pekerjaan); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Informasi anggota keluarga berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); echo json_encode(["error" => "Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); echo json_encode(["error" => "Data NIK dan Nama wajib diisi."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_update_gariskemiskinan.php b/otak_kemiskinan/api_update_gariskemiskinan.php new file mode 100644 index 0000000..4253e42 --- /dev/null +++ b/otak_kemiskinan/api_update_gariskemiskinan.php @@ -0,0 +1,47 @@ +nilai) && is_numeric($data->nilai)) { + try { + // Mengunci eksekusi agar jika satu gagal, semua dibatalkan (ACID Compliance) + $conn->beginTransaction(); + + // 1. Matikan semua standar historis lama + $conn->exec("UPDATE garis_kemiskinan SET is_active = FALSE"); + + // 2. Masukkan standar baru sebagai yang aktif + $stmtConfig = $conn->prepare("INSERT INTO garis_kemiskinan (tahun, nilai_rupiah, is_active) VALUES (:tahun, :nilai, TRUE)"); + $stmtConfig->bindValue(':tahun', date("Y"), PDO::PARAM_INT); + $stmtConfig->bindValue(':nilai', $data->nilai, PDO::PARAM_INT); + $stmtConfig->execute(); + + // 3. KALIBRASI ULANG MASSAL (Cascading Status Update) + $updatePenduduk = "UPDATE rumah_penduduk SET status_kemiskinan = CASE + WHEN pengeluaran > :nilai THEN 'Tidak Miskin' + WHEN pmt_score >= 4 THEN 'Sangat Miskin' + WHEN pmt_score >= 2 THEN 'Miskin' + WHEN pmt_score = 1 THEN 'Rentan Miskin' + ELSE 'Tidak Miskin' + END"; + + $stmtUpdate = $conn->prepare($updatePenduduk); + $stmtUpdate->bindValue(':nilai', $data->nilai, PDO::PARAM_INT); + $stmtUpdate->execute(); + + $conn->commit(); + + echo json_encode(["pesan" => "Garis kemiskinan diubah dan seluruh status penduduk telah dikalibrasi ulang!"]); + } catch(PDOException $e) { + $conn->rollBack(); + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Nilai garis kemiskinan tidak valid."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_update_ibadah.php b/otak_kemiskinan/api_update_ibadah.php new file mode 100644 index 0000000..f40da39 --- /dev/null +++ b/otak_kemiskinan/api_update_ibadah.php @@ -0,0 +1,33 @@ +id) && !empty($data->nama)) { + try { + $query = "UPDATE rumah_ibadah SET + nama = :nama, + jenis = :jenis, + radius_meter = :radius + WHERE id = :id"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':jenis', htmlspecialchars(strip_tags($data->jenis))); + $stmt->bindValue(':radius', $data->radius, PDO::PARAM_INT); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data Rumah Ibadah berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data krusial tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_kemiskinan/api_update_penduduk.php b/otak_kemiskinan/api_update_penduduk.php new file mode 100644 index 0000000..7651261 --- /dev/null +++ b/otak_kemiskinan/api_update_penduduk.php @@ -0,0 +1,52 @@ +id) && !empty($data->nama)) { + try { + $query = "UPDATE rumah_penduduk SET + nama_kk = :nama, + agama = :agama, + jml_tanggungan = :tanggungan, + pengeluaran = :pengeluaran, + pmt_lantai = :lantai, + pmt_dinding = :dinding, + pmt_sanitasi = :sanitasi, + pmt_penerangan = :penerangan, + pmt_air = :air, + pmt_score = :score, + status_kemiskinan = :status + WHERE id = :id"; + + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':agama', htmlspecialchars(strip_tags($data->agama))); + $stmt->bindValue(':tanggungan', $data->tanggungan, PDO::PARAM_INT); + $stmt->bindValue(':pengeluaran', $data->pengeluaran, PDO::PARAM_INT); + + $stmt->bindValue(':lantai', $data->pmt_lantai, PDO::PARAM_INT); + $stmt->bindValue(':dinding', $data->pmt_dinding, PDO::PARAM_INT); + $stmt->bindValue(':sanitasi', $data->pmt_sanitasi, PDO::PARAM_INT); + $stmt->bindValue(':penerangan', $data->pmt_penerangan, PDO::PARAM_INT); + $stmt->bindValue(':air', $data->pmt_air, PDO::PARAM_INT); + + $stmt->bindValue(':score', $data->pmt_score, PDO::PARAM_INT); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data kemiskinan berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Database Error: " . $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data krusial tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_spbu/api_hapus_spbu.php b/otak_spbu/api_hapus_spbu.php new file mode 100644 index 0000000..31eeb82 --- /dev/null +++ b/otak_spbu/api_hapus_spbu.php @@ -0,0 +1,22 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM spbu_markers WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "SPBU berhasil dihapus."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_spbu/api_simpan_spbu.php b/otak_spbu/api_simpan_spbu.php new file mode 100644 index 0000000..acb7881 --- /dev/null +++ b/otak_spbu/api_simpan_spbu.php @@ -0,0 +1,33 @@ +nama) && !empty($data->status) && isset($data->lat) && isset($data->lng)) { + try { + $query = "INSERT INTO spbu_markers (nama_spbu, status_operasional, no_telp, alamat, latitude, longitude) + VALUES (:nama, :status, :telp, :alamat, :lat, :lng)"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':telp', htmlspecialchars(strip_tags($data->telp))); + // Alamat dimasukkan + $stmt->bindValue(':alamat', htmlspecialchars(strip_tags($data->alamat))); + $stmt->bindValue(':lat', $data->lat); + $stmt->bindValue(':lng', $data->lng); + + if($stmt->execute()) { + echo json_encode(["pesan" => "SPBU berhasil ditambahkan."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_spbu/api_tampil_spbu.php b/otak_spbu/api_tampil_spbu.php new file mode 100644 index 0000000..9a8bc07 --- /dev/null +++ b/otak_spbu/api_tampil_spbu.php @@ -0,0 +1,14 @@ +query("SELECT * FROM spbu_markers ORDER BY created_at DESC"); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_spbu/api_update_coords.php b/otak_spbu/api_update_coords.php new file mode 100644 index 0000000..656cf5c --- /dev/null +++ b/otak_spbu/api_update_coords.php @@ -0,0 +1,25 @@ +id) && isset($data->lat) && isset($data->lng)) { + try { + // Alamat ikut di-update saat marker digeser + $stmt = $conn->prepare("UPDATE spbu_markers SET latitude = :lat, longitude = :lng, alamat = :alamat WHERE id = :id"); + $stmt->bindValue(':lat', $data->lat); + $stmt->bindValue(':lng', $data->lng); + $stmt->bindValue(':alamat', htmlspecialchars(strip_tags($data->alamat))); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Koordinat dan alamat diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_spbu/api_update_spbu.php b/otak_spbu/api_update_spbu.php new file mode 100644 index 0000000..0805c4f --- /dev/null +++ b/otak_spbu/api_update_spbu.php @@ -0,0 +1,27 @@ +id) && !empty($data->nama) && !empty($data->status)) { + try { + $query = "UPDATE spbu_markers SET nama_spbu = :nama, status_operasional = :status, no_telp = :telp WHERE id = :id"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':telp', htmlspecialchars(strip_tags($data->telp))); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data SPBU berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_tanah/api_hapus_tanah.php b/otak_tanah/api_hapus_tanah.php new file mode 100644 index 0000000..271cd8f --- /dev/null +++ b/otak_tanah/api_hapus_tanah.php @@ -0,0 +1,21 @@ +id)) { + try { + $stmt = $conn->prepare("DELETE FROM tanah_polygons WHERE id = :id"); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Parsil tanah berhasil dihapus."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/otak_tanah/api_simpan_tanah.php b/otak_tanah/api_simpan_tanah.php new file mode 100644 index 0000000..5a1f124 --- /dev/null +++ b/otak_tanah/api_simpan_tanah.php @@ -0,0 +1,30 @@ +nama) && !empty($data->status) && !empty($data->koordinat)) { + try { + $query = "INSERT INTO tanah_polygons (nama_pemilik, status_kepemilikan, luas_meter, koordinat) + VALUES (:nama, :status, :luas, :koordinat)"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':luas', $data->luas, PDO::PARAM_INT); + $stmt->bindValue(':koordinat', $data->koordinat); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data tanah berhasil ditambahkan."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} else { + http_response_code(400); + echo json_encode(["error" => "Data tidak lengkap."]); +} +?> \ No newline at end of file diff --git a/otak_tanah/api_tampil_tanah.php b/otak_tanah/api_tampil_tanah.php new file mode 100644 index 0000000..ebf0457 --- /dev/null +++ b/otak_tanah/api_tampil_tanah.php @@ -0,0 +1,13 @@ +query("SELECT * FROM tanah_polygons ORDER BY created_at DESC"); + $data = $stmt->fetchAll(PDO::FETCH_ASSOC); + echo json_encode($data); +} catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => "Gagal mengambil data: " . $e->getMessage()]); +} +?> \ No newline at end of file diff --git a/otak_tanah/api_update_tanah.php b/otak_tanah/api_update_tanah.php new file mode 100644 index 0000000..870bed4 --- /dev/null +++ b/otak_tanah/api_update_tanah.php @@ -0,0 +1,25 @@ +id) && !empty($data->nama) && !empty($data->status)) { + try { + $query = "UPDATE tanah_polygons SET nama_pemilik = :nama, status_kepemilikan = :status WHERE id = :id"; + $stmt = $conn->prepare($query); + + $stmt->bindValue(':nama', htmlspecialchars(strip_tags($data->nama))); + $stmt->bindValue(':status', htmlspecialchars(strip_tags($data->status))); + $stmt->bindValue(':id', $data->id, PDO::PARAM_INT); + + if($stmt->execute()) { + echo json_encode(["pesan" => "Data Tanah berhasil diperbarui."]); + } + } catch(PDOException $e) { + http_response_code(500); + echo json_encode(["error" => $e->getMessage()]); + } +} +?> \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..227c363 --- /dev/null +++ b/style.css @@ -0,0 +1,740 @@ +/* ========================================= + style.css - WEBGIS PONTIANAK V3 (TERPADU) + ========================================= */ + +:root { + --navy: #0A2540; + --navy-light: #1A365D; + --white: #FFFFFF; + --gray-bg: #F8F9FA; + --border: #E2E8F0; + --gold: #D69E2E; + --blue: #3182CE; + --red-orange: #DD6B20; + --green-safe: #38A169; + --red-danger: #E53E3E; +} + +html, body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + overflow: hidden; +} + +#map { + position: absolute; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1; + background-color: #e5e5e5; +} + +/* --- SIDEBAR & TABS --- */ +#sidebar-container { + position: absolute; + top: 0; + left: 0; + width: 380px; + height: 100%; + background-color: var(--gray-bg); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + z-index: 1000; + transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 2px 0 15px rgba(0,0,0,0.2); +} + +#sidebar-container.collapsed { + transform: translateX(-100%); +} + +#sidebar-header { + background-color: var(--navy); + color: var(--white); + padding: 15px; + font-size: 1.1rem; + font-weight: bold; + text-align: center; +} + +/* Tab System */ +.sidebar-tabs { + display: flex; + background: var(--navy-light); +} + +.tab-btn { + flex: 1; + padding: 12px 0; + background: transparent; + border: none; + color: #A0AEC0; + font-weight: bold; + cursor: pointer; + border-bottom: 3px solid transparent; + transition: all 0.2s; +} + +.tab-btn:hover { + color: var(--white); + background: rgba(255, 255, 255, 0.05); +} + +.tab-btn.active { + color: var(--white); + border-bottom-color: var(--gold); + background: rgba(255, 255, 255, 0.1); +} + +.tab-content { + display: none; + flex-direction: column; + flex-grow: 1; + overflow: hidden; +} + +.tab-content.active { + display: flex; +} + +/* Control Panel (Tab 1 - Kemiskinan) */ +.control-panel { + padding: 15px; + background: white; + border-bottom: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 12px; +} + +.btn-gps { + background: var(--blue); + color: white; + border-radius: 6px; + padding: 10px; + border: none; + font-weight: bold; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.btn-gps:hover { + background: #2B6CB0; +} + +.add-marker-box { + background: #F8FAFC; + border: 1px solid var(--border); + border-radius: 6px; + display: flex; + flex-direction: column; +} + +.add-marker-box label { + font-size: 0.8rem; + font-weight: bold; + color: var(--navy); +} + +.modern-input { + flex: 1; + padding: 8px; + border: 1px solid #CBD5E0; + border-radius: 4px; + font-weight: bold; + color: var(--navy); + background: var(--white); +} + +.modern-input:focus { + outline: none; + border-color: var(--blue); +} + +/* Kotak Aksi Modern */ +.modern-action-box { + background: #FFFFFF; + border: 1px solid var(--border); + border-radius: 8px; + padding: 15px; + display: flex; + flex-direction: column; + gap: 12px; + box-shadow: 0 2px 5px rgba(0,0,0,0.02); +} + +.modern-action-box label { + font-size: 0.85rem; + font-weight: 700; + color: var(--navy-light); + margin-bottom: -5px; +} + +.modern-select { + width: 100%; + padding: 10px; + border: 1px solid #CBD5E0; + border-radius: 6px; + background-color: #F8FAFC; + font-size: 0.9rem; + font-weight: bold; + color: var(--navy); + transition: all 0.2s; + cursor: pointer; +} + +.modern-select:focus { + outline: none; + border-color: var(--navy); + box-shadow: 0 0 0 3px rgba(10, 37, 64, 0.1); +} + +.btn-modern-add { + background: var(--navy); + color: var(--white); + border: none; + border-radius: 6px; + padding: 12px; + font-weight: bold; + font-size: 0.95rem; + cursor: pointer; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); +} + +.btn-modern-add:hover { + background: var(--navy-light); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +.btn-modern-add.active { + background: var(--gold); + color: var(--white); + box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); + transform: translateY(1px); +} + +/* LEGENDA PETA */ +.map-legend { + background: #F8FAFC; + border: 1px solid var(--border); + border-radius: 8px; + padding: 12px 15px; + margin-top: 5px; +} + +.legend-title { + margin: 0 0 10px 0; + font-size: 0.8rem; + color: var(--navy-light); + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 800; +} + +.legend-item { + display: flex; + align-items: center; + gap: 10px; + font-size: 0.8rem; + color: #4A5568; + margin-bottom: 8px; + font-weight: 500; +} + +.legend-item:last-child { + margin-bottom: 0; +} + +.legend-color { + width: 14px; + height: 14px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 1px 3px rgba(0,0,0,0.2); +} + +.card-list-area { + flex-grow: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +#toggle-sidebar { + position: absolute; + right: -28px; + top: 50%; + transform: translateY(-50%); + width: 28px; + background: var(--navy); + color: var(--white); + border: none; + padding: 15px 0; + cursor: pointer; + border-radius: 0 6px 6px 0; + box-shadow: 3px 0 5px rgba(0,0,0,0.1); + display: flex; + justify-content: center; + align-items: center; +} + +#toggle-sidebar:hover { + background: var(--navy-light); +} + +/* --- CARD & LIST --- */ +#filter-bar { + background-color: var(--navy-light); + padding: 10px 15px; + display: flex; + align-items: center; + gap: 10px; +} + +.filter-tool-btn { + width: 35px; + height: 35px; + background: var(--white); + border: none; + border-radius: 4px; + font-size: 1.1rem; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; +} + +.filter-label { + flex-grow: 1; + text-align: center; + font-size: 0.85rem; + font-weight: bold; + color: var(--white); +} + +.spbu-card { + background: var(--white); + border: 1px solid var(--border); + border-left: 4px solid var(--border); + border-radius: 4px; + padding: 10px 12px; + cursor: pointer; + box-shadow: 0 1px 2px rgba(0,0,0,0.05); + transition: transform 0.1s ease; +} + +.spbu-card:hover { + transform: translateY(-1px); + box-shadow: 0 3px 6px rgba(0,0,0,0.1); +} + +.spbu-card h4 { + margin: 0 0 4px 0; + font-size: 0.95rem; + color: var(--navy); +} + +.spbu-card p { + margin: 0; + font-size: 0.8rem; + color: #4A5568; +} + +/* Hierarki Warna Card */ +.status-buka { border-left-color: #38A169 !important; } +.status-tutup { border-left-color: #E53E3E !important; } +.card-ibadah { border-left-color: var(--gold) !important; } +.card-penduduk-miskin { border-left-color: #E53E3E !important; background: #FFF5F5; } +.card-penduduk-rentan { border-left-color: var(--red-orange) !important; } +.card-penduduk-aman { border-left-color: var(--blue) !important; } + +/* --- LEAFLET UI --- */ +.custom-leaflet-toolbar { + background: var(--white); + border: 1px solid var(--border); + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.tool-btn { + width: 42px; + height: 42px; + background: var(--white); + border: none; + border-bottom: 1px solid var(--border); + font-size: 1.2rem; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + transition: background 0.2s; +} + +.tool-btn:last-child { + border-bottom: none; +} + +.tool-btn.active { + background: var(--navy); + color: var(--white); +} + +.leaflet-right .leaflet-control-zoom { + margin-right: 15px; + margin-bottom: 15px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + border: none; +} + +/* --- MODAL GENERAL --- */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0,0,0,0.6); + z-index: 9999; + display: none; + justify-content: center; + align-items: center; +} + +.modal-content { + background: white; + width: 380px; + max-height: 90vh; + overflow-y: auto; + border-radius: 8px; + padding: 20px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + position: relative; + animation: modalFadeIn 0.3s; +} + +@keyframes modalFadeIn { + from { opacity: 0; transform: translateY(-15px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +.form-group { + margin-bottom: 12px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.form-group label { + font-size: 0.8rem; + font-weight: bold; + color: var(--navy-light); +} + +.form-group input, +.form-group select, +.form-group textarea { + width: 100%; + padding: 8px 10px; + border: 1px solid #A0AEC0; + border-radius: 6px; + box-sizing: border-box; + font-size: 0.9rem; + background: #F8FAFC; + transition: all 0.3s; + resize: none; +} + +.dynamic-select { + border-left-width: 6px !important; +} + +.form-group input:focus, +.form-group select:focus, +.form-group textarea:focus { + outline: none; + border-color: var(--navy); + background: #FFFFFF; + box-shadow: 0 0 0 3px rgba(10, 37, 64, 0.1); +} + +.form-group input[readonly], +.form-group select[disabled], +.form-group textarea[readonly] { + background: #EDF2F7; + color: #718096; + cursor: not-allowed; +} + +input[type=range] { + -webkit-appearance: none; + appearance: none; + width: 100%; + background: transparent; + padding: 0 !important; + border: none !important; +} + +input[type=range]::-webkit-slider-thumb { + -webkit-appearance: none; + height: 16px; + width: 16px; + border-radius: 50%; + background: var(--navy); + cursor: pointer; + margin-top: -6px; + box-shadow: 0 1px 3px rgba(0,0,0,0.3); +} + +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 4px; + cursor: pointer; + background: #CBD5E0; + border-radius: 2px; +} + +.btn-group, +#spbu-action-wrapper, +#jalan-action-wrapper, +#tanah-action-wrapper, +#penduduk-action-wrapper, +#ibadah-action-wrapper { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.btn { + flex: 1; + padding: 10px 15px; + border: none; + border-radius: 6px; + font-weight: bold; + font-size: 0.9rem; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.btn-save { background: var(--navy); color: var(--white); } +.btn-save:hover { background: var(--navy-light); transform: translateY(-2px); } +.btn-cancel { background: #E2E8F0; color: #4A5568; } +.btn-cancel:hover { background: #CBD5E0; } +.btn-warning { background: var(--gold); color: var(--white); } +.btn-warning:hover { background: #B7791F; transform: translateY(-2px); } +.btn-danger { background: #E53E3E; color: var(--white); } +.btn-danger:hover { background: #C53030; transform: translateY(-2px); } +.btn-success { background: #38A169; color: var(--white); } +.btn-success:hover { background: #2F855A; transform: translateY(-2px); } + +/* Tombol Draw Polylines */ +#btn-selesai-jalan, +#btn-selesai-tanah { + position: absolute; + bottom: 30px; + left: 50%; + transform: translateX(-50%); + z-index: 1000; + color: white; + border: none; + padding: 12px 30px; + font-weight: bold; + font-size: 1rem; + border-radius: 30px; + cursor: pointer; + display: none; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + transition: all 0.2s; +} + +#btn-selesai-jalan { background: #38A169; } +#btn-selesai-jalan:hover { background: #2F855A; transform: translateX(-50%) translateY(-2px); } +#btn-selesai-tanah { background: #D69E2E; } +#btn-selesai-tanah:hover { background: #B7791F; transform: translateX(-50%) translateY(-2px); } + +#toast-notification { + position: fixed; + top: -60px; + left: 50%; + transform: translateX(-50%); + background: var(--navy); + color: var(--white); + padding: 12px 30px; + border-radius: 30px; + font-weight: bold; + font-size: 0.9rem; + box-shadow: 0 8px 20px rgba(0,0,0,0.3); + z-index: 10005; + transition: top 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); + border-left: 4px solid #E53E3E; +} + +#toast-notification.show { top: 25px; } + +/* --- KOSMETIK MODAL FILTER --- */ +.filter-modal { width: 320px; } +.filter-section { margin-bottom: 20px; } +.filter-section-title { display: block; font-size: 0.9rem; font-weight: bold; color: var(--navy); margin-bottom: 10px; border-bottom: 1px solid var(--border); padding-bottom: 5px; } +.filter-grid { display: flex; flex-direction: column; gap: 8px; } + +.custom-checkbox-wrapper, .custom-radio-wrapper { display: flex; align-items: center; position: relative; padding: 6px 8px; cursor: pointer; border-radius: 6px; transition: background 0.2s; user-select: none; } +.custom-checkbox-wrapper:hover, .custom-radio-wrapper:hover { background-color: #F8FAFC; } +.custom-checkbox-wrapper input, .custom-radio-wrapper input { position: absolute; opacity: 0; cursor: pointer; height: 0; width: 0; } + +.custom-checkmark { position: relative; height: 18px; width: 18px; background-color: var(--white); border: 2px solid #A0AEC0; border-radius: 4px; margin-right: 12px; transition: all 0.2s; } +.custom-checkbox-wrapper input:checked ~ .custom-checkmark { background-color: var(--navy); border-color: var(--navy); } +.custom-checkmark::after { content: ""; position: absolute; display: none; left: 5px; top: 1px; width: 4px; height: 9px; border: solid white; border-width: 0 2px 2px 0; transform: rotate(45deg) scale(0); transition: transform 0.2s; } +.custom-checkbox-wrapper input:checked ~ .custom-checkmark::after { display: block; transform: rotate(45deg) scale(1); } + +.custom-radiomark { position: relative; height: 18px; width: 18px; background-color: var(--white); border: 2px solid #A0AEC0; border-radius: 50%; margin-right: 12px; transition: all 0.2s; } +.custom-radio-wrapper input:checked ~ .custom-radiomark { background-color: var(--white); border-color: var(--navy); } +.custom-radiomark::after { content: ""; position: absolute; display: none; top: 3px; left: 3px; width: 12px; height: 12px; border-radius: 50%; background: var(--navy); transform: scale(0); transition: transform 0.2s; } +.custom-radio-wrapper input:checked ~ .custom-radiomark::after { display: block; transform: scale(1); } + +.filter-text { flex-grow: 1; font-size: 0.85rem; color: #2D3748; } +.color-dot { width: 12px; height: 12px; border-radius: 50%; box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); } + +/* --- CUSTOM MARKERS DOM --- */ +.custom-div-icon { background: transparent; border: none; } +.marker-wrapper { width: 100%; height: 100%; position: relative; transform-origin: bottom center; transition: transform 0.2s; } +.custom-div-icon:hover .marker-wrapper { transform: scale(1.3); } +.marker-pin { width: 30px; height: 30px; border-radius: 50% 50% 50% 0; position: absolute; transform: rotate(-45deg); left: 0; top: 6px; box-shadow: -3px 3px 6px rgba(0,0,0,0.4); } +.marker-dot { width: 12px; height: 12px; background: white; position: absolute; border-radius: 50%; left: 9px; top: 15px; box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); } + +/* Khusus Rumah Ibadah (Kotak/Emas) */ +.marker-ibadah { width: 24px; height: 24px; background: var(--gold); border: 2px solid white; border-radius: 4px; position: absolute; left: 3px; top: 9px; transform: rotate(45deg); box-shadow: 0 2px 5px rgba(0,0,0,0.4); } +.marker-ibadah-dot { width: 8px; height: 8px; background: white; position: absolute; border-radius: 50%; left: 11px; top: 17px; } + +/* Animasi GPU-Accelerated untuk status Darurat (Musibah) */ +@keyframes siren-blink { + 0% { background-color: var(--blue); } + 50% { background-color: var(--red-danger); transform: scale(1.1); box-shadow: 0 0 15px rgba(229, 62, 62, 0.8); } + 100% { background-color: var(--blue); } +} +.marker-darurat .marker-pin { animation: siren-blink 1s infinite; } + +.spbu-tooltip { background: var(--navy); color: var(--white); border: none; border-radius: 6px; padding: 6px 12px; font-weight: normal; font-size: 0.85rem; box-shadow: 0 4px 10px rgba(0,0,0,0.2); } +.spbu-tooltip .leaflet-tooltip-top::before { border-top-color: var(--navy); } + +/* ==================================================== + KOSMETIK KHUSUS CHAT BUBBLE (POPUP FORM) + ==================================================== */ +.leaflet-popup-content-wrapper { border-radius: 8px; box-shadow: 0 8px 25px rgba(0,0,0,0.2); } +.leaflet-popup-content { width: 350px !important; margin: 15px; } + +.popup-form h4 { margin: 0 0 10px 0; border-bottom: 2px solid var(--border); padding-bottom: 8px; color: var(--navy); font-size: 1.05rem;} +.popup-form .form-group { margin-bottom: 8px; display: flex; flex-direction: column; gap: 4px; } +.popup-form label { font-size: 0.75rem; font-weight: bold; color: var(--navy-light); } + +.popup-form input, .popup-form select, .popup-form textarea { + width: 100%; padding: 8px 8px; border: 1px solid #A0AEC0; + border-radius: 4px; font-size: 0.85rem; background: #F8FAFC; + box-sizing: border-box; resize: none; +} +.popup-form input:focus, .popup-form select:focus, .popup-form textarea:focus { outline: none; border-color: var(--navy); } +.popup-form .pmt-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 5px; } +.popup-form .pmt-grid .form-group { margin-bottom: 0; } +.popup-form .pmt-grid select { font-size: 0.75rem; padding: 6px; } + +/* ==================================================== + UI BACA SAJA & ACCORDION (MARKER PENDUDUK KLIK) + ==================================================== */ +.pop-read-only { display: flex; flex-direction: column; } +.pop-read-title { font-size: 1.35rem; margin: 0 0 10px 0; color: var(--navy); } + +.pop-read-meta { display: flex; flex-direction: column; gap: 3px; font-size: 0.8rem; color: #4A5568; margin-bottom: 10px; font-weight: 600; } + +.pop-read-alamat { font-size: 0.8rem; color: #4A5568; margin: 0 0 12px 0; line-height: 1.4; border-left: 3px solid #CBD5E0; padding-left: 8px; } + +.pop-badge-container { margin-bottom: 12px; } + +.pop-badge { padding: 6px 12px; border-radius: 6px; font-weight: bold; font-size: 0.95rem; color: white; display: inline-block; box-shadow: 0 2px 4px rgba(0,0,0,0.15); } + +.badge-sangat-miskin { background-color: var(--red-danger); } +.badge-miskin { background-color: var(--red-orange); } +.badge-rentan { background-color: var(--gold); } +.badge-aman { background-color: var(--green-safe); } + +.pop-naungan { margin-top: 10px; padding: 8px; font-size: 0.8rem; font-weight: 600; border-radius: 4px; text-align: center; } +.naungan-aktif { background-color: rgba(56, 161, 105, 0.15); color: var(--green-safe); border: 1px solid rgba(56, 161, 105, 0.3); } +.naungan-pasif { background-color: rgba(229, 62, 62, 0.1); color: var(--red-danger); border: 1px dashed rgba(229, 62, 62, 0.3); } + +.pop-divider { border: 0; border-bottom: 1px dashed var(--border); margin: 15px 0; width: 100%; } +.pop-divider-sub { border: 0; border-top: 1px solid #E2E8F0; margin: 8px 0 12px 0; width: 100%; } + +.accordion-wrapper { display: flex; flex-direction: column; width: 100%; } +.btn-accordion { background: #EDF2F7; color: #4A5568; font-weight: bold; padding: 8px; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s; width: 100%; } +.btn-accordion:hover { background: #E2E8F0; } + +.accordion-content { display: none; margin-top: 0; overflow: hidden; opacity: 0; transition: opacity 0.3s; } +.accordion-content.show { display: block; opacity: 1; } + +.table-responsive { width: 100%; overflow-x: auto; max-height: 400px; overflow-y: auto; margin-top: 10px; border: 1px solid var(--border); border-radius: 6px; } +.riwayat-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; } +.riwayat-table th { background-color: var(--navy); color: white; padding: 10px; position: sticky; top: 0; z-index: 1; white-space: nowrap; } +.riwayat-table td { padding: 10px; border-bottom: 1px solid var(--border); white-space: nowrap; } +.riwayat-table tbody tr:nth-child(even) { background-color: #F8FAFC; } +.riwayat-table tbody tr:hover { background-color: #EDF2F7; } + +/* KUNCI PERBAIKAN: CSS UNTUK LAYAR KUNCI AUTENTIKASI (GATEKEEPER) */ +.auth-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(10, 37, 64, 0.95); /* Navy transparent */ + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 999999; /* Absolut di atas segalanya */ + display: flex; + justify-content: center; + align-items: center; +} + +.auth-box { + background: var(--white); + padding: 35px 30px; + border-radius: 12px; + width: 360px; + box-shadow: 0 20px 40px rgba(0,0,0,0.5); + animation: authPop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +@keyframes authPop { + from { opacity: 0; transform: scale(0.8); } + to { opacity: 1; transform: scale(1); } +} + +.auth-tab { + flex: 1; + padding: 12px; + border: none; + background: #EDF2F7; + font-weight: bold; + color: #718096; + cursor: pointer; + transition: all 0.2s; +} + +.auth-tab.active { + background: var(--blue); + color: var(--white); +} \ No newline at end of file