1466 lines
75 KiB
PHP
1466 lines
75 KiB
PHP
<!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 (m²)</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 (m²)</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||'–'} | <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> |