976 lines
34 KiB
HTML
976 lines
34 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 — 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,'&').replace(/</g,'<')
|
||
.replace(/>/g,'>').replace(/"/g,'"');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// INIT
|
||
// ═══════════════════════════════════════════════════════════
|
||
// Ekspos ke window agar bisa dipanggil dari dalam popup Leaflet
|
||
window.openEdit = openEdit;
|
||
window.deleteSpbu = deleteSpbu;
|
||
window.startMovePin = startMovePin;
|
||
|
||
fetchAll();
|
||
</script>
|
||
</body>
|
||
</html> |