Files
2026-06-10 19:36:52 +07:00

479 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Peta SPBU</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
:root {
--bg: #0b1220;
--panel: rgba(255, 255, 255, 0.95);
--text: #0f172a;
--muted: #64748b;
--border: rgba(15, 23, 42, 0.12);
--primary: #2563eb;
--danger: #dc3545;
--warning: #ffc107;
--success: #16a34a;
}
body { margin: 0; padding: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
#map { height: 100vh; width: 100vw; }
form { display: flex; flex-direction: column; gap: 8px; width: 220px; }
input, select {
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 10px;
font: inherit;
}
input:focus, select:focus {
outline: 2px solid rgba(37, 99, 235, 0.25);
outline-offset: 1px;
}
.leaflet-popup-content-wrapper { border-radius: 14px; }
.leaflet-popup-content { margin: 12px 14px; }
.leaflet-control-layers {
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.10);
}
.toolbar {
position: fixed;
top: 12px;
left: 12px;
z-index: 2500;
display: flex;
align-items: center;
gap: 8px;
padding: 10px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--panel);
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.18);
backdrop-filter: blur(8px);
}
.toolbar-title {
font-weight: 700;
color: var(--text);
margin-right: 6px;
letter-spacing: 0.2px;
user-select: none;
}
.hint {
position: fixed;
top: 62px;
left: 12px;
z-index: 2500;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid var(--border);
background: var(--panel);
color: var(--muted);
font-size: 13px;
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.10);
}
.btn {
appearance: none;
border: 1px solid var(--border);
background: white;
color: var(--text);
font-weight: 600;
padding: 8px 10px;
border-radius: 10px;
cursor: pointer;
line-height: 1;
}
.btn:hover { filter: brightness(0.98); }
.btn:active { transform: translateY(1px); }
.btn-primary { background: var(--primary); color: white; border-color: rgba(37, 99, 235, 0.25); }
.btn-ghost { background: white; }
.btn-toggle[aria-pressed="true"] { background: #0f172a; color: white; border-color: rgba(15, 23, 42, 0.25); }
/* Modal Styles */
#editModal {
display: none; position: fixed; top: 50%; left: 50%;
transform: translate(-50%, -50%); z-index: 2000;
background: white; padding: 20px; border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.3); width: 260px;
}
#modalOverlay {
display: none; position: fixed; top: 0; left: 0;
width: 100%; height: 100%; background: rgba(0,0,0,0.5);
z-index: 1500;
}
.btn-group { display: flex; justify-content: space-between; margin-top: 10px; }
.btn-action { padding: 6px 10px; border: none; cursor: pointer; border-radius: 8px; font-weight: 600; }
.btn-edit { background: var(--warning); }
.btn-delete { background: var(--danger); color: white; }
</style>
</head>
<body>
<div class="toolbar" role="region" aria-label="Kontrol peta">
<div class="toolbar-title">Peta SPBU</div>
<button id="btnAdd" type="button" class="btn btn-primary">Tambah titik baru</button>
<button id="btnMove" type="button" class="btn btn-ghost btn-toggle" aria-pressed="false">Aktifkan pindah</button>
</div>
<div id="hint" class="hint" hidden>Klik pada peta untuk menambahkan titik baru.</div>
<div id="map"></div>
<div id="modalOverlay" onclick="closeModal()"></div>
<div id="editModal">
<h3 style="margin-top:0;">Ubah SPBU</h3>
<form id="editForm" onsubmit="updateData(event)">
<input type="hidden" id="editId">
<input type="text" id="editNama" required>
<input type="text" id="editNomor" required>
<input type="text" id="editLat" readonly>
<input type="text" id="editLng" readonly>
<div style="font-size:12px; color:#6c757d; margin-top:-6px;">
Lintang &amp; bujur akan berubah saat titik dipindah (mode Pindah aktif).
</div>
<select id="editStatus" required>
<option value="Buka 24 Jam">Buka 24 Jam</option>
<option value="Tidak">Tidak</option>
</select>
<div class="btn-group">
<button type="button" class="btn-action" onclick="closeModal()">Batal</button>
<button type="submit" class="btn-action" style="background:#28a745; color:white;">Simpan</button>
</div>
</form>
</div>
<script>
const map = L.map('map').setView([-0.055, 109.34], 13);
// Base map layers
const baseOSM = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap'
});
const baseOSMHOT = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
maxZoom: 19,
subdomains: 'abc',
attribution: '&copy; OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France'
});
const baseTopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
maxZoom: 17,
subdomains: 'abc',
attribution: 'Map data: &copy; OpenStreetMap contributors, SRTM | Map style: &copy; OpenTopoMap (CC-BY-SA)'
});
// Default base layer
baseOSM.addTo(map);
const popup = L.popup();
// Define Green and Red Icons
const greenIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
// Layers for filtering via Leaflet Layers Control
const layer24 = L.layerGroup().addTo(map);
const layerNot = L.layerGroup().addTo(map);
const baseLayers = {
'Peta Jalan (OSM)': baseOSM,
'Peta Jalan (HOT)': baseOSMHOT,
'Peta Topografi': baseTopo
};
const overlayLayers = {
'Buka 24 Jam': layer24,
'Tidak': layerNot
};
L.control.layers(baseLayers, overlayLayers, { collapsed: false }).addTo(map);
let isAddMode = false;
let isMoveMode = false;
function setAddMode(enabled) {
isAddMode = enabled;
document.getElementById('hint').hidden = !enabled;
const btn = document.getElementById('btnAdd');
btn.textContent = enabled ? 'Klik peta untuk menaruh…' : 'Tambah titik baru';
}
function setMoveMode(enabled) {
isMoveMode = enabled;
const btn = document.getElementById('btnMove');
btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
btn.textContent = enabled ? 'Matikan pindah' : 'Aktifkan pindah';
const toggle = (marker) => {
if (!marker || !marker.dragging) return;
if (enabled) marker.dragging.enable();
else marker.dragging.disable();
};
layer24.eachLayer(toggle);
layerNot.eachLayer(toggle);
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function buildPopupContent(spbu) {
return `
<b>${escapeHtml(spbu.nama)}</b><br>
Nomor: ${escapeHtml(spbu.nomor)}<br>
Status: ${escapeHtml(spbu.status)}<br>
Lintang: ${escapeHtml(spbu.Latitude)}<br>
Bujur: ${escapeHtml(spbu.Longitude)}<br>
<hr style="margin: 8px 0;">
<button type="button" class="btn-action btn-edit js-edit">Ubah</button>
<button type="button" class="btn-action btn-delete js-delete">Hapus</button>
`;
}
function updateCoordinates(id, latitude, longitude) {
const formData = new FormData();
formData.append('id', id);
formData.append('latitude', latitude);
formData.append('longitude', longitude);
return fetch('update_spbu.php', { method: 'POST', body: formData })
.then(r => r.json());
}
function refreshMarkers() {
return fetch('get_spbu.php')
.then(response => response.json())
.then(data => {
map.closePopup();
layer24.clearLayers();
layerNot.clearLayers();
const rows = Array.isArray(data) ? data : [];
rows.forEach(spbu => {
const is24 = spbu.status === 'Buka 24 Jam';
const markerIcon = is24 ? greenIcon : redIcon;
const lat = parseFloat(spbu.Latitude);
const lng = parseFloat(spbu.Longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
const popupContent = buildPopupContent(spbu);
const marker = L.marker([lat, lng], { icon: markerIcon, draggable: true });
if (!isMoveMode && marker.dragging) marker.dragging.disable();
marker.__spbu = spbu;
marker.__oldLatLng = null;
marker.bindPopup(popupContent);
marker.on('dragstart', function(ev) {
const m = ev.target;
m.closePopup();
m.__oldLatLng = m.getLatLng();
});
marker.on('dragend', function(ev) {
const m = ev.target;
const oldLatLng = m.__oldLatLng;
const current = m.getLatLng();
const newLat = current.lat.toFixed(8);
const newLng = current.lng.toFixed(8);
const spbuRef = m.__spbu;
updateCoordinates(spbuRef.id, newLat, newLng)
.then(res => {
if (res && res.status === 'success') {
spbuRef.Latitude = newLat;
spbuRef.Longitude = newLng;
m.setPopupContent(buildPopupContent(spbuRef));
return;
}
if (oldLatLng) m.setLatLng(oldLatLng);
console.error('Move failed:', res);
})
.catch(err => {
if (oldLatLng) m.setLatLng(oldLatLng);
console.error('Move error:', err);
});
});
marker.addTo(is24 ? layer24 : layerNot);
});
})
.catch(error => console.error('Error fetching data:', error));
}
// Wire popup buttons (avoids inline onclick bugs)
map.on('popupopen', function(e) {
const source = e.popup._source;
const spbu = source && source.__spbu;
if (!spbu) return;
const el = e.popup.getElement();
if (!el) return;
const editBtn = el.querySelector('.js-edit');
const deleteBtn = el.querySelector('.js-delete');
if (editBtn) {
editBtn.addEventListener('click', function(ev) {
ev.preventDefault();
openModal(spbu);
});
}
if (deleteBtn) {
deleteBtn.addEventListener('click', function(ev) {
ev.preventDefault();
deleteData(spbu.id);
});
}
});
// Toolbar
document.getElementById('btnAdd').addEventListener('click', function() {
setAddMode(!isAddMode);
});
document.getElementById('btnMove').addEventListener('click', function() {
setMoveMode(!isMoveMode);
});
// Handle Map Click (add SPBU only when Create mode enabled)
map.on('click', function(e) {
if (!isAddMode) return;
const lat = e.latlng.lat.toFixed(8);
const lng = e.latlng.lng.toFixed(8);
const formHTML = `
<form id="spbuForm" onsubmit="saveData(event)">
<h4 style="margin: 0 0 10px 0;">Tambah SPBU</h4>
<input type="text" id="nama" placeholder="Nama SPBU" required>
<input type="text" id="nomor" placeholder="Nomor SPBU" required>
<select id="status" required>
<option value="Buka 24 Jam">Buka 24 Jam</option>
<option value="Tidak">Tidak</option>
</select>
<div>Lintang: ${lat}</div>
<div>Bujur: ${lng}</div>
<input type="hidden" id="lat" value="${lat}">
<input type="hidden" id="lng" value="${lng}">
<button type="submit" style="background:#28a745; color:white; border:none; padding:5px;">Simpan</button>
</form>
`;
popup.setLatLng(e.latlng).setContent(formHTML).openOn(map);
// One-shot add: after placing the form, leave add mode.
setAddMode(false);
});
// Save (create)
function saveData(event) {
event.preventDefault();
const formData = new FormData();
formData.append('nama', document.getElementById('nama').value);
formData.append('nomor', document.getElementById('nomor').value);
formData.append('status', document.getElementById('status').value);
formData.append('latitude', document.getElementById('lat').value);
formData.append('longitude', document.getElementById('lng').value);
fetch('save_spbu.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
map.closePopup();
return refreshMarkers();
}
console.error('Save failed:', data);
})
.catch(error => console.error('Error:', error));
}
// Delete
function deleteData(id) {
if (!confirm("Yakin ingin menghapus SPBU ini?")) return;
const formData = new FormData();
formData.append('id', id);
fetch('delete_spbu.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
map.closePopup();
return refreshMarkers();
}
console.error('Delete failed:', data);
})
.catch(error => console.error('Error:', error));
}
// Modal
function openModal(spbu) {
document.getElementById('editId').value = spbu.id;
document.getElementById('editNama').value = spbu.nama;
document.getElementById('editNomor').value = spbu.nomor;
document.getElementById('editStatus').value = spbu.status;
document.getElementById('editLat').value = spbu.Latitude;
document.getElementById('editLng').value = spbu.Longitude;
document.getElementById('modalOverlay').style.display = 'block';
document.getElementById('editModal').style.display = 'block';
}
function closeModal() {
document.getElementById('modalOverlay').style.display = 'none';
document.getElementById('editModal').style.display = 'none';
}
// Update
function updateData(event) {
event.preventDefault();
const formData = new FormData();
formData.append('id', document.getElementById('editId').value);
formData.append('nama', document.getElementById('editNama').value);
formData.append('nomor', document.getElementById('editNomor').value);
formData.append('status', document.getElementById('editStatus').value);
fetch('update_spbu.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
closeModal();
return refreshMarkers();
}
console.error('Update failed:', data);
})
.catch(error => console.error('Error:', error));
}
// Initial load
refreshMarkers();
</script>
</body>
</html>