Files
D1041231035_WebGis/map_spbu.html
T
2026-06-11 13:54:32 +07:00

470 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
<style>
/* Menggunakan style dasar yang sama dengan map_bantuan */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', Arial, sans-serif; height: 100vh; display: flex; flex-direction: column; background: #f0f2f5; }
header { background: linear-gradient(135deg, #e08020, #f59e42); color: #fff; padding: 10px 18px; display: flex; align-items: center; gap: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, .3); z-index: 1000; flex-shrink: 0; justify-content: space-between;}
header .title-area { display: flex; align-items: center; gap: 12px; }
header h1 { font-size: 1rem; font-weight: 700; }
header small { font-size: .7rem; opacity: .85; }
.back-btn { color: white; text-decoration: none; font-size: 0.9rem; border: 1px solid rgba(255,255,255,0.5); padding: 5px 10px; border-radius: 5px; transition: background 0.2s;}
.back-btn:hover { background: rgba(255,255,255,0.2); }
.app-body { display: flex; flex: 1; overflow: hidden; position: relative; }
#sidebar { width: 280px; background: #fff; display: flex; flex-direction: column; border-right: 1px solid #dde; z-index: 900; transition: transform .3s; flex-shrink: 0; }
#sidebar.hide { transform: translateX(-100%); position: absolute; top: 0; left: 0; height: 100%; box-shadow: 4px 0 12px rgba(0, 0, 0, .2); }
.sb-head { background: #e08020; color: #fff; padding: 10px 14px; font-weight: 700; font-size: .88rem; }
.filter-box { padding: 10px 14px; border-bottom: 1px solid #eee; background: #fff3e0; }
.filter-box label { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; font-weight: 600; color: #e08020; cursor: pointer; }
.filter-box input[type="checkbox"] { accent-color: #e08020; width: 16px; height: 16px; cursor: pointer;}
.btn-row { padding: 10px 14px; border-bottom: 1px solid #eee; }
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer; font-size: .8rem; font-weight: 700; transition: filter .15s; width: 100%; color: white;}
.btn:hover { filter: brightness(.9); }
.btn-orange { background: #e08020; }
.btn-green { background: #1a6b3c; }
.btn-gray { background: #e0e0e0; color: #333; }
.btn-red { background: #d93025; }
.sb-search { padding: 7px 14px; border-bottom: 1px solid #eee; }
.sb-search input { width: 100%; padding: 7px 10px; border: 1px solid #ccc; border-radius: 6px; font-size: .82rem; }
#sb-list { overflow-y: auto; flex: 1; }
.list-item { padding: 10px 14px; border-bottom: 1px solid #f0f0f0; cursor: pointer; display: flex; align-items: center; gap: 9px; transition: background .15s; }
.list-item:hover { background: #fff8f0; }
.list-item .ico { font-size: 1.3rem; width: 26px; text-align: center; }
.list-item .inf { flex: 1; }
.list-item .inf .nm { font-weight: 700; font-size: .84rem; }
.list-item .inf .jn { font-size: .7rem; color: #777; margin-top: 2px;}
.badge { font-size: .65rem; padding: 2px 6px; border-radius: 10px; font-weight: 700; white-space: nowrap; }
.badge.green { background: #e8f5e9; color: #1a6b3c; }
.badge.red { background: #fdecea; color: #d93025; }
.empty { padding: 16px; text-align: center; color: #bbb; font-size: .82rem; }
#map { flex: 1; z-index: 1; }
#btn-toggle { position: absolute; top: 10px; left: 0; z-index: 950; background: #e08020; color: #fff; border: none; padding: 8px 7px; border-radius: 0 6px 6px 0; cursor: pointer; font-size: 1rem; box-shadow: 2px 0 6px rgba(0, 0, 0, .25); }
/* Modal */
.overlay { display: none; position: fixed; inset: 0; background: rgba(0, 0, 0, .5); z-index: 2000; align-items: center; justify-content: center; }
.overlay.on { display: flex; }
.modal { background: #fff; border-radius: 12px; width: 92%; max-width: 450px; max-height: 92vh; overflow-y: auto; padding: 22px; box-shadow: 0 8px 32px rgba(0, 0, 0, .25); }
.modal h2 { font-size: 1rem; color: #e08020; margin-bottom: 14px; border-bottom: 2px solid #fff3e0; padding-bottom: 8px; }
.fg { margin-bottom: 12px; }
.fg label { display: block; font-size: .78rem; font-weight: 700; color: #444; margin-bottom: 4px; }
.fg input, .fg select { width: 100%; padding: 8px 11px; border: 1px solid #ccc; border-radius: 6px; font-size: .84rem; }
.fg input:focus, .fg select:focus { border-color: #e08020; outline: none; box-shadow: 0 0 0 3px rgba(224, 128, 32, .1); }
.fg-row { display: flex; gap: 8px; }
.fg-row .fg { flex: 1; }
.fg-hint { font-size: .7rem; color: #aaa; margin-top: 3px; }
.err { color: #d93025; font-size: .75rem; margin-top: 3px; display: none; }
.err.on { display: block; }
.modal-footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 14px; }
.btn-petaclick { background: #fff3e0; color: #e08020; border: 1px dashed #e08020; width: 100%; margin-top: 4px; }
.btn-petaclick.active { background: #e08020; color: #fff; }
#toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); background: #323232; color: #fff; padding: 9px 20px; border-radius: 8px; font-size: .84rem; z-index: 3000; opacity: 0; transition: opacity .3s; pointer-events: none; }
#toast.on { opacity: 1; }
#toast.ok { background: #1a6b3c; }
#toast.err { background: #d93025; }
.legenda { background: rgba(255, 255, 255, .95); padding: 9px 13px; border-radius: 8px; font-size: .77rem; box-shadow: 0 2px 8px rgba(0, 0, 0, .15); }
.legenda h4 { font-size: .78rem; margin-bottom: 5px; }
.leg-row { display: flex; align-items: center; gap: 7px; margin-top: 3px; }
.dot { width: 13px; height: 13px; border-radius: 50%; display: inline-block; border: 2px solid rgba(0, 0, 0, .15); }
</style>
</head>
<body>
<header>
<div class="title-area">
<i class="fa-solid fa-gas-pump fa-lg"></i>
<div>
<h1>WebGIS Lokasi SPBU</h1>
<small>Informasi Operasional & Pemetaan SPBU</small>
</div>
</div>
<a href="index.html" class="back-btn"><i class="fa-solid fa-arrow-left"></i> Kembali ke Dashboard</a>
</header>
<div class="app-body">
<div id="sidebar">
<div class="sb-head"><i class="fa-solid fa-list-ul"></i> Daftar SPBU</div>
<div class="filter-box">
<label>
<input type="checkbox" id="filter-24j" onchange="loadSPBU()">
Tampilkan hanya SPBU 24 Jam
</label>
</div>
<div class="btn-row">
<button class="btn btn-orange" onclick="bukaForm()">
<i class="fa-solid fa-plus"></i> Tambah SPBU
</button>
</div>
<div class="sb-search">
<input type="text" id="cari-input" placeholder="🔍 Cari SPBU..." oninput="filterList(this.value)">
</div>
<div id="sb-list">
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
</div>
</div>
<div id="map"></div>
<button id="btn-toggle" onclick="toggleSidebar()"><i class="fa-solid fa-bars"></i></button>
</div>
<!-- Modal Form -->
<div class="overlay" id="ovl-form">
<div class="modal">
<h2 id="form-title"><i class="fa-solid fa-gas-pump"></i> Tambah SPBU</h2>
<input type="hidden" id="f-id">
<div class="fg">
<label>Nama SPBU <span style="color:red">*</span></label>
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pertamina 14.213.11">
<div class="err" id="err-nama">Nama wajib diisi.</div>
</div>
<div class="fg-row">
<div class="fg">
<label>Buka 24 Jam?</label>
<select id="f-buka">
<option value="1">Ya, 24 Jam</option>
<option value="0" selected>Tidak</option>
</select>
</div>
<div class="fg">
<label>Kontak / Telp</label>
<input type="text" id="f-kontak" placeholder="0812...">
</div>
</div>
<div class="fg-row">
<div class="fg">
<label>Latitude <span style="color:red">*</span></label>
<input type="number" id="f-lat" step="any" placeholder="-0.0354">
</div>
<div class="fg">
<label>Longitude <span style="color:red">*</span></label>
<input type="number" id="f-lng" step="any" placeholder="109.3424">
</div>
</div>
<div class="err" id="err-coord">Koordinat tidak valid.</div>
<button class="btn btn-petaclick" id="btn-pc" onclick="aktifKlik()">
<i class="fa-solid fa-map-pin"></i> Pilih Koordinat dari Peta
</button>
<p class="fg-hint">Tutup form lalu klik langsung di peta untuk memilih koordinat.</p>
<div class="modal-footer">
<button class="btn btn-gray" style="width:auto" onclick="tutupForm()">Batal</button>
<button class="btn btn-orange" style="width:auto" onclick="simpan()"><i class="fa-solid fa-floppy-disk"></i> Simpan</button>
</div>
</div>
</div>
<div id="toast">Notifikasi</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API_URL = 'api.php?resource=spbu';
let map, markersLayer;
let modeKlik = false;
let dataGlobal = [];
// Icons
const iconGreen = 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 iconRed = 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]
});
function initMap() {
// Default center Pontianak (menyesuaikan koordinat map_bantuan)
map = L.map('map').setView([-0.0380, 109.3440], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap'
}).addTo(map);
markersLayer = L.layerGroup().addTo(map);
map.on('click', (e) => {
if (!modeKlik) return;
document.getElementById('f-lat').value = e.latlng.lat.toFixed(6);
document.getElementById('f-lng').value = e.latlng.lng.toFixed(6);
// Matikan mode klik
modeKlik = false;
document.getElementById('btn-pc').classList.remove('active');
document.getElementById('btn-pc').innerHTML = '<i class="fa-solid fa-map-pin"></i> Pilih Koordinat dari Peta';
// Buka form lagi
document.getElementById('ovl-form').classList.add('on');
});
// Legenda
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function () {
const div = L.DomUtil.create('div', 'legenda');
div.innerHTML = `
<h4>Legenda SPBU</h4>
<div class="leg-row"><div class="dot" style="background:#28a745"></div> 24 Jam</div>
<div class="leg-row"><div class="dot" style="background:#dc3545"></div> Tidak 24 Jam</div>
`;
return div;
};
legend.addTo(map);
}
async function loadSPBU() {
try {
const res = await fetch(`${API_URL}&action=list`);
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Gagal memuat data');
dataGlobal = data;
renderData();
} catch (e) {
showToast(e.message, true);
document.getElementById('sb-list').innerHTML = `<div class="empty">Gagal memuat data.</div>`;
}
}
function renderData() {
markersLayer.clearLayers();
const list = document.getElementById('sb-list');
const q = document.getElementById('cari-input').value.toLowerCase();
const filter24 = document.getElementById('filter-24j').checked;
let html = '';
let count = 0;
dataGlobal.forEach(d => {
// Filter 24 Jam
if (filter24 && d.buka_24_jam != 1) return;
// Filter Pencarian
if (q && !d.nama.toLowerCase().includes(q)) return;
count++;
const is24 = d.buka_24_jam == 1;
// List HTML
html += `
<div class="list-item" onclick="flyTo(${d.latitude}, ${d.longitude})">
<div class="ico" style="color: ${is24 ? '#1a6b3c' : '#d93025'}"><i class="fa-solid fa-gas-pump"></i></div>
<div class="inf">
<div class="nm">${d.nama}</div>
<div class="jn"><i class="fa-solid fa-phone"></i> ${d.kontak || '-'}</div>
</div>
<div class="badge ${is24 ? 'green' : 'red'}">${is24 ? '24 Jam' : 'Non-24 Jam'}</div>
</div>
`;
// Map Marker (Make it draggable)
const marker = L.marker([d.latitude, d.longitude], {
icon: is24 ? iconGreen : iconRed,
draggable: true // FITUR DRAG AND DROP
});
marker.bindPopup(`
<div style="font-family:'Segoe UI',Arial; min-width:200px">
<h4 style="margin:0 0 5px; color:#e08020; font-size:14px;"><i class="fa-solid fa-gas-pump"></i> ${d.nama}</h4>
<div style="font-size:12px; margin-bottom:10px;">
<span class="badge ${is24 ? 'green' : 'red'}" style="display:inline-block; margin-bottom:5px;">${is24 ? '24 Jam' : 'Non-24 Jam'}</span><br>
<i class="fa-solid fa-phone"></i> Kontak: <b>${d.kontak || '-'}</b>
</div>
<div style="display:flex; gap:5px; padding-top:8px; border-top:1px solid #eee">
<button class="btn btn-orange btn-sm" onclick="editData(${d.id})" style="flex:1; padding:4px"><i class="fa-solid fa-pen"></i> Edit</button>
<button class="btn btn-red btn-sm" onclick="hapusData(${d.id})" style="padding:4px 8px"><i class="fa-solid fa-trash"></i></button>
</div>
<div style="font-size:10px; color:#999; margin-top:8px; text-align:center;">
<i class="fa-solid fa-arrows-up-down-left-right"></i> Drag marker untuk memindahkan
</div>
</div>
`);
// Event ketika marker selesai di-drag
marker.on('dragend', function(event) {
const newLatLng = event.target.getLatLng();
updatePosisiSPBU(d.id, newLatLng.lat, newLatLng.lng);
});
markersLayer.addLayer(marker);
});
if (count === 0) {
html = `<div class="empty">Tidak ada SPBU ditemukan.</div>`;
}
list.innerHTML = html;
}
async function updatePosisiSPBU(id, lat, lng) {
try {
const res = await fetch(`${API_URL}&action=update&id=${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ latitude: lat, longitude: lng })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showToast('Posisi SPBU diperbarui.');
// Update data lokal
const item = dataGlobal.find(x => x.id == id);
if (item) {
item.latitude = lat;
item.longitude = lng;
}
} catch (e) {
showToast('Gagal memindahkan marker: ' + e.message, true);
loadSPBU(); // Kembalikan ke posisi awal jika gagal
}
}
function filterList() { renderData(); }
function flyTo(lat, lng) { map.flyTo([lat, lng], 16); }
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('hide');
}
// --- FORM LOGIC ---
function bukaForm() {
document.getElementById('form-title').innerHTML = '<i class="fa-solid fa-gas-pump"></i> Tambah SPBU';
document.getElementById('f-id').value = '';
document.getElementById('f-nama').value = '';
document.getElementById('f-buka').value = '0';
document.getElementById('f-kontak').value = '';
document.getElementById('f-lat').value = '';
document.getElementById('f-lng').value = '';
document.getElementById('err-nama').classList.remove('on');
document.getElementById('err-coord').classList.remove('on');
document.getElementById('ovl-form').classList.add('on');
}
function tutupForm() {
document.getElementById('ovl-form').classList.remove('on');
modeKlik = false;
}
function editData(id) {
const d = dataGlobal.find(x => x.id == id);
if (!d) return;
map.closePopup();
document.getElementById('form-title').innerHTML = '<i class="fa-solid fa-pen"></i> Edit SPBU';
document.getElementById('f-id').value = d.id;
document.getElementById('f-nama').value = d.nama;
document.getElementById('f-buka').value = d.buka_24_jam;
document.getElementById('f-kontak').value = d.kontak;
document.getElementById('f-lat').value = d.latitude;
document.getElementById('f-lng').value = d.longitude;
document.getElementById('err-nama').classList.remove('on');
document.getElementById('err-coord').classList.remove('on');
document.getElementById('ovl-form').classList.add('on');
}
function aktifKlik() {
modeKlik = true;
document.getElementById('btn-pc').classList.add('active');
document.getElementById('btn-pc').innerHTML = '<i class="fa-solid fa-crosshairs"></i> Menunggu Klik di Peta...';
document.getElementById('ovl-form').classList.remove('on'); // Sembunyikan form sementara
showToast('Klik di area peta untuk menentukan koordinat SPBU.');
}
async function simpan() {
const id = document.getElementById('f-id').value;
const payload = {
nama: document.getElementById('f-nama').value,
buka_24_jam: document.getElementById('f-buka').value,
kontak: document.getElementById('f-kontak').value,
latitude: document.getElementById('f-lat').value,
longitude: document.getElementById('f-lng').value
};
let valid = true;
if (!payload.nama) { document.getElementById('err-nama').classList.add('on'); valid = false; }
else { document.getElementById('err-nama').classList.remove('on'); }
if (!payload.latitude || !payload.longitude) { document.getElementById('err-coord').classList.add('on'); valid = false; }
else { document.getElementById('err-coord').classList.remove('on'); }
if (!valid) return;
const url = id ? `${API_URL}&action=update&id=${id}` : `${API_URL}&action=create`;
const method = id ? 'PUT' : 'POST';
try {
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showToast(data.message);
tutupForm();
loadSPBU();
} catch (e) {
showToast(e.message, true);
}
}
async function hapusData(id) {
if (!confirm('Hapus SPBU ini?')) return;
try {
const res = await fetch(`${API_URL}&action=delete&id=${id}`, { method: 'DELETE' });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
showToast(data.message);
map.closePopup();
loadSPBU();
} catch (e) {
showToast(e.message, true);
}
}
function showToast(msg, isErr = false) {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = 'on ' + (isErr ? 'err' : 'ok');
setTimeout(() => t.className = '', 3000);
}
// Init
initMap();
loadSPBU();
</script>
</body>
</html>