Files
2026-06-10 19:23:50 +07:00

976 lines
34 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 — Kota Pontianak</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/>
<style>
/* ══════════════════════════════════════
RESET & VARIABLES
══════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0d1117;
--surface: #161b22;
--surface2: #21262d;
--border: #30363d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #f97316;
--accent-dk: #c2591a;
--green: #3fb950;
--red: #f85149;
--radius: 12px;
--shadow: 0 8px 32px rgba(0,0,0,.55);
}
html, body {
height: 100%;
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--bg);
color: var(--text);
}
/* ══════════════════════════════════════
LAYOUT
══════════════════════════════════════ */
.app {
display: grid;
grid-template-columns: 360px 1fr;
grid-template-rows: 60px 1fr;
height: 100vh;
}
/* ── Header ─────────────────────────── */
header {
grid-column: 1 / -1;
display: flex;
align-items: center;
gap: 14px;
padding: 0 24px;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 10;
}
.logo-icon {
width: 34px; height: 34px;
background: var(--accent);
border-radius: 8px;
display: grid; place-items: center;
font-size: 18px;
}
header h1 {
font-size: 15px; font-weight: 700;
letter-spacing: -.2px;
}
header h1 span { color: var(--accent); }
.header-meta {
margin-left: auto;
font-family: 'Space Mono', monospace;
font-size: 11px;
color: var(--muted);
background: var(--surface2);
padding: 5px 12px;
border-radius: 20px;
border: 1px solid var(--border);
}
.dot-live {
display: inline-block;
width: 7px; height: 7px;
border-radius: 50%;
background: var(--green);
margin-right: 6px;
animation: pulse 1.8s infinite;
}
@keyframes pulse {
0%,100% { opacity: 1; }
50% { opacity: .35; }
}
/* ── Sidebar ─────────────────────────── */
aside {
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-header {
padding: 18px 20px 14px;
border-bottom: 1px solid var(--border);
}
.sidebar-header h2 {
font-size: 13px; font-weight: 600;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--muted);
margin-bottom: 4px;
}
.count-badge {
font-family: 'Space Mono', monospace;
font-size: 26px; font-weight: 700;
color: var(--accent);
line-height: 1;
}
.count-badge small { font-size: 12px; color: var(--muted); font-family: inherit; }
/* stats row */
.stats-row {
display: flex; gap: 8px;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
}
.stat-pill {
flex: 1; text-align: center;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 4px;
font-size: 11px; color: var(--muted);
}
.stat-pill strong { display: block; font-size: 18px; font-weight: 700; margin-bottom: 2px; }
.stat-pill.green strong { color: var(--green); }
.stat-pill.red strong { color: var(--red); }
/* search */
.search-wrap {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
}
.search-wrap input {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px 8px 34px;
color: var(--text);
font-size: 13px;
font-family: inherit;
outline: none;
transition: border-color .2s;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%238b949e' stroke-width='2'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: 10px center;
}
.search-wrap input:focus { border-color: var(--accent); }
/* list */
#spbu-list {
flex: 1;
overflow-y: auto;
padding: 8px 10px;
}
#spbu-list::-webkit-scrollbar { width: 4px; }
#spbu-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.spbu-card {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 13px 15px;
margin-bottom: 8px;
cursor: pointer;
transition: border-color .2s, transform .15s;
position: relative;
}
.spbu-card:hover {
border-color: var(--accent);
transform: translateX(3px);
}
.spbu-card.active { border-color: var(--accent); background: #1d2029; }
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.card-name { font-size: 13px; font-weight: 600; line-height: 1.35; }
.badge {
font-size: 10px; font-weight: 700; padding: 2px 8px;
border-radius: 20px; white-space: nowrap; flex-shrink: 0;
letter-spacing: .4px; text-transform: uppercase;
}
.badge.yes { background: rgba(63,185,80,.18); color: var(--green); border: 1px solid var(--green); }
.badge.no { background: rgba(248,81,73,.15); color: var(--red); border: 1px solid var(--red); }
.card-wa {
font-size: 11px; color: var(--muted); margin-top: 5px;
display: flex; align-items: center; gap: 5px;
}
.card-wa svg { flex-shrink: 0; }
.card-actions {
display: flex; gap: 6px; margin-top: 10px;
}
.btn-sm {
flex: 1; padding: 5px 0;
font-size: 11px; font-weight: 600;
border-radius: 6px; cursor: pointer;
border: 1px solid var(--border);
background: var(--surface);
color: var(--muted);
font-family: inherit;
transition: all .18s;
}
.btn-sm:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-sm.danger:hover { background: var(--red); border-color: var(--red); }
.btn-sm.move:hover { background: #f97316; border-color: #f97316; color: #fff; }
/* add-new CTA */
.cta-box {
padding: 14px 16px;
border-top: 1px solid var(--border);
}
.btn-add {
width: 100%;
background: var(--accent);
color: #fff;
border: none;
border-radius: var(--radius);
padding: 11px;
font-size: 13px; font-weight: 700;
font-family: inherit;
cursor: pointer;
transition: background .2s, transform .15s;
display: flex; align-items: center; justify-content: center; gap: 7px;
}
.btn-add:hover { background: var(--accent-dk); transform: translateY(-1px); }
/* ── Map ─────────────────────────────── */
#map {
width: 100%; height: 100%;
background: #1a1a2e;
}
/* ══════════════════════════════════════
MODAL
══════════════════════════════════════ */
.overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.7);
backdrop-filter: blur(4px);
z-index: 1000;
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none;
transition: opacity .25s;
}
.overlay.open { opacity: 1; pointer-events: all; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 28px 28px 24px;
width: 420px; max-width: 95vw;
box-shadow: var(--shadow);
transform: translateY(12px);
transition: transform .25s;
}
.overlay.open .modal { transform: translateY(0); }
.modal h3 {
font-size: 16px; font-weight: 700; margin-bottom: 20px;
display: flex; align-items: center; gap: 10px;
}
.modal h3 .icon {
width: 32px; height: 32px;
background: var(--accent);
border-radius: 8px;
display: grid; place-items: center;
font-size: 16px;
}
.form-group { margin-bottom: 16px; }
.form-group label {
display: block; font-size: 12px; font-weight: 600;
color: var(--muted); margin-bottom: 6px;
text-transform: uppercase; letter-spacing: .5px;
}
.form-group input,
.form-group select {
width: 100%;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
color: var(--text);
font-size: 14px;
font-family: inherit;
outline: none;
transition: border-color .2s;
appearance: none;
}
.form-group input:focus,
.form-group select:focus { border-color: var(--accent); }
.coord-row {
display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
}
.form-actions {
display: flex; gap: 10px; margin-top: 20px;
}
.btn-cancel {
flex: 1; padding: 11px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--muted);
font-size: 13px; font-weight: 600;
font-family: inherit; cursor: pointer;
transition: all .2s;
}
.btn-cancel:hover { border-color: var(--red); color: var(--red); }
.btn-save {
flex: 2; padding: 11px;
background: var(--accent);
border: none; border-radius: var(--radius);
color: #fff;
font-size: 13px; font-weight: 700;
font-family: inherit; cursor: pointer;
transition: background .2s;
}
.btn-save:hover { background: var(--accent-dk); }
/* coord hint */
.coord-hint {
font-size: 11px; color: var(--muted);
background: var(--surface2);
border: 1px dashed var(--border);
border-radius: 8px;
padding: 8px 12px; margin-bottom: 16px;
display: flex; align-items: center; gap: 7px;
}
/* ── Floating bar pindah titik ───────── */
#move-bar {
position: absolute;
bottom: 32px; left: 50%; transform: translateX(-50%) translateY(80px);
z-index: 900;
background: var(--surface);
border: 1.5px solid var(--accent);
border-radius: 40px;
padding: 10px 16px 10px 20px;
display: flex; align-items: center; gap: 12px;
box-shadow: 0 8px 28px rgba(0,0,0,.6), 0 0 0 4px rgba(249,115,22,.12);
opacity: 0; pointer-events: none;
transition: transform .3s cubic-bezier(.34,1.56,.64,1), opacity .25s;
white-space: nowrap;
}
#move-bar.active {
transform: translateX(-50%) translateY(0);
opacity: 1; pointer-events: all;
}
.move-bar-label {
font-size: 12px; font-weight: 600; color: var(--accent);
display: flex; align-items: center; gap: 7px;
}
.move-bar-label .pulse-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent);
animation: pulse 1.2s infinite;
}
.btn-confirm {
padding: 6px 16px;
background: var(--green); border: none;
border-radius: 20px; color: #fff;
font-size: 12px; font-weight: 700;
font-family: inherit; cursor: pointer;
transition: opacity .2s;
}
.btn-confirm:hover { opacity: .85; }
.btn-cancel-move {
padding: 6px 14px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px; color: var(--muted);
font-size: 12px; font-weight: 600;
font-family: inherit; cursor: pointer;
transition: all .2s;
}
.btn-cancel-move:hover { border-color: var(--red); color: var(--red); }
/* ── Toast ───────────────────────────── */
#toast {
position: fixed; bottom: 24px; right: 24px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 18px;
font-size: 13px; font-weight: 500;
box-shadow: var(--shadow);
z-index: 9999;
transform: translateY(20px);
opacity: 0;
transition: all .3s;
display: flex; align-items: center; gap: 9px;
}
#toast.show { transform: translateY(0); opacity: 1; }
#toast.success { border-color: var(--green); }
#toast.error { border-color: var(--red); }
/* ── Leaflet popup custom ────────────── */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
box-shadow: var(--shadow) !important;
color: var(--text) !important;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content { margin: 14px 16px !important; }
.popup-title { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
.popup-wa { font-size: 12px; color: var(--muted); }
.popup-status { margin-top: 8px; }
/* empty state */
.empty-state {
text-align: center;
padding: 40px 20px;
color: var(--muted);
font-size: 13px;
}
.empty-state .emoji { font-size: 36px; margin-bottom: 10px; }
@media (max-width: 640px) {
.app { grid-template-columns: 1fr; grid-template-rows: 60px 1fr 260px; }
aside { border-right: none; border-bottom: 1px solid var(--border); }
}
</style>
</head>
<body>
<div class="app">
<!-- ═══════════ HEADER ═══════════ -->
<header>
<div class="logo-icon"></div>
<h1>WebGIS <span>SPBU</span> Pontianak</h1>
<div class="header-meta">
<span class="dot-live"></span>Live Map
</div>
</header>
<!-- ═══════════ SIDEBAR ═══════════ -->
<aside>
<div class="sidebar-header">
<h2>Total SPBU</h2>
<div class="count-badge" id="total-count">0 <small>titik</small></div>
</div>
<div class="stats-row">
<div class="stat-pill green">
<strong id="stat-24">0</strong>
24 Jam
</div>
<div class="stat-pill red">
<strong id="stat-non24">0</strong>
Non-24 Jam
</div>
</div>
<div class="search-wrap">
<input type="text" id="search-input" placeholder="Cari nama SPBU…" oninput="filterList()"/>
</div>
<div id="spbu-list">
<div class="empty-state">
<div class="emoji">🗺️</div>
Memuat data…
</div>
</div>
<div class="cta-box">
<button class="btn-add" onclick="openAddMode()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Klik Peta untuk Tambah SPBU
</button>
</div>
</aside>
<!-- ═══════════ MAP ═══════════ -->
<div id="map" style="position:relative;">
<!-- Floating bar konfirmasi pindah titik -->
<div id="move-bar">
<div class="move-bar-label">
<span class="pulse-dot"></span>
Geser pin ke lokasi baru
</div>
<button class="btn-confirm" onclick="confirmMove()">✅ Konfirmasi</button>
<button class="btn-cancel-move" onclick="cancelMove()">✕ Batal</button>
</div>
</div>
</div>
<!-- ═══════════ MODAL FORM ═══════════ -->
<div class="overlay" id="modal-overlay">
<div class="modal">
<h3>
<span class="icon"></span>
<span id="modal-title">Tambah SPBU</span>
</h3>
<div class="coord-hint" id="coord-hint">
📍 Koordinat diambil dari titik yang Anda klik di peta
</div>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pertamina Jl. Ahmad Yani"/>
</div>
<div class="form-group">
<label>Nomor WhatsApp</label>
<input type="text" id="f-wa" placeholder="Contoh: 6281234567890"/>
</div>
<div class="form-group">
<label>Status 24 Jam</label>
<select id="f-status">
<option value="yes">✅ Ya Buka 24 Jam</option>
<option value="no">❌ Tidak Jam Terbatas</option>
</select>
</div>
<div class="coord-row">
<div class="form-group">
<label>Latitude</label>
<input type="number" id="f-lat" step="0.000001" placeholder="-0.0262619"/>
</div>
<div class="form-group">
<label>Longitude</label>
<input type="number" id="f-lng" step="0.000001" placeholder="109.3425923"/>
</div>
</div>
<div class="form-actions">
<button class="btn-cancel" onclick="closeModal()">Batal</button>
<button class="btn-save" onclick="saveSpbu()">💾 Simpan</button>
</div>
</div>
</div>
<!-- Toast -->
<div id="toast"></div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ═══════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════
let spbuData = [];
let markers = {}; // id → marker
let editId = null;
let addMode = false;
let tempMarker = null;
// ═══════════════════════════════════════════════════════════
// MAP INIT — center: Pontianak
// ═══════════════════════════════════════════════════════════
const map = L.map('map', {
center: [-0.0263, 109.3425],
zoom: 13,
zoomControl: false
});
L.control.zoom({ position: 'bottomright' }).addTo(map);
// Tile layer OpenStreetMap
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
maxZoom: 19
}).addTo(map);
// ── Custom icons ──────────────────────────────────────────
function makeIcon(is24) {
return L.divIcon({
className: '',
html: `<div style="
width:36px;height:36px;
background:${is24 ? '#3fb950' : '#f97316'};
border:3px solid #fff;
border-radius:50% 50% 50% 0;
transform:rotate(-45deg);
box-shadow:0 3px 10px rgba(0,0,0,.5);
"></div>`,
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor: [0, -38]
});
}
const tempIcon = L.divIcon({
className: '',
html: `<div style="
width:34px;height:34px;
background:#f97316;
border:3px solid #fff;
border-radius:50% 50% 50% 0;
transform:rotate(-45deg);
box-shadow:0 3px 14px rgba(249,115,22,.7);
animation: blink .7s infinite alternate;
"></div>
<style>@keyframes blink{from{opacity:1}to{opacity:.4}}</style>`,
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -36]
});
// ═══════════════════════════════════════════════════════════
// API CALLS
// ═══════════════════════════════════════════════════════════
const API = 'api.php';
async function fetchAll() {
try {
const r = await fetch(API);
const j = await r.json();
if (j.status === 'success') {
spbuData = j.data;
renderList();
renderMarkers();
updateStats();
}
} catch(e) { showToast('Gagal memuat data. Pastikan server berjalan.', 'error'); }
}
async function saveSpbu() {
const nama = document.getElementById('f-nama').value.trim();
const wa = document.getElementById('f-wa').value.trim();
const status = document.getElementById('f-status').value;
const lat = parseFloat(document.getElementById('f-lat').value);
const lng = parseFloat(document.getElementById('f-lng').value);
if (!nama || !wa || isNaN(lat) || isNaN(lng)) {
showToast('Harap isi semua field dengan benar!', 'error'); return;
}
const payload = { nama_spbu: nama, nomor_wa: wa, status_24jam: status, latitude: lat, longitude: lng };
try {
const r = await fetch(API + (editId ? `?id=${editId}` : ''), {
method : editId ? 'PUT' : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(payload)
});
const j = await r.json();
if (j.status === 'success') {
showToast(editId ? 'SPBU berhasil diperbarui! ✏️' : 'SPBU berhasil ditambahkan! ⛽', 'success');
closeModal();
fetchAll();
} else {
showToast('Gagal menyimpan: ' + j.message, 'error');
}
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
}
async function deleteSpbu(id) {
if (!confirm('Yakin hapus SPBU ini?')) return;
try {
const r = await fetch(`${API}?id=${id}`, { method: 'DELETE' });
const j = await r.json();
if (j.status === 'success') {
showToast('SPBU berhasil dihapus! 🗑️', 'success');
fetchAll();
}
} catch(e) { showToast('Gagal menghapus.', 'error'); }
}
// ═══════════════════════════════════════════════════════════
// RENDER
// ═══════════════════════════════════════════════════════════
function renderMarkers() {
// clear old
Object.values(markers).forEach(m => map.removeLayer(m));
markers = {};
spbuData.forEach(s => {
const m = L.marker([s.latitude, s.longitude], { icon: makeIcon(s.status_24jam === 'yes') })
.addTo(map)
.bindPopup(popupHTML(s));
m.on('click', () => highlightCard(s.id));
markers[s.id] = m;
});
}
function popupHTML(s) {
return `
<div class="popup-title">⛽ ${escHtml(s.nama_spbu)}</div>
<div class="popup-wa">
📱 ${escHtml(s.nomor_wa)}
</div>
<div class="popup-status">
<span class="badge ${s.status_24jam}">
${s.status_24jam === 'yes' ? '✅ Buka 24 Jam' : '❌ Jam Terbatas'}
</span>
</div>
<div style="margin-top:10px;display:flex;gap:6px;flex-wrap:wrap">
<button onclick="openEdit(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #30363d;background:#21262d;color:#e6edf3;font-family:inherit">✏️ Edit</button>
<button onclick="startMovePin(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f97316;background:rgba(249,115,22,.1);color:#f97316;font-family:inherit">📌 Pindahkan</button>
<button onclick="deleteSpbu(${s.id})" style="flex:1;padding:6px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid #f85149;background:rgba(248,81,73,.1);color:#f85149;font-family:inherit">🗑️ Hapus</button>
</div>`;
}
function renderList(filter = '') {
const list = document.getElementById('spbu-list');
const q = filter.toLowerCase();
const filtered = spbuData.filter(s => s.nama_spbu.toLowerCase().includes(q));
if (!filtered.length) {
list.innerHTML = `<div class="empty-state"><div class="emoji">${filter ? '🔍' : '📍'}</div>${filter ? 'Tidak ditemukan' : 'Belum ada data SPBU'}</div>`;
return;
}
list.innerHTML = filtered.map(s => `
<div class="spbu-card" id="card-${s.id}" onclick="flyTo(${s.id})">
<div class="card-top">
<div class="card-name">${escHtml(s.nama_spbu)}</div>
<span class="badge ${s.status_24jam}">${s.status_24jam === 'yes' ? '24 Jam' : 'Non-24'}</span>
</div>
<div class="card-wa">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8 19.79 19.79 0 01.01 1.18C0 .1.68-.15 1.91.08A2 2 0 013.9 2.05v3a2 2 0 01-1.45 1.93 16 16 0 006.39 6.39A2 2 0 0110.77 12h.1l.07.06"/></svg>
${escHtml(s.nomor_wa)}
</div>
<div class="card-actions">
<button class="btn-sm" onclick="event.stopPropagation();openEdit(${s.id})">✏️ Edit</button>
<button class="btn-sm move" onclick="event.stopPropagation();startMovePin(${s.id})">📌 Pindahkan</button>
<button class="btn-sm danger" onclick="event.stopPropagation();deleteSpbu(${s.id})">🗑️ Hapus</button>
</div>
</div>`
).join('');
}
function updateStats() {
const total = spbuData.length;
const yes = spbuData.filter(s => s.status_24jam === 'yes').length;
document.getElementById('total-count').innerHTML = `${total} <small>titik</small>`;
document.getElementById('stat-24').textContent = yes;
document.getElementById('stat-non24').textContent = total - yes;
}
// ═══════════════════════════════════════════════════════════
// MAP INTERACTIONS
// ═══════════════════════════════════════════════════════════
let addModeActive = false;
function openAddMode() {
addModeActive = true;
map.getContainer().style.cursor = 'crosshair';
showToast('🗺️ Klik pada peta untuk menentukan lokasi SPBU', 'success');
}
map.on('click', function(e) {
if (!addModeActive) return;
addModeActive = false;
map.getContainer().style.cursor = '';
if (tempMarker) map.removeLayer(tempMarker);
tempMarker = L.marker([e.latlng.lat, e.latlng.lng], { icon: tempIcon }).addTo(map);
openModal(null, e.latlng.lat, e.latlng.lng);
});
function flyTo(id) {
const s = spbuData.find(x => x.id === id);
if (!s) return;
map.flyTo([s.latitude, s.longitude], 16, { duration: 1.2 });
setTimeout(() => {
if (markers[id]) markers[id].openPopup();
}, 1300);
highlightCard(id);
}
function highlightCard(id) {
document.querySelectorAll('.spbu-card').forEach(c => c.classList.remove('active'));
const card = document.getElementById(`card-${id}`);
if (card) {
card.classList.add('active');
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
// ═══════════════════════════════════════════════════════════
// MODAL
// ═══════════════════════════════════════════════════════════
function openModal(id = null, lat = '', lng = '') {
editId = id;
const overlay = document.getElementById('modal-overlay');
document.getElementById('modal-title').textContent = id ? 'Edit SPBU' : 'Tambah SPBU Baru';
if (id) {
const s = spbuData.find(x => x.id == id);
if (!s) { showToast('Data SPBU tidak ditemukan!', 'error'); return; }
document.getElementById('f-nama').value = s.nama_spbu;
document.getElementById('f-wa').value = s.nomor_wa;
document.getElementById('f-status').value = s.status_24jam;
document.getElementById('f-lat').value = s.latitude;
document.getElementById('f-lng').value = s.longitude;
document.getElementById('coord-hint').style.display = 'none';
} else {
document.getElementById('f-nama').value = '';
document.getElementById('f-wa').value = '';
document.getElementById('f-status').value = 'no';
document.getElementById('f-lat').value = lat;
document.getElementById('f-lng').value = lng;
document.getElementById('coord-hint').style.display = 'flex';
}
overlay.classList.add('open');
}
function openEdit(id) {
id = parseInt(id);
if (markers[id]) markers[id].closePopup();
openModal(id);
}
function closeModal(cancelled = true) {
document.getElementById('modal-overlay').classList.remove('open');
editId = null;
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
}
// ═══════════════════════════════════════════════════════════
// PINDAHKAN TITIK drag mode tanpa modal
// ═══════════════════════════════════════════════════════════
let _moveId = null;
let _moveOriginal = null;
function startMovePin(id) {
id = parseInt(id);
const s = spbuData.find(x => x.id == id);
if (!s) return;
// Tutup popup jika terbuka
if (markers[id]) markers[id].closePopup();
_moveId = id;
_moveOriginal = { lat: s.latitude, lng: s.longitude };
const m = markers[id];
m.dragging.enable();
// Ganti icon jadi warna biru/terang sebagai penanda aktif
m.setIcon(L.divIcon({
className: '',
html: `<div style="
width:40px;height:40px;
background:#3b82f6;
border:3px solid #fff;
border-radius:50% 50% 50% 0;
transform:rotate(-45deg);
box-shadow:0 0 0 6px rgba(59,130,246,.3),0 4px 14px rgba(0,0,0,.5);
"></div>`,
iconSize: [40,40], iconAnchor: [20,40], popupAnchor: [0,-42]
}));
// Zoom ke marker agar mudah drag
map.flyTo([s.latitude, s.longitude], 16, { duration: .8 });
// Tampilkan floating bar
document.getElementById('move-bar').classList.add('active');
showToast('📌 Geser pin biru ke lokasi baru lalu klik Konfirmasi', 'success');
}
async function confirmMove() {
if (!_moveId) return;
const m = markers[_moveId];
const ll = m.getLatLng();
const s = spbuData.find(x => x.id == _moveId);
try {
const r = await fetch(`api.php?id=${_moveId}`, {
method : 'PUT',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({
nama_spbu : s.nama_spbu,
nomor_wa : s.nomor_wa,
status_24jam: s.status_24jam,
latitude : ll.lat,
longitude : ll.lng
})
});
const j = await r.json();
if (j.status === 'success') {
showToast('📍 Lokasi SPBU berhasil dipindahkan!', 'success');
_endMoveMode(false);
fetchAll();
} else {
showToast('Gagal menyimpan: ' + j.message, 'error');
}
} catch(e) { showToast('Error koneksi ke server.', 'error'); }
}
function cancelMove() {
_endMoveMode(true);
showToast('Pemindahan dibatalkan', 'error');
}
function _endMoveMode(restore) {
if (!_moveId) return;
const m = markers[_moveId];
m.dragging.disable();
// Kembalikan posisi & icon asli jika dibatalkan
if (restore && _moveOriginal) {
m.setLatLng([_moveOriginal.lat, _moveOriginal.lng]);
}
const s = spbuData.find(x => x.id == _moveId);
if (s) m.setIcon(makeIcon(s.status_24jam === 'yes'));
document.getElementById('move-bar').classList.remove('active');
_moveId = null;
_moveOriginal = null;
}
document.getElementById('modal-overlay').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// ═══════════════════════════════════════════════════════════
// SEARCH
// ═══════════════════════════════════════════════════════════
function filterList() {
const q = document.getElementById('search-input').value;
renderList(q);
}
// ═══════════════════════════════════════════════════════════
// TOAST
// ═══════════════════════════════════════════════════════════
let toastTimer;
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = `show ${type}`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.className = ''; }, 3200);
}
// ═══════════════════════════════════════════════════════════
// UTILS
// ═══════════════════════════════════════════════════════════
function escHtml(str) {
return String(str)
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ═══════════════════════════════════════════════════════════
// INIT
// ═══════════════════════════════════════════════════════════
// Ekspos ke window agar bisa dipanggil dari dalam popup Leaflet
window.openEdit = openEdit;
window.deleteSpbu = deleteSpbu;
window.startMovePin = startMovePin;
fetchAll();
</script>
</body>
</html>