1031 lines
45 KiB
HTML
1031 lines
45 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 — Layer Groups & Layers Control</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-badges {
|
||
margin-left: auto;
|
||
display: flex; gap: 8px; align-items: center;
|
||
}
|
||
.header-badge {
|
||
font-family: 'Space Mono', monospace;
|
||
font-size: 10px; font-weight: 700;
|
||
padding: 4px 10px;
|
||
border-radius: 20px;
|
||
display: flex; align-items: center; gap: 5px;
|
||
}
|
||
.badge-24 { background: rgba(63,185,80,.15); color: var(--green); border: 1px solid var(--green); }
|
||
.badge-non24{ background: rgba(249,115,22,.15); color: var(--accent); border: 1px solid var(--accent); }
|
||
.dot-sm { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
|
||
|
||
.header-meta {
|
||
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: livePulse 1.8s infinite;
|
||
}
|
||
@keyframes livePulse { 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.orange strong { color: var(--accent); }
|
||
|
||
/* ── Layer Toggle Panel ──────────────── */
|
||
.layer-panel {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: rgba(249,115,22,.04);
|
||
}
|
||
.layer-panel-title {
|
||
font-size: 11px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: .8px;
|
||
color: var(--muted); margin-bottom: 10px;
|
||
display: flex; align-items: center; gap: 6px;
|
||
}
|
||
.layer-toggles { display: flex; gap: 8px; }
|
||
|
||
.layer-toggle {
|
||
flex: 1;
|
||
display: flex; align-items: center; gap: 8px;
|
||
background: var(--surface2);
|
||
border: 1.5px solid var(--border);
|
||
border-radius: 10px;
|
||
padding: 9px 12px;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
user-select: none;
|
||
}
|
||
.layer-toggle:hover { border-color: var(--muted); }
|
||
.layer-toggle.active-24 { border-color: var(--green); background: rgba(63,185,80,.1); }
|
||
.layer-toggle.active-non24{ border-color: var(--accent); background: rgba(249,115,22,.1); }
|
||
|
||
.layer-toggle input[type="checkbox"] { display: none; }
|
||
|
||
.toggle-icon {
|
||
width: 22px; height: 22px; border-radius: 5px;
|
||
display: grid; place-items: center;
|
||
font-size: 12px; flex-shrink: 0;
|
||
}
|
||
.icon-24 { background: rgba(63,185,80,.2); }
|
||
.icon-non24{ background: rgba(249,115,22,.2); }
|
||
|
||
.toggle-info { flex: 1; }
|
||
.toggle-label { font-size: 11px; font-weight: 700; line-height: 1.2; }
|
||
.toggle-count { font-size: 10px; color: var(--muted); font-family: 'Space Mono', monospace; }
|
||
|
||
.toggle-check {
|
||
width: 16px; height: 16px; border-radius: 4px;
|
||
border: 1.5px solid var(--border);
|
||
display: grid; place-items: center;
|
||
flex-shrink: 0; transition: all .2s;
|
||
}
|
||
.layer-toggle.active-24 .toggle-check { background: var(--green); border-color: var(--green); }
|
||
.layer-toggle.active-non24 .toggle-check { background: var(--accent); border-color: var(--accent); }
|
||
.toggle-check svg { opacity: 0; transition: opacity .15s; }
|
||
.layer-toggle.active-24 .toggle-check svg,
|
||
.layer-toggle.active-non24 .toggle-check svg { opacity: 1; }
|
||
|
||
/* 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;
|
||
}
|
||
.spbu-card:hover { border-color: var(--accent); transform: translateX(3px); }
|
||
.spbu-card.active { border-color: var(--accent); background: #1d2029; }
|
||
/* dim kartu jika layer-nya disembunyikan */
|
||
.spbu-card.layer-hidden { opacity: .35; pointer-events: none; }
|
||
|
||
.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(249,115,22,.15); color: var(--accent); border: 1px solid var(--accent); }
|
||
|
||
.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); color: #fff; border-color: var(--red); }
|
||
.btn-sm.move:hover { background: #3b82f6; color: #fff; border-color: #3b82f6; }
|
||
|
||
/* 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; }
|
||
|
||
/* ══════════════════════════════════════
|
||
LEAFLET LAYERS CONTROL — custom dark theme
|
||
══════════════════════════════════════ */
|
||
.leaflet-control-layers {
|
||
background: var(--surface) !important;
|
||
border: 1px solid var(--border) !important;
|
||
border-radius: 12px !important;
|
||
box-shadow: var(--shadow) !important;
|
||
padding: 4px !important;
|
||
min-width: 200px;
|
||
}
|
||
.leaflet-control-layers-toggle {
|
||
background-color: var(--surface2) !important;
|
||
border: 1px solid var(--border) !important;
|
||
border-radius: 8px !important;
|
||
width: 36px !important; height: 36px !important;
|
||
}
|
||
.leaflet-control-layers-expanded {
|
||
padding: 12px 14px !important;
|
||
}
|
||
.leaflet-control-layers-list { color: var(--text) !important; }
|
||
.leaflet-control-layers-separator { border-color: var(--border) !important; }
|
||
.leaflet-control-layers label {
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
gap: 8px !important;
|
||
padding: 5px 0 !important;
|
||
font-size: 12px !important;
|
||
font-family: 'Plus Jakarta Sans', sans-serif !important;
|
||
font-weight: 600 !important;
|
||
cursor: pointer !important;
|
||
}
|
||
.leaflet-control-layers label:hover { color: var(--accent) !important; }
|
||
.leaflet-control-layers input[type="checkbox"] {
|
||
accent-color: var(--green);
|
||
width: 14px; height: 14px; cursor: pointer;
|
||
}
|
||
/* Title above overlay list */
|
||
.lc-title {
|
||
font-size: 10px; font-weight: 700;
|
||
text-transform: uppercase; letter-spacing: .8px;
|
||
color: var(--muted); margin-bottom: 6px;
|
||
}
|
||
|
||
/* ══════════════════════════════════════
|
||
FLOATING MOVE BAR
|
||
══════════════════════════════════════ */
|
||
#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; }
|
||
.pulse-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); animation: livePulse 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); }
|
||
|
||
/* ══════════════════════════════════════
|
||
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 { 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; }
|
||
|
||
/* 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;
|
||
}
|
||
#toast.show { transform: translateY(0); opacity: 1; }
|
||
#toast.success { border-color: var(--green); }
|
||
#toast.error { border-color: var(--red); }
|
||
|
||
/* Leaflet popup dark */
|
||
.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; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
|
||
<!-- ═══════════ HEADER ═══════════ -->
|
||
<header>
|
||
<div class="logo-icon">⛽</div>
|
||
<h1>WebGIS <span>SPBU</span> Pontianak</h1>
|
||
|
||
<div class="header-badges">
|
||
<span class="header-badge badge-24">
|
||
<span class="dot-sm"></span>
|
||
SPBU 24 Jam
|
||
</span>
|
||
<span class="header-badge badge-non24">
|
||
<span class="dot-sm"></span>
|
||
Non-24 Jam
|
||
</span>
|
||
</div>
|
||
|
||
<div class="header-meta">
|
||
<span class="dot-live"></span>Layer Control Active
|
||
</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>
|
||
Buka 24 Jam
|
||
</div>
|
||
<div class="stat-pill orange">
|
||
<strong id="stat-non24">0</strong>
|
||
Non-24 Jam
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ══ LAYER TOGGLE PANEL (sidebar) ══ -->
|
||
<div class="layer-panel">
|
||
<div class="layer-panel-title">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/></svg>
|
||
Layer Groups
|
||
</div>
|
||
<div class="layer-toggles">
|
||
|
||
<!-- Toggle: 24 Jam -->
|
||
<label class="layer-toggle active-24" id="toggle-24" onclick="toggleLayer('yes')">
|
||
<input type="checkbox" id="cb-24" checked/>
|
||
<div class="toggle-icon icon-24">🟢</div>
|
||
<div class="toggle-info">
|
||
<div class="toggle-label" style="color:var(--green)">SPBU 24 Jam</div>
|
||
<div class="toggle-count" id="lc-count-24">0 titik</div>
|
||
</div>
|
||
<div class="toggle-check">
|
||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" stroke="#0d1117" stroke-width="2.5"><path d="M1 6l3.5 3.5L11 2"/></svg>
|
||
</div>
|
||
</label>
|
||
|
||
<!-- Toggle: Non-24 Jam -->
|
||
<label class="layer-toggle active-non24" id="toggle-non24" onclick="toggleLayer('no')">
|
||
<input type="checkbox" id="cb-non24" checked/>
|
||
<div class="toggle-icon icon-non24">🟠</div>
|
||
<div class="toggle-info">
|
||
<div class="toggle-label" style="color:var(--accent)">Non-24 Jam</div>
|
||
<div class="toggle-count" id="lc-count-non24">0 titik</div>
|
||
</div>
|
||
<div class="toggle-check">
|
||
<svg width="9" height="9" viewBox="0 0 12 12" fill="none" stroke="#0d1117" stroke-width="2.5"><path d="M1 6l3.5 3.5L11 2"/></svg>
|
||
</div>
|
||
</label>
|
||
|
||
</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;">
|
||
<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>
|
||
|
||
<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 → Leaflet marker
|
||
let editId = null;
|
||
let addModeActive = false;
|
||
let tempMarker = null;
|
||
|
||
// Layer visibility state
|
||
let layerVisible = { yes: true, no: true };
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// MAP INIT — Pontianak
|
||
// ═══════════════════════════════════════════════════════════
|
||
const map = L.map('map', {
|
||
center: [-0.0263, 109.3425],
|
||
zoom: 13,
|
||
zoomControl: false
|
||
});
|
||
|
||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||
|
||
// ── Base layer ──────────────────────────────────────────────
|
||
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors',
|
||
maxZoom: 19
|
||
});
|
||
osmLayer.addTo(map);
|
||
|
||
// ══════════════════════════════════════════════════════════
|
||
// LAYER GROUPS ← inti tugas ini
|
||
// Dua LayerGroup terpisah berdasarkan status_24jam
|
||
// ══════════════════════════════════════════════════════════
|
||
const layerGroup24 = L.layerGroup(); // SPBU buka 24 jam
|
||
const layerGroupNon24 = L.layerGroup(); // SPBU tidak buka 24 jam
|
||
|
||
// Tambahkan kedua layer ke peta saat awal (keduanya aktif)
|
||
layerGroup24.addTo(map);
|
||
layerGroupNon24.addTo(map);
|
||
|
||
// ── Leaflet Layers Control (pojok kanan atas peta) ─────────
|
||
// Label dengan dot berwarna sebagai penanda visual
|
||
const overlayMaps = {
|
||
'<span style="display:inline-flex;align-items:center;gap:6px;font-family:\'Plus Jakarta Sans\',sans-serif;font-size:12px;font-weight:700"><span style="width:10px;height:10px;border-radius:50%;background:#3fb950;display:inline-block"></span>SPBU Buka 24 Jam</span>' : layerGroup24,
|
||
'<span style="display:inline-flex;align-items:center;gap:6px;font-family:\'Plus Jakarta Sans\',sans-serif;font-size:12px;font-weight:700"><span style="width:10px;height:10px;border-radius:50%;background:#f97316;display:inline-block"></span>SPBU Tidak 24 Jam</span>': layerGroupNon24
|
||
};
|
||
|
||
const layersControl = L.control.layers(null, overlayMaps, {
|
||
collapsed: false, // langsung tampil terbuka
|
||
position : 'topright'
|
||
}).addTo(map);
|
||
|
||
// Sinkronkan state LayerGroup → toggle sidebar saat user
|
||
// centang/uncentang langsung dari Leaflet Layers Control
|
||
map.on('overlayremove', function(e) {
|
||
if (e.layer === layerGroup24) _setSidebarToggle('yes', false);
|
||
if (e.layer === layerGroupNon24) _setSidebarToggle('no', false);
|
||
});
|
||
map.on('overlayadd', function(e) {
|
||
if (e.layer === layerGroup24) _setSidebarToggle('yes', true);
|
||
if (e.layer === layerGroupNon24) _setSidebarToggle('no', true);
|
||
});
|
||
|
||
// ── Ikon marker ────────────────────────────────────────────
|
||
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
|
||
// ═══════════════════════════════════════════════════════════
|
||
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;
|
||
renderMarkers();
|
||
renderList();
|
||
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 MARKERS — pisahkan ke LayerGroup masing-masing
|
||
// ═══════════════════════════════════════════════════════════
|
||
function renderMarkers() {
|
||
// Bersihkan kedua layer group & registry markers
|
||
layerGroup24.clearLayers();
|
||
layerGroupNon24.clearLayers();
|
||
markers = {};
|
||
|
||
spbuData.forEach(s => {
|
||
const is24 = s.status_24jam === 'yes';
|
||
const m = L.marker([s.latitude, s.longitude], { icon: makeIcon(is24) })
|
||
.bindPopup(popupHTML(s));
|
||
m.on('click', () => highlightCard(s.id));
|
||
|
||
// Tambahkan ke LayerGroup yang sesuai
|
||
if (is24) {
|
||
layerGroup24.addLayer(m);
|
||
} else {
|
||
layerGroupNon24.addLayer(m);
|
||
}
|
||
|
||
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 #3b82f6;background:rgba(59,130,246,.1);color:#3b82f6;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>`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// RENDER LIST (sidebar)
|
||
// ═══════════════════════════════════════════════════════════
|
||
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 => {
|
||
const is24 = s.status_24jam === 'yes';
|
||
const hidden = !layerVisible[s.status_24jam];
|
||
return `
|
||
<div class="spbu-card${hidden?' layer-hidden':''}" 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}">${is24 ? '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;
|
||
const no = total - yes;
|
||
|
||
document.getElementById('total-count').innerHTML = `${total} <small>titik</small>`;
|
||
document.getElementById('stat-24').textContent = yes;
|
||
document.getElementById('stat-non24').textContent = no;
|
||
|
||
// Update counter di toggle sidebar
|
||
document.getElementById('lc-count-24').textContent = `${yes} titik`;
|
||
document.getElementById('lc-count-non24').textContent = `${no} titik`;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// LAYER TOGGLE (sidebar custom toggle)
|
||
// ═══════════════════════════════════════════════════════════
|
||
function toggleLayer(status) {
|
||
// status: 'yes' (24 jam) | 'no' (non-24)
|
||
const newVisible = !layerVisible[status];
|
||
layerVisible[status] = newVisible;
|
||
|
||
const group = status === 'yes' ? layerGroup24 : layerGroupNon24;
|
||
|
||
if (newVisible) {
|
||
group.addTo(map);
|
||
} else {
|
||
map.removeLayer(group);
|
||
}
|
||
|
||
// Update tampilan toggle di sidebar
|
||
_setSidebarToggle(status, newVisible);
|
||
|
||
// Rerender kartu (kartu layer tersembunyi jadi dim)
|
||
renderList(document.getElementById('search-input').value);
|
||
|
||
showToast(
|
||
newVisible
|
||
? `Layer "${status === 'yes' ? 'SPBU 24 Jam' : 'SPBU Non-24 Jam'}" ditampilkan ✅`
|
||
: `Layer "${status === 'yes' ? 'SPBU 24 Jam' : 'SPBU Non-24 Jam'}" disembunyikan 🙈`,
|
||
newVisible ? 'success' : 'error'
|
||
);
|
||
}
|
||
|
||
function _setSidebarToggle(status, visible) {
|
||
layerVisible[status] = visible;
|
||
const toggleEl = document.getElementById(status === 'yes' ? 'toggle-24' : 'toggle-non24');
|
||
const cbEl = document.getElementById(status === 'yes' ? 'cb-24' : 'cb-non24');
|
||
const activeClass = status === 'yes' ? 'active-24' : 'active-non24';
|
||
|
||
cbEl.checked = visible;
|
||
if (visible) toggleEl.classList.add(activeClass);
|
||
else toggleEl.classList.remove(activeClass);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// MAP INTERACTIONS
|
||
// ═══════════════════════════════════════════════════════════
|
||
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;
|
||
|
||
// Pastikan layer yang bersangkutan aktif agar popup bisa muncul
|
||
const group = s.status_24jam === 'yes' ? layerGroup24 : layerGroupNon24;
|
||
if (!map.hasLayer(group)) {
|
||
group.addTo(map);
|
||
_setSidebarToggle(s.status_24jam, true);
|
||
showToast('Layer diaktifkan otomatis untuk menampilkan lokasi', 'success');
|
||
}
|
||
|
||
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;
|
||
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';
|
||
}
|
||
document.getElementById('modal-overlay').classList.add('open');
|
||
}
|
||
|
||
function openEdit(id) {
|
||
id = parseInt(id);
|
||
if (markers[id]) markers[id].closePopup();
|
||
openModal(id);
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('modal-overlay').classList.remove('open');
|
||
editId = null;
|
||
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
||
}
|
||
|
||
document.getElementById('modal-overlay').addEventListener('click', function(e) {
|
||
if (e.target === this) closeModal();
|
||
});
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// PINDAHKAN TITIK (drag pin)
|
||
// ═══════════════════════════════════════════════════════════
|
||
let _moveId = null, _moveOriginal = null;
|
||
|
||
function startMovePin(id) {
|
||
id = parseInt(id);
|
||
const s = spbuData.find(x => x.id == id);
|
||
if (!s) return;
|
||
if (markers[id]) markers[id].closePopup();
|
||
|
||
_moveId = id;
|
||
_moveOriginal = { lat: s.latitude, lng: s.longitude };
|
||
|
||
const m = markers[id];
|
||
m.dragging.enable();
|
||
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]
|
||
}));
|
||
|
||
map.flyTo([s.latitude, s.longitude], 16, { duration: .8 });
|
||
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();
|
||
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;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// SEARCH / FILTER
|
||
// ═══════════════════════════════════════════════════════════
|
||
function filterList() {
|
||
renderList(document.getElementById('search-input').value);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// 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,'"');
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// GLOBALS (dipanggil dari dalam popup Leaflet)
|
||
// ═══════════════════════════════════════════════════════════
|
||
window.openEdit = openEdit;
|
||
window.deleteSpbu = deleteSpbu;
|
||
window.startMovePin = startMovePin;
|
||
|
||
// ═══════════════════════════════════════════════════════════
|
||
// INIT
|
||
// ═══════════════════════════════════════════════════════════
|
||
fetchAll();
|
||
</script>
|
||
</body>
|
||
</html>
|