Files
luthfihadinugroho79 e950468760 add files
2026-06-10 19:42:59 +07:00

790 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<!-- ✅ FIX 1: Leaflet CSS & JS dipindah ke <head> agar sudah siap sebelum script dijalankan -->
<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: #0d0f14;
--surface: #161923;
--surface2: #1e2330;
--border: rgba(255,255,255,0.07);
--accent-green: #00e5a0;
--accent-amber: #ffb830;
--accent-blue: #3d8bff;
--text: #e8ecf4;
--text-muted: #6b7385;
--danger: #ff4d6a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'DM Sans', sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
height: 100vh;
overflow: hidden;
}
/* ─── SIDEBAR ─── */
#sidebar {
width: 340px;
min-width: 340px;
background: var(--surface);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
z-index: 1000;
overflow: hidden;
}
.sidebar-header {
padding: 28px 24px 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, rgba(61,139,255,0.12), rgba(0,229,160,0.06));
position: relative;
overflow: hidden;
}
.sidebar-header::before {
content: '';
position: absolute;
top: -40px; right: -40px;
width: 130px; height: 130px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,229,160,0.15), transparent 70%);
}
.logo-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
}
.logo-icon {
width: 40px; height: 40px;
background: linear-gradient(135deg, var(--accent-green), var(--accent-blue));
border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 18px;
}
.sidebar-header h1 {
font-family: 'Syne', sans-serif;
font-size: 20px;
font-weight: 800;
letter-spacing: -0.5px;
}
.sidebar-header p {
font-size: 12px;
color: var(--text-muted);
margin-left: 52px;
}
/* Stats */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.stat-box {
background: var(--surface2);
border-radius: 10px;
padding: 12px 10px;
text-align: center;
border: 1px solid var(--border);
transition: transform 0.2s;
}
.stat-box:hover { transform: translateY(-2px); }
.stat-num {
font-family: 'Syne', sans-serif;
font-size: 26px;
font-weight: 800;
line-height: 1;
}
.stat-label { font-size: 10px; color: var(--text-muted); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
.stat-box.total .stat-num { color: var(--accent-blue); }
.stat-box.jam24 .stat-num { color: var(--accent-green); }
.stat-box.reguler .stat-num { color: var(--accent-amber); }
/* Layer Control Panel */
.layer-panel {
padding: 16px 20px;
border-bottom: 1px solid var(--border);
}
.panel-title {
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
margin-bottom: 12px;
}
.layer-toggle {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
margin-bottom: 6px;
border: 1px solid var(--border);
}
.layer-toggle:hover { background: var(--surface2); }
.toggle-dot {
width: 12px; height: 12px;
border-radius: 3px;
flex-shrink: 0;
}
.toggle-dot.green { background: var(--accent-green); box-shadow: 0 0 8px rgba(0,229,160,0.5); }
.toggle-dot.amber { background: var(--accent-amber); box-shadow: 0 0 8px rgba(255,184,48,0.5); }
.layer-toggle span { font-size: 13px; flex-grow: 1; }
.toggle-count {
font-size: 11px;
background: var(--surface2);
padding: 2px 7px;
border-radius: 20px;
color: var(--text-muted);
}
/* Custom Checkbox */
.layer-toggle input[type="checkbox"] {
appearance: none;
width: 18px; height: 18px;
border: 2px solid var(--border);
border-radius: 5px;
cursor: pointer;
position: relative;
transition: all 0.2s;
flex-shrink: 0;
}
.layer-toggle input[type="checkbox"]:checked {
background: var(--accent-blue);
border-color: var(--accent-blue);
}
.layer-toggle input[type="checkbox"]:checked::after {
content: '✓';
position: absolute;
color: white;
font-size: 11px;
top: 0px; left: 2px;
font-weight: bold;
}
/* SPBU List */
.list-header {
padding: 14px 20px 8px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.5px;
color: var(--text-muted);
}
#spbu-list {
flex-grow: 1;
overflow-y: auto;
padding: 0 12px 12px;
scrollbar-width: thin;
scrollbar-color: var(--surface2) transparent;
}
.spbu-item {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 12px;
border-radius: 8px;
margin-bottom: 5px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s;
}
.spbu-item:hover {
background: var(--surface2);
border-color: var(--border);
}
.spbu-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.spbu-dot.green { background: var(--accent-green); }
.spbu-dot.amber { background: var(--accent-amber); }
.spbu-item-name { font-size: 13px; font-weight: 500; flex-grow: 1; }
.badge-pill {
font-size: 9px;
padding: 3px 7px;
border-radius: 20px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.badge-pill.green { background: rgba(0,229,160,0.15); color: var(--accent-green); }
.badge-pill.amber { background: rgba(255,184,48,0.15); color: var(--accent-amber); }
/* ─── MAP ─── */
#map-container {
flex-grow: 1;
position: relative;
/* ✅ FIX 2: Pastikan tinggi eksplisit agar peta punya dimensi */
height: 100vh;
min-height: 0;
}
#map {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
}
/* Leaflet overrides for dark theme */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
color: var(--text) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
box-shadow: 0 20px 60px rgba(0,0,0,0.6) !important;
padding: 0 !important;
overflow: hidden;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content { margin: 0 !important; }
.leaflet-control-zoom {
border: none !important;
border-radius: 10px !important;
overflow: hidden;
}
.leaflet-control-zoom a {
background: var(--surface) !important;
color: var(--text) !important;
border-bottom: 1px solid var(--border) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface2) !important; }
/* Popup Form */
.popup-wrap { padding: 18px; min-width: 220px; }
.popup-type {
font-size: 9px;
font-family: 'Syne', sans-serif;
font-weight: 700;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 4px;
}
.popup-name {
font-family: 'Syne', sans-serif;
font-size: 16px;
font-weight: 700;
margin-bottom: 12px;
}
.popup-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}
.popup-row strong { color: var(--text); }
.popup-divider { height: 1px; background: var(--border); margin: 14px 0; }
.popup-input {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 7px;
padding: 9px 12px;
color: var(--text);
font-family: 'DM Sans', sans-serif;
font-size: 13px;
margin-bottom: 8px;
outline: none;
transition: border-color 0.2s;
}
.popup-input:focus { border-color: var(--accent-blue); }
.popup-actions { display: flex; gap: 7px; margin-top: 12px; }
.pop-btn {
flex: 1;
padding: 9px;
border: none;
border-radius: 7px;
font-family: 'Syne', sans-serif;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.2s;
}
.pop-btn:hover { filter: brightness(1.1); transform: translateY(-1px); }
.pop-btn.blue { background: var(--accent-blue); color: white; }
.pop-btn.amber { background: var(--accent-amber); color: #1a1000; }
.pop-btn.red { background: var(--danger); color: white; }
.pop-btn.gray { background: var(--surface2); color: var(--text-muted); border: 1px solid var(--border); }
/* Map hint badge */
.map-hint {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(13,15,20,0.85);
backdrop-filter: blur(10px);
border: 1px solid var(--border);
border-radius: 30px;
padding: 8px 18px;
font-size: 12px;
color: var(--text-muted);
z-index: 999;
pointer-events: none;
}
.map-hint span { color: var(--accent-blue); font-weight: 600; }
/* ✅ FIX 3: Loading overlay HTML ditambahkan */
.loading-overlay {
position: absolute;
inset: 0;
background: var(--bg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s;
}
.loading-overlay.hidden { opacity: 0; pointer-events: none; }
.loader-ring {
width: 50px; height: 50px;
border: 3px solid var(--surface2);
border-top-color: var(--accent-green);
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Animations */
.spbu-item { animation: fadeUp 0.3s ease backwards; }
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<div id="sidebar">
<div class="sidebar-header">
<div class="logo-row">
<div class="logo-icon"></div>
<h1>SPBU Pontianak</h1>
</div>
<p>WebGIS — Sistem Informasi Geografis</p>
</div>
<div class="stats-grid">
<div class="stat-box total">
<div class="stat-num" id="stat-total"></div>
<div class="stat-label">Total</div>
</div>
<div class="stat-box jam24">
<div class="stat-num" id="stat-24"></div>
<div class="stat-label">24 Jam</div>
</div>
<div class="stat-box reguler">
<div class="stat-num" id="stat-reg"></div>
<div class="stat-label">Reguler</div>
</div>
</div>
<div class="layer-panel">
<div class="panel-title">Layer Control</div>
<label class="layer-toggle">
<div class="toggle-dot green"></div>
<span>SPBU Buka 24 Jam</span>
<span class="toggle-count" id="cnt-24">0</span>
<input type="checkbox" id="chk-24" checked>
</label>
<label class="layer-toggle">
<div class="toggle-dot amber"></div>
<span>SPBU Buka Reguler</span>
<span class="toggle-count" id="cnt-reg">0</span>
<input type="checkbox" id="chk-reg" checked>
</label>
</div>
<div class="list-header">Daftar SPBU</div>
<div id="spbu-list">
<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Memuat data...</p>
</div>
</div>
<div id="map-container">
<!-- ✅ FIX 3: Loading overlay ditambahkan ke HTML -->
<div class="loading-overlay" id="loading-overlay">
<div class="loader-ring"></div>
<p style="color:var(--text-muted);font-size:13px;">Memuat peta...</p>
</div>
<div id="map"></div>
<div class="map-hint">Klik <span>di mana saja</span> pada peta untuk menambah SPBU baru</div>
</div>
<!-- ✅ FIX 1: Script Leaflet DIHAPUS dari sini karena sudah dipindah ke <head> -->
<script>
// ── MAP INIT ──
// ✅ FIX 4: Inisialisasi map dibungkus dalam DOMContentLoaded agar aman
document.addEventListener('DOMContentLoaded', function() {
const map = L.map('map', { zoomControl: false }).setView([-0.026330, 109.342504], 13);
L.control.zoom({ position: 'topright' }).addTo(map);
// ✅ FIX 5: Tile layer dengan 2 pilihan — CartoDB dark (utama) + OSM (fallback)
const tileCartoDB = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_matter/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/">CartoDB</a>',
maxZoom: 19,
subdomains: 'abcd'
});
const tileOSM = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
// Coba CartoDB dulu, jika tile gagal load pakai OSM
tileCartoDB.addTo(map);
tileCartoDB.on('tileerror', function() {
map.removeLayer(tileCartoDB);
tileOSM.addTo(map);
});
// ── LAYER GROUPS ──
// Dibuat dulu, belum ditambah ke map — ditambah sesuai state checkbox
const layer24 = L.layerGroup();
const layerReg = L.layerGroup();
// ── EVENT LAYER CONTROL (FIX UTAMA) ──
document.getElementById('chk-24').addEventListener('change', function() {
if (this.checked) {
layer24.addTo(map);
} else {
map.removeLayer(layer24);
}
});
document.getElementById('chk-reg').addEventListener('change', function() {
if (this.checked) {
layerReg.addTo(map);
} else {
map.removeLayer(layerReg);
}
});
// ── ICONS ──
function makeIcon(color) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="42" viewBox="0 0 32 42">
<defs>
<filter id="s${color.replace('#','')}"><feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="${color}" flood-opacity="0.4"/></filter>
</defs>
<path fill="${color}" filter="url(#s${color.replace('#','')})" d="M16 0C7.163 0 0 7.163 0 16c0 12 16 26 16 26s16-14 16-26C32 7.163 24.837 0 16 0z"/>
<circle cx="16" cy="16" r="8" fill="white" opacity="0.9"/>
<circle cx="16" cy="16" r="5" fill="${color}"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [32, 42],
iconAnchor: [16, 42],
popupAnchor: [0, -42]
});
}
const iconGreen = makeIcon('#00e5a0');
const iconAmber = makeIcon('#ffb830');
const iconTemp = makeIcon('#3d8bff');
// ── DATA ──
let allData = [];
let tempMarker = null;
function muatData() {
fetch('ambil_data.php')
.then(r => r.json())
.then(data => {
allData = data;
renderAll(data);
})
.catch(() => {
// Demo mode jika tidak ada server
renderAll(demoData());
});
}
function demoData() {
return [
{id:'1', nama_spbu:'SPBU Jl. Ahmad Yani', no_wa:'08123456789', status_24_jam:'yes', lat:-0.0202, lng:109.3423},
{id:'2', nama_spbu:'SPBU Jl. Gajah Mada', no_wa:'08234567890', status_24_jam:'no', lat:-0.0280, lng:109.3310},
{id:'3', nama_spbu:'SPBU Sui Raya', no_wa:'08345678901', status_24_jam:'yes', lat:-0.0450, lng:109.3580},
{id:'4', nama_spbu:'SPBU Jl. Tanjungpura', no_wa:'08456789012', status_24_jam:'no', lat:-0.0120, lng:109.3390},
{id:'5', nama_spbu:'SPBU Jl. Imam Bonjol', no_wa:'08567890123', status_24_jam:'yes', lat:-0.0330, lng:109.3500},
{id:'6', nama_spbu:'SPBU Siantan', no_wa:'08678901234', status_24_jam:'no', lat:0.0050, lng:109.3350},
];
}
function renderAll(data) {
layer24.clearLayers();
layerReg.clearLayers();
let c24 = 0, cReg = 0;
let listHTML = '';
data.forEach((item, i) => {
const is24 = item.status_24_jam === 'yes';
const icon = is24 ? iconGreen : iconAmber;
const layer = is24 ? layer24 : layerReg;
if (is24) c24++; else cReg++;
// Tambah marker ke layerGroup (bukan langsung ke map)
const marker = L.marker([parseFloat(item.lat), parseFloat(item.lng)], { icon, draggable: true });
layer.addLayer(marker);
marker.bindPopup(buildInfoPopup(item), { maxWidth: 280 });
marker.on('dragend', function() {
const pos = marker.getLatLng();
if (confirm('Simpan koordinat baru?')) updateLokasi(item.id, pos.lat, pos.lng);
else muatData();
});
const delay = i * 40;
listHTML += `
<div class="spbu-item" style="animation-delay:${delay}ms" onclick="flyTo(${item.lat},${item.lng})">
<div class="spbu-dot ${is24 ? 'green' : 'amber'}"></div>
<span class="spbu-item-name">${item.nama_spbu}</span>
<span class="badge-pill ${is24 ? 'green' : 'amber'}">${is24 ? '24 Jam' : 'Reguler'}</span>
</div>
`;
});
document.getElementById('stat-total').textContent = data.length;
document.getElementById('stat-24').textContent = c24;
document.getElementById('stat-reg').textContent = cReg;
document.getElementById('cnt-24').textContent = c24;
document.getElementById('cnt-reg').textContent = cReg;
document.getElementById('spbu-list').innerHTML = listHTML || '<p style="text-align:center;color:var(--text-muted);font-size:13px;padding:20px;">Belum ada data.</p>';
// Terapkan state checkbox saat ini ke masing-masing layer
const show24 = document.getElementById('chk-24').checked;
const showReg = document.getElementById('chk-reg').checked;
show24 ? layer24.addTo(map) : map.removeLayer(layer24);
showReg ? layerReg.addTo(map) : map.removeLayer(layerReg);
// Sembunyikan loading overlay setelah data selesai dirender
const overlay = document.getElementById('loading-overlay');
if (overlay) overlay.classList.add('hidden');
}
window.flyTo = function flyTo(lat, lng) {
map.flyTo([lat, lng], 16, { duration: 1 });
};
// ── POPUP BUILDERS ──
function buildInfoPopup(item) {
const is24 = item.status_24_jam === 'yes';
return `
<div class="popup-wrap">
<div class="popup-type">Stasiun Bahan Bakar</div>
<div class="popup-name">${item.nama_spbu}</div>
<div class="popup-row">📞 <strong>${item.no_wa}</strong></div>
<div class="popup-row">🕐 Status: <span class="badge-pill ${is24?'green':'amber'}" style="margin-left:4px">${is24?'Buka 24 Jam':'Buka Reguler'}</span></div>
<div class="popup-divider"></div>
<div class="popup-actions">
<button class="pop-btn amber" onclick="bukaEdit('${item.id}','${item.nama_spbu}','${item.no_wa}','${item.status_24_jam}',${item.lat},${item.lng})">✏ Edit</button>
<button class="pop-btn red" onclick="hapusData('${item.id}')">🗑 Hapus</button>
</div>
</div>
`;
}
function buildAddPopup(lat, lng) {
return `
<div class="popup-wrap">
<div class="popup-type">Tambah Lokasi Baru</div>
<div class="popup-name" style="font-size:14px;margin-bottom:14px;">Data SPBU</div>
<input class="popup-input" id="add_nama" placeholder="Nama SPBU" />
<input class="popup-input" id="add_wa" placeholder="Nomor WhatsApp" />
<select class="popup-input" id="add_status">
<option value="no">Buka Reguler</option>
<option value="yes">Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'tambah')">💾 Simpan</button>
<button class="pop-btn gray" onclick="batalTambah()">✕</button>
</div>
</div>
`;
}
function buildEditPopup(id, nama, wa, status, lat, lng) {
const sY = status==='yes'?'selected':'';
const sN = status==='no'?'selected':'';
return `
<div class="popup-wrap">
<div class="popup-type">Edit Data</div>
<input type="hidden" id="edit_id" value="${id}">
<input class="popup-input" id="edit_nama" value="${nama}">
<input class="popup-input" id="edit_wa" value="${wa}">
<select class="popup-input" id="edit_status">
<option value="no" ${sN}>Buka Reguler</option>
<option value="yes" ${sY}>Buka 24 Jam</option>
</select>
<div class="popup-actions">
<button class="pop-btn blue" onclick="prosesSimpan(${lat},${lng},'edit')">✓ Update</button>
<button class="pop-btn gray" onclick="map.closePopup()">✕ Batal</button>
</div>
</div>
`;
}
// ── MAP CLICK ADD ──
map.on('click', function(e) {
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: iconTemp }).addTo(map);
tempMarker.bindPopup(buildAddPopup(e.latlng.lat, e.latlng.lng), { maxWidth: 280 }).openPopup();
});
window.batalTambah = function() {
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
map.closePopup();
};
// ── EDIT ──
window.bukaEdit = function(id, nama, wa, status, lat, lng) {
L.popup({ maxWidth: 280 })
.setLatLng([lat, lng])
.setContent(buildEditPopup(id, nama, wa, status, lat, lng))
.openOn(map);
};
// ── SIMPAN (POST) ──
window.prosesSimpan = function prosesSimpan(lat, lng, mode) {
const pfx = mode === 'edit' ? 'edit_' : 'add_';
const id = mode === 'edit' ? document.getElementById('edit_id').value : '';
const nama = document.getElementById(pfx + 'nama').value.trim();
const wa = document.getElementById(pfx + 'wa').value.trim();
const stat = document.getElementById(pfx + 'status').value;
if (!nama || !wa) return alert('Nama dan nomor WA wajib diisi!');
const fd = new FormData();
if (mode === 'edit') fd.append('id', id);
fd.append('nama_spbu', nama);
fd.append('no_wa', wa);
fd.append('status_24_jam', stat);
fd.append('lat', lat);
fd.append('lng', lng);
const url = mode === 'edit' ? 'edit_data.php' : 'simpan_data.php';
fetch(url, { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => {
showToast(msg, 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
muatData();
})
.catch(() => {
// Demo mode: update state lokal agar data langsung tampil tanpa server
if (mode === 'tambah') {
const newItem = {
id: Date.now().toString(),
nama_spbu: nama,
no_wa: wa,
status_24_jam: stat,
lat: lat,
lng: lng
};
allData.push(newItem);
} else {
const idx = allData.findIndex(d => d.id === id);
if (idx !== -1) {
allData[idx].nama_spbu = nama;
allData[idx].no_wa = wa;
allData[idx].status_24_jam = stat;
}
}
renderAll(allData);
showToast(mode === 'tambah' ? 'Data berhasil disimpan! (demo mode)' : 'Data berhasil diperbarui! (demo mode)', 'success');
map.closePopup();
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
});
}
// ── HAPUS ──
window.hapusData = function hapusData(id) {
if (!confirm('Hapus data SPBU ini?')) return;
const fd = new FormData();
fd.append('id', id);
fetch('hapus_data.php', { method: 'POST', body: fd })
.then(r => r.text())
.then(msg => { showToast(msg, 'danger'); muatData(); })
.catch(() => {
allData = allData.filter(d => d.id !== id);
renderAll(allData);
showToast('Data dihapus! (demo mode)', 'danger');
});
}
// ── UPDATE LOKASI ──
window.updateLokasi = function updateLokasi(id, lat, lng) {
const fd = new FormData();
fd.append('id', id); fd.append('lat', lat); fd.append('lng', lng);
fetch('update_lokasi.php', { method: 'POST', body: fd })
.then(() => { showToast('Lokasi diperbarui!', 'success'); muatData(); })
.catch(() => { showToast('Lokasi diperbarui! (demo mode)', 'success'); muatData(); });
}
// ── TOAST ──
function showToast(msg, type) {
const t = document.createElement('div');
t.style.cssText = `
position:fixed; bottom:30px; right:30px; z-index:9999;
background:${type==='success'?'#00e5a0':'#ff4d6a'};
color:${type==='success'?'#001a0d':'#fff'};
padding:12px 20px; border-radius:10px; font-size:13px; font-weight:600;
font-family:'Syne',sans-serif; box-shadow:0 8px 30px rgba(0,0,0,0.4);
animation: slideIn 0.3s ease;
`;
t.textContent = msg;
document.body.appendChild(t);
setTimeout(() => t.remove(), 3000);
}
// ── START ──
muatData();
// ✅ FIX 6: invalidateSize dipanggil beberapa kali untuk memastikan peta terrender sempurna
setTimeout(() => map.invalidateSize(), 100);
setTimeout(() => map.invalidateSize(), 500);
}); // tutup DOMContentLoaded
</script>
</body>
</html>