Files
2026-06-10 21:46:56 +07:00

1466 lines
75 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>WebGIS SIG Jalan & Parsil Tanah Pontianak</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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
.navbar { background: linear-gradient(135deg,#1e293b,#0f172a); border-bottom: 2px solid #334155; padding: 0 20px; height: 56px; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; z-index: 1000; box-shadow: 0 2px 20px rgba(0,0,0,.4); }
.navbar-brand { display: flex; align-items: center; gap: 10px; font-size: 1.1rem; font-weight: 700; color: #38bdf8; }
.navbar-brand i { font-size: 1.3rem; }
.navbar-sub { font-size: .72rem; color: #94a3b8; font-weight: 400; }
.navbar-tabs { display: flex; gap: 4px; }
.tab-btn { padding: 7px 14px; border: none; border-radius: 8px; cursor: pointer; font-size: .78rem; font-weight: 600; transition: all .2s; display: flex; align-items: center; gap: 6px; }
.tab-btn.jalan { background: #1e3a5f; color: #60a5fa; }
.tab-btn.tanah { background: #1a3a2a; color: #4ade80; }
.tab-btn.kerusakan{ background: #3a1a1a; color: #f87171; }
.tab-btn.jalan.active { background: #2563eb; color: #fff; }
.tab-btn.tanah.active { background: #16a34a; color: #fff; }
.tab-btn.kerusakan.active { background: #dc2626; color: #fff; }
.tab-btn:hover { filter: brightness(1.2); }
.main { display: flex; flex: 1; overflow: hidden; }
.sidebar { width: 340px; flex-shrink: 0; background: #1e293b; border-right: 1px solid #334155; display: flex; flex-direction: column; overflow: hidden; }
.sidebar-header { padding: 14px 16px 10px; border-bottom: 1px solid #334155; background: #0f172a; }
.sidebar-header h2 { font-size: .9rem; font-weight: 700; margin-bottom: 4px; }
.sidebar-header .hint { font-size: .72rem; color: #64748b; }
.form-wrap { padding: 14px 16px; border-bottom: 1px solid #334155; }
.form-group { margin-bottom: 10px; }
.form-group label { display: block; font-size: .75rem; font-weight: 600; color: #94a3b8; margin-bottom: 4px; text-transform: uppercase; letter-spacing: .5px; }
.form-control { width: 100%; padding: 8px 10px; border-radius: 7px; border: 1px solid #334155; background: #0f172a; color: #e2e8f0; font-size: .83rem; transition: border .2s; }
.form-control:focus { outline: none; border-color: #38bdf8; }
.form-control[readonly] { color: #94a3b8; cursor: default; }
textarea.form-control { resize: vertical; min-height: 60px; }
select.form-control { cursor: pointer; }
.btn { padding: 8px 14px; border: none; border-radius: 7px; cursor: pointer; font-size: .82rem; font-weight: 600; display: inline-flex; align-items: center; gap: 6px; transition: all .2s; }
.btn-primary { background: #2563eb; color: #fff; }
.btn-success { background: #16a34a; color: #fff; }
.btn-warning { background: #d97706; color: #fff; }
.btn-danger { background: #dc2626; color: #fff; }
.btn-secondary { background: #334155; color: #94a3b8; }
.btn-info { background: #0891b2; color: #fff; }
.btn-purple { background: #7c3aed; color: #fff; }
.btn:hover { filter: brightness(1.15); transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn-block { width: 100%; justify-content: center; margin-top: 4px; }
.draw-info { padding: 8px 10px; border-radius: 7px; background: #0f172a; border: 1px dashed #334155; font-size: .75rem; color: #64748b; margin-bottom: 8px; text-align: center; }
.draw-info.active { border-color: #38bdf8; color: #38bdf8; background: #0c1929; }
.draw-info.editing { border-color: #f59e0b; color: #f59e0b; background: #1c1206; }
/* Search bar */
.search-bar { padding: 10px 12px; border-bottom: 1px solid #334155; background: #0f172a; }
.search-row { display: flex; gap: 6px; }
.search-row .form-control { flex: 1; }
.filter-row { display: flex; gap: 6px; margin-top: 8px; align-items: center; flex-wrap: wrap; }
.filter-row select, .filter-row input { flex: 1; min-width: 80px; }
.filter-row .filter-label { font-size: .72rem; color: #64748b; white-space: nowrap; }
.list-header { padding: 10px 16px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid #334155; }
.list-header span { font-size: .8rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; }
.badge { background: #334155; color: #94a3b8; font-size: .68rem; padding: 2px 8px; border-radius: 12px; font-weight: 700; }
.list-wrap { flex: 1; min-height: 120px; padding: 8px 10px; }
.list-wrap::-webkit-scrollbar { width: 4px; }
.list-wrap::-webkit-scrollbar-track { background: #0f172a; }
.list-wrap::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
.list-item { background: #0f172a; border: 1px solid #1e293b; border-radius: 8px; margin-bottom: 8px; padding: 10px 12px; transition: all .2s; }
.list-item:hover { border-color: #38bdf8; background: #0c1929; }
.list-item .item-name { font-size: .85rem; font-weight: 700; margin-bottom: 3px; }
.list-item .item-meta { font-size: .72rem; color: #64748b; }
.list-item .item-actions { display: flex; gap: 5px; margin-top: 8px; flex-wrap: wrap; }
.item-actions .btn { padding: 4px 10px; font-size: .72rem; }
.status-badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: .68rem; font-weight: 700; margin-left: 4px; }
.s-nasional { background: #450a0a; color: #f87171; }
.s-provinsi { background: #172554; color: #60a5fa; }
.s-kabupaten { background: #052e16; color: #4ade80; }
.s-shm { background: #451a03; color: #fb923c; }
.s-hgb { background: #3b0764; color: #c084fc; }
.s-hgu { background: #0c4a6e; color: #38bdf8; }
.s-hp { background: #422006; color: #fbbf24; }
.s-rendah { background: #052e16; color: #4ade80; }
.s-sedang { background: #422006; color: #fbbf24; }
.s-tinggi { background: #450a0a; color: #f87171; }
.s-kritis { background: #3b0764; color: #c084fc; }
.urgency-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 5px; }
.urgensi-rendah { background:#4ade80; }
.urgensi-sedang { background:#fbbf24; }
.urgensi-tinggi { background:#f87171; }
.urgensi-kritis { background:#c084fc; animation: pulse 1s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.4} }
.cluster-badge { background: #dc2626; color: #fff; font-size: .65rem; padding: 1px 6px; border-radius: 8px; margin-left: 4px; font-weight: 700; }
#map { flex: 1; }
.toast-container { position: fixed; top: 70px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
.toast { padding: 10px 18px; border-radius: 8px; font-size: .82rem; font-weight: 600; box-shadow: 0 4px 20px rgba(0,0,0,.4); animation: slideIn .3s ease; max-width: 300px; }
.toast.success { background: #16a34a; color: #fff; }
.toast.error { background: #dc2626; color: #fff; }
.toast.info { background: #2563eb; color: #fff; }
.toast.warning { background: #d97706; color: #fff; }
@keyframes slideIn { from{opacity:0;transform:translateX(40px)} to{opacity:1;transform:translateX(0)} }
.map-legend { background: rgba(15,23,42,.92); border: 1px solid #334155; border-radius: 8px; padding: 10px 14px; font-size: .75rem; }
.legend-title { font-weight: 700; margin-bottom: 6px; color: #94a3b8; text-transform: uppercase; letter-spacing: .5px; }
.legend-item { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.legend-color { width: 24px; height: 4px; border-radius: 2px; }
.legend-poly { width: 14px; height: 14px; border-radius: 3px; opacity: .8; }
.tab-content { display: none; flex-direction: column; height: 100%; overflow: hidden; }
.tab-content.active { display: flex; overflow-y: auto; }
.tab-content.active > * { flex-shrink: 0; }
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 5000; align-items: center; justify-content: center; }
.modal-overlay.show { display: flex; }
.modal-box { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; width: 420px; max-height: 90vh; overflow-y: auto; }
.modal-title { font-size: 1rem; font-weight: 700; margin-bottom: 16px; color: #38bdf8; }
.modal-actions { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
.edit-toolbar { display: none; position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%); z-index: 2000; background: #1e293b; border: 2px solid #f59e0b; border-radius: 12px; padding: 12px 20px; gap: 12px; align-items: center; box-shadow: 0 4px 30px rgba(245,158,11,.3); }
.edit-toolbar.show { display: flex; }
.edit-toolbar-label { font-size: .82rem; color: #f59e0b; font-weight: 600; }
/* Histori timeline */
.histori-list { margin-top: 8px; }
.histori-item { display: flex; gap: 10px; margin-bottom: 10px; }
.histori-dot { width: 12px; height: 12px; border-radius: 50%; background: #38bdf8; flex-shrink: 0; margin-top: 3px; }
.histori-content { font-size: .78rem; }
.histori-content .h-status { font-weight: 700; color: #e2e8f0; }
.histori-content .h-time { color: #64748b; font-size: .7rem; }
.histori-content .h-catatan { color: #94a3b8; margin-top: 2px; }
/* foto preview */
.foto-preview { width: 100%; border-radius: 8px; margin-top: 6px; max-height: 160px; object-fit: cover; display: none; }
.foto-preview.show { display: block; }
/* search highlight */
.search-highlight { background: #1e3a5f !important; border-color: #38bdf8 !important; }
/* data viz panel */
.viz-panel { padding: 10px 12px; border-bottom: 1px solid #334155; }
.viz-bar-wrap { margin-bottom: 6px; }
.viz-bar-label { font-size: .72rem; color: #94a3b8; display: flex; justify-content: space-between; margin-bottom: 3px; }
.viz-bar-bg { background: #0f172a; border-radius: 4px; height: 8px; overflow: hidden; }
.viz-bar-fill { height: 8px; border-radius: 4px; transition: width .6s ease; }
.viz-title { font-size: .78rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px; }
</style>
</head>
<body>
<nav class="navbar">
<div class="navbar-brand">
<i class="fa-solid fa-map-location-dot"></i>
<div>WebGIS Pontianak<div class="navbar-sub">Manajemen Data Jalan, Parsil Tanah & Kerusakan</div></div>
</div>
<div class="navbar-tabs">
<button class="tab-btn jalan active" onclick="switchTab('jalan')"><i class="fa-solid fa-road"></i> Jalan</button>
<button class="tab-btn tanah" onclick="switchTab('tanah')"><i class="fa-solid fa-land-mine-on"></i> Tanah</button>
<button class="tab-btn kerusakan" onclick="switchTab('kerusakan')"><i class="fa-solid fa-triangle-exclamation"></i> Kerusakan</button>
</div>
</nav>
<div class="main">
<div class="sidebar">
<!-- ═══════════ TAB JALAN ═══════════ -->
<div id="tab-jalan" class="tab-content active">
<div class="sidebar-header">
<h2><i class="fa-solid fa-road" style="color:#60a5fa"></i> Tambah Data Jalan</h2>
<div class="hint">Klik shape popup muncul pilih aksi</div>
</div>
<!-- Search & Filter Jalan -->
<div class="search-bar">
<div class="search-row">
<input type="text" id="search-jalan" class="form-control" placeholder="🔍 Cari nama jalan..." oninput="filterJalan()"/>
<button class="btn btn-primary" onclick="searchZoomJalan()" title="Zoom ke hasil"><i class="fa-solid fa-location-dot"></i></button>
</div>
<div class="filter-row">
<span class="filter-label">Panjang:</span>
<select id="filter-jalan-op" class="form-control" onchange="filterJalan()">
<option value="">Semua</option>
<option value="gt">Lebih dari</option>
<option value="lt">Kurang dari</option>
</select>
<input type="number" id="filter-jalan-val" class="form-control" placeholder="meter" oninput="filterJalan()"/>
</div>
</div>
<div class="form-wrap">
<div id="draw-info-jalan" class="draw-info"><i class="fa-solid fa-draw-polygon"></i> Belum ada garis digambar</div>
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="j-nama" class="form-control" placeholder="Contoh: Jl. Ahmad Yani"/>
</div>
<div class="form-group">
<label>Status Jalan</label>
<select id="j-status" class="form-control">
<option value="">-- Pilih Status --</option>
<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 (meter)</label>
<input type="text" id="j-panjang" class="form-control" readonly placeholder="Otomatis dihitung Leaflet"/>
</div>
<button class="btn btn-primary btn-block" onclick="startDrawJalan()"><i class="fa-solid fa-pen-to-square"></i> Gambar Garis di Peta</button>
<button class="btn btn-success btn-block" onclick="simpanJalan()"><i class="fa-solid fa-floppy-disk"></i> Simpan Data Jalan</button>
<button class="btn btn-secondary btn-block" onclick="cancelDraw()"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
<!-- Data Viz Jalan -->
<div class="viz-panel" id="viz-jalan">
<div class="viz-title"><i class="fa-solid fa-chart-bar"></i> Distribusi Status Jalan</div>
<div id="viz-jalan-bars"></div>
</div>
<div class="list-header">
<span><i class="fa-solid fa-list"></i> Daftar Jalan</span>
<span class="badge" id="count-jalan">0</span>
</div>
<div class="list-wrap" id="list-jalan"></div>
</div>
<!-- ═══════════ TAB TANAH ═══════════ -->
<div id="tab-tanah" class="tab-content">
<div class="sidebar-header">
<h2><i class="fa-solid fa-land-mine-on" style="color:#4ade80"></i> Tambah Parsil Tanah</h2>
<div class="hint">Klik shape popup muncul pilih aksi</div>
</div>
<!-- Search & Filter Tanah -->
<div class="search-bar">
<div class="search-row">
<input type="text" id="search-tanah" class="form-control" placeholder="🔍 Cari kode/pemilik..." oninput="filterTanah()"/>
<button class="btn btn-success" onclick="searchZoomTanah()" title="Zoom ke hasil"><i class="fa-solid fa-location-dot"></i></button>
</div>
<div class="filter-row">
<span class="filter-label">Luas:</span>
<select id="filter-tanah-op" class="form-control" onchange="filterTanah()">
<option value="">Semua</option>
<option value="gt">Lebih dari</option>
<option value="lt">Kurang dari</option>
</select>
<input type="number" id="filter-tanah-val" class="form-control" placeholder="m²" oninput="filterTanah()"/>
</div>
</div>
<div class="form-wrap">
<div id="draw-info-tanah" class="draw-info"><i class="fa-solid fa-draw-polygon"></i> Belum ada polygon digambar</div>
<div class="form-group">
<label>Kode Kavling</label>
<input type="text" id="t-kode" class="form-control" placeholder="Contoh: KAV-001"/>
</div>
<div class="form-group">
<label>Nama Pemilik</label>
<input type="text" id="t-nama" class="form-control" placeholder="Nama pemilik / perusahaan"/>
</div>
<div class="form-group">
<label>Status Kepemilikan</label>
<select id="t-status" class="form-control">
<option value="">-- Pilih Status --</option>
<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 ()</label>
<input type="text" id="t-luas" class="form-control" readonly placeholder="Otomatis dihitung Leaflet"/>
</div>
<button class="btn btn-success btn-block" onclick="startDrawTanah()"><i class="fa-solid fa-draw-polygon"></i> Gambar Polygon di Peta</button>
<button class="btn btn-primary btn-block" onclick="simpanTanah()"><i class="fa-solid fa-floppy-disk"></i> Simpan Data Tanah</button>
<button class="btn btn-secondary btn-block" onclick="cancelDraw()"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
<!-- Data Viz Tanah -->
<div class="viz-panel" id="viz-tanah">
<div class="viz-title"><i class="fa-solid fa-chart-bar"></i> Distribusi Status Tanah</div>
<div id="viz-tanah-bars"></div>
</div>
<div class="list-header">
<span><i class="fa-solid fa-list"></i> Daftar Parsil Tanah</span>
<span class="badge" id="count-tanah">0</span>
</div>
<div class="list-wrap" id="list-tanah"></div>
</div>
<!-- ═══════════ TAB KERUSAKAN ═══════════ -->
<div id="tab-kerusakan" class="tab-content">
<div class="sidebar-header">
<h2><i class="fa-solid fa-triangle-exclamation" style="color:#f87171"></i> Laporan Kerusakan Jalan</h2>
<div class="hint">Klik titik di peta untuk input lokasi kerusakan</div>
</div>
<!-- Search Kerusakan -->
<div class="search-bar">
<div class="search-row">
<input type="text" id="search-kerusakan" class="form-control" placeholder="🔍 Cari deskripsi/jalan..." oninput="filterKerusakan()"/>
</div>
<div class="filter-row">
<span class="filter-label">Urgensi:</span>
<select id="filter-urgensi" class="form-control" onchange="filterKerusakan()">
<option value="">Semua</option>
<option value="Kritis">Kritis</option>
<option value="Tinggi">Tinggi</option>
<option value="Sedang">Sedang</option>
<option value="Rendah">Rendah</option>
</select>
<select id="filter-status-k" class="form-control" onchange="filterKerusakan()">
<option value="">Semua Status</option>
<option value="Dilaporkan">Dilaporkan</option>
<option value="Diverifikasi">Diverifikasi</option>
<option value="Dalam Perbaikan">Dalam Perbaikan</option>
<option value="Sudah Diperbaiki">Sudah Diperbaiki</option>
</select>
</div>
</div>
<div class="form-wrap">
<div id="draw-info-kerusakan" class="draw-info"><i class="fa-solid fa-map-pin"></i> Klik "Pilih Lokasi" lalu klik di peta</div>
<div class="form-group">
<label>Nama Jalan (opsional)</label>
<input type="text" id="k-jalan" class="form-control" placeholder="Nama jalan terdekat"/>
</div>
<div class="form-group">
<label>Deskripsi Kerusakan</label>
<textarea id="k-deskripsi" class="form-control" placeholder="Contoh: Lubang besar di tengah jalan..."></textarea>
</div>
<div class="form-group">
<label>Tingkat Urgensi</label>
<select id="k-urgensi" class="form-control">
<option value="">-- Pilih Urgensi --</option>
<option value="Rendah">🟢 Rendah</option>
<option value="Sedang">🟡 Sedang</option>
<option value="Tinggi">🔴 Tinggi</option>
<option value="Kritis">🟣 Kritis (Segera!)</option>
</select>
</div>
<div class="form-group">
<label>Foto Kerusakan <span style="color:#64748b;font-weight:400;text-transform:none">(opsional)</span></label>
<input type="file" id="k-foto" accept="image/*" class="form-control" onchange="previewFoto()" style="padding:5px"/>
<img id="foto-preview" class="foto-preview" alt="Preview Foto"/>
</div>
<div class="form-group">
<label>Koordinat <span style="color:#64748b;font-weight:400;text-transform:none">(otomatis dari klik peta)</span></label>
<input type="text" id="k-koordinat" class="form-control" readonly placeholder="Klik lokasi di peta..."/>
</div>
<input type="hidden" id="k-lat"/>
<input type="hidden" id="k-lng"/>
<input type="hidden" id="k-tgl"/>
<button class="btn btn-danger btn-block" onclick="startPickKerusakan()"><i class="fa-solid fa-map-pin"></i> Pilih Lokasi di Peta</button>
<button class="btn btn-warning btn-block" onclick="simpanKerusakan()"><i class="fa-solid fa-floppy-disk"></i> Simpan Laporan</button>
<button class="btn btn-secondary btn-block" onclick="cancelPickKerusakan()"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
<!-- Data Viz Kerusakan -->
<div class="viz-panel">
<div class="viz-title"><i class="fa-solid fa-chart-bar"></i> Ringkasan Urgensi</div>
<div id="viz-kerusakan-bars"></div>
</div>
<div class="list-header">
<span><i class="fa-solid fa-list"></i> Daftar Laporan</span>
<span class="badge" id="count-kerusakan">0</span>
</div>
<div class="list-wrap" id="list-kerusakan"></div>
</div>
</div>
<div id="map"></div>
</div>
<!-- Floating toolbar saat edit shape aktif -->
<div class="edit-toolbar" id="edit-toolbar">
<span class="edit-toolbar-label" id="edit-toolbar-label"><i class="fa-solid fa-pen"></i> Mode Edit Shape Aktif</span>
<button class="btn btn-info" onclick="toggleDragShape()" id="btn-toggle-drag"><i class="fa-solid fa-arrows-up-down-left-right"></i> Geser Seluruh Shape</button>
<button class="btn btn-success" onclick="simpanHasilEdit()"><i class="fa-solid fa-check"></i> Simpan Perubahan</button>
<button class="btn btn-secondary" onclick="batalEdit()"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
<!-- MODAL EDIT ATRIBUT JALAN -->
<div class="modal-overlay" id="modal-jalan">
<div class="modal-box">
<div class="modal-title"><i class="fa-solid fa-road"></i> Edit Atribut Jalan</div>
<input type="hidden" id="edit-j-id"/>
<div class="form-group"><label>Nama Jalan</label><input type="text" id="edit-j-nama" class="form-control"/></div>
<div class="form-group">
<label>Status Jalan</label>
<select id="edit-j-status" class="form-control">
<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 (meter)</label><input type="text" id="edit-j-panjang" class="form-control" readonly/></div>
<div class="modal-actions">
<button class="btn btn-warning" onclick="updateJalan()"><i class="fa-solid fa-save"></i> Update</button>
<button class="btn btn-secondary" onclick="closeModal('modal-jalan')"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
</div>
</div>
<!-- MODAL EDIT ATRIBUT TANAH -->
<div class="modal-overlay" id="modal-tanah">
<div class="modal-box">
<div class="modal-title"><i class="fa-solid fa-land-mine-on"></i> Edit Atribut Parsil Tanah</div>
<input type="hidden" id="edit-t-id"/>
<div class="form-group"><label>Kode Kavling</label><input type="text" id="edit-t-kode" class="form-control"/></div>
<div class="form-group"><label>Nama Pemilik</label><input type="text" id="edit-t-nama" class="form-control"/></div>
<div class="form-group">
<label>Status Kepemilikan</label>
<select id="edit-t-status" class="form-control">
<option value="SHM">SHM</option><option value="HGB">HGB</option>
<option value="HGU">HGU</option><option value="HP">HP</option>
</select>
</div>
<div class="form-group"><label>Luas ()</label><input type="text" id="edit-t-luas" class="form-control" readonly/></div>
<div class="modal-actions">
<button class="btn btn-warning" onclick="updateTanah()"><i class="fa-solid fa-save"></i> Update</button>
<button class="btn btn-secondary" onclick="closeModal('modal-tanah')"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
</div>
</div>
<!-- MODAL EDIT KERUSAKAN -->
<div class="modal-overlay" id="modal-kerusakan">
<div class="modal-box">
<div class="modal-title"><i class="fa-solid fa-triangle-exclamation"></i> Edit Laporan Kerusakan</div>
<input type="hidden" id="edit-k-id"/>
<div class="form-group"><label>Nama Jalan</label><input type="text" id="edit-k-jalan" class="form-control"/></div>
<div class="form-group"><label>Deskripsi</label><textarea id="edit-k-deskripsi" class="form-control"></textarea></div>
<div class="form-group">
<label>Tingkat Urgensi</label>
<select id="edit-k-urgensi" class="form-control">
<option value="Rendah">Rendah</option><option value="Sedang">Sedang</option>
<option value="Tinggi">Tinggi</option><option value="Kritis">Kritis</option>
</select>
</div>
<div class="form-group">
<label>Update Status</label>
<select id="edit-k-status" class="form-control">
<option value="Dilaporkan">Dilaporkan</option>
<option value="Diverifikasi">Diverifikasi</option>
<option value="Dalam Perbaikan">Dalam Perbaikan</option>
<option value="Sudah Diperbaiki">Sudah Diperbaiki</option>
</select>
</div>
<div class="form-group">
<label>Catatan Perubahan Status</label>
<textarea id="edit-k-catatan" class="form-control" placeholder="Catatan opsional saat ubah status..."></textarea>
</div>
<div class="modal-actions">
<button class="btn btn-warning" onclick="updateKerusakan()"><i class="fa-solid fa-save"></i> Update</button>
<button class="btn btn-secondary" onclick="closeModal('modal-kerusakan')"><i class="fa-solid fa-xmark"></i> Batal</button>
</div>
</div>
</div>
<!-- MODAL HISTORI KERUSAKAN -->
<div class="modal-overlay" id="modal-histori">
<div class="modal-box" style="width:480px">
<div class="modal-title"><i class="fa-solid fa-clock-rotate-left"></i> Histori Kerusakan Jalan</div>
<div id="histori-lokasi" style="font-size:.8rem;color:#94a3b8;margin-bottom:12px"></div>
<div id="histori-content" class="histori-list"></div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="closeModal('modal-histori')"><i class="fa-solid fa-xmark"></i> Tutup</button>
</div>
</div>
</div>
<div class="toast-container" id="toast-container"></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 src="https://cdn.jsdelivr.net/npm/leaflet-geometryutil@0.10.3/src/leaflet.geometryutil.js"></script>
<script>
// ═══════════════════════════════════
// PETA
// ═══════════════════════════════════
const map = L.map('map', { center:[-0.0264,109.3425], zoom:14 });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
attribution:'© OpenStreetMap | WebGIS SIG Pontianak', maxZoom:19
}).addTo(map);
const layerJalan = L.layerGroup().addTo(map);
const layerTanah = L.layerGroup().addTo(map);
const layerKerusakan= L.layerGroup().addTo(map);
L.control.layers(null,{'Data Jalan':layerJalan,'Parsil Tanah':layerTanah,'Kerusakan Jalan':layerKerusakan}).addTo(map);
const legend = L.control({position:'bottomright'});
legend.onAdd = () => {
const d = L.DomUtil.create('div','map-legend');
d.innerHTML=`
<div class="legend-title">Legenda</div>
<div style="margin-bottom:5px;font-size:.7rem;color:#475569;font-weight:700">JALAN</div>
<div class="legend-item"><div class="legend-color" style="background:#ef4444"></div>Jalan Nasional</div>
<div class="legend-item"><div class="legend-color" style="background:#3b82f6"></div>Jalan Provinsi</div>
<div class="legend-item"><div class="legend-color" style="background:#22c55e"></div>Jalan Kabupaten</div>
<div style="margin:8px 0 5px;font-size:.7rem;color:#475569;font-weight:700">PARSIL TANAH</div>
<div class="legend-item"><div class="legend-poly" style="background:#f97316"></div>SHM</div>
<div class="legend-item"><div class="legend-poly" style="background:#a855f7"></div>HGB</div>
<div class="legend-item"><div class="legend-poly" style="background:#0ea5e9"></div>HGU</div>
<div class="legend-item"><div class="legend-poly" style="background:#eab308"></div>Hak Pakai (HP)</div>
<div style="margin:8px 0 5px;font-size:.7rem;color:#475569;font-weight:700">KERUSAKAN JALAN</div>
<div class="legend-item"><span class="urgency-dot urgensi-rendah"></span>Rendah</div>
<div class="legend-item"><span class="urgency-dot urgensi-sedang"></span>Sedang</div>
<div class="legend-item"><span class="urgency-dot urgensi-tinggi"></span>Tinggi</div>
<div class="legend-item"><span class="urgency-dot urgensi-kritis"></span>Kritis</div>`;
return d;
};
legend.addTo(map);
// ═══════════════════════════════════
// HELPER
// ═══════════════════════════════════
const jalanColors = {Nasional:'#ef4444',Provinsi:'#3b82f6',Kabupaten:'#22c55e'};
const tanahColors = {SHM:'#f97316',HGB:'#a855f7',HGU:'#0ea5e9',HP:'#eab308'};
const kerusakanColors = {Rendah:'#4ade80',Sedang:'#fbbf24',Tinggi:'#f87171',Kritis:'#c084fc'};
const getJalanColor = s => jalanColors[s]||'#94a3b8';
const getTanahColor = s => tanahColors[s]||'#94a3b8';
const getKerusakanColor = s => kerusakanColors[s]||'#94a3b8';
const jalanBadgeCls = {Nasional:'s-nasional',Provinsi:'s-provinsi',Kabupaten:'s-kabupaten'};
const tanahBadgeCls = {SHM:'s-shm',HGB:'s-hgb',HGU:'s-hgu',HP:'s-hp'};
const urgensiCls = {Rendah:'s-rendah',Sedang:'s-sedang',Tinggi:'s-tinggi',Kritis:'s-kritis'};
function mkBadge(label,cls){ return `<span class="status-badge ${cls||''}">${label}</span>`; }
function fmt(n){ return parseFloat(n).toLocaleString('id-ID',{maximumFractionDigits:2}); }
function fmtDate(ts){ return ts ? new Date(ts).toLocaleString('id-ID',{dateStyle:'medium',timeStyle:'short'}) : '-'; }
function toast(msg,type='info'){
const c=document.getElementById('toast-container');
const el=document.createElement('div');
const icons={success:'fa-check-circle',error:'fa-circle-xmark',warning:'fa-triangle-exclamation',info:'fa-circle-info'};
el.className=`toast ${type}`;
el.innerHTML=`<i class="fa-solid ${icons[type]||'fa-circle-info'}"></i> ${msg}`;
c.appendChild(el);
setTimeout(()=>el.remove(),3500);
}
function hitungPanjang(latlngs){
let t=0;
for(let i=0;i<latlngs.length-1;i++) t+=latlngs[i].distanceTo(latlngs[i+1]);
return Math.round(t*100)/100;
}
function hitungLuas(latlngs){
return Math.round(L.GeometryUtil.geodesicArea(latlngs)*100)/100;
}
// ═══════════════════════════════════
// DATA STORE (untuk search & filter)
// ═══════════════════════════════════
let allJalan = [];
let allTanah = [];
let allKerusakan = [];
// ═══════════════════════════════════
// STATE
// ═══════════════════════════════════
let drawHandler=null, tempLayer=null, drawnGeometry=null, drawnValue=0, drawMode=null;
let editingLayer=null, editingId=null, editingType=null;
let dragMode=false, dragStartLatLng=null, dragOriginalLatLngs=null;
const jalanLayerMap={}, tanahLayerMap={}, kerusakanLayerMap={};
let pickingKerusakan=false;
// ═══════════════════════════════════
// DRAW BARU
// ═══════════════════════════════════
function startDrawJalan(){
batalEdit(); cancelDraw();
drawMode='jalan';
const info=document.getElementById('draw-info-jalan');
info.classList.add('active');
info.innerHTML='<i class="fa-solid fa-spinner fa-spin"></i> Klik peta untuk mulai, double-klik untuk selesai';
drawHandler=new L.Draw.Polyline(map,{shapeOptions:{color:'#38bdf8',weight:3,dashArray:'6,4'}});
drawHandler.enable();
map.once(L.Draw.Event.CREATED,e=>{
tempLayer=e.layer;
const ll=tempLayer.getLatLngs();
drawnValue=hitungPanjang(ll);
drawnGeometry={type:'LineString',coordinates:ll.map(l=>[l.lng,l.lat])};
tempLayer.setStyle({color:'#38bdf8',weight:3});
tempLayer.addTo(map);
document.getElementById('j-panjang').value=fmt(drawnValue)+' m';
info.classList.remove('active');
info.innerHTML=`<i class="fa-solid fa-check-circle" style="color:#4ade80"></i> Panjang: <b>${fmt(drawnValue)} m</b>`;
toast('Garis digambar! Panjang: '+fmt(drawnValue)+' m','success');
});
}
function startDrawTanah(){
batalEdit(); cancelDraw();
drawMode='tanah';
const info=document.getElementById('draw-info-tanah');
info.classList.add('active');
info.innerHTML='<i class="fa-solid fa-spinner fa-spin"></i> Klik peta untuk titik, double-klik untuk selesai';
drawHandler=new L.Draw.Polygon(map,{shapeOptions:{color:'#4ade80',fillColor:'#4ade80',fillOpacity:.2,weight:2,dashArray:'6,4'}});
drawHandler.enable();
map.once(L.Draw.Event.CREATED,e=>{
tempLayer=e.layer;
const ll=tempLayer.getLatLngs()[0];
drawnValue=hitungLuas(ll);
drawnGeometry={type:'Polygon',coordinates:[ll.map(l=>[l.lng,l.lat])]};
drawnGeometry.coordinates[0].push(drawnGeometry.coordinates[0][0]);
tempLayer.setStyle({color:'#4ade80',fillColor:'#4ade80',fillOpacity:.2,weight:2});
tempLayer.addTo(map);
document.getElementById('t-luas').value=fmt(drawnValue)+' m²';
info.classList.remove('active');
info.innerHTML=`<i class="fa-solid fa-check-circle" style="color:#4ade80"></i> Luas: <b>${fmt(drawnValue)} m²</b>`;
toast('Polygon digambar! Luas: '+fmt(drawnValue)+' m²','success');
});
}
function cancelDraw(){
if(drawHandler){try{drawHandler.disable();}catch(e){} drawHandler=null;}
if(tempLayer){map.removeLayer(tempLayer);tempLayer=null;}
drawnGeometry=null; drawnValue=0; drawMode=null;
['draw-info-jalan','draw-info-tanah'].forEach(id=>{
const el=document.getElementById(id);
el.classList.remove('active','editing');
el.innerHTML='<i class="fa-solid fa-draw-polygon"></i> Belum ada shape digambar';
});
document.getElementById('j-panjang').value='';
document.getElementById('t-luas').value='';
}
// ═══════════════════════════════════
// PICK LOKASI KERUSAKAN
// ═══════════════════════════════════
function startPickKerusakan(){
pickingKerusakan=true;
map.getContainer().style.cursor='crosshair';
const info=document.getElementById('draw-info-kerusakan');
info.classList.add('active');
info.innerHTML='<i class="fa-solid fa-spinner fa-spin"></i> Klik pada peta untuk menandai lokasi kerusakan...';
toast('Klik lokasi kerusakan di peta','info');
}
function cancelPickKerusakan(){
pickingKerusakan=false;
map.getContainer().style.cursor='';
if(tempPickMarker){map.removeLayer(tempPickMarker);tempPickMarker=null;}
const info=document.getElementById('draw-info-kerusakan');
info.classList.remove('active');
info.innerHTML='<i class="fa-solid fa-map-pin"></i> Klik "Pilih Lokasi" lalu klik di peta';
document.getElementById('k-koordinat').value='';
document.getElementById('k-lat').value='';
document.getElementById('k-lng').value='';
}
let tempPickMarker = null;
map.on('click',function(e){
if(pickingKerusakan){
pickingKerusakan=false;
map.getContainer().style.cursor='';
const lat=e.latlng.lat.toFixed(6);
const lng=e.latlng.lng.toFixed(6);
const now=new Date().toISOString();
document.getElementById('k-lat').value=lat;
document.getElementById('k-lng').value=lng;
document.getElementById('k-tgl').value=now;
document.getElementById('k-koordinat').value=`${lat}, ${lng}`;
const info=document.getElementById('draw-info-kerusakan');
info.classList.remove('active');
info.innerHTML=`<i class="fa-solid fa-check-circle" style="color:#4ade80"></i> Lokasi: <b>${lat}, ${lng}</b><br/><span style="font-size:.68rem;color:#64748b">📅 ${new Date(now).toLocaleString('id-ID')}</span>`;
// Tampilkan marker preview sementara
if(tempPickMarker) map.removeLayer(tempPickMarker);
tempPickMarker = L.circleMarker([parseFloat(lat),parseFloat(lng)],{
radius:10, color:'#f87171', fillColor:'#f87171', fillOpacity:.5, weight:3,
dashArray:'4,3'
}).addTo(map).bindTooltip('📍 Lokasi baru (belum disimpan)',{permanent:false});
toast('Lokasi kerusakan dipilih!','success');
return;
}
map.closePopup();
});
// ═══════════════════════════════════
// POPUP AKSI
// ═══════════════════════════════════
function popupJalan(item){
return `<div style="font-family:'Segoe UI',sans-serif;min-width:210px">
<b style="color:#60a5fa">${item.nama_jalan}</b><br/>
<span style="font-size:.75rem;color:#94a3b8">Status:</span> <b>${item.status_jalan}</b><br/>
<span style="font-size:.75rem;color:#94a3b8">Panjang:</span> <b>${fmt(item.panjang)} m</b>
<hr style="border:none;border-top:1px solid #334155;margin:8px 0"/>
<div style="display:flex;gap:5px;flex-wrap:wrap">
<button onclick="mulaiEditJalan(${item.id})" style="padding:5px 10px;background:#d97706;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">✏️ Edit Shape</button>
<button onclick="openEditJalan(${item.id})" style="padding:5px 10px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🏷️ Atribut</button>
<button onclick="hapusJalan(${item.id})" style="padding:5px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🗑️ Hapus</button>
</div>
</div>`;
}
function popupTanah(item){
return `<div style="font-family:'Segoe UI',sans-serif;min-width:210px">
<b style="color:#4ade80">${item.kode_kavling}</b><br/>
<span style="font-size:.75rem;color:#94a3b8">Pemilik:</span> <b>${item.nama_pemilik}</b><br/>
<span style="font-size:.75rem;color:#94a3b8">Status:</span> <b>${item.status_tanah}</b><br/>
<span style="font-size:.75rem;color:#94a3b8">Luas:</span> <b>${fmt(item.luas)} m²</b>
<hr style="border:none;border-top:1px solid #334155;margin:8px 0"/>
<div style="display:flex;gap:5px;flex-wrap:wrap">
<button onclick="mulaiEditTanah(${item.id})" style="padding:5px 10px;background:#d97706;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">✏️ Edit Shape</button>
<button onclick="openEditTanah(${item.id})" style="padding:5px 10px;background:#2563eb;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🏷️ Atribut</button>
<button onclick="hapusTanah(${item.id})" style="padding:5px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🗑️ Hapus</button>
</div>
</div>`;
}
function popupKerusakan(item){
const warna=getKerusakanColor(item.tingkat_urgensi);
const fotoHtml=item.foto_base64 ? `<img src="${item.foto_base64}" style="width:100%;border-radius:6px;margin:6px 0;max-height:120px;object-fit:cover"/>` : '';
const clusterInfo=item.laporan_terdekat>1 ? `<div style="background:#450a0a;color:#f87171;padding:4px 8px;border-radius:6px;font-size:.72rem;margin:4px 0">⚠️ ${item.laporan_terdekat} laporan dalam radius 50m</div>` : '';
return `<div style="font-family:'Segoe UI',sans-serif;min-width:230px">
<b style="color:${warna}"><span style="margin-right:5px">⚠️</span>${item.tingkat_urgensi}</b>
<span style="font-size:.72rem;color:#94a3b8;margin-left:6px">${item.status_laporan}</span><br/>
<span style="font-size:.78rem;color:#e2e8f0">${item.deskripsi}</span><br/>
${item.nama_jalan_ref ? `<span style="font-size:.72rem;color:#94a3b8">📍 ${item.nama_jalan_ref}</span><br/>` : ''}
<span style="font-size:.68rem;color:#64748b">🕐 ${fmtDate(item.tgl_laporan)}</span>
${clusterInfo}
${fotoHtml}
<hr style="border:none;border-top:1px solid #334155;margin:8px 0"/>
<div style="display:flex;gap:5px;flex-wrap:wrap">
<button onclick="openEditKerusakan(${item.id})" style="padding:5px 10px;background:#d97706;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">✏️ Edit</button>
<button onclick="lihatHistori(${item.id},'${(item.nama_jalan_ref||'').replace(/'/g,"\\'")}','${item.deskripsi.substring(0,30).replace(/'/g,"\\'")}...')" style="padding:5px 10px;background:#7c3aed;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🕐 Histori</button>
<button onclick="hapusKerusakan(${item.id})" style="padding:5px 10px;background:#dc2626;color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:11px;font-weight:600">🗑️ Hapus</button>
</div>
</div>`;
}
// ═══════════════════════════════════
// ATTACH EVENTS
// ═══════════════════════════════════
function attachJalanEvents(layer,item){
layer.on('click',function(e){
L.DomEvent.stopPropagation(e);
if(editingLayer&&editingLayer!==layer){toast('Selesaikan edit yang aktif dulu!','warning');return;}
layer.bindPopup(popupJalan(item),{maxWidth:290}).openPopup(e.latlng);
});
layer.on('mouseover',function(){if(editingLayer===layer)return;layer.setStyle({weight:6,opacity:1});});
layer.on('mouseout',function(){if(editingLayer===layer)return;layer.setStyle({weight:4,opacity:.9});});
}
function attachTanahEvents(layer,item){
layer.on('click',function(e){
L.DomEvent.stopPropagation(e);
if(editingLayer&&editingLayer!==layer){toast('Selesaikan edit yang aktif dulu!','warning');return;}
layer.bindPopup(popupTanah(item),{maxWidth:290}).openPopup(e.latlng);
});
layer.on('mouseover',function(){if(editingLayer===layer)return;layer.setStyle({weight:4,fillOpacity:.5});});
layer.on('mouseout',function(){if(editingLayer===layer)return;layer.setStyle({weight:2,fillOpacity:.3});});
}
function attachKerusakanEvents(marker,item){
marker.on('click',function(e){
L.DomEvent.stopPropagation(e);
marker.bindPopup(popupKerusakan(item),{maxWidth:300}).openPopup();
});
}
// ═══════════════════════════════════
// EDIT SHAPE titik + DRAG SELURUH SHAPE
// ═══════════════════════════════════
function mulaiEditJalan(id){
map.closePopup();
const layer=jalanLayerMap[id];
if(!layer)return;
batalEdit();
editingLayer=layer; editingId=id; editingType='jalan';
layer.editing.enable();
layer.setStyle({color:'#f59e0b',weight:5,dashArray:'8,4',opacity:1});
document.getElementById('edit-toolbar-label').innerHTML='<i class="fa-solid fa-pen"></i> Edit Jalan geser titik atau geser seluruh garis';
document.getElementById('edit-toolbar').classList.add('show');
document.getElementById('btn-toggle-drag').style.display='';
dragMode=false;
toast('Mode edit aktif! Geser titik pada garis atau aktifkan "Geser Seluruh Shape"','warning');
}
function mulaiEditTanah(id){
map.closePopup();
const layer=tanahLayerMap[id];
if(!layer)return;
batalEdit();
editingLayer=layer; editingId=id; editingType='tanah';
layer.editing.enable();
layer.setStyle({color:'#f59e0b',fillColor:'#f59e0b',fillOpacity:.35,weight:3,dashArray:'8,4'});
document.getElementById('edit-toolbar-label').innerHTML='<i class="fa-solid fa-pen"></i> Edit Polygon geser titik atau geser seluruh polygon';
document.getElementById('edit-toolbar').classList.add('show');
document.getElementById('btn-toggle-drag').style.display='';
dragMode=false;
toast('Mode edit aktif! Geser titik polygon atau aktifkan "Geser Seluruh Shape"','warning');
}
// Toggle drag seluruh shape
function toggleDragShape(){
if(!editingLayer)return;
dragMode=!dragMode;
const btn=document.getElementById('btn-toggle-drag');
if(dragMode){
// Nonaktifkan edit titik, aktifkan drag
editingLayer.editing.disable();
btn.innerHTML='<i class="fa-solid fa-pen"></i> Mode Edit Titik';
btn.classList.remove('btn-info'); btn.classList.add('btn-warning');
toast('Mode Geser Shape aktif seret shape ke posisi baru','warning');
// Simpan posisi awal
if(editingType==='jalan'){
dragOriginalLatLngs=editingLayer.getLatLngs().map(ll=>L.latLng(ll.lat,ll.lng));
} else {
dragOriginalLatLngs=editingLayer.getLatLngs()[0].map(ll=>L.latLng(ll.lat,ll.lng));
}
// Event mousedown di peta → mulai drag
editingLayer.on('mousedown', onDragStart);
editingLayer.getElement && (editingLayer.getElement().style.cursor='grab');
map.dragging.disable();
editingLayer._map.on('mousemove', onDragMove);
editingLayer._map.on('mouseup', onDragEnd);
} else {
// Kembali ke mode edit titik
editingLayer.off('mousedown', onDragStart);
editingLayer._map.off('mousemove', onDragMove);
editingLayer._map.off('mouseup', onDragEnd);
map.dragging.enable();
editingLayer.editing.enable();
btn.innerHTML='<i class="fa-solid fa-arrows-up-down-left-right"></i> Geser Seluruh Shape';
btn.classList.remove('btn-warning'); btn.classList.add('btn-info');
toast('Mode Edit Titik aktif','info');
dragStartLatLng=null; dragOriginalLatLngs=null;
}
}
function onDragStart(e){
dragStartLatLng=e.latlng;
L.DomEvent.stopPropagation(e);
}
function onDragMove(e){
if(!dragStartLatLng||!dragOriginalLatLngs)return;
const dlat=e.latlng.lat-dragStartLatLng.lat;
const dlng=e.latlng.lng-dragStartLatLng.lng;
const newLL=dragOriginalLatLngs.map(ll=>L.latLng(ll.lat+dlat,ll.lng+dlng));
if(editingType==='jalan') editingLayer.setLatLngs(newLL);
else editingLayer.setLatLngs([newLL]);
}
function onDragEnd(e){
if(!dragStartLatLng||!dragOriginalLatLngs)return;
const dlat=e.latlng.lat-dragStartLatLng.lat;
const dlng=e.latlng.lng-dragStartLatLng.lng;
dragOriginalLatLngs=dragOriginalLatLngs.map(ll=>L.latLng(ll.lat+dlat,ll.lng+dlng));
dragStartLatLng=null;
}
function simpanHasilEdit(){
if(!editingLayer||!editingId)return;
// Matikan drag dulu jika aktif
if(dragMode){
editingLayer.off('mousedown',onDragStart);
editingLayer._map.off('mousemove',onDragMove);
editingLayer._map.off('mouseup',onDragEnd);
map.dragging.enable();
dragMode=false; dragStartLatLng=null; dragOriginalLatLngs=null;
} else {
editingLayer.editing.disable();
}
if(editingType==='jalan'){
const ll=editingLayer.getLatLngs();
const val=hitungPanjang(ll);
const geo={type:'LineString',coordinates:ll.map(l=>[l.lng,l.lat])};
fetch(`api/jalan.php?id=${editingId}`).then(r=>r.json()).then(res=>{
if(!res.success){toast('Gagal ambil data','error');batalEdit();return;}
const d=res.data;
return fetch('api/jalan.php',{
method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id:editingId,nama_jalan:d.nama_jalan,status_jalan:d.status_jalan,panjang:val,geometry:geo})
});
}).then(r=>r&&r.json()).then(res=>{
if(res&&res.success){toast(`Shape diupdate! Panjang: ${fmt(val)} m`,'success');selesaiEdit();loadJalan();}
else{toast('Gagal simpan','error');batalEdit();}
}).catch(()=>{toast('Error koneksi','error');batalEdit();});
}else{
const ll=editingLayer.getLatLngs()[0];
const val=hitungLuas(ll);
const geo={type:'Polygon',coordinates:[ll.map(l=>[l.lng,l.lat])]};
geo.coordinates[0].push(geo.coordinates[0][0]);
fetch(`api/tanah.php?id=${editingId}`).then(r=>r.json()).then(res=>{
if(!res.success){toast('Gagal ambil data','error');batalEdit();return;}
const d=res.data;
return fetch('api/tanah.php',{
method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id:editingId,kode_kavling:d.kode_kavling,nama_pemilik:d.nama_pemilik,status_tanah:d.status_tanah,luas:val,geometry:geo})
});
}).then(r=>r&&r.json()).then(res=>{
if(res&&res.success){toast(`Shape diupdate! Luas: ${fmt(val)} m²`,'success');selesaiEdit();loadTanah();}
else{toast('Gagal simpan','error');batalEdit();}
}).catch(()=>{toast('Error koneksi','error');batalEdit();});
}
}
function batalEdit(){
if(dragMode&&editingLayer){
editingLayer.off('mousedown',onDragStart);
try{editingLayer._map.off('mousemove',onDragMove);editingLayer._map.off('mouseup',onDragEnd);}catch(e){}
map.dragging.enable();
dragMode=false;
}
if(editingLayer){try{editingLayer.editing.disable();}catch(e){} editingLayer=null;}
editingId=null; editingType=null; dragStartLatLng=null; dragOriginalLatLngs=null;
document.getElementById('edit-toolbar').classList.remove('show');
const btn=document.getElementById('btn-toggle-drag');
btn.innerHTML='<i class="fa-solid fa-arrows-up-down-left-right"></i> Geser Seluruh Shape';
btn.classList.remove('btn-warning'); btn.classList.add('btn-info');
}
function selesaiEdit(){
if(dragMode) map.dragging.enable();
editingLayer=null; editingId=null; editingType=null;
dragMode=false; dragStartLatLng=null; dragOriginalLatLngs=null;
document.getElementById('edit-toolbar').classList.remove('show');
}
// ═══════════════════════════════════
// CRUD JALAN
// ═══════════════════════════════════
function simpanJalan(){
const nama=document.getElementById('j-nama').value.trim();
const status=document.getElementById('j-status').value;
if(!nama) return toast('Nama jalan wajib diisi!','error');
if(!status) return toast('Status jalan wajib dipilih!','error');
if(!drawnGeometry||drawMode!=='jalan') return toast('Gambar garis di peta terlebih dahulu!','error');
fetch('api/jalan.php',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({nama_jalan:nama,status_jalan:status,panjang:drawnValue,geometry:drawnGeometry})
}).then(r=>r.json()).then(res=>{
if(res.success){
toast('Data jalan berhasil disimpan!','success');
document.getElementById('j-nama').value='';
document.getElementById('j-status').value='';
if(tempLayer){map.removeLayer(tempLayer);tempLayer=null;}
drawnGeometry=null;drawnValue=0;drawMode=null;
document.getElementById('j-panjang').value='';
document.getElementById('draw-info-jalan').innerHTML='<i class="fa-solid fa-draw-polygon"></i> Belum ada garis digambar';
loadJalan();
}else toast(res.message||'Gagal menyimpan','error');
}).catch(()=>toast('Error koneksi','error'));
}
function loadJalan(){
fetch('api/jalan.php').then(r=>r.json()).then(res=>{
if(!res.success)return;
allJalan=res.data;
renderJalan(allJalan);
updateVizJalan(allJalan);
});
}
function renderJalan(data){
layerJalan.clearLayers();
Object.keys(jalanLayerMap).forEach(k=>delete jalanLayerMap[k]);
const list=document.getElementById('list-jalan');
list.innerHTML='';
document.getElementById('count-jalan').textContent=data.length;
data.forEach(item=>{
const coords=item.geometry.coordinates.map(c=>[c[1],c[0]]);
const layer=L.polyline(coords,{color:getJalanColor(item.status_jalan),weight:4,opacity:.9});
attachJalanEvents(layer,item);
layer.addTo(layerJalan);
jalanLayerMap[item.id]=layer;
const div=document.createElement('div');
div.className='list-item';
div.dataset.id=item.id;
div.innerHTML=`
<div class="item-name">${item.nama_jalan} ${mkBadge(item.status_jalan,jalanBadgeCls[item.status_jalan])}</div>
<div class="item-meta"><i class="fa-solid fa-ruler"></i> ${fmt(item.panjang)} meter</div>
<div class="item-actions">
<button class="btn btn-warning" onclick="mulaiEditJalan(${item.id})"><i class="fa-solid fa-pen"></i> Edit Shape</button>
<button class="btn btn-info" onclick="openEditJalan(${item.id})"><i class="fa-solid fa-tag"></i> Atribut</button>
<button class="btn btn-danger" onclick="hapusJalan(${item.id})"><i class="fa-solid fa-trash"></i> Hapus</button>
<button class="btn btn-primary" onclick="focusJalan(${JSON.stringify(item.geometry.coordinates)})"><i class="fa-solid fa-location-crosshairs"></i> Lihat</button>
</div>`;
list.appendChild(div);
});
}
function focusJalan(coords){
map.fitBounds(L.latLngBounds(coords.map(c=>[c[1],c[0]])),{padding:[60,60]});
}
function filterJalan(){
const q=document.getElementById('search-jalan').value.toLowerCase();
const op=document.getElementById('filter-jalan-op').value;
const val=parseFloat(document.getElementById('filter-jalan-val').value)||0;
const filtered=allJalan.filter(item=>{
const matchQ=!q||item.nama_jalan.toLowerCase().includes(q)||item.status_jalan.toLowerCase().includes(q);
let matchF=true;
if(op==='gt') matchF=item.panjang>val;
else if(op==='lt') matchF=item.panjang<val;
return matchQ&&matchF;
});
renderJalan(filtered);
}
function searchZoomJalan(){
const q=document.getElementById('search-jalan').value.toLowerCase().trim();
if(!q)return;
const item=allJalan.find(j=>j.nama_jalan.toLowerCase().includes(q));
if(!item)return toast('Jalan tidak ditemukan','error');
focusJalan(item.geometry.coordinates);
// Highlight di list
document.querySelectorAll('#list-jalan .list-item').forEach(el=>{
el.classList.toggle('search-highlight',el.querySelector('.item-name').textContent.toLowerCase().includes(q));
});
const layer=jalanLayerMap[item.id];
if(layer){
layer.bindPopup(popupJalan(item),{maxWidth:290}).openPopup();
}
toast(`Ditemukan: ${item.nama_jalan}`, 'success');
}
function openEditJalan(id){
map.closePopup();
fetch(`api/jalan.php?id=${id}`).then(r=>r.json()).then(res=>{
if(!res.success)return toast('Data tidak ditemukan','error');
const d=res.data;
document.getElementById('edit-j-id').value=d.id;
document.getElementById('edit-j-nama').value=d.nama_jalan;
document.getElementById('edit-j-status').value=d.status_jalan;
document.getElementById('edit-j-panjang').value=fmt(d.panjang)+' m';
document.getElementById('edit-j-id').dataset.geometry=JSON.stringify(d.geometry);
document.getElementById('edit-j-id').dataset.panjang=d.panjang;
document.getElementById('modal-jalan').classList.add('show');
});
}
function updateJalan(){
const id=document.getElementById('edit-j-id').value;
const nama=document.getElementById('edit-j-nama').value.trim();
const status=document.getElementById('edit-j-status').value;
const geometry=JSON.parse(document.getElementById('edit-j-id').dataset.geometry);
const panjang=parseFloat(document.getElementById('edit-j-id').dataset.panjang);
if(!nama)return toast('Nama jalan wajib diisi','error');
fetch('api/jalan.php',{
method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,nama_jalan:nama,status_jalan:status,panjang,geometry})
}).then(r=>r.json()).then(res=>{
if(res.success){toast('Atribut jalan diupdate!','success');closeModal('modal-jalan');loadJalan();}
else toast(res.message,'error');
});
}
function hapusJalan(id){
map.closePopup();
if(!confirm('Hapus data jalan ini?'))return;
fetch('api/jalan.php',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})
.then(r=>r.json()).then(res=>{
if(res.success){toast('Data jalan dihapus!','success');loadJalan();}
else toast(res.message,'error');
});
}
// ═══════════════════════════════════
// CRUD TANAH
// ═══════════════════════════════════
function simpanTanah(){
const kode=document.getElementById('t-kode').value.trim();
const nama=document.getElementById('t-nama').value.trim();
const status=document.getElementById('t-status').value;
if(!kode) return toast('Kode kavling wajib diisi!','error');
if(!nama) return toast('Nama pemilik wajib diisi!','error');
if(!status) return toast('Status tanah wajib dipilih!','error');
if(!drawnGeometry||drawMode!=='tanah') return toast('Gambar polygon di peta terlebih dahulu!','error');
fetch('api/tanah.php',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({kode_kavling:kode,nama_pemilik:nama,status_tanah:status,luas:drawnValue,geometry:drawnGeometry})
}).then(r=>r.json()).then(res=>{
if(res.success){
toast('Data tanah berhasil disimpan!','success');
document.getElementById('t-kode').value='';
document.getElementById('t-nama').value='';
document.getElementById('t-status').value='';
document.getElementById('t-luas').value='';
if(tempLayer){map.removeLayer(tempLayer);tempLayer=null;}
drawnGeometry=null;drawnValue=0;drawMode=null;
document.getElementById('draw-info-tanah').innerHTML='<i class="fa-solid fa-draw-polygon"></i> Belum ada polygon digambar';
loadTanah();
}else toast(res.message||'Gagal menyimpan','error');
}).catch(()=>toast('Error koneksi','error'));
}
function loadTanah(){
fetch('api/tanah.php').then(r=>r.json()).then(res=>{
if(!res.success)return;
allTanah=res.data;
renderTanah(allTanah);
updateVizTanah(allTanah);
});
}
function renderTanah(data){
layerTanah.clearLayers();
Object.keys(tanahLayerMap).forEach(k=>delete tanahLayerMap[k]);
const list=document.getElementById('list-tanah');
list.innerHTML='';
document.getElementById('count-tanah').textContent=data.length;
data.forEach(item=>{
const coords=item.geometry.coordinates[0].map(c=>[c[1],c[0]]);
const warna=getTanahColor(item.status_tanah);
const layer=L.polygon(coords,{color:warna,fillColor:warna,fillOpacity:.3,weight:2});
attachTanahEvents(layer,item);
layer.addTo(layerTanah);
tanahLayerMap[item.id]=layer;
const div=document.createElement('div');
div.className='list-item';
div.innerHTML=`
<div class="item-name">${item.kode_kavling} ${mkBadge(item.status_tanah,tanahBadgeCls[item.status_tanah])}</div>
<div class="item-meta"><i class="fa-solid fa-user"></i> ${item.nama_pemilik}</div>
<div class="item-meta"><i class="fa-solid fa-vector-square"></i> ${fmt(item.luas)} m²</div>
<div class="item-actions">
<button class="btn btn-warning" onclick="mulaiEditTanah(${item.id})"><i class="fa-solid fa-pen"></i> Edit Shape</button>
<button class="btn btn-info" onclick="openEditTanah(${item.id})"><i class="fa-solid fa-tag"></i> Atribut</button>
<button class="btn btn-danger" onclick="hapusTanah(${item.id})"><i class="fa-solid fa-trash"></i> Hapus</button>
<button class="btn btn-primary" onclick="focusTanah(${JSON.stringify(item.geometry.coordinates[0])})"><i class="fa-solid fa-location-crosshairs"></i> Lihat</button>
</div>`;
list.appendChild(div);
});
}
function focusTanah(coords){
map.fitBounds(L.latLngBounds(coords.map(c=>[c[1],c[0]])),{padding:[60,60]});
}
function filterTanah(){
const q=document.getElementById('search-tanah').value.toLowerCase();
const op=document.getElementById('filter-tanah-op').value;
const val=parseFloat(document.getElementById('filter-tanah-val').value)||0;
const filtered=allTanah.filter(item=>{
const matchQ=!q||item.kode_kavling.toLowerCase().includes(q)||item.nama_pemilik.toLowerCase().includes(q)||item.status_tanah.toLowerCase().includes(q);
let matchF=true;
if(op==='gt') matchF=item.luas>val;
else if(op==='lt') matchF=item.luas<val;
return matchQ&&matchF;
});
renderTanah(filtered);
}
function searchZoomTanah(){
const q=document.getElementById('search-tanah').value.toLowerCase().trim();
if(!q)return;
const item=allTanah.find(t=>t.kode_kavling.toLowerCase().includes(q)||t.nama_pemilik.toLowerCase().includes(q));
if(!item)return toast('Parsil tidak ditemukan','error');
focusTanah(item.geometry.coordinates[0]);
const layer=tanahLayerMap[item.id];
if(layer) layer.bindPopup(popupTanah(item),{maxWidth:290}).openPopup();
toast(`Ditemukan: ${item.kode_kavling} ${item.nama_pemilik}`,'success');
}
function openEditTanah(id){
map.closePopup();
fetch(`api/tanah.php?id=${id}`).then(r=>r.json()).then(res=>{
if(!res.success)return toast('Data tidak ditemukan','error');
const d=res.data;
document.getElementById('edit-t-id').value=d.id;
document.getElementById('edit-t-kode').value=d.kode_kavling;
document.getElementById('edit-t-nama').value=d.nama_pemilik;
document.getElementById('edit-t-status').value=d.status_tanah;
document.getElementById('edit-t-luas').value=fmt(d.luas)+' m²';
document.getElementById('edit-t-id').dataset.geometry=JSON.stringify(d.geometry);
document.getElementById('edit-t-id').dataset.luas=d.luas;
document.getElementById('modal-tanah').classList.add('show');
});
}
function updateTanah(){
const id=document.getElementById('edit-t-id').value;
const kode=document.getElementById('edit-t-kode').value.trim();
const nama=document.getElementById('edit-t-nama').value.trim();
const status=document.getElementById('edit-t-status').value;
const geometry=JSON.parse(document.getElementById('edit-t-id').dataset.geometry);
const luas=parseFloat(document.getElementById('edit-t-id').dataset.luas);
fetch('api/tanah.php',{
method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,kode_kavling:kode,nama_pemilik:nama,status_tanah:status,luas,geometry})
}).then(r=>r.json()).then(res=>{
if(res.success){toast('Atribut tanah diupdate!','success');closeModal('modal-tanah');loadTanah();}
else toast(res.message,'error');
});
}
function hapusTanah(id){
map.closePopup();
if(!confirm('Hapus data parsil tanah ini?'))return;
fetch('api/tanah.php',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})
.then(r=>r.json()).then(res=>{
if(res.success){toast('Data tanah dihapus!','success');loadTanah();}
else toast(res.message,'error');
});
}
// ═══════════════════════════════════
// CRUD KERUSAKAN JALAN
// ═══════════════════════════════════
function previewFoto(){
const file=document.getElementById('k-foto').files[0];
const preview=document.getElementById('foto-preview');
if(!file){preview.classList.remove('show');return;}
const reader=new FileReader();
reader.onload=e=>{preview.src=e.target.result;preview.classList.add('show');};
reader.readAsDataURL(file);
}
function simpanKerusakan(){
const jalan=document.getElementById('k-jalan').value.trim();
const deskripsi=document.getElementById('k-deskripsi').value.trim();
const urgensi=document.getElementById('k-urgensi').value;
const lat=document.getElementById('k-lat').value;
const lng=document.getElementById('k-lng').value;
if(!deskripsi) return toast('Deskripsi kerusakan wajib diisi!','error');
if(!urgensi) return toast('Tingkat urgensi wajib dipilih!','error');
if(!lat||!lng) return toast('Pilih lokasi di peta terlebih dahulu!','error');
// Baca foto jika ada
const file=document.getElementById('k-foto').files[0];
if(file){
const reader=new FileReader();
reader.onload=e=>kirimKerusakan(jalan,deskripsi,urgensi,parseFloat(lat),parseFloat(lng),e.target.result);
reader.readAsDataURL(file);
} else {
kirimKerusakan(jalan,deskripsi,urgensi,parseFloat(lat),parseFloat(lng),null);
}
}
function kirimKerusakan(jalan,deskripsi,urgensi,lat,lng,foto_base64){
fetch('api/kerusakan.php',{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({nama_jalan_ref:jalan,deskripsi,tingkat_urgensi:urgensi,lat,lng,foto_base64})
}).then(r=>r.json()).then(res=>{
if(res.success){
toast('Laporan kerusakan disimpan!','success');
// Hapus marker preview sementara
if(tempPickMarker){map.removeLayer(tempPickMarker);tempPickMarker=null;}
document.getElementById('k-jalan').value='';
document.getElementById('k-deskripsi').value='';
document.getElementById('k-urgensi').value='';
document.getElementById('k-koordinat').value='';
document.getElementById('k-lat').value='';
document.getElementById('k-lng').value='';
document.getElementById('k-foto').value='';
document.getElementById('foto-preview').classList.remove('show');
document.getElementById('draw-info-kerusakan').innerHTML='<i class="fa-solid fa-map-pin"></i> Klik "Pilih Lokasi" lalu klik di peta';
loadKerusakan().then(()=>{ map.setView([lat,lng],17); });
}else toast(res.message||'Gagal menyimpan','error');
}).catch(()=>toast('Error koneksi','error'));
}
function loadKerusakan(){
return fetch('api/kerusakan.php').then(r=>r.json()).then(res=>{
if(!res.success)return;
allKerusakan=res.data;
renderKerusakan(allKerusakan);
updateVizKerusakan(allKerusakan);
});
}
function renderKerusakan(data){
layerKerusakan.clearLayers();
Object.keys(kerusakanLayerMap).forEach(k=>delete kerusakanLayerMap[k]);
const list=document.getElementById('list-kerusakan');
list.innerHTML='';
document.getElementById('count-kerusakan').textContent=data.length;
data.forEach(item=>{
const warna=getKerusakanColor(item.tingkat_urgensi);
const radius=item.laporan_terdekat>1?8+Math.min(item.laporan_terdekat*2,14):8;
const marker=L.circleMarker([item.lat,item.lng],{
radius, color:warna, fillColor:warna, fillOpacity:.7, weight:2
});
attachKerusakanEvents(marker,item);
marker.addTo(layerKerusakan);
kerusakanLayerMap[item.id]=marker;
const clusterHtml=item.laporan_terdekat>1?`<span class="cluster-badge">×${item.laporan_terdekat} laporan</span>`:'';
const div=document.createElement('div');
div.className='list-item';
div.innerHTML=`
<div class="item-name">
<span class="urgency-dot urgensi-${item.tingkat_urgensi.toLowerCase()}"></span>
${item.tingkat_urgensi} ${mkBadge(item.status_laporan,'')}${clusterHtml}
</div>
<div class="item-meta"><i class="fa-solid fa-road"></i> ${item.nama_jalan_ref||''}</div>
<div class="item-meta" style="margin-top:2px">${item.deskripsi.substring(0,60)}${item.deskripsi.length>60?'...':''}</div>
<div class="item-meta"><i class="fa-solid fa-calendar"></i> ${fmtDate(item.tgl_laporan)}</div>
<div class="item-actions">
<button class="btn btn-warning" onclick="openEditKerusakan(${item.id})"><i class="fa-solid fa-pen"></i> Edit</button>
<button class="btn btn-purple" onclick="lihatHistori(${item.id},'${(item.nama_jalan_ref||'').replace(/'/g,"\\'")}','${item.deskripsi.substring(0,30).replace(/'/g,"\\'")}')"><i class="fa-solid fa-clock-rotate-left"></i> Histori</button>
<button class="btn btn-primary" onclick="focusKerusakan(${item.lat},${item.lng})"><i class="fa-solid fa-location-crosshairs"></i> Lihat</button>
<button class="btn btn-danger" onclick="hapusKerusakan(${item.id})"><i class="fa-solid fa-trash"></i> Hapus</button>
</div>`;
list.appendChild(div);
});
}
function focusKerusakan(lat,lng){
map.setView([lat,lng],17);
}
function filterKerusakan(){
const q=document.getElementById('search-kerusakan').value.toLowerCase();
const urgensi=document.getElementById('filter-urgensi').value;
const status=document.getElementById('filter-status-k').value;
const filtered=allKerusakan.filter(item=>{
const matchQ=!q||item.deskripsi.toLowerCase().includes(q)||(item.nama_jalan_ref||'').toLowerCase().includes(q);
const matchU=!urgensi||item.tingkat_urgensi===urgensi;
const matchS=!status||item.status_laporan===status;
return matchQ&&matchU&&matchS;
});
renderKerusakan(filtered);
}
function openEditKerusakan(id){
map.closePopup();
const item=allKerusakan.find(k=>k.id===id)||allKerusakan.find(k=>parseInt(k.id)===parseInt(id));
if(!item)return toast('Data tidak ditemukan','error');
document.getElementById('edit-k-id').value=item.id;
document.getElementById('edit-k-jalan').value=item.nama_jalan_ref||'';
document.getElementById('edit-k-deskripsi').value=item.deskripsi;
document.getElementById('edit-k-urgensi').value=item.tingkat_urgensi;
document.getElementById('edit-k-status').value=item.status_laporan;
document.getElementById('edit-k-catatan').value='';
document.getElementById('modal-kerusakan').classList.add('show');
}
function updateKerusakan(){
const id=document.getElementById('edit-k-id').value;
const jalan=document.getElementById('edit-k-jalan').value.trim();
const deskripsi=document.getElementById('edit-k-deskripsi').value.trim();
const urgensi=document.getElementById('edit-k-urgensi').value;
const statusBaru=document.getElementById('edit-k-status').value;
const catatan=document.getElementById('edit-k-catatan').value.trim();
const item=allKerusakan.find(k=>parseInt(k.id)===parseInt(id));
const statusLama=item?item.status_laporan:'';
// Update atribut
fetch('api/kerusakan.php',{
method:'PUT',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,nama_jalan_ref:jalan,deskripsi,tingkat_urgensi:urgensi})
}).then(r=>r.json()).then(res=>{
if(!res.success){toast('Gagal update atribut','error');return;}
// Update status jika berubah
if(statusBaru!==statusLama){
return fetch(`api/kerusakan.php?action=update_status`,{
method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({id,status_baru:statusBaru,catatan})
}).then(r=>r.json());
}
}).then(()=>{
toast('Data kerusakan diupdate!','success');
closeModal('modal-kerusakan');
loadKerusakan();
}).catch(()=>toast('Error koneksi','error'));
}
function hapusKerusakan(id){
map.closePopup();
if(!confirm('Hapus laporan kerusakan ini?'))return;
fetch('api/kerusakan.php',{method:'DELETE',headers:{'Content-Type':'application/json'},body:JSON.stringify({id})})
.then(r=>r.json()).then(res=>{
if(res.success){toast('Laporan kerusakan dihapus!','success');loadKerusakan();}
else toast(res.message,'error');
});
}
// ═══════════════════════════════════
// HISTORI KERUSAKAN
// ═══════════════════════════════════
function lihatHistori(id,jalan,deskripsi){
document.getElementById('histori-lokasi').innerHTML=`
<i class="fa-solid fa-road"></i> ${jalan||''} &nbsp;|&nbsp; <i class="fa-solid fa-info-circle"></i> ${deskripsi}`;
document.getElementById('histori-content').innerHTML='<div style="color:#64748b;font-size:.8rem">Memuat histori...</div>';
document.getElementById('modal-histori').classList.add('show');
fetch(`api/kerusakan.php?action=histori&id=${id}`)
.then(r=>r.json()).then(res=>{
const content=document.getElementById('histori-content');
if(!res.success||!res.data.length){
content.innerHTML='<div style="color:#64748b;font-size:.8rem;padding:8px">Belum ada histori perubahan status.</div>';
return;
}
const statusColors={Dilaporkan:'#94a3b8',Diverifikasi:'#38bdf8','Dalam Perbaikan':'#f59e0b','Sudah Diperbaiki':'#4ade80'};
content.innerHTML=res.data.map(h=>`
<div class="histori-item">
<div class="histori-dot" style="background:${statusColors[h.status_baru]||'#38bdf8'}"></div>
<div class="histori-content">
<div class="h-status">
<span style="color:#64748b">${h.status_lama}</span>
<i class="fa-solid fa-arrow-right" style="font-size:.7rem;margin:0 5px;color:#475569"></i>
<span style="color:${statusColors[h.status_baru]||'#38bdf8'}">${h.status_baru}</span>
</div>
<div class="h-time"><i class="fa-solid fa-clock"></i> ${fmtDate(h.tgl_perubahan)}</div>
${h.catatan?`<div class="h-catatan"><i class="fa-solid fa-comment"></i> ${h.catatan}</div>`:''}
</div>
</div>`).join('');
}).catch(()=>{
document.getElementById('histori-content').innerHTML='<div style="color:#f87171">Gagal memuat histori</div>';
});
}
// ═══════════════════════════════════
// DATA VISUALIZATION
// ═══════════════════════════════════
function renderBars(containerId,data,colors){
const total=Object.values(data).reduce((a,b)=>a+b,0)||1;
const container=document.getElementById(containerId);
container.innerHTML=Object.entries(data).map(([label,count])=>`
<div class="viz-bar-wrap">
<div class="viz-bar-label"><span>${label}</span><span>${count}</span></div>
<div class="viz-bar-bg">
<div class="viz-bar-fill" style="width:${Math.round(count/total*100)}%;background:${colors[label]||'#94a3b8'}"></div>
</div>
</div>`).join('');
}
function updateVizJalan(data){
const counts={Nasional:0,Provinsi:0,Kabupaten:0};
data.forEach(j=>counts[j.status_jalan]=(counts[j.status_jalan]||0)+1);
renderBars('viz-jalan-bars',counts,jalanColors);
}
function updateVizTanah(data){
const counts={SHM:0,HGB:0,HGU:0,HP:0};
data.forEach(t=>counts[t.status_tanah]=(counts[t.status_tanah]||0)+1);
renderBars('viz-tanah-bars',counts,tanahColors);
}
function updateVizKerusakan(data){
const counts={Kritis:0,Tinggi:0,Sedang:0,Rendah:0};
data.forEach(k=>counts[k.tingkat_urgensi]=(counts[k.tingkat_urgensi]||0)+1);
renderBars('viz-kerusakan-bars',counts,kerusakanColors);
}
// ═══════════════════════════════════
// UI
// ═══════════════════════════════════
function switchTab(tab){
document.querySelectorAll('.tab-content').forEach(el=>el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(el=>el.classList.remove('active'));
document.getElementById('tab-'+tab).classList.add('active');
document.querySelector('.tab-btn.'+tab).classList.add('active');
cancelDraw(); batalEdit(); cancelPickKerusakan();
}
function closeModal(id){ document.getElementById(id).classList.remove('show'); }
document.querySelectorAll('.modal-overlay').forEach(m=>{
m.addEventListener('click',function(e){ if(e.target===this)this.classList.remove('show'); });
});
// Geotagging: pencet Enter di search
document.getElementById('search-jalan').addEventListener('keydown',e=>{ if(e.key==='Enter') searchZoomJalan(); });
document.getElementById('search-tanah').addEventListener('keydown',e=>{ if(e.key==='Enter') searchZoomTanah(); });
loadJalan();
loadTanah();
loadKerusakan();
</script>
</body>
</html>