Files
WebGis_UAS_Project/02/index.html
T
2026-06-10 19:23:50 +07:00

1312 lines
55 KiB
HTML
Raw 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 — Manajemen Jalan, Parsil & Kerusakan Jalan</title>
<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 href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0b0f14;
--surface: #131920;
--surface2: #1c2530;
--border: #263040;
--text: #dde6f0;
--muted: #6b7f94;
--radius: 10px;
--shadow: 0 8px 32px rgba(0,0,0,.6);
--nasional: #ef4444;
--provinsi: #f59e0b;
--kabupaten: #22c55e;
--shm: #6366f1;
--hgb: #06b6d4;
--hgu: #f97316;
--hp: #ec4899;
/* Kerusakan jalan */
--rusak-berat: #ff2d55;
--rusak-sedang: #ff9f0a;
--rusak-ringan: #ffd60a;
--accent: #38bdf8;
--accent-dk: #0ea5e9;
}
html, body { height: 100%; font-family: 'DM Sans', sans-serif; background: var(--bg); color: var(--text); }
.app {
display: grid;
grid-template-columns: 380px 1fr;
grid-template-rows: 56px 1fr;
height: 100vh;
}
/* ── Header ── */
header {
grid-column: 1 / -1;
display: flex; align-items: center; gap: 12px;
padding: 0 20px;
background: var(--surface);
border-bottom: 1px solid var(--border);
z-index: 10;
}
.logo { font-size: 20px; }
header h1 { font-size: 14px; font-weight: 700; }
header h1 span { color: var(--accent); }
.tab-switcher {
margin-left: auto;
display: flex; gap: 4px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 8px;
padding: 3px;
}
.tab-btn {
padding: 5px 14px; border-radius: 6px; border: none;
font-size: 12px; font-weight: 600; cursor: pointer;
font-family: inherit; color: var(--muted); background: transparent;
transition: all .2s;
}
.tab-btn.active { background: var(--accent); color: #fff; }
/* ── Sidebar ── */
aside {
background: var(--surface);
border-right: 1px solid var(--border);
display: flex; flex-direction: column;
overflow: hidden;
}
.sidebar-top {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.legend-row { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 10px; }
.legend-pill {
display: flex; align-items: center; gap: 5px;
font-size: 10px; font-weight: 600;
padding: 3px 9px; border-radius: 20px;
border: 1px solid currentColor;
}
.legend-pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.pill-nasional { color: var(--nasional); }
.pill-provinsi { color: var(--provinsi); }
.pill-kabupaten { color: var(--kabupaten); }
.pill-shm { color: var(--shm); }
.pill-hgb { color: var(--hgb); }
.pill-hgu { color: var(--hgu); }
.pill-hp { color: var(--hp); }
.pill-rusak-berat { color: var(--rusak-berat); }
.pill-rusak-sedang { color: var(--rusak-sedang); }
.pill-rusak-ringan { color: var(--rusak-ringan); }
/* ── Unified Search ── */
.search-container { position: relative; }
.search-wrap { position: relative; }
.search-wrap input {
width: 100%; background: var(--surface2);
border: 1px solid var(--border); border-radius: 8px;
padding: 8px 10px 8px 34px; color: var(--text);
font-size: 12px; font-family: inherit; outline: none;
transition: border-color .2s;
}
.search-wrap input:focus { border-color: var(--accent); }
.search-icon {
position: absolute; left: 10px; top: 50%; transform: translateY(-50%);
color: var(--muted); pointer-events: none; font-size: 13px;
}
.search-wrap input:not(:placeholder-shown) ~ .search-clear { display: flex; }
.search-clear {
display: none;
position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
background: var(--border); border: none; border-radius: 50%;
width: 18px; height: 18px; align-items: center; justify-content: center;
cursor: pointer; color: var(--muted); font-size: 10px;
}
.search-info {
font-size: 10px; color: var(--muted); margin-top: 5px;
display: flex; gap: 8px; align-items: center;
}
.search-badge {
background: rgba(56,189,248,.15); color: var(--accent);
border: 1px solid rgba(56,189,248,.3);
border-radius: 12px; padding: 1px 7px; font-size: 10px; font-weight: 600;
}
/* data list */
#data-list { flex: 1; overflow-y: auto; padding: 8px 10px; }
#data-list::-webkit-scrollbar { width: 4px; }
#data-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
/* data card */
.data-card {
background: var(--surface2); border: 1px solid var(--border);
border-radius: var(--radius); padding: 11px 13px;
margin-bottom: 7px; cursor: pointer;
transition: border-color .2s, transform .15s;
}
.data-card:hover { border-color: var(--accent); transform: translateX(3px); }
.data-card.active { border-color: var(--accent); background: #1a2535; }
/* type indicator strip on card */
.data-card[data-type="kerusakan"] { border-left: 3px solid var(--rusak-berat); }
.data-card[data-type="jalan"] { border-left: 3px solid var(--nasional); }
.data-card[data-type="parsil"] { border-left: 3px solid var(--shm); }
.card-type-badge {
font-size: 9px; font-weight: 700; letter-spacing: .5px;
text-transform: uppercase; color: var(--muted);
margin-bottom: 3px;
}
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.card-name { font-size: 12px; font-weight: 600; line-height: 1.4; }
.status-tag {
font-size: 10px; font-weight: 700; padding: 2px 8px;
border-radius: 20px; white-space: nowrap; flex-shrink: 0;
border: 1px solid currentColor; letter-spacing: .3px;
}
.tag-nasional { color: var(--nasional); background: rgba(239,68,68,.12); }
.tag-provinsi { color: var(--provinsi); background: rgba(245,158,11,.12); }
.tag-kabupaten { color: var(--kabupaten); background: rgba(34,197,94,.12); }
.tag-shm { color: var(--shm); background: rgba(99,102,241,.12); }
.tag-hgb { color: var(--hgb); background: rgba(6,182,212,.12); }
.tag-hgu { color: var(--hgu); background: rgba(249,115,22,.12); }
.tag-hp { color: var(--hp); background: rgba(236,72,153,.12); }
.tag-rusak-berat { color: var(--rusak-berat); background: rgba(255,45,85,.12); }
.tag-rusak-sedang { color: var(--rusak-sedang); background: rgba(255,159,10,.12); }
.tag-rusak-ringan { color: var(--rusak-ringan); background: rgba(255,214,10,.12); }
.card-meta {
font-size: 11px; color: var(--muted); margin-top: 4px;
font-family: 'JetBrains Mono', monospace;
}
.card-actions { display: flex; gap: 5px; margin-top: 9px; }
.btn-sm {
flex: 1; padding: 5px 0;
font-size: 10px; font-weight: 600; border-radius: 6px;
cursor: pointer; border: 1px solid var(--border);
background: var(--surface); color: var(--muted);
font-family: inherit; transition: all .18s;
}
.btn-sm:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
.btn-sm.danger:hover { background: #ef4444; color: #fff; border-color: #ef4444; }
/* bottom CTA */
.cta-box { padding: 12px 14px; border-top: 1px solid var(--border); flex-shrink: 0; }
.btn-draw {
width: 100%; padding: 10px;
background: var(--accent); color: var(--bg);
border: none; border-radius: var(--radius);
font-size: 12px; font-weight: 700; font-family: inherit;
cursor: pointer; transition: background .2s;
display: flex; align-items: center; justify-content: center; gap: 7px;
}
.btn-draw:hover { background: var(--accent-dk); }
/* Kerusakan draw button special color */
.btn-draw.kerusakan-mode {
background: var(--rusak-berat);
}
.btn-draw.kerusakan-mode:hover { background: #d91240; }
/* ── Map ── */
#map-wrap { position: relative; overflow: hidden; }
#map { width: 100%; height: 100%; }
#draw-banner {
position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
z-index: 900;
background: var(--surface);
border: 1.5px solid var(--accent);
border-radius: 30px;
padding: 8px 20px;
font-size: 12px; font-weight: 600; color: var(--accent);
box-shadow: var(--shadow), 0 0 0 4px rgba(56,189,248,.1);
display: none; align-items: center; gap: 8px;
animation: slideDown .3s ease;
}
#draw-banner.show { display: flex; }
#draw-banner.danger-mode {
border-color: var(--rusak-berat);
color: var(--rusak-berat);
box-shadow: var(--shadow), 0 0 0 4px rgba(255,45,85,.1);
}
@keyframes slideDown { from { opacity:0; transform: translateX(-50%) translateY(-10px); } to { opacity:1; transform: translateX(-50%) translateY(0); } }
.blink { animation: pulse 1.2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
/* ══ MODAL ══ */
.overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.72);
backdrop-filter: blur(5px);
z-index: 2000;
display: flex; align-items: center; justify-content: center;
opacity: 0; pointer-events: none; transition: opacity .25s;
}
.overlay.open { opacity: 1; pointer-events: all; }
.modal {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 24px 24px 20px;
width: 420px; max-width: 96vw;
box-shadow: var(--shadow);
transform: translateY(10px); transition: transform .25s;
max-height: 90vh; overflow-y: auto;
}
.overlay.open .modal { transform: translateY(0); }
.modal-header {
display: flex; align-items: center; gap: 10px; margin-bottom: 18px;
}
.modal-icon {
width: 30px; height: 30px; border-radius: 8px;
background: var(--accent); display: grid; place-items: center;
font-size: 14px; color: var(--bg);
}
.modal-icon.danger { background: var(--rusak-berat); }
.modal-header h3 { font-size: 14px; font-weight: 700; }
.form-group { margin-bottom: 13px; }
.form-group label {
display: block; font-size: 11px; font-weight: 600;
color: var(--muted); margin-bottom: 5px;
text-transform: uppercase; letter-spacing: .5px;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%; background: var(--surface2);
border: 1px solid var(--border); border-radius: 8px;
padding: 9px 12px; color: var(--text);
font-size: 13px; font-family: inherit;
outline: none; transition: border-color .2s;
appearance: none;
}
.form-group textarea { resize: vertical; min-height: 80px; }
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus { border-color: var(--accent); }
.form-group input[readonly] {
background: rgba(56,189,248,.05);
border-color: rgba(56,189,248,.3);
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
font-size: 12px;
}
/* Koordinat display */
.coords-display {
background: rgba(56,189,248,.06);
border: 1px solid rgba(56,189,248,.2);
border-radius: 8px; padding: 8px 12px;
font-size: 11px; font-family: 'JetBrains Mono', monospace;
color: var(--accent);
}
.coords-display span { color: var(--muted); font-size: 10px; }
.form-actions { display: flex; gap: 8px; margin-top: 18px; }
.btn-cancel {
flex: 1; padding: 10px;
background: var(--surface2); border: 1px solid var(--border);
border-radius: var(--radius); color: var(--muted);
font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer;
transition: all .2s;
}
.btn-cancel:hover { border-color: #ef4444; color: #ef4444; }
.btn-save {
flex: 2; padding: 10px;
background: var(--accent); border: none;
border-radius: var(--radius); color: var(--bg);
font-size: 12px; font-weight: 700; font-family: inherit; cursor: pointer;
transition: background .2s;
}
.btn-save:hover { background: var(--accent-dk); }
.btn-save.danger { background: var(--rusak-berat); }
.btn-save.danger:hover { background: #d91240; }
/* ── Toast ── */
#toast {
position: fixed; bottom: 20px; right: 20px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 10px 16px;
font-size: 12px; font-weight: 500;
box-shadow: var(--shadow); z-index: 9999;
transform: translateY(20px); opacity: 0; transition: all .3s;
}
#toast.show { transform: translateY(0); opacity: 1; }
#toast.success { border-color: #22c55e; }
#toast.error { border-color: #ef4444; }
/* ── Custom Leaflet Popup ── */
.leaflet-popup-content-wrapper {
background: var(--surface) !important;
border: 1px solid var(--border) !important;
border-radius: 12px !important;
color: var(--text) !important;
box-shadow: var(--shadow) !important;
}
.leaflet-popup-tip { background: var(--surface) !important; }
.leaflet-popup-content { margin: 12px 14px !important; font-family: 'DM Sans', sans-serif; }
.popup-name { font-weight: 700; font-size: 13px; margin-bottom: 5px; }
.popup-meta { font-size: 11px; color: var(--muted); margin-bottom: 3px; }
.popup-desc { font-size: 11px; color: var(--text); margin-bottom: 3px; font-style: italic; }
.popup-actions { display: flex; gap: 6px; margin-top: 10px; }
/* Severity indicator */
.severity-dot {
display: inline-block; width: 10px; height: 10px; border-radius: 50%;
margin-right: 5px; vertical-align: middle;
}
.severity-berat .severity-dot { background: var(--rusak-berat); }
.severity-sedang .severity-dot { background: var(--rusak-sedang); }
.severity-ringan .severity-dot { background: var(--rusak-ringan); }
/* empty state */
.empty-state { text-align: center; padding: 36px 16px; color: var(--muted); font-size: 12px; }
.empty-state .ei { font-size: 30px; margin-bottom: 8px; }
/* Section divider in list when showing all */
.list-section-header {
font-size: 10px; font-weight: 700; text-transform: uppercase;
letter-spacing: .8px; color: var(--muted);
padding: 8px 4px 4px; margin-bottom: 4px;
border-bottom: 1px solid var(--border);
}
</style>
</head>
<body>
<div class="app">
<!-- ═══ HEADER ═══ -->
<header>
<span class="logo">🗺️</span>
<h1>WebGIS <span>Jalan, Parsil & Kerusakan</span></h1>
<div class="tab-switcher">
<button class="tab-btn active" id="tab-jalan" onclick="switchTab('jalan')">🛣️ Jalan</button>
<button class="tab-btn" id="tab-parsil" onclick="switchTab('parsil')">🏡 Parsil</button>
<button class="tab-btn" id="tab-kerusakan" onclick="switchTab('kerusakan')">⚠️ Kerusakan</button>
<button class="tab-btn" id="tab-semua" onclick="switchTab('semua')">🔍 Cari Semua</button>
</div>
</header>
<!-- ═══ SIDEBAR ═══ -->
<aside>
<div class="sidebar-top">
<!-- Legend Jalan -->
<div id="legend-jalan" class="legend-row">
<span class="legend-pill pill-nasional"><span class="dot"></span>Nasional</span>
<span class="legend-pill pill-provinsi"><span class="dot"></span>Provinsi</span>
<span class="legend-pill pill-kabupaten"><span class="dot"></span>Kabupaten</span>
</div>
<!-- Legend Parsil -->
<div id="legend-parsil" class="legend-row" style="display:none">
<span class="legend-pill pill-shm"><span class="dot"></span>SHM</span>
<span class="legend-pill pill-hgb"><span class="dot"></span>HGB</span>
<span class="legend-pill pill-hgu"><span class="dot"></span>HGU</span>
<span class="legend-pill pill-hp"><span class="dot"></span>HP</span>
</div>
<!-- Legend Kerusakan -->
<div id="legend-kerusakan" class="legend-row" style="display:none">
<span class="legend-pill pill-rusak-berat"><span class="dot"></span>Berat</span>
<span class="legend-pill pill-rusak-sedang"><span class="dot"></span>Sedang</span>
<span class="legend-pill pill-rusak-ringan"><span class="dot"></span>Ringan</span>
</div>
<!-- Legend Semua -->
<div id="legend-semua" class="legend-row" style="display:none">
<span class="legend-pill pill-nasional"><span class="dot"></span>Jalan</span>
<span class="legend-pill pill-shm"><span class="dot"></span>Parsil</span>
<span class="legend-pill pill-rusak-berat"><span class="dot"></span>Kerusakan</span>
</div>
<div class="search-container">
<div class="search-wrap">
<span class="search-icon">🔍</span>
<input type="text" id="search-input"
placeholder="Cari nama, status, nomor sertifikat…"
oninput="filterList()"/>
<button class="search-clear" onclick="clearSearch()">×</button>
</div>
<div class="search-info" id="search-info" style="display:none">
<span id="search-count" class="search-badge">0</span>
<span>hasil ditemukan</span>
</div>
</div>
</div>
<div id="data-list">
<div class="empty-state"><div class="ei"></div>Memuat data…</div>
</div>
<div class="cta-box">
<button class="btn-draw" id="btn-draw" onclick="startDraw()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
<span id="btn-draw-label">Gambar Jalan di Peta</span>
</button>
</div>
</aside>
<!-- ═══ MAP ═══ -->
<div id="map-wrap">
<div id="draw-banner">
<span class="blink"></span>
<span id="draw-banner-text">Klik peta untuk mulai menggambar</span>
</div>
<div id="map"></div>
</div>
</div>
<!-- ═══ MODAL ═══ -->
<div class="overlay" id="modal-overlay">
<div class="modal">
<div class="modal-header">
<div class="modal-icon" id="modal-icon">✏️</div>
<h3 id="modal-title">Tambah Data</h3>
</div>
<!-- FORM JALAN -->
<div id="form-jalan">
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="j-nama" placeholder="Contoh: Jl. Ahmad Yani"/>
</div>
<div class="form-group">
<label>Status Jalan</label>
<select id="j-status">
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang Jalan (otomatis dari peta)</label>
<input type="text" id="j-panjang" readonly placeholder="Dihitung otomatis…"/>
</div>
</div>
<!-- FORM PARSIL -->
<div id="form-parsil" style="display:none">
<div class="form-group">
<label>Nama Pemilik</label>
<input type="text" id="p-nama" placeholder="Nama pemilik tanah"/>
</div>
<div class="form-group">
<label>Nomor Sertifikat</label>
<input type="text" id="p-nosert" placeholder="Contoh: SHM-PTK-001234"/>
</div>
<div class="form-group">
<label>Status Sertifikat</label>
<select id="p-status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label>Luas Tanah (otomatis dari peta)</label>
<input type="text" id="p-luas" readonly placeholder="Dihitung otomatis…"/>
</div>
</div>
<!-- FORM KERUSAKAN (Point) -->
<div id="form-kerusakan" style="display:none">
<div class="form-group">
<label>Nama / Lokasi Kerusakan</label>
<input type="text" id="k-nama" placeholder="Contoh: Lubang besar Jl. Ahmad Yani KM3"/>
</div>
<div class="form-group">
<label>Tingkat Kerusakan</label>
<select id="k-tingkat">
<option value="Rusak Berat">⛔ Rusak Berat</option>
<option value="Rusak Sedang">⚠️ Rusak Sedang</option>
<option value="Rusak Ringan">🔶 Rusak Ringan</option>
</select>
</div>
<div class="form-group">
<label>Deskripsi Kerusakan</label>
<textarea id="k-deskripsi" placeholder="Deskripsikan kondisi kerusakan secara detail…"></textarea>
</div>
<div class="form-group">
<label>Koordinat (diklik dari peta)</label>
<div class="coords-display" id="k-coords">
<span>Klik titik di peta untuk mendapat koordinat</span>
</div>
<input type="hidden" id="k-lat"/>
<input type="hidden" id="k-lng"/>
</div>
</div>
<div class="form-actions">
<button class="btn-cancel" onclick="closeModal()">Batal</button>
<button class="btn-save" id="btn-save" onclick="saveData()">💾 Simpan</button>
</div>
</div>
</div>
<div id="toast"></div>
<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>
// ══════════════════════════════════════════════════════
// WARNA & CONFIG
// ══════════════════════════════════════════════════════
const COLORS = {
jalan : { Nasional:'#ef4444', Provinsi:'#f59e0b', Kabupaten:'#22c55e' },
parsil : { SHM:'#6366f1', HGB:'#06b6d4', HGU:'#f97316', HP:'#ec4899' },
kerusakan: { 'Rusak Berat':'#ff2d55', 'Rusak Sedang':'#ff9f0a', 'Rusak Ringan':'#ffd60a' }
};
const ICONS = {
'Rusak Berat' : createIcon('#ff2d55', '⛔'),
'Rusak Sedang': createIcon('#ff9f0a', '⚠️'),
'Rusak Ringan': createIcon('#ffd60a', '🔶'),
};
function createIcon(color, emoji) {
return L.divIcon({
className: '',
html: `<div style="
background:${color};
border:2px solid #fff;
border-radius:50%;
width:32px;height:32px;
display:flex;align-items:center;justify-content:center;
font-size:14px;
box-shadow:0 2px 8px rgba(0,0,0,.5);
">${emoji}</div>`,
iconSize: [32,32],
iconAnchor: [16,16],
popupAnchor: [0,-18],
});
}
// ══════════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════════
let activeTab = 'jalan';
let jalanData = [];
let parsilData = [];
let kerusakanData = [];
let layersJalan = {};
let layersParsil = {};
let layersKerusakan= {};
let editId = null;
let pendingGeojson = null;
let pendingLayer = null;
let drawMode = false;
let pointPickMode = false; // mode klik titik untuk kerusakan
// ══════════════════════════════════════════════════════
// MAP INIT Pontianak
// ══════════════════════════════════════════════════════
const map = L.map('map', { center:[-0.028, 109.342], zoom:13, zoomControl:false });
L.control.zoom({ position:'bottomright' }).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:'© OpenStreetMap contributors', maxZoom:19
}).addTo(map);
const drawnItems = new L.FeatureGroup().addTo(map);
// ══════════════════════════════════════════════════════
// TAB SWITCH
// ══════════════════════════════════════════════════════
function switchTab(tab) {
activeTab = tab;
['jalan','parsil','kerusakan','semua'].forEach(t => {
document.getElementById(`tab-${t}`).classList.toggle('active', t === tab);
const leg = document.getElementById(`legend-${t}`);
if (leg) leg.style.display = t === tab ? 'flex' : 'none';
});
const drawBtn = document.getElementById('btn-draw');
const drawLabel= document.getElementById('btn-draw-label');
const ctaBox = document.querySelector('.cta-box');
if (tab === 'jalan') {
drawLabel.textContent = 'Gambar Jalan di Peta';
drawBtn.className = 'btn-draw';
ctaBox.style.display = '';
} else if (tab === 'parsil') {
drawLabel.textContent = 'Gambar Parsil di Peta';
drawBtn.className = 'btn-draw';
ctaBox.style.display = '';
} else if (tab === 'kerusakan') {
drawLabel.textContent = '📍 Tandai Titik Kerusakan Jalan';
drawBtn.className = 'btn-draw kerusakan-mode';
ctaBox.style.display = '';
} else {
// tab semua — sembunyikan tombol draw
ctaBox.style.display = 'none';
}
document.getElementById('search-input').value = '';
document.getElementById('search-info').style.display = 'none';
renderList();
}
// ══════════════════════════════════════════════════════
// FETCH
// ══════════════════════════════════════════════════════
async function fetchAll() {
await Promise.all([fetchJalan(), fetchParsil(), fetchKerusakan()]);
}
async function fetchJalan() {
try {
const r = await fetch('api_jalan.php');
const j = await r.json();
if (j.status === 'success') {
jalanData = j.data;
renderJalanLayers();
if (activeTab === 'jalan' || activeTab === 'semua') renderList();
}
} catch(e) { showToast('Gagal memuat data jalan', 'error'); }
}
async function fetchParsil() {
try {
const r = await fetch('api_parsil.php');
const j = await r.json();
if (j.status === 'success') {
parsilData = j.data;
renderParsilLayers();
if (activeTab === 'parsil' || activeTab === 'semua') renderList();
}
} catch(e) { showToast('Gagal memuat data parsil', 'error'); }
}
async function fetchKerusakan() {
try {
const r = await fetch('api_kerusakan.php');
const j = await r.json();
if (j.status === 'success') {
kerusakanData = j.data;
renderKerusakanLayers();
if (activeTab === 'kerusakan' || activeTab === 'semua') renderList();
}
} catch(e) { showToast('Gagal memuat data kerusakan', 'error'); }
}
// ══════════════════════════════════════════════════════
// RENDER LAYERS JALAN (Polyline)
// ══════════════════════════════════════════════════════
function renderJalanLayers() {
Object.values(layersJalan).forEach(l => map.removeLayer(l));
layersJalan = {};
jalanData.forEach(d => {
const color = COLORS.jalan[d.status_jalan] || '#fff';
const latlngs= d.geojson.coordinates.map(c => [c[1],c[0]]);
const poly = L.polyline(latlngs, { color, weight:5, opacity:.85 })
.addTo(map).bindPopup(popupJalan(d));
poly.on('click', () => highlightCard(d.id, 'jalan'));
layersJalan[d.id] = poly;
});
}
function popupJalan(d) {
return `
<div class="popup-name">🛣️ ${esc(d.nama_jalan)}</div>
<div class="popup-meta">Status: <b>${esc(d.status_jalan)}</b></div>
<div class="popup-meta">Panjang: <b>${formatNum(d.panjang_meter)} m</b></div>
<div class="popup-actions">
<button onclick="openEdit('jalan',${d.id})" style="${popBtn('#263040','#dde6f0')}">✏️ Edit</button>
<button onclick="deleteData('jalan',${d.id})" style="${popBtn('#3b0f0e','#ef4444')}">🗑️ Hapus</button>
</div>`;
}
// ══════════════════════════════════════════════════════
// RENDER LAYERS PARSIL (Polygon)
// ══════════════════════════════════════════════════════
function renderParsilLayers() {
Object.values(layersParsil).forEach(l => map.removeLayer(l));
layersParsil = {};
parsilData.forEach(d => {
const color = COLORS.parsil[d.status_sertifikat] || '#fff';
const latlngs= d.geojson.coordinates[0].map(c => [c[1],c[0]]);
const poly = L.polygon(latlngs, { color, fillColor:color, fillOpacity:.25, weight:2.5 })
.addTo(map).bindPopup(popupParsil(d));
poly.on('click', () => highlightCard(d.id, 'parsil'));
layersParsil[d.id] = poly;
});
}
function popupParsil(d) {
return `
<div class="popup-name">🏡 ${esc(d.nama_pemilik)}</div>
<div class="popup-meta">No. Sertifikat: <b>${esc(d.no_sertifikat)}</b></div>
<div class="popup-meta">Status: <b>${esc(d.status_sertifikat)}</b></div>
<div class="popup-meta">Luas: <b>${formatNum(d.luas_meter2)} m²</b></div>
<div class="popup-actions">
<button onclick="openEdit('parsil',${d.id})" style="${popBtn('#263040','#dde6f0')}">✏️ Edit</button>
<button onclick="deleteData('parsil',${d.id})" style="${popBtn('#3b0f0e','#ef4444')}">🗑️ Hapus</button>
</div>`;
}
// ══════════════════════════════════════════════════════
// RENDER LAYERS KERUSAKAN (Point Marker)
// ══════════════════════════════════════════════════════
function renderKerusakanLayers() {
Object.values(layersKerusakan).forEach(l => map.removeLayer(l));
layersKerusakan = {};
kerusakanData.forEach(d => {
const icon = ICONS[d.tingkat_kerusakan] || ICONS['Rusak Sedang'];
const marker = L.marker([d.latitude, d.longitude], { icon })
.addTo(map).bindPopup(popupKerusakan(d));
marker.on('click', () => highlightCard(d.id, 'kerusakan'));
layersKerusakan[d.id] = marker;
});
}
function popupKerusakan(d) {
const colorMap = {
'Rusak Berat':'#ff2d55','Rusak Sedang':'#ff9f0a','Rusak Ringan':'#ffd60a'
};
const c = colorMap[d.tingkat_kerusakan] || '#fff';
return `
<div class="popup-name">⚠️ ${esc(d.nama_kerusakan)}</div>
<div class="popup-meta">Tingkat: <b style="color:${c}">${esc(d.tingkat_kerusakan)}</b></div>
${d.deskripsi ? `<div class="popup-desc">${esc(d.deskripsi)}</div>` : ''}
<div class="popup-meta" style="font-family:monospace;font-size:10px">
📍 ${parseFloat(d.latitude).toFixed(6)}, ${parseFloat(d.longitude).toFixed(6)}
</div>
<div class="popup-actions">
<button onclick="openEdit('kerusakan',${d.id})" style="${popBtn('#263040','#dde6f0')}">✏️ Edit</button>
<button onclick="deleteData('kerusakan',${d.id})" style="${popBtn('#3b0f0e','#ef4444')}">🗑️ Hapus</button>
</div>`;
}
function popBtn(bg, color) {
return `flex:1;padding:5px 0;font-size:11px;font-weight:600;border-radius:6px;cursor:pointer;border:1px solid ${color};background:${bg};color:${color};font-family:inherit`;
}
// ══════════════════════════════════════════════════════
// RENDER LIST (SIDEBAR)
// ══════════════════════════════════════════════════════
function renderList(filter = '') {
const list = document.getElementById('data-list');
const q = filter.toLowerCase().trim();
if (activeTab === 'semua') {
renderAllSearch(q, list);
return;
}
let items = [];
let html = '';
if (activeTab === 'jalan') {
items = jalanData.filter(d =>
d.nama_jalan.toLowerCase().includes(q) || d.status_jalan.toLowerCase().includes(q)
);
if (!items.length) { list.innerHTML = emptyState(filter); updateSearchInfo(0, q); return; }
html = items.map(d => cardJalan(d)).join('');
} else if (activeTab === 'parsil') {
items = parsilData.filter(d =>
d.nama_pemilik.toLowerCase().includes(q) ||
d.no_sertifikat.toLowerCase().includes(q) ||
d.status_sertifikat.toLowerCase().includes(q)
);
if (!items.length) { list.innerHTML = emptyState(filter); updateSearchInfo(0, q); return; }
html = items.map(d => cardParsil(d)).join('');
} else if (activeTab === 'kerusakan') {
items = kerusakanData.filter(d =>
d.nama_kerusakan.toLowerCase().includes(q) ||
d.tingkat_kerusakan.toLowerCase().includes(q) ||
(d.deskripsi || '').toLowerCase().includes(q)
);
if (!items.length) { list.innerHTML = emptyState(filter); updateSearchInfo(0, q); return; }
html = items.map(d => cardKerusakan(d)).join('');
}
list.innerHTML = html;
updateSearchInfo(items.length, q);
}
function renderAllSearch(q, list) {
const filteredJalan = jalanData.filter(d =>
d.nama_jalan.toLowerCase().includes(q) || d.status_jalan.toLowerCase().includes(q)
);
const filteredParsil = parsilData.filter(d =>
d.nama_pemilik.toLowerCase().includes(q) ||
d.no_sertifikat.toLowerCase().includes(q) ||
d.status_sertifikat.toLowerCase().includes(q)
);
const filteredKerusakan = kerusakanData.filter(d =>
d.nama_kerusakan.toLowerCase().includes(q) ||
d.tingkat_kerusakan.toLowerCase().includes(q) ||
(d.deskripsi || '').toLowerCase().includes(q)
);
const total = filteredJalan.length + filteredParsil.length + filteredKerusakan.length;
if (!total) { list.innerHTML = emptyState(q); updateSearchInfo(0, q); return; }
let html = '';
if (filteredJalan.length) {
html += `<div class="list-section-header">🛣️ Jalan (${filteredJalan.length})</div>`;
html += filteredJalan.map(d => cardJalan(d, true)).join('');
}
if (filteredParsil.length) {
html += `<div class="list-section-header">🏡 Parsil Tanah (${filteredParsil.length})</div>`;
html += filteredParsil.map(d => cardParsil(d, true)).join('');
}
if (filteredKerusakan.length) {
html += `<div class="list-section-header">⚠️ Kerusakan Jalan (${filteredKerusakan.length})</div>`;
html += filteredKerusakan.map(d => cardKerusakan(d, true)).join('');
}
list.innerHTML = html;
updateSearchInfo(total, q);
}
// ─ Card builders ────────────────────────────────────
function cardJalan(d, showType = false) {
return `
<div class="data-card" data-type="jalan" id="card-jalan-${d.id}" onclick="zoomTo('jalan',${d.id})">
${showType ? '<div class="card-type-badge">🛣️ Jalan</div>' : ''}
<div class="card-top">
<div class="card-name">${esc(d.nama_jalan)}</div>
<span class="status-tag tag-${d.status_jalan.toLowerCase()}">${d.status_jalan}</span>
</div>
<div class="card-meta">📏 ${formatNum(d.panjang_meter)} m</div>
<div class="card-actions">
<button class="btn-sm" onclick="event.stopPropagation();openEdit('jalan',${d.id})">✏️ Edit</button>
<button class="btn-sm danger" onclick="event.stopPropagation();deleteData('jalan',${d.id})">🗑️ Hapus</button>
</div>
</div>`;
}
function cardParsil(d, showType = false) {
return `
<div class="data-card" data-type="parsil" id="card-parsil-${d.id}" onclick="zoomTo('parsil',${d.id})">
${showType ? '<div class="card-type-badge">🏡 Parsil Tanah</div>' : ''}
<div class="card-top">
<div class="card-name">${esc(d.nama_pemilik)}</div>
<span class="status-tag tag-${d.status_sertifikat.toLowerCase()}">${d.status_sertifikat}</span>
</div>
<div class="card-meta">📄 ${esc(d.no_sertifikat)} | 📐 ${formatNum(d.luas_meter2)} m²</div>
<div class="card-actions">
<button class="btn-sm" onclick="event.stopPropagation();openEdit('parsil',${d.id})">✏️ Edit</button>
<button class="btn-sm danger" onclick="event.stopPropagation();deleteData('parsil',${d.id})">🗑️ Hapus</button>
</div>
</div>`;
}
function cardKerusakan(d, showType = false) {
const tagClass = d.tingkat_kerusakan === 'Rusak Berat'
? 'rusak-berat' : d.tingkat_kerusakan === 'Rusak Sedang'
? 'rusak-sedang' : 'rusak-ringan';
return `
<div class="data-card" data-type="kerusakan" id="card-kerusakan-${d.id}" onclick="zoomTo('kerusakan',${d.id})">
${showType ? '<div class="card-type-badge">⚠️ Kerusakan</div>' : ''}
<div class="card-top">
<div class="card-name">${esc(d.nama_kerusakan)}</div>
<span class="status-tag tag-${tagClass}">${d.tingkat_kerusakan}</span>
</div>
<div class="card-meta">📍 ${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</div>
${d.deskripsi ? `<div class="card-meta" style="font-family:inherit;color:#8fa3b8">${esc(d.deskripsi.slice(0,60))}${d.deskripsi.length>60?'…':''}</div>` : ''}
<div class="card-actions">
<button class="btn-sm" onclick="event.stopPropagation();openEdit('kerusakan',${d.id})">✏️ Edit</button>
<button class="btn-sm danger" onclick="event.stopPropagation();deleteData('kerusakan',${d.id})">🗑️ Hapus</button>
</div>
</div>`;
}
function emptyState(filter) {
return `<div class="empty-state"><div class="ei">${filter ? '🔍' : '📭'}</div>${filter ? 'Tidak ditemukan hasil pencarian' : 'Belum ada data'}</div>`;
}
function updateSearchInfo(count, q) {
const info = document.getElementById('search-info');
if (q) {
info.style.display = 'flex';
document.getElementById('search-count').textContent = count;
} else {
info.style.display = 'none';
}
}
// ══════════════════════════════════════════════════════
// ZOOM TO FEATURE
// ══════════════════════════════════════════════════════
function zoomTo(type, id) {
let layer;
if (type === 'jalan') layer = layersJalan[id];
if (type === 'parsil') layer = layersParsil[id];
if (type === 'kerusakan') layer = layersKerusakan[id];
if (!layer) return;
if (type === 'kerusakan') {
map.setView(layer.getLatLng(), 17);
} else {
map.fitBounds(layer.getBounds(), { padding:[40,40] });
}
setTimeout(() => layer.openPopup(), 500);
highlightCard(id, type);
}
function highlightCard(id, type) {
document.querySelectorAll('.data-card').forEach(c => c.classList.remove('active'));
const card = document.getElementById(`card-${type}-${id}`);
if (card) { card.classList.add('active'); card.scrollIntoView({ behavior:'smooth', block:'nearest' }); }
}
// ══════════════════════════════════════════════════════
// DRAW MODE
// ══════════════════════════════════════════════════════
let drawHandler = null;
function startDraw() {
if (pendingLayer) { drawnItems.removeLayer(pendingLayer); pendingLayer = null; }
pendingGeojson = null;
editId = null;
const banner = document.getElementById('draw-banner');
if (activeTab === 'kerusakan') {
// Mode klik titik untuk kerusakan
pointPickMode = true;
banner.classList.add('show', 'danger-mode');
document.getElementById('draw-banner-text').textContent =
'Klik lokasi kerusakan jalan di peta untuk menandai titik';
map.getContainer().style.cursor = 'crosshair';
return;
}
drawMode = true;
banner.classList.remove('danger-mode');
banner.classList.add('show');
if (activeTab === 'jalan') {
document.getElementById('draw-banner-text').textContent =
'Klik peta untuk titik jalan, klik ganda untuk selesai';
drawHandler = new L.Draw.Polyline(map, { shapeOptions: { color:'#38bdf8', weight:4 } });
} else {
document.getElementById('draw-banner-text').textContent =
'Klik peta untuk titik parsil, klik titik awal untuk menutup polygon';
drawHandler = new L.Draw.Polygon(map, { shapeOptions: { color:'#38bdf8', fillOpacity:.2 } });
}
drawHandler.enable();
}
// Klik peta hanya aktif saat mode titik kerusakan
map.on('click', function(e) {
if (!pointPickMode) return;
const lat = e.latlng.lat;
const lng = e.latlng.lng;
// Hapus marker temp sebelumnya
if (pendingLayer) { map.removeLayer(pendingLayer); pendingLayer = null; }
pendingLayer = L.marker([lat, lng], { icon: ICONS['Rusak Berat'] }).addTo(map);
map.getContainer().style.cursor = '';
document.getElementById('draw-banner').classList.remove('show', 'danger-mode');
pointPickMode = false;
openModal('kerusakan', null, null, lat, lng);
});
// Polyline/Polygon selesai digambar
map.on(L.Draw.Event.CREATED, function(e) {
document.getElementById('draw-banner').classList.remove('show');
drawMode = false;
const layer = e.layer;
drawnItems.addLayer(layer);
pendingLayer = layer;
if (activeTab === 'jalan') {
const latlngs = layer.getLatLngs();
let total = 0;
for (let i = 0; i < latlngs.length - 1; i++) total += latlngs[i].distanceTo(latlngs[i+1]);
const coords = latlngs.map(ll => [ll.lng, ll.lat]);
pendingGeojson = { type:'LineString', coordinates:coords };
openModal('jalan', null, total);
} else {
const latlngs = layer.getLatLngs()[0];
const luasM2 = L.GeometryUtil.geodesicArea(latlngs);
const coords = latlngs.map(ll => [ll.lng, ll.lat]);
coords.push(coords[0]);
pendingGeojson = { type:'Polygon', coordinates:[coords] };
openModal('parsil', null, luasM2);
}
});
// ESC membatalkan draw
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
if (drawMode && drawHandler) { drawHandler.disable(); }
if (pointPickMode) { map.getContainer().style.cursor = ''; pointPickMode = false; }
document.getElementById('draw-banner').classList.remove('show','danger-mode');
drawMode = false;
}
});
// ══════════════════════════════════════════════════════
// MODAL
// ══════════════════════════════════════════════════════
function openModal(type, id = null, measurement = null, lat = null, lng = null) {
editId = id;
const isEdit = id !== null;
const isKerusakan= type === 'kerusakan';
document.getElementById('modal-title').textContent =
(isEdit ? 'Edit ' : 'Tambah ') +
(type === 'jalan' ? 'Data Jalan' : type === 'parsil' ? 'Data Parsil Tanah' : 'Titik Kerusakan Jalan');
const icon = document.getElementById('modal-icon');
icon.textContent = type === 'jalan' ? '🛣️' : type === 'parsil' ? '🏡' : '⚠️';
icon.className = isKerusakan ? 'modal-icon danger' : 'modal-icon';
const btnSave = document.getElementById('btn-save');
btnSave.className = isKerusakan ? 'btn-save danger' : 'btn-save';
['jalan','parsil','kerusakan'].forEach(t => {
document.getElementById(`form-${t}`).style.display = t === type ? 'block' : 'none';
});
if (isEdit) {
if (type === 'jalan') {
const d = jalanData.find(x => x.id == id);
document.getElementById('j-nama').value = d.nama_jalan;
document.getElementById('j-status').value = d.status_jalan;
document.getElementById('j-panjang').value = formatNum(d.panjang_meter) + ' m';
if (!pendingGeojson) pendingGeojson = d.geojson;
} else if (type === 'parsil') {
const d = parsilData.find(x => x.id == id);
document.getElementById('p-nama').value = d.nama_pemilik;
document.getElementById('p-nosert').value = d.no_sertifikat;
document.getElementById('p-status').value = d.status_sertifikat;
document.getElementById('p-luas').value = formatNum(d.luas_meter2) + ' m²';
if (!pendingGeojson) pendingGeojson = d.geojson;
} else if (type === 'kerusakan') {
const d = kerusakanData.find(x => x.id == id);
document.getElementById('k-nama').value = d.nama_kerusakan;
document.getElementById('k-tingkat').value = d.tingkat_kerusakan;
document.getElementById('k-deskripsi').value= d.deskripsi || '';
document.getElementById('k-lat').value = d.latitude;
document.getElementById('k-lng').value = d.longitude;
document.getElementById('k-coords').innerHTML =
`<span>Koordinat saat ini:</span><br>Lat: ${parseFloat(d.latitude).toFixed(6)} | Lng: ${parseFloat(d.longitude).toFixed(6)}`;
}
} else {
// Tambah baru
if (type === 'jalan') {
document.getElementById('j-nama').value = '';
document.getElementById('j-status').value = 'Nasional';
document.getElementById('j-panjang').value = measurement ? formatNum(measurement) + ' m' : '';
} else if (type === 'parsil') {
document.getElementById('p-nama').value = '';
document.getElementById('p-nosert').value = '';
document.getElementById('p-status').value = 'SHM';
document.getElementById('p-luas').value = measurement ? formatNum(measurement) + ' m²' : '';
} else if (type === 'kerusakan') {
document.getElementById('k-nama').value = '';
document.getElementById('k-tingkat').value = 'Rusak Berat';
document.getElementById('k-deskripsi').value = '';
document.getElementById('k-lat').value = lat || '';
document.getElementById('k-lng').value = lng || '';
document.getElementById('k-coords').innerHTML= lat
? `<span>Titik dipilih:</span><br>Lat: ${lat.toFixed(6)} | Lng: ${lng.toFixed(6)}`
: '<span>Klik titik di peta untuk mendapat koordinat</span>';
}
}
document.getElementById('modal-overlay').classList.add('open');
document.getElementById('modal-overlay').dataset.type = type;
}
function closeModal() {
document.getElementById('modal-overlay').classList.remove('open');
if (!editId && pendingLayer) {
// Hapus marker/layer sementara jika batal tambah kerusakan
if (pendingLayer._latlng) map.removeLayer(pendingLayer);
else drawnItems.removeLayer(pendingLayer);
pendingLayer = null;
}
pendingGeojson = null;
editId = null;
}
document.getElementById('modal-overlay').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
// ══════════════════════════════════════════════════════
// SAVE DATA
// ══════════════════════════════════════════════════════
async function saveData() {
const type = document.getElementById('modal-overlay').dataset.type;
const isEdit = editId !== null;
let payload, url, method;
if (type === 'jalan') {
const nama = document.getElementById('j-nama').value.trim();
const status = document.getElementById('j-status').value;
const panjang= parseFloat(document.getElementById('j-panjang').value.replace(/[^\d.]/g,''));
if (!nama) { showToast('Nama jalan wajib diisi!','error'); return; }
if (!pendingGeojson) { showToast('Gambar rute jalan di peta terlebih dahulu!','error'); return; }
payload = { nama_jalan:nama, status_jalan:status, panjang_meter:panjang, geojson:pendingGeojson };
url = isEdit ? `api_jalan.php?id=${editId}` : 'api_jalan.php';
method = isEdit ? 'PUT' : 'POST';
} else if (type === 'parsil') {
const nama = document.getElementById('p-nama').value.trim();
const nosert = document.getElementById('p-nosert').value.trim();
const status = document.getElementById('p-status').value;
const luas = parseFloat(document.getElementById('p-luas').value.replace(/[^\d.]/g,''));
if (!nama || !nosert) { showToast('Nama pemilik & no. sertifikat wajib diisi!','error'); return; }
if (!pendingGeojson) { showToast('Gambar parsil di peta terlebih dahulu!','error'); return; }
payload = { nama_pemilik:nama, no_sertifikat:nosert, status_sertifikat:status, luas_meter2:luas, geojson:pendingGeojson };
url = isEdit ? `api_parsil.php?id=${editId}` : 'api_parsil.php';
method = isEdit ? 'PUT' : 'POST';
} else if (type === 'kerusakan') {
const nama = document.getElementById('k-nama').value.trim();
const tingkat = document.getElementById('k-tingkat').value;
const desk = document.getElementById('k-deskripsi').value.trim();
const lat = parseFloat(document.getElementById('k-lat').value);
const lng = parseFloat(document.getElementById('k-lng').value);
if (!nama) { showToast('Nama/lokasi kerusakan wajib diisi!','error'); return; }
if (isNaN(lat) || isNaN(lng)) { showToast('Klik titik di peta terlebih dahulu!','error'); return; }
payload = { nama_kerusakan:nama, tingkat_kerusakan:tingkat, deskripsi:desk, latitude:lat, longitude:lng };
url = isEdit ? `api_kerusakan.php?id=${editId}` : 'api_kerusakan.php';
method = isEdit ? 'PUT' : 'POST';
}
try {
const r = await fetch(url, {
method,
headers: { 'Content-Type':'application/json' },
body : JSON.stringify(payload)
});
const j = await r.json();
if (j.status === 'success') {
showToast(isEdit ? 'Data berhasil diperbarui! ✏️' : 'Data berhasil disimpan! ✅', 'success');
if (pendingLayer) {
if (pendingLayer._latlng) map.removeLayer(pendingLayer);
else drawnItems.removeLayer(pendingLayer);
pendingLayer = null;
}
closeModal();
if (type === 'jalan') await fetchJalan();
else if (type === 'parsil') await fetchParsil();
else await fetchKerusakan();
renderList(document.getElementById('search-input').value);
} else {
showToast('Gagal: ' + j.message, 'error');
}
} catch(e) { showToast('Error koneksi ke server', 'error'); }
}
// ══════════════════════════════════════════════════════
// EDIT
// ══════════════════════════════════════════════════════
function openEdit(type, id) {
id = parseInt(id);
const layers = { jalan:layersJalan, parsil:layersParsil, kerusakan:layersKerusakan };
const layer = layers[type]?.[id];
if (layer) layer.closePopup();
pendingGeojson = null;
openModal(type, id);
}
// ══════════════════════════════════════════════════════
// DELETE
// ══════════════════════════════════════════════════════
async function deleteData(type, id) {
const label = type === 'jalan' ? 'data jalan' : type === 'parsil' ? 'data parsil' : 'titik kerusakan';
if (!confirm(`Yakin ingin menghapus ${label} ini?`)) return;
const urls = {
jalan : `api_jalan.php?id=${id}`,
parsil : `api_parsil.php?id=${id}`,
kerusakan: `api_kerusakan.php?id=${id}`
};
try {
const r = await fetch(urls[type], { method:'DELETE' });
const j = await r.json();
if (j.status === 'success') {
showToast('Data berhasil dihapus! 🗑️','success');
if (type === 'jalan') await fetchJalan();
else if (type === 'parsil') await fetchParsil();
else await fetchKerusakan();
renderList(document.getElementById('search-input').value);
}
} catch(e) { showToast('Gagal menghapus','error'); }
}
// ══════════════════════════════════════════════════════
// SEARCH
// ══════════════════════════════════════════════════════
function filterList() {
renderList(document.getElementById('search-input').value);
}
function clearSearch() {
document.getElementById('search-input').value = '';
document.getElementById('search-info').style.display = 'none';
renderList();
}
// ══════════════════════════════════════════════════════
// TOAST
// ══════════════════════════════════════════════════════
let toastTimer;
function showToast(msg, type = 'success') {
const t = document.getElementById('toast');
t.textContent = msg;
t.className = `show ${type}`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => { t.className = ''; }, 3200);
}
// ══════════════════════════════════════════════════════
// UTILS
// ══════════════════════════════════════════════════════
function esc(str) {
return String(str)
.replace(/&/g,'&amp;').replace(/</g,'&lt;')
.replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
function formatNum(n) {
return Number(n).toLocaleString('id-ID', { maximumFractionDigits:2 });
}
// Globals untuk popup Leaflet
window.openEdit = openEdit;
window.deleteData = deleteData;
// ══════════════════════════════════════════════════════
// INIT
// ══════════════════════════════════════════════════════
fetchAll();
</script>
</body>
</html>