Files
D1041231091-WebGIS-All/04/index.php
T

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 &amp; 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: '&copy; <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> &nbsp;|&nbsp; 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, '&quot;')}" 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>