470 lines
22 KiB
HTML
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: '© 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>
|