1228 lines
73 KiB
HTML
1228 lines
73 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 – Jalan, Parsil Tanah & 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>
|
||
<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 |
|
||
<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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');}
|
||
|
||
/* ================================================================
|
||
INIT
|
||
================================================================ */
|
||
(async function(){
|
||
renderLegend();
|
||
await Promise.all([loadJalan(),loadParsil(),loadSpbu()]);
|
||
renderList();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html> |