794 lines
33 KiB
PHP
794 lines
33 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebGIS SPBU — Layer Groups</title>
|
|
|
|
<!-- Leaflet CSS (tanpa integrity dan crossorigin) -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
|
|
<style>
|
|
/* ── Reset ────────────────────────────────────────────────── */
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html { height: 100%; }
|
|
body {
|
|
height: 100%;
|
|
font-family: sans-serif;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
/* ── Header ──────────────────────────────────────────────── */
|
|
#header {
|
|
background: #1a56db;
|
|
color: white;
|
|
padding: 0 20px;
|
|
height: 50px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-shrink: 0;
|
|
z-index: 1000;
|
|
}
|
|
#header h1 { font-size: 1rem; font-weight: 700; }
|
|
#header span { font-size: 0.75rem; opacity: 0.85; }
|
|
|
|
/* ── Peta (tinggi wajib sesuai spec) ─────────────────────── */
|
|
#map {
|
|
width: 100%;
|
|
height: calc(100vh - 50px);
|
|
flex: 1;
|
|
}
|
|
|
|
/* ── Toast notification ──────────────────────────────────── */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(60px);
|
|
background: #1f2937;
|
|
color: white;
|
|
padding: 8px 20px;
|
|
border-radius: 20px;
|
|
font-size: 0.85rem;
|
|
z-index: 9999;
|
|
opacity: 0;
|
|
transition: all 0.3s;
|
|
pointer-events: none;
|
|
}
|
|
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
#toast.ok { background: #057a55; }
|
|
#toast.err { background: #e02424; }
|
|
|
|
/* ── Loading spinner ─────────────────────────────────────── */
|
|
#loading {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(255,255,255,0.5);
|
|
z-index: 8000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
#loading.show { display: flex; }
|
|
.spin {
|
|
width: 36px; height: 36px;
|
|
border: 4px solid #ddd;
|
|
border-top-color: #1a56db;
|
|
border-radius: 50%;
|
|
animation: sp 0.7s linear infinite;
|
|
}
|
|
@keyframes sp { to { transform: rotate(360deg); } }
|
|
|
|
/* ── Popup Leaflet ───────────────────────────────────────── */
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 10px !important;
|
|
padding: 0 !important;
|
|
overflow: hidden;
|
|
min-width: 270px;
|
|
}
|
|
.leaflet-popup-content { margin: 0 !important; }
|
|
|
|
/* Header popup */
|
|
.ph {
|
|
background: #1a56db;
|
|
color: white;
|
|
padding: 10px 14px;
|
|
font-weight: 700;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* Body popup */
|
|
.pb { padding: 12px 14px; background: white; }
|
|
|
|
/* Tabel detail */
|
|
table.dt {
|
|
width: 100%; font-size: 0.82rem;
|
|
border-collapse: collapse; margin-bottom: 10px;
|
|
}
|
|
table.dt td { padding: 4px 3px; vertical-align: top; }
|
|
table.dt td:first-child { color: #6b7280; width: 110px; }
|
|
table.dt td:last-child { font-weight: 600; }
|
|
|
|
/* Form group */
|
|
.fg { margin-bottom: 9px; }
|
|
.fg label {
|
|
display: block; font-size: 0.75rem; font-weight: 700;
|
|
color: #6b7280; text-transform: uppercase;
|
|
letter-spacing: .04em; margin-bottom: 3px;
|
|
}
|
|
.fg input, .fg select {
|
|
width: 100%; padding: 6px 9px;
|
|
border: 1.5px solid #d1d9e0; border-radius: 6px;
|
|
font-size: 0.84rem; outline: none;
|
|
}
|
|
.fg input:focus, .fg select:focus { border-color: #1a56db; }
|
|
|
|
/* Button row */
|
|
.br { display: flex; gap: 7px; margin-top: 10px; }
|
|
.btn {
|
|
flex: 1; padding: 7px 10px; border: none;
|
|
border-radius: 6px; font-size: 0.82rem;
|
|
font-weight: 600; cursor: pointer;
|
|
}
|
|
.bp { background: #1a56db; color: white; }
|
|
.bd { background: #e02424; color: white; }
|
|
.bs { background: #f1f5f9; color: #333; border: 1px solid #ddd; }
|
|
.bg { background: #057a55; color: white; }
|
|
|
|
/* Kotak koordinat */
|
|
.coord-box {
|
|
background: #f0f9ff; border: 1px solid #bae6fd;
|
|
border-radius: 6px; padding: 6px 10px;
|
|
font-size: 0.78rem; color: #0369a1; margin-bottom: 9px;
|
|
}
|
|
|
|
/* ── Legend ──────────────────────────────────────────────── */
|
|
.legend {
|
|
background: white; border-radius: 8px;
|
|
padding: 10px 12px; font-size: 0.77rem;
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.15);
|
|
}
|
|
.legend b { display: block; margin-bottom: 6px; font-size: 0.8rem; }
|
|
.li { display: flex; align-items: center; gap: 7px; margin-bottom: 4px; }
|
|
.dot {
|
|
width: 13px; height: 13px; border-radius: 50%;
|
|
border: 2px solid rgba(0,0,0,0.2); flex-shrink: 0;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="header">
|
|
<h1>⛽ WebGIS SPBU — Layer Groups & Layers Control</h1>
|
|
<span>💡 Klik lokasi pada peta untuk menambahkan SPBU</span>
|
|
</div>
|
|
|
|
<div id="map"></div>
|
|
<div id="toast"></div>
|
|
<div id="loading"><div class="spin"></div></div>
|
|
|
|
<!-- Leaflet JS (tanpa integrity dan crossorigin) -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
|
|
<script>
|
|
// ════════════════════════════════════════════════════════════════
|
|
// CEK LEAFLET SEBELUM INISIALISASI
|
|
// Pastikan library Leaflet berhasil dimuat dari CDN
|
|
// ════════════════════════════════════════════════════════════════
|
|
if (typeof L === 'undefined') {
|
|
document.getElementById('map').innerHTML =
|
|
'<p style="padding:20px;color:red;">❌ Leaflet gagal dimuat. Periksa koneksi internet.</p>';
|
|
throw new Error('Leaflet not loaded');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// VARIABEL GLOBAL
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
// markersMap menyimpan referensi marker dengan key = id SPBU
|
|
// Setiap entry: { marker: L.Marker, layer: layerGroup }
|
|
// Menyimpan layer asal memudahkan pemindahan saat edit buka_24_jam berubah
|
|
const markersMap = {};
|
|
|
|
// Popup sementara untuk form tambah data
|
|
let tempPopup = null;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// INISIALISASI PETA
|
|
// Koordinat awal: sekitar Pontianak, Kalimantan Barat
|
|
// ════════════════════════════════════════════════════════════════
|
|
const map = L.map('map').setView([-0.05566422376778593, 109.34854622732368], 17);
|
|
|
|
// Tile layer OpenStreetMap
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
}).addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PEMBUATAN LAYER GROUPS
|
|
// Dua layer group untuk memisahkan SPBU berdasarkan jam operasi
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
// Layer group untuk SPBU yang buka 24 jam
|
|
const layerSPBU24Jam = L.layerGroup();
|
|
|
|
// Layer group untuk SPBU yang tidak buka 24 jam
|
|
const layerSPBUTidak24Jam = L.layerGroup();
|
|
|
|
// Tambahkan kedua layer ke peta secara default (langsung tampil saat halaman dibuka)
|
|
layerSPBU24Jam.addTo(map);
|
|
layerSPBUTidak24Jam.addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PEMBUATAN LAYERS CONTROL (OVERLAY)
|
|
// Checkbox overlay — bukan radio button base layer
|
|
// Null pada parameter pertama berarti tidak ada base layer switcher
|
|
// ════════════════════════════════════════════════════════════════
|
|
const overlayMaps = {
|
|
"SPBU Buka 24 Jam": layerSPBU24Jam,
|
|
"SPBU Tidak Buka 24 Jam": layerSPBUTidak24Jam
|
|
};
|
|
|
|
// L.control.layers(baseLayers, overlayMaps) — baseLayers = null → hanya checkbox overlay
|
|
L.control.layers(null, overlayMaps).addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// LEGEND
|
|
// ════════════════════════════════════════════════════════════════
|
|
const leg = L.control({ position: 'bottomright' });
|
|
leg.onAdd = () => {
|
|
const d = L.DomUtil.create('div', 'legend');
|
|
d.innerHTML = `<b>🗺️ Keterangan Warna</b>
|
|
<div class="li"><div class="dot" style="background:#f97316"></div>Ramai · 24 Jam · Buka</div>
|
|
<div class="li"><div class="dot" style="background:#22c55e"></div>Tdk Ramai · 24 Jam · Buka</div>
|
|
<div class="li"><div class="dot" style="background:#ef4444"></div>Ramai · Tdk 24 Jam · Buka</div>
|
|
<div class="li"><div class="dot" style="background:#3b82f6"></div>Tdk Ramai · Tdk 24 Jam · Buka</div>
|
|
<div class="li"><div class="dot" style="background:#1f2937"></div>Tutup (semua jenis)</div>`;
|
|
return d;
|
|
};
|
|
leg.addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// UTILITAS: TOAST, LOADING
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* showToast — tampilkan notifikasi singkat di bawah layar
|
|
* @param {string} message - Teks yang ditampilkan
|
|
* @param {string} type - 'ok' (hijau) atau 'err' (merah)
|
|
*/
|
|
function showToast(message, type = 'ok') {
|
|
const t = document.getElementById('toast');
|
|
t.textContent = message;
|
|
t.className = 'show ' + type;
|
|
setTimeout(() => { t.className = ''; }, 3000);
|
|
}
|
|
|
|
/** showLoading — tampilkan overlay loading spinner */
|
|
function showLoading() {
|
|
document.getElementById('loading').classList.add('show');
|
|
}
|
|
|
|
/** hideLoading — sembunyikan overlay loading spinner */
|
|
function hideLoading() {
|
|
document.getElementById('loading').classList.remove('show');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PENENTUAN WARNA MARKER
|
|
// Prioritas: tutup → hitam (apapun kondisi lain)
|
|
// Jika buka, warna ditentukan kombinasi status_ramai + buka_24_jam
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* getMarkerColor — menentukan warna marker berdasarkan data SPBU
|
|
* @param {Object} data - Objek data SPBU
|
|
* @returns {string} Kode warna HEX
|
|
*/
|
|
function getMarkerColor(data) {
|
|
// Prioritas 1: jika tutup → warna HITAM
|
|
if (data.status_operasional === 'tutup') return '#1f2937';
|
|
|
|
// Prioritas 2: jika buka, tentukan warna dari kombinasi status_ramai & buka_24_jam
|
|
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'ya') return '#f97316'; // ORANGE
|
|
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'ya') return '#22c55e'; // HIJAU
|
|
if (data.status_ramai === 'ramai' && data.buka_24_jam === 'tidak') return '#ef4444'; // MERAH
|
|
if (data.status_ramai === 'tidak_ramai' && data.buka_24_jam === 'tidak') return '#3b82f6'; // BIRU
|
|
|
|
return '#9ca3af'; // fallback abu-abu
|
|
}
|
|
|
|
/**
|
|
* createMarkerIcon — membuat ikon marker SVG inline melalui L.divIcon
|
|
* @param {Object} data - Objek data SPBU
|
|
* @returns {L.DivIcon}
|
|
*/
|
|
function createMarkerIcon(data) {
|
|
const c = getMarkerColor(data);
|
|
// SVG pin berbentuk teardrop dengan lingkaran putih di tengah
|
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 36" width="26" height="38">
|
|
<path d="M12 0C5.4 0 0 5.4 0 12c0 9 12 24 12 24S24 21 24 12C24 5.4 18.6 0 12 0z"
|
|
fill="${c}" stroke="#fff" stroke-width="1.5"/>
|
|
<circle cx="12" cy="12" r="5" fill="#fff" opacity="0.9"/>
|
|
</svg>`;
|
|
return L.divIcon({
|
|
html: svg,
|
|
className: '',
|
|
iconSize: [26, 38],
|
|
iconAnchor: [13, 38],
|
|
popupAnchor: [0, -38]
|
|
});
|
|
}
|
|
|
|
/**
|
|
* getTargetLayer — menentukan layer group yang tepat berdasarkan buka_24_jam
|
|
* @param {Object} data - Objek data SPBU
|
|
* @returns {L.LayerGroup} Layer group yang sesuai
|
|
*/
|
|
function getTargetLayer(data) {
|
|
// buka_24_jam = 'ya' → masuk layerSPBU24Jam
|
|
// buka_24_jam = 'tidak' → masuk layerSPBUTidak24Jam
|
|
return data.buka_24_jam === 'ya' ? layerSPBU24Jam : layerSPBUTidak24Jam;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// BADGE HELPER — menghasilkan label berwarna untuk popup detail
|
|
// ════════════════════════════════════════════════════════════════
|
|
function badgeR(v) {
|
|
return v === 'ramai'
|
|
? '<span style="background:#fef3c7;color:#92400e;padding:1px 7px;border-radius:10px;font-size:.75rem">Ramai</span>'
|
|
: '<span style="background:#ecfdf5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak Ramai</span>';
|
|
}
|
|
function badge24(v) {
|
|
return v === 'ya'
|
|
? '<span style="background:#eff6ff;color:#1e40af;padding:1px 7px;border-radius:10px;font-size:.75rem">Ya</span>'
|
|
: '<span style="background:#f9fafb;color:#374151;padding:1px 7px;border-radius:10px;font-size:.75rem">Tidak</span>';
|
|
}
|
|
function badgeO(v) {
|
|
return v === 'buka'
|
|
? '<span style="background:#d1fae5;color:#065f46;padding:1px 7px;border-radius:10px;font-size:.75rem">Buka</span>'
|
|
: '<span style="background:#fee2e2;color:#991b1b;padding:1px 7px;border-radius:10px;font-size:.75rem">Tutup</span>';
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// POPUP DETAIL MARKER
|
|
// Menampilkan semua kolom termasuk lat, lng, created_at, updated_at
|
|
// ════════════════════════════════════════════════════════════════
|
|
function buildPopupDetail(s) {
|
|
const namaSafe = s.nama_spbu.replace(/'/g, "\\'");
|
|
return `<div class="ph">⛽ ${s.nama_spbu}</div>
|
|
<div class="pb">
|
|
<table class="dt">
|
|
<tr><td>Status Ramai</td><td>${badgeR(s.status_ramai)}</td></tr>
|
|
<tr><td>Buka 24 Jam</td><td>${badge24(s.buka_24_jam)}</td></tr>
|
|
<tr><td>Status</td><td>${badgeO(s.status_operasional)}</td></tr>
|
|
<tr><td>Nomor HP</td><td>📞 ${s.nomor_hp}</td></tr>
|
|
<tr><td>Latitude</td><td>${parseFloat(s.latitude).toFixed(6)}</td></tr>
|
|
<tr><td>Longitude</td><td>${parseFloat(s.longitude).toFixed(6)}</td></tr>
|
|
<tr><td>Dibuat</td><td>${s.created_at}</td></tr>
|
|
<tr><td>Diperbarui</td><td>${s.updated_at}</td></tr>
|
|
</table>
|
|
<div class="br">
|
|
<button class="btn bp" onclick="bukaEdit(${s.id})">✏️ Edit</button>
|
|
<button class="btn bd" onclick="hapus(${s.id},'${namaSafe}')">🗑️ Hapus</button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PENEMPATAN MARKER KE LAYER GROUP
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* addMarkerToMap — membuat marker dan menempatkannya ke layer group yang sesuai
|
|
* @param {Object} data - Objek data SPBU dari database
|
|
*/
|
|
function addMarkerToMap(data) {
|
|
// Tentukan layer group berdasarkan nilai buka_24_jam
|
|
const targetLayer = getTargetLayer(data);
|
|
|
|
// Buat marker dengan ikon SVG sesuai warna
|
|
const marker = L.marker([data.latitude, data.longitude], {
|
|
icon: createMarkerIcon(data)
|
|
});
|
|
|
|
// Pasang popup detail ke marker
|
|
marker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
|
|
|
|
// Tambahkan marker ke layer group yang tepat (bukan langsung ke map)
|
|
// Layer group sudah ditambahkan ke map, jadi marker otomatis tampil
|
|
marker.addTo(targetLayer);
|
|
|
|
// Simpan referensi marker + layer asalnya ke markersMap
|
|
markersMap[data.id] = {
|
|
marker: marker,
|
|
layer: targetLayer // simpan layer asal untuk kebutuhan edit
|
|
};
|
|
}
|
|
|
|
/**
|
|
* removeMarkerFromMap — hapus marker dari layer group dan dari markersMap
|
|
* @param {number|string} id - ID SPBU yang akan dihapus
|
|
*/
|
|
function removeMarkerFromMap(id) {
|
|
if (!markersMap[id]) return;
|
|
|
|
// Hapus marker dari layer group asalnya
|
|
markersMap[id].layer.removeLayer(markersMap[id].marker);
|
|
|
|
// Hapus entry dari markersMap
|
|
delete markersMap[id];
|
|
}
|
|
|
|
/**
|
|
* refreshMarkerAfterEdit — perbarui marker setelah data diedit
|
|
* Jika buka_24_jam berubah, marker dipindahkan ke layer group yang baru
|
|
* @param {Object} data - Data SPBU terbaru dari server
|
|
*/
|
|
function refreshMarkerAfterEdit(data) {
|
|
if (!markersMap[data.id]) return;
|
|
|
|
const oldEntry = markersMap[data.id];
|
|
const newLayer = getTargetLayer(data);
|
|
const layerChanged = oldEntry.layer !== newLayer;
|
|
|
|
if (layerChanged) {
|
|
// ── Hapus marker lama dari layer group lama ──────────────
|
|
oldEntry.layer.removeLayer(oldEntry.marker);
|
|
|
|
// ── Buat marker baru dengan data terbaru ─────────────────
|
|
const newMarker = L.marker([data.latitude, data.longitude], {
|
|
icon: createMarkerIcon(data)
|
|
});
|
|
newMarker.bindPopup(buildPopupDetail(data), { maxWidth: 320 });
|
|
|
|
// ── Masukkan ke layer group baru ──────────────────────────
|
|
newMarker.addTo(newLayer);
|
|
newMarker.openPopup();
|
|
|
|
// ── Perbarui entry di markersMap ──────────────────────────
|
|
markersMap[data.id] = { marker: newMarker, layer: newLayer };
|
|
|
|
} else {
|
|
// Layer tidak berubah — cukup update icon dan konten popup
|
|
oldEntry.marker.setIcon(createMarkerIcon(data));
|
|
oldEntry.marker.setPopupContent(buildPopupDetail(data));
|
|
oldEntry.marker.openPopup();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// LOAD DATA — muat semua marker dari database saat halaman dibuka
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* loadData — fetch semua data SPBU dari ambil_data.php
|
|
* lalu tempatkan marker ke layer group masing-masing
|
|
*/
|
|
async function loadData() {
|
|
try {
|
|
const res = await fetch('ambil_data.php');
|
|
const json = await res.json();
|
|
if (json.status !== 'success') return;
|
|
|
|
// Bersihkan semua marker yang ada (jika ada dari load sebelumnya)
|
|
Object.keys(markersMap).forEach(id => removeMarkerFromMap(id));
|
|
|
|
// Tambahkan semua marker ke layer group sesuai buka_24_jam masing-masing
|
|
json.data.forEach(spbu => addMarkerToMap(spbu));
|
|
|
|
} catch (err) {
|
|
console.error('loadData error:', err);
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// KLIK PETA → POPUP FORM TAMBAH DATA (menggunakan L.popup())
|
|
// Latitude dan longitude diambil otomatis dari titik klik,
|
|
// TIDAK ditampilkan di form — hanya dikirim saat simpan
|
|
// ════════════════════════════════════════════════════════════════
|
|
map.on('click', function(e) {
|
|
const lat = e.latlng.lat.toFixed(7);
|
|
const lng = e.latlng.lng.toFixed(7);
|
|
|
|
// Tutup popup tambah sebelumnya jika ada
|
|
if (tempPopup) { map.closePopup(tempPopup); tempPopup = null; }
|
|
|
|
// HTML form tambah — lat & lng tidak tampil di form, hanya di kotak info
|
|
const html = `<div class="ph">📍 Tambah SPBU Baru</div>
|
|
<div class="pb">
|
|
<div class="coord-box">📌 Lat: <b>${lat}</b> | Lng: <b>${lng}</b></div>
|
|
<div class="fg">
|
|
<label>Nama SPBU</label>
|
|
<input id="f-nm" placeholder="Contoh: SPBU 44.501.12" maxlength="100">
|
|
</div>
|
|
<div class="fg">
|
|
<label>Status Keramaian</label>
|
|
<select id="f-rm">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="ramai">Ramai</option>
|
|
<option value="tidak_ramai">Tidak Ramai</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Buka 24 Jam?</label>
|
|
<select id="f-24">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="ya">Ya</option>
|
|
<option value="tidak">Tidak</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Status Operasional</label>
|
|
<select id="f-op">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="buka">Buka</option>
|
|
<option value="tutup">Tutup</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Nomor HP</label>
|
|
<input id="f-hp" placeholder="0812-3456-7890" maxlength="20">
|
|
</div>
|
|
<div class="br">
|
|
<button class="btn bp" onclick="simpan(${lat}, ${lng})">💾 Simpan</button>
|
|
<button class="btn bs" onclick="map.closePopup()">✕ Batal</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
// Buat popup tambah menggunakan L.popup() dan buka di titik klik
|
|
tempPopup = L.popup({ maxWidth: 320 })
|
|
.setLatLng(e.latlng)
|
|
.setContent(html)
|
|
.openOn(map);
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PROSES TAMBAH DATA via fetch()
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* simpan — ambil nilai form, kirim ke tambah.php via fetch + FormData,
|
|
* lalu tambahkan marker baru ke layer group yang sesuai
|
|
* @param {number} lat - Latitude dari titik klik
|
|
* @param {number} lng - Longitude dari titik klik
|
|
*/
|
|
async function simpan(lat, lng) {
|
|
const nm = document.getElementById('f-nm')?.value.trim();
|
|
const rm = document.getElementById('f-rm')?.value;
|
|
const j24 = document.getElementById('f-24')?.value;
|
|
const op = document.getElementById('f-op')?.value;
|
|
const hp = document.getElementById('f-hp')?.value.trim();
|
|
|
|
// Validasi sisi klien sebelum kirim ke server
|
|
if (!nm || !rm || !j24 || !op || !hp) {
|
|
showToast('⚠️ Semua field wajib diisi!', 'err');
|
|
return;
|
|
}
|
|
|
|
// Buat FormData untuk dikirim via fetch POST
|
|
const fd = new FormData();
|
|
fd.append('nama_spbu', nm);
|
|
fd.append('status_ramai', rm);
|
|
fd.append('buka_24_jam', j24);
|
|
fd.append('status_operasional', op);
|
|
fd.append('nomor_hp', hp);
|
|
fd.append('latitude', lat); // lat dari klik peta
|
|
fd.append('longitude', lng); // lng dari klik peta
|
|
|
|
showLoading();
|
|
try {
|
|
// Kirim data ke tambah.php menggunakan fetch POST
|
|
const res = await fetch('tambah.php', { method: 'POST', body: fd });
|
|
const json = await res.json();
|
|
|
|
if (json.status === 'success') {
|
|
// Tutup popup form tambah
|
|
map.closePopup(); tempPopup = null;
|
|
|
|
// Tambahkan marker baru ke layer group yang sesuai (berdasarkan buka_24_jam)
|
|
addMarkerToMap(json.data);
|
|
|
|
// Pan ke marker baru dan buka popup detail-nya
|
|
map.panTo([json.data.latitude, json.data.longitude]);
|
|
markersMap[json.data.id].marker.openPopup();
|
|
|
|
showToast('✅ Data berhasil ditambahkan!', 'ok');
|
|
} else {
|
|
showToast('❌ ' + json.message, 'err');
|
|
}
|
|
} catch (err) {
|
|
showToast('❌ Gagal menghubungi server', 'err');
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// BUKA FORM EDIT (dari tombol di popup detail)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* bukaEdit — fetch data SPBU lalu tampilkan form edit di popup marker
|
|
* @param {number} id - ID SPBU yang akan diedit
|
|
*/
|
|
async function bukaEdit(id) {
|
|
showLoading();
|
|
try {
|
|
// Ambil data terbaru dari server untuk mengisi form edit
|
|
const res = await fetch('ambil_data.php');
|
|
const json = await res.json();
|
|
hideLoading();
|
|
|
|
const s = json.data.find(x => x.id == id);
|
|
if (!s) { showToast('❌ Data tidak ditemukan', 'err'); return; }
|
|
|
|
// HTML form edit — lat & lng tidak tampil, hanya field yang boleh diubah
|
|
const html = `<div class="ph">✏️ Edit Data SPBU</div>
|
|
<div class="pb">
|
|
<div class="fg">
|
|
<label>Nama SPBU</label>
|
|
<input id="e-nm" value="${s.nama_spbu.replace(/"/g, '"')}" maxlength="100">
|
|
</div>
|
|
<div class="fg">
|
|
<label>Status Keramaian</label>
|
|
<select id="e-rm">
|
|
<option value="ramai" ${s.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
|
|
<option value="tidak_ramai" ${s.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Buka 24 Jam?</label>
|
|
<select id="e-24">
|
|
<option value="ya" ${s.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
|
|
<option value="tidak" ${s.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Status Operasional</label>
|
|
<select id="e-op">
|
|
<option value="buka" ${s.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
|
|
<option value="tutup" ${s.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Nomor HP</label>
|
|
<input id="e-hp" value="${s.nomor_hp}" maxlength="20">
|
|
</div>
|
|
<div class="br">
|
|
<button class="btn bg" onclick="simpanEdit(${id})">💾 Simpan</button>
|
|
<button class="btn bs" onclick="kembaliDetail(${id})">✕ Batal</button>
|
|
</div>
|
|
</div>`;
|
|
|
|
// Tampilkan form edit di popup marker yang bersangkutan
|
|
if (markersMap[id]) {
|
|
markersMap[id].marker.setPopupContent(html).openPopup();
|
|
}
|
|
} catch (err) {
|
|
hideLoading();
|
|
showToast('❌ Gagal menghubungi server', 'err');
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PROSES EDIT DATA via fetch()
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* simpanEdit — kirim perubahan ke edit.php via fetch POST,
|
|
* lalu perbarui marker (termasuk pindah layer group jika buka_24_jam berubah)
|
|
* @param {number} id - ID SPBU yang sedang diedit
|
|
*/
|
|
async function simpanEdit(id) {
|
|
const nm = document.getElementById('e-nm')?.value.trim();
|
|
const rm = document.getElementById('e-rm')?.value;
|
|
const j24 = document.getElementById('e-24')?.value;
|
|
const op = document.getElementById('e-op')?.value;
|
|
const hp = document.getElementById('e-hp')?.value.trim();
|
|
|
|
// Validasi sisi klien
|
|
if (!nm || !rm || !j24 || !op || !hp) {
|
|
showToast('⚠️ Semua field wajib diisi!', 'err');
|
|
return;
|
|
}
|
|
|
|
// FormData hanya berisi field yang boleh diubah — lat & lng tidak dikirim
|
|
const fd = new FormData();
|
|
fd.append('id', id);
|
|
fd.append('nama_spbu', nm);
|
|
fd.append('status_ramai', rm);
|
|
fd.append('buka_24_jam', j24);
|
|
fd.append('status_operasional', op);
|
|
fd.append('nomor_hp', hp);
|
|
|
|
showLoading();
|
|
try {
|
|
// Kirim ke edit.php menggunakan fetch POST
|
|
const res = await fetch('edit.php', { method: 'POST', body: fd });
|
|
const json = await res.json();
|
|
|
|
if (json.status === 'success') {
|
|
// Perbarui marker: hapus dari layer lama, buat baru di layer yang sesuai
|
|
// Jika buka_24_jam berubah, marker otomatis berpindah layer group
|
|
refreshMarkerAfterEdit(json.data);
|
|
showToast('✅ Data berhasil diperbarui!', 'ok');
|
|
} else {
|
|
showToast('❌ ' + json.message, 'err');
|
|
}
|
|
} catch (err) {
|
|
showToast('❌ Gagal menghubungi server', 'err');
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// PROSES HAPUS DATA via fetch()
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* hapus — konfirmasi lalu kirim request hapus ke hapus.php,
|
|
* setelah sukses hapus marker dari layer group dan markersMap
|
|
* @param {number} id - ID SPBU yang akan dihapus
|
|
* @param {string} nama - Nama SPBU untuk konfirmasi dialog
|
|
*/
|
|
async function hapus(id, nama) {
|
|
if (!confirm(`Hapus SPBU "${nama}"? Tindakan ini tidak dapat dibatalkan.`)) return;
|
|
|
|
const fd = new FormData();
|
|
fd.append('id', id);
|
|
|
|
showLoading();
|
|
try {
|
|
// Kirim ke hapus.php menggunakan fetch POST
|
|
const res = await fetch('hapus.php', { method: 'POST', body: fd });
|
|
const json = await res.json();
|
|
|
|
if (json.status === 'success') {
|
|
// Hapus marker dari layer group asalnya dan dari markersMap
|
|
removeMarkerFromMap(id);
|
|
showToast('🗑️ Data berhasil dihapus', 'ok');
|
|
} else {
|
|
showToast('❌ ' + json.message, 'err');
|
|
}
|
|
} catch (err) {
|
|
showToast('❌ Gagal menghubungi server', 'err');
|
|
} finally {
|
|
hideLoading();
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// KEMBALI KE POPUP DETAIL (dari form edit saat batal)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/**
|
|
* kembaliDetail — ambil data terbaru lalu tampilkan kembali popup detail
|
|
* @param {number} id - ID SPBU
|
|
*/
|
|
async function kembaliDetail(id) {
|
|
try {
|
|
const res = await fetch('ambil_data.php');
|
|
const json = await res.json();
|
|
const s = json.data.find(x => x.id == id);
|
|
if (s && markersMap[id]) {
|
|
markersMap[id].marker.setPopupContent(buildPopupDetail(s)).openPopup();
|
|
}
|
|
} catch (err) {
|
|
console.error('kembaliDetail error:', err);
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// INISIALISASI — muat semua marker saat halaman pertama dibuka
|
|
// ════════════════════════════════════════════════════════════════
|
|
loadData();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|