initialize
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
$host = "localhost";
|
||||
$user = "root";
|
||||
$pass = "";
|
||||
$db = "webgis";
|
||||
|
||||
// Coba koneksi ke server dan pilih database
|
||||
$conn = new mysqli($host, $user, $pass, $db);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
// Set charset
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
// Jika di-include oleh file API, biarkan $conn tersedia
|
||||
?>
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
// --- Setup Leaflet Draw ---
|
||||
|
||||
// Layer terpisah untuk hasil gambar sementara sebelum disimpan
|
||||
const drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true
|
||||
},
|
||||
polyline: {
|
||||
metric: true
|
||||
},
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer
|
||||
edit: false,
|
||||
remove: false
|
||||
}
|
||||
});
|
||||
// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS
|
||||
|
||||
|
||||
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
|
||||
map.on(L.Draw.Event.DRAWSTART, function (e) {
|
||||
isDrawingMode = true;
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.DRAWSTOP, function (e) {
|
||||
setTimeout(() => {
|
||||
isDrawingMode = false;
|
||||
window.currentDrawMode = null;
|
||||
window.activeDrawHandler = null;
|
||||
window.deactivateAddMode();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
window.activateDraw = function(type) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (!isAdmin) return;
|
||||
|
||||
console.log("-> activateDraw dipanggil untuk tipe:", type);
|
||||
|
||||
// Toggle: jika mode yang sama ditekan lagi, batalkan
|
||||
if (window.currentDrawMode === type) {
|
||||
console.log("Mode", type, "sudah aktif, membatalkan...");
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.deactivateAddMode();
|
||||
window.currentDrawMode = type;
|
||||
console.log("Mengaktifkan mode menggambar:", type);
|
||||
|
||||
if (type === 'polyline') {
|
||||
const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan';
|
||||
} else if (type === 'polygon') {
|
||||
const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil';
|
||||
}
|
||||
};
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const type = e.layerType;
|
||||
const layer = e.layer;
|
||||
|
||||
// Convert layer to GeoJSON geometry
|
||||
const geoJson = layer.toGeoJSON().geometry;
|
||||
const geoJsonStr = JSON.stringify(geoJson);
|
||||
|
||||
if (type === 'polyline') {
|
||||
let length = 0;
|
||||
const latlngs = layer.getLatLngs();
|
||||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||||
length += latlngs[i].distanceTo(latlngs[i + 1]);
|
||||
}
|
||||
|
||||
// Gunakan Modal alih-alih prompt
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="modalJalanStatus">
|
||||
<option value="Nasional">Nasional</option>
|
||||
<option value="Provinsi">Provinsi</option>
|
||||
<option value="Kabupaten" selected>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${length.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Jalan Baru", bodyHTML, function() {
|
||||
window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus');
|
||||
});
|
||||
|
||||
}
|
||||
else if (type === 'polygon') {
|
||||
const latlngs = layer.getLatLngs()[0];
|
||||
let area = 0;
|
||||
if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
|
||||
area = L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status Tanah</label>
|
||||
<select id="modalParsilStatus">
|
||||
<option value="SHM" selected>SHM</option>
|
||||
<option value="HGB">HGB</option>
|
||||
<option value="HGU">HGU</option>
|
||||
<option value="HP">HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${area.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Parsil Tanah", bodyHTML, function() {
|
||||
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
|
||||
});
|
||||
}
|
||||
});
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
+1098
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,172 @@
|
||||
// --- Fitur Jalan ---
|
||||
|
||||
const jalanColors = {
|
||||
'Nasional': '#ff0000', // Merah
|
||||
'Provinsi': '#0000ff', // Biru
|
||||
'Kabupaten': '#00ff00' // Hijau
|
||||
};
|
||||
|
||||
function loadJalan() {
|
||||
jalanLayer.clearLayers();
|
||||
fetch('api/jalan/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addJalanToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addJalanToMap(item) {
|
||||
// geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]}
|
||||
const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polyline = L.polyline(latlngs, {
|
||||
color: jalanColors[item.status] || '#3388ff',
|
||||
weight: 5
|
||||
});
|
||||
|
||||
polyline.jalanData = item;
|
||||
|
||||
// Tampilkan label nama jalan sejajar dengan garis (diagonal)
|
||||
polyline.setText(item.nama, {
|
||||
center: true,
|
||||
offset: 0,
|
||||
attributes: {
|
||||
fill: '#000000',
|
||||
'font-weight': 'bold',
|
||||
'font-size': '14px',
|
||||
'dy': '7'
|
||||
}
|
||||
});
|
||||
|
||||
// Hitung popupContent sekali saat jalan dibuat
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
|
||||
polyline.bindPopup(popupContent);
|
||||
|
||||
jalanLayer.addLayer(polyline);
|
||||
}
|
||||
|
||||
window.openEditJalanModal = function(id) {
|
||||
let d = null;
|
||||
jalanLayer.eachLayer(function(layer) {
|
||||
if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="editJalanNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editJalanStatus">
|
||||
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
|
||||
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
|
||||
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Jalan", bodyHTML, function() {
|
||||
window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditJalan = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('api/jalan/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteJalan = function(id) {
|
||||
openConfirmModal("Yakin hapus jalan ini?", function() {
|
||||
fetch('api/jalan/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama jalan harus diisi!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('api/jalan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, panjang, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadJalan();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
// --- Setup Leaflet Draw ---
|
||||
|
||||
// Layer terpisah untuk hasil gambar sementara sebelum disimpan
|
||||
const drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true
|
||||
},
|
||||
polyline: {
|
||||
metric: true
|
||||
},
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer
|
||||
edit: false,
|
||||
remove: false
|
||||
}
|
||||
});
|
||||
// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS
|
||||
|
||||
|
||||
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
|
||||
map.on(L.Draw.Event.DRAWSTART, function (e) {
|
||||
isDrawingMode = true;
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.DRAWSTOP, function (e) {
|
||||
setTimeout(() => {
|
||||
isDrawingMode = false;
|
||||
window.currentDrawMode = null;
|
||||
window.activeDrawHandler = null;
|
||||
window.deactivateAddMode();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
window.activateDraw = function(type) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (!isAdmin) return;
|
||||
|
||||
console.log("-> activateDraw dipanggil untuk tipe:", type);
|
||||
|
||||
// Toggle: jika mode yang sama ditekan lagi, batalkan
|
||||
if (window.currentDrawMode === type) {
|
||||
console.log("Mode", type, "sudah aktif, membatalkan...");
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.deactivateAddMode();
|
||||
window.currentDrawMode = type;
|
||||
console.log("Mengaktifkan mode menggambar:", type);
|
||||
|
||||
if (type === 'polyline') {
|
||||
const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan';
|
||||
} else if (type === 'polygon') {
|
||||
const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil';
|
||||
}
|
||||
};
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const type = e.layerType;
|
||||
const layer = e.layer;
|
||||
|
||||
// Convert layer to GeoJSON geometry
|
||||
const geoJson = layer.toGeoJSON().geometry;
|
||||
const geoJsonStr = JSON.stringify(geoJson);
|
||||
|
||||
if (type === 'polyline') {
|
||||
let length = 0;
|
||||
const latlngs = layer.getLatLngs();
|
||||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||||
length += latlngs[i].distanceTo(latlngs[i + 1]);
|
||||
}
|
||||
|
||||
// Gunakan Modal alih-alih prompt
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="modalJalanStatus">
|
||||
<option value="Nasional">Nasional</option>
|
||||
<option value="Provinsi">Provinsi</option>
|
||||
<option value="Kabupaten" selected>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${length.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Jalan Baru", bodyHTML, function() {
|
||||
window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus');
|
||||
});
|
||||
|
||||
}
|
||||
else if (type === 'polygon') {
|
||||
const latlngs = layer.getLatLngs()[0];
|
||||
let area = 0;
|
||||
if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
|
||||
area = L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status Tanah</label>
|
||||
<select id="modalParsilStatus">
|
||||
<option value="SHM" selected>SHM</option>
|
||||
<option value="HGB">HGB</option>
|
||||
<option value="HGU">HGU</option>
|
||||
<option value="HP">HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${area.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Parsil Tanah", bodyHTML, function() {
|
||||
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('../poverty/api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// --- Fitur Jalan ---
|
||||
|
||||
const jalanColors = {
|
||||
'Nasional': '#ff0000', // Merah
|
||||
'Provinsi': '#0000ff', // Biru
|
||||
'Kabupaten': '#00ff00' // Hijau
|
||||
};
|
||||
|
||||
function loadJalan() {
|
||||
jalanLayer.clearLayers();
|
||||
fetch('../jalan/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addJalanToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addJalanToMap(item) {
|
||||
// geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]}
|
||||
const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polyline = L.polyline(latlngs, {
|
||||
color: jalanColors[item.status] || '#3388ff',
|
||||
weight: 5
|
||||
});
|
||||
|
||||
polyline.jalanData = item;
|
||||
|
||||
// Tampilkan label nama jalan sejajar dengan garis (diagonal)
|
||||
polyline.setText(item.nama, {
|
||||
center: true,
|
||||
offset: 0,
|
||||
attributes: {
|
||||
fill: '#000000',
|
||||
'font-weight': 'bold',
|
||||
'font-size': '14px',
|
||||
'dy': '7'
|
||||
}
|
||||
});
|
||||
|
||||
// Hitung popupContent sekali saat jalan dibuat
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
|
||||
polyline.bindPopup(popupContent);
|
||||
|
||||
jalanLayer.addLayer(polyline);
|
||||
}
|
||||
|
||||
window.openEditJalanModal = function(id) {
|
||||
let d = null;
|
||||
jalanLayer.eachLayer(function(layer) {
|
||||
if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="editJalanNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editJalanStatus">
|
||||
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
|
||||
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
|
||||
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Jalan", bodyHTML, function() {
|
||||
window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditJalan = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../jalan/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteJalan = function(id) {
|
||||
openConfirmModal("Yakin hapus jalan ini?", function() {
|
||||
fetch('../jalan/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama jalan harus diisi!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../jalan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, panjang, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadJalan();
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// --- Fitur Parsil ---
|
||||
|
||||
const parsilColors = {
|
||||
'SHM': '#28a745', // Hijau
|
||||
'HGB': '#17a2b8', // Biru Muda
|
||||
'HGU': '#ffc107', // Kuning
|
||||
'HP': '#fd7e14' // Oranye
|
||||
};
|
||||
|
||||
function loadParsil() {
|
||||
parsilLayer.clearLayers();
|
||||
fetch('../parsil/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addParsilToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addParsilToMap(item) {
|
||||
// geom adalah GeoJSON Polygon
|
||||
// coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]]
|
||||
// Leaflet Polygon butuh array of [lat, lng]
|
||||
const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polygon = L.polygon(latlngs, {
|
||||
color: parsilColors[item.status] || '#3388ff',
|
||||
fillColor: parsilColors[item.status] || '#3388ff',
|
||||
fillOpacity: 0.5,
|
||||
weight: 2
|
||||
});
|
||||
|
||||
polygon.parsilData = item;
|
||||
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
polygon.bindPopup(popupContent);
|
||||
|
||||
parsilLayer.addLayer(polygon);
|
||||
}
|
||||
|
||||
window.openEditParsilModal = function(id) {
|
||||
let d = null;
|
||||
parsilLayer.eachLayer(function(layer) {
|
||||
if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="editParsilNama" value="${d.nama || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editParsilStatus">
|
||||
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
|
||||
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
|
||||
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
|
||||
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${d.luas.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Parsil Tanah", bodyHTML, function() {
|
||||
window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditParsil = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../parsil/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteParsil = function(id) {
|
||||
openConfirmModal("Yakin hapus parsil ini?", function() {
|
||||
fetch('../parsil/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../parsil/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, luas, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadParsil();
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// --- Fitur SPBU ---
|
||||
|
||||
// Emoji Bubble Icon builder
|
||||
function makeSpbuIcon(is24) {
|
||||
const cls = is24 ? 'spbu-24' : 'spbu-not24';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
const spbuGreenIcon = makeSpbuIcon(true);
|
||||
const spbuRedIcon = makeSpbuIcon(false);
|
||||
|
||||
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
|
||||
|
||||
|
||||
// Fetch SPBU
|
||||
function loadSpbu() {
|
||||
spbuLayer.clearLayers();
|
||||
fetch('../spbu/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addSpbuMarker(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addSpbuMarker(item) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
const icon = makeSpbuIcon(item.is_24_jam);
|
||||
const marker = L.marker([item.lat, item.lng], {
|
||||
icon: icon,
|
||||
draggable: isAdmin
|
||||
});
|
||||
|
||||
marker.spbuData = item; // Simpan data di objek marker
|
||||
|
||||
// Hitung popupContent sekali saat marker dibuat
|
||||
const d = item;
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
// Event drag untuk update koordinat
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
spbuLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Map Click -> Form Add
|
||||
map.on('click', function(e) {
|
||||
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
|
||||
|
||||
// Hanya proses jika mode tambah SPBU aktif
|
||||
if (window.currentAddMode === 'spbu') {
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="modalSpbuWa" placeholder="No. WA">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
|
||||
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah SPBU Baru", bodyHTML, function() {
|
||||
window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24');
|
||||
});
|
||||
|
||||
// Nonaktifkan mode tambah setelah modal muncul
|
||||
window.deactivateAddMode();
|
||||
}
|
||||
});
|
||||
|
||||
window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama SPBU harus diisi!");
|
||||
return;
|
||||
}
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
fetch('../spbu/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditSpbuModal = function(id) {
|
||||
let d = null;
|
||||
spbuLayer.eachLayer(function(layer) {
|
||||
if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="editSpbuNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="editSpbuWa" value="${d.no_wa}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
|
||||
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit SPBU", bodyHTML, function() {
|
||||
window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
updateSpbu(id, nama, no_wa, is_24_jam, lat, lng);
|
||||
};
|
||||
|
||||
function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) {
|
||||
fetch('../spbu/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteSpbu = function(id) {
|
||||
openConfirmModal("Yakin hapus SPBU ini?", function() {
|
||||
fetch('../spbu/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadSpbu();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->geom)) {
|
||||
$nama = $conn->real_escape_string($data->nama ?? 'Jalan Baru');
|
||||
$status = $conn->real_escape_string($data->status ?? 'Kabupaten');
|
||||
$panjang = (float)($data->panjang ?? 0);
|
||||
$geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string
|
||||
|
||||
$query = "INSERT INTO jalan (nama, status, panjang, geom) VALUES ('$nama', '$status', $panjang, ST_GeomFromGeoJSON('$geom'))";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Jalan berhasil ditambahkan."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal menambahkan jalan: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
$host = "localhost";
|
||||
$user = "root";
|
||||
$pass = "";
|
||||
$db = "webgis";
|
||||
|
||||
// Coba koneksi ke server dan pilih database
|
||||
$conn = new mysqli($host, $user, $pass, $db);
|
||||
|
||||
if ($conn->connect_error) {
|
||||
die("Koneksi gagal: " . $conn->connect_error);
|
||||
}
|
||||
// Set charset
|
||||
$conn->set_charset("utf8mb4");
|
||||
|
||||
// Jika di-include oleh file API, biarkan $conn tersedia
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
|
||||
$query = "DELETE FROM jalan WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Jalan berhasil dihapus."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal hapus jalan: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pemetaan Kemiskinan</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- FontAwesome (Icons) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<!-- Leaflet Draw CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- UI Container over map -->
|
||||
<div class="ui-container">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-bar">
|
||||
<div class="search-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
|
||||
<button class="search-clear" id="searchClear">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Zoom Control -->
|
||||
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
|
||||
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
|
||||
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
|
||||
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Left Sidebar Menu -->
|
||||
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
|
||||
<div class="sidebar-section-label">Umum</div>
|
||||
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
|
||||
<span class="sidebar-emoji">⛽</span>
|
||||
<span class="sidebar-label">SPBU</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
|
||||
<span class="sidebar-emoji">🛣️</span>
|
||||
<span class="sidebar-label">Jalan</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
|
||||
<span class="sidebar-emoji">🗺️</span>
|
||||
<span class="sidebar-label">Parsil</span>
|
||||
</button>
|
||||
|
||||
<div class="sidebar-divider"></div>
|
||||
|
||||
<div class="sidebar-section-label">Poverty</div>
|
||||
<button class="sidebar-btn" id="menuIbadah" data-panel="ibadah" title="Rumah Ibadah">
|
||||
<span class="sidebar-emoji">🕌</span>
|
||||
<span class="sidebar-label">Ibadah</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuMiskin" data-panel="miskin" title="Penduduk Miskin">
|
||||
<span class="sidebar-emoji">🏠</span>
|
||||
<span class="sidebar-label">Miskin</span>
|
||||
</button>
|
||||
|
||||
<div class="sidebar-divider" id="sidebarAdminDivider" style="display:none;"></div>
|
||||
<button class="sidebar-btn" id="menuUsers" style="display:none;" title="Manajemen Pengguna">
|
||||
<span class="sidebar-emoji">👥</span>
|
||||
<span class="sidebar-label">User</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<!-- Custom Layer Control Button -->
|
||||
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
|
||||
<i class="fas fa-layer-group fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<!-- Custom Layer Control Panel -->
|
||||
<div class="custom-layer-panel" id="layerPanel">
|
||||
<h3>Daftar Layer</h3>
|
||||
|
||||
<div class="layer-section-title">Infrastruktur & Lahan</div>
|
||||
|
||||
<!-- SPBU -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerSpbu" checked> SPBU
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subSpbu" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jalan -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerJalan" checked> Jalan
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subJalan" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parsil -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subParsil" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layer-section-title">Pemetaan Kemiskinan</div>
|
||||
|
||||
<!-- Rumah Ibadah -->
|
||||
<label class="layer-option">
|
||||
<input type="checkbox" id="layerRumahIbadah" checked> Rumah Ibadah
|
||||
</label>
|
||||
|
||||
<!-- Penduduk Miskin -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerMiskin" checked> Penduduk Miskin
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subMiskin', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subMiskin" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-miskin" value="Makan" checked onchange="applySubFilter('miskin')"> Bantuan Makan
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-miskin" value="Pemberdayaan" checked onchange="applySubFilter('miskin')"> Pemberdayaan
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GeoJSON Eksternal -->
|
||||
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
|
||||
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
|
||||
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
|
||||
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
|
||||
</div>
|
||||
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
|
||||
<div id="geoJsonLayersContainer"></div>
|
||||
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
|
||||
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
|
||||
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /custom-layer-panel -->
|
||||
|
||||
<!-- Right Data Panel -->
|
||||
<div class="right-panel" id="rightPanel">
|
||||
<div class="right-panel-header">
|
||||
<h3 id="rightPanelTitle">Data</h3>
|
||||
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="right-panel-actions" id="rightPanelActions"></div>
|
||||
<div class="right-panel-body" id="rightPanelBody">
|
||||
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
|
||||
</div>
|
||||
</div><!-- /right-panel -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Map Container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Unified Modal -->
|
||||
<div id="unifiedModal" class="unified-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Judul Form</h3>
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Konten dinamis -->
|
||||
</div>
|
||||
<div class="modal-footer" id="modalFooter">
|
||||
<button id="modalSaveBtn" class="btn-save">Simpan</button>
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div id="confirmModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 300px;">
|
||||
<div class="modal-header">
|
||||
<h3>Konfirmasi</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
|
||||
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Bantuan Modal -->
|
||||
<div id="logBantuanModal" class="unified-modal">
|
||||
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div class="modal-header">
|
||||
<h3 id="logBantuanTitle">Log Bantuan</h3>
|
||||
<span class="modal-close" onclick="closeLogBantuanModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
|
||||
<!-- Riwayat -->
|
||||
<div style="padding:15px; border-bottom:1px solid #eee;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
|
||||
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
|
||||
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Tambah -->
|
||||
<div style="padding:15px;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
|
||||
<div class="form-group">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select id="logIbadahId"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipe Bantuan</label>
|
||||
<select id="logTipeBantuan">
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Uang Tunai">Uang Tunai</option>
|
||||
<option value="Pakaian">Pakaian</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal</label>
|
||||
<input type="date" id="logTanggal">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Keterangan (opsional)</label>
|
||||
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
|
||||
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Profile / Auth Control -->
|
||||
<div id="authWidget" class="auth-widget">
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div id="loginModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 360px;">
|
||||
<div class="modal-header">
|
||||
<h3>Login Ke Sistem</h3>
|
||||
<span class="modal-close" id="closeLoginModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">Username</label>
|
||||
<input type="text" id="loginUsername" placeholder="Masukkan username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" placeholder="Masukkan password">
|
||||
</div>
|
||||
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Modal -->
|
||||
<div id="userManagementModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 700px; max-width: 95%;">
|
||||
<div class="modal-header">
|
||||
<h3>Manajemen Pengguna (Admin)</h3>
|
||||
<span class="modal-close" id="closeUserManagementModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="gap: 15px;">
|
||||
<!-- Form Tambah/Edit User -->
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
|
||||
<input type="hidden" id="manageUserId" value="">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="manageUsername" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password (Kosongkan jika tidak diubah)</label>
|
||||
<input type="password" id="managePassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="manageRole">
|
||||
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="manageIbadahGroup">
|
||||
<label>Rumah Ibadah yang Dikelola</label>
|
||||
<select id="manageIbadahId">
|
||||
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
|
||||
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
|
||||
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Daftar User -->
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
|
||||
<th style="padding: 10px 12px;">Username</th>
|
||||
<th style="padding: 10px 12px;">Role</th>
|
||||
<th style="padding: 10px 12px;">Rumah Ibadah</th>
|
||||
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<!-- Diisi dinamis -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<!-- Leaflet Draw JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
|
||||
<!-- Leaflet TextPath JS (untuk teks diagonal) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js"></script>
|
||||
|
||||
<!-- Main Map Initialization -->
|
||||
<script src="assets/js/map.js?v=<?= time() ?>"></script>
|
||||
<!-- Fitur & Komponen -->
|
||||
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/kemiskinan.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Core Auth Feature -->
|
||||
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$query = "SELECT id, nama, status, panjang, ST_AsGeoJSON(geom) as geom FROM jalan";
|
||||
$result = $conn->query($query);
|
||||
|
||||
$jalan_arr = array();
|
||||
|
||||
if($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$jalan_item = array(
|
||||
"id" => $row['id'],
|
||||
"nama" => $row['nama'],
|
||||
"status" => $row['status'],
|
||||
"panjang" => (float)$row['panjang'],
|
||||
"geom" => json_decode($row['geom'])
|
||||
);
|
||||
array_push($jalan_arr, $jalan_item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $jalan_arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$status = $conn->real_escape_string($data->status ?? 'Kabupaten');
|
||||
|
||||
// Jika ada update geometri
|
||||
if(!empty($data->geom)) {
|
||||
$panjang = (float)($data->panjang ?? 0);
|
||||
$geom = $conn->real_escape_string(json_encode($data->geom));
|
||||
$query = "UPDATE jalan SET nama='$nama', status='$status', panjang=$panjang, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id";
|
||||
} else {
|
||||
$query = "UPDATE jalan SET nama='$nama', status='$status' WHERE id=$id";
|
||||
}
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Jalan berhasil diupdate."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal update jalan: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,158 @@
|
||||
// --- Fitur Parsil ---
|
||||
|
||||
const parsilColors = {
|
||||
'SHM': '#28a745', // Hijau
|
||||
'HGB': '#17a2b8', // Biru Muda
|
||||
'HGU': '#ffc107', // Kuning
|
||||
'HP': '#fd7e14' // Oranye
|
||||
};
|
||||
|
||||
function loadParsil() {
|
||||
parsilLayer.clearLayers();
|
||||
fetch('api/parsil/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addParsilToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addParsilToMap(item) {
|
||||
// geom adalah GeoJSON Polygon
|
||||
// coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]]
|
||||
// Leaflet Polygon butuh array of [lat, lng]
|
||||
const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polygon = L.polygon(latlngs, {
|
||||
color: parsilColors[item.status] || '#3388ff',
|
||||
fillColor: parsilColors[item.status] || '#3388ff',
|
||||
fillOpacity: 0.5,
|
||||
weight: 2
|
||||
});
|
||||
|
||||
polygon.parsilData = item;
|
||||
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
polygon.bindPopup(popupContent);
|
||||
|
||||
parsilLayer.addLayer(polygon);
|
||||
}
|
||||
|
||||
window.openEditParsilModal = function(id) {
|
||||
let d = null;
|
||||
parsilLayer.eachLayer(function(layer) {
|
||||
if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="editParsilNama" value="${d.nama || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editParsilStatus">
|
||||
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
|
||||
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
|
||||
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
|
||||
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${d.luas.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Parsil Tanah", bodyHTML, function() {
|
||||
window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditParsil = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('api/parsil/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteParsil = function(id) {
|
||||
openConfirmModal("Yakin hapus parsil ini?", function() {
|
||||
fetch('api/parsil/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('api/parsil/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, luas, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadParsil();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
|
||||
|
||||
(function() {
|
||||
window.currentUser = null;
|
||||
|
||||
// Elements
|
||||
const authWidget = document.getElementById('authWidget');
|
||||
const loginModal = document.getElementById('loginModal');
|
||||
const loginUsernameInput = document.getElementById('loginUsername');
|
||||
const loginPasswordInput = document.getElementById('loginPassword');
|
||||
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
|
||||
const loginErrorMsg = document.getElementById('loginErrorMsg');
|
||||
const closeLoginModal = document.getElementById('closeLoginModal');
|
||||
|
||||
// Startup Session Check
|
||||
function checkSession() {
|
||||
fetch('../poverty/api/check_session.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.isLoggedIn) {
|
||||
window.currentUser = data.data;
|
||||
} else {
|
||||
window.currentUser = null;
|
||||
}
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Session check failed:", err);
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Update UI based on logged-in state
|
||||
function updateAuthUI() {
|
||||
if (!authWidget) return;
|
||||
|
||||
if (window.currentUser) {
|
||||
// Logged In state
|
||||
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
|
||||
|
||||
authWidget.innerHTML = `
|
||||
<div class="user-profile-pill">
|
||||
<div class="user-profile-info">
|
||||
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
|
||||
<div class="user-profile-role">${roleLabel}</div>
|
||||
</div>
|
||||
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('authLogoutBtn').addEventListener('click', logout);
|
||||
} else {
|
||||
// Logged Out state
|
||||
authWidget.innerHTML = `
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
`;
|
||||
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAllLayers() {
|
||||
if (typeof loadSpbu === 'function') loadSpbu();
|
||||
if (typeof loadJalan === 'function') loadJalan();
|
||||
if (typeof loadParsil === 'function') loadParsil();
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
}
|
||||
|
||||
// Login Modals logic
|
||||
function showLoginModal() {
|
||||
loginUsernameInput.value = '';
|
||||
loginPasswordInput.value = '';
|
||||
loginErrorMsg.style.display = 'none';
|
||||
loginModal.classList.add('show');
|
||||
}
|
||||
|
||||
function hideLoginModal() {
|
||||
loginModal.classList.remove('show');
|
||||
}
|
||||
|
||||
if (closeLoginModal) {
|
||||
closeLoginModal.addEventListener('click', hideLoginModal);
|
||||
}
|
||||
|
||||
if (loginSubmitBtn) {
|
||||
loginSubmitBtn.addEventListener('click', function() {
|
||||
const username = loginUsernameInput.value.trim();
|
||||
const password = loginPasswordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
loginErrorMsg.textContent = 'Username dan password wajib diisi';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('../poverty/api/login.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
window.currentUser = data.data;
|
||||
hideLoginModal();
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Login berhasil', 'success');
|
||||
} else {
|
||||
loginErrorMsg.textContent = data.message || 'Login gagal';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
loginErrorMsg.textContent = 'Koneksi ke server terputus';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch('../poverty/api/logout.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Logout berhasil', 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal logout', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Toast Notification helper
|
||||
function showToast(message, type = 'success') {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Run session check on initialization
|
||||
checkSession();
|
||||
})();
|
||||
@@ -0,0 +1,142 @@
|
||||
// --- Setup Leaflet Draw ---
|
||||
|
||||
// Layer terpisah untuk hasil gambar sementara sebelum disimpan
|
||||
const drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true
|
||||
},
|
||||
polyline: {
|
||||
metric: true
|
||||
},
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer
|
||||
edit: false,
|
||||
remove: false
|
||||
}
|
||||
});
|
||||
// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS
|
||||
|
||||
|
||||
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
|
||||
map.on(L.Draw.Event.DRAWSTART, function (e) {
|
||||
isDrawingMode = true;
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.DRAWSTOP, function (e) {
|
||||
setTimeout(() => {
|
||||
isDrawingMode = false;
|
||||
window.currentDrawMode = null;
|
||||
window.activeDrawHandler = null;
|
||||
window.deactivateAddMode();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
window.activateDraw = function(type) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (!isAdmin) return;
|
||||
|
||||
console.log("-> activateDraw dipanggil untuk tipe:", type);
|
||||
|
||||
// Toggle: jika mode yang sama ditekan lagi, batalkan
|
||||
if (window.currentDrawMode === type) {
|
||||
console.log("Mode", type, "sudah aktif, membatalkan...");
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.deactivateAddMode();
|
||||
window.currentDrawMode = type;
|
||||
console.log("Mengaktifkan mode menggambar:", type);
|
||||
|
||||
if (type === 'polyline') {
|
||||
const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan';
|
||||
} else if (type === 'polygon') {
|
||||
const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil';
|
||||
}
|
||||
};
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const type = e.layerType;
|
||||
const layer = e.layer;
|
||||
|
||||
// Convert layer to GeoJSON geometry
|
||||
const geoJson = layer.toGeoJSON().geometry;
|
||||
const geoJsonStr = JSON.stringify(geoJson);
|
||||
|
||||
if (type === 'polyline') {
|
||||
let length = 0;
|
||||
const latlngs = layer.getLatLngs();
|
||||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||||
length += latlngs[i].distanceTo(latlngs[i + 1]);
|
||||
}
|
||||
|
||||
// Gunakan Modal alih-alih prompt
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="modalJalanStatus">
|
||||
<option value="Nasional">Nasional</option>
|
||||
<option value="Provinsi">Provinsi</option>
|
||||
<option value="Kabupaten" selected>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${length.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Jalan Baru", bodyHTML, function() {
|
||||
window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus');
|
||||
});
|
||||
|
||||
}
|
||||
else if (type === 'polygon') {
|
||||
const latlngs = layer.getLatLngs()[0];
|
||||
let area = 0;
|
||||
if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
|
||||
area = L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status Tanah</label>
|
||||
<select id="modalParsilStatus">
|
||||
<option value="SHM" selected>SHM</option>
|
||||
<option value="HGB">HGB</option>
|
||||
<option value="HGU">HGU</option>
|
||||
<option value="HP">HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${area.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Parsil Tanah", bodyHTML, function() {
|
||||
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('../poverty/api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// --- Fitur Jalan ---
|
||||
|
||||
const jalanColors = {
|
||||
'Nasional': '#ff0000', // Merah
|
||||
'Provinsi': '#0000ff', // Biru
|
||||
'Kabupaten': '#00ff00' // Hijau
|
||||
};
|
||||
|
||||
function loadJalan() {
|
||||
jalanLayer.clearLayers();
|
||||
fetch('../jalan/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addJalanToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addJalanToMap(item) {
|
||||
// geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]}
|
||||
const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polyline = L.polyline(latlngs, {
|
||||
color: jalanColors[item.status] || '#3388ff',
|
||||
weight: 5
|
||||
});
|
||||
|
||||
polyline.jalanData = item;
|
||||
|
||||
// Tampilkan label nama jalan sejajar dengan garis (diagonal)
|
||||
polyline.setText(item.nama, {
|
||||
center: true,
|
||||
offset: 0,
|
||||
attributes: {
|
||||
fill: '#000000',
|
||||
'font-weight': 'bold',
|
||||
'font-size': '14px',
|
||||
'dy': '7'
|
||||
}
|
||||
});
|
||||
|
||||
// Hitung popupContent sekali saat jalan dibuat
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
|
||||
polyline.bindPopup(popupContent);
|
||||
|
||||
jalanLayer.addLayer(polyline);
|
||||
}
|
||||
|
||||
window.openEditJalanModal = function(id) {
|
||||
let d = null;
|
||||
jalanLayer.eachLayer(function(layer) {
|
||||
if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="editJalanNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editJalanStatus">
|
||||
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
|
||||
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
|
||||
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Jalan", bodyHTML, function() {
|
||||
window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditJalan = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../jalan/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteJalan = function(id) {
|
||||
openConfirmModal("Yakin hapus jalan ini?", function() {
|
||||
fetch('../jalan/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama jalan harus diisi!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../jalan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, panjang, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadJalan();
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// --- Fitur Parsil ---
|
||||
|
||||
const parsilColors = {
|
||||
'SHM': '#28a745', // Hijau
|
||||
'HGB': '#17a2b8', // Biru Muda
|
||||
'HGU': '#ffc107', // Kuning
|
||||
'HP': '#fd7e14' // Oranye
|
||||
};
|
||||
|
||||
function loadParsil() {
|
||||
parsilLayer.clearLayers();
|
||||
fetch('../parsil/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addParsilToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addParsilToMap(item) {
|
||||
// geom adalah GeoJSON Polygon
|
||||
// coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]]
|
||||
// Leaflet Polygon butuh array of [lat, lng]
|
||||
const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polygon = L.polygon(latlngs, {
|
||||
color: parsilColors[item.status] || '#3388ff',
|
||||
fillColor: parsilColors[item.status] || '#3388ff',
|
||||
fillOpacity: 0.5,
|
||||
weight: 2
|
||||
});
|
||||
|
||||
polygon.parsilData = item;
|
||||
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
polygon.bindPopup(popupContent);
|
||||
|
||||
parsilLayer.addLayer(polygon);
|
||||
}
|
||||
|
||||
window.openEditParsilModal = function(id) {
|
||||
let d = null;
|
||||
parsilLayer.eachLayer(function(layer) {
|
||||
if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="editParsilNama" value="${d.nama || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editParsilStatus">
|
||||
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
|
||||
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
|
||||
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
|
||||
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${d.luas.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Parsil Tanah", bodyHTML, function() {
|
||||
window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditParsil = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../parsil/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteParsil = function(id) {
|
||||
openConfirmModal("Yakin hapus parsil ini?", function() {
|
||||
fetch('../parsil/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../parsil/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, luas, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadParsil();
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// --- Fitur SPBU ---
|
||||
|
||||
// Emoji Bubble Icon builder
|
||||
function makeSpbuIcon(is24) {
|
||||
const cls = is24 ? 'spbu-24' : 'spbu-not24';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
const spbuGreenIcon = makeSpbuIcon(true);
|
||||
const spbuRedIcon = makeSpbuIcon(false);
|
||||
|
||||
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
|
||||
|
||||
|
||||
// Fetch SPBU
|
||||
function loadSpbu() {
|
||||
spbuLayer.clearLayers();
|
||||
fetch('../spbu/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addSpbuMarker(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addSpbuMarker(item) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
const icon = makeSpbuIcon(item.is_24_jam);
|
||||
const marker = L.marker([item.lat, item.lng], {
|
||||
icon: icon,
|
||||
draggable: isAdmin
|
||||
});
|
||||
|
||||
marker.spbuData = item; // Simpan data di objek marker
|
||||
|
||||
// Hitung popupContent sekali saat marker dibuat
|
||||
const d = item;
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
// Event drag untuk update koordinat
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
spbuLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Map Click -> Form Add
|
||||
map.on('click', function(e) {
|
||||
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
|
||||
|
||||
// Hanya proses jika mode tambah SPBU aktif
|
||||
if (window.currentAddMode === 'spbu') {
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="modalSpbuWa" placeholder="No. WA">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
|
||||
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah SPBU Baru", bodyHTML, function() {
|
||||
window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24');
|
||||
});
|
||||
|
||||
// Nonaktifkan mode tambah setelah modal muncul
|
||||
window.deactivateAddMode();
|
||||
}
|
||||
});
|
||||
|
||||
window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama SPBU harus diisi!");
|
||||
return;
|
||||
}
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
fetch('../spbu/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditSpbuModal = function(id) {
|
||||
let d = null;
|
||||
spbuLayer.eachLayer(function(layer) {
|
||||
if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="editSpbuNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="editSpbuWa" value="${d.no_wa}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
|
||||
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit SPBU", bodyHTML, function() {
|
||||
window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
updateSpbu(id, nama, no_wa, is_24_jam, lat, lng);
|
||||
};
|
||||
|
||||
function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) {
|
||||
fetch('../spbu/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteSpbu = function(id) {
|
||||
openConfirmModal("Yakin hapus SPBU ini?", function() {
|
||||
fetch('../spbu/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadSpbu();
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// Inisialisasi Peta
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// ===== Toast Notification =====
|
||||
window.showToast = function(message, type = 'success', duration = 3500) {
|
||||
const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
|
||||
const container = document.getElementById('toastContainer');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `<span class="toast-icon">${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Custom Zoom Control Logic
|
||||
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
|
||||
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
|
||||
|
||||
// Base Map dari OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Inisialisasi FeatureGroups untuk masing-masing layer
|
||||
const spbuLayer = L.featureGroup().addTo(map);
|
||||
const jalanLayer = L.featureGroup().addTo(map);
|
||||
const parsilLayer = L.featureGroup().addTo(map);
|
||||
const rumahIbadahLayer = L.featureGroup().addTo(map);
|
||||
const pendudukMiskinLayer = L.featureGroup().addTo(map);
|
||||
const geoJsonLayer = L.featureGroup().addTo(map);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchClear = document.getElementById('searchClear');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const val = this.value.toLowerCase();
|
||||
if (val.length > 0) {
|
||||
searchClear.style.visibility = 'visible';
|
||||
window.showSearchResults(val);
|
||||
} else {
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
window.showSearchResults = function(query) {
|
||||
const resultsContainer = document.getElementById('searchResults');
|
||||
if (!resultsContainer) return;
|
||||
resultsContainer.innerHTML = '';
|
||||
let results = [];
|
||||
|
||||
if (typeof spbuLayer !== 'undefined') {
|
||||
spbuLayer.eachLayer(layer => {
|
||||
if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof jalanLayer !== 'undefined') {
|
||||
jalanLayer.eachLayer(layer => {
|
||||
if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof parsilLayer !== 'undefined') {
|
||||
parsilLayer.eachLayer(layer => {
|
||||
if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
|
||||
} else {
|
||||
results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '10px 15px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid #eee';
|
||||
item.style.fontSize = '14px';
|
||||
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
|
||||
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
|
||||
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
|
||||
item.addEventListener('click', () => {
|
||||
if (res.layer instanceof L.Marker) {
|
||||
map.setView(res.layer.getLatLng(), 17);
|
||||
} else if (res.layer.getBounds) {
|
||||
map.fitBounds(res.layer.getBounds());
|
||||
}
|
||||
res.layer.openPopup();
|
||||
resultsContainer.style.display = 'none';
|
||||
});
|
||||
resultsContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
resultsContainer.style.display = 'block';
|
||||
};
|
||||
|
||||
// UI Logic: Custom Layer Control Toggle
|
||||
const layerBtn = document.getElementById('layerBtn');
|
||||
const layerPanel = document.getElementById('layerPanel');
|
||||
|
||||
if (layerBtn && layerPanel) {
|
||||
layerBtn.addEventListener('click', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Sembunyikan layer panel saat klik di peta
|
||||
map.on('click', function() {
|
||||
if (layerPanel) layerPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Logic untuk Toggle Layer Visibility
|
||||
const setupLayerToggle = (id, layer) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setupLayerToggle('layerSpbu', spbuLayer);
|
||||
setupLayerToggle('layerJalan', jalanLayer);
|
||||
setupLayerToggle('layerParsil', parsilLayer);
|
||||
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
|
||||
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
|
||||
|
||||
// --- Sub-layer Toggle (expand/collapse) ---
|
||||
window.toggleSubLayer = function(subId, iconEl) {
|
||||
const sub = document.getElementById(subId);
|
||||
if (!sub) return;
|
||||
const isHidden = sub.style.display === 'none';
|
||||
sub.style.display = isHidden ? '' : 'none';
|
||||
iconEl.classList.toggle('collapsed', !isHidden);
|
||||
};
|
||||
|
||||
// --- Sub-layer Filter ---
|
||||
window.applySubFilter = function(type) {
|
||||
if (type === 'spbu') {
|
||||
const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value === '1');
|
||||
spbuLayer.eachLayer(layer => {
|
||||
if (layer.spbuData === undefined) return;
|
||||
if (checked.includes(layer.spbuData.is_24_jam)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
} else if (type === 'jalan') {
|
||||
const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value);
|
||||
jalanLayer.eachLayer(layer => {
|
||||
if (!layer.jalanData) return;
|
||||
if (checked.includes(layer.jalanData.status)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
} else if (type === 'parsil') {
|
||||
const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value);
|
||||
parsilLayer.eachLayer(layer => {
|
||||
if (!layer.parsilData) return;
|
||||
if (checked.includes(layer.parsilData.status)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- Modal Logic ---
|
||||
const unifiedModal = document.getElementById('unifiedModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalBody = document.getElementById('modalBody');
|
||||
|
||||
window.openModal = function(title, bodyHTML, saveCallback) {
|
||||
if (!unifiedModal) return;
|
||||
modalTitle.textContent = title;
|
||||
modalBody.innerHTML = bodyHTML;
|
||||
unifiedModal.classList.add('show');
|
||||
|
||||
const currentSaveBtn = document.getElementById('modalSaveBtn');
|
||||
if (currentSaveBtn) {
|
||||
const newSaveBtn = currentSaveBtn.cloneNode(true);
|
||||
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
|
||||
newSaveBtn.addEventListener('click', saveCallback);
|
||||
}
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
if (unifiedModal) unifiedModal.classList.remove('show');
|
||||
};
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
window.openConfirmModal = function(msg, confirmCallback) {
|
||||
if (!confirmModal) return;
|
||||
confirmMessage.textContent = msg;
|
||||
confirmModal.classList.add('show');
|
||||
|
||||
const currentYesBtn = document.getElementById('confirmYesBtn');
|
||||
if (currentYesBtn) {
|
||||
const newYesBtn = currentYesBtn.cloneNode(true);
|
||||
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
|
||||
newYesBtn.addEventListener('click', function() {
|
||||
confirmModal.classList.remove('show');
|
||||
confirmCallback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.closeConfirmModal = function() {
|
||||
if (confirmModal) confirmModal.classList.remove('show');
|
||||
};
|
||||
|
||||
window.currentAddMode = null; // 'spbu', 'jalan', 'parsil'
|
||||
window.activateAddMode = function(mode) {
|
||||
if (window.currentAddMode === mode) {
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
window.currentAddMode = mode;
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
};
|
||||
|
||||
window.deactivateAddMode = function() {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
};
|
||||
@@ -0,0 +1,270 @@
|
||||
// ===== Panel Management =====
|
||||
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
|
||||
|
||||
(function () {
|
||||
// ---- State ----
|
||||
let activePanel = null;
|
||||
|
||||
// ---- Elements ----
|
||||
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
||||
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
|
||||
const leftSidebar = document.getElementById('leftSidebar');
|
||||
const rightPanel = document.getElementById('rightPanel');
|
||||
const rightPanelTitle = document.getElementById('rightPanelTitle');
|
||||
const rightPanelActions = document.getElementById('rightPanelActions');
|
||||
const rightPanelBody = document.getElementById('rightPanelBody');
|
||||
const rightPanelClose = document.getElementById('rightPanelClose');
|
||||
|
||||
// ---- Sidebar Toggle ----
|
||||
if (sidebarToggleBtn) {
|
||||
sidebarToggleBtn.addEventListener('click', function () {
|
||||
const isOpen = leftSidebar.style.display !== 'none';
|
||||
if (isOpen) {
|
||||
leftSidebar.style.display = 'none';
|
||||
sidebarToggleBtn.classList.remove('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-right';
|
||||
} else {
|
||||
leftSidebar.style.display = 'flex';
|
||||
sidebarToggleBtn.classList.add('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-left';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Sidebar Buttons ----
|
||||
document.querySelectorAll('.sidebar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const panel = this.dataset.panel;
|
||||
if (activePanel === panel) {
|
||||
closePanel();
|
||||
} else {
|
||||
openPanel(panel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (rightPanelClose) {
|
||||
rightPanelClose.addEventListener('click', closePanel);
|
||||
}
|
||||
|
||||
// ---- Open / Close Panel ----
|
||||
function openPanel(panel) {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = panel;
|
||||
rightPanel.classList.add('open');
|
||||
document.body.classList.add('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
renderPanel(panel);
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = null;
|
||||
rightPanel.classList.remove('open');
|
||||
document.body.classList.remove('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
|
||||
window.refreshActivePanel = function () {
|
||||
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
|
||||
};
|
||||
|
||||
// ---- Render Panel by Type ----
|
||||
function renderPanel(type) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
switch (type) {
|
||||
case 'spbu': renderSpbuPanel(); break;
|
||||
case 'jalan': renderJalanPanel(); break;
|
||||
case 'parsil': renderParsilPanel(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== SPBU Panel =======
|
||||
// ==========================
|
||||
function renderSpbuPanel() {
|
||||
rightPanelTitle.textContent = '⛽ Daftar SPBU';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (isAdmin) {
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelAddSpbu">
|
||||
<i class="fas fa-plus"></i> Tambah SPBU
|
||||
</button>`;
|
||||
document.getElementById('btnPanelAddSpbu').addEventListener('click', () => {
|
||||
window.activateAddMode('spbu');
|
||||
});
|
||||
}
|
||||
|
||||
const items = [];
|
||||
spbuLayer.eachLayer(l => {
|
||||
if (l.spbuData) items.push(l.spbuData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data SPBU</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#fef3c7; color:#d97706; padding:8px; border-radius:8px; font-size:18px;">⛽</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">📞 ${escHtml(d.no_wa || '-')} • 🕐 ${d.is_24_jam ? '24 Jam' : 'Tidak 24 Jam'}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditSpbuModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteSpbu(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
spbuLayer.eachLayer(l => {
|
||||
if (l.spbuData && l.spbuData.id == d.id) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Jalan Panel =======
|
||||
// ==========================
|
||||
function renderJalanPanel() {
|
||||
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
const items = [];
|
||||
jalanLayer.eachLayer(l => {
|
||||
if (l.jalanData) items.push(l.jalanData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data Jalan</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#e0e7ff; color:#4f46e5; padding:8px; border-radius:8px; font-size:18px;">🛣️</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">📏 ${d.panjang.toFixed(2)} m • 🏷️ ${escHtml(d.status)}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditJalanModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteJalan(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
jalanLayer.eachLayer(l => {
|
||||
if (l.jalanData && l.jalanData.id == d.id) {
|
||||
map.fitBounds(l.getBounds()); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Parsil Panel =======
|
||||
// ==========================
|
||||
function renderParsilPanel() {
|
||||
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
const items = [];
|
||||
parsilLayer.eachLayer(l => {
|
||||
if (l.parsilData) items.push(l.parsilData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data Parsil</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#d1fae5; color:#059669; padding:8px; border-radius:8px; font-size:18px;">🗺️</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama || 'Tanpa Nama')}</div>
|
||||
<div class="data-card-sub">📐 ${d.luas.toFixed(2)} m² • 🏷️ ${escHtml(d.status)}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditParsilModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteParsil(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
parsilLayer.eachLayer(l => {
|
||||
if (l.parsilData && l.parsilData.id == d.id) {
|
||||
map.fitBounds(l.getBounds()); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Utility ----
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- Expose ----
|
||||
window.openRightPanel = openPanel;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->geom)) {
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$status = $conn->real_escape_string($data->status ?? 'SHM');
|
||||
$luas = (float)($data->luas ?? 0);
|
||||
$geom = $conn->real_escape_string(json_encode($data->geom)); // GeoJSON string
|
||||
|
||||
$query = "INSERT INTO parsil (nama, status, luas, geom) VALUES ('$nama', '$status', $luas, ST_GeomFromGeoJSON('$geom'))";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "Parsil berhasil ditambahkan."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal menambahkan parsil: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data geometri tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
|
||||
$query = "DELETE FROM parsil WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Parsil berhasil dihapus."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal hapus parsil: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pemetaan Jaringan Jalan</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- FontAwesome (Icons) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<!-- Leaflet Draw CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- UI Container over map -->
|
||||
<div class="ui-container">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-bar">
|
||||
<div class="search-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
|
||||
<button class="search-clear" id="searchClear">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Zoom Control -->
|
||||
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
|
||||
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
|
||||
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
|
||||
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Left Sidebar Menu -->
|
||||
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
|
||||
<div class="sidebar-section-label">Umum</div>
|
||||
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
|
||||
<span class="sidebar-emoji">⛽</span>
|
||||
<span class="sidebar-label">SPBU</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
|
||||
<span class="sidebar-emoji">🛣️</span>
|
||||
<span class="sidebar-label">Jalan</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
|
||||
<span class="sidebar-emoji">🗺️</span>
|
||||
<span class="sidebar-label">Parsil</span>
|
||||
</button>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<!-- Custom Layer Control Button -->
|
||||
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
|
||||
<i class="fas fa-layer-group fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<!-- Custom Layer Control Panel -->
|
||||
<div class="custom-layer-panel" id="layerPanel">
|
||||
<h3>Daftar Layer</h3>
|
||||
|
||||
<div class="layer-section-title">Infrastruktur & Lahan</div>
|
||||
|
||||
<!-- SPBU -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerSpbu" checked> SPBU
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subSpbu" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jalan -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerJalan" checked> Jalan
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subJalan" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parsil -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subParsil" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- GeoJSON Eksternal -->
|
||||
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
|
||||
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
|
||||
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
|
||||
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
|
||||
</div>
|
||||
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
|
||||
<div id="geoJsonLayersContainer"></div>
|
||||
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
|
||||
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
|
||||
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /custom-layer-panel -->
|
||||
|
||||
<!-- Right Data Panel -->
|
||||
<div class="right-panel" id="rightPanel">
|
||||
<div class="right-panel-header">
|
||||
<h3 id="rightPanelTitle">Data</h3>
|
||||
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="right-panel-actions" id="rightPanelActions"></div>
|
||||
<div class="right-panel-body" id="rightPanelBody">
|
||||
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
|
||||
</div>
|
||||
</div><!-- /right-panel -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Map Container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Unified Modal -->
|
||||
<div id="unifiedModal" class="unified-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Judul Form</h3>
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Konten dinamis -->
|
||||
</div>
|
||||
<div class="modal-footer" id="modalFooter">
|
||||
<button id="modalSaveBtn" class="btn-save">Simpan</button>
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div id="confirmModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 300px;">
|
||||
<div class="modal-header">
|
||||
<h3>Konfirmasi</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
|
||||
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Bantuan Modal -->
|
||||
<div id="logBantuanModal" class="unified-modal">
|
||||
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div class="modal-header">
|
||||
<h3 id="logBantuanTitle">Log Bantuan</h3>
|
||||
<span class="modal-close" onclick="closeLogBantuanModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
|
||||
<!-- Riwayat -->
|
||||
<div style="padding:15px; border-bottom:1px solid #eee;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
|
||||
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
|
||||
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Tambah -->
|
||||
<div style="padding:15px;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
|
||||
<div class="form-group">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select id="logIbadahId"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipe Bantuan</label>
|
||||
<select id="logTipeBantuan">
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Uang Tunai">Uang Tunai</option>
|
||||
<option value="Pakaian">Pakaian</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal</label>
|
||||
<input type="date" id="logTanggal">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Keterangan (opsional)</label>
|
||||
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
|
||||
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Profile / Auth Control -->
|
||||
<div id="authWidget" class="auth-widget">
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div id="loginModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 360px;">
|
||||
<div class="modal-header">
|
||||
<h3>Login Ke Sistem</h3>
|
||||
<span class="modal-close" id="closeLoginModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">Username</label>
|
||||
<input type="text" id="loginUsername" placeholder="Masukkan username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" placeholder="Masukkan password">
|
||||
</div>
|
||||
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Modal -->
|
||||
<div id="userManagementModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 700px; max-width: 95%;">
|
||||
<div class="modal-header">
|
||||
<h3>Manajemen Pengguna (Admin)</h3>
|
||||
<span class="modal-close" id="closeUserManagementModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="gap: 15px;">
|
||||
<!-- Form Tambah/Edit User -->
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
|
||||
<input type="hidden" id="manageUserId" value="">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="manageUsername" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password (Kosongkan jika tidak diubah)</label>
|
||||
<input type="password" id="managePassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="manageRole">
|
||||
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="manageIbadahGroup">
|
||||
<label>Rumah Ibadah yang Dikelola</label>
|
||||
<select id="manageIbadahId">
|
||||
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
|
||||
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
|
||||
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Daftar User -->
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
|
||||
<th style="padding: 10px 12px;">Username</th>
|
||||
<th style="padding: 10px 12px;">Role</th>
|
||||
<th style="padding: 10px 12px;">Rumah Ibadah</th>
|
||||
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<!-- Diisi dinamis -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<!-- Leaflet Draw JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
|
||||
<!-- Leaflet TextPath JS (untuk teks diagonal) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js"></script>
|
||||
|
||||
<!-- Main Map Initialization -->
|
||||
<script src="assets/js/map.js?v=<?= time() ?>"></script>
|
||||
<!-- Fitur & Komponen -->
|
||||
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
|
||||
|
||||
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Core Auth Feature -->
|
||||
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$query = "SELECT id, nama, status, luas, ST_AsGeoJSON(geom) as geom FROM parsil";
|
||||
$result = $conn->query($query);
|
||||
|
||||
$parsil_arr = array();
|
||||
|
||||
if($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$parsil_item = array(
|
||||
"id" => $row['id'],
|
||||
"nama" => $row['nama'],
|
||||
"status" => $row['status'],
|
||||
"luas" => (float)$row['luas'],
|
||||
"geom" => json_decode($row['geom'])
|
||||
);
|
||||
array_push($parsil_arr, $parsil_item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $parsil_arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$status = $conn->real_escape_string($data->status ?? 'SHM');
|
||||
|
||||
// Jika ada update geometri
|
||||
if(!empty($data->geom)) {
|
||||
$luas = (float)($data->luas ?? 0);
|
||||
$geom = $conn->real_escape_string(json_encode($data->geom));
|
||||
$query = "UPDATE parsil SET nama='$nama', status='$status', luas=$luas, geom=ST_GeomFromGeoJSON('$geom') WHERE id=$id";
|
||||
} else {
|
||||
$query = "UPDATE parsil SET nama='$nama', status='$status' WHERE id=$id";
|
||||
}
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "Parsil berhasil diupdate."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal update parsil: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,213 @@
|
||||
// --- Fitur SPBU ---
|
||||
|
||||
// Emoji Bubble Icon builder
|
||||
function makeSpbuIcon(is24) {
|
||||
const cls = is24 ? 'spbu-24' : 'spbu-not24';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
const spbuGreenIcon = makeSpbuIcon(true);
|
||||
const spbuRedIcon = makeSpbuIcon(false);
|
||||
|
||||
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
|
||||
|
||||
|
||||
// Fetch SPBU
|
||||
function loadSpbu() {
|
||||
spbuLayer.clearLayers();
|
||||
fetch('api/spbu/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addSpbuMarker(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addSpbuMarker(item) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
const icon = makeSpbuIcon(item.is_24_jam);
|
||||
const marker = L.marker([item.lat, item.lng], {
|
||||
icon: icon,
|
||||
draggable: isAdmin
|
||||
});
|
||||
|
||||
marker.spbuData = item; // Simpan data di objek marker
|
||||
|
||||
// Hitung popupContent sekali saat marker dibuat
|
||||
const d = item;
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
// Event drag untuk update koordinat
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
spbuLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Map Click -> Form Add
|
||||
map.on('click', function(e) {
|
||||
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
|
||||
|
||||
// Hanya proses jika mode tambah SPBU aktif
|
||||
if (window.currentAddMode === 'spbu') {
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="modalSpbuWa" placeholder="No. WA">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
|
||||
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah SPBU Baru", bodyHTML, function() {
|
||||
window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24');
|
||||
});
|
||||
|
||||
// Nonaktifkan mode tambah setelah modal muncul
|
||||
window.deactivateAddMode();
|
||||
}
|
||||
});
|
||||
|
||||
window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama SPBU harus diisi!");
|
||||
return;
|
||||
}
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
fetch('api/spbu/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditSpbuModal = function(id) {
|
||||
let d = null;
|
||||
spbuLayer.eachLayer(function(layer) {
|
||||
if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="editSpbuNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="editSpbuWa" value="${d.no_wa}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
|
||||
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit SPBU", bodyHTML, function() {
|
||||
window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
updateSpbu(id, nama, no_wa, is_24_jam, lat, lng);
|
||||
};
|
||||
|
||||
function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) {
|
||||
fetch('api/spbu/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteSpbu = function(id) {
|
||||
openConfirmModal("Yakin hapus SPBU ini?", function() {
|
||||
fetch('api/spbu/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadSpbu();
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,157 @@
|
||||
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
|
||||
|
||||
(function() {
|
||||
window.currentUser = null;
|
||||
|
||||
// Elements
|
||||
const authWidget = document.getElementById('authWidget');
|
||||
const loginModal = document.getElementById('loginModal');
|
||||
const loginUsernameInput = document.getElementById('loginUsername');
|
||||
const loginPasswordInput = document.getElementById('loginPassword');
|
||||
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
|
||||
const loginErrorMsg = document.getElementById('loginErrorMsg');
|
||||
const closeLoginModal = document.getElementById('closeLoginModal');
|
||||
|
||||
// Startup Session Check
|
||||
function checkSession() {
|
||||
fetch('../poverty/api/check_session.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.isLoggedIn) {
|
||||
window.currentUser = data.data;
|
||||
} else {
|
||||
window.currentUser = null;
|
||||
}
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
})
|
||||
.catch(err => {
|
||||
console.error("Session check failed:", err);
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Update UI based on logged-in state
|
||||
function updateAuthUI() {
|
||||
if (!authWidget) return;
|
||||
|
||||
if (window.currentUser) {
|
||||
// Logged In state
|
||||
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
|
||||
|
||||
authWidget.innerHTML = `
|
||||
<div class="user-profile-pill">
|
||||
<div class="user-profile-info">
|
||||
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
|
||||
<div class="user-profile-role">${roleLabel}</div>
|
||||
</div>
|
||||
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('authLogoutBtn').addEventListener('click', logout);
|
||||
} else {
|
||||
// Logged Out state
|
||||
authWidget.innerHTML = `
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
`;
|
||||
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshAllLayers() {
|
||||
if (typeof loadSpbu === 'function') loadSpbu();
|
||||
if (typeof loadJalan === 'function') loadJalan();
|
||||
if (typeof loadParsil === 'function') loadParsil();
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
}
|
||||
|
||||
// Login Modals logic
|
||||
function showLoginModal() {
|
||||
loginUsernameInput.value = '';
|
||||
loginPasswordInput.value = '';
|
||||
loginErrorMsg.style.display = 'none';
|
||||
loginModal.classList.add('show');
|
||||
}
|
||||
|
||||
function hideLoginModal() {
|
||||
loginModal.classList.remove('show');
|
||||
}
|
||||
|
||||
if (closeLoginModal) {
|
||||
closeLoginModal.addEventListener('click', hideLoginModal);
|
||||
}
|
||||
|
||||
if (loginSubmitBtn) {
|
||||
loginSubmitBtn.addEventListener('click', function() {
|
||||
const username = loginUsernameInput.value.trim();
|
||||
const password = loginPasswordInput.value;
|
||||
|
||||
if (!username || !password) {
|
||||
loginErrorMsg.textContent = 'Username dan password wajib diisi';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('../poverty/api/login.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
window.currentUser = data.data;
|
||||
hideLoginModal();
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Login berhasil', 'success');
|
||||
} else {
|
||||
loginErrorMsg.textContent = data.message || 'Login gagal';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
loginErrorMsg.textContent = 'Koneksi ke server terputus';
|
||||
loginErrorMsg.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logout() {
|
||||
fetch('../poverty/api/logout.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
window.currentUser = null;
|
||||
updateAuthUI();
|
||||
refreshAllLayers();
|
||||
showToast('Logout berhasil', 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal logout', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// Toast Notification helper
|
||||
function showToast(message, type = 'success') {
|
||||
if (typeof window.showToast === 'function') {
|
||||
window.showToast(message, type);
|
||||
return;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
|
||||
// Run session check on initialization
|
||||
checkSession();
|
||||
})();
|
||||
@@ -0,0 +1,142 @@
|
||||
// --- Setup Leaflet Draw ---
|
||||
|
||||
// Layer terpisah untuk hasil gambar sementara sebelum disimpan
|
||||
const drawnItems = new L.FeatureGroup();
|
||||
map.addLayer(drawnItems);
|
||||
|
||||
const drawControl = new L.Control.Draw({
|
||||
draw: {
|
||||
polygon: {
|
||||
allowIntersection: false,
|
||||
showArea: true
|
||||
},
|
||||
polyline: {
|
||||
metric: true
|
||||
},
|
||||
circle: false,
|
||||
circlemarker: false,
|
||||
marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU
|
||||
rectangle: false
|
||||
},
|
||||
edit: {
|
||||
featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer
|
||||
edit: false,
|
||||
remove: false
|
||||
}
|
||||
});
|
||||
// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS
|
||||
|
||||
|
||||
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
|
||||
map.on(L.Draw.Event.DRAWSTART, function (e) {
|
||||
isDrawingMode = true;
|
||||
});
|
||||
|
||||
map.on(L.Draw.Event.DRAWSTOP, function (e) {
|
||||
setTimeout(() => {
|
||||
isDrawingMode = false;
|
||||
window.currentDrawMode = null;
|
||||
window.activeDrawHandler = null;
|
||||
window.deactivateAddMode();
|
||||
}, 200);
|
||||
});
|
||||
|
||||
window.activateDraw = function(type) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (!isAdmin) return;
|
||||
|
||||
console.log("-> activateDraw dipanggil untuk tipe:", type);
|
||||
|
||||
// Toggle: jika mode yang sama ditekan lagi, batalkan
|
||||
if (window.currentDrawMode === type) {
|
||||
console.log("Mode", type, "sudah aktif, membatalkan...");
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.deactivateAddMode();
|
||||
window.currentDrawMode = type;
|
||||
console.log("Mengaktifkan mode menggambar:", type);
|
||||
|
||||
if (type === 'polyline') {
|
||||
const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan';
|
||||
} else if (type === 'polygon') {
|
||||
const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
||||
handler.enable();
|
||||
window.activeDrawHandler = handler;
|
||||
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil';
|
||||
}
|
||||
};
|
||||
|
||||
map.on(L.Draw.Event.CREATED, function (e) {
|
||||
const type = e.layerType;
|
||||
const layer = e.layer;
|
||||
|
||||
// Convert layer to GeoJSON geometry
|
||||
const geoJson = layer.toGeoJSON().geometry;
|
||||
const geoJsonStr = JSON.stringify(geoJson);
|
||||
|
||||
if (type === 'polyline') {
|
||||
let length = 0;
|
||||
const latlngs = layer.getLatLngs();
|
||||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||||
length += latlngs[i].distanceTo(latlngs[i + 1]);
|
||||
}
|
||||
|
||||
// Gunakan Modal alih-alih prompt
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="modalJalanStatus">
|
||||
<option value="Nasional">Nasional</option>
|
||||
<option value="Provinsi">Provinsi</option>
|
||||
<option value="Kabupaten" selected>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${length.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Jalan Baru", bodyHTML, function() {
|
||||
window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus');
|
||||
});
|
||||
|
||||
}
|
||||
else if (type === 'polygon') {
|
||||
const latlngs = layer.getLatLngs()[0];
|
||||
let area = 0;
|
||||
if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
|
||||
area = L.GeometryUtil.geodesicArea(latlngs);
|
||||
}
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status Tanah</label>
|
||||
<select id="modalParsilStatus">
|
||||
<option value="SHM" selected>SHM</option>
|
||||
<option value="HGB">HGB</option>
|
||||
<option value="HGU">HGU</option>
|
||||
<option value="HP">HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${area.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
openModal("Tambah Parsil Tanah", bodyHTML, function() {
|
||||
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('../poverty/api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// --- Fitur Jalan ---
|
||||
|
||||
const jalanColors = {
|
||||
'Nasional': '#ff0000', // Merah
|
||||
'Provinsi': '#0000ff', // Biru
|
||||
'Kabupaten': '#00ff00' // Hijau
|
||||
};
|
||||
|
||||
function loadJalan() {
|
||||
jalanLayer.clearLayers();
|
||||
fetch('../jalan/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addJalanToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addJalanToMap(item) {
|
||||
// geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]}
|
||||
const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polyline = L.polyline(latlngs, {
|
||||
color: jalanColors[item.status] || '#3388ff',
|
||||
weight: 5
|
||||
});
|
||||
|
||||
polyline.jalanData = item;
|
||||
|
||||
// Tampilkan label nama jalan sejajar dengan garis (diagonal)
|
||||
polyline.setText(item.nama, {
|
||||
center: true,
|
||||
offset: 0,
|
||||
attributes: {
|
||||
fill: '#000000',
|
||||
'font-weight': 'bold',
|
||||
'font-size': '14px',
|
||||
'dy': '7'
|
||||
}
|
||||
});
|
||||
|
||||
// Hitung popupContent sekali saat jalan dibuat
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
|
||||
polyline.bindPopup(popupContent);
|
||||
|
||||
jalanLayer.addLayer(polyline);
|
||||
}
|
||||
|
||||
window.openEditJalanModal = function(id) {
|
||||
let d = null;
|
||||
jalanLayer.eachLayer(function(layer) {
|
||||
if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Jalan</label>
|
||||
<input type="text" id="editJalanNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editJalanStatus">
|
||||
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
|
||||
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
|
||||
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Jalan", bodyHTML, function() {
|
||||
window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditJalan = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../jalan/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteJalan = function(id) {
|
||||
openConfirmModal("Yakin hapus jalan ini?", function() {
|
||||
fetch('../jalan/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama jalan harus diisi!");
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../jalan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, panjang, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadJalan();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadJalan();
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// --- Fitur Parsil ---
|
||||
|
||||
const parsilColors = {
|
||||
'SHM': '#28a745', // Hijau
|
||||
'HGB': '#17a2b8', // Biru Muda
|
||||
'HGU': '#ffc107', // Kuning
|
||||
'HP': '#fd7e14' // Oranye
|
||||
};
|
||||
|
||||
function loadParsil() {
|
||||
parsilLayer.clearLayers();
|
||||
fetch('../parsil/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addParsilToMap(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addParsilToMap(item) {
|
||||
// geom adalah GeoJSON Polygon
|
||||
// coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]]
|
||||
// Leaflet Polygon butuh array of [lat, lng]
|
||||
const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]);
|
||||
|
||||
const polygon = L.polygon(latlngs, {
|
||||
color: parsilColors[item.status] || '#3388ff',
|
||||
fillColor: parsilColors[item.status] || '#3388ff',
|
||||
fillOpacity: 0.5,
|
||||
weight: 2
|
||||
});
|
||||
|
||||
polygon.parsilData = item;
|
||||
|
||||
const d = item;
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
polygon.bindPopup(popupContent);
|
||||
|
||||
parsilLayer.addLayer(polygon);
|
||||
}
|
||||
|
||||
window.openEditParsilModal = function(id) {
|
||||
let d = null;
|
||||
parsilLayer.eachLayer(function(layer) {
|
||||
if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama Pemilik/Area</label>
|
||||
<input type="text" id="editParsilNama" value="${d.nama || ''}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Status</label>
|
||||
<select id="editParsilStatus">
|
||||
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
|
||||
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
|
||||
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
|
||||
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Luas: ${d.luas.toFixed(2)} m²</label>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit Parsil Tanah", bodyHTML, function() {
|
||||
window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditParsil = function(id, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
fetch('../parsil/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, status })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.deleteParsil = function(id) {
|
||||
openConfirmModal("Yakin hapus parsil ini?", function() {
|
||||
fetch('../parsil/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const status = document.getElementById(statusId).value;
|
||||
|
||||
const geom = JSON.parse(geoJsonStr);
|
||||
|
||||
fetch('../parsil/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, status, luas, geom })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadParsil();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadParsil();
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
// --- Fitur SPBU ---
|
||||
|
||||
// Emoji Bubble Icon builder
|
||||
function makeSpbuIcon(is24) {
|
||||
const cls = is24 ? 'spbu-24' : 'spbu-not24';
|
||||
return L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>⛽</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
}
|
||||
|
||||
const spbuGreenIcon = makeSpbuIcon(true);
|
||||
const spbuRedIcon = makeSpbuIcon(false);
|
||||
|
||||
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
|
||||
|
||||
|
||||
// Fetch SPBU
|
||||
function loadSpbu() {
|
||||
spbuLayer.clearLayers();
|
||||
fetch('../spbu/read.php')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success' && data.data) {
|
||||
data.data.forEach(item => {
|
||||
addSpbuMarker(item);
|
||||
});
|
||||
}
|
||||
if (window.refreshActivePanel) window.refreshActivePanel();
|
||||
});
|
||||
}
|
||||
|
||||
function addSpbuMarker(item) {
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
const icon = makeSpbuIcon(item.is_24_jam);
|
||||
const marker = L.marker([item.lat, item.lng], {
|
||||
icon: icon,
|
||||
draggable: isAdmin
|
||||
});
|
||||
|
||||
marker.spbuData = item; // Simpan data di objek marker
|
||||
|
||||
// Hitung popupContent sekali saat marker dibuat
|
||||
const d = item;
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div style="display:flex; gap:5px;">
|
||||
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
|
||||
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const popupContent = `
|
||||
<div style="font-family: Arial, sans-serif; min-width: 150px;">
|
||||
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
|
||||
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
|
||||
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
|
||||
${actionButtons}
|
||||
</div>
|
||||
`;
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
// Event drag untuk update koordinat
|
||||
marker.on('dragend', function(e) {
|
||||
const newPos = marker.getLatLng();
|
||||
updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng);
|
||||
});
|
||||
|
||||
spbuLayer.addLayer(marker);
|
||||
}
|
||||
|
||||
// Map Click -> Form Add
|
||||
map.on('click', function(e) {
|
||||
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
|
||||
|
||||
// Hanya proses jika mode tambah SPBU aktif
|
||||
if (window.currentAddMode === 'spbu') {
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="modalSpbuWa" placeholder="No. WA">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
|
||||
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
openModal("Tambah SPBU Baru", bodyHTML, function() {
|
||||
window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24');
|
||||
});
|
||||
|
||||
// Nonaktifkan mode tambah setelah modal muncul
|
||||
window.deactivateAddMode();
|
||||
}
|
||||
});
|
||||
|
||||
window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
if (!nama) {
|
||||
alert("Nama SPBU harus diisi!");
|
||||
return;
|
||||
}
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
fetch('../spbu/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
window.openEditSpbuModal = function(id) {
|
||||
let d = null;
|
||||
spbuLayer.eachLayer(function(layer) {
|
||||
if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData;
|
||||
});
|
||||
if (!d) return;
|
||||
|
||||
const bodyHTML = `
|
||||
<div class="form-group">
|
||||
<label>Nama SPBU</label>
|
||||
<input type="text" id="editSpbuNama" value="${d.nama}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>No. WA</label>
|
||||
<input type="text" id="editSpbuWa" value="${d.no_wa}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<div class="radio-group" style="margin-top:5px;">
|
||||
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
|
||||
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
map.closePopup();
|
||||
openModal("Edit SPBU", bodyHTML, function() {
|
||||
window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24');
|
||||
});
|
||||
};
|
||||
|
||||
window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) {
|
||||
const nama = document.getElementById(namaId).value;
|
||||
const no_wa = document.getElementById(waId).value;
|
||||
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
|
||||
|
||||
updateSpbu(id, nama, no_wa, is_24_jam, lat, lng);
|
||||
};
|
||||
|
||||
function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) {
|
||||
fetch('../spbu/update.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
closeModal();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteSpbu = function(id) {
|
||||
openConfirmModal("Yakin hapus SPBU ini?", function() {
|
||||
fetch('../spbu/delete.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if(data.status === 'success') {
|
||||
map.closePopup();
|
||||
loadSpbu();
|
||||
} else {
|
||||
alert(data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial Load
|
||||
loadSpbu();
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// Inisialisasi Peta
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// ===== Toast Notification =====
|
||||
window.showToast = function(message, type = 'success', duration = 3500) {
|
||||
const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
|
||||
const container = document.getElementById('toastContainer');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `<span class="toast-icon">${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Custom Zoom Control Logic
|
||||
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
|
||||
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
|
||||
|
||||
// Base Map dari OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Inisialisasi FeatureGroups untuk masing-masing layer
|
||||
const spbuLayer = L.featureGroup().addTo(map);
|
||||
const jalanLayer = L.featureGroup().addTo(map);
|
||||
const parsilLayer = L.featureGroup().addTo(map);
|
||||
const rumahIbadahLayer = L.featureGroup().addTo(map);
|
||||
const pendudukMiskinLayer = L.featureGroup().addTo(map);
|
||||
const geoJsonLayer = L.featureGroup().addTo(map);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchClear = document.getElementById('searchClear');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const val = this.value.toLowerCase();
|
||||
if (val.length > 0) {
|
||||
searchClear.style.visibility = 'visible';
|
||||
window.showSearchResults(val);
|
||||
} else {
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
}
|
||||
|
||||
window.showSearchResults = function(query) {
|
||||
const resultsContainer = document.getElementById('searchResults');
|
||||
if (!resultsContainer) return;
|
||||
resultsContainer.innerHTML = '';
|
||||
let results = [];
|
||||
|
||||
if (typeof spbuLayer !== 'undefined') {
|
||||
spbuLayer.eachLayer(layer => {
|
||||
if (layer.spbuData && layer.spbuData.nama && layer.spbuData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'SPBU', nama: layer.spbuData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof jalanLayer !== 'undefined') {
|
||||
jalanLayer.eachLayer(layer => {
|
||||
if (layer.jalanData && layer.jalanData.nama && layer.jalanData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'Jalan', nama: layer.jalanData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof parsilLayer !== 'undefined') {
|
||||
parsilLayer.eachLayer(layer => {
|
||||
if (layer.parsilData && layer.parsilData.nama && layer.parsilData.nama.toLowerCase().includes(query)) {
|
||||
results.push({ type: 'Parsil', nama: layer.parsilData.nama, layer: layer });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
|
||||
} else {
|
||||
results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '10px 15px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid #eee';
|
||||
item.style.fontSize = '14px';
|
||||
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
|
||||
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
|
||||
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
|
||||
item.addEventListener('click', () => {
|
||||
if (res.layer instanceof L.Marker) {
|
||||
map.setView(res.layer.getLatLng(), 17);
|
||||
} else if (res.layer.getBounds) {
|
||||
map.fitBounds(res.layer.getBounds());
|
||||
}
|
||||
res.layer.openPopup();
|
||||
resultsContainer.style.display = 'none';
|
||||
});
|
||||
resultsContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
resultsContainer.style.display = 'block';
|
||||
};
|
||||
|
||||
// UI Logic: Custom Layer Control Toggle
|
||||
const layerBtn = document.getElementById('layerBtn');
|
||||
const layerPanel = document.getElementById('layerPanel');
|
||||
|
||||
if (layerBtn && layerPanel) {
|
||||
layerBtn.addEventListener('click', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Sembunyikan layer panel saat klik di peta
|
||||
map.on('click', function() {
|
||||
if (layerPanel) layerPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
// Logic untuk Toggle Layer Visibility
|
||||
const setupLayerToggle = (id, layer) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(layer) : map.removeLayer(layer);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
setupLayerToggle('layerSpbu', spbuLayer);
|
||||
setupLayerToggle('layerJalan', jalanLayer);
|
||||
setupLayerToggle('layerParsil', parsilLayer);
|
||||
setupLayerToggle('layerRumahIbadah', rumahIbadahLayer);
|
||||
setupLayerToggle('layerMiskin', pendudukMiskinLayer);
|
||||
|
||||
// --- Sub-layer Toggle (expand/collapse) ---
|
||||
window.toggleSubLayer = function(subId, iconEl) {
|
||||
const sub = document.getElementById(subId);
|
||||
if (!sub) return;
|
||||
const isHidden = sub.style.display === 'none';
|
||||
sub.style.display = isHidden ? '' : 'none';
|
||||
iconEl.classList.toggle('collapsed', !isHidden);
|
||||
};
|
||||
|
||||
// --- Sub-layer Filter ---
|
||||
window.applySubFilter = function(type) {
|
||||
if (type === 'spbu') {
|
||||
const checked = [...document.querySelectorAll('.sub-spbu:checked')].map(el => el.value === '1');
|
||||
spbuLayer.eachLayer(layer => {
|
||||
if (layer.spbuData === undefined) return;
|
||||
if (checked.includes(layer.spbuData.is_24_jam)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
} else if (type === 'jalan') {
|
||||
const checked = [...document.querySelectorAll('.sub-jalan:checked')].map(el => el.value);
|
||||
jalanLayer.eachLayer(layer => {
|
||||
if (!layer.jalanData) return;
|
||||
if (checked.includes(layer.jalanData.status)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
} else if (type === 'parsil') {
|
||||
const checked = [...document.querySelectorAll('.sub-parsil:checked')].map(el => el.value);
|
||||
parsilLayer.eachLayer(layer => {
|
||||
if (!layer.parsilData) return;
|
||||
if (checked.includes(layer.parsilData.status)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- Modal Logic ---
|
||||
const unifiedModal = document.getElementById('unifiedModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalBody = document.getElementById('modalBody');
|
||||
|
||||
window.openModal = function(title, bodyHTML, saveCallback) {
|
||||
if (!unifiedModal) return;
|
||||
modalTitle.textContent = title;
|
||||
modalBody.innerHTML = bodyHTML;
|
||||
unifiedModal.classList.add('show');
|
||||
|
||||
const currentSaveBtn = document.getElementById('modalSaveBtn');
|
||||
if (currentSaveBtn) {
|
||||
const newSaveBtn = currentSaveBtn.cloneNode(true);
|
||||
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
|
||||
newSaveBtn.addEventListener('click', saveCallback);
|
||||
}
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
if (unifiedModal) unifiedModal.classList.remove('show');
|
||||
};
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
window.openConfirmModal = function(msg, confirmCallback) {
|
||||
if (!confirmModal) return;
|
||||
confirmMessage.textContent = msg;
|
||||
confirmModal.classList.add('show');
|
||||
|
||||
const currentYesBtn = document.getElementById('confirmYesBtn');
|
||||
if (currentYesBtn) {
|
||||
const newYesBtn = currentYesBtn.cloneNode(true);
|
||||
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
|
||||
newYesBtn.addEventListener('click', function() {
|
||||
confirmModal.classList.remove('show');
|
||||
confirmCallback();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.closeConfirmModal = function() {
|
||||
if (confirmModal) confirmModal.classList.remove('show');
|
||||
};
|
||||
|
||||
window.currentAddMode = null; // 'spbu', 'jalan', 'parsil'
|
||||
window.activateAddMode = function(mode) {
|
||||
if (window.currentAddMode === mode) {
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
window.currentAddMode = mode;
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
};
|
||||
|
||||
window.deactivateAddMode = function() {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
};
|
||||
@@ -0,0 +1,270 @@
|
||||
// ===== Panel Management =====
|
||||
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
|
||||
|
||||
(function () {
|
||||
// ---- State ----
|
||||
let activePanel = null;
|
||||
|
||||
// ---- Elements ----
|
||||
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
||||
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
|
||||
const leftSidebar = document.getElementById('leftSidebar');
|
||||
const rightPanel = document.getElementById('rightPanel');
|
||||
const rightPanelTitle = document.getElementById('rightPanelTitle');
|
||||
const rightPanelActions = document.getElementById('rightPanelActions');
|
||||
const rightPanelBody = document.getElementById('rightPanelBody');
|
||||
const rightPanelClose = document.getElementById('rightPanelClose');
|
||||
|
||||
// ---- Sidebar Toggle ----
|
||||
if (sidebarToggleBtn) {
|
||||
sidebarToggleBtn.addEventListener('click', function () {
|
||||
const isOpen = leftSidebar.style.display !== 'none';
|
||||
if (isOpen) {
|
||||
leftSidebar.style.display = 'none';
|
||||
sidebarToggleBtn.classList.remove('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-right';
|
||||
} else {
|
||||
leftSidebar.style.display = 'flex';
|
||||
sidebarToggleBtn.classList.add('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-left';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Sidebar Buttons ----
|
||||
document.querySelectorAll('.sidebar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const panel = this.dataset.panel;
|
||||
if (activePanel === panel) {
|
||||
closePanel();
|
||||
} else {
|
||||
openPanel(panel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (rightPanelClose) {
|
||||
rightPanelClose.addEventListener('click', closePanel);
|
||||
}
|
||||
|
||||
// ---- Open / Close Panel ----
|
||||
function openPanel(panel) {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = panel;
|
||||
rightPanel.classList.add('open');
|
||||
document.body.classList.add('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
renderPanel(panel);
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = null;
|
||||
rightPanel.classList.remove('open');
|
||||
document.body.classList.remove('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
|
||||
window.refreshActivePanel = function () {
|
||||
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
|
||||
};
|
||||
|
||||
// ---- Render Panel by Type ----
|
||||
function renderPanel(type) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
switch (type) {
|
||||
case 'spbu': renderSpbuPanel(); break;
|
||||
case 'jalan': renderJalanPanel(); break;
|
||||
case 'parsil': renderParsilPanel(); break;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== SPBU Panel =======
|
||||
// ==========================
|
||||
function renderSpbuPanel() {
|
||||
rightPanelTitle.textContent = '⛽ Daftar SPBU';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (isAdmin) {
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelAddSpbu">
|
||||
<i class="fas fa-plus"></i> Tambah SPBU
|
||||
</button>`;
|
||||
document.getElementById('btnPanelAddSpbu').addEventListener('click', () => {
|
||||
window.activateAddMode('spbu');
|
||||
});
|
||||
}
|
||||
|
||||
const items = [];
|
||||
spbuLayer.eachLayer(l => {
|
||||
if (l.spbuData) items.push(l.spbuData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data SPBU</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#fef3c7; color:#d97706; padding:8px; border-radius:8px; font-size:18px;">⛽</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">📞 ${escHtml(d.no_wa || '-')} • 🕐 ${d.is_24_jam ? '24 Jam' : 'Tidak 24 Jam'}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditSpbuModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteSpbu(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
spbuLayer.eachLayer(l => {
|
||||
if (l.spbuData && l.spbuData.id == d.id) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Jalan Panel =======
|
||||
// ==========================
|
||||
function renderJalanPanel() {
|
||||
rightPanelTitle.textContent = '🛣️ Daftar Jalan';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
const items = [];
|
||||
jalanLayer.eachLayer(l => {
|
||||
if (l.jalanData) items.push(l.jalanData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data Jalan</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#e0e7ff; color:#4f46e5; padding:8px; border-radius:8px; font-size:18px;">🛣️</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">📏 ${d.panjang.toFixed(2)} m • 🏷️ ${escHtml(d.status)}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditJalanModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteJalan(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
jalanLayer.eachLayer(l => {
|
||||
if (l.jalanData && l.jalanData.id == d.id) {
|
||||
map.fitBounds(l.getBounds()); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Parsil Panel =======
|
||||
// ==========================
|
||||
function renderParsilPanel() {
|
||||
rightPanelTitle.textContent = '🗺️ Daftar Parsil Tanah';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
const items = [];
|
||||
parsilLayer.eachLayer(l => {
|
||||
if (l.parsilData) items.push(l.parsilData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data Parsil</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon" style="background:#d1fae5; color:#059669; padding:8px; border-radius:8px; font-size:18px;">🗺️</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama || 'Tanpa Nama')}</div>
|
||||
<div class="data-card-sub">📐 ${d.luas.toFixed(2)} m² • 🏷️ ${escHtml(d.status)}</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); window.openEditParsilModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); window.deleteParsil(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => {
|
||||
parsilLayer.eachLayer(l => {
|
||||
if (l.parsilData && l.parsilData.id == d.id) {
|
||||
map.fitBounds(l.getBounds()); l.openPopup();
|
||||
}
|
||||
});
|
||||
});
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Utility ----
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- Expose ----
|
||||
window.openRightPanel = openPanel;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->lat) && !empty($data->lng)) {
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$no_wa = $conn->real_escape_string($data->no_wa ?? '');
|
||||
$is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0;
|
||||
$lat = (float)$data->lat;
|
||||
$lng = (float)$data->lng;
|
||||
|
||||
$query = "INSERT INTO spbu (nama, no_wa, is_24_jam, lat, lng) VALUES ('$nama', '$no_wa', $is_24_jam, $lat, $lng)";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "id" => $conn->insert_id, "message" => "SPBU berhasil ditambahkan."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal menambahkan SPBU: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id)) {
|
||||
$id = (int)$data->id;
|
||||
|
||||
$query = "DELETE FROM spbu WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "SPBU berhasil dihapus."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal hapus SPBU: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "ID tidak diberikan."]);
|
||||
}
|
||||
?>
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Pemetaan Jaringan Jalan</title>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Google+Sans+Flex:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- FontAwesome (Icons) -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||
|
||||
<!-- Leaflet Draw CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- UI Container over map -->
|
||||
<div class="ui-container">
|
||||
<!-- Search Bar -->
|
||||
<div class="search-bar">
|
||||
<div class="search-icon">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<input type="text" class="search-input" id="searchInput" placeholder="Cari SPBU, jalan, parsil, rumah ibadah...">
|
||||
<button class="search-clear" id="searchClear">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Search Results -->
|
||||
<div id="searchResults" style="display: none; background: white; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-height: 200px; overflow-y: auto;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Zoom Control -->
|
||||
<div class="custom-zoom-control" style="position: absolute; top: 20px; left: 20px; z-index: 1000; display: flex; flex-direction: column; gap: 5px;">
|
||||
<button class="custom-layer-btn" id="zoomInBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom In"><i class="fas fa-plus"></i></button>
|
||||
<button class="custom-layer-btn" id="zoomOutBtn" style="position: relative; top: 0; left: 0; right: auto;" title="Zoom Out"><i class="fas fa-minus"></i></button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Toggle Button -->
|
||||
<button class="sidebar-toggle-btn" id="sidebarToggleBtn" title="Menu">
|
||||
<i class="fas fa-chevron-right" id="sidebarToggleIcon"></i>
|
||||
</button>
|
||||
|
||||
<!-- Left Sidebar Menu -->
|
||||
<nav class="left-sidebar" id="leftSidebar" style="display:none;">
|
||||
<div class="sidebar-section-label">Umum</div>
|
||||
<button class="sidebar-btn" id="menuSpbu" data-panel="spbu" title="SPBU">
|
||||
<span class="sidebar-emoji">⛽</span>
|
||||
<span class="sidebar-label">SPBU</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuJalan" data-panel="jalan" title="Jalan">
|
||||
<span class="sidebar-emoji">🛣️</span>
|
||||
<span class="sidebar-label">Jalan</span>
|
||||
</button>
|
||||
<button class="sidebar-btn" id="menuParsil" data-panel="parsil" title="Parsil Tanah">
|
||||
<span class="sidebar-emoji">🗺️</span>
|
||||
<span class="sidebar-label">Parsil</span>
|
||||
</button>
|
||||
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<!-- Custom Layer Control Button -->
|
||||
<button class="custom-layer-btn" id="layerBtn" title="Atur Layer">
|
||||
<i class="fas fa-layer-group fa-lg"></i>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<!-- Custom Layer Control Panel -->
|
||||
<div class="custom-layer-panel" id="layerPanel">
|
||||
<h3>Daftar Layer</h3>
|
||||
|
||||
<div class="layer-section-title">Infrastruktur & Lahan</div>
|
||||
|
||||
<!-- SPBU -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerSpbu" checked> SPBU
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subSpbu', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subSpbu" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="1" checked onchange="applySubFilter('spbu')"> 24 Jam
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-spbu" value="0" checked onchange="applySubFilter('spbu')"> Tidak 24 Jam
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jalan -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerJalan" checked> Jalan
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subJalan', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subJalan" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Nasional" checked onchange="applySubFilter('jalan')"> Nasional
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Provinsi" checked onchange="applySubFilter('jalan')"> Provinsi
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-jalan" value="Kabupaten" checked onchange="applySubFilter('jalan')"> Kabupaten
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parsil -->
|
||||
<div class="layer-group-item">
|
||||
<div class="layer-group-header">
|
||||
<label class="layer-option" style="margin:0; flex:1;">
|
||||
<input type="checkbox" id="layerParsil" checked> Parsil Tanah
|
||||
</label>
|
||||
<span class="layer-toggle-icon collapsed" onclick="toggleSubLayer('subParsil', this)"><i class="fas fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<div id="subParsil" class="sub-layer-list" style="display:none;">
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="SHM" checked onchange="applySubFilter('parsil')"> SHM
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGB" checked onchange="applySubFilter('parsil')"> HGB
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HGU" checked onchange="applySubFilter('parsil')"> HGU
|
||||
</label>
|
||||
<label class="layer-option sub-option">
|
||||
<input type="checkbox" class="sub-parsil" value="HP" checked onchange="applySubFilter('parsil')"> HP
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- GeoJSON Eksternal -->
|
||||
<div class="layer-group" style="margin-top: 10px; border-top: 1px solid #eee; padding-top: 10px;">
|
||||
<div class="layer-group-header" onclick="window.toggleGeoJsonMenu()" style="cursor:pointer; display:flex; justify-content:space-between; align-items:center; font-size:14px; margin-bottom:8px; font-weight: 500;">
|
||||
<span><i class="fas fa-folder"></i> GeoJSON Eksternal</span>
|
||||
<i id="geoJsonToggleIcon" class="fas fa-plus"></i>
|
||||
</div>
|
||||
<div id="geoJsonFileList" style="display:none; padding-left:15px; margin-bottom:10px;">
|
||||
<div id="geoJsonLayersContainer"></div>
|
||||
<div style="margin-top: 10px; padding-top: 10px; border-top: 1px dashed #ddd;">
|
||||
<label style="font-size: 12px; display:block; margin-bottom: 5px;">Import GeoJSON Baru:</label>
|
||||
<input type="file" id="fileGeoJson" accept=".geojson,.json" style="font-size: 12px; max-width: 100%;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /custom-layer-panel -->
|
||||
|
||||
<!-- Right Data Panel -->
|
||||
<div class="right-panel" id="rightPanel">
|
||||
<div class="right-panel-header">
|
||||
<h3 id="rightPanelTitle">Data</h3>
|
||||
<button class="right-panel-close" id="rightPanelClose"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="right-panel-actions" id="rightPanelActions"></div>
|
||||
<div class="right-panel-body" id="rightPanelBody">
|
||||
<div class="panel-empty">Pilih menu di kiri untuk menampilkan data</div>
|
||||
</div>
|
||||
</div><!-- /right-panel -->
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Map Container -->
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Unified Modal -->
|
||||
<div id="unifiedModal" class="unified-modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Judul Form</h3>
|
||||
<span class="modal-close" onclick="closeModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" id="modalBody">
|
||||
<!-- Konten dinamis -->
|
||||
</div>
|
||||
<div class="modal-footer" id="modalFooter">
|
||||
<button id="modalSaveBtn" class="btn-save">Simpan</button>
|
||||
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div id="confirmModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 300px;">
|
||||
<div class="modal-header">
|
||||
<h3>Konfirmasi</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p id="confirmMessage"></p>
|
||||
</div>
|
||||
<div class="modal-footer" style="display: flex; gap: 10px; justify-content: flex-end;">
|
||||
<button id="confirmYesBtn" class="btn-save" style="background-color: #dc3545;">Ya, Hapus</button>
|
||||
<button class="btn-cancel" onclick="closeConfirmModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log Bantuan Modal -->
|
||||
<div id="logBantuanModal" class="unified-modal">
|
||||
<div class="modal-content" style="width:500px; max-height:85vh; overflow:hidden; display:flex; flex-direction:column;">
|
||||
<div class="modal-header">
|
||||
<h3 id="logBantuanTitle">Log Bantuan</h3>
|
||||
<span class="modal-close" onclick="closeLogBantuanModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="flex:1; overflow-y:auto; gap:0; padding:0;">
|
||||
<!-- Riwayat -->
|
||||
<div style="padding:15px; border-bottom:1px solid #eee;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Riwayat Bantuan</h4>
|
||||
<div id="logBantuanList" style="max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:8px;">
|
||||
<div class="panel-empty" style="padding:20px;">Belum ada riwayat</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Form Tambah -->
|
||||
<div style="padding:15px;">
|
||||
<h4 style="font-size:13px; font-weight:600; color:#555; margin-bottom:10px;">Tambah Log Bantuan</h4>
|
||||
<div class="form-group">
|
||||
<label>Rumah Ibadah</label>
|
||||
<select id="logIbadahId"></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipe Bantuan</label>
|
||||
<select id="logTipeBantuan">
|
||||
<option value="Sembako">Sembako</option>
|
||||
<option value="Uang Tunai">Uang Tunai</option>
|
||||
<option value="Pakaian">Pakaian</option>
|
||||
<option value="Pendidikan">Pendidikan</option>
|
||||
<option value="Kesehatan">Kesehatan</option>
|
||||
<option value="Lainnya">Lainnya</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tanggal</label>
|
||||
<input type="date" id="logTanggal">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Keterangan (opsional)</label>
|
||||
<textarea id="logKeterangan" rows="2" placeholder="Catatan tambahan..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-save" id="logSaveBtn">Simpan Log</button>
|
||||
<button class="btn-cancel" onclick="closeLogBantuanModal()">Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Profile / Auth Control -->
|
||||
<div id="authWidget" class="auth-widget">
|
||||
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div id="loginModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 360px;">
|
||||
<div class="modal-header">
|
||||
<h3>Login Ke Sistem</h3>
|
||||
<span class="modal-close" id="closeLoginModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="loginUsername">Username</label>
|
||||
<input type="text" id="loginUsername" placeholder="Masukkan username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="loginPassword">Password</label>
|
||||
<input type="password" id="loginPassword" placeholder="Masukkan password">
|
||||
</div>
|
||||
<div id="loginErrorMsg" style="display:none; color:#ef4444; font-size:12px; font-weight:500;"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button id="loginSubmitBtn" class="btn-save" style="width: 100%;">Masuk</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User Management Modal -->
|
||||
<div id="userManagementModal" class="unified-modal">
|
||||
<div class="modal-content" style="width: 700px; max-width: 95%;">
|
||||
<div class="modal-header">
|
||||
<h3>Manajemen Pengguna (Admin)</h3>
|
||||
<span class="modal-close" id="closeUserManagementModal">×</span>
|
||||
</div>
|
||||
<div class="modal-body" style="gap: 15px;">
|
||||
<!-- Form Tambah/Edit User -->
|
||||
<div style="background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 8px; padding: 12px; display: flex; flex-direction: column; gap: 10px;">
|
||||
<h4 id="userFormTitle" style="font-size: 13px; font-weight:700; color:#334155; margin-bottom: 2px;">Tambah User Baru</h4>
|
||||
<input type="hidden" id="manageUserId" value="">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Username</label>
|
||||
<input type="text" id="manageUsername" placeholder="Username">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Password (Kosongkan jika tidak diubah)</label>
|
||||
<input type="password" id="managePassword" placeholder="Password">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
||||
<div class="form-group">
|
||||
<label>Role</label>
|
||||
<select id="manageRole">
|
||||
<option value="pengelola" selected>Pengelola Rumah Ibadah</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="manageIbadahGroup">
|
||||
<label>Rumah Ibadah yang Dikelola</label>
|
||||
<select id="manageIbadahId">
|
||||
<!-- Dikonstruksi dinamis dari data rumah ibadah -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 5px;">
|
||||
<button id="btnCancelUserEdit" class="btn-cancel" style="padding: 6px 12px; display:none;">Batal</button>
|
||||
<button id="btnSaveUser" class="btn-save" style="padding: 6px 16px;">Simpan User</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabel Daftar User -->
|
||||
<div style="max-height: 200px; overflow-y: auto; border: 1px solid #cbd5e1; border-radius: 8px;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px; text-align: left;">
|
||||
<thead>
|
||||
<tr style="background: #f1f5f9; border-bottom: 1px solid #cbd5e1; color:#475569; position: sticky; top: 0; z-index: 10;">
|
||||
<th style="padding: 10px 12px;">Username</th>
|
||||
<th style="padding: 10px 12px;">Role</th>
|
||||
<th style="padding: 10px 12px;">Rumah Ibadah</th>
|
||||
<th style="padding: 10px 12px; text-align: center;">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="userTableBody">
|
||||
<!-- Diisi dinamis -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div id="toastContainer"></div>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
|
||||
<!-- Leaflet Draw JS -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
|
||||
<!-- Leaflet TextPath JS (untuk teks diagonal) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-textpath@1.2.3/leaflet.textpath.min.js"></script>
|
||||
|
||||
<!-- Main Map Initialization -->
|
||||
<script src="assets/js/map.js?v=<?= time() ?>"></script>
|
||||
<!-- Fitur & Komponen -->
|
||||
<script src="assets/js/features/spbu.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/jalan.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/parsil.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/draw_control.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/features/geolocation.js?v=<?= time() ?>"></script>
|
||||
|
||||
<script src="assets/js/features/geojson.js?v=<?= time() ?>"></script>
|
||||
<script src="assets/js/panel.js?v=<?= time() ?>"></script>
|
||||
|
||||
<!-- Core Auth Feature -->
|
||||
<script src="assets/js/features/auth.js?v=<?= time() ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$query = "SELECT * FROM spbu";
|
||||
$result = $conn->query($query);
|
||||
|
||||
$spbu_arr = array();
|
||||
|
||||
if($result->num_rows > 0) {
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$spbu_item = array(
|
||||
"id" => $row['id'],
|
||||
"nama" => $row['nama'],
|
||||
"no_wa" => $row['no_wa'],
|
||||
"is_24_jam" => (bool)$row['is_24_jam'],
|
||||
"lat" => (float)$row['lat'],
|
||||
"lng" => (float)$row['lng']
|
||||
);
|
||||
array_push($spbu_arr, $spbu_item);
|
||||
}
|
||||
echo json_encode(["status" => "success", "data" => $spbu_arr]);
|
||||
} else {
|
||||
echo json_encode(["status" => "success", "data" => []]);
|
||||
}
|
||||
?>
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
require_once '../auth_helper.php';
|
||||
requireAdmin();
|
||||
include_once '../db_connect.php';
|
||||
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
|
||||
if(!empty($data->id) && !empty($data->lat) && !empty($data->lng)) {
|
||||
$id = (int)$data->id;
|
||||
$nama = $conn->real_escape_string($data->nama ?? '');
|
||||
$no_wa = $conn->real_escape_string($data->no_wa ?? '');
|
||||
$is_24_jam = isset($data->is_24_jam) ? (int)$data->is_24_jam : 0;
|
||||
$lat = (float)$data->lat;
|
||||
$lng = (float)$data->lng;
|
||||
|
||||
$query = "UPDATE spbu SET nama='$nama', no_wa='$no_wa', is_24_jam=$is_24_jam, lat=$lat, lng=$lng WHERE id=$id";
|
||||
|
||||
if($conn->query($query)) {
|
||||
echo json_encode(["status" => "success", "message" => "SPBU berhasil diupdate."]);
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Gagal update SPBU: " . $conn->error]);
|
||||
}
|
||||
} else {
|
||||
echo json_encode(["status" => "error", "message" => "Data tidak lengkap."]);
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user