// ============================================ // app.js — Main Application Logic // ============================================ // ------- API Endpoints ------- const API = { SPBU: { READ: 'backend/read.php', CREATE: 'backend/create.php', UPDATE: 'backend/update.php', DELETE: 'backend/delete.php' }, JALAN: { READ: 'backend/read_jalan.php', CREATE: 'backend/create_jalan.php', UPDATE: 'backend/update_jalan.php', DELETE: 'backend/delete_jalan.php' }, PARSIL: { READ: 'backend/read_parsil.php', CREATE: 'backend/create_parsil.php', UPDATE: 'backend/update_parsil.php', DELETE: 'backend/delete_parsil.php' }, IBADAH: { READ: 'backend/read_ibadah.php', CREATE: 'backend/create_ibadah.php', UPDATE: 'backend/update_ibadah.php', DELETE: 'backend/delete_ibadah.php' }, MISKIN: { READ: 'backend/read_miskin.php', CREATE: 'backend/create_miskin.php', UPDATE: 'backend/update_miskin.php', DELETE: 'backend/delete_miskin.php' } }; // ------- Global State ------- window.appState = { data: { spbu: [], jalan: [], parsil: [], ibadah: [], miskin: [] }, currentPanel: 'panel-map', editingId: null, editingType: null, mapInstance: null, layers: {} }; // ============================================ // Map Toolbar — Global Functions (called via onclick in HTML) // ============================================ window._mapClickMode = null; window.mapToolActivatePoint = function(type, label) { window._mapClickMode = type; document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById('map-add-' + type); if (btn) btn.classList.add('active'); const status = document.getElementById('map-tool-status'); const cancel = document.getElementById('map-tool-cancel'); if (status) { status.style.display = 'block'; status.innerHTML = '📍 Klik lokasi di peta
untuk tambah ' + label + ''; } if (cancel) cancel.style.display = 'block'; const inst = window.appState.mapInstance; if (inst) inst.getContainer().style.cursor = 'crosshair'; }; window.mapToolActivateDraw = function(type) { const inst = window.appState.mapInstance; document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); const btn = document.getElementById('map-add-' + type); if (btn) btn.classList.add('active'); const status = document.getElementById('map-tool-status'); const cancel = document.getElementById('map-tool-cancel'); if (inst && inst.pm && typeof inst.pm.enableDraw === 'function') { inst.pm.enableDraw(type === 'jalan' ? 'Line' : 'Polygon'); if (status) { status.style.display = 'block'; status.innerHTML = type === 'jalan' ? '✏️ Gambar garis
Klik tiap titik, double-klik selesai' : '✏️ Gambar area
Klik tiap sudut, double-klik selesai'; } if (cancel) cancel.style.display = 'block'; } else { if (type === 'jalan') openAddJalanModal(); else openAddParsilModal(); } }; window.mapToolCancel = function() { window._mapClickMode = null; document.querySelectorAll('.map-tool-btn').forEach(b => b.classList.remove('active')); const status = document.getElementById('map-tool-status'); const cancel = document.getElementById('map-tool-cancel'); if (status) status.style.display = 'none'; if (cancel) cancel.style.display = 'none'; const inst = window.appState.mapInstance; if (inst) { inst.getContainer().style.cursor = ''; if (inst.pm && typeof inst.pm.disableDraw === 'function') inst.pm.disableDraw(); } }; // ============================================ // Utility Functions // ============================================ function showToast(message, type = 'info', duration = 3000) { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; const icon = { success: '✓', error: '✗', info: 'i', warning: '!' }[type] || 'i'; toast.innerHTML = `${icon}${message}`; container.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300); }, duration); } function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add('show'); } } function hideModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove('show'); } } function switchPanel(panelId) { // Hide all panels document.querySelectorAll('.content-panel').forEach(p => { p.classList.remove('active'); }); // Deactivate all sidebar items document.querySelectorAll('.sidebar-item').forEach(item => { item.classList.remove('active'); }); // Show selected panel const panel = document.getElementById(panelId); if (panel) { panel.classList.add('active'); // Mark sidebar item as active const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`); if (sidebarItem) { sidebarItem.classList.add('active'); } } // Update page title const sidebarItem = document.querySelector(`[data-panel="${panelId}"]`); if (sidebarItem) { const title = sidebarItem.querySelector('.sidebar-item-text')?.textContent || 'Dashboard'; document.getElementById('page-title').textContent = title; document.getElementById('breadcrumb-text').textContent = title; } window.appState.currentPanel = panelId; } // ============================================ // Data Loading & Management // ============================================ async function loadAllData() { try { const [spbuRes, jalanRes, parsilRes, ibadahRes, miskinRes] = await Promise.all([ fetch(API.SPBU.READ), fetch(API.JALAN.READ), fetch(API.PARSIL.READ), fetch(API.IBADAH.READ), fetch(API.MISKIN.READ) ]); const spbuData = await spbuRes.json(); const jalanData = await jalanRes.json(); const parsilData = await parsilRes.json(); const ibadahData = await ibadahRes.json(); const miskinData = await miskinRes.json(); window.appState.data.spbu = spbuData.success ? spbuData.data : []; window.appState.data.jalan = jalanData.success ? jalanData.data : []; window.appState.data.parsil = parsilData.success ? parsilData.data : []; window.appState.data.ibadah = ibadahData.success ? ibadahData.data : []; window.appState.data.miskin = miskinData.success ? miskinData.data : []; updateBadges(); updateAnalytics(); renderTables(); renderMapLayers(); } catch (err) { console.error('Error loading data:', err); showToast('Gagal memuat data', 'error'); } } function updateBadges() { document.getElementById('badge-spbu').textContent = window.appState.data.spbu.length; document.getElementById('badge-jalan').textContent = window.appState.data.jalan.length; document.getElementById('badge-parsil').textContent = window.appState.data.parsil.length; document.getElementById('badge-ibadah').textContent = window.appState.data.ibadah.length; document.getElementById('badge-miskin').textContent = window.appState.data.miskin.length; const totalData = window.appState.data.spbu.length + window.appState.data.jalan.length + window.appState.data.parsil.length + window.appState.data.ibadah.length + window.appState.data.miskin.length; document.getElementById('total-data-count').textContent = `${totalData} Data`; } function updateAnalytics() { document.getElementById('stat-spbu').textContent = window.appState.data.spbu.length; document.getElementById('stat-jalan').textContent = window.appState.data.jalan.length; document.getElementById('stat-parsil').textContent = window.appState.data.parsil.length; document.getElementById('stat-ibadah').textContent = window.appState.data.ibadah.length; document.getElementById('stat-miskin').textContent = window.appState.data.miskin.length; // Count SPBU 24 jam vs tidak const spbu24 = window.appState.data.spbu.filter(s => s.status === '24jam').length; const spbuTidak = window.appState.data.spbu.filter(s => s.status === 'tidak').length; document.getElementById('stat-spbu-trend').textContent = `${spbu24} aktif 24jam`; // Count jalan by type const jalanNasional = window.appState.data.jalan.filter(j => j.status === 'Nasional').length; document.getElementById('stat-jalan-trend').textContent = `${jalanNasional} Nasional`; } // ============================================ // Table Rendering // ============================================ function renderTables() { renderSPBUTable(); renderJalanTable(); renderParsilTable(); renderIbadahTable(); renderMiskinTable(); } function renderSPBUTable(filter = 'all', search = '') { const tbody = document.getElementById('table-spbu-body'); let data = window.appState.data.spbu; if (filter !== 'all') { data = data.filter(item => item.status === filter); } if (search) { data = data.filter(item => item.nama.toLowerCase().includes(search.toLowerCase()) || item.no_spbu.toLowerCase().includes(search.toLowerCase()) ); } if (data.length === 0) { tbody.innerHTML = 'Tidak ada data'; return; } tbody.innerHTML = data.map(item => ` ${item.nama} ${item.no_spbu} ${item.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'} ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}
`).join(''); } function renderJalanTable(filter = 'all', search = '') { const tbody = document.getElementById('table-jalan-body'); let data = window.appState.data.jalan; if (filter !== 'all') { data = data.filter(item => item.status === filter); } if (search) { data = data.filter(item => item.nama_jalan.toLowerCase().includes(search.toLowerCase())); } if (data.length === 0) { tbody.innerHTML = 'Tidak ada data'; return; } tbody.innerHTML = data.map(item => ` ${item.nama_jalan} ${item.status} ${item.panjang.toFixed(0)} m
`).join(''); } function renderParsilTable(filter = 'all', search = '') { const tbody = document.getElementById('table-parsil-body'); let data = window.appState.data.parsil; if (filter !== 'all') { data = data.filter(item => item.status === filter); } if (search) { data = data.filter(item => item.nama_pemilik.toLowerCase().includes(search.toLowerCase())); } if (data.length === 0) { tbody.innerHTML = 'Tidak ada data'; return; } tbody.innerHTML = data.map(item => ` ${item.nama_pemilik} ${item.status} ${item.luas.toFixed(0)} m2
`).join(''); } function renderIbadahTable(search = '') { const tbody = document.getElementById('table-ibadah-body'); let data = window.appState.data.ibadah; if (search) { data = data.filter(item => item.nama.toLowerCase().includes(search.toLowerCase()) || item.alamat.toLowerCase().includes(search.toLowerCase()) ); } if (data.length === 0) { tbody.innerHTML = 'Tidak ada data'; return; } tbody.innerHTML = data.map(item => ` ${item.nama} ${item.alamat.substring(0, 30)}... ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}
`).join(''); } function renderMiskinTable(search = '') { const tbody = document.getElementById('table-miskin-body'); let data = window.appState.data.miskin; if (search) { data = data.filter(item => item.nama_kk.toLowerCase().includes(search.toLowerCase()) ); } if (data.length === 0) { tbody.innerHTML = 'Tidak ada data'; return; } tbody.innerHTML = data.map(item => ` ${item.nama_kk} ${item.keterangan || '-'} ${item.latitude.toFixed(4)}, ${item.longitude.toFixed(4)}
`).join(''); } // ============================================ // CRUD Operations - SPBU // ============================================ function openAddSPBUModal() { document.getElementById('form-spbu-id').value = ''; document.getElementById('form-spbu-nama').value = ''; document.getElementById('form-spbu-no').value = ''; document.getElementById('form-spbu-status').value = ''; document.getElementById('form-spbu-lat').value = ''; document.getElementById('form-spbu-lng').value = ''; document.getElementById('modal-spbu-title').textContent = 'Add SPBU'; window.appState.editingId = null; window.appState.editingType = 'spbu'; showModal('modal-spbu'); } async function editSPBU(id) { const item = window.appState.data.spbu.find(s => s.id == id); if (!item) return; document.getElementById('form-spbu-id').value = item.id; document.getElementById('form-spbu-nama').value = item.nama; document.getElementById('form-spbu-no').value = item.no_spbu; document.getElementById('form-spbu-status').value = item.status; document.getElementById('form-spbu-lat').value = item.latitude; document.getElementById('form-spbu-lng').value = item.longitude; document.getElementById('modal-spbu-title').textContent = 'Edit SPBU'; window.appState.editingId = id; window.appState.editingType = 'spbu'; showModal('modal-spbu'); } async function saveSPBU() { const id = document.getElementById('form-spbu-id').value; const nama = document.getElementById('form-spbu-nama').value; const no_spbu = document.getElementById('form-spbu-no').value; const status = document.getElementById('form-spbu-status').value; const latitude = parseFloat(document.getElementById('form-spbu-lat').value); const longitude = parseFloat(document.getElementById('form-spbu-lng').value); if (!nama || !no_spbu || !status || !latitude || !longitude) { showToast('Silakan isi semua field', 'error'); return; } try { const payload = { nama, no_spbu, status, latitude, longitude }; const endpoint = id ? API.SPBU.UPDATE : API.SPBU.CREATE; if (id) payload.id = id; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (result.success) { showToast(id ? 'SPBU updated successfully' : 'SPBU created successfully', 'success'); hideModal('modal-spbu'); loadAllData(); } else { showToast(result.message || 'Error saving SPBU', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error saving SPBU', 'error'); } } async function deleteSPBU(id) { if (!confirm('Are you sure you want to delete this SPBU?')) return; try { const res = await fetch(API.SPBU.DELETE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const result = await res.json(); if (result.success) { showToast('SPBU deleted successfully', 'success'); loadAllData(); } else { showToast(result.message || 'Error deleting SPBU', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error deleting SPBU', 'error'); } } // ============================================ // CRUD Operations - JALAN // ============================================ function openAddJalanModal() { document.getElementById('form-jalan-id').value = ''; document.getElementById('form-jalan-nama').value = ''; document.getElementById('form-jalan-status').value = ''; document.getElementById('form-jalan-panjang').value = ''; document.getElementById('form-jalan-geom').value = ''; document.getElementById('modal-jalan-title').textContent = 'Add Jalan'; showModal('modal-jalan'); } async function editJalan(id) { const item = window.appState.data.jalan.find(j => j.id == id); if (!item) return; document.getElementById('form-jalan-id').value = item.id; document.getElementById('form-jalan-nama').value = item.nama_jalan; document.getElementById('form-jalan-status').value = item.status; document.getElementById('form-jalan-panjang').value = item.panjang; document.getElementById('form-jalan-geom').value = item.geom; document.getElementById('modal-jalan-title').textContent = 'Edit Jalan'; showModal('modal-jalan'); } async function saveJalan() { const id = document.getElementById('form-jalan-id').value; const nama_jalan = document.getElementById('form-jalan-nama').value; const status = document.getElementById('form-jalan-status').value; const panjang = parseFloat(document.getElementById('form-jalan-panjang').value); const geom = document.getElementById('form-jalan-geom').value; if (!nama_jalan || !status || !panjang || !geom) { showToast('Silakan isi semua field', 'error'); return; } try { const payload = { nama_jalan, status, panjang, geom }; const endpoint = id ? API.JALAN.UPDATE : API.JALAN.CREATE; if (id) payload.id = id; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (result.success) { showToast(id ? 'Jalan updated successfully' : 'Jalan created successfully', 'success'); hideModal('modal-jalan'); loadAllData(); } else { showToast(result.message || 'Error saving Jalan', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error saving Jalan', 'error'); } } async function deleteJalan(id) { if (!confirm('Are you sure you want to delete this Jalan?')) return; try { const res = await fetch(API.JALAN.DELETE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const result = await res.json(); if (result.success) { showToast('Jalan deleted successfully', 'success'); loadAllData(); } else { showToast(result.message || 'Error deleting Jalan', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error deleting Jalan', 'error'); } } // ============================================ // CRUD Operations - PARSIL // ============================================ function openAddParsilModal() { document.getElementById('form-parsil-id').value = ''; document.getElementById('form-parsil-nama').value = ''; document.getElementById('form-parsil-status').value = ''; document.getElementById('form-parsil-luas').value = ''; document.getElementById('form-parsil-geom').value = ''; document.getElementById('modal-parsil-title').textContent = 'Add Parsil'; showModal('modal-parsil'); } async function editParsil(id) { const item = window.appState.data.parsil.find(p => p.id == id); if (!item) return; document.getElementById('form-parsil-id').value = item.id; document.getElementById('form-parsil-nama').value = item.nama_pemilik; document.getElementById('form-parsil-status').value = item.status; document.getElementById('form-parsil-luas').value = item.luas; document.getElementById('form-parsil-geom').value = item.geom; document.getElementById('modal-parsil-title').textContent = 'Edit Parsil'; showModal('modal-parsil'); } async function saveParsil() { const id = document.getElementById('form-parsil-id').value; const nama_pemilik = document.getElementById('form-parsil-nama').value; const status = document.getElementById('form-parsil-status').value; const luas = parseFloat(document.getElementById('form-parsil-luas').value); const geom = document.getElementById('form-parsil-geom').value; if (!nama_pemilik || !status || !luas || !geom) { showToast('Silakan isi semua field', 'error'); return; } try { const payload = { nama_pemilik, status, luas, geom }; const endpoint = id ? API.PARSIL.UPDATE : API.PARSIL.CREATE; if (id) payload.id = id; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (result.success) { showToast(id ? 'Parsil updated successfully' : 'Parsil created successfully', 'success'); hideModal('modal-parsil'); loadAllData(); } else { showToast(result.message || 'Error saving Parsil', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error saving Parsil', 'error'); } } async function deleteParsil(id) { if (!confirm('Are you sure you want to delete this Parsil?')) return; try { const res = await fetch(API.PARSIL.DELETE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const result = await res.json(); if (result.success) { showToast('Parsil deleted successfully', 'success'); loadAllData(); } else { showToast(result.message || 'Error deleting Parsil', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error deleting Parsil', 'error'); } } // ============================================ // CRUD Operations - IBADAH // ============================================ function openAddIbadahModal() { document.getElementById('form-ibadah-id').value = ''; document.getElementById('form-ibadah-nama').value = ''; document.getElementById('form-ibadah-alamat').value = ''; document.getElementById('form-ibadah-lat').value = ''; document.getElementById('form-ibadah-lng').value = ''; document.getElementById('modal-ibadah-title').textContent = 'Add Rumah Ibadah'; showModal('modal-ibadah'); } async function editIbadah(id) { const item = window.appState.data.ibadah.find(i => i.id == id); if (!item) return; document.getElementById('form-ibadah-id').value = item.id; document.getElementById('form-ibadah-nama').value = item.nama; document.getElementById('form-ibadah-alamat').value = item.alamat; document.getElementById('form-ibadah-lat').value = item.latitude; document.getElementById('form-ibadah-lng').value = item.longitude; document.getElementById('modal-ibadah-title').textContent = 'Edit Rumah Ibadah'; showModal('modal-ibadah'); } async function saveIbadah() { const id = document.getElementById('form-ibadah-id').value; const nama = document.getElementById('form-ibadah-nama').value; const alamat = document.getElementById('form-ibadah-alamat').value; const latitude = parseFloat(document.getElementById('form-ibadah-lat').value); const longitude = parseFloat(document.getElementById('form-ibadah-lng').value); if (!nama || !alamat || !latitude || !longitude) { showToast('Silakan isi semua field', 'error'); return; } try { const payload = { nama, alamat, latitude, longitude }; const endpoint = id ? API.IBADAH.UPDATE : API.IBADAH.CREATE; if (id) payload.id = id; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (result.success) { showToast(id ? 'Ibadah updated successfully' : 'Ibadah created successfully', 'success'); hideModal('modal-ibadah'); loadAllData(); } else { showToast(result.message || 'Error saving Ibadah', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error saving Ibadah', 'error'); } } async function deleteIbadah(id) { if (!confirm('Are you sure you want to delete this Rumah Ibadah?')) return; try { const res = await fetch(API.IBADAH.DELETE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const result = await res.json(); if (result.success) { showToast('Ibadah deleted successfully', 'success'); loadAllData(); } else { showToast(result.message || 'Error deleting Ibadah', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error deleting Ibadah', 'error'); } } // ============================================ // CRUD Operations - MISKIN // ============================================ function openAddMiskinModal() { document.getElementById('form-miskin-id').value = ''; document.getElementById('form-miskin-nama').value = ''; document.getElementById('form-miskin-ket').value = ''; document.getElementById('form-miskin-lat').value = ''; document.getElementById('form-miskin-lng').value = ''; document.getElementById('modal-miskin-title').textContent = 'Add Penduduk Miskin'; showModal('modal-miskin'); } async function editMiskin(id) { const item = window.appState.data.miskin.find(m => m.id == id); if (!item) return; document.getElementById('form-miskin-id').value = item.id; document.getElementById('form-miskin-nama').value = item.nama_kk; document.getElementById('form-miskin-ket').value = item.keterangan || ''; document.getElementById('form-miskin-lat').value = item.latitude; document.getElementById('form-miskin-lng').value = item.longitude; document.getElementById('modal-miskin-title').textContent = 'Edit Penduduk Miskin'; showModal('modal-miskin'); } async function saveMiskin() { const id = document.getElementById('form-miskin-id').value; const nama_kk = document.getElementById('form-miskin-nama').value; const keterangan = document.getElementById('form-miskin-ket').value; const latitude = parseFloat(document.getElementById('form-miskin-lat').value); const longitude = parseFloat(document.getElementById('form-miskin-lng').value); if (!nama_kk || !latitude || !longitude) { showToast('Silakan isi field yang diperlukan', 'error'); return; } try { const payload = { nama_kk, keterangan, latitude, longitude }; const endpoint = id ? API.MISKIN.UPDATE : API.MISKIN.CREATE; if (id) payload.id = id; const res = await fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const result = await res.json(); if (result.success) { showToast(id ? 'Miskin updated successfully' : 'Miskin created successfully', 'success'); hideModal('modal-miskin'); loadAllData(); } else { showToast(result.message || 'Error saving Miskin', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error saving Miskin', 'error'); } } async function deleteMiskin(id) { if (!confirm('Are you sure you want to delete this Penduduk Miskin?')) return; try { const res = await fetch(API.MISKIN.DELETE, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); const result = await res.json(); if (result.success) { showToast('Miskin deleted successfully', 'success'); loadAllData(); } else { showToast(result.message || 'Error deleting Miskin', 'error'); } } catch (err) { console.error('Error:', err); showToast('Error deleting Miskin', 'error'); } } // ============================================ // Map Initialization & Rendering // ============================================ function initMap() { const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }); const map = L.map('map', { center: [-0.0260, 109.3425], zoom: 13, zoomControl: true, layers: [osm] }); const baseMaps = { "Standard (OSM)": osm, "Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 }) }; L.control.layers(baseMaps).addTo(map); // Initialize Geoman Controls (only if Geoman library is loaded) if (map.pm && typeof map.pm.addControls === 'function') { map.pm.addControls({ position: 'topleft', drawCircle: false, drawCircleMarker: false, drawText: false, editMode: false, dragMode: false, cutPolygon: false, removalMode: false }); } // ALWAYS set mapInstance — regardless of whether Geoman loaded window.appState.mapInstance = map; window.appState.layers = { spbu24: L.layerGroup().addTo(map), spbuTidak: L.layerGroup().addTo(map), jalan: L.layerGroup().addTo(map), parsil: L.layerGroup().addTo(map), ibadah: L.layerGroup().addTo(map), miskin: L.layerGroup().addTo(map) }; // Map click handler — capture coordinates for toolbar point mode map.on('click', function(e) { if (!window._mapClickMode) return; const lat = e.latlng.lat.toFixed(7); const lng = e.latlng.lng.toFixed(7); const mode = window._mapClickMode; window.mapToolCancel(); // reset mode if (mode === 'spbu') { openAddSPBUModal(); setTimeout(function() { var el1 = document.getElementById('form-spbu-lat'); var el2 = document.getElementById('form-spbu-lng'); if (el1) el1.value = lat; if (el2) el2.value = lng; }, 80); } else if (mode === 'ibadah') { openAddIbadahModal(); setTimeout(function() { var el1 = document.getElementById('form-ibadah-lat'); var el2 = document.getElementById('form-ibadah-lng'); if (el1) el1.value = lat; if (el2) el2.value = lng; }, 80); } else if (mode === 'miskin') { openAddMiskinModal(); setTimeout(function() { var el1 = document.getElementById('form-miskin-lat'); var el2 = document.getElementById('form-miskin-lng'); if (el1) el1.value = lat; if (el2) el2.value = lng; }, 80); } }); // Geoman pm:create handler (for line/polygon) if (map.pm) { map.on('pm:create', function(e) { var layer = e.layer; var shape = e.shape; window.mapToolCancel(); if (shape === 'Line' || shape === 'Polyline') { openAddJalanModal(); var gj = layer.toGeoJSON(); var geomEl = document.getElementById('form-jalan-geom'); if (geomEl) geomEl.value = JSON.stringify(gj.geometry); var panjEl = document.getElementById('form-jalan-panjang'); if (panjEl && window.turf) panjEl.value = Math.round(turf.length(gj, { units: 'kilometers' }) * 1000); } else if (shape === 'Polygon' || shape === 'Rectangle') { openAddParsilModal(); var gj = layer.toGeoJSON(); var geomEl = document.getElementById('form-parsil-geom'); if (geomEl) geomEl.value = JSON.stringify(gj.geometry); var luasEl = document.getElementById('form-parsil-luas'); if (luasEl && window.turf) luasEl.value = Math.round(turf.area(gj)); } map.removeLayer(layer); }); } return map; } function renderMapLayers() { const map = window.appState.mapInstance; if (!map) return; // Clear layers Object.values(window.appState.layers).forEach(layer => layer.clearLayers()); // Render SPBU window.appState.data.spbu.forEach(item => { const is24 = item.status === '24jam'; const haloClass = is24 ? 'spbu' : 'spbu-off'; const customIcon = L.divIcon({ className: 'custom-marker', html: `
`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] }); const marker = L.marker([item.latitude, item.longitude], { icon: customIcon }) .bindPopup(`${item.nama}
Status: ${is24 ? '24 Jam' : 'Tidak 24 Jam'}`); const layer = item.status === '24jam' ? window.appState.layers.spbu24 : window.appState.layers.spbuTidak; marker.addTo(layer); }); // Render Jalan window.appState.data.jalan.forEach(item => { try { const geom = JSON.parse(item.geom); if (geom.coordinates) { const colorMap = { 'Nasional': '#ef4444', 'Provinsi': '#eab308', 'Kabupaten': '#10b981' }; L.polyline(geom.coordinates, { color: colorMap[item.status] || '#3b82f6', weight: 3, opacity: 0.7 }).bindPopup(`${item.nama_jalan}
Status: ${item.status}
Panjang: ${item.panjang.toFixed(0)}m`).addTo(window.appState.layers.jalan); } } catch (e) {} }); // Render Parsil window.appState.data.parsil.forEach(item => { try { const geom = JSON.parse(item.geom); if (geom.coordinates) { const colorMap = { 'SHM': '#3b82f6', 'HGB': '#8b5cf6', 'HGU': '#ec4899', 'HP': '#14b8a6' }; L.polygon(geom.coordinates, { color: colorMap[item.status] || '#3b82f6', weight: 2, opacity: 0.6, fillOpacity: 0.4 }).bindPopup(`${item.nama_pemilik}
Status: ${item.status}
Luas: ${item.luas.toFixed(0)}m2`).addTo(window.appState.layers.parsil); } } catch (e) {} }); // Render Ibadah window.appState.data.ibadah.forEach(item => { const ibadahIcon = L.divIcon({ className: 'custom-marker', html: `
🕌
`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] }); const marker = L.marker([item.latitude, item.longitude], { icon: ibadahIcon }) .bindPopup(`${item.nama}
${item.alamat}`); marker.addTo(window.appState.layers.ibadah); }); // Render Miskin window.appState.data.miskin.forEach(item => { const miskinIcon = L.divIcon({ className: 'custom-marker', html: `
🏠
`, iconSize: [28, 28], iconAnchor: [14, 14], popupAnchor: [0, -14] }); const marker = L.marker([item.latitude, item.longitude], { icon: miskinIcon }) .bindPopup(`${item.nama_kk}
${item.keterangan || ''}`); marker.addTo(window.appState.layers.miskin); }); } // ============================================ // Event Listeners & Initialization // ============================================ document.addEventListener('DOMContentLoaded', () => { // Initialize map initMap(); // Sidebar navigation document.querySelectorAll('[data-panel]').forEach(item => { item.addEventListener('click', () => { const panelId = item.dataset.panel; switchPanel(panelId); }); }); // Sidebar toggle document.getElementById('sidebar-toggle').addEventListener('click', () => { document.querySelector('.sidebar').classList.toggle('collapsed'); }); // Modal close buttons document.querySelectorAll('[data-modal]').forEach(btn => { btn.addEventListener('click', () => { const modalId = btn.dataset.modal; hideModal(modalId); }); }); // Modal overlay close document.querySelectorAll('.modal-overlay').forEach(overlay => { overlay.addEventListener('click', (e) => { if (e.target === overlay) { hideModal(overlay.id); } }); }); // Add buttons document.getElementById('btn-add-spbu').addEventListener('click', openAddSPBUModal); document.getElementById('btn-add-jalan').addEventListener('click', openAddJalanModal); document.getElementById('btn-add-parsil').addEventListener('click', openAddParsilModal); document.getElementById('btn-add-ibadah').addEventListener('click', openAddIbadahModal); document.getElementById('btn-add-miskin').addEventListener('click', openAddMiskinModal); // Save buttons document.getElementById('btn-save-spbu').addEventListener('click', saveSPBU); document.getElementById('btn-save-jalan').addEventListener('click', saveJalan); document.getElementById('btn-save-parsil').addEventListener('click', saveParsil); document.getElementById('btn-save-ibadah').addEventListener('click', saveIbadah); document.getElementById('btn-save-miskin').addEventListener('click', saveMiskin); // Search and filter - SPBU document.getElementById('search-spbu').addEventListener('input', (e) => { renderSPBUTable('all', e.target.value); }); document.querySelectorAll('#panel-spbu .filter-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#panel-spbu .filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const search = document.getElementById('search-spbu').value; renderSPBUTable(btn.dataset.filter, search); }); }); // Search and filter - JALAN document.getElementById('search-jalan').addEventListener('input', (e) => { renderJalanTable('all', e.target.value); }); document.querySelectorAll('#panel-jalan .filter-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#panel-jalan .filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const search = document.getElementById('search-jalan').value; renderJalanTable(btn.dataset.filter, search); }); }); // Search and filter - PARSIL document.getElementById('search-parsil').addEventListener('input', (e) => { renderParsilTable('all', e.target.value); }); document.querySelectorAll('#panel-parsil .filter-btn').forEach(btn => { btn.addEventListener('click', () => { document.querySelectorAll('#panel-parsil .filter-btn').forEach(b => b.classList.remove('active')); btn.classList.add('active'); const search = document.getElementById('search-parsil').value; renderParsilTable(btn.dataset.filter, search); }); }); // Search - IBADAH document.getElementById('search-ibadah').addEventListener('input', (e) => { renderIbadahTable(e.target.value); }); // Search - MISKIN document.getElementById('search-miskin').addEventListener('input', (e) => { renderMiskinTable(e.target.value); }); // Load initial data loadAllData(); showToast('Welcome to WebGIS SPBU System', 'info'); });