790 lines
28 KiB
HTML
790 lines
28 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 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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <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: '© <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> |