1st commit: Arsitektur WebGis tersentralisasi dan dokumentasi lengkap
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Mengabaikan folder lingkungan kerja masa depan
|
||||||
|
Poverty_Mapping/
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
// ===================================================
|
||||||
|
// app.js - ARSITEKTUR WEBGIS V2 (KODE BERSIH & TERPUSAT)
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
isMarkerMode: false, isRoadMode: false, isLandMode: false,
|
||||||
|
tempRoadCoords: [], tempRoadLayer: null,
|
||||||
|
tempLandCoords: [], tempLandLayer: null,
|
||||||
|
filterSPBU: 'Semua',
|
||||||
|
filterJalan: ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'],
|
||||||
|
filterTanah: ['SHM', 'HGB', 'HGU', 'HP']
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataStore = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
const mapLayers = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
|
||||||
|
const latPontianak = -0.0227; const lngPontianak = 109.3366; const levelZoom = 13;
|
||||||
|
|
||||||
|
const savedLat = localStorage.getItem('mapCenterLat');
|
||||||
|
const savedLng = localStorage.getItem('mapCenterLng');
|
||||||
|
const savedZoom = localStorage.getItem('mapZoom');
|
||||||
|
|
||||||
|
const initialLat = savedLat ? parseFloat(savedLat) : latPontianak;
|
||||||
|
const initialLng = savedLng ? parseFloat(savedLng) : lngPontianak;
|
||||||
|
const initialZoom = savedZoom ? parseInt(savedZoom) : levelZoom;
|
||||||
|
|
||||||
|
const map = L.map('map', { zoomControl: false }).setView([initialLat, initialLng], initialZoom);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);
|
||||||
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||||
|
|
||||||
|
// Menyimpan posisi peta saat bergeser atau zoom
|
||||||
|
map.on('moveend', function() {
|
||||||
|
const center = map.getCenter();
|
||||||
|
localStorage.setItem('mapCenterLat', center.lat);
|
||||||
|
localStorage.setItem('mapCenterLng', center.lng);
|
||||||
|
});
|
||||||
|
map.on('zoomend', function() {
|
||||||
|
localStorage.setItem('mapZoom', map.getZoom());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// FITUR BARU: REVERSE GEOCODING (OSM NOMINATIM)
|
||||||
|
// ====================================================
|
||||||
|
async function dapatkanAlamat(lat, lng) {
|
||||||
|
try {
|
||||||
|
// Memanggil API Geocoding Gratis dari OpenStreetMap
|
||||||
|
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("Gagal mengambil alamat");
|
||||||
|
const data = await response.json();
|
||||||
|
return data.display_name || "Alamat detail tidak ditemukan di area ini";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Geocoding Error:", error);
|
||||||
|
return "Gagal memuat alamat. Periksa koneksi internet.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- KOMPONEN UI & FILTERING ---
|
||||||
|
window.toggleSidebar = () => {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||||
|
btn.innerHTML = isCollapsed ? '❯' : '❮';
|
||||||
|
|
||||||
|
// Simpan status sidebar ke memori browser
|
||||||
|
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||||
|
|
||||||
|
setTimeout(() => map.invalidateSize(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mengembalikan status sidebar saat halaman dimuat
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
if(sidebar && btn) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
btn.innerHTML = '❯';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Khusus 001: Sembunyikan seluruh filter bar (layer jalan/tanah & filter SPBU)
|
||||||
|
const filterBar = document.getElementById('filter-bar');
|
||||||
|
if (filterBar) filterBar.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.bukaModalFilterLayer = () => document.getElementById('modal-filter-layer').style.display = 'flex';
|
||||||
|
window.bukaModalFilterSPBU = () => document.getElementById('modal-filter-spbu').style.display = 'flex';
|
||||||
|
|
||||||
|
const RightToolbar = L.Control.extend({
|
||||||
|
options: { position: 'topright' },
|
||||||
|
onAdd: function() {
|
||||||
|
const container = L.DomUtil.create('div', 'custom-leaflet-toolbar');
|
||||||
|
container.innerHTML = `
|
||||||
|
<button class="tool-btn" id="btn-tool-marker" title="Mode Marker" onclick="toggleMarkerMode()">📍</button>
|
||||||
|
`;
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addControl(new RightToolbar());
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- LOGIKA ALAT PETA ---
|
||||||
|
window.toggleMarkerMode = () => {
|
||||||
|
if (state.isRoadMode) toggleRoadMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isMarkerMode = !state.isMarkerMode;
|
||||||
|
const btn = document.getElementById('btn-tool-marker');
|
||||||
|
if (state.isMarkerMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.enable());
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = '';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.disable());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleRoadMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isRoadMode = !state.isRoadMode;
|
||||||
|
const btn = document.getElementById('btn-tool-jalan');
|
||||||
|
if (state.isRoadMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarJalan();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleLandMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isRoadMode) toggleRoadMode();
|
||||||
|
state.isLandMode = !state.isLandMode;
|
||||||
|
const btn = document.getElementById('btn-tool-tanah');
|
||||||
|
if (state.isLandMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarTanah();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Atur state loading untuk alamat
|
||||||
|
const alamatInput = document.getElementById('spbu-alamat');
|
||||||
|
alamatInput.value = "Sedang melacak alamat dari satelit...";
|
||||||
|
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" id="btn-eksekusi-spbu" onclick="eksekusiSimpanBaru()" disabled>Tunggu Alamat...</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Batal</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
|
||||||
|
// KUNCI: Tarik alamat secara asinkron lalu buka kunci tombol simpan
|
||||||
|
const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng);
|
||||||
|
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 btnSelesai = document.getElementById('btn-selesai-jalan');
|
||||||
|
if (btnSelesai && state.tempRoadCoords.length >= 2) btnSelesai.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 btnSelesai = document.getElementById('btn-selesai-tanah');
|
||||||
|
if (btnSelesai && state.tempLandCoords.length >= 3) btnSelesai.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- UTILITAS & KOSMETIK MODAL ---
|
||||||
|
function triggerToastNotification(pesan = "Aksi berhasil dieksekusi!") {
|
||||||
|
const toast = document.getElementById('toast-notification');
|
||||||
|
toast.innerText = pesan; toast.classList.add('show');
|
||||||
|
setTimeout(() => { toast.classList.remove('show'); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupDropdownColor = (idSelect, colorMap) => {
|
||||||
|
const select = document.getElementById(idSelect);
|
||||||
|
if(select) {
|
||||||
|
const update = () => { select.style.borderLeftColor = colorMap[select.value] || '#CBD5E0'; };
|
||||||
|
select.addEventListener('change', update);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setupDropdownColor('jalan-status', { 'Jalan Nasional': '#E53E3E', 'Jalan Provinsi': '#3182CE', 'Jalan Kabupaten': '#38A169' });
|
||||||
|
setupDropdownColor('tanah-status', { 'SHM': '#38A169', 'HGB': '#DD6B20', 'HGU': '#3182CE', 'HP': '#718096' });
|
||||||
|
|
||||||
|
function bukaModalSpbu() { document.getElementById('modal-spbu').style.display = 'flex'; }
|
||||||
|
window.tutupModalSpbu = function() {
|
||||||
|
document.getElementById('modal-spbu').style.display = 'none';
|
||||||
|
document.getElementById('spbu-nama').readOnly = false; document.getElementById('spbu-telp').readOnly = false; document.getElementById('spbu-status').disabled = false;
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
document.getElementById('spbu-alamat').value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleMarkerClick = function(id) {
|
||||||
|
if (state.isMarkerMode || state.isRoadMode || state.isLandMode) return;
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Detail & Kelola SPBU";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdate(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapus(${id})">Hapus Marker</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Tutup</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-nama').readOnly = true;
|
||||||
|
document.getElementById('spbu-telp').readOnly = true;
|
||||||
|
document.getElementById('spbu-status').disabled = true;
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Informasi SPBU (Read-Only)";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `<button class="btn btn-cancel" style="width:100%" onclick="tutupModalSpbu()">Kembali</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... JALAN & TANAH UI FUNCTIONS (TETAP SAMA SEPERTI SEBELUMNYA) ...
|
||||||
|
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);
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Simpan Data Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanJalan()">Simpan Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarJalan()">Batal</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex'; document.getElementById('btn-selesai-jalan').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarJalan = function() {
|
||||||
|
state.tempRoadCoords = []; if (state.tempRoadLayer) map.removeLayer(state.tempRoadLayer); state.tempRoadLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-jalan'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('jalan-nama').readOnly = false; document.getElementById('jalan-status').disabled = false;
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Detail & Kelola Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateJalan(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusJalan(${id})">Hapus Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-jalan').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selesaiGambarTanah = function() {
|
||||||
|
if (state.tempLandCoords.length < 3) return;
|
||||||
|
let area = 0, R = 6378137;
|
||||||
|
const ll = state.tempLandCoords.map(c => L.latLng(c[0], c[1]));
|
||||||
|
for (let i = 0; i < ll.length; i++) {
|
||||||
|
let p1 = ll[i], p2 = ll[(i + 1) % ll.length];
|
||||||
|
area += (p2.lng - p1.lng) * Math.PI / 180 * (2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180));
|
||||||
|
}
|
||||||
|
document.getElementById('tanah-id').value = ""; document.getElementById('tanah-nama').value = ""; document.getElementById('tanah-status').value = "SHM";
|
||||||
|
document.getElementById('tanah-status').dispatchEvent(new Event('change'));
|
||||||
|
document.getElementById('tanah-luas').value = Math.round(Math.abs(area * R * R / 2.0)); document.getElementById('tanah-koordinat').value = JSON.stringify(state.tempLandCoords);
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Simpan Data Parsil Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanTanah()">Simpan Area</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarTanah()">Batal</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex'; document.getElementById('btn-selesai-tanah').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarTanah = function() {
|
||||||
|
state.tempLandCoords = []; if (state.tempLandLayer) map.removeLayer(state.tempLandLayer); state.tempLandLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-tanah'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Detail & Kelola Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateTanah(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusTanah(${id})">Hapus Tanah</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-tanah').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// DECOUPLED RENDERING ENGINE (LOGIKA PENGGAMBARAN)
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
function renderSPBU() {
|
||||||
|
const cardContainer = document.getElementById('card-container');
|
||||||
|
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 isBuka = d.status_operasional === 'Buka 24 Jam';
|
||||||
|
const customIcon = L.divIcon({
|
||||||
|
className: 'custom-div-icon',
|
||||||
|
html: `<div class="marker-wrapper"><div class="marker-pin" style="background-color: ${isBuka ? '#28a745' : '#dc3545'};"></div><i class="marker-dot"></i></div>`,
|
||||||
|
iconSize: [30, 42], iconAnchor: [15, 42]
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([d.latitude, d.longitude], { icon: customIcon, draggable: state.isMarkerMode }).addTo(map);
|
||||||
|
marker.bindTooltip(`<b>${d.nama_spbu}</b>`, { direction: 'top', offset: [0, -35], className: 'spbu-tooltip' });
|
||||||
|
marker.on('click', () => handleMarkerClick(d.id));
|
||||||
|
|
||||||
|
// KUNCI: Reverse Geocoding saat Drag-and-Drop
|
||||||
|
marker.on('dragend', async function(e) {
|
||||||
|
const pos = e.target.getLatLng();
|
||||||
|
triggerToastNotification("⏳ Menganalisis alamat baru...");
|
||||||
|
|
||||||
|
// Tunggu alamat baru dari satelit sebelum dikirim ke database
|
||||||
|
const alamatBaru = await dapatkanAlamat(pos.lat, pos.lng);
|
||||||
|
|
||||||
|
fetch('otak_spbu/api_update_coords.php', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: d.id, lat: pos.lat, lng: pos.lng, alamat: alamatBaru })
|
||||||
|
}).then(res => res.ok ? res.json() : Promise.reject(res)).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(); // Gambar ulang untuk update text di Sidebar
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Gagal menyimpan posisi."); e.target.setLatLng([dataStore.spbu[d.id].latitude, dataStore.spbu[d.id].longitude]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mapLayers.spbu[d.id] = marker;
|
||||||
|
|
||||||
|
// Menampilkan Alamat yang terpotong rapi di Sidebar Card
|
||||||
|
const statusClass = isBuka ? 'status-buka' : 'status-tutup';
|
||||||
|
const alamatRingkas = d.alamat ? (d.alamat.substring(0, 45) + '...') : 'Alamat belum direkam';
|
||||||
|
cardContainer.innerHTML += `
|
||||||
|
<div class="spbu-card ${statusClass}" onclick="lihatSpbuDariCard(${d.id})">
|
||||||
|
<h4>${d.nama_spbu}</h4>
|
||||||
|
<p style="font-weight:bold; margin-bottom:4px;">${d.status_operasional} | ${d.no_telp || '-'}</p>
|
||||||
|
<p style="font-size:0.75rem; color:#718096; line-height:1.2;">${alamatRingkas}</p>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJalan() {
|
||||||
|
Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m));
|
||||||
|
mapLayers.jalan = {};
|
||||||
|
|
||||||
|
Object.values(dataStore.jalan).forEach(d => {
|
||||||
|
if (!state.filterJalan.includes(d.status_jalan)) return;
|
||||||
|
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(JSON.parse(d.koordinat), { color: lineColor, weight: lineWeight, opacity: 0.8 }).addTo(map);
|
||||||
|
polyline.bindTooltip(`<b>${d.nama_jalan}</b><br>${d.jarak_meter} meter`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polyline.on('mouseover', function(e) { this.setStyle({ weight: lineWeight + 3, opacity: 1 }); });
|
||||||
|
polyline.on('mouseout', function(e) { this.setStyle({ weight: lineWeight, opacity: 0.8 }); });
|
||||||
|
polyline.on('click', () => handleRoadClick(d.id));
|
||||||
|
mapLayers.jalan[d.id] = polyline;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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(JSON.parse(d.koordinat), { color: fillColor, fillColor: fillColor, fillOpacity: 0.4, weight: 2 }).addTo(map);
|
||||||
|
polygon.bindTooltip(`<b>${d.nama_pemilik}</b><br>${d.luas_meter} m² (${d.status_kepemilikan})`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polygon.on('mouseover', function(e) { this.setStyle({ fillOpacity: 0.7, weight: 4 }); });
|
||||||
|
polygon.on('mouseout', function(e) { this.setStyle({ fillOpacity: 0.4, weight: 2 }); });
|
||||||
|
polygon.on('click', () => handleLandClick(d.id));
|
||||||
|
mapLayers.tanah[d.id] = polygon;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// API FETCH MANAGER (KOMUNIKASI DATABASE)
|
||||||
|
// ====================================================
|
||||||
|
const safeFetch = async (url, payload = null) => {
|
||||||
|
const options = payload ? { method: 'POST', body: JSON.stringify(payload) } : {};
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SPBU ---
|
||||||
|
window.muatDataSPBU = () => {
|
||||||
|
return safeFetch('../otak_spbu/api_tampil.php').then(data => {
|
||||||
|
dataStore.spbu = {}; data.forEach(d => { dataStore.spbu[d.id] = d; }); renderSPBU();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.eksekusiSimpanBaru = () => {
|
||||||
|
// Alamat ikut ditangkap dan dikirim
|
||||||
|
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').value,
|
||||||
|
lat: document.getElementById('spbu-lat').value,
|
||||||
|
lng: document.getElementById('spbu-lng').value
|
||||||
|
};
|
||||||
|
safeFetch('../otak_spbu/api_simpan.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU ditambahkan!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU diperbarui!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapus = (id) => {
|
||||||
|
if(!confirm("Hapus SPBU ini?")) return;
|
||||||
|
safeFetch('../otak_spbu/api_hapus.php', { id: id }).then(() => { tutupModalSpbu(); triggerToastNotification("🗑️ SPBU dihapus."); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- JALAN ---
|
||||||
|
window.muatDataJalan = () => {
|
||||||
|
return safeFetch('../otak_jalan/api_tampil.php').then(data => {
|
||||||
|
dataStore.jalan = {}; data.forEach(d => { dataStore.jalan[d.id] = d; }); renderJalan();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { batalGambarJalan(); triggerToastNotification("✅ Jalan ditambahkan!"); muatDataJalan(); }).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.php', payload).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("✅ Jalan diperbarui!"); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusJalan = (id) => {
|
||||||
|
if(!confirm("Hapus jalur ini?")) return;
|
||||||
|
safeFetch('../otak_jalan/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("🗑️ Jalan dihapus."); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TANAH ---
|
||||||
|
window.muatDataTanah = () => {
|
||||||
|
return safeFetch('../otak_parsil/api_tampil.php').then(data => {
|
||||||
|
dataStore.tanah = {}; data.forEach(d => { dataStore.tanah[d.id] = d; }); renderTanah();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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_parsil/api_simpan.php', payload).then(() => { batalGambarTanah(); triggerToastNotification("✅ Tanah ditambahkan!"); muatDataTanah(); }).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_parsil/api_update.php', payload).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("✅ Tanah diperbarui!"); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusTanah = (id) => {
|
||||||
|
if(!confirm("Hapus parsil ini?")) return;
|
||||||
|
safeFetch('../otak_parsil/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("🗑️ Tanah dihapus."); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// LIFECYCLE AWAL (PROMISE ALL MANAGEMENT)
|
||||||
|
// ====================================================
|
||||||
|
setTimeout(() => {
|
||||||
|
map.invalidateSize();
|
||||||
|
console.log("Menghubungkan ke Database untuk penarikan data masal...");
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
muatDataSPBU()
|
||||||
|
]).then(() => {
|
||||||
|
console.log("✅ Sistem WebGIS V2 Siap dan Data Tersinkronisasi Penuh.");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Kegagalan Fatal Jaringan:", err);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// RENDER LAYER GEOJSON (BATAS WILAYAH)
|
||||||
|
// ====================================================
|
||||||
|
fetch('../components/Admin_Kecamatan.json')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error("File GeoJSON tidak ditemukan.");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
L.geoJSON(data, {
|
||||||
|
style: function(feature) {
|
||||||
|
return {
|
||||||
|
color: "#000000", // Warna garis batas (Hitam)
|
||||||
|
weight: 2, // Ketebalan garis
|
||||||
|
opacity: 0.7,
|
||||||
|
fillColor: "#0056b3", // Warna isi wilayah
|
||||||
|
fillOpacity: 0.1 // Transparansi agar peta di bawahnya tetap terlihat
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onEachFeature: function(feature, layer) {
|
||||||
|
if (feature.properties && feature.properties.KECAMATAN) {
|
||||||
|
layer.bindPopup(`<b>Wilayah:</b> ${feature.properties.KECAMATAN}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).addTo(map);
|
||||||
|
console.log("Layer GeoJSON Batas Wilayah berhasil dimuat.");
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Gagal memuat layer wilayah:", error));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE spbu_markers (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
nama_spbu VARCHAR(100) NOT NULL,
|
||||||
|
no_wa VARCHAR(20) NOT NULL,
|
||||||
|
status_operasional ENUM('Buka 24 Jam', 'Tidak 24 Jam') NOT NULL,
|
||||||
|
latitude DECIMAL(10, 8) NOT NULL,
|
||||||
|
longitude DECIMAL(11, 8) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Pontianak V1</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="sidebar-container">
|
||||||
|
<div id="sidebar-header">
|
||||||
|
WebGis-Pontianak
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="card-container" class="card-list-area">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button id="toggle-sidebar" onclick="toggleSidebar()">❮</button>
|
||||||
|
</div>
|
||||||
|
<?php include '../components/modals.php'; ?>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
|
||||||
|
<script src="app.js?v=<?= time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+534
@@ -0,0 +1,534 @@
|
|||||||
|
// ===================================================
|
||||||
|
// app.js - ARSITEKTUR WEBGIS V2 (KODE BERSIH & TERPUSAT)
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
isMarkerMode: false, isRoadMode: false, isLandMode: false,
|
||||||
|
tempRoadCoords: [], tempRoadLayer: null,
|
||||||
|
tempLandCoords: [], tempLandLayer: null
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataStore = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
const mapLayers = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
|
||||||
|
const latPontianak = -0.0227; const lngPontianak = 109.3366; const levelZoom = 13;
|
||||||
|
|
||||||
|
const savedLat = localStorage.getItem('mapCenterLat');
|
||||||
|
const savedLng = localStorage.getItem('mapCenterLng');
|
||||||
|
const savedZoom = localStorage.getItem('mapZoom');
|
||||||
|
|
||||||
|
const initialLat = savedLat ? parseFloat(savedLat) : latPontianak;
|
||||||
|
const initialLng = savedLng ? parseFloat(savedLng) : lngPontianak;
|
||||||
|
const initialZoom = savedZoom ? parseInt(savedZoom) : levelZoom;
|
||||||
|
|
||||||
|
const map = L.map('map', { zoomControl: false }).setView([initialLat, initialLng], initialZoom);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);
|
||||||
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||||
|
|
||||||
|
// Menyimpan posisi peta saat bergeser atau zoom
|
||||||
|
map.on('moveend', function() {
|
||||||
|
const center = map.getCenter();
|
||||||
|
localStorage.setItem('mapCenterLat', center.lat);
|
||||||
|
localStorage.setItem('mapCenterLng', center.lng);
|
||||||
|
});
|
||||||
|
map.on('zoomend', function() {
|
||||||
|
localStorage.setItem('mapZoom', map.getZoom());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// FITUR BARU: REVERSE GEOCODING (OSM NOMINATIM)
|
||||||
|
// ====================================================
|
||||||
|
async function dapatkanAlamat(lat, lng) {
|
||||||
|
try {
|
||||||
|
// Memanggil API Geocoding Gratis dari OpenStreetMap
|
||||||
|
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("Gagal mengambil alamat");
|
||||||
|
const data = await response.json();
|
||||||
|
return data.display_name || "Alamat detail tidak ditemukan di area ini";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Geocoding Error:", error);
|
||||||
|
return "Gagal memuat alamat. Periksa koneksi internet.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- KOMPONEN UI & FILTERING ---
|
||||||
|
window.toggleSidebar = () => {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||||
|
btn.innerHTML = isCollapsed ? '❯' : '❮';
|
||||||
|
|
||||||
|
// Simpan status sidebar ke memori browser
|
||||||
|
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||||
|
|
||||||
|
setTimeout(() => map.invalidateSize(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mengembalikan status sidebar saat halaman dimuat
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
if(sidebar && btn) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
btn.innerHTML = '❯';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter dihapus dari 002
|
||||||
|
|
||||||
|
const RightToolbar = L.Control.extend({
|
||||||
|
options: { position: 'topright' },
|
||||||
|
onAdd: function() {
|
||||||
|
const container = L.DomUtil.create('div', 'custom-leaflet-toolbar');
|
||||||
|
container.innerHTML = `
|
||||||
|
<button class="tool-btn" id="btn-tool-marker" title="Mode Marker" onclick="toggleMarkerMode()">📍</button>
|
||||||
|
<button class="tool-btn" id="btn-tool-jalan" title="Mode Jalan" onclick="toggleRoadMode()">🛣️</button>
|
||||||
|
<button class="tool-btn" id="btn-tool-tanah" title="Mode Tanah" onclick="toggleLandMode()">🟩</button>
|
||||||
|
`;
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addControl(new RightToolbar());
|
||||||
|
|
||||||
|
// Logika filter dihapus dari 002
|
||||||
|
|
||||||
|
|
||||||
|
// --- LOGIKA ALAT PETA ---
|
||||||
|
window.toggleMarkerMode = () => {
|
||||||
|
if (state.isRoadMode) toggleRoadMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isMarkerMode = !state.isMarkerMode;
|
||||||
|
const btn = document.getElementById('btn-tool-marker');
|
||||||
|
if (state.isMarkerMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.enable());
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = '';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.disable());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleRoadMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isRoadMode = !state.isRoadMode;
|
||||||
|
const btn = document.getElementById('btn-tool-jalan');
|
||||||
|
if (state.isRoadMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarJalan();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleLandMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isRoadMode) toggleRoadMode();
|
||||||
|
state.isLandMode = !state.isLandMode;
|
||||||
|
const btn = document.getElementById('btn-tool-tanah');
|
||||||
|
if (state.isLandMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarTanah();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Atur state loading untuk alamat
|
||||||
|
const alamatInput = document.getElementById('spbu-alamat');
|
||||||
|
alamatInput.value = "Sedang melacak alamat dari satelit...";
|
||||||
|
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" id="btn-eksekusi-spbu" onclick="eksekusiSimpanBaru()" disabled>Tunggu Alamat...</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Batal</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
|
||||||
|
// KUNCI: Tarik alamat secara asinkron lalu buka kunci tombol simpan
|
||||||
|
const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng);
|
||||||
|
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 btnSelesai = document.getElementById('btn-selesai-jalan');
|
||||||
|
if (btnSelesai && state.tempRoadCoords.length >= 2) btnSelesai.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 btnSelesai = document.getElementById('btn-selesai-tanah');
|
||||||
|
if (btnSelesai && state.tempLandCoords.length >= 3) btnSelesai.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- UTILITAS & KOSMETIK MODAL ---
|
||||||
|
function triggerToastNotification(pesan = "Aksi berhasil dieksekusi!") {
|
||||||
|
const toast = document.getElementById('toast-notification');
|
||||||
|
toast.innerText = pesan; toast.classList.add('show');
|
||||||
|
setTimeout(() => { toast.classList.remove('show'); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupDropdownColor = (idSelect, colorMap) => {
|
||||||
|
const select = document.getElementById(idSelect);
|
||||||
|
if(select) {
|
||||||
|
const update = () => { select.style.borderLeftColor = colorMap[select.value] || '#CBD5E0'; };
|
||||||
|
select.addEventListener('change', update);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setupDropdownColor('jalan-status', { 'Jalan Nasional': '#E53E3E', 'Jalan Provinsi': '#3182CE', 'Jalan Kabupaten': '#38A169' });
|
||||||
|
setupDropdownColor('tanah-status', { 'SHM': '#38A169', 'HGB': '#DD6B20', 'HGU': '#3182CE', 'HP': '#718096' });
|
||||||
|
|
||||||
|
function bukaModalSpbu() { document.getElementById('modal-spbu').style.display = 'flex'; }
|
||||||
|
window.tutupModalSpbu = function() {
|
||||||
|
document.getElementById('modal-spbu').style.display = 'none';
|
||||||
|
document.getElementById('spbu-nama').readOnly = false; document.getElementById('spbu-telp').readOnly = false; document.getElementById('spbu-status').disabled = false;
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
document.getElementById('spbu-alamat').value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleMarkerClick = function(id) {
|
||||||
|
if (state.isMarkerMode || state.isRoadMode || state.isLandMode) return;
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Detail & Kelola SPBU";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdate(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapus(${id})">Hapus Marker</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Tutup</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-nama').readOnly = true;
|
||||||
|
document.getElementById('spbu-telp').readOnly = true;
|
||||||
|
document.getElementById('spbu-status').disabled = true;
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Informasi SPBU (Read-Only)";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `<button class="btn btn-cancel" style="width:100%" onclick="tutupModalSpbu()">Kembali</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... JALAN & TANAH UI FUNCTIONS (TETAP SAMA SEPERTI SEBELUMNYA) ...
|
||||||
|
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);
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Simpan Data Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanJalan()">Simpan Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarJalan()">Batal</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex'; document.getElementById('btn-selesai-jalan').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarJalan = function() {
|
||||||
|
state.tempRoadCoords = []; if (state.tempRoadLayer) map.removeLayer(state.tempRoadLayer); state.tempRoadLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-jalan'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('jalan-nama').readOnly = false; document.getElementById('jalan-status').disabled = false;
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Detail & Kelola Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateJalan(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusJalan(${id})">Hapus Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-jalan').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selesaiGambarTanah = function() {
|
||||||
|
if (state.tempLandCoords.length < 3) return;
|
||||||
|
let area = 0, R = 6378137;
|
||||||
|
const ll = state.tempLandCoords.map(c => L.latLng(c[0], c[1]));
|
||||||
|
for (let i = 0; i < ll.length; i++) {
|
||||||
|
let p1 = ll[i], p2 = ll[(i + 1) % ll.length];
|
||||||
|
area += (p2.lng - p1.lng) * Math.PI / 180 * (2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180));
|
||||||
|
}
|
||||||
|
document.getElementById('tanah-id').value = ""; document.getElementById('tanah-nama').value = ""; document.getElementById('tanah-status').value = "SHM";
|
||||||
|
document.getElementById('tanah-status').dispatchEvent(new Event('change'));
|
||||||
|
document.getElementById('tanah-luas').value = Math.round(Math.abs(area * R * R / 2.0)); document.getElementById('tanah-koordinat').value = JSON.stringify(state.tempLandCoords);
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Simpan Data Parsil Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanTanah()">Simpan Area</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarTanah()">Batal</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex'; document.getElementById('btn-selesai-tanah').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarTanah = function() {
|
||||||
|
state.tempLandCoords = []; if (state.tempLandLayer) map.removeLayer(state.tempLandLayer); state.tempLandLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-tanah'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Detail & Kelola Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateTanah(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusTanah(${id})">Hapus Tanah</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-tanah').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// DECOUPLED RENDERING ENGINE (LOGIKA PENGGAMBARAN)
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
function renderSPBU() {
|
||||||
|
const cardContainer = document.getElementById('card-container');
|
||||||
|
cardContainer.innerHTML = '';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => map.removeLayer(m));
|
||||||
|
mapLayers.spbu = {};
|
||||||
|
|
||||||
|
Object.values(dataStore.spbu).forEach(d => {
|
||||||
|
const isBuka = d.status_operasional === 'Buka 24 Jam';
|
||||||
|
const customIcon = L.divIcon({
|
||||||
|
className: 'custom-div-icon',
|
||||||
|
html: `<div class="marker-wrapper"><div class="marker-pin" style="background-color: ${isBuka ? '#28a745' : '#dc3545'};"></div><i class="marker-dot"></i></div>`,
|
||||||
|
iconSize: [30, 42], iconAnchor: [15, 42]
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([d.latitude, d.longitude], { icon: customIcon, draggable: state.isMarkerMode }).addTo(map);
|
||||||
|
marker.bindTooltip(`<b>${d.nama_spbu}</b>`, { direction: 'top', offset: [0, -35], className: 'spbu-tooltip' });
|
||||||
|
marker.on('click', () => handleMarkerClick(d.id));
|
||||||
|
|
||||||
|
// KUNCI: Reverse Geocoding saat Drag-and-Drop
|
||||||
|
marker.on('dragend', async function(e) {
|
||||||
|
const pos = e.target.getLatLng();
|
||||||
|
triggerToastNotification("⏳ Menganalisis alamat baru...");
|
||||||
|
|
||||||
|
// Tunggu alamat baru dari satelit sebelum dikirim ke database
|
||||||
|
const alamatBaru = await dapatkanAlamat(pos.lat, pos.lng);
|
||||||
|
|
||||||
|
fetch('otak_spbu/api_update_coords.php', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: d.id, lat: pos.lat, lng: pos.lng, alamat: alamatBaru })
|
||||||
|
}).then(res => res.ok ? res.json() : Promise.reject(res)).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(); // Gambar ulang untuk update text di Sidebar
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Gagal menyimpan posisi."); e.target.setLatLng([dataStore.spbu[d.id].latitude, dataStore.spbu[d.id].longitude]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mapLayers.spbu[d.id] = marker;
|
||||||
|
|
||||||
|
// Menampilkan Alamat yang terpotong rapi di Sidebar Card
|
||||||
|
const statusClass = isBuka ? 'status-buka' : 'status-tutup';
|
||||||
|
const alamatRingkas = d.alamat ? (d.alamat.substring(0, 45) + '...') : 'Alamat belum direkam';
|
||||||
|
cardContainer.innerHTML += `
|
||||||
|
<div class="spbu-card ${statusClass}" onclick="lihatSpbuDariCard(${d.id})">
|
||||||
|
<h4>${d.nama_spbu}</h4>
|
||||||
|
<p style="font-weight:bold; margin-bottom:4px;">${d.status_operasional} | ${d.no_telp || '-'}</p>
|
||||||
|
<p style="font-size:0.75rem; color:#718096; line-height:1.2;">${alamatRingkas}</p>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJalan() {
|
||||||
|
Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m));
|
||||||
|
mapLayers.jalan = {};
|
||||||
|
|
||||||
|
Object.values(dataStore.jalan).forEach(d => {
|
||||||
|
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(JSON.parse(d.koordinat), { color: lineColor, weight: lineWeight, opacity: 0.8 }).addTo(map);
|
||||||
|
polyline.bindTooltip(`<b>${d.nama_jalan}</b><br>${d.jarak_meter} meter`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polyline.on('mouseover', function(e) { this.setStyle({ weight: lineWeight + 3, opacity: 1 }); });
|
||||||
|
polyline.on('mouseout', function(e) { this.setStyle({ weight: lineWeight, opacity: 0.8 }); });
|
||||||
|
polyline.on('click', () => handleRoadClick(d.id));
|
||||||
|
mapLayers.jalan[d.id] = polyline;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTanah() {
|
||||||
|
Object.values(mapLayers.tanah).forEach(m => map.removeLayer(m));
|
||||||
|
mapLayers.tanah = {};
|
||||||
|
|
||||||
|
Object.values(dataStore.tanah).forEach(d => {
|
||||||
|
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(JSON.parse(d.koordinat), { color: fillColor, fillColor: fillColor, fillOpacity: 0.4, weight: 2 }).addTo(map);
|
||||||
|
polygon.bindTooltip(`<b>${d.nama_pemilik}</b><br>${d.luas_meter} m² (${d.status_kepemilikan})`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polygon.on('mouseover', function(e) { this.setStyle({ fillOpacity: 0.7, weight: 4 }); });
|
||||||
|
polygon.on('mouseout', function(e) { this.setStyle({ fillOpacity: 0.4, weight: 2 }); });
|
||||||
|
polygon.on('click', () => handleLandClick(d.id));
|
||||||
|
mapLayers.tanah[d.id] = polygon;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// API FETCH MANAGER (KOMUNIKASI DATABASE)
|
||||||
|
// ====================================================
|
||||||
|
const safeFetch = async (url, payload = null) => {
|
||||||
|
const options = payload ? { method: 'POST', body: JSON.stringify(payload) } : {};
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SPBU ---
|
||||||
|
window.muatDataSPBU = () => {
|
||||||
|
return safeFetch('../otak_spbu/api_tampil.php').then(data => {
|
||||||
|
dataStore.spbu = {}; data.forEach(d => { dataStore.spbu[d.id] = d; }); renderSPBU();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.eksekusiSimpanBaru = () => {
|
||||||
|
// Alamat ikut ditangkap dan dikirim
|
||||||
|
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').value,
|
||||||
|
lat: document.getElementById('spbu-lat').value,
|
||||||
|
lng: document.getElementById('spbu-lng').value
|
||||||
|
};
|
||||||
|
safeFetch('../otak_spbu/api_simpan.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU ditambahkan!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU diperbarui!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapus = (id) => {
|
||||||
|
if(!confirm("Hapus SPBU ini?")) return;
|
||||||
|
safeFetch('../otak_spbu/api_hapus.php', { id: id }).then(() => { tutupModalSpbu(); triggerToastNotification("🗑️ SPBU dihapus."); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- JALAN ---
|
||||||
|
window.muatDataJalan = () => {
|
||||||
|
return safeFetch('../otak_jalan/api_tampil.php').then(data => {
|
||||||
|
dataStore.jalan = {}; data.forEach(d => { dataStore.jalan[d.id] = d; }); renderJalan();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { batalGambarJalan(); triggerToastNotification("✅ Jalan ditambahkan!"); muatDataJalan(); }).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.php', payload).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("✅ Jalan diperbarui!"); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusJalan = (id) => {
|
||||||
|
if(!confirm("Hapus jalur ini?")) return;
|
||||||
|
safeFetch('../otak_jalan/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("🗑️ Jalan dihapus."); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TANAH ---
|
||||||
|
window.muatDataTanah = () => {
|
||||||
|
return safeFetch('../otak_parsil/api_tampil.php').then(data => {
|
||||||
|
dataStore.tanah = {}; data.forEach(d => { dataStore.tanah[d.id] = d; }); renderTanah();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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_parsil/api_simpan.php', payload).then(() => { batalGambarTanah(); triggerToastNotification("✅ Tanah ditambahkan!"); muatDataTanah(); }).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_parsil/api_update.php', payload).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("✅ Tanah diperbarui!"); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusTanah = (id) => {
|
||||||
|
if(!confirm("Hapus parsil ini?")) return;
|
||||||
|
safeFetch('../otak_parsil/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("🗑️ Tanah dihapus."); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// LIFECYCLE AWAL (PROMISE ALL MANAGEMENT)
|
||||||
|
// ====================================================
|
||||||
|
setTimeout(() => {
|
||||||
|
map.invalidateSize();
|
||||||
|
console.log("Menghubungkan ke Database untuk penarikan data masal...");
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
muatDataSPBU(),
|
||||||
|
muatDataJalan(),
|
||||||
|
muatDataTanah()
|
||||||
|
]).then(() => {
|
||||||
|
console.log("✅ Sistem WebGIS V2 Siap dan Data Tersinkronisasi Penuh.");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Kegagalan Fatal Jaringan:", err);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// RENDER LAYER GEOJSON (BATAS WILAYAH)
|
||||||
|
// ====================================================
|
||||||
|
fetch('../components/Admin_Kecamatan.json')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error("File GeoJSON tidak ditemukan.");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
L.geoJSON(data, {
|
||||||
|
style: function(feature) {
|
||||||
|
return {
|
||||||
|
color: "#000000", // Warna garis batas (Hitam)
|
||||||
|
weight: 2, // Ketebalan garis
|
||||||
|
opacity: 0.7,
|
||||||
|
fillColor: "#0056b3", // Warna isi wilayah
|
||||||
|
fillOpacity: 0.1 // Transparansi agar peta di bawahnya tetap terlihat
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onEachFeature: function(feature, layer) {
|
||||||
|
if (feature.properties && feature.properties.KECAMATAN) {
|
||||||
|
layer.bindPopup(`<b>Wilayah:</b> ${feature.properties.KECAMATAN}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).addTo(map);
|
||||||
|
console.log("Layer GeoJSON Batas Wilayah berhasil dimuat.");
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Gagal memuat layer wilayah:", error));
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Pontianak V2</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="sidebar-container">
|
||||||
|
<div id="sidebar-header">
|
||||||
|
WebGis-Pontianak
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="card-container" class="card-list-area">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button id="toggle-sidebar" onclick="toggleSidebar()">❮</button>
|
||||||
|
</div>
|
||||||
|
<?php include '../components/modals.php'; ?>
|
||||||
|
|
||||||
|
<button id="btn-selesai-jalan" onclick="selesaiGambarJalan()">✓ Selesai Gambar Jalan</button>
|
||||||
|
<button id="btn-selesai-tanah" onclick="selesaiGambarTanah()">✓ Selesai Area Tanah</button>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
|
||||||
|
<script src="app.js?v=<?= time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
// ===================================================
|
||||||
|
// app.js - ARSITEKTUR WEBGIS V2 (KODE BERSIH & TERPUSAT)
|
||||||
|
// ===================================================
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
isMarkerMode: false, isRoadMode: false, isLandMode: false,
|
||||||
|
tempRoadCoords: [], tempRoadLayer: null,
|
||||||
|
tempLandCoords: [], tempLandLayer: null,
|
||||||
|
filterSPBU: 'Semua',
|
||||||
|
filterJalan: ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'],
|
||||||
|
filterTanah: ['SHM', 'HGB', 'HGU', 'HP']
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataStore = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
const mapLayers = { spbu: {}, jalan: {}, tanah: {} };
|
||||||
|
|
||||||
|
const latPontianak = -0.0227; const lngPontianak = 109.3366; const levelZoom = 13;
|
||||||
|
|
||||||
|
const savedLat = localStorage.getItem('mapCenterLat');
|
||||||
|
const savedLng = localStorage.getItem('mapCenterLng');
|
||||||
|
const savedZoom = localStorage.getItem('mapZoom');
|
||||||
|
|
||||||
|
const initialLat = savedLat ? parseFloat(savedLat) : latPontianak;
|
||||||
|
const initialLng = savedLng ? parseFloat(savedLng) : lngPontianak;
|
||||||
|
const initialZoom = savedZoom ? parseInt(savedZoom) : levelZoom;
|
||||||
|
|
||||||
|
const map = L.map('map', { zoomControl: false }).setView([initialLat, initialLng], initialZoom);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }).addTo(map);
|
||||||
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||||
|
|
||||||
|
// Menyimpan posisi peta saat bergeser atau zoom
|
||||||
|
map.on('moveend', function() {
|
||||||
|
const center = map.getCenter();
|
||||||
|
localStorage.setItem('mapCenterLat', center.lat);
|
||||||
|
localStorage.setItem('mapCenterLng', center.lng);
|
||||||
|
});
|
||||||
|
map.on('zoomend', function() {
|
||||||
|
localStorage.setItem('mapZoom', map.getZoom());
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// FITUR BARU: REVERSE GEOCODING (OSM NOMINATIM)
|
||||||
|
// ====================================================
|
||||||
|
async function dapatkanAlamat(lat, lng) {
|
||||||
|
try {
|
||||||
|
// Memanggil API Geocoding Gratis dari OpenStreetMap
|
||||||
|
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("Gagal mengambil alamat");
|
||||||
|
const data = await response.json();
|
||||||
|
return data.display_name || "Alamat detail tidak ditemukan di area ini";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Geocoding Error:", error);
|
||||||
|
return "Gagal memuat alamat. Periksa koneksi internet.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- KOMPONEN UI & FILTERING ---
|
||||||
|
window.toggleSidebar = () => {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
sidebar.classList.toggle('collapsed');
|
||||||
|
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||||
|
btn.innerHTML = isCollapsed ? '❯' : '❮';
|
||||||
|
|
||||||
|
// Simpan status sidebar ke memori browser
|
||||||
|
localStorage.setItem('sidebarCollapsed', isCollapsed);
|
||||||
|
|
||||||
|
setTimeout(() => map.invalidateSize(), 300);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mengembalikan status sidebar saat halaman dimuat
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (localStorage.getItem('sidebarCollapsed') === 'true') {
|
||||||
|
const sidebar = document.getElementById('sidebar-container');
|
||||||
|
const btn = document.getElementById('toggle-sidebar');
|
||||||
|
if(sidebar && btn) {
|
||||||
|
sidebar.classList.add('collapsed');
|
||||||
|
btn.innerHTML = '❯';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.bukaModalFilterLayer = () => document.getElementById('modal-filter-layer').style.display = 'flex';
|
||||||
|
window.bukaModalFilterSPBU = () => document.getElementById('modal-filter-spbu').style.display = 'flex';
|
||||||
|
|
||||||
|
const RightToolbar = L.Control.extend({
|
||||||
|
options: { position: 'topright' },
|
||||||
|
onAdd: function() {
|
||||||
|
const container = L.DomUtil.create('div', 'custom-leaflet-toolbar');
|
||||||
|
container.innerHTML = `
|
||||||
|
<button class="tool-btn" id="btn-tool-marker" title="Mode Marker" onclick="toggleMarkerMode()">📍</button>
|
||||||
|
<button class="tool-btn" id="btn-tool-jalan" title="Mode Jalan" onclick="toggleRoadMode()">🛣️</button>
|
||||||
|
<button class="tool-btn" id="btn-tool-tanah" title="Mode Tanah" onclick="toggleLandMode()">🟩</button>
|
||||||
|
`;
|
||||||
|
L.DomEvent.disableClickPropagation(container);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.addControl(new RightToolbar());
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// --- LOGIKA ALAT PETA ---
|
||||||
|
window.toggleMarkerMode = () => {
|
||||||
|
if (state.isRoadMode) toggleRoadMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isMarkerMode = !state.isMarkerMode;
|
||||||
|
const btn = document.getElementById('btn-tool-marker');
|
||||||
|
if (state.isMarkerMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.enable());
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = '';
|
||||||
|
Object.values(mapLayers.spbu).forEach(m => m.dragging.disable());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleRoadMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isLandMode) toggleLandMode();
|
||||||
|
state.isRoadMode = !state.isRoadMode;
|
||||||
|
const btn = document.getElementById('btn-tool-jalan');
|
||||||
|
if (state.isRoadMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarJalan();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleLandMode = () => {
|
||||||
|
if (state.isMarkerMode) toggleMarkerMode(); if (state.isRoadMode) toggleRoadMode();
|
||||||
|
state.isLandMode = !state.isLandMode;
|
||||||
|
const btn = document.getElementById('btn-tool-tanah');
|
||||||
|
if (state.isLandMode) {
|
||||||
|
btn.classList.add('active'); map.getContainer().style.cursor = 'crosshair';
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active'); map.getContainer().style.cursor = ''; batalGambarTanah();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Atur state loading untuk alamat
|
||||||
|
const alamatInput = document.getElementById('spbu-alamat');
|
||||||
|
alamatInput.value = "Sedang melacak alamat dari satelit...";
|
||||||
|
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" id="btn-eksekusi-spbu" onclick="eksekusiSimpanBaru()" disabled>Tunggu Alamat...</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Batal</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
|
||||||
|
// KUNCI: Tarik alamat secara asinkron lalu buka kunci tombol simpan
|
||||||
|
const alamatDitemukan = await dapatkanAlamat(e.latlng.lat, e.latlng.lng);
|
||||||
|
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 btnSelesai = document.getElementById('btn-selesai-jalan');
|
||||||
|
if (btnSelesai && state.tempRoadCoords.length >= 2) btnSelesai.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 btnSelesai = document.getElementById('btn-selesai-tanah');
|
||||||
|
if (btnSelesai && state.tempLandCoords.length >= 3) btnSelesai.style.display = 'block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// --- UTILITAS & KOSMETIK MODAL ---
|
||||||
|
function triggerToastNotification(pesan = "Aksi berhasil dieksekusi!") {
|
||||||
|
const toast = document.getElementById('toast-notification');
|
||||||
|
toast.innerText = pesan; toast.classList.add('show');
|
||||||
|
setTimeout(() => { toast.classList.remove('show'); }, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setupDropdownColor = (idSelect, colorMap) => {
|
||||||
|
const select = document.getElementById(idSelect);
|
||||||
|
if(select) {
|
||||||
|
const update = () => { select.style.borderLeftColor = colorMap[select.value] || '#CBD5E0'; };
|
||||||
|
select.addEventListener('change', update);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setupDropdownColor('jalan-status', { 'Jalan Nasional': '#E53E3E', 'Jalan Provinsi': '#3182CE', 'Jalan Kabupaten': '#38A169' });
|
||||||
|
setupDropdownColor('tanah-status', { 'SHM': '#38A169', 'HGB': '#DD6B20', 'HGU': '#3182CE', 'HP': '#718096' });
|
||||||
|
|
||||||
|
function bukaModalSpbu() { document.getElementById('modal-spbu').style.display = 'flex'; }
|
||||||
|
window.tutupModalSpbu = function() {
|
||||||
|
document.getElementById('modal-spbu').style.display = 'none';
|
||||||
|
document.getElementById('spbu-nama').readOnly = false; document.getElementById('spbu-telp').readOnly = false; document.getElementById('spbu-status').disabled = false;
|
||||||
|
};
|
||||||
|
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";
|
||||||
|
document.getElementById('spbu-alamat').value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.handleMarkerClick = function(id) {
|
||||||
|
if (state.isMarkerMode || state.isRoadMode || state.isLandMode) return;
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Detail & Kelola SPBU";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdate(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapus(${id})">Hapus Marker</button>
|
||||||
|
<button class="btn btn-cancel" onclick="tutupModalSpbu()">Tutup</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
document.getElementById('spbu-alamat').value = d.alamat || "Alamat belum direkam";
|
||||||
|
|
||||||
|
document.getElementById('spbu-nama').readOnly = true;
|
||||||
|
document.getElementById('spbu-telp').readOnly = true;
|
||||||
|
document.getElementById('spbu-status').disabled = true;
|
||||||
|
|
||||||
|
document.getElementById('spbu-modal-title').innerText = "Informasi SPBU (Read-Only)";
|
||||||
|
document.getElementById('spbu-action-wrapper').innerHTML = `<button class="btn btn-cancel" style="width:100%" onclick="tutupModalSpbu()">Kembali</button>`;
|
||||||
|
bukaModalSpbu();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ... JALAN & TANAH UI FUNCTIONS (TETAP SAMA SEPERTI SEBELUMNYA) ...
|
||||||
|
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);
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Simpan Data Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanJalan()">Simpan Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarJalan()">Batal</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex'; document.getElementById('btn-selesai-jalan').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarJalan = function() {
|
||||||
|
state.tempRoadCoords = []; if (state.tempRoadLayer) map.removeLayer(state.tempRoadLayer); state.tempRoadLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-jalan'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('jalan-nama').readOnly = false; document.getElementById('jalan-status').disabled = false;
|
||||||
|
document.getElementById('jalan-modal-title').innerText = "Detail & Kelola Jalan";
|
||||||
|
document.getElementById('jalan-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateJalan(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusJalan(${id})">Hapus Jalan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-jalan').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-jalan').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.selesaiGambarTanah = function() {
|
||||||
|
if (state.tempLandCoords.length < 3) return;
|
||||||
|
let area = 0, R = 6378137;
|
||||||
|
const ll = state.tempLandCoords.map(c => L.latLng(c[0], c[1]));
|
||||||
|
for (let i = 0; i < ll.length; i++) {
|
||||||
|
let p1 = ll[i], p2 = ll[(i + 1) % ll.length];
|
||||||
|
area += (p2.lng - p1.lng) * Math.PI / 180 * (2 + Math.sin(p1.lat * Math.PI / 180) + Math.sin(p2.lat * Math.PI / 180));
|
||||||
|
}
|
||||||
|
document.getElementById('tanah-id').value = ""; document.getElementById('tanah-nama').value = ""; document.getElementById('tanah-status').value = "SHM";
|
||||||
|
document.getElementById('tanah-status').dispatchEvent(new Event('change'));
|
||||||
|
document.getElementById('tanah-luas').value = Math.round(Math.abs(area * R * R / 2.0)); document.getElementById('tanah-koordinat').value = JSON.stringify(state.tempLandCoords);
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Simpan Data Parsil Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanTanah()">Simpan Area</button>
|
||||||
|
<button class="btn btn-cancel" onclick="batalGambarTanah()">Batal</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex'; document.getElementById('btn-selesai-tanah').style.display = 'none';
|
||||||
|
};
|
||||||
|
window.batalGambarTanah = function() {
|
||||||
|
state.tempLandCoords = []; if (state.tempLandLayer) map.removeLayer(state.tempLandLayer); state.tempLandLayer = null;
|
||||||
|
const btn = document.getElementById('btn-selesai-tanah'); if (btn) btn.style.display = 'none'; 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'));
|
||||||
|
document.getElementById('tanah-modal-title').innerText = "Detail & Kelola Tanah";
|
||||||
|
document.getElementById('tanah-action-wrapper').innerHTML = `
|
||||||
|
<button class="btn btn-warning" onclick="eksekusiUpdateTanah(${id})">Simpan Perubahan</button>
|
||||||
|
<button class="btn btn-danger" onclick="eksekusiHapusTanah(${id})">Hapus Tanah</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-tanah').style.display='none'">Tutup</button>`;
|
||||||
|
document.getElementById('modal-tanah').style.display = 'flex';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// DECOUPLED RENDERING ENGINE (LOGIKA PENGGAMBARAN)
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
function renderSPBU() {
|
||||||
|
const cardContainer = document.getElementById('card-container');
|
||||||
|
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 isBuka = d.status_operasional === 'Buka 24 Jam';
|
||||||
|
const customIcon = L.divIcon({
|
||||||
|
className: 'custom-div-icon',
|
||||||
|
html: `<div class="marker-wrapper"><div class="marker-pin" style="background-color: ${isBuka ? '#28a745' : '#dc3545'};"></div><i class="marker-dot"></i></div>`,
|
||||||
|
iconSize: [30, 42], iconAnchor: [15, 42]
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([d.latitude, d.longitude], { icon: customIcon, draggable: state.isMarkerMode }).addTo(map);
|
||||||
|
marker.bindTooltip(`<b>${d.nama_spbu}</b>`, { direction: 'top', offset: [0, -35], className: 'spbu-tooltip' });
|
||||||
|
marker.on('click', () => handleMarkerClick(d.id));
|
||||||
|
|
||||||
|
// KUNCI: Reverse Geocoding saat Drag-and-Drop
|
||||||
|
marker.on('dragend', async function(e) {
|
||||||
|
const pos = e.target.getLatLng();
|
||||||
|
triggerToastNotification("⏳ Menganalisis alamat baru...");
|
||||||
|
|
||||||
|
// Tunggu alamat baru dari satelit sebelum dikirim ke database
|
||||||
|
const alamatBaru = await dapatkanAlamat(pos.lat, pos.lng);
|
||||||
|
|
||||||
|
fetch('otak_spbu/api_update_coords.php', {
|
||||||
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: d.id, lat: pos.lat, lng: pos.lng, alamat: alamatBaru })
|
||||||
|
}).then(res => res.ok ? res.json() : Promise.reject(res)).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(); // Gambar ulang untuk update text di Sidebar
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Gagal menyimpan posisi."); e.target.setLatLng([dataStore.spbu[d.id].latitude, dataStore.spbu[d.id].longitude]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mapLayers.spbu[d.id] = marker;
|
||||||
|
|
||||||
|
// Menampilkan Alamat yang terpotong rapi di Sidebar Card
|
||||||
|
const statusClass = isBuka ? 'status-buka' : 'status-tutup';
|
||||||
|
const alamatRingkas = d.alamat ? (d.alamat.substring(0, 45) + '...') : 'Alamat belum direkam';
|
||||||
|
cardContainer.innerHTML += `
|
||||||
|
<div class="spbu-card ${statusClass}" onclick="lihatSpbuDariCard(${d.id})">
|
||||||
|
<h4>${d.nama_spbu}</h4>
|
||||||
|
<p style="font-weight:bold; margin-bottom:4px;">${d.status_operasional} | ${d.no_telp || '-'}</p>
|
||||||
|
<p style="font-size:0.75rem; color:#718096; line-height:1.2;">${alamatRingkas}</p>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderJalan() {
|
||||||
|
Object.values(mapLayers.jalan).forEach(m => map.removeLayer(m));
|
||||||
|
mapLayers.jalan = {};
|
||||||
|
|
||||||
|
Object.values(dataStore.jalan).forEach(d => {
|
||||||
|
if (!state.filterJalan.includes(d.status_jalan)) return;
|
||||||
|
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(JSON.parse(d.koordinat), { color: lineColor, weight: lineWeight, opacity: 0.8 }).addTo(map);
|
||||||
|
polyline.bindTooltip(`<b>${d.nama_jalan}</b><br>${d.jarak_meter} meter`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polyline.on('mouseover', function(e) { this.setStyle({ weight: lineWeight + 3, opacity: 1 }); });
|
||||||
|
polyline.on('mouseout', function(e) { this.setStyle({ weight: lineWeight, opacity: 0.8 }); });
|
||||||
|
polyline.on('click', () => handleRoadClick(d.id));
|
||||||
|
mapLayers.jalan[d.id] = polyline;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
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(JSON.parse(d.koordinat), { color: fillColor, fillColor: fillColor, fillOpacity: 0.4, weight: 2 }).addTo(map);
|
||||||
|
polygon.bindTooltip(`<b>${d.nama_pemilik}</b><br>${d.luas_meter} m² (${d.status_kepemilikan})`, { direction: 'auto', className: 'spbu-tooltip' });
|
||||||
|
polygon.on('mouseover', function(e) { this.setStyle({ fillOpacity: 0.7, weight: 4 }); });
|
||||||
|
polygon.on('mouseout', function(e) { this.setStyle({ fillOpacity: 0.4, weight: 2 }); });
|
||||||
|
polygon.on('click', () => handleLandClick(d.id));
|
||||||
|
mapLayers.tanah[d.id] = polygon;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// API FETCH MANAGER (KOMUNIKASI DATABASE)
|
||||||
|
// ====================================================
|
||||||
|
const safeFetch = async (url, payload = null) => {
|
||||||
|
const options = payload ? { method: 'POST', body: JSON.stringify(payload) } : {};
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) throw new Error(data.error);
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- SPBU ---
|
||||||
|
window.muatDataSPBU = () => {
|
||||||
|
return safeFetch('../otak_spbu/api_tampil.php').then(data => {
|
||||||
|
dataStore.spbu = {}; data.forEach(d => { dataStore.spbu[d.id] = d; }); renderSPBU();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
window.eksekusiSimpanBaru = () => {
|
||||||
|
// Alamat ikut ditangkap dan dikirim
|
||||||
|
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').value,
|
||||||
|
lat: document.getElementById('spbu-lat').value,
|
||||||
|
lng: document.getElementById('spbu-lng').value
|
||||||
|
};
|
||||||
|
safeFetch('../otak_spbu/api_simpan.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU ditambahkan!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { tutupModalSpbu(); triggerToastNotification("✅ SPBU diperbarui!"); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapus = (id) => {
|
||||||
|
if(!confirm("Hapus SPBU ini?")) return;
|
||||||
|
safeFetch('../otak_spbu/api_hapus.php', { id: id }).then(() => { tutupModalSpbu(); triggerToastNotification("🗑️ SPBU dihapus."); muatDataSPBU(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- JALAN ---
|
||||||
|
window.muatDataJalan = () => {
|
||||||
|
return safeFetch('../otak_jalan/api_tampil.php').then(data => {
|
||||||
|
dataStore.jalan = {}; data.forEach(d => { dataStore.jalan[d.id] = d; }); renderJalan();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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.php', payload).then(() => { batalGambarJalan(); triggerToastNotification("✅ Jalan ditambahkan!"); muatDataJalan(); }).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.php', payload).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("✅ Jalan diperbarui!"); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusJalan = (id) => {
|
||||||
|
if(!confirm("Hapus jalur ini?")) return;
|
||||||
|
safeFetch('../otak_jalan/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-jalan').style.display='none'; triggerToastNotification("🗑️ Jalan dihapus."); muatDataJalan(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- TANAH ---
|
||||||
|
window.muatDataTanah = () => {
|
||||||
|
return safeFetch('../otak_parsil/api_tampil.php').then(data => {
|
||||||
|
dataStore.tanah = {}; data.forEach(d => { dataStore.tanah[d.id] = d; }); renderTanah();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
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_parsil/api_simpan.php', payload).then(() => { batalGambarTanah(); triggerToastNotification("✅ Tanah ditambahkan!"); muatDataTanah(); }).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_parsil/api_update.php', payload).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("✅ Tanah diperbarui!"); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
window.eksekusiHapusTanah = (id) => {
|
||||||
|
if(!confirm("Hapus parsil ini?")) return;
|
||||||
|
safeFetch('../otak_parsil/api_hapus.php', { id: id }).then(() => { document.getElementById('modal-tanah').style.display='none'; triggerToastNotification("🗑️ Tanah dihapus."); muatDataTanah(); }).catch(err => alert(err));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// LIFECYCLE AWAL (PROMISE ALL MANAGEMENT)
|
||||||
|
// ====================================================
|
||||||
|
setTimeout(() => {
|
||||||
|
map.invalidateSize();
|
||||||
|
console.log("Menghubungkan ke Database untuk penarikan data masal...");
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
muatDataSPBU(),
|
||||||
|
muatDataJalan(),
|
||||||
|
muatDataTanah()
|
||||||
|
]).then(() => {
|
||||||
|
console.log("✅ Sistem WebGIS V2 Siap dan Data Tersinkronisasi Penuh.");
|
||||||
|
}).catch(err => {
|
||||||
|
console.error("Kegagalan Fatal Jaringan:", err);
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// RENDER LAYER GEOJSON (BATAS WILAYAH)
|
||||||
|
// ====================================================
|
||||||
|
fetch('../components/Admin_Kecamatan.json')
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error("File GeoJSON tidak ditemukan.");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
L.geoJSON(data, {
|
||||||
|
style: function(feature) {
|
||||||
|
return {
|
||||||
|
color: "#000000", // Warna garis batas (Hitam)
|
||||||
|
weight: 2, // Ketebalan garis
|
||||||
|
opacity: 0.7,
|
||||||
|
fillColor: "#0056b3", // Warna isi wilayah
|
||||||
|
fillOpacity: 0.1 // Transparansi agar peta di bawahnya tetap terlihat
|
||||||
|
};
|
||||||
|
},
|
||||||
|
onEachFeature: function(feature, layer) {
|
||||||
|
if (feature.properties && feature.properties.KECAMATAN) {
|
||||||
|
layer.bindPopup(`<b>Wilayah:</b> ${feature.properties.KECAMATAN}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).addTo(map);
|
||||||
|
console.log("Layer GeoJSON Batas Wilayah berhasil dimuat.");
|
||||||
|
})
|
||||||
|
.catch(error => console.error("Gagal memuat layer wilayah:", error));
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Pontianak V3</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="sidebar-container">
|
||||||
|
<div id="sidebar-header">
|
||||||
|
WebGis-Pontianak
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="filter-bar">
|
||||||
|
<button class="filter-tool-btn" id="btn-filter-layer" title="Filter Layer" onclick="bukaModalFilterLayer()">⚙️</button>
|
||||||
|
<span class="filter-label">Status SPBU:</span>
|
||||||
|
<button class="filter-tool-btn" id="btn-filter-spbu" title="Filter SPBU" onclick="bukaModalFilterSPBU()">🔍</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="card-container" class="card-list-area">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button id="toggle-sidebar" onclick="toggleSidebar()">❮</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include '../components/modals.php'; ?>
|
||||||
|
|
||||||
|
<button id="btn-selesai-jalan" onclick="selesaiGambarJalan()">✓ Selesai Gambar Jalan</button>
|
||||||
|
<button id="btn-selesai-tanah" onclick="selesaiGambarTanah()">✓ Selesai Area Tanah</button>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
|
||||||
|
<script src="app.js?v=<?= time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1131
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,80 @@
|
|||||||
|
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);
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebGIS Pontianak V4</title>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="sidebar-container">
|
||||||
|
<div id="sidebar-header">WebGis-Pontianak</div>
|
||||||
|
<!-- Tab Navigasi -->
|
||||||
|
<div class="sidebar-tabs">
|
||||||
|
<button class="tab-btn active" onclick="switchTab('tab-kemiskinan')">Pemetaan</button>
|
||||||
|
<button class="tab-btn" onclick="switchTab('tab-filter')">Filter Map</button>
|
||||||
|
</div>
|
||||||
|
<!-- TAB 1: KEMISKINAN (BARU) -->
|
||||||
|
<div id="tab-kemiskinan" class="tab-content active">
|
||||||
|
<div class="control-panel">
|
||||||
|
<button class="btn-gps" onclick="lokasiSaya()">📍 Arahkan ke Lokasi Saya</button>
|
||||||
|
<!-- Input Garis Kemiskinan -->
|
||||||
|
<div class="add-marker-box" style="margin-bottom: 0; padding: 10px 12px;">
|
||||||
|
<label>Batas Garis Kemiskinan (Rp):</label>
|
||||||
|
<div style="display:flex; gap:8px; margin-top:5px;">
|
||||||
|
<input type="number" id="input-garis-kemiskinan" placeholder="Loading..." class="modern-input">
|
||||||
|
<button class="btn btn-save" onclick="eksekusiUpdateGaris()" style="padding:8px 12px;">Simpan</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Tombol Kelola Program Pelatihan -->
|
||||||
|
<button class="btn btn-warning" onclick="bukaModalPelatihan()" style="margin-bottom: 10px; width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;">
|
||||||
|
<span style="font-size: 1.2rem;">📋</span> Kelola Program Pelatihan
|
||||||
|
</button>
|
||||||
|
<!-- Tombol Tambah Marker Modern -->
|
||||||
|
<div class="modern-action-box">
|
||||||
|
<label>Mode Penambahan Data:</label>
|
||||||
|
<select id="pilihan-marker-baru" class="modern-select">
|
||||||
|
<option value="ibadah">🕌 Pusat Radius (Rumah Ibadah)</option>
|
||||||
|
<option value="penduduk">🏠 Data Keluarga (Penduduk)</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn-modern-add" id="btn-add-kemiskinan" onclick="toggleKemiskinanMode()">
|
||||||
|
<span class="icon">+</span> Aktifkan Kursor Marker
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Legenda Peta -->
|
||||||
|
<div class="map-legend">
|
||||||
|
<h4 class="legend-title">Keterangan Marker</h4>
|
||||||
|
<div class="legend-item"><span class="legend-color" style="background-color: var(--gold); border-radius:4px;"></span> Pusat Radius Ibadah</div>
|
||||||
|
<div class="legend-item"><span class="legend-color" style="background-color: #3182CE;"></span> Keluarga Bernaung</div>
|
||||||
|
<div class="legend-item"><span class="legend-color" style="background-color: #805AD5;"></span> Keluarga Luar Naungan</div>
|
||||||
|
<div class="legend-item"><span class="legend-color" style="background-color: #E53E3E;"></span> Kondisi Darurat (Musibah)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="kemiskinan-card-container" class="card-list-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TAB 2: FILTER & SPBU (LAMA) -->
|
||||||
|
<div id="tab-filter" class="tab-content">
|
||||||
|
<div id="filter-bar">
|
||||||
|
<button class="filter-tool-btn" onclick="bukaModalFilterLayer()">⚙️</button>
|
||||||
|
<span class="filter-label">Status SPBU:</span>
|
||||||
|
<button class="filter-tool-btn" onclick="bukaModalFilterSPBU()">🔍</button>
|
||||||
|
</div>
|
||||||
|
<div id="card-container" class="card-list-area"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="toggle-sidebar" onclick="toggleSidebar()">❮</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php include '../components/modals.php'; ?>
|
||||||
|
|
||||||
|
<button id="btn-selesai-jalan" onclick="selesaiGambarJalan()">✓ Selesai Gambar Jalan</button>
|
||||||
|
<button id="btn-selesai-tanah" onclick="selesaiGambarTanah()">✓ Selesai Area Tanah</button>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.js"></script>
|
||||||
|
<script src="app.js?v=<?= time(); ?>"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
# WebGIS Terintegrasi Kota Pontianak
|
||||||
|
|
||||||
|
Selamat datang di repositori **WebGIS Kota Pontianak**. Proyek ini merupakan aplikasi Sistem Informasi Geografis (SIG) berbasis web yang dirancang untuk memetakan dan mengelola berbagai infrastruktur spasial di wilayah Pontianak, seperti SPBU, Jaringan Jalan, dan Parsil Tanah. Proyek ini disusun secara khusus sebagai **Tugas/Proyek untuk Mata Kuliah Sistem Informasi Geografis (SIG)**.
|
||||||
|
|
||||||
|
Proyek ini dibangun secara iteratif (bertahap) dengan arsitektur terpusat, memungkinkan beberapa antarmuka (*interface*) berbeda untuk berbagi satu sumber *database* dan API yang sama.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👨💻 Identitas Pengembang
|
||||||
|
|
||||||
|
- **Nama**: Nelson Davey
|
||||||
|
- **NIM**: D1041231058
|
||||||
|
- **Instansi/Mata Kuliah**: Sistem Informasi Geografis (SIG)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌟 Preview Fitur Utama
|
||||||
|
|
||||||
|
Aplikasi WebGIS ini dilengkapi dengan fitur-fitur pemetaan interaktif tingkat lanjut yang mencakup:
|
||||||
|
- **📍 Manajemen SPBU (Point)**: Penambahan *marker* SPBU dengan informasi status operasional (Buka 24 Jam / Tidak). Didukung oleh fitur *Reverse Geocoding* otomatis untuk membaca alamat riil berdasarkan pergeseran koordinat *marker* (*drag-and-drop*).
|
||||||
|
- **🛣️ Jaringan Jalan (Polyline)**: Penggambaran rute/ruas jalan secara presisi dengan kalkulator perhitungan jarak otomatis (dalam meter) serta pengelompokan berdasarkan status (Jalan Nasional, Provinsi, Kabupaten).
|
||||||
|
- **🟩 Pemetaan Parsil Tanah (Polygon)**: Pembuatan area bidang tanah interaktif yang dilengkapi algoritma perhitungan luas area matematis (m²) secara *real-time* dan klasifikasi status kepemilikan (SHM, HGB, HGU, HP).
|
||||||
|
- **🔍 Smart Filter & Layer Control**: Panel penyaringan visual (*layer control*) untuk mengisolasi dan menganalisis data spasial berdasarkan kategori spesifik tanpa menumpuk tampilan layar.
|
||||||
|
- **📡 Sistem CRUD Asinkron**: Modifikasi data spasial (Tambah, Edit, Hapus) menggunakan API *fetch* yang mulus dan instan tanpa proses pemuatan ulang halaman (*Seamless UI*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arsitektur Proyek
|
||||||
|
|
||||||
|
Proyek ini menggunakan pendekatan **Centralized Backend & Modular Frontend**. Di mana fungsi-fungsi koneksi *database* dan API diletakkan di luar (akar direktori), sementara setiap versi/iterasi peta (`001`, `002`, `003`, `004`) bertindak sebagai "wajah" (antarmuka pengguna) yang saling independen namun tetap terhubung pada satu pusat otak data.
|
||||||
|
|
||||||
|
### Struktur Direktori Utama
|
||||||
|
|
||||||
|
Berikut adalah gambaran besar susunan *folder* dalam *project* ini:
|
||||||
|
|
||||||
|
```text
|
||||||
|
📦 webgis/
|
||||||
|
├── 📁 001/ # Iterasi V1 (Peta dasar)
|
||||||
|
├── 📁 002/ # Iterasi V2 (Peta murni tanpa filter)
|
||||||
|
├── 📁 003/ # Iterasi V3 (Peta interaktif dengan filter)
|
||||||
|
├── 📁 004/ # Iterasi V4 (Arsitektur lanjutan)
|
||||||
|
├── 📁 assets/ # Aset CSS & gambar terpusat
|
||||||
|
├── 📁 components/ # Komponen UI dapat dipakai ulang (Modals, file JSON)
|
||||||
|
├── 📁 otak_jalan/ # REST API terpusat untuk fungsi Jalan
|
||||||
|
├── 📁 otak_kemiskinan/ # REST API terpusat untuk fungsi Kemiskinan
|
||||||
|
├── 📁 otak_spbu/ # REST API terpusat untuk fungsi SPBU
|
||||||
|
├── 📁 otak_tanah/ # REST API terpusat untuk fungsi Tanah
|
||||||
|
├── 📄 koneksi.php # Titik pusat koneksi ke Database
|
||||||
|
└── 📄 README.md # Dokumentasi Proyek
|
||||||
|
```
|
||||||
|
|
||||||
|
- `koneksi.php`: Jantung aplikasi. Menghubungkan semua iterasi ke dalam satu *database* terpusat bernama `webgis_pontianak`.
|
||||||
|
- `assets/`: Berisi `style.css` global yang mengontrol seluruh desain antarmuka (UI) mulai dari tombol, *sidebar*, hingga animasi *popup*.
|
||||||
|
- `components/`: Berisi kumpulan kerangka HTML *reusable* (komponen yang bisa dipakai ulang) seperti `modals.php` dan data batas kecamatan (`Admin_Kecamatan.json`).
|
||||||
|
- **REST API Lokal (Otak Logika)**:
|
||||||
|
- `otak_spbu/`: Menangani operasi *Create, Read, Update, Delete* (CRUD) serta Reverse Geocoding untuk data SPBU.
|
||||||
|
- `otak_jalan/`: Menangani kalkulasi jarak *polyline* dan CRUD data Jaringan Jalan.
|
||||||
|
- `otak_tanah/`: Menangani kalkulasi luas area *polygon* dan CRUD data Parsil Tanah.
|
||||||
|
- `otak_kemiskinan/`: Menangani pendataan infrastruktur spasial tingkat lanjut.
|
||||||
|
|
||||||
|
### Iterasi Antarmuka (Versi Peta)
|
||||||
|
|
||||||
|
Repositori ini menyimpan beberapa rekam jejak pengembangan antarmuka pengguna:
|
||||||
|
1. **Folder `001`**: Iterasi paling awal. Menyajikan peta dasar dengan konektivitas awal ke *database* terpusat.
|
||||||
|
2. **Folder `002` (Pure Map View)**: Iterasi antarmuka yang difokuskan pada visualisasi murni. Memiliki fitur rendering SPBU, Jalan, dan Tanah, namun secara sengaja disetel **tanpa panel Filter Layer**, menjadikannya peta pameran data (*showcase*).
|
||||||
|
3. **Folder `003` (Interactive Filter Map)**: Iterasi ini memiliki kerangka kerja kontrol layer (*layer control*) penuh. Pengguna dapat secara interaktif menyaring SPBU berdasarkan status operasional (Buka/Tutup), dan menyaring garis jalan serta polygon tanah berdasarkan kepemilikannya.
|
||||||
|
4. **Folder `004` (Advanced Architecture)**: Pengembangan lanjutan dari iterasi sebelumnya. Berfungsi sebagai *bridge* (jembatan) untuk menopang struktur kompleks pemetaan tata letak secara dinamis.
|
||||||
|
|
||||||
|
*(Catatan: Direktori lingkungan kerja lanjutan seperti `Poverty_Mapping` sengaja dikecualikan dari repositori ini untuk menjaga kemurnian arsitektur inti).*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Panduan Instalasi dan Penggunaan
|
||||||
|
|
||||||
|
### Persyaratan Sistem
|
||||||
|
- Web Server Lokal (seperti XAMPP, Laragon, atau MAMP).
|
||||||
|
- PHP versi 7.4 atau lebih baru (direkomendasikan versi 8+).
|
||||||
|
- MySQL / MariaDB.
|
||||||
|
|
||||||
|
### Langkah-langkah
|
||||||
|
1. *Clone* atau unduh *repository* ini.
|
||||||
|
2. Pindahkan seluruh folder `webgis` ke dalam direktori server lokal Anda (misal: `C:\xampp\htdocs\webgis`).
|
||||||
|
3. Buka phpMyAdmin (http://localhost/phpmyadmin).
|
||||||
|
4. Buat *database* baru dengan nama persis: **`webgis_pontianak`**.
|
||||||
|
5. *Import file* SQL (*jika tersedia di paket backup Anda*) ke dalam *database* tersebut untuk mengisi tabel `spbu`, `jalan`, dan `tanah`.
|
||||||
|
6. Akses aplikasi melalui *browser* Anda:
|
||||||
|
- Untuk versi visual murni: `http://localhost/webgis/002/`
|
||||||
|
- Untuk versi interaktif dengan filter: `http://localhost/webgis/003/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Teknologi yang Digunakan
|
||||||
|
- **Frontend**: HTML5, Vanilla JavaScript, CSS3 (Modern Flexbox & CSS Variables).
|
||||||
|
- **Pemetaan (WebGIS)**: Leaflet.js (Librari Javascript *Open-Source* untuk peta interaktif).
|
||||||
|
- **Backend API**: PHP murni dengan PDO (PHP Data Objects) untuk keamanan *database*.
|
||||||
|
- **Database**: MySQL.
|
||||||
|
- **Geocoding**: Nominatim API (OpenStreetMap) untuk menerjemahkan koordinat lintang/bujur menjadi alamat riil.
|
||||||
|
|
||||||
|
---
|
||||||
|
*Dibuat untuk memenuhi tugas & pembelajaran pengembangan Sistem Informasi Geografis.*
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,667 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="toast-notification">Peringatan: Data SPBU berhasil dihapus!</div>
|
||||||
|
|
||||||
|
<div id="modal-spbu" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="spbu-modal-title" class="modal-header">Form Data SPBU</h3>
|
||||||
|
|
||||||
|
<input type="hidden" id="spbu-id">
|
||||||
|
<input type="hidden" id="spbu-lat">
|
||||||
|
<input type="hidden" id="spbu-lng">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="spbu-nama">Nama SPBU:</label>
|
||||||
|
<input type="text" id="spbu-nama" placeholder="Misal: SPBU Ahmad Yani">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="spbu-status">Status Operasional:</label>
|
||||||
|
<select id="spbu-status">
|
||||||
|
<option value="Buka 24 Jam">Buka 24 Jam</option>
|
||||||
|
<option value="Tidak 24 Jam">Tidak 24 Jam</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="spbu-telp">No. Telepon:</label>
|
||||||
|
<input type="tel" id="spbu-telp" placeholder="Misal: 0561xxxxxx">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="spbu-alamat">Alamat (Otomatis dari Koordinat):</label>
|
||||||
|
<textarea id="spbu-alamat" rows="3" readonly class="readonly-input" placeholder="Mengambil data lokasi..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="spbu-action-wrapper">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-jalan" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="jalan-modal-title" class="modal-header">Form Data Jalan</h3>
|
||||||
|
|
||||||
|
<input type="hidden" id="jalan-id">
|
||||||
|
<input type="hidden" id="jalan-koordinat">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jalan-nama">Nama Jalan:</label>
|
||||||
|
<input type="text" id="jalan-nama" placeholder="Misal: Jl. Jenderal Sudirman">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dropdown dengan id khusus untuk dimanipulasi oleh JS -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jalan-status">Status Jalan:</label>
|
||||||
|
<select id="jalan-status" class="dynamic-select">
|
||||||
|
<option value="Jalan Nasional">Jalan Nasional</option>
|
||||||
|
<option value="Jalan Provinsi">Jalan Provinsi</option>
|
||||||
|
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="jalan-jarak">Jarak (Meter):</label>
|
||||||
|
<input type="text" id="jalan-jarak" readonly class="readonly-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="jalan-action-wrapper" style="display: flex; gap: 10px; margin-top: 25px;">
|
||||||
|
<!-- Dinamis oleh JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-tanah" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="tanah-modal-title" class="modal-header">Form Parsil Tanah</h3>
|
||||||
|
|
||||||
|
<input type="hidden" id="tanah-id">
|
||||||
|
<input type="hidden" id="tanah-koordinat">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tanah-nama">Nama Pemilik/Area:</label>
|
||||||
|
<input type="text" id="tanah-nama" placeholder="Misal: Tanah Budi / Area 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tanah-status">Status Kepemilikan:</label>
|
||||||
|
<select id="tanah-status" class="dynamic-select">
|
||||||
|
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
|
||||||
|
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
|
||||||
|
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
|
||||||
|
<option value="HP">Sertifikat Hak Pakai (HP)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="tanah-luas">Luas (m²):</label>
|
||||||
|
<input type="text" id="tanah-luas" readonly class="readonly-input">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="tanah-action-wrapper" style="display: flex; gap: 10px; margin-top: 25px;">
|
||||||
|
<!-- Dinamis oleh JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-filter-layer" class="modal-overlay">
|
||||||
|
<div class="modal-content filter-modal">
|
||||||
|
<h3 class="modal-header">Filter Layer Peta</h3>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<label class="filter-section-title">Status Jalan:</label>
|
||||||
|
<div class="filter-grid">
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-jalan" value="Jalan Nasional" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">Jalan Nasional</span>
|
||||||
|
<span class="color-dot" style="background-color: #E53E3E;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-jalan" value="Jalan Provinsi" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">Jalan Provinsi</span>
|
||||||
|
<span class="color-dot" style="background-color: #3182CE;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-jalan" value="Jalan Kabupaten" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">Jalan Kabupaten</span>
|
||||||
|
<span class="color-dot" style="background-color: #38A169;"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<label class="filter-section-title">Status Tanah:</label>
|
||||||
|
<div class="filter-grid">
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-tanah" value="SHM" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">SHM</span>
|
||||||
|
<span class="color-dot" style="background-color: #38A169;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-tanah" value="HGB" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">HGB</span>
|
||||||
|
<span class="color-dot" style="background-color: #DD6B20;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-tanah" value="HGU" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">HGU</span>
|
||||||
|
<span class="color-dot" style="background-color: #3182CE;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-checkbox-wrapper">
|
||||||
|
<input type="checkbox" class="cb-tanah" value="HP" checked>
|
||||||
|
<span class="custom-checkmark"></span>
|
||||||
|
<span class="filter-text">HP</span>
|
||||||
|
<span class="color-dot" style="background-color: #718096;"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-save" onclick="terapkanFilterLayer()">Terapkan Filter</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-filter-layer').style.display='none'">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-filter-spbu" class="modal-overlay">
|
||||||
|
<div class="modal-content filter-modal">
|
||||||
|
<h3 class="modal-header">Filter Status SPBU</h3>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<label class="filter-section-title">Pilih Operasional:</label>
|
||||||
|
<div class="filter-grid">
|
||||||
|
<!-- Kita menggunakan struktur custom radio button -->
|
||||||
|
<label class="custom-radio-wrapper">
|
||||||
|
<input type="radio" name="rad-spbu" value="Semua" checked>
|
||||||
|
<span class="custom-radiomark"></span>
|
||||||
|
<span class="filter-text">Semua SPBU</span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-radio-wrapper">
|
||||||
|
<input type="radio" name="rad-spbu" value="Buka 24 Jam">
|
||||||
|
<span class="custom-radiomark"></span>
|
||||||
|
<span class="filter-text">Buka 24 Jam</span>
|
||||||
|
<span class="color-dot" style="background-color: #38A169;"></span>
|
||||||
|
</label>
|
||||||
|
<label class="custom-radio-wrapper">
|
||||||
|
<input type="radio" name="rad-spbu" value="Tidak 24 Jam">
|
||||||
|
<span class="custom-radiomark"></span>
|
||||||
|
<span class="filter-text">Tidak 24 Jam</span>
|
||||||
|
<span class="color-dot" style="background-color: #E53E3E;"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-save" onclick="terapkanFilterSPBU()">Terapkan Filter</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-filter-spbu').style.display='none'">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL INTERAKTIF RUMAH PENDUDUK & PMT -->
|
||||||
|
<div id="modal-penduduk" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="penduduk-modal-title" class="modal-header">Formulir Data Penduduk</h3>
|
||||||
|
|
||||||
|
<input type="hidden" id="penduduk-id">
|
||||||
|
<input type="hidden" id="penduduk-lat">
|
||||||
|
<input type="hidden" id="penduduk-lng">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nama Kepala Keluarga (KK):</label>
|
||||||
|
<input type="text" id="penduduk-kk" placeholder="Misal: Budi Santoso">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<div class="form-group" style="flex:1;">
|
||||||
|
<label>Agama:</label>
|
||||||
|
<select id="penduduk-agama">
|
||||||
|
<option value="Islam">Islam</option><option value="Kristen">Kristen</option>
|
||||||
|
<option value="Katolik">Katolik</option><option value="Hindu">Hindu</option>
|
||||||
|
<option value="Buddha">Buddha</option><option value="Konghucu">Konghucu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="width:100px;">
|
||||||
|
<label>Tanggungan:</label>
|
||||||
|
<input type="number" id="penduduk-tanggungan" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Pengeluaran Bulanan (Rp):</label>
|
||||||
|
<input type="number" id="penduduk-pengeluaran" placeholder="Misal: 1500000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="border: 0; border-bottom: 1px dashed var(--border); margin: 15px 0;">
|
||||||
|
<h4 style="margin:0 0 10px 0; color:var(--navy); font-size:0.9rem;">Penilaian PMT (0 = Layak, 1 = Buruk)</h4>
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Lantai</label>
|
||||||
|
<select class="pmt-score-input" id="pmt-lantai">
|
||||||
|
<option value="0">0 - Bagus/Semen</option><option value="1">1 - Tanah/Bambu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Dinding</label>
|
||||||
|
<select class="pmt-score-input" id="pmt-dinding">
|
||||||
|
<option value="0">0 - Tembok/Kayu Baik</option><option value="1">1 - Rumbia/Bambu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sanitasi (BAB)</label>
|
||||||
|
<select class="pmt-score-input" id="pmt-sanitasi">
|
||||||
|
<option value="0">0 - Toilet Sendiri</option><option value="1">1 - Umum/Sungai</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Listrik/Penerangan</label>
|
||||||
|
<select class="pmt-score-input" id="pmt-penerangan">
|
||||||
|
<option value="0">0 - PLN Resmi</option><option value="1">1 - Numpang/Pelita</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sumber Air</label>
|
||||||
|
<select class="pmt-score-input" id="pmt-air">
|
||||||
|
<option value="0">0 - Leding/Sumur Bor</option><option value="1">1 - Sungai/Hujan</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top: 10px;">
|
||||||
|
<label>Alamat Otomatis:</label>
|
||||||
|
<textarea id="penduduk-alamat" rows="2" readonly class="readonly-input"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="penduduk-action-wrapper" class="btn-group"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template id="template-popup-ibadah">
|
||||||
|
<div class="popup-form">
|
||||||
|
<h4>Form Rumah Ibadah</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nama Ibadah:</label>
|
||||||
|
<input type="text" id="pop-ib-nama" placeholder="Cth: Masjid At-Taqwa">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Jenis:</label>
|
||||||
|
<select id="pop-ib-jenis">
|
||||||
|
<option value="Masjid">Masjid</option>
|
||||||
|
<option value="Gereja">Gereja</option>
|
||||||
|
<option value="Vihara">Vihara</option>
|
||||||
|
<option value="Pura">Pura</option>
|
||||||
|
<option value="Klenteng">Klenteng</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Radius Efektif (m): <span id="pop-ib-rad-val">100</span>m</label>
|
||||||
|
<input type="range" id="pop-ib-rad" min="50" max="3000" step="50" value="100" oninput="document.getElementById('pop-ib-rad-val').innerText=this.value">
|
||||||
|
</div>
|
||||||
|
<textarea id="pop-ib-alamat" readonly rows="2" style="font-size:0.75rem; color:#718096">Sedang melacak alamat dari satelit...</textarea>
|
||||||
|
<button class="btn btn-save" style="width:100%; margin-top:10px;" id="btn-pop-simpan" disabled>Tunggu...</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="template-popup-penduduk">
|
||||||
|
<div class="popup-form">
|
||||||
|
<h4>Form Rumah Penduduk</h4>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Nama KK:</label>
|
||||||
|
<input type="text" id="pop-pd-kk" placeholder="Cth: Budi Santoso">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:6px;">
|
||||||
|
<div class="form-group" style="flex:1">
|
||||||
|
<label>Agama:</label>
|
||||||
|
<select id="pop-pd-agama">
|
||||||
|
<option>Islam</option><option>Kristen</option><option>Katolik</option>
|
||||||
|
<option>Hindu</option><option>Buddha</option><option>Konghucu</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="width:80px">
|
||||||
|
<label>Tanggungan:</label>
|
||||||
|
<input type="number" id="pop-pd-tanggungan" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Pengeluaran (Rp):</label>
|
||||||
|
<input type="number" id="pop-pd-pengeluaran" placeholder="Cth: 1500000">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label style="font-size:0.85rem; font-weight:bold; color:var(--navy); border-bottom:1px dashed #A0AEC0; display:block; padding-bottom:4px; margin-top:12px; margin-bottom: 8px;">Indikator Fisik (0=Layak, 1=Buruk)</label>
|
||||||
|
|
||||||
|
<div class="pmt-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Lantai:</label>
|
||||||
|
<select id="pop-pmt-lantai"><option value="0">0 - Bagus</option><option value="1">1 - Tanah</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Dinding:</label>
|
||||||
|
<select id="pop-pmt-dinding"><option value="0">0 - Tembok</option><option value="1">1 - Rumbia</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Sanitasi/WC:</label>
|
||||||
|
<select id="pop-pmt-sanitasi"><option value="0">0 - Sendiri</option><option value="1">1 - Umum</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Listrik:</label>
|
||||||
|
<select id="pop-pmt-listrik"><option value="0">0 - PLN</option><option value="1">1 - Numpang</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="grid-column: span 2;">
|
||||||
|
<label>Sumber Air:</label>
|
||||||
|
<select id="pop-pmt-air"><option value="0">0 - PDAM/Bor</option><option value="1">1 - Sungai/Hujan</option></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="pop-pd-alamat" readonly rows="2" style="font-size:0.75rem; color:#718096; margin-top:8px;">Sedang melacak alamat dari satelit...</textarea>
|
||||||
|
<button class="btn btn-save" style="width:100%; margin-top:10px;" id="btn-pop-simpan" disabled>Tunggu...</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="template-popup-read-penduduk">
|
||||||
|
<div class="pop-read-only">
|
||||||
|
<h3 class="pop-read-title" id="read-pd-nama">Nama Keluarga</h3>
|
||||||
|
|
||||||
|
<div class="pop-read-meta">
|
||||||
|
<span id="read-pd-agama">Agama: -</span>
|
||||||
|
<span id="read-pd-anggota">Anggota/Tanggungan: - Jiwa</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="pop-read-alamat" id="read-pd-alamat">Alamat lengkap...</p>
|
||||||
|
|
||||||
|
<div class="pop-badge-container">
|
||||||
|
<span class="pop-badge" id="read-pd-badge">Status</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="read-pd-naungan" class="pop-naungan">Tidak berada di bawah wewenang rumah ibadah manapun</div>
|
||||||
|
|
||||||
|
<hr class="pop-divider">
|
||||||
|
|
||||||
|
<div class="pop-action-main">
|
||||||
|
<button class="btn btn-success" id="btn-pd-bantuan" style="width:100%; padding: 12px; margin-bottom: 8px;">Catat Log Bantuan</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion-wrapper">
|
||||||
|
<button class="btn btn-accordion" id="btn-pd-opsi">Opsi Lainnya</button>
|
||||||
|
|
||||||
|
<div class="accordion-content" id="acc-pd-content">
|
||||||
|
<hr class="pop-divider-sub">
|
||||||
|
<button class="btn btn-success" id="btn-pd-riwayat-pelatihan" style="width:100%; margin-bottom:8px; padding: 10px;">Lihat Log Pelatihan</button>
|
||||||
|
<button class="btn btn-success" id="btn-pd-riwayat-musibah" style="width:100%; margin-bottom:8px; padding: 10px;">Lihat Laporan Musibah</button>
|
||||||
|
<button class="btn btn-success" id="btn-pd-riwayat-bantuan" style="width:100%; margin-bottom:8px; padding: 10px;">Lihat Log Bantuan</button>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button class="btn btn-warning" id="btn-pd-edit">Edit Data</button>
|
||||||
|
<button class="btn btn-danger" id="btn-pd-hapus">Hapus</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template id="template-popup-read-ibadah">
|
||||||
|
<div class="pop-read-only">
|
||||||
|
<h3 class="pop-read-title" id="read-ib-nama">Nama Rumah Ibadah</h3>
|
||||||
|
|
||||||
|
<div class="pop-read-meta">
|
||||||
|
<span id="read-ib-jenis">Jenis: -</span>
|
||||||
|
<span id="read-ib-radius">Radius Jangkauan: - m</span>
|
||||||
|
<span id="read-ib-naungan" style="color: var(--navy); font-weight: bold;">Keluarga dalam radius: 0 KK</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="pop-read-alamat" id="read-ib-alamat">Alamat lengkap...</p>
|
||||||
|
|
||||||
|
<hr class="pop-divider">
|
||||||
|
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button class="btn btn-warning" id="btn-ib-edit">Edit Data</button>
|
||||||
|
<button class="btn btn-danger" id="btn-ib-hapus">Hapus</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div id="modal-log-bantuan" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 id="log-modal-title" class="modal-header">Catat Bantuan Diberikan</h3>
|
||||||
|
<p style="font-size: 0.85rem; color: #4A5568; margin-top: -10px; margin-bottom: 15px;">Penerima: <span id="log-nama-penerima" style="font-weight: bold; color: var(--navy);"></span></p>
|
||||||
|
|
||||||
|
<input type="hidden" id="log-id-penduduk">
|
||||||
|
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Beras (Kg):</label>
|
||||||
|
<input type="number" id="log-beras" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Minyak (Liter):</label>
|
||||||
|
<input type="number" id="log-minyak" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Gula (Kg):</label>
|
||||||
|
<input type="number" id="log-gula" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Telur (Kg):</label>
|
||||||
|
<input type="number" id="log-telur" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Susu (Kaleng):</label>
|
||||||
|
<input type="number" id="log-susu" value="0" min="0">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Uang Tunai (Rp):</label>
|
||||||
|
<input type="number" id="log-tunai" value="0" min="0" step="50000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="margin-top: 5px;">
|
||||||
|
<label>Catatan / Keterangan (Opsional):</label>
|
||||||
|
<textarea id="log-catatan" rows="2" placeholder="Misal: Bantuan paket Idul Fitri"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanLogBantuan()">Simpan Catatan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-log-bantuan').style.display='none'">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-riwayat-bantuan" class="modal-overlay">
|
||||||
|
<div class="modal-content" style="width: 850px; max-width: 95vw;">
|
||||||
|
<h3 class="modal-header">Riwayat Penerimaan Bantuan</h3>
|
||||||
|
<p style="font-size: 0.85rem; color: #4A5568; margin-top: -10px; margin-bottom: 15px;">Keluarga: <span id="riwayat-nama-penerima" style="font-weight: bold; color: var(--navy);"></span></p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="riwayat-table" id="tabel-riwayat-bantuan">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Waktu Log</th>
|
||||||
|
<th>Beras (Kg)</th>
|
||||||
|
<th>Minyak (L)</th>
|
||||||
|
<th>Gula (Kg)</th>
|
||||||
|
<th>Telur (Kg)</th>
|
||||||
|
<th>Susu (Klg)</th>
|
||||||
|
<th>Tunai (Rp)</th>
|
||||||
|
<th>Catatan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="riwayat-tbody">
|
||||||
|
<!-- Data disuntikkan secara dinamis via app.js -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-riwayat-bantuan').style.display='none'">Tutup Riwayat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-musibah" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h3 class="modal-header" style="color: var(--red-danger);">Lapor Darurat Musibah</h3>
|
||||||
|
<p style="font-size: 0.85rem; color: #4A5568; margin-top: -10px; margin-bottom: 15px;">
|
||||||
|
Keluarga: <span id="musibah-nama" style="font-weight: bold; color: var(--navy);"></span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input type="hidden" id="musibah-id-penduduk">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tanggal Lapor</label>
|
||||||
|
<!-- Dikunci (readonly) karena sistem JS yang akan mengisi secara otomatis -->
|
||||||
|
<input type="date" id="musibah-tgl" readonly style="background: #EDF2F7; cursor: not-allowed; color: #718096;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Jenis Musibah</label>
|
||||||
|
<select id="musibah-jenis" class="modern-select">
|
||||||
|
<option value="Kecelakaan">Kecelakaan</option>
|
||||||
|
<option value="Kematian">Kematian</option>
|
||||||
|
<option value="Sakit Keras">Sakit Keras</option>
|
||||||
|
<option value="Kebakaran">Kebakaran</option>
|
||||||
|
<option value="Bencana Alam">Bencana Alam</option>
|
||||||
|
<option value="Lainnya">Lainnya</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Deskripsi Kerusakan / Kondisi</label>
|
||||||
|
<textarea id="musibah-deskripsi" rows="3" placeholder="Jelaskan kondisi darurat secara ringkas..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Status Penanganan Awal</label>
|
||||||
|
<select id="musibah-status" class="modern-select">
|
||||||
|
<option value="Belum Ditangani">Belum Ditangani</option>
|
||||||
|
<option value="Sudah Ditangani">Sudah Ditangani</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-danger" onclick="simpanMusibah()">Kirim Laporan</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-musibah').style.display='none'">Batal</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL PROGRAM PELATIHAN -->
|
||||||
|
<div id="modal-master-pelatihan" class="modal-overlay">
|
||||||
|
<div class="modal-content" style="width: 500px;">
|
||||||
|
<h3 class="modal-header">Buat Program Pelatihan Baru</h3>
|
||||||
|
<div class="form-group"><label>Nama Pelatihan</label><input type="text" id="latih-nama"></div>
|
||||||
|
<div class="form-group"><label>Instansi Penyelenggara</label><input type="text" id="latih-instansi"></div>
|
||||||
|
<div style="display:flex; gap:10px;">
|
||||||
|
<div class="form-group" style="flex:1;"><label>Tanggal Mulai</label><input type="date" id="latih-mulai"></div>
|
||||||
|
<div class="form-group" style="flex:1;"><label>Tanggal Selesai</label><input type="date" id="latih-selesai"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>Deskripsi</label><textarea id="latih-deskripsi" rows="2"></textarea></div>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-save" onclick="eksekusiSimpanPelatihan()">Simpan Program</button>
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-master-pelatihan').style.display='none'">Tutup</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL DETAIL KELUARGA (ANGGOTA) -->
|
||||||
|
<div id="modal-detail-keluarga" class="modal-overlay">
|
||||||
|
<div class="modal-content" style="width: 950px; max-width: 95vw;">
|
||||||
|
<h3 class="modal-header">Detail Anggota Keluarga</h3>
|
||||||
|
<p style="font-size: 0.85rem; margin-bottom: 15px;">Keluarga: <span id="detail-keluarga-nama" style="font-weight:bold; color: var(--navy);"></span></p>
|
||||||
|
|
||||||
|
<input type="hidden" id="detail-id-penduduk">
|
||||||
|
<input type="hidden" id="detail-is-bernaung"> <!-- Hidden State untuk Auto-Refresh -->
|
||||||
|
|
||||||
|
<div class="table-responsive" style="margin-bottom:20px; max-height: 350px;">
|
||||||
|
<table class="riwayat-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>NIK</th>
|
||||||
|
<th>Nama Lengkap</th>
|
||||||
|
<th>Usia</th>
|
||||||
|
<th>Pendidikan</th>
|
||||||
|
<th>Pekerjaan</th>
|
||||||
|
<th>Status Pelatihan</th>
|
||||||
|
<th>Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tbody-anggota-keluarga">
|
||||||
|
<!-- Data ditarik oleh Fetch JS -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4 style="border-bottom:1px solid var(--border); padding-bottom:5px; color: var(--navy);">Tambah Anggota Baru</h4>
|
||||||
|
<div style="display:grid; grid-template-columns: 1fr 1fr 1fr; gap:10px;">
|
||||||
|
<div class="form-group"><label>NIK</label><input type="text" id="anggota-nik" placeholder="16 Digit NIK"></div>
|
||||||
|
<div class="form-group"><label>Nama Lengkap</label><input type="text" id="anggota-nama"></div>
|
||||||
|
<div class="form-group"><label>Tanggal Lahir</label><input type="date" id="anggota-lahir"></div>
|
||||||
|
<div class="form-group"><label>Pendidikan</label>
|
||||||
|
<select id="anggota-pendidikan">
|
||||||
|
<option value="Tidak Sekolah">Tidak Sekolah</option>
|
||||||
|
<option value="SD">SD</option><option value="SMP">SMP</option>
|
||||||
|
<option value="SMA/SMK">SMA/SMK</option><option value="D3">D3</option><option value="S1">S1</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group"><label>Pekerjaan (Kosongkan jika nganggur)</label><input type="text" id="anggota-pekerjaan" placeholder="Contoh: Petani"></div>
|
||||||
|
<div class="form-group" style="display:flex; align-items:flex-end;">
|
||||||
|
<button class="btn btn-success" style="width:100%; padding: 10px;" onclick="eksekusiSimpanAnggota()">Tambah Anggota</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="btn-group" style="margin-top:20px;">
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-detail-keluarga').style.display='none'">Tutup Jendela</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="modal-riwayat-pelatihan" class="modal-overlay">
|
||||||
|
<div class="modal-content" style="width: 850px; max-width: 95vw;">
|
||||||
|
<h3 class="modal-header">Riwayat Mengikuti Pelatihan</h3>
|
||||||
|
<p style="font-size: 0.85rem; color: #4A5568; margin-top: -10px; margin-bottom: 15px;">Keluarga: <span id="riwayat-pelatihan-nama-penerima" style="font-weight: bold; color: var(--navy);"></span></p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="riwayat-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Tgl Daftar</th><th>NIK</th><th>Nama Anggota</th><th>Program Pelatihan</th><th>Instansi</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="riwayat-pelatihan-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-riwayat-pelatihan').style.display='none'">Tutup Riwayat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- MODAL RIWAYAT MUSIBAH -->
|
||||||
|
<div id="modal-riwayat-musibah" class="modal-overlay">
|
||||||
|
<div class="modal-content" style="width: 850px; max-width: 95vw;">
|
||||||
|
<h3 class="modal-header">Riwayat Pelaporan Musibah</h3>
|
||||||
|
<p style="font-size: 0.85rem; color: #4A5568; margin-top: -10px; margin-bottom: 15px;">Keluarga: <span id="riwayat-musibah-nama-penerima" style="font-weight: bold; color: var(--navy);"></span></p>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="riwayat-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>Tgl Lapor</th><th>Jenis Musibah</th><th>Deskripsi</th><th>Status</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="riwayat-musibah-tbody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group" style="margin-top: 20px;">
|
||||||
|
<button class="btn btn-cancel" onclick="document.getElementById('modal-riwayat-musibah').style.display='none'">Tutup Riwayat</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
// koneksi.php (Terpusat untuk semua versi)
|
||||||
|
|
||||||
|
$host = 'localhost';
|
||||||
|
$user = 'root';
|
||||||
|
$pass = ''; // Kosongkan jika menggunakan XAMPP default
|
||||||
|
$db = 'webgis_pontianak'; // Database bersama
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Membangun koneksi PDO
|
||||||
|
$conn = new PDO("mysql:host=$host;dbname=$db;charset=utf8", $user, $pass);
|
||||||
|
|
||||||
|
// Mengatur PDO agar melempar Exception jika terjadi error
|
||||||
|
$conn->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()]));
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $conn->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()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->id_penduduk)) {
|
||||||
|
try {
|
||||||
|
$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)));
|
||||||
|
|
||||||
|
if($stmt->execute()) {
|
||||||
|
echo json_encode(["pesan" => "Catatan penyaluran bantuan berhasil disimpan."]);
|
||||||
|
}
|
||||||
|
} 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 Keluarga Penerima tidak valid."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
// Validasi ketat: Nama, lat, dan lng adalah harga mati
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Hanya ambil yang sedang aktif
|
||||||
|
$stmt = $conn->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()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $conn->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()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
// Menangkap ID Penduduk dari URL (GET Request)
|
||||||
|
$id_penduduk = isset($_GET['id']) ? intval($_GET['id']) : 0;
|
||||||
|
|
||||||
|
if ($id_penduduk > 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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $conn->query("SELECT * FROM rumah_penduduk 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 kemiskinan: " . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $conn->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()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
// otak_spbu/api_hapus_spbu.php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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."]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
// otak_spbu/api_tampil_spbu.php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $conn->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()]);
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
// otak_spbu/api_update_spbu.php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
header("Content-Type: application/json; charset=UTF-8");
|
||||||
|
header("Access-Control-Allow-Methods: POST");
|
||||||
|
require_once '../koneksi.php';
|
||||||
|
|
||||||
|
$data = json_decode(file_get_contents("php://input"));
|
||||||
|
|
||||||
|
if (!empty($data->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
Reference in New Issue
Block a user