Files
2026-06-09 19:36:57 +07:00

1228 lines
73 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 Jalan, Parsil Tanah &amp; SPBU Pontianak</title>
<meta name="description" content="Sistem Informasi Geografis manajemen data jalan, parsil tanah, dan SPBU berbasis LeafletJS Kota Pontianak" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg-dark: #0d1117; --bg-panel: #161b22; --bg-card: #21262d; --bg-input: #0d1117;
--border: #30363d; --border-focus: #388bfd;
--text-primary: #e6edf3; --text-secondary: #8b949e; --text-muted: #6e7681;
--jalan-nasional: #f97316; --jalan-provinsi: #22c55e; --jalan-kabupaten: #3b82f6;
--shm: #a855f7; --hgb: #06b6d4; --hgu: #eab308; --hp: #ef4444;
--spbu-24: #22c55e; --spbu-tdk: #ef4444;
--accent: #388bfd; --accent-hover: #58a6ff;
--danger: #f85149; --success: #3fb950; --warning: #d29922;
--radius: 8px; --radius-lg: 12px;
--shadow: 0 4px 24px rgba(0,0,0,.5);
--transition: all .2s ease;
}
/* Popup SPBU form style */
.spbu-popup-form { width: 210px; }
.spbu-popup-form h4 { margin: 0 0 10px 0; font-size: .93rem; color: #222; font-weight: 700; }
.spbu-popup-form label { font-size: 11px; display: block; margin-bottom: 3px; color: #555; font-weight: 600; }
.spbu-popup-form input[type=text],
.spbu-popup-form input[type=tel] { width: 100%; box-sizing: border-box; padding: 5px 7px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; margin-bottom: 8px; }
.spbu-popup-form .radio-row { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; font-size: 12px; }
.spbu-popup-form .btn-simpan { width: 100%; padding: 6px; background: #16a34a; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 600; }
.spbu-popup-form .btn-simpan:hover { background: #15803d; }
body { font-family:'Inter',sans-serif; background:var(--bg-dark); color:var(--text-primary); height:100vh; display:flex; flex-direction:column; overflow:hidden; }
/* ── TOPBAR ─────────────────────────────────────────── */
.topbar { background:var(--bg-panel); border-bottom:1px solid var(--border); padding:0 20px; height:56px; display:grid; grid-template-columns:1fr auto 1fr; align-items:center; flex-shrink:0; z-index:1000; }
.topbar-brand { display:flex; align-items:center; gap:10px; font-weight:700; font-size:.95rem; }
.brand-icon { width:32px; height:32px; background:linear-gradient(135deg,#388bfd,#a855f7); border-radius:8px; display:flex; align-items:center; justify-content:center; }
.topbar-tabs { display:flex; gap:4px; justify-content:center; }
.tab-btn { padding:6px 14px; border:1px solid var(--border); border-radius:6px; background:transparent; color:var(--text-secondary); font-family:inherit; font-size:.78rem; font-weight:500; cursor:pointer; transition:var(--transition); display:flex; align-items:center; gap:6px; }
.tab-btn:hover { border-color:var(--accent); color:var(--accent); }
.tab-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
.tab-btn.tab-spbu.active { background:#16a34a; border-color:#16a34a; }
.tab-btn.tab-spbu:hover { border-color:var(--spbu-24); color:var(--spbu-24); }
.topbar-right { font-size:.76rem; color:var(--text-muted); display:flex; align-items:center; gap:8px; justify-content:flex-end; }
#coord-display { font-family:'Courier New',monospace; font-size:.74rem; width:230px; text-align:right; white-space:nowrap; overflow:hidden; }
/* ── LAYOUT ─────────────────────────────────────────── */
.app-body { display:flex; flex:1; overflow:hidden; }
#map { flex:1; height:100%; z-index:1; }
.sidebar { width:360px; min-width:360px; background:var(--bg-panel); border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; z-index:2; }
.sidebar-header { padding:14px 18px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; flex-shrink:0; }
.sidebar-title { font-size:.9rem; font-weight:600; display:flex; align-items:center; gap:8px; }
.dot-live { width:8px; height:8px; border-radius:50%; background:var(--success); animation:pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.4;transform:scale(.7)} }
.btn-add { padding:6px 14px; background:var(--accent); color:#fff; border:none; border-radius:6px; font-family:inherit; font-size:.78rem; font-weight:600; cursor:pointer; transition:var(--transition); display:flex; align-items:center; gap:5px; }
.btn-add:hover { background:var(--accent-hover); transform:translateY(-1px); }
.btn-add.spbu-add { background:#16a34a; }
.btn-add.spbu-add:hover { background:#15803d; }
/* ── LEGEND ─────────────────────────────────────────── */
.legend-bar { padding:10px 18px; border-bottom:1px solid var(--border); display:flex; flex-wrap:wrap; gap:8px; flex-shrink:0; }
.legend-item { display:flex; align-items:center; gap:5px; font-size:.71rem; color:var(--text-secondary); }
.legend-line { width:28px; height:4px; border-radius:3px; }
.legend-sq { width:13px; height:13px; border-radius:3px; opacity:.8; }
.legend-dot { width:12px; height:12px; border-radius:50%; border:2px solid rgba(255,255,255,.3); }
/* ── LIST ───────────────────────────────────────────── */
.data-list { flex:1; overflow-y:auto; padding:10px; }
.data-list::-webkit-scrollbar { width:4px; }
.data-list::-webkit-scrollbar-thumb { background:var(--border); border-radius:4px; }
.data-card { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--radius); padding:11px 13px; margin-bottom:7px; cursor:pointer; transition:var(--transition); border-left:3px solid transparent; }
.data-card:hover { border-color:var(--accent); transform:translateX(2px); }
.data-card.active-card { border-color:var(--accent); background:rgba(56,139,253,.08); }
.card-top { display:flex; align-items:flex-start; justify-content:space-between; gap:8px; }
.card-name { font-size:.86rem; font-weight:600; color:var(--text-primary); margin-bottom:3px; }
.badge { font-size:.64rem; font-weight:600; padding:2px 8px; border-radius:20px; white-space:nowrap; flex-shrink:0; }
.badge-nasional { background:rgba(249,115,22,.15); color:var(--jalan-nasional); border:1px solid rgba(249,115,22,.3); }
.badge-provinsi { background:rgba(34,197,94,.15); color:var(--jalan-provinsi); border:1px solid rgba(34,197,94,.3); }
.badge-kabupaten { background:rgba(59,130,246,.15); color:var(--jalan-kabupaten); border:1px solid rgba(59,130,246,.3); }
.badge-shm { background:rgba(168,85,247,.15); color:var(--shm); border:1px solid rgba(168,85,247,.3); }
.badge-hgb { background:rgba(6,182,212,.15); color:var(--hgb); border:1px solid rgba(6,182,212,.3); }
.badge-hgu { background:rgba(234,179,8,.15); color:var(--hgu); border:1px solid rgba(234,179,8,.3); }
.badge-hp { background:rgba(239,68,68,.15); color:var(--hp); border:1px solid rgba(239,68,68,.3); }
.badge-24 { background:rgba(34,197,94,.15); color:var(--spbu-24); border:1px solid rgba(34,197,94,.3); }
.badge-no24{ background:rgba(239,68,68,.15); color:var(--spbu-tdk); border:1px solid rgba(239,68,68,.3); }
.card-meta { font-size:.71rem; color:var(--text-muted); margin-top:4px; display:flex; gap:10px; flex-wrap:wrap; }
.card-meta span { display:flex; align-items:center; gap:4px; }
.card-actions { display:flex; gap:4px; margin-top:8px; }
.btn-sm { padding:4px 10px; border-radius:5px; font-size:.71rem; font-weight:500; font-family:inherit; cursor:pointer; border:1px solid; transition:var(--transition); display:flex; align-items:center; gap:4px; }
.btn-edit { background:transparent; border-color:var(--accent); color:var(--accent); }
.btn-edit:hover { background:var(--accent); color:#fff; }
.btn-del { background:transparent; border-color:var(--danger); color:var(--danger); }
.btn-del:hover { background:var(--danger); color:#fff; }
.btn-zoom { background:transparent; border-color:var(--border); color:var(--text-secondary); }
.btn-zoom:hover { border-color:var(--success); color:var(--success); }
.empty-state { text-align:center; padding:36px 16px; color:var(--text-muted); }
.empty-state i { font-size:2.2rem; margin-bottom:10px; opacity:.35; display:block; }
.empty-state p { font-size:.81rem; line-height:1.5; }
/* ── INSTRUCTION BANNER ─────────────────────────────── */
.map-instruction { position:absolute; top:66px; left:50%; transform:translateX(-50%); background:rgba(13,17,23,.93); border:1px solid var(--border); border-radius:8px; padding:8px 20px; font-size:.78rem; color:var(--text-secondary); z-index:1000; display:none; backdrop-filter:blur(10px); pointer-events:none; white-space:nowrap; }
.map-instruction.visible { display:block; }
.map-instruction strong { color:var(--accent); }
/* ── MODALS ──────────────────────────────────────────── */
.modal-overlay { position:fixed; inset:0; background:rgba(0,0,0,.72); backdrop-filter:blur(4px); z-index:9000; display:flex; align-items:center; justify-content:center; opacity:0; pointer-events:none; transition:opacity .2s ease; }
.modal-overlay.open { opacity:1; pointer-events:all; }
.modal { background:var(--bg-panel); border:1px solid var(--border); border-radius:var(--radius-lg); width:480px; max-width:95vw; max-height:90vh; overflow-y:auto; box-shadow:var(--shadow); transform:scale(.95) translateY(-10px); transition:transform .25s ease; }
.modal-overlay.open .modal { transform:scale(1) translateY(0); }
.modal::-webkit-scrollbar { width:4px; }
.modal::-webkit-scrollbar-thumb { background:var(--border); border-radius:4px; }
.modal-header { padding:16px 22px; border-bottom:1px solid var(--border); display:flex; align-items:center; justify-content:space-between; }
.modal-title { font-size:.97rem; font-weight:700; display:flex; align-items:center; gap:8px; }
.modal-title i { color:var(--accent); }
.modal-close { background:none; border:none; color:var(--text-secondary); cursor:pointer; font-size:1.1rem; width:30px; height:30px; border-radius:6px; display:flex; align-items:center; justify-content:center; transition:var(--transition); }
.modal-close:hover { background:var(--bg-card); color:var(--text-primary); }
.modal-body { padding:18px 22px; }
.form-group { margin-bottom:14px; }
.form-label { display:block; font-size:.74rem; font-weight:600; color:var(--text-secondary); margin-bottom:5px; text-transform:uppercase; letter-spacing:.05em; }
.form-control { width:100%; background:var(--bg-input); border:1px solid var(--border); border-radius:var(--radius); color:var(--text-primary); font-family:inherit; font-size:.86rem; padding:9px 12px; transition:var(--transition); outline:none; }
.form-control:focus { border-color:var(--border-focus); box-shadow:0 0 0 3px rgba(56,139,253,.15); }
.auto-field { display:flex; align-items:center; gap:8px; }
.auto-field .form-control { flex:1; background:rgba(56,139,253,.05); }
.auto-badge { font-size:.63rem; background:rgba(63,185,80,.15); color:var(--success); border:1px solid rgba(63,185,80,.3); padding:2px 8px; border-radius:20px; font-weight:600; white-space:nowrap; }
.form-info { font-size:.71rem; color:var(--text-muted); margin-top:4px; display:flex; align-items:center; gap:4px; flex-wrap:wrap; }
.geojson-preview { background:var(--bg-input); border:1px solid var(--border); border-radius:var(--radius); padding:9px 12px; font-size:.7rem; font-family:'Courier New',monospace; color:var(--success); word-break:break-all; max-height:70px; overflow-y:auto; }
.geojson-empty { font-size:.77rem; color:var(--warning); display:flex; align-items:center; gap:6px; padding:8px 12px; background:rgba(210,153,34,.08); border:1px solid rgba(210,153,34,.25); border-radius:var(--radius); }
/* Toggle 24 jam */
.toggle-group { display:flex; gap:8px; }
.toggle-opt { flex:1; padding:10px 8px; border-radius:8px; border:2px solid var(--border); cursor:pointer; transition:var(--transition); text-align:center; }
.toggle-opt input { display:none; }
.toggle-opt .opt-icon { font-size:1.4rem; display:block; margin-bottom:4px; }
.toggle-opt .opt-label { font-size:.76rem; font-weight:600; color:var(--text-secondary); }
.toggle-opt:has(input:checked).opt-yes { border-color:var(--spbu-24); background:rgba(34,197,94,.1); }
.toggle-opt:has(input:checked).opt-no { border-color:var(--spbu-tdk); background:rgba(239,68,68,.1); }
.toggle-opt:has(input:checked) .opt-label { color:var(--text-primary); }
/* Tombol pilih lokasi peta */
.btn-pick-map { display:flex; align-items:center; gap:6px; padding:8px 14px; background:rgba(56,139,253,.1); border:1px dashed var(--accent); color:var(--accent); border-radius:var(--radius); font-family:inherit; font-size:.8rem; font-weight:600; cursor:pointer; transition:var(--transition); width:100%; justify-content:center; margin-top:6px; }
.btn-pick-map:hover { background:rgba(56,139,253,.2); }
.btn-pick-map.picking { background:rgba(34,197,94,.15); border-color:var(--success); color:var(--success); animation:blink 1s step-end infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.4} }
.coord-display { display:flex; gap:8px; }
.coord-display .form-control { flex:1; background:rgba(63,185,80,.05); border-color:rgba(63,185,80,.3); color:var(--success); font-size:.8rem; }
.modal-footer { padding:14px 22px; border-top:1px solid var(--border); display:flex; gap:8px; justify-content:flex-end; }
.btn-cancel { padding:8px 18px; background:transparent; border:1px solid var(--border); color:var(--text-secondary); border-radius:6px; font-family:inherit; font-size:.84rem; cursor:pointer; transition:var(--transition); }
.btn-cancel:hover { border-color:var(--text-secondary); color:var(--text-primary); }
.btn-save { padding:8px 22px; background:var(--accent); border:none; color:#fff; border-radius:6px; font-family:inherit; font-size:.84rem; font-weight:600; cursor:pointer; transition:var(--transition); display:flex; align-items:center; gap:6px; }
.btn-save:hover { background:var(--accent-hover); transform:translateY(-1px); }
.btn-save:disabled { opacity:.5; cursor:not-allowed; transform:none; }
.confirm-modal { width:360px; }
/* ── STATS ──────────────────────────────────────────── */
.stats-strip { display:flex; gap:5px; padding:9px 10px; border-top:1px solid var(--border); flex-shrink:0; }
.stat-item { flex:1; background:var(--bg-card); border:1px solid var(--border); border-radius:6px; padding:7px 8px; text-align:center; }
.stat-num { font-size:1.05rem; font-weight:700; color:var(--accent); }
.stat-label { font-size:.6rem; color:var(--text-muted); margin-top:1px; }
/* ── TOAST ──────────────────────────────────────────── */
.toast-container { position:fixed; bottom:22px; right:22px; z-index:9999; display:flex; flex-direction:column; gap:7px; }
.toast { background:var(--bg-card); border:1px solid var(--border); border-radius:8px; padding:11px 16px; font-size:.81rem; display:flex; align-items:center; gap:10px; box-shadow:var(--shadow); animation:slideIn .3s ease; max-width:300px; }
.toast.success { border-color:rgba(63,185,80,.4); }
.toast.error { border-color:rgba(248,81,73,.4); }
.toast.success i { color:var(--success); }
.toast.error i { color:var(--danger); }
@keyframes slideIn { from{opacity:0;transform:translateX(40px)} to{opacity:1;transform:translateX(0)} }
/* Leaflet dark overrides */
.leaflet-draw-toolbar a, .leaflet-bar a, .leaflet-bar a:hover { background-color:var(--bg-card) !important; color:var(--text-primary) !important; border-color:var(--border) !important; }
.leaflet-popup-content-wrapper { border-radius:10px !important; }
</style>
</head>
<body>
<!-- TOP BAR -->
<header class="topbar">
<div class="topbar-brand">
<div class="brand-icon"><i class="fas fa-map-location-dot" style="color:#fff"></i></div>
<span>WebGIS Pontianak <span style="color:var(--text-muted);font-weight:400;font-size:.82rem"> Jalan · Parsil · SPBU</span></span>
</div>
<div class="topbar-tabs">
<button class="tab-btn active" id="tab-jalan" onclick="switchTab('jalan')"><i class="fas fa-road"></i> Data Jalan</button>
<button class="tab-btn" id="tab-parsil" onclick="switchTab('parsil')"><i class="fas fa-draw-polygon"></i> Parsil Tanah</button>
<button class="tab-btn tab-spbu" id="tab-spbu" onclick="switchTab('spbu')"><i class="fas fa-gas-pump"></i> SPBU</button>
</div>
<div class="topbar-right">
<i class="fas fa-circle" style="color:var(--success);font-size:.45rem"></i>
<span id="coord-display">Arahkan kursor ke peta</span>
</div>
</header>
<!-- BODY -->
<div class="app-body">
<div style="position:relative;flex:1;">
<div id="map"></div>
<div class="map-instruction" id="map-instruction">
<i class="fas fa-info-circle"></i>&nbsp;
<strong id="instruction-text"></strong>
</div>
</div>
<aside class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">
<span class="dot-live"></span>
<span id="sidebar-title-text">Data Jalan</span>
</div>
<button class="btn-add" id="btn-add-main" onclick="startDrawMode()">
<i class="fas fa-plus"></i> Tambah
</button>
</div>
<div class="legend-bar" id="legend-bar"></div>
<div class="data-list" id="data-list"><div class="empty-state"><i class="fas fa-spinner fa-spin"></i><p>Memuat data…</p></div></div>
<div class="stats-strip" id="stats-strip"></div>
</aside>
</div>
<!-- MODAL: JALAN -->
<div class="modal-overlay" id="modal-jalan-overlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title"><i class="fas fa-road"></i><span id="modal-jalan-title">Tambah Data Jalan</span></div>
<button class="modal-close" onclick="closeModal('jalan')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body">
<input type="hidden" id="jalan-id" />
<div class="form-group">
<label class="form-label">Nama Jalan <span style="color:var(--danger)">*</span></label>
<input class="form-control" id="jalan-nama" type="text" placeholder="Contoh: Jl. Ahmad Yani" />
</div>
<div class="form-group">
<label class="form-label">Status Jalan <span style="color:var(--danger)">*</span></label>
<select class="form-control" id="jalan-status">
<option value="">-- Pilih Status --</option>
<option value="Jalan Nasional">Jalan Nasional</option>
<option value="Jalan Provinsi">Jalan Provinsi</option>
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Panjang Jalan</label>
<div class="auto-field">
<input class="form-control" id="jalan-panjang" type="text" readonly placeholder="Dihitung otomatis…" />
<span class="auto-badge"><i class="fas fa-magic"></i> Auto</span>
</div>
<p class="form-info"><i class="fas fa-info-circle"></i> Dihitung otomatis oleh LeafletJS (meter)</p>
</div>
<div class="form-group">
<label class="form-label">Geometri GeoJSON</label>
<div id="jalan-geojson-preview" class="geojson-empty"><i class="fas fa-triangle-exclamation"></i> Gambar polyline di peta terlebih dahulu</div>
</div>
<div class="form-group">
<label class="form-label">Keterangan</label>
<textarea class="form-control" id="jalan-keterangan" rows="2" placeholder="Opsional"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('jalan')">Batal</button>
<button class="btn-save" id="btn-save-jalan" onclick="saveJalan()"><i class="fas fa-floppy-disk"></i> Simpan</button>
</div>
</div>
</div>
<!-- MODAL: PARSIL -->
<div class="modal-overlay" id="modal-parsil-overlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title"><i class="fas fa-draw-polygon"></i><span id="modal-parsil-title">Tambah Data Parsil Tanah</span></div>
<button class="modal-close" onclick="closeModal('parsil')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body">
<input type="hidden" id="parsil-id" />
<div class="form-group">
<label class="form-label">Nama / Nomor Parsil <span style="color:var(--danger)">*</span></label>
<input class="form-control" id="parsil-nama" type="text" placeholder="Contoh: Parsil A-001" />
</div>
<div class="form-group">
<label class="form-label">Status Kepemilikan <span style="color:var(--danger)">*</span></label>
<select class="form-control" id="parsil-status">
<option value="">-- Pilih Status Hak --</option>
<option value="Sertifikat Hak Milik">Sertifikat Hak Milik (SHM)</option>
<option value="Sertifikat Hak Guna Bangunan">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="Sertifikat Hak Guna Usaha">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="Sertifikat Hak Pakai">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Luas Tanah</label>
<div class="auto-field">
<input class="form-control" id="parsil-luas" type="text" readonly placeholder="Dihitung otomatis…" />
<span class="auto-badge"><i class="fas fa-magic"></i> Auto</span>
</div>
<p class="form-info"><i class="fas fa-info-circle"></i> Dihitung otomatis oleh LeafletJS (m²)</p>
</div>
<div class="form-group">
<label class="form-label">Geometri GeoJSON</label>
<div id="parsil-geojson-preview" class="geojson-empty"><i class="fas fa-triangle-exclamation"></i> Gambar polygon di peta terlebih dahulu</div>
</div>
<div class="form-group">
<label class="form-label">Nama Pemilik</label>
<input class="form-control" id="parsil-pemilik" type="text" placeholder="Opsional" />
</div>
<div class="form-group">
<label class="form-label">Keterangan</label>
<textarea class="form-control" id="parsil-keterangan" rows="2" placeholder="Opsional"></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('parsil')">Batal</button>
<button class="btn-save" id="btn-save-parsil" onclick="saveParsil()"><i class="fas fa-floppy-disk"></i> Simpan</button>
</div>
</div>
</div>
<!-- MODAL: SPBU -->
<div class="modal-overlay" id="modal-spbu-overlay">
<div class="modal">
<div class="modal-header">
<div class="modal-title">
<i class="fas fa-gas-pump" style="color:#22c55e"></i>
<span id="modal-spbu-title">Tambah Data SPBU</span>
</div>
<button class="modal-close" onclick="closeModal('spbu')"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body">
<input type="hidden" id="spbu-id" />
<div class="form-group">
<label class="form-label">Nama SPBU <span style="color:var(--danger)">*</span></label>
<input class="form-control" id="spbu-nama" type="text" placeholder="Contoh: SPBU Jl. Ahmad Yani" />
</div>
<div class="form-group">
<label class="form-label">Nomor Telepon</label>
<input class="form-control" id="spbu-telepon" type="tel" placeholder="Contoh: 0561-123456" />
</div>
<div class="form-group">
<label class="form-label">Jam Operasional <span style="color:var(--danger)">*</span></label>
<div class="toggle-group">
<label class="toggle-opt opt-yes">
<input type="radio" name="spbu-jam" id="spbu-24-ya" value="1" />
<span class="opt-icon">🟢</span>
<span class="opt-label">Buka 24 Jam</span>
</label>
<label class="toggle-opt opt-no">
<input type="radio" name="spbu-jam" id="spbu-24-tidak" value="0" checked />
<span class="opt-icon">🔴</span>
<span class="opt-label">Tidak 24 Jam</span>
</label>
</div>
<p class="form-info" style="margin-top:6px">
<i class="fas fa-circle" style="color:var(--spbu-24)"></i> Buka 24 jam = marker hijau &nbsp;|&nbsp;
<i class="fas fa-circle" style="color:var(--spbu-tdk)"></i> Tidak = marker merah
</p>
</div>
<!-- ======================================================
LOKASI SPBU — klik tombol atau tutup modal & klik peta
====================================================== -->
<div class="form-group">
<label class="form-label">Lokasi SPBU <span style="color:var(--danger)">*</span></label>
<!-- Tombol utama: tutup modal sementara, klik peta -->
<button class="btn-pick-map" id="btn-pick-loc" onclick="activateMapPick()">
<i class="fas fa-map-pin"></i> Klik Peta untuk Pilih Lokasi
</button>
<!-- Menampilkan koordinat setelah dipilih -->
<div class="coord-display" style="margin-top:8px">
<input class="form-control" id="spbu-lat" type="text" readonly placeholder="Lat —" />
<input class="form-control" id="spbu-lng" type="text" readonly placeholder="Lng —" />
</div>
<p class="form-info"><i class="fas fa-info-circle"></i>
Klik tombol di atas → tutup form → klik titik di peta → form kembali otomatis.
</p>
</div>
<div class="form-group">
<label class="form-label">GeoJSON Point</label>
<div id="spbu-geojson-preview" class="geojson-empty">
<i class="fas fa-triangle-exclamation"></i> Belum ada lokasi dipilih
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('spbu')">Batal</button>
<button class="btn-save" id="btn-save-spbu" onclick="saveSpbu()" style="background:#16a34a">
<i class="fas fa-floppy-disk"></i> Simpan
</button>
</div>
</div>
</div>
<!-- MODAL: KONFIRMASI HAPUS -->
<div class="modal-overlay" id="modal-confirm-overlay">
<div class="modal confirm-modal">
<div class="modal-header">
<div class="modal-title" style="color:var(--danger)"><i class="fas fa-trash-alt"></i> Konfirmasi Hapus</div>
<button class="modal-close" onclick="closeConfirm()"><i class="fas fa-xmark"></i></button>
</div>
<div class="modal-body">
<p style="color:var(--text-secondary);font-size:.87rem;line-height:1.6">
Hapus data <strong id="confirm-name" style="color:var(--text-primary)"></strong>? Tindakan ini tidak dapat dibatalkan.
</p>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeConfirm()">Batal</button>
<button class="btn-save" style="background:var(--danger)" id="btn-confirm-del"><i class="fas fa-trash-alt"></i> Hapus</button>
</div>
</div>
</div>
<div class="toast-container" id="toast-container"></div>
<!-- SCRIPTS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-geometryutil@0.10.3/src/leaflet.geometryutil.js"></script>
<script>
/* ================================================================
CONFIG & STATE
================================================================ */
const API = { jalan:'api_jalan.php', parsil:'api_parsil.php', spbu:'api_spbu.php' };
// ── Center peta: Kota Pontianak ──────────────────────────────
const MAP_CENTER = [-0.055319, 109.349502];
const MAP_ZOOM = 13;
const WARNA_JALAN = { 'Jalan Nasional':'#f97316', 'Jalan Provinsi':'#22c55e', 'Jalan Kabupaten':'#3b82f6' };
const WARNA_PARSIL= { 'Sertifikat Hak Milik':'#a855f7','Sertifikat Hak Guna Bangunan':'#06b6d4','Sertifikat Hak Guna Usaha':'#eab308','Sertifikat Hak Pakai':'#ef4444' };
const BADGE = { 'Jalan Nasional':'badge-nasional','Jalan Provinsi':'badge-provinsi','Jalan Kabupaten':'badge-kabupaten','Sertifikat Hak Milik':'badge-shm','Sertifikat Hak Guna Bangunan':'badge-hgb','Sertifikat Hak Guna Usaha':'badge-hgu','Sertifikat Hak Pakai':'badge-hp' };
// ── Icon marker SPBU (hijau = 24 jam, merah = tidak) ─────────
const iconHijau = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const iconMerah = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
let currentTab = 'jalan';
let dataJalan=[], dataParsil=[], dataSpbu=[];
let layersJalan={}, layersParsil={}, layersSpbu={};
let currentDrawGeojson=null;
let spbuClickLat=null, spbuClickLng=null;
let editMode=false, editTargetId=null;
let drawControl=null;
// ── Mode klik SPBU ───────────────────────────────────────────
// 'idle' : tidak ada mode aktif (klik peta = buka form popup)
// mode lama (pick / modal-open) dihapus, diganti langsung klik peta
let spbuPickMode = 'idle';
let spbuTempMarker=null;
const daftarMarker = {}; // referensi marker SPBU per id
/* ================================================================
INISIALISASI PETA
================================================================ */
const map = L.map('map', { zoomControl:true }).setView(MAP_CENTER, MAP_ZOOM);
const streets = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap',maxZoom:19}).addTo(map);
const satellite= L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:19});
// Layer Groups untuk SPBU
const spbu24Layer = L.layerGroup().addTo(map);
const spbuTidak24Layer = L.layerGroup().addTo(map);
const baseMaps = {
'🗺️ Street': streets,
'🛰️ Satellite': satellite
};
const overlayMaps = {
'🟢 SPBU Buka 24 Jam': spbu24Layer,
'🔴 SPBU Tidak 24 Jam': spbuTidak24Layer
};
L.control.layers(baseMaps, overlayMaps, {position:'topleft'}).addTo(map);
map.on('mousemove', e => {
document.getElementById('coord-display').textContent =
`Lat: ${e.latlng.lat.toFixed(6)} Lng: ${e.latlng.lng.toFixed(6)}`;
});
const drawnItems = new L.FeatureGroup().addTo(map);
/* ================================================================
KLIK PETA → SPBU point
Bekerja pada dua mode:
• 'pick' : modal belum terbuka, klik pertama → buka modal
• 'modal-open' : modal sudah terbuka (edit / setelah klik pertama)
================================================================ */
/* ================================================================
KLIK PETA → Form Popup SPBU (mode: tab SPBU aktif)
================================================================ */
const spbuPopup = L.popup();
map.on('click', function(e) {
// Jika tab bukan SPBU → lewati (untuk jalan/parsil, klik peta tidak perlu diproses di sini)
if (currentTab !== 'spbu') return;
// Jika sedang di mode edit/pick spbu via modal → proses koordinat ke modal
if (spbuPickMode === 'pick' || spbuPickMode === 'modal-open') {
spbuClickLat = parseFloat(e.latlng.lat.toFixed(7));
spbuClickLng = parseFloat(e.latlng.lng.toFixed(7));
if (spbuTempMarker) map.removeLayer(spbuTempMarker);
const rad = document.querySelector('input[name="spbu-jam"]:checked');
const is24 = rad ? rad.value === '1' : false;
const pinColor = is24 ? '#22c55e' : '#ef4444';
spbuTempMarker = L.circleMarker([spbuClickLat, spbuClickLng], {
radius:12, fillColor:pinColor, color:'#fff', weight:3, fillOpacity:.9
}).addTo(map);
spbuTempMarker.bindTooltip('⛽ SPBU baru', {permanent:true, direction:'top', offset:[0,-12]});
const geojson = { type:'Point', coordinates:[spbuClickLng, spbuClickLat] };
currentDrawGeojson = geojson;
const latEl = document.getElementById('spbu-lat');
const lngEl = document.getElementById('spbu-lng');
if (latEl) latEl.value = spbuClickLat;
if (lngEl) lngEl.value = spbuClickLng;
const preEl = document.getElementById('spbu-geojson-preview');
if (preEl) preEl.outerHTML = `<div id="spbu-geojson-preview" class="geojson-preview">${JSON.stringify(geojson)}</div>`;
if (spbuPickMode === 'pick') {
spbuPickMode = 'modal-open';
openModal('spbu');
const btn = document.getElementById('btn-pick-loc');
if (btn) { btn.innerHTML = '<i class="fas fa-map-pin"></i> Ubah Lokasi (klik tombol lalu klik peta)'; btn.classList.remove('picking'); }
}
showInstruction(true, 'Lokasi ditandai ✓ — Lengkapi form lalu Simpan.');
map.getContainer().style.cursor = 'crosshair';
return;
}
// ── MODE NORMAL: Klik peta langsung buka popup form (seperti template) ──
const lat = parseFloat(e.latlng.lat.toFixed(7));
const lng = parseFloat(e.latlng.lng.toFixed(7));
const formContent = `
<div class="spbu-popup-form">
<h4>⛽ Input Data SPBU</h4>
<label>Nama SPBU *</label>
<input type="text" id="popupNamaPom" placeholder="Contoh: SPBU Jl. Ahmad Yani" required>
<label>No. WhatsApp *</label>
<input type="tel" id="popupNoWa" placeholder="Contoh: 08123456789" required>
<label>Jam Operasional *</label>
<div class="radio-row">
<label><input type="radio" name="popupBuka24" value="Ya" checked> 🟢 Buka 24 Jam</label>
<label><input type="radio" name="popupBuka24" value="Tidak"> 🔴 Tidak 24 Jam</label>
</div>
<button class="btn-simpan" onclick="simpanDataPopup(${lat}, ${lng})">💾 Simpan SPBU</button>
</div>
`;
spbuPopup.setLatLng(e.latlng).setContent(formContent).openOn(map);
});
/* ================================================================
LEAFLET DRAW polyline & polygon
================================================================ */
function initDrawControl(type) {
if (drawControl) { map.removeControl(drawControl); drawControl=null; }
drawControl = new L.Control.Draw({
edit:{ featureGroup:drawnItems, remove:false },
draw:{
polyline : type==='polyline' ? { shapeOptions:{color:'#388bfd',weight:4} } : false,
polygon : type==='polygon' ? { shapeOptions:{color:'#388bfd',fillOpacity:.2,weight:2} } : false,
circle:false, rectangle:false, marker:false, circlemarker:false
}
});
map.addControl(drawControl);
}
map.on(L.Draw.Event.CREATED, function(e) {
drawnItems.clearLayers();
drawnItems.addLayer(e.layer);
const layer = e.layer;
if (currentTab === 'jalan') {
const ll = layer.getLatLngs();
let totalM=0; for(let i=0;i<ll.length-1;i++) totalM+=ll[i].distanceTo(ll[i+1]);
const panjang = parseFloat(totalM.toFixed(2));
const geojson = { type:'LineString', coordinates:ll.map(p=>[p.lng,p.lat]) };
currentDrawGeojson = geojson;
document.getElementById('jalan-panjang').value = `${panjang.toLocaleString('id-ID')} m`;
document.getElementById('jalan-panjang').dataset.raw = panjang;
const p = document.getElementById('jalan-geojson-preview');
if(p) p.outerHTML=`<div id="jalan-geojson-preview" class="geojson-preview">${JSON.stringify(geojson)}</div>`;
showInstruction(true,'Polyline dibuat! Lengkapi form lalu Simpan.');
openModal('jalan');
} else if (currentTab === 'parsil') {
const ll = layer.getLatLngs()[0];
const luas = parseFloat(L.GeometryUtil.geodesicArea(ll).toFixed(2));
const coords = ll.map(p=>[p.lng,p.lat]); coords.push(coords[0]);
const geojson = { type:'Polygon', coordinates:[coords] };
currentDrawGeojson = geojson;
document.getElementById('parsil-luas').value = `${luas.toLocaleString('id-ID')} m²`;
document.getElementById('parsil-luas').dataset.raw = luas;
const p = document.getElementById('parsil-geojson-preview');
if(p) p.outerHTML=`<div id="parsil-geojson-preview" class="geojson-preview">${JSON.stringify(geojson)}</div>`;
showInstruction(true,'Polygon dibuat! Lengkapi form lalu Simpan.');
openModal('parsil');
}
});
/* ================================================================
SWITCH TAB
================================================================ */
function switchTab(tab) {
currentTab = tab;
['jalan','parsil','spbu'].forEach(t=>document.getElementById(`tab-${t}`).classList.remove('active'));
document.getElementById(`tab-${tab}`).classList.add('active');
document.getElementById('sidebar-title-text').textContent = {jalan:'Data Jalan',parsil:'Parsil Tanah',spbu:'SPBU Gas Stations'}[tab];
const addBtn = document.getElementById('btn-add-main');
addBtn.className = tab==='spbu' ? 'btn-add spbu-add' : 'btn-add';
stopSpbuPick();
if(drawControl){map.removeControl(drawControl);drawControl=null;}
showInstruction(false);
drawnItems.clearLayers();
renderLegend();
renderList();
}
/* ================================================================
LEGENDA
================================================================ */
function renderLegend() {
const bar = document.getElementById('legend-bar');
if (currentTab==='jalan') {
bar.innerHTML = Object.entries(WARNA_JALAN).map(([k,v])=>
`<div class="legend-item"><div class="legend-line" style="background:${v}"></div><span>${k}</span></div>`).join('');
} else if (currentTab==='parsil') {
const s={'Sertifikat Hak Milik':'SHM','Sertifikat Hak Guna Bangunan':'HGB','Sertifikat Hak Guna Usaha':'HGU','Sertifikat Hak Pakai':'HP'};
bar.innerHTML = Object.entries(WARNA_PARSIL).map(([k,v])=>
`<div class="legend-item"><div class="legend-sq" style="background:${v}"></div><span>${s[k]}</span></div>`).join('');
} else {
bar.innerHTML=`<div class="legend-item"><div class="legend-dot" style="background:#22c55e"></div><span>Buka 24 Jam</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div><span>Tidak 24 Jam</span></div>`;
}
}
/* ================================================================
RENDER LIST
================================================================ */
function renderList() {
const list=document.getElementById('data-list'), strip=document.getElementById('stats-strip');
if (currentTab==='jalan') {
if (!dataJalan.length){list.innerHTML=emptyEl('road','Belum ada data. Klik <b>Tambah</b> lalu gambar polyline.');strip.innerHTML='';return;}
list.innerHTML=dataJalan.map(d=>{
const w=WARNA_JALAN[d.status_jalan]||'#888';
return `<div class="data-card" id="card-jalan-${d.id}" onclick="zoomToJalan(${d.id})">
<div class="card-top"><div class="card-name"><i class="fas fa-road" style="color:${w}"></i> ${esc(d.nama_jalan)}</div>
<span class="badge ${BADGE[d.status_jalan]||''}">${d.status_jalan.replace('Jalan ','')}</span></div>
<div class="card-meta"><span><i class="fas fa-ruler-horizontal"></i> ${(+d.panjang_m).toLocaleString('id-ID')} m</span>
${d.keterangan?`<span><i class="fas fa-note-sticky"></i> ${esc(d.keterangan).substring(0,28)}</span>`:''}</div>
<div class="card-actions">
<button class="btn-sm btn-zoom" onclick="event.stopPropagation();zoomToJalan(${d.id})"><i class="fas fa-magnifying-glass-location"></i> Zoom</button>
<button class="btn-sm btn-edit" onclick="event.stopPropagation();editJalan(${d.id})"><i class="fas fa-pen"></i> Edit</button>
<button class="btn-sm btn-del" onclick="event.stopPropagation();confirmDelete('jalan',${d.id},'${esc(d.nama_jalan)}')"><i class="fas fa-trash"></i> Hapus</button>
</div></div>`;
}).join('');
const n=dataJalan.filter(d=>d.status_jalan==='Jalan Nasional').length;
const p=dataJalan.filter(d=>d.status_jalan==='Jalan Provinsi').length;
const k=dataJalan.filter(d=>d.status_jalan==='Jalan Kabupaten').length;
strip.innerHTML=`<div class="stat-item"><div class="stat-num">${dataJalan.length}</div><div class="stat-label">Total</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--jalan-nasional)">${n}</div><div class="stat-label">Nasional</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--jalan-provinsi)">${p}</div><div class="stat-label">Provinsi</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--jalan-kabupaten)">${k}</div><div class="stat-label">Kabupaten</div></div>`;
} else if (currentTab==='parsil') {
if (!dataParsil.length){list.innerHTML=emptyEl('draw-polygon','Belum ada data. Klik <b>Tambah</b> lalu gambar polygon.');strip.innerHTML='';return;}
const s2s={'Sertifikat Hak Milik':'SHM','Sertifikat Hak Guna Bangunan':'HGB','Sertifikat Hak Guna Usaha':'HGU','Sertifikat Hak Pakai':'HP'};
list.innerHTML=dataParsil.map(d=>{
const w=WARNA_PARSIL[d.status_hak]||'#888';
return `<div class="data-card" id="card-parsil-${d.id}" onclick="zoomToParsil(${d.id})">
<div class="card-top"><div class="card-name"><i class="fas fa-vector-square" style="color:${w}"></i> ${esc(d.nama_parsil)}</div>
<span class="badge ${BADGE[d.status_hak]||''}">${s2s[d.status_hak]||d.status_hak}</span></div>
<div class="card-meta"><span><i class="fas fa-ruler-combined"></i> ${(+d.luas_m2).toLocaleString('id-ID')} m²</span>
${d.pemilik?`<span><i class="fas fa-user"></i> ${esc(d.pemilik)}</span>`:''}</div>
<div class="card-actions">
<button class="btn-sm btn-zoom" onclick="event.stopPropagation();zoomToParsil(${d.id})"><i class="fas fa-magnifying-glass-location"></i> Zoom</button>
<button class="btn-sm btn-edit" onclick="event.stopPropagation();editParsil(${d.id})"><i class="fas fa-pen"></i> Edit</button>
<button class="btn-sm btn-del" onclick="event.stopPropagation();confirmDelete('parsil',${d.id},'${esc(d.nama_parsil)}')"><i class="fas fa-trash"></i> Hapus</button>
</div></div>`;
}).join('');
const shm=dataParsil.filter(d=>d.status_hak==='Sertifikat Hak Milik').length;
const hgb=dataParsil.filter(d=>d.status_hak==='Sertifikat Hak Guna Bangunan').length;
const hgu=dataParsil.filter(d=>d.status_hak==='Sertifikat Hak Guna Usaha').length;
const hp =dataParsil.filter(d=>d.status_hak==='Sertifikat Hak Pakai').length;
strip.innerHTML=`<div class="stat-item"><div class="stat-num">${dataParsil.length}</div><div class="stat-label">Total</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--shm)">${shm}</div><div class="stat-label">SHM</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--hgb)">${hgb}</div><div class="stat-label">HGB</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--hgu)">${hgu}</div><div class="stat-label">HGU</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--hp)">${hp}</div><div class="stat-label">HP</div></div>`;
} else { // SPBU
if (!dataSpbu.length){list.innerHTML=emptyEl('gas-pump','Belum ada SPBU. Klik <b>Tambah</b> → klik peta untuk pilih lokasi.');strip.innerHTML='';return;}
list.innerHTML=dataSpbu.map(d=>{
const is24=d.buka_24jam==1||d.buka_24jam===true;
const w=is24?'#22c55e':'#ef4444';
return `<div class="data-card" id="card-spbu-${d.id}" onclick="zoomToSpbu(${d.id})">
<div class="card-top"><div class="card-name"><i class="fas fa-gas-pump" style="color:${w}"></i> ${esc(d.nama_spbu)}</div>
<span class="badge ${is24?'badge-24':'badge-no24'}">${is24?'24 Jam':'Tdk 24 Jam'}</span></div>
<div class="card-meta">
${d.telepon?`<span><i class="fas fa-phone"></i> ${esc(d.telepon)}</span>`:''}
<span><i class="fas fa-location-dot"></i> ${(+d.lat).toFixed(5)}, ${(+d.lng).toFixed(5)}</span>
</div>
<div class="card-actions">
<button class="btn-sm btn-zoom" onclick="event.stopPropagation();zoomToSpbu(${d.id})"><i class="fas fa-magnifying-glass-location"></i> Zoom</button>
<button class="btn-sm btn-edit" onclick="event.stopPropagation();editSpbu(${d.id})"><i class="fas fa-pen"></i> Edit</button>
<button class="btn-sm btn-del" onclick="event.stopPropagation();confirmDelete('spbu',${d.id},'${esc(d.nama_spbu)}')"><i class="fas fa-trash"></i> Hapus</button>
</div></div>`;
}).join('');
const j24=dataSpbu.filter(d=>d.buka_24jam==1||d.buka_24jam===true).length;
strip.innerHTML=`<div class="stat-item"><div class="stat-num">${dataSpbu.length}</div><div class="stat-label">Total SPBU</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--spbu-24)">${j24}</div><div class="stat-label">24 Jam</div></div>
<div class="stat-item"><div class="stat-num" style="color:var(--spbu-tdk)">${dataSpbu.length-j24}</div><div class="stat-label">Tdk 24 Jam</div></div>`;
}
}
function emptyEl(icon,msg){return `<div class="empty-state"><i class="fas fa-${icon}"></i><p>${msg}</p></div>`;}
/* ================================================================
LOAD DATA
================================================================ */
async function loadJalan() {
try { const r=await fetch(API.jalan),j=await r.json(); dataJalan=j.data||[]; renderJalanLayers(); if(currentTab==='jalan')renderList(); }
catch { showToast('Gagal memuat data jalan','error'); }
}
async function loadParsil() {
try { const r=await fetch(API.parsil),j=await r.json(); dataParsil=j.data||[]; renderParsilLayers(); if(currentTab==='parsil')renderList(); }
catch { showToast('Gagal memuat data parsil','error'); }
}
async function loadSpbu() {
try { const r=await fetch(API.spbu),j=await r.json(); dataSpbu=j.data||[]; renderSpbuLayers(); if(currentTab==='spbu')renderList(); }
catch { showToast('Gagal memuat data SPBU','error'); }
}
/* ================================================================
RENDER LAYER PETA
================================================================ */
function renderJalanLayers() {
Object.values(layersJalan).forEach(l=>map.removeLayer(l)); layersJalan={};
dataJalan.forEach(d=>{
const gj=typeof d.geojson==='string'?JSON.parse(d.geojson):d.geojson; if(!gj?.coordinates)return;
const layer=L.polyline(gj.coordinates.map(c=>[c[1],c[0]]),{color:WARNA_JALAN[d.status_jalan]||'#888',weight:4,opacity:.85}).addTo(map);
layer.bindPopup(popupJalan(d)); layer.on('click',()=>highlightCard('jalan',d.id)); layersJalan[d.id]=layer;
});
}
function renderParsilLayers() {
Object.values(layersParsil).forEach(l=>map.removeLayer(l)); layersParsil={};
dataParsil.forEach(d=>{
const gj=typeof d.geojson==='string'?JSON.parse(d.geojson):d.geojson; if(!gj?.coordinates)return;
const w=WARNA_PARSIL[d.status_hak]||'#888';
const layer=L.polygon(gj.coordinates[0].map(c=>[c[1],c[0]]),{color:w,weight:2,fillColor:w,fillOpacity:.25}).addTo(map);
layer.bindPopup(popupParsil(d)); layer.on('click',()=>highlightCard('parsil',d.id)); layersParsil[d.id]=layer;
});
}
function renderSpbuLayers() {
Object.values(layersSpbu).forEach(l => {
map.removeLayer(l);
spbu24Layer.removeLayer(l);
spbuTidak24Layer.removeLayer(l);
});
layersSpbu={};
// Hapus semua entry di daftarMarker
Object.keys(daftarMarker).forEach(k=>{
if(daftarMarker[k]){
map.removeLayer(daftarMarker[k]);
spbu24Layer.removeLayer(daftarMarker[k]);
spbuTidak24Layer.removeLayer(daftarMarker[k]);
}
delete daftarMarker[k];
});
dataSpbu.forEach(d=>{
const is24=d.buka_24jam==1||d.buka_24jam===true;
tampilkanMarkerSpbu(d.id, d.nama_spbu, d.telepon||'', is24?'Ya':'Tidak', +d.lat, +d.lng);
});
}
/* ── Render marker SPBU dengan ikon hijau/merah & draggable (sesuai template) ── */
function tampilkanMarkerSpbu(id, nama, wa, buka24, lat, lng) {
const ikonDipilih = (buka24 === 'Ya') ? iconHijau : iconMerah;
const is24 = (buka24 === 'Ya');
const marker = L.marker([lat, lng], {
icon: ikonDipilih,
draggable: true
}).addTo(is24 ? spbu24Layer : spbuTidak24Layer);
const popupIsi = `
<div style="font-family:'Inter',sans-serif;min-width:200px">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
<span style="font-size:1.3rem">⛽</span>
<b style="color:#222">${esc(nama)}</b>
</div>
<table style="font-size:.76rem;width:100%">
<tr><td style="color:#666;padding-right:8px">No WA</td><td><a href="https://wa.me/${wa.replace(/\D/g,'')}" target="_blank" style="color:#16a34a">${esc(wa)||'-'}</a></td></tr>
<tr><td style="color:#666">Status</td><td><b style="color:${is24?'#22c55e':'#ef4444'}">${is24?'🟢 24 Jam':'🔴 Tdk 24 Jam'}</b></td></tr>
<tr><td style="color:#666">Lokasi</td><td style="font-size:.68rem">${lat.toFixed(5)}, ${lng.toFixed(5)}</td></tr>
</table>
<p style="font-size:10px;color:#999;margin:6px 0 8px;font-style:italic">Geser pin untuk pindah lokasi</p>
<div style="display:flex;gap:4px">
<button onclick="editSpbu(${id})" style="flex:1;padding:4px;background:#388bfd;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">✏️ Edit</button>
<button onclick="confirmDelete('spbu',${id},'${esc(nama)}')" style="flex:1;padding:4px;background:#f85149;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">🗑️ Hapus</button>
</div>
</div>
`;
marker.bindPopup(popupIsi);
marker.on('click', () => highlightCard('spbu', id));
layersSpbu[id] = marker;
daftarMarker[id] = marker;
// Drag end → update lokasi ke database
marker.on('dragend', function(event) {
const posisiBaru = event.target.getLatLng();
const konfirmasi = confirm(`Pindahkan "${nama}" ke lokasi baru ini?`);
if (konfirmasi) {
updateLokasiSpbu(id, posisiBaru.lat, posisiBaru.lng);
} else {
marker.setLatLng([lat, lng]);
}
});
}
/* ── Update lokasi SPBU via PUT ── */
function updateLokasiSpbu(id, latBaru, lngBaru) {
const body = { id: id, lat: latBaru, lng: lngBaru };
fetch(API.spbu, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
showToast('Lokasi SPBU berhasil dipindahkan!', 'success');
// Update data lokal
const d = dataSpbu.find(x => x.id == id);
if (d) { d.lat = latBaru; d.lng = lngBaru; }
} else {
showToast('Gagal memindahkan lokasi: ' + (res.message || ''), 'error');
}
})
.catch(() => showToast('Kesalahan jaringan saat update lokasi', 'error'));
}
/* ================================================================
POPUP
================================================================ */
function popupJalan(d){const w=WARNA_JALAN[d.status_jalan]||'#888';return `<div style="font-family:'Inter',sans-serif;min-width:190px"><b style="color:#222"><span style="color:${w}">▬</span> ${d.nama_jalan}</b><table style="font-size:.76rem;margin-top:6px;width:100%"><tr><td style="color:#666;padding-right:8px">Status</td><td><b>${d.status_jalan}</b></td></tr><tr><td style="color:#666">Panjang</td><td><b>${(+d.panjang_m).toLocaleString('id-ID')} m</b></td></tr>${d.keterangan?`<tr><td style="color:#666">Ket.</td><td>${d.keterangan}</td></tr>`:''}</table><div style="display:flex;gap:4px;margin-top:8px"><button onclick="editJalan(${d.id})" style="flex:1;padding:4px;background:#388bfd;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">✏️ Edit</button><button onclick="confirmDelete('jalan',${d.id},'${d.nama_jalan}')" style="flex:1;padding:4px;background:#f85149;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">🗑️ Hapus</button></div></div>`;}
function popupParsil(d){const w=WARNA_PARSIL[d.status_hak]||'#888';const s={'Sertifikat Hak Milik':'SHM','Sertifikat Hak Guna Bangunan':'HGB','Sertifikat Hak Guna Usaha':'HGU','Sertifikat Hak Pakai':'HP'};return `<div style="font-family:'Inter',sans-serif;min-width:190px"><b style="color:#222"><span style="color:${w}">■</span> ${d.nama_parsil}</b><table style="font-size:.76rem;margin-top:6px;width:100%"><tr><td style="color:#666;padding-right:8px">Hak</td><td><b>${s[d.status_hak]||d.status_hak}</b></td></tr><tr><td style="color:#666">Luas</td><td><b>${(+d.luas_m2).toLocaleString('id-ID')} m²</b></td></tr>${d.pemilik?`<tr><td style="color:#666">Pemilik</td><td>${d.pemilik}</td></tr>`:''}</table><div style="display:flex;gap:4px;margin-top:8px"><button onclick="editParsil(${d.id})" style="flex:1;padding:4px;background:#388bfd;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">✏️ Edit</button><button onclick="confirmDelete('parsil',${d.id},'${d.nama_parsil}')" style="flex:1;padding:4px;background:#f85149;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">🗑️ Hapus</button></div></div>`;}
function popupSpbu(d){const is24=d.buka_24jam==1||d.buka_24jam===true;const c=is24?'#22c55e':'#ef4444';return `<div style="font-family:'Inter',sans-serif;min-width:200px"><div style="display:flex;align-items:center;gap:6px;margin-bottom:6px"><span style="font-size:1.3rem">⛽</span><b style="color:#222">${d.nama_spbu}</b></div><table style="font-size:.76rem;width:100%"><tr><td style="color:#666;padding-right:8px">Status</td><td><b style="color:${c}">${is24?'🟢 24 Jam':'🔴 Tdk 24 Jam'}</b></td></tr>${d.telepon?`<tr><td style="color:#666">No WA</td><td><a href="https://wa.me/${d.telepon.replace(/\D/g,'')}" target="_blank" style="color:#16a34a">${d.telepon}</a></td></tr>`:''}<tr><td style="color:#666">Lokasi</td><td style="font-size:.68rem">${(+d.lat).toFixed(5)}, ${(+d.lng).toFixed(5)}</td></tr></table><p style="font-size:10px;color:#999;margin:6px 0 4px;font-style:italic">Geser pin untuk pindah lokasi</p><div style="display:flex;gap:4px;margin-top:4px"><button onclick="editSpbu(${d.id})" style="flex:1;padding:4px;background:#388bfd;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">✏️ Edit</button><button onclick="confirmDelete('spbu',${d.id},'${d.nama_spbu}')" style="flex:1;padding:4px;background:#f85149;color:#fff;border:none;border-radius:4px;font-size:.7rem;cursor:pointer">🗑️ Hapus</button></div></div>`;}
/* ================================================================
ZOOM TO
================================================================ */
function zoomToJalan(id){const l=layersJalan[id];if(!l)return;map.fitBounds(l.getBounds(),{padding:[40,40]});l.openPopup();highlightCard('jalan',id);}
function zoomToParsil(id){const l=layersParsil[id];if(!l)return;map.fitBounds(l.getBounds(),{padding:[40,40]});l.openPopup();highlightCard('parsil',id);}
function zoomToSpbu(id){const d=dataSpbu.find(x=>x.id==id);if(!d)return;map.setView([+d.lat,+d.lng],16);const l=layersSpbu[id];if(l)l.openPopup();highlightCard('spbu',id);}
function highlightCard(type,id){document.querySelectorAll('.data-card').forEach(c=>c.classList.remove('active-card'));const c=document.getElementById(`card-${type}-${id}`);if(c){c.classList.add('active-card');c.scrollIntoView({behavior:'smooth',block:'nearest'});}}
/* ================================================================
DRAW MODE
================================================================ */
function startDrawMode() {
currentDrawGeojson=null; drawnItems.clearLayers(); editMode=false; editTargetId=null;
if (currentTab==='jalan') {
resetForm('jalan'); initDrawControl('polyline');
showInstruction(true,'Klik peta untuk menambah titik polyline. Double-klik untuk selesai.');
} else if (currentTab==='parsil') {
resetForm('parsil'); initDrawControl('polygon');
showInstruction(true,'Klik peta untuk menggambar polygon. Double-klik untuk menutup.');
} else { // SPBU tampilkan instruksi klik peta langsung
showInstruction(true,'Klik lokasi di peta untuk menambah SPBU baru → form muncul di titik klik.');
map.getContainer().style.cursor = 'crosshair';
// Popup akan terbuka saat event click pada peta
}
}
/* Tombol di dalam modal SPBU: tutup modal sementara, aktifkan mode pick */
function activateMapPick() {
document.getElementById('modal-spbu-overlay').classList.remove('open');
spbuPickMode = 'pick';
map.getContainer().style.cursor = 'crosshair';
showInstruction(true,'Klik titik lokasi SPBU di peta…');
}
function stopSpbuPick() {
spbuPickMode = 'idle';
map.getContainer().style.cursor = '';
if (spbuTempMarker) { map.removeLayer(spbuTempMarker); spbuTempMarker=null; }
currentDrawGeojson=null; spbuClickLat=null; spbuClickLng=null;
}
function resetForm(type) {
if (type==='jalan') {
['jalan-id','jalan-nama','jalan-keterangan'].forEach(id=>document.getElementById(id).value='');
document.getElementById('jalan-status').value='';
document.getElementById('jalan-panjang').value='';
document.getElementById('modal-jalan-title').textContent='Tambah Data Jalan';
resetGeoPreview('jalan');
} else if (type==='parsil') {
['parsil-id','parsil-nama','parsil-pemilik','parsil-keterangan'].forEach(id=>document.getElementById(id).value='');
document.getElementById('parsil-status').value='';
document.getElementById('parsil-luas').value='';
document.getElementById('modal-parsil-title').textContent='Tambah Data Parsil Tanah';
resetGeoPreview('parsil');
} else {
['spbu-id','spbu-nama','spbu-telepon','spbu-lat','spbu-lng'].forEach(id=>document.getElementById(id).value='');
document.getElementById('spbu-24-tidak').checked=true;
document.getElementById('modal-spbu-title').textContent='Tambah Data SPBU';
const btn=document.getElementById('btn-pick-loc');
if(btn){btn.innerHTML='<i class="fas fa-map-pin"></i> Klik Peta untuk Pilih Lokasi'; btn.classList.remove('picking');}
spbuClickLat=null; spbuClickLng=null; currentDrawGeojson=null;
resetGeoPreview('spbu');
}
}
function resetGeoPreview(type) {
const msgs={jalan:'Gambar polyline di peta terlebih dahulu',parsil:'Gambar polygon di peta terlebih dahulu',spbu:'Belum ada lokasi dipilih'};
const el=document.getElementById(`${type}-geojson-preview`);
if(el) el.outerHTML=`<div id="${type}-geojson-preview" class="geojson-empty"><i class="fas fa-triangle-exclamation"></i> ${msgs[type]}</div>`;
}
/* ================================================================
EDIT
================================================================ */
function editJalan(id) {
const d=dataJalan.find(x=>x.id==id); if(!d)return;
editMode=true; editTargetId=id;
currentDrawGeojson=typeof d.geojson==='string'?JSON.parse(d.geojson):d.geojson;
drawnItems.clearLayers();
drawnItems.addLayer(L.polyline(currentDrawGeojson.coordinates.map(c=>[c[1],c[0]]),{color:'#388bfd',dashArray:'6,4'}));
document.getElementById('jalan-id').value=d.id;
document.getElementById('jalan-nama').value=d.nama_jalan;
document.getElementById('jalan-status').value=d.status_jalan;
document.getElementById('jalan-panjang').value=`${(+d.panjang_m).toLocaleString('id-ID')} m`;
document.getElementById('jalan-panjang').dataset.raw=d.panjang_m;
document.getElementById('jalan-keterangan').value=d.keterangan||'';
document.getElementById('modal-jalan-title').textContent='Edit Data Jalan';
const p=document.getElementById('jalan-geojson-preview');
if(p) p.outerHTML=`<div id="jalan-geojson-preview" class="geojson-preview">${JSON.stringify(currentDrawGeojson)}</div>`;
initDrawControl('polyline');
showInstruction(true,'Gambar ulang polyline atau langsung Simpan.');
openModal('jalan');
}
function editParsil(id) {
const d=dataParsil.find(x=>x.id==id); if(!d)return;
editMode=true; editTargetId=id;
currentDrawGeojson=typeof d.geojson==='string'?JSON.parse(d.geojson):d.geojson;
drawnItems.clearLayers();
drawnItems.addLayer(L.polygon(currentDrawGeojson.coordinates[0].map(c=>[c[1],c[0]]),{color:'#388bfd',dashArray:'6,4',fillOpacity:.1}));
document.getElementById('parsil-id').value=d.id;
document.getElementById('parsil-nama').value=d.nama_parsil;
document.getElementById('parsil-status').value=d.status_hak;
document.getElementById('parsil-luas').value=`${(+d.luas_m2).toLocaleString('id-ID')} m²`;
document.getElementById('parsil-luas').dataset.raw=d.luas_m2;
document.getElementById('parsil-pemilik').value=d.pemilik||'';
document.getElementById('parsil-keterangan').value=d.keterangan||'';
document.getElementById('modal-parsil-title').textContent='Edit Data Parsil Tanah';
const p=document.getElementById('parsil-geojson-preview');
if(p) p.outerHTML=`<div id="parsil-geojson-preview" class="geojson-preview">${JSON.stringify(currentDrawGeojson)}</div>`;
initDrawControl('polygon');
showInstruction(true,'Gambar ulang polygon atau langsung Simpan.');
openModal('parsil');
}
function editSpbu(id) {
const d=dataSpbu.find(x=>x.id==id); if(!d)return;
editMode=true; editTargetId=id;
currentDrawGeojson=typeof d.geojson==='string'?JSON.parse(d.geojson):d.geojson;
spbuClickLat=+d.lat; spbuClickLng=+d.lng;
document.getElementById('spbu-id').value=d.id;
document.getElementById('spbu-nama').value=d.nama_spbu;
document.getElementById('spbu-telepon').value=d.telepon||'';
document.getElementById('spbu-lat').value=d.lat;
document.getElementById('spbu-lng').value=d.lng;
document.getElementById('modal-spbu-title').textContent='Edit Data SPBU';
const is24=d.buka_24jam==1||d.buka_24jam===true;
document.getElementById(is24?'spbu-24-ya':'spbu-24-tidak').checked=true;
const p=document.getElementById('spbu-geojson-preview');
if(p) p.outerHTML=`<div id="spbu-geojson-preview" class="geojson-preview">${JSON.stringify(currentDrawGeojson)}</div>`;
const btn=document.getElementById('btn-pick-loc');
if(btn){btn.innerHTML='<i class="fas fa-map-pin"></i> Ubah Lokasi (klik tombol lalu klik peta)'; btn.classList.remove('picking');}
// Marker lokasi lama
if(spbuTempMarker) map.removeLayer(spbuTempMarker);
const col=is24?'#22c55e':'#ef4444';
spbuTempMarker=L.circleMarker([spbuClickLat,spbuClickLng],{radius:12,fillColor:col,color:'#fff',weight:3,fillOpacity:.9}).addTo(map);
spbuTempMarker.bindTooltip('⛽ '+d.nama_spbu,{permanent:true,direction:'top',offset:[0,-12]});
map.setView([spbuClickLat,spbuClickLng],15);
// Saat edit, klik peta tetap bisa mengubah lokasi dari dalam modal
spbuPickMode='modal-open';
showInstruction(true,'Klik tombol "Ubah Lokasi" di form atau klik peta untuk pindah lokasi.');
openModal('spbu');
}
/* ================================================================
SAVE
================================================================ */
async function saveJalan() {
const nama=document.getElementById('jalan-nama').value.trim();
const status=document.getElementById('jalan-status').value;
const panjang=parseFloat(document.getElementById('jalan-panjang').dataset.raw||0);
const ket=document.getElementById('jalan-keterangan').value.trim();
const id=document.getElementById('jalan-id').value;
if(!nama) return showToast('Nama jalan wajib diisi','error');
if(!status) return showToast('Status jalan wajib dipilih','error');
if(!currentDrawGeojson) return showToast('Gambar polyline di peta terlebih dahulu','error');
const btn=document.getElementById('btn-save-jalan'); setLoading(btn,true);
const body={nama_jalan:nama,status_jalan:status,panjang_m:panjang,geojson:currentDrawGeojson,keterangan:ket};
try {
let res;
if(editMode&&id){body.id=+id;res=await fetch(API.jalan,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});}
else res=await fetch(API.jalan,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const json=await res.json();
if(json.status==='success'){showToast(json.message,'success');closeModal('jalan');drawnItems.clearLayers();await loadJalan();renderList();}
else showToast(json.message||'Gagal menyimpan','error');
} catch{showToast('Kesalahan jaringan','error');}
setLoading(btn,false);
}
async function saveParsil() {
const nama=document.getElementById('parsil-nama').value.trim();
const status=document.getElementById('parsil-status').value;
const luas=parseFloat(document.getElementById('parsil-luas').dataset.raw||0);
const pemilik=document.getElementById('parsil-pemilik').value.trim();
const ket=document.getElementById('parsil-keterangan').value.trim();
const id=document.getElementById('parsil-id').value;
if(!nama) return showToast('Nama parsil wajib diisi','error');
if(!status) return showToast('Status kepemilikan wajib dipilih','error');
if(!currentDrawGeojson) return showToast('Gambar polygon di peta terlebih dahulu','error');
const btn=document.getElementById('btn-save-parsil'); setLoading(btn,true);
const body={nama_parsil:nama,status_hak:status,luas_m2:luas,geojson:currentDrawGeojson,pemilik,keterangan:ket};
try {
let res;
if(editMode&&id){body.id=+id;res=await fetch(API.parsil,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});}
else res=await fetch(API.parsil,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const json=await res.json();
if(json.status==='success'){showToast(json.message,'success');closeModal('parsil');drawnItems.clearLayers();await loadParsil();renderList();}
else showToast(json.message||'Gagal menyimpan','error');
} catch{showToast('Kesalahan jaringan','error');}
setLoading(btn,false);
}
async function saveSpbu() {
const nama=document.getElementById('spbu-nama').value.trim();
const telepon=document.getElementById('spbu-telepon').value.trim();
const buka24=document.querySelector('input[name="spbu-jam"]:checked')?.value==='1'?1:0;
const id=document.getElementById('spbu-id').value;
if(!nama) return showToast('Nama SPBU wajib diisi','error');
if(spbuClickLat===null||spbuClickLng===null) return showToast('Tentukan lokasi SPBU di peta terlebih dahulu','error');
const geojson=currentDrawGeojson||{type:'Point',coordinates:[spbuClickLng,spbuClickLat]};
const btn=document.getElementById('btn-save-spbu'); setLoading(btn,true);
const body={nama_spbu:nama,telepon,buka_24jam:buka24,lat:spbuClickLat,lng:spbuClickLng,geojson};
try {
let res;
if(editMode&&id){body.id=+id;res=await fetch(API.spbu,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});}
else res=await fetch(API.spbu,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
const json=await res.json();
if(json.status==='success'){
showToast(json.message,'success');
closeModal('spbu');
await loadSpbu();
renderList();
} else showToast(json.message||'Gagal menyimpan','error');
} catch{showToast('Kesalahan jaringan','error');}
setLoading(btn,false);
}
/* ================================================================
SIMPAN DATA SPBU VIA POPUP PETA (seperti template)
================================================================ */
async function simpanDataPopup(lat, lng) {
const namaPom = document.getElementById('popupNamaPom')?.value.trim();
const noWa = document.getElementById('popupNoWa')?.value.trim();
const buka24El = document.querySelector('input[name="popupBuka24"]:checked');
const buka24 = buka24El ? buka24El.value : 'Tidak';
if (!namaPom || !noWa) {
alert('Nama SPBU dan Nomor WA tidak boleh kosong!');
return;
}
const geojson = { type:'Point', coordinates:[lng, lat] };
const body = {
nama_spbu : namaPom,
telepon : noWa,
buka_24jam: buka24 === 'Ya' ? 1 : 0,
lat : lat,
lng : lng,
geojson : geojson
};
try {
const res = await fetch(API.spbu, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
const json = await res.json();
if (json.status === 'success') {
map.closePopup();
showToast('SPBU berhasil disimpan!', 'success');
// Tambah ke data lokal & render marker langsung
const newId = json.id;
const newData = { id:newId, nama_spbu:namaPom, telepon:noWa, buka_24jam:buka24==='Ya'?1:0, lat:lat, lng:lng, geojson:geojson };
dataSpbu.push(newData);
tampilkanMarkerSpbu(newId, namaPom, noWa, buka24, lat, lng);
renderList();
} else {
showToast('Gagal menyimpan: ' + (json.message || ''), 'error');
}
} catch {
showToast('Kesalahan jaringan', 'error');
}
}
/* ================================================================
DELETE
================================================================ */
function confirmDelete(type,id,name){document.getElementById('confirm-name').textContent=name;document.getElementById('btn-confirm-del').onclick=()=>deleteRecord(type,id);document.getElementById('modal-confirm-overlay').classList.add('open');}
function closeConfirm(){document.getElementById('modal-confirm-overlay').classList.remove('open');}
async function deleteRecord(type,id){
closeConfirm();
try {
const res=await fetch(`${API[type]}?id=${id}`,{method:'DELETE'});
const json=await res.json();
if(json.status==='success'){
showToast(json.message,'success');
if(type==='jalan'){if(layersJalan[id])map.removeLayer(layersJalan[id]);delete layersJalan[id];dataJalan=dataJalan.filter(d=>d.id!=id);}
if(type==='parsil'){if(layersParsil[id])map.removeLayer(layersParsil[id]);delete layersParsil[id];dataParsil=dataParsil.filter(d=>d.id!=id);}
if(type==='spbu'){
if(layersSpbu[id]){
map.removeLayer(layersSpbu[id]);
spbu24Layer.removeLayer(layersSpbu[id]);
spbuTidak24Layer.removeLayer(layersSpbu[id]);
delete layersSpbu[id];
}
if(daftarMarker[id]){
map.removeLayer(daftarMarker[id]);
spbu24Layer.removeLayer(daftarMarker[id]);
spbuTidak24Layer.removeLayer(daftarMarker[id]);
delete daftarMarker[id];
}
dataSpbu=dataSpbu.filter(d=>d.id!=id);
}
renderList();
} else showToast(json.message||'Gagal menghapus','error');
} catch{showToast('Kesalahan jaringan','error');}
}
/* ================================================================
MODAL HELPERS
================================================================ */
function openModal(type){document.getElementById(`modal-${type}-overlay`).classList.add('open');}
function closeModal(type){
document.getElementById(`modal-${type}-overlay`).classList.remove('open');
showInstruction(false);
if(drawControl){map.removeControl(drawControl);drawControl=null;}
drawnItems.clearLayers(); currentDrawGeojson=null;
editMode=false; editTargetId=null;
if(type==='spbu') stopSpbuPick();
}
document.querySelectorAll('.modal-overlay').forEach(el=>{
el.addEventListener('click',function(e){
if(e.target!==this)return;
const id=this.id;
if(id==='modal-confirm-overlay') closeConfirm();
else if(id==='modal-jalan-overlay') closeModal('jalan');
else if(id==='modal-parsil-overlay') closeModal('parsil');
else if(id==='modal-spbu-overlay') closeModal('spbu');
});
});
/* ================================================================
UTILITIES
================================================================ */
function showInstruction(show,text=''){
const el=document.getElementById('map-instruction');
document.getElementById('instruction-text').textContent=text;
el.classList.toggle('visible',show);
}
function setLoading(btn,loading){
btn.disabled=loading;
btn.innerHTML=loading?'<i class="fas fa-spinner fa-spin"></i> Menyimpan…':'<i class="fas fa-floppy-disk"></i> Simpan';
}
function showToast(msg,type='success'){
const tc=document.getElementById('toast-container');
const icon=type==='success'?'fa-circle-check':'fa-circle-xmark';
const el=document.createElement('div'); el.className=`toast ${type}`;
el.innerHTML=`<i class="fas ${icon}"></i> ${msg}`; tc.appendChild(el);
setTimeout(()=>{el.style.opacity='0';el.style.transform='translateX(40px)';el.style.transition='.3s';setTimeout(()=>el.remove(),300);},3200);
}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');}
/* ================================================================
INIT
================================================================ */
(async function(){
renderLegend();
await Promise.all([loadJalan(),loadParsil(),loadSpbu()]);
renderList();
})();
</script>
</body>
</html>