Files

1137 lines
50 KiB
PHP
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 Manajemen SPBU</title>
<!-- ── Leaflet CSS ─────────────────────────────────────────────────────── -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- ── Leaflet Draw CSS ──────────────────────────────────────────────── -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<style>
/* ════════════════════════════════════════════════════════════════════
GLOBAL RESET & VARIABLES
════════════════════════════════════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--clr-bg: #0f172a;
--clr-header: #1e293b;
--clr-accent: #f59e0b;
--clr-text: #f1f5f9;
--clr-muted: #94a3b8;
--clr-border: #334155;
--clr-card: #1e293b;
--clr-success: #22c55e;
--clr-danger: #ef4444;
--clr-warning: #f97316;
--clr-info: #3b82f6;
--header-h: 52px;
--radius: 8px;
--font-main: 'Segoe UI', system-ui, sans-serif;
}
html, body {
height: 100%;
font-family: var(--font-main);
background: var(--clr-bg);
color: var(--clr-text);
overflow: hidden;
}
/* ════════════════════════════════════════════════════════════════════
HEADER
════════════════════════════════════════════════════════════════════ */
#app-header {
height: var(--header-h);
background: var(--clr-header);
border-bottom: 1px solid var(--clr-border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
gap: 12px;
position: relative;
z-index: 1000;
box-shadow: 0 2px 8px rgba(0,0,0,.4);
}
#app-header .logo {
display: flex;
align-items: center;
gap: 8px;
}
#app-header .logo svg { color: var(--clr-accent); flex-shrink: 0; }
#app-header h1 {
font-size: 1rem;
font-weight: 700;
letter-spacing: .02em;
color: var(--clr-text);
white-space: nowrap;
}
#app-header h1 span { color: var(--clr-accent); }
/* Keterangan mode */
.header-hints {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: nowrap;
}
.hint-item {
display: flex;
align-items: center;
gap: 6px;
font-size: .72rem;
color: var(--clr-muted);
white-space: nowrap;
}
.hint-item .dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--clr-accent);
flex-shrink: 0;
}
/* Legend warna marker */
.legend-wrap {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: nowrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 4px;
font-size: .65rem;
color: var(--clr-muted);
}
.legend-dot {
width: 10px; height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
/* ════════════════════════════════════════════════════════════════════
PETA
════════════════════════════════════════════════════════════════════ */
#map {
width: 100%;
height: calc(100vh - var(--header-h));
}
/* ════════════════════════════════════════════════════════════════════
POPUP LEAFLET CUSTOM
════════════════════════════════════════════════════════════════════ */
.leaflet-popup-content-wrapper {
background: var(--clr-card) !important;
color: var(--clr-text) !important;
border: 1px solid var(--clr-border) !important;
border-radius: var(--radius) !important;
box-shadow: 0 8px 32px rgba(0,0,0,.5) !important;
padding: 0 !important;
}
.leaflet-popup-tip { background: var(--clr-card) !important; }
.leaflet-popup-content { margin: 0 !important; }
/* Popup Form & Detail */
.popup-box {
min-width: 260px;
max-width: 320px;
padding: 16px;
}
.popup-box h3 {
font-size: .9rem;
font-weight: 700;
margin-bottom: 12px;
color: var(--clr-accent);
border-bottom: 1px solid var(--clr-border);
padding-bottom: 8px;
}
/* Form fields */
.form-group { margin-bottom: 10px; }
.form-group label {
display: block;
font-size: .72rem;
color: var(--clr-muted);
margin-bottom: 4px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
background: #0f172a;
border: 1px solid var(--clr-border);
border-radius: 5px;
padding: 7px 9px;
color: var(--clr-text);
font-size: .82rem;
outline: none;
transition: border-color .2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
border-color: var(--clr-accent);
}
.form-group textarea { resize: vertical; min-height: 56px; }
.form-group select option { background: #0f172a; }
/* Detail rows */
.detail-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
padding: 5px 0;
border-bottom: 1px solid rgba(255,255,255,.05);
font-size: .8rem;
}
.detail-row:last-child { border-bottom: none; }
.detail-label {
color: var(--clr-muted);
font-size: .72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .04em;
white-space: nowrap;
}
.detail-value { color: var(--clr-text); text-align: right; word-break: break-all; }
/* Status badges */
.badge {
display: inline-block;
padding: 2px 7px;
border-radius: 20px;
font-size: .68rem;
font-weight: 700;
text-transform: uppercase;
}
.badge-ramai { background: rgba(249,115,22,.2); color: #f97316; }
.badge-tenang { background: rgba(34,197,94,.2); color: #22c55e; }
.badge-buka { background: rgba(34,197,94,.2); color: #22c55e; }
.badge-tutup { background: rgba(239,68,68,.2); color: #ef4444; }
.badge-ya { background: rgba(251,191,36,.2); color: #fbbf24; }
.badge-tidak { background: rgba(148,163,184,.2); color: #94a3b8; }
/* Buttons */
.btn-row {
display: flex;
gap: 8px;
margin-top: 12px;
flex-wrap: wrap;
}
.btn {
flex: 1;
padding: 7px 12px;
border: none;
border-radius: 5px;
font-size: .78rem;
font-weight: 700;
cursor: pointer;
transition: opacity .2s, transform .1s;
letter-spacing: .03em;
}
.btn:hover { opacity: .85; }
.btn:active { transform: scale(.97); }
.btn-save { background: var(--clr-accent); color: #0f172a; }
.btn-edit { background: var(--clr-info); color: #fff; }
.btn-delete { background: var(--clr-danger); color: #fff; }
.btn-cancel { background: var(--clr-border); color: var(--clr-text); }
.btn-back { background: var(--clr-border); color: var(--clr-text); }
/* Color swatch preview */
.color-preview-row {
display: flex;
align-items: center;
gap: 8px;
}
.color-preview-row input[type="color"] {
width: 36px; height: 32px;
padding: 2px;
cursor: pointer;
flex-shrink: 0;
}
.color-hex-input { flex: 1; }
/* ════════════════════════════════════════════════════════════════════
TOAST NOTIFICATION
════════════════════════════════════════════════════════════════════ */
#toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 99999;
display: flex;
flex-direction: column;
gap: 8px;
}
.toast {
min-width: 220px;
max-width: 320px;
padding: 10px 14px;
border-radius: var(--radius);
font-size: .82rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,.4);
animation: toast-in .3s ease forwards;
border-left: 4px solid transparent;
}
.toast-success { background: #14532d; border-color: var(--clr-success); color: #bbf7d0; }
.toast-error { background: #450a0a; border-color: var(--clr-danger); color: #fecaca; }
.toast-info { background: #1e3a5f; border-color: var(--clr-info); color: #bfdbfe; }
@keyframes toast-in {
from { opacity: 0; transform: translateX(40px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes toast-out {
from { opacity: 1; transform: translateX(0); }
to { opacity: 0; transform: translateX(40px); }
}
/* ════════════════════════════════════════════════════════════════════
LOADING SPINNER
════════════════════════════════════════════════════════════════════ */
#loading-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.45);
z-index: 88888;
display: none;
align-items: center;
justify-content: center;
}
#loading-overlay.active { display: flex; }
.spinner {
width: 44px; height: 44px;
border: 4px solid rgba(255,255,255,.15);
border-top-color: var(--clr-accent);
border-radius: 50%;
animation: spin .7s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ════════════════════════════════════════════════════════════════════
LEAFLET DRAW TOOLBAR override (agar terlihat di dark mode)
════════════════════════════════════════════════════════════════════ */
.leaflet-draw-toolbar a {
background-color: var(--clr-card) !important;
border-color: var(--clr-border) !important;
color: var(--clr-text) !important;
}
.leaflet-draw-toolbar a:hover { background-color: var(--clr-border) !important; }
.leaflet-touch .leaflet-bar a { color: var(--clr-text); }
</style>
</head>
<body>
<!-- ══ HEADER ════════════════════════════════════════════════════════════════ -->
<header id="app-header">
<div class="logo">
<!-- Fuel pump icon -->
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 22V8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v14"/><path d="M2 22h14"/><path d="M15 4h1a2 2 0 0 1 2 2v3a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V7.5L18 4"/><line x1="7" y1="14" x2="11" y2="14"/>
</svg>
<h1>WebGIS <span>SPBU</span></h1>
</div>
<div class="header-hints">
<div class="hint-item"><span class="dot"></span>Klik peta Tambah SPBU</div>
<div class="hint-item"><span class="dot" style="background:#3b82f6"></span>Toolbar kiri Gambar Polyline / Polygon</div>
</div>
<div class="legend-wrap">
<div class="legend-item"><span class="legend-dot" style="background:#22c55e"></span>Tenang+24j</div>
<div class="legend-item"><span class="legend-dot" style="background:#3b82f6"></span>Tenang</div>
<div class="legend-item"><span class="legend-dot" style="background:#f97316"></span>Ramai+24j</div>
<div class="legend-item"><span class="legend-dot" style="background:#ef4444"></span>Ramai</div>
<div class="legend-item"><span class="legend-dot" style="background:#1e293b;border:2px solid #64748b"></span>Tutup</div>
</div>
</header>
<!-- ══ PETA ═══════════════════════════════════════════════════════════════════ -->
<div id="map"></div>
<!-- ══ LOADING OVERLAY ════════════════════════════════════════════════════════ -->
<div id="loading-overlay"><div class="spinner"></div></div>
<!-- ══ TOAST CONTAINER ════════════════════════════════════════════════════════ -->
<div id="toast-container"></div>
<!-- ══ Leaflet JS ──────────────────────────────────────────────────────────── -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- ══ Leaflet Draw JS ─────────────────────────────────────────────────────── -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
/* ═══════════════════════════════════════════════════════════════════════════
INISIALISASI PETA
═══════════════════════════════════════════════════════════════════════════ */
if (typeof L === 'undefined') {
document.body.innerHTML = '<p style="color:red;padding:20px">Leaflet gagal dimuat. Periksa koneksi internet.</p>';
throw new Error('Leaflet not loaded');
}
const MAP_LAT = -0.05566422376778593;
const MAP_LNG = 109.34854622732368;
const MAP_ZOOM = 17;
const map = L.map('map', { zoomControl: true }).setView([MAP_LAT, MAP_LNG], MAP_ZOOM);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors',
maxZoom: 20
}).addTo(map);
/* ═══════════════════════════════════════════════════════════════════════════
LEAFLET DRAW — Toolbar & Drawn Items Layer
═══════════════════════════════════════════════════════════════════════════ */
const drawnItems = new L.FeatureGroup().addTo(map);
const drawControl = new L.Control.Draw({
draw: {
marker: false, // marker dikelola manual via klik peta
circle: false,
rectangle: false,
circlemarker: false,
polyline: {
shapeOptions: { color: '#3b82f6', weight: 4 }
},
polygon: {
shapeOptions: { color: '#22c55e', weight: 2, fillOpacity: 0.25 }
}
},
edit: false // edit shape tidak diperlukan
});
map.addControl(drawControl);
/* ═══════════════════════════════════════════════════════════════════════════
STORAGE OBJEK (key = id database)
═══════════════════════════════════════════════════════════════════════════ */
const markersMap = {}; // { id: L.marker }
const polylinesMap = {}; // { id: L.polyline }
const polygonsMap = {}; // { id: L.polygon }
/* ═══════════════════════════════════════════════════════════════════════════
HELPER: LOADING & TOAST
═══════════════════════════════════════════════════════════════════════════ */
function showLoading() { document.getElementById('loading-overlay').classList.add('active'); }
function hideLoading() { document.getElementById('loading-overlay').classList.remove('active'); }
function showToast(msg, type = 'info') {
const c = document.getElementById('toast-container');
const el = document.createElement('div');
el.className = `toast toast-${type}`;
const icon = type === 'success' ? '✓' : type === 'error' ? '✕' : '';
el.innerHTML = `<span>${icon}</span><span>${msg}</span>`;
c.appendChild(el);
setTimeout(() => {
el.style.animation = 'toast-out .3s ease forwards';
setTimeout(() => el.remove(), 300);
}, 3000);
}
/* ═══════════════════════════════════════════════════════════════════════════
HELPER: WARNA MARKER SPBU
Aturan warna sesuai spesifikasi
═══════════════════════════════════════════════════════════════════════════ */
function getMarkerColor(spbu) {
const { status_operasional, status_ramai, buka_24_jam } = spbu;
if (status_operasional === 'tutup') return '#1e293b'; // hitam (dark)
if (status_ramai === 'ramai' && buka_24_jam === 'ya') return '#f97316'; // orange
if (status_ramai === 'tidak_ramai'&& buka_24_jam === 'ya') return '#22c55e'; // hijau
if (status_ramai === 'ramai' && buka_24_jam === 'tidak') return '#ef4444'; // merah
if (status_ramai === 'tidak_ramai'&& buka_24_jam === 'tidak') return '#3b82f6'; // biru
return '#94a3b8'; // fallback abu
}
/* Buat SVG inline marker */
function makeDivIcon(color) {
const border = color === '#1e293b' ? '#64748b' : color;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="36" viewBox="0 0 28 36">
<path d="M14 0C6.268 0 0 6.268 0 14c0 9.333 14 22 14 22S28 23.333 28 14C28 6.268 21.732 0 14 0z"
fill="${color}" stroke="${border}" stroke-width="2"/>
<circle cx="14" cy="14" r="5" fill="white" opacity="0.85"/>
</svg>`;
return L.divIcon({
html: svg,
className: '',
iconSize: [28, 36],
iconAnchor: [14, 36],
popupAnchor:[0, -36]
});
}
/* ═══════════════════════════════════════════════════════════════════════════
RENDER MARKER SPBU
═══════════════════════════════════════════════════════════════════════════ */
function renderMarker(spbu) {
const color = getMarkerColor(spbu);
const marker = L.marker([spbu.latitude, spbu.longitude], { icon: makeDivIcon(color) }).addTo(map);
// Bind popup detail saat klik marker
marker.on('click', function() {
const popup = showDetailSpbuPopup(spbu);
marker.bindPopup(popup, { maxWidth: 340 }).openPopup();
});
markersMap[spbu.id] = marker;
}
/* ═══════════════════════════════════════════════════════════════════════════
RENDER POLYLINE
═══════════════════════════════════════════════════════════════════════════ */
function renderPolyline(data) {
// koordinat berupa array of [lat, lng]
const latlngs = data.koordinat.map(k => [k[0], k[1]]);
const line = L.polyline(latlngs, {
color: data.warna || '#3b82f6',
weight: 4,
opacity: 0.9
}).addTo(map);
line.on('click', function(e) {
L.DomEvent.stopPropagation(e);
showDetailPolylinePopup(data, e.latlng);
});
polylinesMap[data.id] = line;
}
/* ═══════════════════════════════════════════════════════════════════════════
RENDER POLYGON
═══════════════════════════════════════════════════════════════════════════ */
function renderPolygon(data) {
const latlngs = data.koordinat.map(k => [k[0], k[1]]);
const poly = L.polygon(latlngs, {
color: data.warna || '#22c55e',
weight: 2,
fillColor: data.warna || '#22c55e',
fillOpacity: 0.25,
opacity: 0.9
}).addTo(map);
poly.on('click', function(e) {
L.DomEvent.stopPropagation(e);
showDetailPolygonPopup(data, e.latlng);
});
polygonsMap[data.id] = poly;
}
/* ═══════════════════════════════════════════════════════════════════════════
LOAD SEMUA DATA DARI DATABASE
═══════════════════════════════════════════════════════════════════════════ */
async function loadData() {
showLoading();
try {
const res = await fetch('ambil_data.php');
const json = await res.json();
if (json.status === 'success' || json.status === 'partial_error') {
// Bersihkan layer lama
Object.values(markersMap).forEach(m => map.removeLayer(m));
Object.keys(markersMap).forEach(k => delete markersMap[k]);
Object.values(polylinesMap).forEach(m => map.removeLayer(m));
Object.keys(polylinesMap).forEach(k => delete polylinesMap[k]);
Object.values(polygonsMap).forEach(m => map.removeLayer(m));
Object.keys(polygonsMap).forEach(k => delete polygonsMap[k]);
// Render ulang
json.spbu.forEach(renderMarker);
json.polyline.forEach(renderPolyline);
json.polygon.forEach(renderPolygon);
} else {
showToast('Gagal memuat data dari server.', 'error');
}
} catch (err) {
showToast('Error koneksi: ' + err.message, 'error');
} finally {
hideLoading();
}
}
/* ═══════════════════════════════════════════════════════════════════════════
HAPUS DATA (SPBU / POLYLINE / POLYGON)
═══════════════════════════════════════════════════════════════════════════ */
async function hapusData(tipe, id) {
if (!confirm(`Yakin ingin menghapus ${tipe} ini?`)) return;
showLoading();
try {
const fd = new FormData();
fd.append('tipe', tipe);
fd.append('id', id);
const res = await fetch('hapus.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
// Hapus dari peta
if (tipe === 'spbu' && markersMap[id]) {
map.removeLayer(markersMap[id]);
delete markersMap[id];
} else if (tipe === 'polyline' && polylinesMap[id]) {
map.removeLayer(polylinesMap[id]);
delete polylinesMap[id];
} else if (tipe === 'polygon' && polygonsMap[id]) {
map.removeLayer(polygonsMap[id]);
delete polygonsMap[id];
}
map.closePopup();
showToast(json.message, 'success');
} else {
showToast(json.message || 'Gagal menghapus.', 'error');
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
} finally {
hideLoading();
}
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: TAMBAH SPBU (klik peta)
═══════════════════════════════════════════════════════════════════════════ */
function showTambahSpbuPopup(latlng) {
const content = `
<div class="popup-box">
<h3> Tambah SPBU</h3>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="spbu-nama" placeholder="Contoh: SPBU 64.773.xx" />
</div>
<div class="form-group">
<label>Status Keramaian</label>
<select id="spbu-ramai">
<option value="ramai">Ramai</option>
<option value="tidak_ramai">Tidak Ramai</option>
</select>
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<select id="spbu-24jam">
<option value="ya">Ya</option>
<option value="tidak">Tidak</option>
</select>
</div>
<div class="form-group">
<label>Status Operasional</label>
<select id="spbu-ops">
<option value="buka">Buka</option>
<option value="tutup">Tutup</option>
</select>
</div>
<div class="form-group">
<label>Nomor HP</label>
<input type="text" id="spbu-hp" placeholder="Contoh: 0812xxxxxxxx" />
</div>
<div class="btn-row">
<button class="btn btn-save" id="btn-simpan-spbu">💾 Simpan</button>
<button class="btn btn-cancel" onclick="map.closePopup()">Batal</button>
</div>
</div>`;
const popup = L.popup({ maxWidth: 340, closeButton: true })
.setLatLng(latlng)
.setContent(content)
.openOn(map);
// Setelah popup dibuka, pasang event listener tombol simpan
setTimeout(() => {
const btn = document.getElementById('btn-simpan-spbu');
if (!btn) return;
btn.addEventListener('click', async () => {
const nama = document.getElementById('spbu-nama').value.trim();
const ramai = document.getElementById('spbu-ramai').value;
const jam24 = document.getElementById('spbu-24jam').value;
const ops = document.getElementById('spbu-ops').value;
const hp = document.getElementById('spbu-hp').value.trim();
if (!nama) { showToast('Nama SPBU wajib diisi.', 'error'); return; }
showLoading();
try {
const fd = new FormData();
fd.append('tipe', 'spbu');
fd.append('nama_spbu', nama);
fd.append('status_ramai', ramai);
fd.append('buka_24_jam', jam24);
fd.append('status_operasional', ops);
fd.append('nomor_hp', hp);
fd.append('latitude', latlng.lat);
fd.append('longitude', latlng.lng);
const res = await fetch('tambah.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
map.closePopup();
renderMarker(json.data);
showToast('SPBU berhasil ditambahkan!', 'success');
} else {
showToast(json.message || 'Gagal menyimpan.', 'error');
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
} finally {
hideLoading();
}
});
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: DETAIL SPBU
═══════════════════════════════════════════════════════════════════════════ */
function showDetailSpbuPopup(spbu) {
const badgeRamai = spbu.status_ramai === 'ramai'
? '<span class="badge badge-ramai">Ramai</span>'
: '<span class="badge badge-tenang">Tidak Ramai</span>';
const badgeOps = spbu.status_operasional === 'buka'
? '<span class="badge badge-buka">Buka</span>'
: '<span class="badge badge-tutup">Tutup</span>';
const badge24 = spbu.buka_24_jam === 'ya'
? '<span class="badge badge-ya">Ya</span>'
: '<span class="badge badge-tidak">Tidak</span>';
const content = `
<div class="popup-box">
<h3>⛽ ${escapeHtml(spbu.nama_spbu)}</h3>
<div class="detail-row"><span class="detail-label">Status Ramai</span><span class="detail-value">${badgeRamai}</span></div>
<div class="detail-row"><span class="detail-label">Buka 24 Jam</span><span class="detail-value">${badge24}</span></div>
<div class="detail-row"><span class="detail-label">Operasional</span><span class="detail-value">${badgeOps}</span></div>
<div class="detail-row"><span class="detail-label">Nomor HP</span><span class="detail-value">${escapeHtml(spbu.nomor_hp || '-')}</span></div>
<div class="detail-row"><span class="detail-label">Latitude</span><span class="detail-value">${parseFloat(spbu.latitude).toFixed(6)}</span></div>
<div class="detail-row"><span class="detail-label">Longitude</span><span class="detail-value">${parseFloat(spbu.longitude).toFixed(6)}</span></div>
<div class="btn-row">
<button class="btn btn-edit" id="btn-edit-spbu-${spbu.id}">✏️ Edit</button>
<button class="btn btn-delete" id="btn-hapus-spbu-${spbu.id}">🗑️ Hapus</button>
</div>
</div>`;
setTimeout(() => {
const btnEdit = document.getElementById(`btn-edit-spbu-${spbu.id}`);
const btnHapus = document.getElementById(`btn-hapus-spbu-${spbu.id}`);
if (btnEdit) btnEdit.addEventListener('click', () => showEditSpbuPopup(spbu));
if (btnHapus) btnHapus.addEventListener('click', () => hapusData('spbu', spbu.id));
}, 50);
return content;
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: EDIT SPBU
═══════════════════════════════════════════════════════════════════════════ */
function showEditSpbuPopup(spbu) {
const content = `
<div class="popup-box">
<h3>✏️ Edit SPBU</h3>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="edit-spbu-nama" value="${escapeHtml(spbu.nama_spbu)}" />
</div>
<div class="form-group">
<label>Status Keramaian</label>
<select id="edit-spbu-ramai">
<option value="ramai" ${spbu.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
<option value="tidak_ramai" ${spbu.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
</select>
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<select id="edit-spbu-24jam">
<option value="ya" ${spbu.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
<option value="tidak" ${spbu.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
</select>
</div>
<div class="form-group">
<label>Status Operasional</label>
<select id="edit-spbu-ops">
<option value="buka" ${spbu.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
<option value="tutup" ${spbu.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
</select>
</div>
<div class="form-group">
<label>Nomor HP</label>
<input type="text" id="edit-spbu-hp" value="${escapeHtml(spbu.nomor_hp || '')}" />
</div>
<div class="btn-row">
<button class="btn btn-save" id="btn-update-spbu-${spbu.id}">💾 Simpan</button>
<button class="btn btn-cancel" id="btn-edit-cancel-${spbu.id}">Batal</button>
</div>
</div>`;
// Ganti konten popup yang sudah terbuka
if (markersMap[spbu.id]) {
markersMap[spbu.id].setPopupContent(content).openPopup();
}
setTimeout(() => {
const btnUpdate = document.getElementById(`btn-update-spbu-${spbu.id}`);
const btnCancel = document.getElementById(`btn-edit-cancel-${spbu.id}`);
if (btnCancel) {
btnCancel.addEventListener('click', () => {
if (markersMap[spbu.id]) {
markersMap[spbu.id].setPopupContent(showDetailSpbuPopup(spbu)).openPopup();
}
});
}
if (btnUpdate) {
btnUpdate.addEventListener('click', async () => {
const nama = document.getElementById('edit-spbu-nama').value.trim();
const ramai = document.getElementById('edit-spbu-ramai').value;
const jam24 = document.getElementById('edit-spbu-24jam').value;
const ops = document.getElementById('edit-spbu-ops').value;
const hp = document.getElementById('edit-spbu-hp').value.trim();
if (!nama) { showToast('Nama SPBU wajib diisi.', 'error'); return; }
showLoading();
try {
const fd = new FormData();
fd.append('tipe', 'spbu');
fd.append('id', spbu.id);
fd.append('nama_spbu', nama);
fd.append('status_ramai', ramai);
fd.append('buka_24_jam', jam24);
fd.append('status_operasional', ops);
fd.append('nomor_hp', hp);
const res = await fetch('edit.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
const updatedSpbu = json.data;
// Update icon warna
const newColor = getMarkerColor(updatedSpbu);
markersMap[spbu.id].setIcon(makeDivIcon(newColor));
// Refresh popup detail dengan data baru
markersMap[spbu.id].setPopupContent(showDetailSpbuPopup(updatedSpbu)).openPopup();
showToast('SPBU berhasil diupdate!', 'success');
} else {
showToast(json.message || 'Gagal update.', 'error');
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
} finally {
hideLoading();
}
});
}
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: TAMBAH POLYLINE (setelah selesai menggambar)
═══════════════════════════════════════════════════════════════════════════ */
function showTambahPolylinePopup(latlngs, drawnLayer) {
const koordinatJSON = JSON.stringify(latlngs.map(ll => [ll.lat, ll.lng]));
const center = drawnLayer.getBounds().getCenter();
const content = `
<div class="popup-box">
<h3>📏 Tambah Polyline</h3>
<div class="form-group">
<label>Nama Line</label>
<input type="text" id="line-nama" placeholder="Contoh: Jalur Utama" />
</div>
<div class="form-group">
<label>Keterangan</label>
<textarea id="line-ket" placeholder="Deskripsi polyline..."></textarea>
</div>
<div class="form-group">
<label>Warna</label>
<div class="color-preview-row">
<input type="color" id="line-warna-picker" value="#3b82f6" />
<input type="text" id="line-warna-hex" class="color-hex-input" value="#3b82f6" placeholder="#3b82f6" />
</div>
</div>
<small style="color:var(--clr-muted);font-size:.7rem">${latlngs.length} titik koordinat</small>
<div class="btn-row">
<button class="btn btn-save" id="btn-simpan-line">💾 Simpan</button>
<button class="btn btn-cancel" id="btn-cancel-line">Batal</button>
</div>
</div>`;
const popup = L.popup({ maxWidth: 340 })
.setLatLng(center)
.setContent(content)
.openOn(map);
setTimeout(() => {
// Sinkron color picker dan hex input
const picker = document.getElementById('line-warna-picker');
const hexInp = document.getElementById('line-warna-hex');
if (picker && hexInp) {
picker.addEventListener('input', () => { hexInp.value = picker.value; });
hexInp.addEventListener('input', () => { if (/^#[0-9a-fA-F]{6}$/.test(hexInp.value)) picker.value = hexInp.value; });
}
const btnCancel = document.getElementById('btn-cancel-line');
if (btnCancel) {
btnCancel.addEventListener('click', () => {
map.removeLayer(drawnLayer);
map.closePopup();
});
}
const btnSimpan = document.getElementById('btn-simpan-line');
if (!btnSimpan) return;
btnSimpan.addEventListener('click', async () => {
const nama = document.getElementById('line-nama').value.trim();
const ket = document.getElementById('line-ket').value.trim();
const warna = document.getElementById('line-warna-hex').value.trim() || '#3b82f6';
if (!nama) { showToast('Nama line wajib diisi.', 'error'); return; }
showLoading();
try {
const fd = new FormData();
fd.append('tipe', 'polyline');
fd.append('nama_line', nama);
fd.append('keterangan', ket);
fd.append('warna', warna);
fd.append('koordinat', koordinatJSON);
const res = await fetch('tambah.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
map.removeLayer(drawnLayer); // hapus layer sementara dari draw
map.closePopup();
// Render polyline dari data yang baru disimpan
renderPolyline({
id: json.data.id,
nama_line: json.data.nama_line,
keterangan: json.data.keterangan,
warna: json.data.warna,
koordinat: JSON.parse(json.data.koordinat)
});
showToast('Polyline berhasil disimpan!', 'success');
} else {
showToast(json.message || 'Gagal menyimpan.', 'error');
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
} finally {
hideLoading();
}
});
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: DETAIL POLYLINE
═══════════════════════════════════════════════════════════════════════════ */
function showDetailPolylinePopup(data, latlng) {
const jumlah = Array.isArray(data.koordinat) ? data.koordinat.length : '?';
const content = `
<div class="popup-box">
<h3>📏 ${escapeHtml(data.nama_line)}</h3>
<div class="detail-row"><span class="detail-label">Keterangan</span><span class="detail-value">${escapeHtml(data.keterangan || '-')}</span></div>
<div class="detail-row"><span class="detail-label">Warna</span><span class="detail-value"><span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${escapeHtml(data.warna)};vertical-align:middle;margin-right:4px"></span>${escapeHtml(data.warna)}</span></div>
<div class="detail-row"><span class="detail-label">Titik Koordinat</span><span class="detail-value">${jumlah} titik</span></div>
<div class="btn-row">
<button class="btn btn-delete" id="btn-hapus-line-${data.id}">🗑️ Hapus</button>
</div>
</div>`;
L.popup({ maxWidth: 320 })
.setLatLng(latlng)
.setContent(content)
.openOn(map);
setTimeout(() => {
const btn = document.getElementById(`btn-hapus-line-${data.id}`);
if (btn) btn.addEventListener('click', () => hapusData('polyline', data.id));
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: TAMBAH POLYGON (setelah selesai menggambar)
═══════════════════════════════════════════════════════════════════════════ */
function showTambahPolygonPopup(latlngs, drawnLayer) {
const koordinatJSON = JSON.stringify(latlngs.map(ll => [ll.lat, ll.lng]));
const center = drawnLayer.getBounds().getCenter();
const content = `
<div class="popup-box">
<h3>🔷 Tambah Polygon</h3>
<div class="form-group">
<label>Nama Polygon</label>
<input type="text" id="pgon-nama" placeholder="Contoh: Area Layanan A" />
</div>
<div class="form-group">
<label>Keterangan</label>
<textarea id="pgon-ket" placeholder="Deskripsi area..."></textarea>
</div>
<div class="form-group">
<label>Warna</label>
<div class="color-preview-row">
<input type="color" id="pgon-warna-picker" value="#22c55e" />
<input type="text" id="pgon-warna-hex" class="color-hex-input" value="#22c55e" placeholder="#22c55e" />
</div>
</div>
<small style="color:var(--clr-muted);font-size:.7rem">${latlngs.length} titik koordinat</small>
<div class="btn-row">
<button class="btn btn-save" id="btn-simpan-pgon">💾 Simpan</button>
<button class="btn btn-cancel" id="btn-cancel-pgon">Batal</button>
</div>
</div>`;
const popup = L.popup({ maxWidth: 340 })
.setLatLng(center)
.setContent(content)
.openOn(map);
setTimeout(() => {
const picker = document.getElementById('pgon-warna-picker');
const hexInp = document.getElementById('pgon-warna-hex');
if (picker && hexInp) {
picker.addEventListener('input', () => { hexInp.value = picker.value; });
hexInp.addEventListener('input', () => { if (/^#[0-9a-fA-F]{6}$/.test(hexInp.value)) picker.value = hexInp.value; });
}
const btnCancel = document.getElementById('btn-cancel-pgon');
if (btnCancel) {
btnCancel.addEventListener('click', () => {
map.removeLayer(drawnLayer);
map.closePopup();
});
}
const btnSimpan = document.getElementById('btn-simpan-pgon');
if (!btnSimpan) return;
btnSimpan.addEventListener('click', async () => {
const nama = document.getElementById('pgon-nama').value.trim();
const ket = document.getElementById('pgon-ket').value.trim();
const warna = document.getElementById('pgon-warna-hex').value.trim() || '#22c55e';
if (!nama) { showToast('Nama polygon wajib diisi.', 'error'); return; }
showLoading();
try {
const fd = new FormData();
fd.append('tipe', 'polygon');
fd.append('nama_polygon', nama);
fd.append('keterangan', ket);
fd.append('warna', warna);
fd.append('koordinat', koordinatJSON);
const res = await fetch('tambah.php', { method: 'POST', body: fd });
const json = await res.json();
if (json.status === 'success') {
map.removeLayer(drawnLayer);
map.closePopup();
renderPolygon({
id: json.data.id,
nama_polygon: json.data.nama_polygon,
keterangan: json.data.keterangan,
warna: json.data.warna,
koordinat: JSON.parse(json.data.koordinat)
});
showToast('Polygon berhasil disimpan!', 'success');
} else {
showToast(json.message || 'Gagal menyimpan.', 'error');
}
} catch (err) {
showToast('Error: ' + err.message, 'error');
} finally {
hideLoading();
}
});
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
POPUP: DETAIL POLYGON
═══════════════════════════════════════════════════════════════════════════ */
function showDetailPolygonPopup(data, latlng) {
const jumlah = Array.isArray(data.koordinat) ? data.koordinat.length : '?';
const content = `
<div class="popup-box">
<h3>🔷 ${escapeHtml(data.nama_polygon)}</h3>
<div class="detail-row"><span class="detail-label">Keterangan</span><span class="detail-value">${escapeHtml(data.keterangan || '-')}</span></div>
<div class="detail-row"><span class="detail-label">Warna</span><span class="detail-value"><span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${escapeHtml(data.warna)};vertical-align:middle;margin-right:4px"></span>${escapeHtml(data.warna)}</span></div>
<div class="detail-row"><span class="detail-label">Titik Koordinat</span><span class="detail-value">${jumlah} titik</span></div>
<div class="btn-row">
<button class="btn btn-delete" id="btn-hapus-pgon-${data.id}">🗑️ Hapus</button>
</div>
</div>`;
L.popup({ maxWidth: 320 })
.setLatLng(latlng)
.setContent(content)
.openOn(map);
setTimeout(() => {
const btn = document.getElementById(`btn-hapus-pgon-${data.id}`);
if (btn) btn.addEventListener('click', () => hapusData('polygon', data.id));
}, 50);
}
/* ═══════════════════════════════════════════════════════════════════════════
EVENT: Klik peta → tambah SPBU
(hanya jika bukan sedang dalam mode draw)
═══════════════════════════════════════════════════════════════════════════ */
let isDrawing = false;
map.on('draw:drawstart', () => { isDrawing = true; });
map.on('draw:drawstop', () => { isDrawing = false; });
map.on('click', function(e) {
if (isDrawing) return; // abaikan jika sedang menggambar
showTambahSpbuPopup(e.latlng);
});
/* ═══════════════════════════════════════════════════════════════════════════
EVENT: Leaflet Draw — setelah shape selesai digambar
═══════════════════════════════════════════════════════════════════════════ */
map.on(L.Draw.Event.CREATED, function(e) {
const layer = e.layer;
const type = e.layerType;
if (type === 'polyline') {
const latlngs = layer.getLatLngs();
// Jangan tambahkan ke drawnItems, biarkan handler yang kelola
showTambahPolylinePopup(latlngs, layer);
} else if (type === 'polygon') {
// Polygon getLatLngs() mengembalikan array of array, ambil ring pertama
const latlngs = layer.getLatLngs()[0];
showTambahPolygonPopup(latlngs, layer);
}
});
/* ═══════════════════════════════════════════════════════════════════════════
HELPER: escape HTML untuk mencegah XSS
═══════════════════════════════════════════════════════════════════════════ */
function escapeHtml(str) {
if (str === null || str === undefined) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
/* ═══════════════════════════════════════════════════════════════════════════
INIT — load semua data saat halaman pertama kali dibuka
═══════════════════════════════════════════════════════════════════════════ */
loadData();
</script>
</body>
</html>