907 lines
46 KiB
HTML
907 lines
46 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebGIS - Jalan, Parsil & Jalan Rusak</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
|
|
<style>
|
|
*{margin:0;padding:0;box-sizing:border-box;}
|
|
body{font-family:'Segoe UI',sans-serif;background:#1a1d23;color:#e0e0e0;height:100vh;display:flex;flex-direction:column;}
|
|
|
|
/* HEADER */
|
|
header{background:#111318;padding:0 16px;height:56px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #2a2d35;z-index:1000;flex-shrink:0;gap:10px;}
|
|
.header-brand{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:700;color:#fff;white-space:nowrap;}
|
|
.header-brand i{color:#4ade80;font-size:18px;}
|
|
.header-tabs{display:flex;gap:3px;flex-shrink:0;}
|
|
.tab-btn{padding:5px 13px;border-radius:6px;border:none;cursor:pointer;font-size:12px;font-weight:500;transition:all .2s;background:transparent;color:#888;white-space:nowrap;}
|
|
.tab-btn.active{background:#2a5bd7;color:#fff;}
|
|
.tab-btn:hover:not(.active){background:#2a2d35;color:#ddd;}
|
|
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;vertical-align:middle;}
|
|
.dot-jalan{background:#f59e0b;}.dot-parsil{background:#4ade80;}.dot-rusak{background:#ef4444;}
|
|
.count-badge{background:#2a5bd733;color:#60a5fa;font-size:10px;padding:1px 6px;border-radius:10px;margin-left:5px;}
|
|
|
|
/* SEARCH BAR */
|
|
.search-wrap{flex:1;max-width:280px;position:relative;}
|
|
.search-wrap input{width:100%;padding:6px 10px 6px 32px;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;color:#e0e0e0;font-size:12px;outline:none;}
|
|
.search-wrap input:focus{border-color:#2a5bd7;}
|
|
.search-wrap i{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#555;font-size:12px;}
|
|
.search-wrap .btn-clear-s{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:#555;cursor:pointer;font-size:11px;display:none;}
|
|
.search-wrap input:not(:placeholder-shown) ~ .btn-clear-s{display:block;}
|
|
|
|
/* LAYOUT */
|
|
.main-layout{display:flex;flex:1;overflow:hidden;}
|
|
|
|
/* SIDEBAR */
|
|
.sidebar{width:320px;background:#111318;border-right:1px solid #2a2d35;display:flex;flex-direction:column;flex-shrink:0;overflow:hidden;}
|
|
.sidebar-header{padding:12px 14px;border-bottom:1px solid #2a2d35;display:flex;align-items:center;justify-content:space-between;}
|
|
.sidebar-title{font-size:12px;font-weight:600;color:#aaa;text-transform:uppercase;letter-spacing:.5px;}
|
|
.btn-tambah{background:#2a5bd7;color:#fff;border:none;border-radius:6px;padding:5px 11px;font-size:12px;cursor:pointer;display:flex;align-items:center;gap:4px;}
|
|
.btn-tambah:hover{background:#3467e8;}
|
|
|
|
/* LIST */
|
|
.data-list{flex:1;overflow-y:auto;padding:7px;}
|
|
.data-list::-webkit-scrollbar{width:3px;}
|
|
.data-list::-webkit-scrollbar-thumb{background:#333;border-radius:2px;}
|
|
.data-card{background:#1a1d23;border:1px solid #2a2d35;border-radius:8px;padding:9px 11px;margin-bottom:6px;cursor:pointer;transition:all .15s;}
|
|
.data-card:hover{border-color:#3d4150;background:#1e2128;}
|
|
.data-card.selected{border-color:#2a5bd7;background:#1a2540;}
|
|
.card-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;}
|
|
.card-name{font-size:13px;font-weight:600;color:#e0e0e0;flex:1;}
|
|
.card-badge{font-size:10px;padding:2px 6px;border-radius:4px;font-weight:600;white-space:nowrap;flex-shrink:0;}
|
|
.badge-nasional{background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44;}
|
|
.badge-provinsi{background:#05966922;color:#34d399;border:1px solid #05966944;}
|
|
.badge-kabupaten{background:#d9770622;color:#fbbf24;border:1px solid #d9770644;}
|
|
.badge-shm{background:#1d4ed822;color:#60a5fa;border:1px solid #1d4ed844;}
|
|
.badge-hgb{background:#7c3aed22;color:#a78bfa;border:1px solid #7c3aed44;}
|
|
.badge-hgu{background:#d9770622;color:#fbbf24;border:1px solid #d9770644;}
|
|
.badge-hp{background:#05966922;color:#34d399;border:1px solid #05966944;}
|
|
.badge-ringan{background:#16a34a22;color:#4ade80;border:1px solid #16a34a44;}
|
|
.badge-sedang{background:#d9770622;color:#fb923c;border:1px solid #d9770644;}
|
|
.badge-berat{background:#7f1d1d22;color:#f87171;border:1px solid #7f1d1d44;}
|
|
.card-meta{margin-top:4px;font-size:11px;color:#666;}
|
|
.card-meta span{margin-right:8px;}
|
|
.card-meta i{margin-right:3px;}
|
|
.card-thumb{width:100%;height:60px;object-fit:cover;border-radius:4px;margin-top:6px;border:1px solid #2a2d35;}
|
|
.card-actions{display:flex;gap:3px;margin-top:7px;}
|
|
.btn-sm{padding:4px 7px;border-radius:4px;border:none;cursor:pointer;font-size:11px;font-weight:500;transition:all .15s;}
|
|
.btn-zoom{background:#1e3a8a22;color:#60a5fa;border:1px solid #1e3a8a44;}
|
|
.btn-edit{background:#78350f22;color:#fbbf24;border:1px solid #78350f44;}
|
|
.btn-hapus{background:#7f1d1d22;color:#f87171;border:1px solid #7f1d1d44;}
|
|
.btn-sm:hover{filter:brightness(1.3);}
|
|
|
|
/* LEGEND */
|
|
.legend-box{padding:9px 14px;border-top:1px solid #2a2d35;font-size:11px;}
|
|
.legend-title{color:#888;margin-bottom:5px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;font-size:10px;}
|
|
.legend-item{display:flex;align-items:center;gap:5px;margin-bottom:3px;color:#aaa;}
|
|
.legend-line{width:20px;height:3px;border-radius:2px;}
|
|
.legend-poly{width:12px;height:12px;border-radius:2px;border:2px solid;}
|
|
.legend-point{width:10px;height:10px;border-radius:50%;border:2px solid;}
|
|
|
|
/* MAP */
|
|
#map{flex:1;}
|
|
|
|
/* MODAL */
|
|
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.75);z-index:9999;align-items:center;justify-content:center;}
|
|
.modal-overlay.show{display:flex;}
|
|
.modal{background:#111318;border:1px solid #2a2d35;border-radius:12px;padding:22px;width:440px;max-width:95vw;max-height:90vh;overflow-y:auto;}
|
|
.modal h3{font-size:15px;color:#fff;margin-bottom:16px;}
|
|
.form-group{margin-bottom:12px;}
|
|
.form-group label{display:block;font-size:11px;color:#888;margin-bottom:4px;font-weight:500;}
|
|
.form-group input,.form-group select,.form-group textarea{width:100%;padding:8px 10px;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;color:#e0e0e0;font-size:13px;outline:none;}
|
|
.form-group input:focus,.form-group select:focus,.form-group textarea:focus{border-color:#2a5bd7;}
|
|
.form-group input[type=file]{padding:5px;color:#aaa;cursor:pointer;}
|
|
.readonly-val{padding:8px 10px;background:#0d0f14;border:1px solid #1e2128;border-radius:6px;color:#4ade80;font-size:13px;font-weight:600;}
|
|
.modal-actions{display:flex;gap:8px;margin-top:16px;justify-content:flex-end;}
|
|
.btn-primary{background:#2a5bd7;color:#fff;border:none;border-radius:6px;padding:8px 16px;font-size:13px;cursor:pointer;}
|
|
.btn-primary:hover{background:#3467e8;}
|
|
.btn-cancel{background:#2a2d35;color:#aaa;border:none;border-radius:6px;padding:8px 16px;font-size:13px;cursor:pointer;}
|
|
.foto-preview-wrap{margin-top:6px;}
|
|
.foto-preview-wrap img{width:100%;max-height:140px;object-fit:cover;border-radius:6px;border:1px solid #2a2d35;}
|
|
.foto-current{font-size:11px;color:#60a5fa;margin-bottom:4px;}
|
|
|
|
/* DRAW INFO */
|
|
.draw-info{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);background:#111318dd;color:#e0e0e0;padding:8px 16px;border-radius:8px;font-size:12px;z-index:999;border:1px solid #2a5bd7;display:none;pointer-events:none;}
|
|
.draw-info.show{display:block;}
|
|
|
|
/* EMPTY STATE */
|
|
.empty-state{text-align:center;padding:28px 16px;color:#555;}
|
|
.empty-state i{font-size:26px;margin-bottom:7px;display:block;}
|
|
.empty-state p{font-size:12px;}
|
|
|
|
/* LEAFLET POPUP */
|
|
.leaflet-popup-content-wrapper{background:#111318;border:1px solid #2a2d35;border-radius:8px;color:#e0e0e0;}
|
|
.leaflet-popup-tip{background:#111318;}
|
|
.popup-title{font-weight:600;font-size:13px;margin-bottom:5px;color:#fff;}
|
|
.popup-row{font-size:11px;color:#aaa;margin-bottom:2px;}
|
|
.popup-row span{color:#e0e0e0;}
|
|
.popup-foto{width:100%;max-height:100px;object-fit:cover;border-radius:4px;margin-top:6px;}
|
|
|
|
/* TOAST */
|
|
.toast{position:fixed;bottom:24px;right:24px;background:#1e2a1e;border:1px solid #4ade80;color:#4ade80;padding:10px 18px;border-radius:8px;font-size:13px;z-index:99999;transform:translateY(60px);opacity:0;transition:all .3s;}
|
|
.toast.show{transform:translateY(0);opacity:1;}
|
|
.toast.error{background:#2a1e1e;border-color:#f87171;color:#f87171;}
|
|
|
|
/* STAT BADGE */
|
|
.stat-row{display:flex;gap:6px;padding:8px 14px;border-top:1px solid #2a2d35;flex-shrink:0;}
|
|
.stat-item{flex:1;background:#1a1d23;border:1px solid #2a2d35;border-radius:6px;padding:5px 8px;text-align:center;}
|
|
.stat-item .sv{font-size:16px;font-weight:700;color:#e0e0e0;}
|
|
.stat-item .sl{font-size:9px;color:#666;margin-top:1px;}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- HEADER -->
|
|
<header>
|
|
<div class="header-brand">
|
|
<i class="fas fa-map-marked-alt"></i>
|
|
WebGIS Jalan & Parsil
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="search-wrap">
|
|
<i class="fas fa-search"></i>
|
|
<input type="text" id="searchInput" placeholder="Cari data..." oninput="handleSearch(this.value)">
|
|
<button class="btn-clear-s" onclick="clearSearch()"><i class="fas fa-times"></i></button>
|
|
</div>
|
|
|
|
<div class="header-tabs">
|
|
<button class="tab-btn active" id="tabJalan" onclick="switchTab('jalan')">
|
|
<span class="dot dot-jalan"></span>Jalan
|
|
<span class="count-badge" id="countJalan">0</span>
|
|
</button>
|
|
<button class="tab-btn" id="tabParsil" onclick="switchTab('parsil')">
|
|
<span class="dot dot-parsil"></span>Parsil
|
|
<span class="count-badge" id="countParsil">0</span>
|
|
</button>
|
|
<button class="tab-btn" id="tabRusak" onclick="switchTab('rusak')">
|
|
<span class="dot dot-rusak"></span>Jalan Rusak
|
|
<span class="count-badge" id="countRusak">0</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="main-layout">
|
|
<!-- SIDEBAR -->
|
|
<div class="sidebar">
|
|
<div class="sidebar-header">
|
|
<span class="sidebar-title" id="sidebarTitle">Data Jalan</span>
|
|
<button class="btn-tambah" onclick="startDraw()">
|
|
<i class="fas fa-plus"></i> Tambah
|
|
</button>
|
|
</div>
|
|
|
|
<div class="data-list" id="listJalan"><div class="empty-state"><i class="fas fa-road"></i><p>Belum ada data jalan</p></div></div>
|
|
<div class="data-list" id="listParsil" style="display:none"><div class="empty-state"><i class="fas fa-map"></i><p>Belum ada data parsil</p></div></div>
|
|
<div class="data-list" id="listRusak" style="display:none"><div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>Belum ada data jalan rusak</p></div></div>
|
|
|
|
<!-- STAT -->
|
|
<div class="stat-row" id="statRow">
|
|
<div class="stat-item"><div class="sv" id="statJalan">0</div><div class="sl">Jalan</div></div>
|
|
<div class="stat-item"><div class="sv" id="statParsil">0</div><div class="sl">Parsil</div></div>
|
|
<div class="stat-item"><div class="sv" id="statRusak">0</div><div class="sl">Rusak</div></div>
|
|
</div>
|
|
|
|
<!-- LEGEND -->
|
|
<div class="legend-box" id="legendJalan">
|
|
<div class="legend-title">Keterangan Jalan</div>
|
|
<div class="legend-item"><div class="legend-line" style="background:#a78bfa"></div> Nasional</div>
|
|
<div class="legend-item"><div class="legend-line" style="background:#34d399"></div> Provinsi</div>
|
|
<div class="legend-item"><div class="legend-line" style="background:#fbbf24"></div> Kabupaten</div>
|
|
</div>
|
|
<div class="legend-box" id="legendParsil" style="display:none">
|
|
<div class="legend-title">Status Kepemilikan</div>
|
|
<div class="legend-item"><div class="legend-poly" style="background:#1d4ed822;border-color:#60a5fa"></div> SHM</div>
|
|
<div class="legend-item"><div class="legend-poly" style="background:#7c3aed22;border-color:#a78bfa"></div> HGB</div>
|
|
<div class="legend-item"><div class="legend-poly" style="background:#d9770622;border-color:#fbbf24"></div> HGU</div>
|
|
<div class="legend-item"><div class="legend-poly" style="background:#05966922;border-color:#34d399"></div> HP</div>
|
|
</div>
|
|
<div class="legend-box" id="legendRusak" style="display:none">
|
|
<div class="legend-title">Tingkat Kerusakan</div>
|
|
<div class="legend-item"><div class="legend-point" style="background:#4ade8044;border-color:#4ade80"></div> Ringan</div>
|
|
<div class="legend-item"><div class="legend-point" style="background:#fb923c44;border-color:#fb923c"></div> Sedang</div>
|
|
<div class="legend-item"><div class="legend-point" style="background:#f8717144;border-color:#f87171"></div> Berat</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="map"></div>
|
|
</div>
|
|
|
|
<!-- draw hint -->
|
|
<div class="draw-info" id="drawInfo">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span id="drawInfoText">Klik pada peta untuk menggambar.</span>
|
|
</div>
|
|
|
|
<!-- TOAST -->
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<!-- ===================== MODAL JALAN ===================== -->
|
|
<div class="modal-overlay" id="modalJalan">
|
|
<div class="modal">
|
|
<h3 id="modalJalanTitle"><i class="fas fa-road" style="color:#fbbf24;margin-right:8px"></i>Data Jalan</h3>
|
|
<div class="form-group"><label>Nama Jalan *</label><input type="text" id="jNama" placeholder="Contoh: Jl. Sudirman"></div>
|
|
<div class="form-group"><label>Status Jalan *</label>
|
|
<select id="jStatus"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select>
|
|
</div>
|
|
<div class="form-group"><label><i class="fas fa-ruler" style="margin-right:4px"></i>Panjang (otomatis)</label><div class="readonly-val" id="jPanjang">- meter</div></div>
|
|
<div class="form-group"><label>Keterangan</label><textarea id="jKet" rows="2" placeholder="Keterangan tambahan..."></textarea></div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Jalan</label>
|
|
<div class="foto-current" id="jFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
|
<input type="file" id="jFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'jFotoPreview')">
|
|
<div class="foto-preview-wrap" id="jFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
|
<button class="btn-primary" onclick="saveJalan()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===================== MODAL PARSIL ===================== -->
|
|
<div class="modal-overlay" id="modalParsil">
|
|
<div class="modal">
|
|
<h3 id="modalParsilTitle"><i class="fas fa-map" style="color:#4ade80;margin-right:8px"></i>Data Parsil Tanah</h3>
|
|
<div class="form-group"><label>Nama Pemilik *</label><input type="text" id="pNama" placeholder="Nama lengkap pemilik"></div>
|
|
<div class="form-group"><label>Nomor Sertifikat *</label><input type="text" id="pNoSert" placeholder="Contoh: SHM-001/KEL/2024"></div>
|
|
<div class="form-group"><label>Status Kepemilikan *</label>
|
|
<select id="pStatus"><option value="SHM">SHM - Hak Milik</option><option value="HGB">HGB - Hak Guna Bangunan</option><option value="HGU">HGU - Hak Guna Usaha</option><option value="HP">HP - Hak Pakai</option></select>
|
|
</div>
|
|
<div class="form-group"><label><i class="fas fa-vector-square" style="margin-right:4px"></i>Luas (otomatis)</label><div class="readonly-val" id="pLuas">- m²</div></div>
|
|
<div class="form-group"><label>Alamat</label><textarea id="pAlamat" rows="2" placeholder="Alamat lokasi parsil..."></textarea></div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Parsil</label>
|
|
<div class="foto-current" id="pFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
|
<input type="file" id="pFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'pFotoPreview')">
|
|
<div class="foto-preview-wrap" id="pFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
|
<button class="btn-primary" onclick="saveParsil()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===================== MODAL JALAN RUSAK ===================== -->
|
|
<div class="modal-overlay" id="modalRusak">
|
|
<div class="modal">
|
|
<h3 id="modalRusakTitle"><i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Data Jalan Rusak</h3>
|
|
<div class="form-group"><label>Nama / Lokasi *</label><input type="text" id="rLokasi" placeholder="Contoh: Depan SPBU Jl. Ahmad Yani"></div>
|
|
<div class="form-group"><label>Tingkat Kerusakan *</label>
|
|
<select id="rTingkat">
|
|
<option value="Ringan">Ringan</option>
|
|
<option value="Sedang" selected>Sedang</option>
|
|
<option value="Berat">Berat</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group"><label>Jenis Kerusakan</label><input type="text" id="rJenis" placeholder="Berlubang / Retak / Amblas..."></div>
|
|
<div class="form-group"><label>Tanggal Lapor</label><input type="date" id="rTanggal"></div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-map-pin" style="margin-right:4px"></i>Koordinat (otomatis dari peta)</label>
|
|
<div style="display:flex;gap:8px;align-items:center">
|
|
<div class="readonly-val" id="rKoord" style="flex:1">- klik titik di peta -</div>
|
|
<button type="button" onclick="pickTitikPeta()" style="background:#1e3a8a;color:#60a5fa;border:1px solid #1e3a8a44;border-radius:6px;padding:7px 10px;font-size:12px;cursor:pointer;white-space:nowrap;flex-shrink:0"><i class="fas fa-map-marker-alt"></i> Pilih di Peta</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group"><label>Keterangan</label><textarea id="rKet" rows="2" placeholder="Deskripsi kerusakan..."></textarea></div>
|
|
<div class="form-group">
|
|
<label><i class="fas fa-camera" style="margin-right:4px"></i>Foto Kerusakan</label>
|
|
<div class="foto-current" id="rFotoCurrent" style="display:none"><i class="fas fa-image"></i> Foto saat ini tersimpan</div>
|
|
<input type="file" id="rFoto" accept="image/jpeg,image/png,image/webp" onchange="previewFoto(this,'rFotoPreview')">
|
|
<div class="foto-preview-wrap" id="rFotoPreview" style="display:none"><img src="" alt="preview"></div>
|
|
</div>
|
|
<div class="modal-actions">
|
|
<button class="btn-cancel" onclick="closeModal()">Batal</button>
|
|
<button class="btn-primary" onclick="saveRusak()"><i class="fas fa-save" style="margin-right:5px"></i>Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JS LIBRARIES -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
|
|
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
|
|
|
<script>
|
|
// =============================================================
|
|
// 1. INIT MAP
|
|
// =============================================================
|
|
const map = L.map('map').setView([-0.02, 109.34], 13);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OpenStreetMap contributors'}).addTo(map);
|
|
|
|
const layerJalan = L.layerGroup().addTo(map);
|
|
const layerParsil = L.layerGroup().addTo(map);
|
|
const layerRusak = L.layerGroup().addTo(map);
|
|
|
|
function parseGeojsonSafe(raw){
|
|
try{
|
|
const gj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
if(!gj || !gj.geometry || !gj.geometry.type) return null;
|
|
return gj;
|
|
}catch(e){
|
|
console.warn('GeoJSON tidak valid:', raw);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// =============================================================
|
|
// 2. WARNA & IKON
|
|
// =============================================================
|
|
const WARNA_JALAN = {
|
|
'Nasional': {color:'#a78bfa',weight:5,opacity:.9},
|
|
'Provinsi': {color:'#34d399',weight:4,opacity:.9},
|
|
'Kabupaten':{color:'#fbbf24',weight:3,opacity:.9}
|
|
};
|
|
const WARNA_PARSIL = {
|
|
'SHM':{color:'#60a5fa',fillColor:'#1d4ed8',fillOpacity:.25,weight:2},
|
|
'HGB':{color:'#a78bfa',fillColor:'#7c3aed',fillOpacity:.25,weight:2},
|
|
'HGU':{color:'#fbbf24',fillColor:'#d97706',fillOpacity:.25,weight:2},
|
|
'HP': {color:'#34d399',fillColor:'#059669',fillOpacity:.25,weight:2}
|
|
};
|
|
const WARNA_RUSAK = {
|
|
'Ringan':{color:'#4ade80',radius:8,fillColor:'#166534',fillOpacity:.8,weight:2},
|
|
'Sedang':{color:'#fb923c',radius:10,fillColor:'#c2410c',fillOpacity:.8,weight:2},
|
|
'Berat': {color:'#f87171',radius:12,fillColor:'#991b1b',fillOpacity:.8,weight:2}
|
|
};
|
|
|
|
// =============================================================
|
|
// 3. STATE
|
|
// =============================================================
|
|
let currentTab = 'jalan';
|
|
let editMode = false;
|
|
let editId = null;
|
|
let tempGeojson = null;
|
|
let tempPanjang = 0;
|
|
let tempLuas = 0;
|
|
let searchTimer = null;
|
|
|
|
const drawControl = new L.Control.Draw({
|
|
draw:{
|
|
polyline:{shapeOptions:{color:'#2a5bd7',weight:3}},
|
|
polygon: {shapeOptions:{color:'#4ade80',fillOpacity:.2}},
|
|
marker: {icon: L.divIcon({className:'',html:'<div style="width:18px;height:18px;border-radius:50%;background:#ef4444;border:3px solid #fff;box-shadow:0 2px 6px #0005"></div>',iconSize:[18,18],iconAnchor:[9,9]})},
|
|
rectangle:false, circle:false, circlemarker:false
|
|
},
|
|
edit:false
|
|
});
|
|
|
|
// =============================================================
|
|
// 4. TOAST
|
|
// =============================================================
|
|
function toast(msg, type='ok'){
|
|
const t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.className = 'toast show' + (type==='error'?' error':'');
|
|
clearTimeout(t._timer);
|
|
t._timer = setTimeout(()=>t.classList.remove('show'), 3000);
|
|
}
|
|
|
|
// =============================================================
|
|
// 5. TAB SWITCH
|
|
// =============================================================
|
|
function switchTab(tab){
|
|
currentTab = tab;
|
|
['jalan','parsil','rusak'].forEach(t=>{
|
|
document.getElementById('tab'+cap(t)).classList.toggle('active', t===tab);
|
|
document.getElementById('list'+cap(t)).style.display = t===tab?'block':'none';
|
|
document.getElementById('legend'+cap(t)).style.display = t===tab?'block':'none';
|
|
});
|
|
document.getElementById('sidebarTitle').textContent =
|
|
tab==='jalan'?'Data Jalan':tab==='parsil'?'Parsil Tanah':'Jalan Rusak';
|
|
// clear search
|
|
document.getElementById('searchInput').value = '';
|
|
}
|
|
function cap(s){return s.charAt(0).toUpperCase()+s.slice(1);}
|
|
|
|
// =============================================================
|
|
// 6. SEARCH
|
|
// =============================================================
|
|
function handleSearch(q){
|
|
clearTimeout(searchTimer);
|
|
searchTimer = setTimeout(()=>{
|
|
if(q.trim()===''){
|
|
if(currentTab==='jalan') loadJalan();
|
|
else if(currentTab==='parsil') loadParsil();
|
|
else loadRusak();
|
|
} else {
|
|
if(currentTab==='jalan') loadJalan(q);
|
|
else if(currentTab==='parsil') loadParsil(q);
|
|
else loadRusak(q);
|
|
}
|
|
}, 300);
|
|
}
|
|
function clearSearch(){
|
|
document.getElementById('searchInput').value='';
|
|
handleSearch('');
|
|
}
|
|
|
|
// =============================================================
|
|
// 7. DRAW
|
|
// =============================================================
|
|
function pickTitikPeta(){
|
|
// Hide modal, activate marker draw, then re-show modal after click
|
|
document.getElementById('modalRusak').classList.remove('show');
|
|
map.addControl(drawControl);
|
|
const info = document.getElementById('drawInfo');
|
|
document.getElementById('drawInfoText').textContent = 'Klik lokasi jalan rusak di peta.';
|
|
info.classList.add('show');
|
|
new L.Draw.Marker(map, drawControl.options.draw.marker).enable();
|
|
}
|
|
|
|
function startDraw(){
|
|
editMode=false; editId=null;
|
|
map.addControl(drawControl);
|
|
const info=document.getElementById('drawInfo');
|
|
const txt=document.getElementById('drawInfoText');
|
|
if(currentTab==='jalan'){
|
|
new L.Draw.Polyline(map,drawControl.options.draw.polyline).enable();
|
|
txt.textContent='Klik untuk menambah titik jalan. Double-klik untuk selesai.';
|
|
} else if(currentTab==='parsil'){
|
|
new L.Draw.Polygon(map,drawControl.options.draw.polygon).enable();
|
|
txt.textContent='Klik untuk menggambar batas parsil. Klik titik awal untuk menutup.';
|
|
} else {
|
|
new L.Draw.Marker(map,drawControl.options.draw.marker).enable();
|
|
txt.textContent='Klik lokasi jalan rusak di peta.';
|
|
}
|
|
info.classList.add('show');
|
|
}
|
|
|
|
map.on(L.Draw.Event.CREATED, function(e){
|
|
document.getElementById('drawInfo').classList.remove('show');
|
|
map.removeControl(drawControl);
|
|
const layer = e.layer;
|
|
const geoJSON = layer.toGeoJSON();
|
|
tempGeojson = JSON.stringify(geoJSON);
|
|
|
|
if(currentTab==='jalan' && e.layerType==='polyline'){
|
|
const lls = layer.getLatLngs();
|
|
tempPanjang = 0;
|
|
for(let i=0;i<lls.length-1;i++) tempPanjang+=lls[i].distanceTo(lls[i+1]);
|
|
document.getElementById('jPanjang').textContent = tempPanjang.toFixed(2)+' meter';
|
|
openModalJalan(false);
|
|
} else if(currentTab==='parsil' && e.layerType==='polygon'){
|
|
tempLuas = turf.area(geoJSON);
|
|
document.getElementById('pLuas').textContent = tempLuas.toFixed(2)+' m²';
|
|
openModalParsil(false);
|
|
} else if(currentTab==='rusak' && e.layerType==='marker'){
|
|
const ll = layer.getLatLng();
|
|
tempGeojson = JSON.stringify(layer.toGeoJSON());
|
|
document.getElementById('rKoord').textContent = ll.lat.toFixed(6)+', '+ll.lng.toFixed(6);
|
|
// If modal already open (re-pick mode), just update coords and re-show
|
|
document.getElementById('modalRusak').classList.add('show');
|
|
if(!editMode){
|
|
// fill fresh form
|
|
document.getElementById('rLokasi').value='';
|
|
document.getElementById('rTingkat').value='Sedang';
|
|
document.getElementById('rJenis').value='';
|
|
document.getElementById('rKet').value='';
|
|
document.getElementById('rTanggal').value=new Date().toISOString().slice(0,10);
|
|
resetFoto('rFoto','rFotoPreview','rFotoCurrent');
|
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Tambah Jalan Rusak';
|
|
}
|
|
}
|
|
});
|
|
|
|
// =============================================================
|
|
// 8. FOTO PREVIEW
|
|
// =============================================================
|
|
function previewFoto(input, previewId){
|
|
const wrap = document.getElementById(previewId);
|
|
const img = wrap.querySelector('img');
|
|
if(input.files && input.files[0]){
|
|
img.src = URL.createObjectURL(input.files[0]);
|
|
wrap.style.display='block';
|
|
} else {
|
|
wrap.style.display='none';
|
|
}
|
|
}
|
|
|
|
// =============================================================
|
|
// 9. MODAL OPEN / CLOSE
|
|
// =============================================================
|
|
function openModalJalan(isEdit, data=null){
|
|
resetFoto('jFoto','jFotoPreview','jFotoCurrent');
|
|
if(isEdit && data){
|
|
document.getElementById('modalJalanTitle').innerHTML='<i class="fas fa-edit" style="color:#fbbf24;margin-right:8px"></i>Edit Data Jalan';
|
|
document.getElementById('jNama').value=data.nama_jalan;
|
|
document.getElementById('jStatus').value=data.status_jalan;
|
|
document.getElementById('jPanjang').textContent=parseFloat(data.panjang_meter).toFixed(2)+' meter';
|
|
document.getElementById('jKet').value=data.keterangan||'';
|
|
if(data.foto) document.getElementById('jFotoCurrent').style.display='block';
|
|
tempGeojson=data.geojson; tempPanjang=parseFloat(data.panjang_meter);
|
|
editMode=true; editId=data.id;
|
|
} else {
|
|
document.getElementById('modalJalanTitle').innerHTML='<i class="fas fa-road" style="color:#fbbf24;margin-right:8px"></i>Tambah Data Jalan';
|
|
document.getElementById('jNama').value='';
|
|
document.getElementById('jStatus').value='Nasional';
|
|
document.getElementById('jKet').value='';
|
|
editMode=false; editId=null;
|
|
}
|
|
document.getElementById('modalJalan').classList.add('show');
|
|
}
|
|
|
|
function openModalParsil(isEdit, data=null){
|
|
resetFoto('pFoto','pFotoPreview','pFotoCurrent');
|
|
if(isEdit && data){
|
|
document.getElementById('modalParsilTitle').innerHTML='<i class="fas fa-edit" style="color:#4ade80;margin-right:8px"></i>Edit Data Parsil';
|
|
document.getElementById('pNama').value=data.nama_pemilik;
|
|
document.getElementById('pNoSert').value=data.no_sertifikat;
|
|
document.getElementById('pStatus').value=data.status_kepemilikan;
|
|
document.getElementById('pLuas').textContent=parseFloat(data.luas_m2).toFixed(2)+' m²';
|
|
document.getElementById('pAlamat').value=data.alamat||'';
|
|
if(data.foto) document.getElementById('pFotoCurrent').style.display='block';
|
|
tempGeojson=data.geojson; tempLuas=parseFloat(data.luas_m2);
|
|
editMode=true; editId=data.id;
|
|
} else {
|
|
document.getElementById('modalParsilTitle').innerHTML='<i class="fas fa-map" style="color:#4ade80;margin-right:8px"></i>Tambah Parsil Tanah';
|
|
document.getElementById('pNama').value='';
|
|
document.getElementById('pNoSert').value='';
|
|
document.getElementById('pStatus').value='SHM';
|
|
document.getElementById('pAlamat').value='';
|
|
editMode=false; editId=null;
|
|
}
|
|
document.getElementById('modalParsil').classList.add('show');
|
|
}
|
|
|
|
function openModalRusak(isEdit, data=null){
|
|
resetFoto('rFoto','rFotoPreview','rFotoCurrent');
|
|
document.getElementById('rTanggal').value=new Date().toISOString().slice(0,10);
|
|
if(isEdit && data){
|
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-edit" style="color:#ef4444;margin-right:8px"></i>Edit Jalan Rusak';
|
|
document.getElementById('rLokasi').value=data.lokasi;
|
|
document.getElementById('rTingkat').value=data.tingkat;
|
|
document.getElementById('rJenis').value=data.jenis||'';
|
|
document.getElementById('rTanggal').value=data.tanggal_lapor||'';
|
|
document.getElementById('rKet').value=data.keterangan||'';
|
|
if(data.foto) document.getElementById('rFotoCurrent').style.display='block';
|
|
// parse geojson to show coords
|
|
try{
|
|
const gj=JSON.parse(data.geojson);
|
|
const c=gj.geometry.coordinates;
|
|
document.getElementById('rKoord').textContent=c[1].toFixed(6)+', '+c[0].toFixed(6);
|
|
}catch(e){}
|
|
tempGeojson=data.geojson;
|
|
editMode=true; editId=data.id;
|
|
} else {
|
|
document.getElementById('modalRusakTitle').innerHTML='<i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:8px"></i>Tambah Jalan Rusak';
|
|
document.getElementById('rLokasi').value='';
|
|
document.getElementById('rTingkat').value='Sedang';
|
|
document.getElementById('rJenis').value='';
|
|
document.getElementById('rKet').value='';
|
|
document.getElementById('rKoord').textContent='- klik titik di peta -';
|
|
editMode=false; editId=null;
|
|
}
|
|
document.getElementById('modalRusak').classList.add('show');
|
|
}
|
|
|
|
function closeModal(){
|
|
['modalJalan','modalParsil','modalRusak'].forEach(id=>document.getElementById(id).classList.remove('show'));
|
|
tempGeojson=null;
|
|
}
|
|
|
|
function resetFoto(inputId, previewId, currentId){
|
|
document.getElementById(inputId).value='';
|
|
document.getElementById(previewId).style.display='none';
|
|
document.getElementById(currentId).style.display='none';
|
|
}
|
|
|
|
// =============================================================
|
|
// 10. SAVE — JALAN (multipart/form-data)
|
|
// =============================================================
|
|
async function saveJalan(){
|
|
const nama = document.getElementById('jNama').value.trim();
|
|
if(!nama){ toast('Nama jalan wajib diisi!','error'); return; }
|
|
if(!tempGeojson && !editMode){ toast('Gambar polyline di peta terlebih dahulu!','error'); return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append('nama_jalan', nama);
|
|
fd.append('status_jalan', document.getElementById('jStatus').value);
|
|
fd.append('panjang_meter', tempPanjang);
|
|
fd.append('geojson', tempGeojson||'');
|
|
fd.append('keterangan', document.getElementById('jKet').value.trim());
|
|
const fotoFile = document.getElementById('jFoto').files[0];
|
|
if(fotoFile) fd.append('foto', fotoFile);
|
|
|
|
let url='jalan.php', method='POST';
|
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
|
|
|
try{
|
|
const res = await fetch(url,{method,body:fd});
|
|
const data = await res.json();
|
|
if(data.status==='ok'){ closeModal(); loadJalan(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
|
}
|
|
|
|
// =============================================================
|
|
// 11. SAVE — PARSIL
|
|
// =============================================================
|
|
async function saveParsil(){
|
|
const nama = document.getElementById('pNama').value.trim();
|
|
const noSert = document.getElementById('pNoSert').value.trim();
|
|
if(!nama){ toast('Nama pemilik wajib diisi!','error'); return; }
|
|
if(!noSert){ toast('Nomor sertifikat wajib diisi!','error'); return; }
|
|
if(!tempGeojson && !editMode){ toast('Gambar polygon di peta terlebih dahulu!','error'); return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append('nama_pemilik', nama);
|
|
fd.append('no_sertifikat', noSert);
|
|
fd.append('status_kepemilikan', document.getElementById('pStatus').value);
|
|
fd.append('luas_m2', tempLuas);
|
|
fd.append('geojson', tempGeojson||'');
|
|
fd.append('alamat', document.getElementById('pAlamat').value.trim());
|
|
const fotoFile = document.getElementById('pFoto').files[0];
|
|
if(fotoFile) fd.append('foto', fotoFile);
|
|
|
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
|
|
|
try{
|
|
const res = await fetch('parsil.php',{method:'POST',body:fd});
|
|
const data = await res.json();
|
|
if(data.status==='ok'){ closeModal(); loadParsil(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
|
}
|
|
|
|
// =============================================================
|
|
// 12. SAVE — JALAN RUSAK
|
|
// =============================================================
|
|
async function saveRusak(){
|
|
const lokasi = document.getElementById('rLokasi').value.trim();
|
|
if(!lokasi){ toast('Lokasi wajib diisi!','error'); return; }
|
|
if(!tempGeojson){ toast('Klik titik lokasi di peta terlebih dahulu!','error'); return; }
|
|
|
|
const fd = new FormData();
|
|
fd.append('lokasi', lokasi);
|
|
fd.append('tingkat', document.getElementById('rTingkat').value);
|
|
fd.append('jenis', document.getElementById('rJenis').value.trim());
|
|
fd.append('tanggal_lapor', document.getElementById('rTanggal').value);
|
|
fd.append('geojson', tempGeojson);
|
|
fd.append('keterangan', document.getElementById('rKet').value.trim());
|
|
const fotoFile = document.getElementById('rFoto').files[0];
|
|
if(fotoFile) fd.append('foto', fotoFile);
|
|
|
|
if(editMode){ fd.append('id', editId); fd.append('_method','PUT'); }
|
|
|
|
try{
|
|
const res = await fetch('jalan_rusak.php',{method:'POST',body:fd});
|
|
const data = await res.json();
|
|
if(data.status==='ok'){ closeModal(); loadRusak(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}catch(e){ toast('Gagal konek ke server','error'); }
|
|
}
|
|
|
|
// =============================================================
|
|
// 13. LOAD — JALAN
|
|
// =============================================================
|
|
async function loadJalan(q=''){
|
|
let res,json;
|
|
try{
|
|
const url = q ? `jalan.php?q=${encodeURIComponent(q)}` : 'jalan.php';
|
|
res = await fetch(url);
|
|
json = await res.json();
|
|
}catch(err){
|
|
document.getElementById('listJalan').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server. Pastikan XAMPP menyala & database sudah diimport.</p></div>';
|
|
return;
|
|
}
|
|
if(json.status==='error'){
|
|
document.getElementById('listJalan').innerHTML=`<div class="empty-state"><i class="fas fa-database" style="color:#f87171"></i><p style="color:#f87171">Error DB: ${json.message}</p></div>`;
|
|
return;
|
|
}
|
|
const {data} = json;
|
|
document.getElementById('countJalan').textContent = data.length;
|
|
document.getElementById('statJalan').textContent = data.length;
|
|
layerJalan.clearLayers();
|
|
const list = document.getElementById('listJalan');
|
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-road"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data jalan')+'</p></div>'; return; }
|
|
list.innerHTML='';
|
|
data.forEach(item=>{
|
|
const gj = parseGeojsonSafe(item.geojson);
|
|
const style = WARNA_JALAN[item.status_jalan]||WARNA_JALAN['Kabupaten'];
|
|
let layer = null;
|
|
if(gj){
|
|
layer = L.geoJSON(gj,{style}).addTo(layerJalan);
|
|
layer.bindPopup(`
|
|
<div class="popup-title">${item.nama_jalan}</div>
|
|
<div class="popup-row">Status: <span>${item.status_jalan}</span></div>
|
|
<div class="popup-row">Panjang: <span>${parseFloat(item.panjang_meter).toFixed(2)} m</span></div>
|
|
${item.keterangan?`<div class="popup-row">Ket: <span>${item.keterangan}</span></div>`:''}
|
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
|
`);
|
|
}
|
|
const card = document.createElement('div');
|
|
card.className='data-card';
|
|
card.innerHTML=`
|
|
<div class="card-top">
|
|
<div class="card-name">${item.nama_jalan}</div>
|
|
<div class="card-badge badge-${item.status_jalan.toLowerCase()}">${item.status_jalan}</div>
|
|
</div>
|
|
<div class="card-meta"><span><i class="fas fa-ruler"></i>${parseFloat(item.panjang_meter).toFixed(0)} m</span></div>
|
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
|
<div class="card-actions">
|
|
<button class="btn-sm btn-zoom" ${layer ? `onclick="zoomJalan${item.id}()"` : 'disabled title="Lokasi belum valid"'}><i class="fas fa-search-location"></i> Zoom</button>
|
|
<button class="btn-sm btn-edit" onclick="editJalan(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
|
<button class="btn-sm btn-hapus" onclick="hapusJalan(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>`;
|
|
list.appendChild(card);
|
|
if(layer) window[`zoomJalan${item.id}`]=()=>map.fitBounds(layer.getBounds(),{padding:[50,50]});
|
|
});
|
|
}
|
|
|
|
// =============================================================
|
|
// 14. LOAD — PARSIL
|
|
// =============================================================
|
|
async function loadParsil(q=''){
|
|
let res,json;
|
|
try{
|
|
const url = q ? `parsil.php?q=${encodeURIComponent(q)}` : 'parsil.php';
|
|
res = await fetch(url);
|
|
json = await res.json();
|
|
}catch(err){
|
|
document.getElementById('listParsil').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server.</p></div>';
|
|
return;
|
|
}
|
|
if(json.status==='error'){ document.getElementById('listParsil').innerHTML=`<div class="empty-state"><p style="color:#f87171">Error DB: ${json.message}</p></div>`; return; }
|
|
const {data} = json;
|
|
document.getElementById('countParsil').textContent = data.length;
|
|
document.getElementById('statParsil').textContent = data.length;
|
|
layerParsil.clearLayers();
|
|
const list = document.getElementById('listParsil');
|
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-map"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data parsil')+'</p></div>'; return; }
|
|
list.innerHTML='';
|
|
data.forEach(item=>{
|
|
const gj = parseGeojsonSafe(item.geojson);
|
|
const style = WARNA_PARSIL[item.status_kepemilikan]||WARNA_PARSIL['SHM'];
|
|
let layer = null;
|
|
if(gj){
|
|
layer = L.geoJSON(gj,{style}).addTo(layerParsil);
|
|
layer.bindPopup(`
|
|
<div class="popup-title">${item.nama_pemilik}</div>
|
|
<div class="popup-row">No. Sertifikat: <span>${item.no_sertifikat}</span></div>
|
|
<div class="popup-row">Status: <span>${item.status_kepemilikan}</span></div>
|
|
<div class="popup-row">Luas: <span>${parseFloat(item.luas_m2).toFixed(2)} m²</span></div>
|
|
${item.alamat?`<div class="popup-row">Alamat: <span>${item.alamat}</span></div>`:''}
|
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
|
`);
|
|
}
|
|
const card = document.createElement('div');
|
|
card.className='data-card';
|
|
card.innerHTML=`
|
|
<div class="card-top">
|
|
<div class="card-name">${item.nama_pemilik}</div>
|
|
<div class="card-badge badge-${item.status_kepemilikan.toLowerCase()}">${item.status_kepemilikan}</div>
|
|
</div>
|
|
<div class="card-meta">
|
|
<span><i class="fas fa-vector-square"></i>${parseFloat(item.luas_m2).toFixed(0)} m²</span>
|
|
<span><i class="fas fa-certificate"></i>${item.no_sertifikat}</span>
|
|
</div>
|
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
|
<div class="card-actions">
|
|
<button class="btn-sm btn-zoom" ${layer ? `onclick="zoomParsil${item.id}()"` : 'disabled title="Lokasi belum valid"'}><i class="fas fa-search-location"></i> Zoom</button>
|
|
<button class="btn-sm btn-edit" onclick="editParsil(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
|
<button class="btn-sm btn-hapus" onclick="hapusParsil(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>`;
|
|
list.appendChild(card);
|
|
if(layer) window[`zoomParsil${item.id}`]=()=>map.fitBounds(layer.getBounds(),{padding:[50,50]});
|
|
});
|
|
}
|
|
|
|
// =============================================================
|
|
// 15. LOAD — JALAN RUSAK
|
|
// =============================================================
|
|
async function loadRusak(q=''){
|
|
let res,json;
|
|
try{
|
|
const url = q ? `jalan_rusak.php?q=${encodeURIComponent(q)}` : 'jalan_rusak.php';
|
|
res = await fetch(url);
|
|
json = await res.json();
|
|
}catch(err){
|
|
document.getElementById('listRusak').innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle" style="color:#f87171"></i><p style="color:#f87171">Gagal konek ke server.</p></div>';
|
|
return;
|
|
}
|
|
if(json.status==='error'){ document.getElementById('listRusak').innerHTML=`<div class="empty-state"><p style="color:#f87171">Error DB: ${json.message}</p></div>`; return; }
|
|
const {data} = json;
|
|
document.getElementById('countRusak').textContent = data.length;
|
|
document.getElementById('statRusak').textContent = data.length;
|
|
layerRusak.clearLayers();
|
|
const list = document.getElementById('listRusak');
|
|
if(!data.length){ list.innerHTML='<div class="empty-state"><i class="fas fa-exclamation-triangle"></i><p>'+(q?'Tidak ada hasil pencarian':'Belum ada data jalan rusak')+'</p></div>'; return; }
|
|
list.innerHTML='';
|
|
data.forEach(item=>{
|
|
const gj = parseGeojsonSafe(item.geojson);
|
|
const cfg = WARNA_RUSAK[item.tingkat]||WARNA_RUSAK['Sedang'];
|
|
const coord = gj && Array.isArray(gj.geometry.coordinates) ? gj.geometry.coordinates : null; // [lng, lat]
|
|
let layer = null;
|
|
if(coord){
|
|
layer = L.circleMarker([coord[1],coord[0]], cfg).addTo(layerRusak);
|
|
layer.bindPopup(`
|
|
<div class="popup-title"><i class="fas fa-exclamation-triangle" style="color:#ef4444;margin-right:5px"></i>${item.lokasi}</div>
|
|
<div class="popup-row">Tingkat: <span style="color:${cfg.color};font-weight:700">${item.tingkat}</span></div>
|
|
${item.jenis?`<div class="popup-row">Jenis: <span>${item.jenis}</span></div>`:''}
|
|
<div class="popup-row">Dilaporkan: <span>${item.tanggal_lapor||'-'}</span></div>
|
|
${item.keterangan?`<div class="popup-row">Ket: <span>${item.keterangan}</span></div>`:''}
|
|
${item.foto?`<img class="popup-foto" src="${item.foto}" alt="foto">`:''}
|
|
`);
|
|
}
|
|
const card = document.createElement('div');
|
|
card.className='data-card';
|
|
card.innerHTML=`
|
|
<div class="card-top">
|
|
<div class="card-name">${item.lokasi}</div>
|
|
<div class="card-badge badge-${item.tingkat.toLowerCase()}">${item.tingkat}</div>
|
|
</div>
|
|
<div class="card-meta">
|
|
${item.jenis?`<span><i class="fas fa-tools"></i>${item.jenis}</span>`:''}
|
|
<span><i class="fas fa-calendar"></i>${item.tanggal_lapor||'-'}</span>
|
|
</div>
|
|
${item.foto?`<img class="card-thumb" src="${item.foto}" alt="foto">`:''}
|
|
<div class="card-actions">
|
|
<button class="btn-sm btn-zoom" ${layer ? `onclick="zoomRusak${item.id}()"` : 'disabled title="Lokasi belum valid"'}><i class="fas fa-search-location"></i> Zoom</button>
|
|
<button class="btn-sm btn-edit" onclick="editRusak(${item.id})"><i class="fas fa-edit"></i> Edit</button>
|
|
<button class="btn-sm btn-hapus" onclick="hapusRusak(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>`;
|
|
list.appendChild(card);
|
|
if(layer) window[`zoomRusak${item.id}`]=()=>map.setView([coord[1],coord[0]],17);
|
|
});
|
|
}
|
|
|
|
// =============================================================
|
|
// 16. EDIT
|
|
// =============================================================
|
|
async function editJalan(id){
|
|
const res=await fetch(`jalan.php?id=${id}`);
|
|
const {data}=await res.json();
|
|
switchTab('jalan');
|
|
openModalJalan(true,data);
|
|
}
|
|
async function editParsil(id){
|
|
const res=await fetch(`parsil.php?id=${id}`);
|
|
const {data}=await res.json();
|
|
switchTab('parsil');
|
|
openModalParsil(true,data);
|
|
}
|
|
async function editRusak(id){
|
|
const res=await fetch(`jalan_rusak.php?id=${id}`);
|
|
const {data}=await res.json();
|
|
switchTab('rusak');
|
|
openModalRusak(true,data);
|
|
}
|
|
|
|
// =============================================================
|
|
// 17. HAPUS
|
|
// =============================================================
|
|
async function hapusJalan(id){
|
|
if(!confirm('Yakin ingin menghapus data jalan ini?')) return;
|
|
const res=await fetch(`jalan.php?id=${id}`,{method:'DELETE'});
|
|
const data=await res.json();
|
|
if(data.status==='ok'){ loadJalan(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}
|
|
async function hapusParsil(id){
|
|
if(!confirm('Yakin ingin menghapus data parsil ini?')) return;
|
|
const res=await fetch(`parsil.php?id=${id}`,{method:'DELETE'});
|
|
const data=await res.json();
|
|
if(data.status==='ok'){ loadParsil(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}
|
|
async function hapusRusak(id){
|
|
if(!confirm('Yakin ingin menghapus data jalan rusak ini?')) return;
|
|
const res=await fetch(`jalan_rusak.php?id=${id}`,{method:'DELETE'});
|
|
const data=await res.json();
|
|
if(data.status==='ok'){ loadRusak(); toast(data.message); }
|
|
else toast('Error: '+data.message,'error');
|
|
}
|
|
|
|
// =============================================================
|
|
// 18. INIT
|
|
// =============================================================
|
|
loadJalan();
|
|
loadParsil();
|
|
loadRusak();
|
|
</script>
|
|
</body>
|
|
</html>
|