1416 lines
104 KiB
PHP
1416 lines
104 KiB
PHP
<?php
|
||
$mode = (isset($_GET['mode']) && $_GET['mode'] === 'input') ? 'input' : 'view';
|
||
if (isset($_GET['action']) || isset($_POST['action'])) { include 'api.php'; exit; }
|
||
include 'db2.php';
|
||
require_once 'includes/stats.php';
|
||
$s = getStats($conn);
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="id">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Sistem Informasi Infrastruktur Jalan — Pontianak</title>
|
||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="assets/jalan.css"/>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
|
||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||
<style>
|
||
:root{
|
||
--blue:#2563eb;--blue-dk:#1d4ed8;--blue-lt:#eff6ff;
|
||
--green:#16a34a;--red:#dc2626;--violet:#7c3aed;
|
||
--border:#e5e7eb;--text:#111827;--muted:#6b7280;
|
||
--radius:12px;--tb-h:62px;--sp-w:340px;
|
||
}
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
html,body{height:100%;font-family:'DM Sans',sans-serif;color:var(--text);overflow:hidden}
|
||
body{display:flex}
|
||
#content-area{display:flex;flex:1;overflow:hidden;min-width:0}
|
||
#map-wrap{flex:1;position:relative;min-width:0}
|
||
#map{height:100%;width:100%}
|
||
|
||
/* sidebar - light theme */
|
||
#sidebar{width:320px;min-width:320px;background:#fff;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;z-index:200;box-shadow: 2px 0 8px rgba(0,0,0,.04);}
|
||
.sb-head{padding:18px 16px 14px;border-bottom:1px solid #e2e8f0;background:#ffffff;color:#0f172a}
|
||
.sb-title h1{font-size:14px;font-weight:700;color:#0f172a;line-height:1.2}
|
||
.sb-title p{font-size:10.5px;color:#94a3b8;margin-top:2px}
|
||
.sb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:4px}
|
||
.sb-stat{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:8px 6px;text-align:center}
|
||
.sb-stat b{display:block;font-size:16px;font-weight:800;font-family:'DM Mono',monospace;color:#1e293b;line-height:1}
|
||
.sb-stat span{font-size:8.5px;color:#94a3b8;text-transform:uppercase;margin-top:4px;display:block;font-weight:600;letter-spacing:.3px}
|
||
.sb-stat:nth-child(1) b{color:#1d4ed8}
|
||
.sb-stat:nth-child(2) b{color:#ca8a04}
|
||
.sb-stat:nth-child(3) b{color:#dc2626}
|
||
.sb-nav{display:flex;padding:10px 12px 0;gap:4px;border-bottom:1px solid #e2e8f0}
|
||
.sb-nav a{flex:1;padding:8px;text-align:center;font-size:12px;font-weight:600;color:#475569;text-decoration:none;border-radius:7px 7px 0 0;border:1px solid transparent;transition:all .15s}
|
||
.sb-nav a:hover{color:#0f172a;background:#f8fafc}
|
||
.sb-nav a.active{color:#1d4ed8;background:#fff;border-color:#e2e8f0;margin-bottom:-1px;font-weight:700}
|
||
.sb-section-lbl {
|
||
font-size: 9.5px; font-weight: 700; color: #94a3b8;
|
||
text-transform: uppercase; letter-spacing: .8px; padding: 10px 8px 4px;
|
||
}
|
||
.sb-item {
|
||
display: flex; align-items: center; gap: 9px; padding: 8px 10px;
|
||
border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500;
|
||
color: #475569; transition: all .15s; border: 1px solid transparent; text-decoration: none;
|
||
}
|
||
.sb-item:hover { background: #f1f5f9; color: #0f172a; }
|
||
.sb-item.active { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; font-weight: 600; }
|
||
.sb-item .ico { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
|
||
.lnav-search{padding:12px;border-bottom:1px solid #e2e8f0}
|
||
.lnav-search input{width:100%;padding:9px 12px 9px 34px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-family:inherit;outline:none;background:#f8fafc url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z'/%3E%3C/svg%3E") no-repeat 11px center}
|
||
.lnav-search input:focus{border-color:#1d4ed8;background-color:#fff}
|
||
.lnav-filters{padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;flex-direction:column;gap:10px;flex-shrink:0;max-height:220px;overflow-y:auto}
|
||
.lnav-filters::-webkit-scrollbar{width:3px}
|
||
.lnav-filters::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}
|
||
.filter-block{display:flex;flex-direction:column;gap:6px}
|
||
.filter-label{font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
|
||
.filter-seg{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
|
||
.filter-seg-btn{
|
||
padding:7px 4px;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;
|
||
border:1px solid #e2e8f0;background:#f8fafc;color:#64748b;font-family:inherit;
|
||
transition:all .12s;text-align:center;
|
||
}
|
||
.filter-seg-btn:hover{background:#fff;border-color:#cbd5e1}
|
||
.filter-seg-btn.active{background:#1d4ed8;border-color:#1d4ed8;color:#fff}
|
||
.filter-chips{display:flex;flex-wrap:wrap;gap:5px}
|
||
.filter-chip{
|
||
display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border-radius:7px;
|
||
font-size:11px;font-weight:600;cursor:pointer;border:1px solid #e2e8f0;
|
||
background:#fff;color:#475569;font-family:inherit;transition:all .12s;
|
||
}
|
||
.filter-chip:hover{background:#f8fafc}
|
||
.filter-chip.active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}
|
||
.filter-chip .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.lnav-panjang{display:flex;align-items:center;gap:6px}
|
||
.lnav-panjang span{font-size:11px;color:#94a3b8;white-space:nowrap}
|
||
.lnav-panjang input{width:68px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:12px;font-family:inherit;outline:none;background:#fff}
|
||
.lnav-panjang input:focus{border-color:#1d4ed8}
|
||
.lnav-view-body{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
|
||
.lnav-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}
|
||
.lnav-list::-webkit-scrollbar{width:3px}
|
||
.lnav-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}
|
||
.lnav-item{display:flex;align-items:center;gap:9px;padding:10px 14px;cursor:pointer;border-bottom:1px solid #f1f5f9;transition:background .12s;border-left:3px solid transparent}
|
||
.lnav-item:hover{background:#f8fafc}
|
||
.lnav-item.active{background:#eff6ff;border-left-color:#1d4ed8}
|
||
.lnav-item-icon{font-size:14px;flex-shrink:0;width:20px;text-align:center}
|
||
.lnav-item-body{flex:1;min-width:0}
|
||
.lnav-item-name{font-size:13px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.lnav-item-meta{font-size:11px;color:#94a3b8;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.lnav-item-badge{font-size:9.5px;font-weight:700;padding:2px 6px;border-radius:10px;white-space:nowrap;flex-shrink:0}
|
||
.lnav-badge-jalan{background:rgba(37,99,235,.25);color:#93c5fd}
|
||
.lnav-badge-parsil{background:rgba(124,58,237,.25);color:#c4b5fd}
|
||
.lnav-badge-rusak{background:rgba(249,115,22,.25);color:#fdba74}
|
||
.lnav-empty{padding:24px 14px;text-align:center;color:#94a3b8;font-size:13px}
|
||
.lnav-count{padding:6px 14px;font-size:11px;color:#94a3b8;border-bottom:1px solid #f1f5f9;font-weight:600}
|
||
.input-tools{padding:16px;display:flex;flex-direction:column;gap:12px;flex:1;overflow-y:auto}
|
||
.input-tools button.active-jalan{border-color:#1d4ed8 !important;background:#eff6ff !important;box-shadow:0 0 0 2px rgba(29,78,216,0.15) !important}
|
||
.input-tools button.active-parsil{border-color:#7c3aed !important;background:#f5f3ff !important;box-shadow:0 0 0 2px rgba(124,58,237,0.15) !important}
|
||
.input-tools button.active-rusak{border-color:#ea580c !important;background:#fff7ed !important;box-shadow:0 0 0 2px rgba(234,88,12,0.15) !important}
|
||
.draw-actions{display:flex;gap:6px;margin-top:8px}
|
||
.draw-actions button{flex:1;padding:8px;border-radius:7px;border:1px solid #cbd5e1;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
|
||
|
||
#topbar{
|
||
position:relative;z-index:1000;flex-shrink:0;
|
||
height:var(--tb-h);
|
||
background:#ffffff;
|
||
backdrop-filter:blur(20px) saturate(1.4);
|
||
-webkit-backdrop-filter:blur(20px) saturate(1.4);
|
||
border-bottom:1px solid #e2e8f0;
|
||
padding:0 16px;
|
||
display:flex;align-items:center;gap:9px;overflow-x:auto;
|
||
}
|
||
#topbar::-webkit-scrollbar{height:0}
|
||
.brand{display:flex;align-items:center;gap:9px;flex-shrink:0}
|
||
.brand-icon{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:16px;box-shadow:0 2px 10px rgba(99,102,241,.4)}
|
||
.brand-text{font-size:13.5px;font-weight:800;color:#1e293b;letter-spacing:-.4px}
|
||
.brand-sub{font-size:10px;color:#64748b;font-weight:500;margin-top:1px}
|
||
.sep{width:1px;height:24px;background:#e2e8f0;flex-shrink:0}
|
||
|
||
.search-wrap{position:relative;flex:1;max-width:290px;min-width:140px}
|
||
.search-ico{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;pointer-events:none;opacity:.5}
|
||
#search-input{
|
||
width:100%;padding:7px 34px 7px 30px;
|
||
background:#f8fafc;border:1.5px solid #e2e8f0;
|
||
border-radius:9px;color:#1e293b;font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:all .18s;
|
||
}
|
||
#search-input::placeholder{color:#94a3b8}
|
||
#search-input:focus{background:#ffffff;border-color:rgba(99,102,241,.65);box-shadow:0 0 0 3px rgba(99,102,241,.15)}
|
||
.search-filter-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:rgba(148,163,184,.6);font-size:14px;padding:2px;transition:color .15s}
|
||
.search-filter-btn:hover{color:#a5b4fc}
|
||
.search-filter-btn.active{color:#818cf8}
|
||
|
||
#search-drop{
|
||
display:none;
|
||
position:fixed;
|
||
top:var(--tb-h);
|
||
width:380px;
|
||
background:#1e293b;
|
||
border:1px solid rgba(255,255,255,.12);
|
||
border-radius:0 0 12px 12px;
|
||
overflow:hidden;
|
||
box-shadow:0 10px 40px rgba(0,0,0,.55);
|
||
z-index:5000;
|
||
max-height:430px;
|
||
overflow-y:auto;
|
||
}
|
||
#search-drop.open{display:block}
|
||
#search-drop::-webkit-scrollbar{width:4px}
|
||
#search-drop::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:4px}
|
||
|
||
.sf-panel{padding:10px 12px;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.08)}
|
||
.sf-row{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:6px}
|
||
.sf-row:last-child{margin-bottom:0}
|
||
.sf-chip{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;cursor:pointer;border:1.5px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);color:rgba(148,163,184,.8);transition:all .14s;white-space:nowrap}
|
||
.sf-chip:hover{background:rgba(255,255,255,.1);color:#cbd5e1}
|
||
.sf-chip.active{background:rgba(99,102,241,.25);border-color:#818cf8;color:#a5b4fc}
|
||
.sf-chip.jalan-nas.active{background:rgba(220,38,38,.2);border-color:#f87171;color:#fca5a5}
|
||
.sf-chip.jalan-pro.active{background:rgba(245,158,11,.2);border-color:#fbbf24;color:#fcd34d}
|
||
.sf-chip.jalan-kab.active{background:rgba(22,163,74,.2);border-color:#4ade80;color:#86efac}
|
||
.sf-chip.parsil-shm.active{background:rgba(245,158,11,.2);border-color:#fbbf24;color:#fcd34d}
|
||
.sf-chip.parsil-hgb.active{background:rgba(37,99,235,.2);border-color:#60a5fa;color:#93c5fd}
|
||
.sf-chip.parsil-hgu.active{background:rgba(22,163,74,.2);border-color:#4ade80;color:#86efac}
|
||
.sf-chip.parsil-hp.active{background:rgba(220,38,38,.2);border-color:#f87171;color:#fca5a5}
|
||
.sf-chip.rusak.active{background:rgba(249,115,22,.2);border-color:#fb923c;color:#fdba74}
|
||
.sf-range{display:flex;align-items:center;gap:6px;margin-top:4px}
|
||
.sf-range-label{font-size:11px;color:rgba(148,163,184,.65);white-space:nowrap}
|
||
.sf-range-input{width:80px;padding:4px 8px;background:rgba(255,255,255,.07);border:1.5px solid rgba(255,255,255,.1);border-radius:7px;color:#f1f5f9;font-size:12px;font-family:'Inter',sans-serif;outline:none}
|
||
.sf-range-input:focus{border-color:rgba(99,102,241,.6)}
|
||
|
||
.sr-item{display:flex;align-items:center;gap:9px;padding:9px 13px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.05);transition:background .12s}
|
||
.sr-item:last-child{border-bottom:none}
|
||
.sr-item:hover{background:rgba(255,255,255,.08)}
|
||
.sr-icon{font-size:15px;flex-shrink:0}
|
||
.sr-txt .sr-name{font-size:13px;font-weight:600;color:#f1f5f9}
|
||
.sr-txt .sr-meta{font-size:11px;color:rgba(148,163,184,.7);margin-top:1px}
|
||
.sr-badge{margin-left:auto;padding:2px 7px;border-radius:20px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0}
|
||
.sr-badge.jalan{background:rgba(37,99,235,.25);color:#93c5fd}
|
||
.sr-badge.parsil{background:rgba(124,58,237,.25);color:#c4b5fd}
|
||
.sr-badge.rusak{background:rgba(249,115,22,.25);color:#fdba74}
|
||
.sr-empty{padding:16px;text-align:center;color:rgba(148,163,184,.6);font-size:13px}
|
||
|
||
.stat-section{display:flex;align-items:center;gap:4px;flex-shrink:0}
|
||
.stat-label{font-size:9px;font-weight:700;color:rgba(148,163,184,.65);text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;margin-right:1px}
|
||
.stat-pill{display:flex;align-items:center;gap:4px;padding:4px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;border:1px solid;flex-shrink:0;transition:transform .1s;cursor:default}
|
||
.stat-pill:hover{transform:translateY(-1px)}
|
||
.pill-total{background:#f8fafc;border-color:#cbd5e1;color:#334155}
|
||
.pill-nas{background:#fef2f2;border-color:#fca5a5;color:#dc2626}
|
||
.pill-pro{background:#fffbeb;border-color:#fcd34d;color:#b45309}
|
||
.pill-kab{background:#f0fdf4;border-color:#86efac;color:#15803d}
|
||
.pill-shm{background:#fffbeb;border-color:#fcd34d;color:#b45309}
|
||
.pill-hgb{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}
|
||
.pill-hgu{background:#f0fdf4;border-color:#86efac;color:#15803d}
|
||
.pill-hp{background:#fef2f2;border-color:#fca5a5;color:#dc2626}
|
||
.pill-rusak{background:#fff7ed;border-color:#fdba74;color:#c2410c}
|
||
.pill-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
||
.pill-num{font-size:12.5px;font-weight:800}
|
||
.nav-link{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;border:1.5px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#94a3b8;font-size:12.5px;font-weight:600;font-family:'Inter',sans-serif;text-decoration:none;transition:all .15s;white-space:nowrap;flex-shrink:0;margin-left:auto}
|
||
.nav-link:hover{background:rgba(255,255,255,.1);color:#cbd5e1}
|
||
|
||
#legend{
|
||
position:absolute;bottom:16px;left:16px;z-index:900;
|
||
background:#fff;border:1px solid #e2e8f0;border-radius:10px;
|
||
padding:10px 12px;box-shadow:0 4px 16px rgba(15,23,42,.1);
|
||
min-width:150px;max-width:180px;font-size:11px;
|
||
}
|
||
.leg-title{font-size:9px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin-bottom:5px}
|
||
.leg-title:not(:first-child){margin-top:8px}
|
||
.leg-row{display:flex;align-items:center;gap:7px;margin-bottom:4px;font-size:11px;font-weight:500;color:#475569}
|
||
.leg-line{width:20px;height:3px;border-radius:2px;flex-shrink:0}
|
||
.leg-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;border:1.5px solid}
|
||
.leg-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;border:2px solid}
|
||
|
||
.leaflet-popup-content-wrapper{border-radius:14px!important;box-shadow:0 8px 36px rgba(0,0,0,.2)!important;border:1px solid var(--border)!important;padding:0!important;overflow:hidden!important}
|
||
.leaflet-popup-content{margin:0!important}
|
||
.leaflet-popup-tip{background:white!important}
|
||
.pw{font-family:'Inter',sans-serif;min-width:210px}
|
||
.pw-head{padding:13px 15px 9px}
|
||
.pw-title{font-size:13px;font-weight:700;color:#111827;margin-bottom:4px}
|
||
.pw-badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px}
|
||
.pw-row{font-size:12px;color:#6b7280;margin-bottom:3px}
|
||
.pw-row b{color:#111827}
|
||
.pw-foot{display:flex;border-top:1px solid #f0f0f0}
|
||
.pw-btn{flex:1;padding:9px 0;border:none;background:white;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:background .12s;color:#374151}
|
||
.pw-btn:not(:last-child){border-right:1px solid #f0f0f0}
|
||
.pw-btn:hover{background:#f9fafb}
|
||
.pw-btn.edit{color:#2563eb}.pw-btn.edit:hover{background:#eff6ff}
|
||
.pw-btn.danger{color:#dc2626}.pw-btn.danger:hover{background:#fef2f2}
|
||
|
||
#side-panel{display:none;position:fixed;inset:0;z-index:9999;background:rgba(15,23,42,0.4);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);align-items:center;justify-content:center;padding:20px}
|
||
#side-panel.open{display:flex}
|
||
.sp-card{background:#ffffff;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04);width:100%;max-width:420px;max-height:90vh;display:flex;flex-direction:column;overflow:hidden;border:1px solid #e2e8f0;animation:spFadeIn 0.2s ease-out}
|
||
@keyframes spFadeIn{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}
|
||
.geo-btn-mini {background:#f8fafc;color:#64748b;border:1.5px solid #e2e8f0;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.15s;display:inline-flex;align-items:center;gap:4px;font-family:'Inter',sans-serif}
|
||
.geo-btn-mini:hover {background:#f1f5f9;color:#1e293b;border-color:#cbd5e1}
|
||
.geo-btn-mini.active {background:rgba(29,78,216,0.1);border-color:#1d4ed8;color:#1d4ed8}
|
||
.sp-head{padding:18px 20px 14px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:12px;flex-shrink:0}
|
||
.sp-icon{width:40px;height:40px;border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:19px;flex-shrink:0}
|
||
.sp-icon.jalan{background:linear-gradient(135deg,rgba(37,99,235,.35),rgba(37,99,235,.1));border:1px solid rgba(59,130,246,.3)}
|
||
.sp-icon.parsil{background:linear-gradient(135deg,rgba(124,58,237,.35),rgba(124,58,237,.1));border:1px solid rgba(139,92,246,.3)}
|
||
.sp-icon.rusak{background:linear-gradient(135deg,rgba(249,115,22,.35),rgba(249,115,22,.1));border:1px solid rgba(251,146,60,.3)}
|
||
.sp-title{font-size:16px;font-weight:800;color:#1e293b;letter-spacing:-.3px}
|
||
.sp-sub{font-size:11.5px;color:#64748b;margin-top:2px}
|
||
.sp-close{margin-left:auto;width:30px;height:30px;border-radius:8px;background:#f1f5f9;border:1px solid #e2e8f0;color:#94a3b8;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
|
||
.sp-close:hover{background:#e2e8f0;color:#1e293b}
|
||
.sp-tabs{display:flex;border-bottom:1px solid #e2e8f0;flex-shrink:0}
|
||
.sp-tab{flex:1;padding:11px;text-align:center;font-size:12.5px;font-weight:700;color:#94a3b8;cursor:pointer;transition:all .15s;border-bottom:2px solid transparent;margin-bottom:-1px}
|
||
.sp-tab:hover{color:#94a3b8}
|
||
.sp-tab.active{color:#818cf8;border-color:#818cf8}
|
||
.sp-body{flex:1;overflow-y:auto;padding:18px 20px}
|
||
.sp-body::-webkit-scrollbar{width:4px}
|
||
.sp-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:4px}
|
||
.sp-field{margin-bottom:14px}
|
||
.sp-label{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.3px;margin-bottom:6px}
|
||
.sp-input{width:100%;padding:9px 12px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:9px;font-size:13.5px;font-family:'Inter',sans-serif;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s,background .15s}
|
||
.sp-input::placeholder{color:#94a3b8}
|
||
.sp-input:focus{border-color:rgba(99,102,241,.7);background:#ffffff;box-shadow:0 0 0 3px rgba(99,102,241,.18)}
|
||
.sp-input[readonly]{background:#f1f5f9;color:#64748b;cursor:default}
|
||
.sp-input[readonly]:focus{border-color:#e2e8f0;box-shadow:none}
|
||
.sp-input option{background:#ffffff;color:#1e293b}
|
||
.sp-row{display:grid;grid-template-columns:1fr 1fr;gap:11px}
|
||
.sp-auto{float:right;font-size:10px;font-weight:700;color:#4ade80;background:rgba(74,222,128,.15);padding:1px 7px;border-radius:20px;text-transform:none;letter-spacing:0}
|
||
.geo-hint{background:rgba(99,102,241,.12);border:1px solid rgba(99,102,241,.25);border-radius:10px;padding:12px 14px;margin-bottom:14px;font-size:12px;color:#a5b4fc;line-height:1.6}
|
||
.geo-hint strong{color:#c7d2fe;display:block;margin-bottom:4px;font-size:12.5px}
|
||
.geo-toolbar{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
|
||
.geo-btn{display:flex;align-items:center;gap:9px;padding:10px 14px;border-radius:10px;background:#f8fafc;border:1.5px solid #e2e8f0;color:#64748b;font-size:13px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s;text-align:left;width:100%}
|
||
.geo-btn:hover{background:#f1f5f9;color:#1e293b;border-color:#cbd5e1}
|
||
.geo-btn.active{background:rgba(99,102,241,.2);border-color:#818cf8;color:#a5b4fc}
|
||
.geo-btn-icon{font-size:17px;flex-shrink:0}
|
||
.geo-btn-title{font-size:13px;font-weight:700}
|
||
.geo-btn-desc{font-size:11px;color:#94a3b8;margin-top:1px}
|
||
.geo-note{font-size:11.5px;color:#64748b;line-height:1.7;padding:2px}
|
||
.sp-foot{padding:14px 20px;border-top:1px solid #e2e8f0;display:flex;gap:9px;flex-shrink:0}
|
||
.sp-save{flex:1;background:linear-gradient(135deg,#3b82f6,#6366f1);color:white;border:none;padding:11px;border-radius:10px;cursor:pointer;font-size:13.5px;font-weight:700;font-family:'Inter',sans-serif;transition:all .15s;box-shadow:0 4px 14px rgba(99,102,241,.35)}
|
||
.sp-save:hover{box-shadow:0 6px 20px rgba(99,102,241,.5);transform:translateY(-1px)}
|
||
.sp-save:active{transform:none}
|
||
.sp-save:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
|
||
.sp-cancel{padding:11px 18px;border-radius:10px;background:#f1f5f9;color:#64748b;border:1.5px solid #e2e8f0;cursor:pointer;font-size:13.5px;font-weight:600;font-family:'Inter',sans-serif;transition:all .15s}
|
||
.sp-cancel:hover{background:#e2e8f0;color:#1e293b}
|
||
|
||
#sp-foto-preview{width:100%;max-height:140px;object-fit:cover;border-radius:8px;margin-top:6px;display:none}
|
||
.photo-btns{display:flex;gap:8px;margin-top:8px}
|
||
.photo-btn{flex:1;padding:7px;border-radius:8px;border:1.5px solid #e2e8f0;background:#f8fafc;color:#64748b;font-size:11.5px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:5px}
|
||
.photo-btn:hover{background:#f1f5f9;color:#1e293b}
|
||
|
||
.leaflet-control-zoom{border:none!important;border-radius:10px!important;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.2)!important}
|
||
.leaflet-control-zoom a{background:#ffffff!important;color:#64748b!important;border:none!important;width:33px!important;height:33px!important;line-height:33px!important;font-size:16px!important;transition:background .12s,color .12s!important}
|
||
.leaflet-control-zoom a:hover{background:#f1f5f9!important;color:#1e293b!important}
|
||
.leaflet-control-zoom-in{border-bottom:1px solid #e2e8f0!important}
|
||
|
||
/* Input modals — light theme */
|
||
.modal-overlay{display:none;position:fixed;z-index:9999;inset:0;background:rgba(15,23,42,.45);backdrop-filter:blur(4px);justify-content:center;align-items:center;padding:16px}
|
||
.modal-overlay.active{display:flex}
|
||
.modal-card{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:22px 22px 18px;min-width:min(420px,94vw);max-width:480px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 50px rgba(15,23,42,.18);animation:mIn .2s ease}
|
||
@keyframes mIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
|
||
.modal-header{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #f1f5f9}
|
||
.modal-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
||
.modal-icon.jalan{background:#eff6ff;border:1px solid #bfdbfe}
|
||
.modal-icon.parsil{background:#f5f3ff;border:1px solid #ddd6fe}
|
||
.modal-icon.rusak{background:#fff7ed;border:1px solid #fed7aa}
|
||
.modal-title{font-size:16px;font-weight:700;color:#0f172a}
|
||
.modal-sub{font-size:12px;color:#64748b;margin-top:2px}
|
||
.field-group{margin-bottom:13px}
|
||
.field-label{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.35px;margin-bottom:5px}
|
||
.field-input{width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:inherit;color:#0f172a;outline:none;background:#fff;transition:border-color .15s,box-shadow .15s}
|
||
.field-input::placeholder{color:#94a3b8}
|
||
.field-input:focus{border-color:#1d4ed8;box-shadow:0 0 0 3px rgba(29,78,216,.12)}
|
||
.field-input[readonly]{background:#f8fafc;color:#64748b}
|
||
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||
.readonly-tag{float:right;font-size:10px;font-weight:600;color:#15803d;background:#f0fdf4;padding:2px 7px;border-radius:20px;text-transform:none;letter-spacing:0}
|
||
.modal-footer{display:flex;gap:8px;margin-top:18px}
|
||
.btn-primary{flex:1;background:#1d4ed8;color:#fff;border:none;padding:11px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;font-family:inherit;transition:background .15s}
|
||
.btn-primary:hover{background:#1e40af}
|
||
.btn-primary:disabled{opacity:.55;cursor:default}
|
||
.btn-ghost{flex:1;background:#fff;color:#64748b;border:1px solid #e2e8f0;padding:11px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;font-family:inherit}
|
||
.btn-ghost:hover{background:#f8fafc}
|
||
.photo-upload-area{border:2px dashed #e2e8f0;border-radius:10px;padding:14px;text-align:center;cursor:pointer;background:#f8fafc;transition:border-color .15s}
|
||
.photo-upload-area:hover{border-color:#93c5fd;background:#eff6ff}
|
||
.photo-upload-area .pu-icon{font-size:22px;margin-bottom:4px}
|
||
.photo-upload-area .pu-text{font-size:12px;color:#475569;font-weight:500}
|
||
.photo-upload-area .pu-sub{font-size:11px;color:#94a3b8;margin-top:2px}
|
||
#photo-preview{width:100%;max-height:150px;object-fit:cover;border-radius:8px;margin-top:8px;display:none}
|
||
.gps-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600;background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;margin-top:5px}
|
||
.gps-badge.loading{background:#fffbeb;color:#b45309;border-color:#fde68a}
|
||
</style>
|
||
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
|
||
</head>
|
||
<body>
|
||
|
||
<div id="content-area">
|
||
|
||
<aside id="sidebar">
|
||
<div class="sb-head">
|
||
<a href="index.php" style="display:flex; align-items:center; gap:10px; text-decoration:none; color:inherit">
|
||
<div class="sb-logo" style="width:38px; height:38px; border-radius:10px; background:linear-gradient(135deg, #1d4ed8, #60a5fa); display:flex; align-items:center; justify-content:center; flex-shrink:0; box-shadow:0 2px 8px rgba(29,78,216,.25)">
|
||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M5 3v18M19 3v18M12 3v3M12 9v3M12 15v3"/></svg>
|
||
</div>
|
||
<div class="sb-title"><h1>Infrastruktur Jalan</h1><p>Sistem Informasi Geografis</p></div>
|
||
</a>
|
||
|
||
<!-- 2x2 Stats Grid -->
|
||
<div class="sb-stats" style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px">
|
||
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
|
||
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#1d4ed8" id="s-tj"><?=$s['tj']?></div>
|
||
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Jalan</div>
|
||
</div>
|
||
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
|
||
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#ca8a04" id="s-tp"><?=$s['tp']?></div>
|
||
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Kavling</div>
|
||
</div>
|
||
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
|
||
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#dc2626" id="s-tr"><?=$s['tr']?></div>
|
||
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Titik Rusak</div>
|
||
</div>
|
||
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
|
||
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#7c3aed" id="s-total"><?=$s['tj'] + $s['tp'] + $s['tr']?></div>
|
||
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Total Data</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Navigation Sections -->
|
||
<div class="sb-nav-container" style="padding:8px; display:flex; flex-direction:column; gap:2px; border-bottom:1px solid #e2e8f0">
|
||
<div class="sb-section-lbl">Aksi Admin</div>
|
||
<a href="index.php?mode=input" class="sb-item <?= $mode==='input'?'active':'' ?>">
|
||
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></span>
|
||
<span>Input Data Baru</span>
|
||
</a>
|
||
</div>
|
||
|
||
<?php if ($mode === 'view'): ?>
|
||
<div class="lnav-view-body" style="flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden">
|
||
<div class="lnav-search" style="padding:12px; border-bottom:1px solid #e2e8f0">
|
||
<input type="text" id="lnav-input" placeholder="Cari jalan, kavling, atau titik rusak…" autocomplete="off" style="width:100%; padding:9px 12px 9px 34px; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-family:inherit; outline:none; background:#f8fafc url('data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'14\' height=\'14\' fill=\'%2394a3b8\' viewBox=\'0 0 16 16\'%3E%3Cpath d=\'M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z\'/%3E%3C/svg%3E') no-repeat 11px center">
|
||
</div>
|
||
<div class="lnav-filters">
|
||
<div class="filter-block">
|
||
<span class="filter-label">Jenis Data</span>
|
||
<div class="filter-seg">
|
||
<button type="button" class="filter-seg-btn active" data-group="tipe" data-val="semua" onclick="lnavChip(this)">Semua</button>
|
||
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="jalan" onclick="lnavChip(this)">Jalan</button>
|
||
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="parsil" onclick="lnavChip(this)">Kavling</button>
|
||
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="rusak" onclick="lnavChip(this)">Rusak</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-block" id="lnav-jalan-block">
|
||
<span class="filter-label">Klasifikasi Jalan</span>
|
||
<div class="filter-chips" id="lnav-jalan-chips">
|
||
<button type="button" class="filter-chip active" data-group="status" data-val="" onclick="lnavChip(this)">Semua</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Nasional" onclick="lnavChip(this)"><span class="dot" style="background:#e63946"></span>Nasional</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Provinsi" onclick="lnavChip(this)"><span class="dot" style="background:#f59e0b"></span>Provinsi</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Kabupaten" onclick="lnavChip(this)"><span class="dot" style="background:#16a34a"></span>Kabupaten</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-block" id="lnav-parsil-block" style="display:none">
|
||
<span class="filter-label">Status Kepemilikan</span>
|
||
<div class="filter-chips" id="lnav-parsil-chips">
|
||
<button type="button" class="filter-chip active" data-group="status" data-val="" onclick="lnavChip(this)">Semua</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="SHM" onclick="lnavChip(this)"><span class="dot" style="background:#f59e0b"></span>SHM</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="HGB" onclick="lnavChip(this)"><span class="dot" style="background:#2563eb"></span>HGB</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="HGU" onclick="lnavChip(this)"><span class="dot" style="background:#16a34a"></span>HGU</button>
|
||
<button type="button" class="filter-chip" data-group="status" data-val="HP" onclick="lnavChip(this)"><span class="dot" style="background:#dc2626"></span>HP</button>
|
||
</div>
|
||
</div>
|
||
<div class="filter-block" id="lnav-panjang-row">
|
||
<span class="filter-label">Panjang Jalan (meter)</span>
|
||
<div class="lnav-panjang">
|
||
<input type="number" id="lnav-pmin" placeholder="Min" min="0">
|
||
<span>s/d</span>
|
||
<input type="number" id="lnav-pmax" placeholder="Maks" min="0">
|
||
<span>m</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="lnav-count" id="lnav-count">Memuat…</div>
|
||
<div class="lnav-list" id="lnav-list"></div>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="input-tools" style="padding: 16px; display: flex; flex-direction: column; gap: 12px; flex: 1; overflow-y: auto;">
|
||
<a href="index.php" style="display:flex; align-items:center; gap:8px; padding:10px 12px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; font-size:12.5px; font-weight:600; color:#475569; text-decoration:none; transition:all 0.15s;" onmouseover="this.style.background='#eff6ff'; this.style.borderColor='#bfdbfe'; this.style.color='#1d4ed8'" onmouseout="this.style.background='#f8fafc'; this.style.borderColor='#e2e8f0'; this.style.color='#475569'">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||
Kembali ke Data
|
||
</a>
|
||
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 10px; font-size: 12px; color: #1e40af; line-height: 1.5; display: flex; gap: 8px; align-items: flex-start;">
|
||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-top:2px; flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||
<span>Pilih jenis data di bawah, lalu klik/gambar di peta sebelah kanan.</span>
|
||
</div>
|
||
|
||
<div style="display:flex; flex-direction:column; gap:8px">
|
||
<button type="button" id="btn-jalan" onclick="setMode('jalan')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
|
||
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#eff6ff; color:#1d4ed8"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 3v18M19 3v18M12 3v3M12 9v3M12 15v3"/></svg></span>
|
||
<span>Tambah Jalan</span>
|
||
</button>
|
||
|
||
<button type="button" id="btn-parsil" onclick="setMode('parsil')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
|
||
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#f5f3ff; color:#7c3aed"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></span>
|
||
<span>Tambah Kavling</span>
|
||
</button>
|
||
|
||
<button type="button" id="btn-rusak" onclick="setMode('rusak')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
|
||
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#fff7ed; color:#ea580c"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
|
||
<span>Titik Jalan Rusak</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="draw-actions" id="draw-actions" style="display:none; flex-direction:column; gap:8px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:12px; margin-top:4px">
|
||
<p style="font-size:11px; font-weight:600; color:#64748b; margin-bottom:4px; text-transform:uppercase; letter-spacing:0.5px">Kontrol Menggambar</p>
|
||
<div style="display:flex; gap:6px">
|
||
<button type="button" id="undo-btn" onclick="undoLast()" style="flex:1; padding:8px; background:#fff; border:1px solid #cbd5e1; border-radius:6px; font-size:12px; font-weight:600; color:#475569; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg> Undo
|
||
</button>
|
||
<button type="button" id="finish-btn" onclick="finishShape()" style="flex:1.2; padding:8px; background:#10b981; border:1px solid #059669; border-radius:6px; font-size:12px; font-weight:600; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px; box-shadow:0 2px 4px rgba(16,185,129,0.2)">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Selesai
|
||
</button>
|
||
<button type="button" id="cancel-btn" onclick="cancelDraw()" style="flex:1; padding:8px; background:#ef4444; border:1px solid #dc2626; border-radius:6px; font-size:12px; font-weight:600; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px; box-shadow:0 2px 4px rgba(239,68,68,0.2)">
|
||
Batal
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p id="hint" style="font-size:12px; text-align:center; color:#94a3b8; font-style:italic; margin-top:8px">Pilih mode input di atas untuk mulai</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="sb-foot" style="padding: 12px; border-top: 1px solid #e2e8f0; margin-top: auto; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; background: #fff;">
|
||
<a href="../index.php" class="btn-portal" style="display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: 8px; font-weight: 600; text-decoration: none; font-size: 12px; transition: all 0.15s;">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||
Kembali ke Portal
|
||
</a>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- ── Peta ── -->
|
||
<div id="map-wrap" style="display:flex; flex-direction:column; flex:1; position:relative; min-width:0; height:100%">
|
||
<!-- Topbar -->
|
||
<div id="topbar" style="position:relative; z-index:1000; height:56px; background:#fff; border-bottom:1px solid #e2e8f0; display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0">
|
||
<div style="display:flex; align-items:center; gap:8px">
|
||
<button type="button" id="sb-toggle-jt" onclick="document.getElementById('sidebar').style.display=document.getElementById('sidebar').style.display==='none'?'flex':'none';setTimeout(function(){map.invalidateSize()},300)" style="background:none;border:none;cursor:pointer;color:#64748b;padding:4px;display:flex;align-items:center"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
|
||
<div style="font-size:15px; font-weight:700; color:#1e293b">Peta GIS <span style="font-size:13px; font-weight:500; color:#64748b; margin-left:4px">Jalan & Kavling Tanah</span></div>
|
||
</div>
|
||
<div style="display:flex; gap:8px">
|
||
<?php if ($mode === 'view'): ?>
|
||
<a href="index.php?mode=input" style="display:flex; align-items:center; gap:6px; padding:8px 14px; background:#1d4ed8; color:#fff; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; box-shadow:0 2px 4px rgba(29,78,216,0.15)">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||
Input Baru
|
||
</a>
|
||
<?php else: ?>
|
||
<a href="index.php" style="display:flex; align-items:center; gap:6px; padding:8px 14px; border:1px solid #cbd5e1; color:#475569; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; background:#fff">
|
||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
|
||
Lihat Peta
|
||
</a>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="map" style="flex:1; min-height:0; width:100%; position:relative"></div>
|
||
|
||
<!-- Map controls (top-right overlay) -->
|
||
<div id="map-controls" style="position: absolute; top: 68px; right: 12px; z-index: 900; display: flex; flex-direction: column; gap: 8px; width: 220px;">
|
||
<!-- Legend Card -->
|
||
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
|
||
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>Legenda</h4>
|
||
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px">Jalan</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#e63946"></div>Jalan Nasional</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#f59e0b"></div>Jalan Provinsi</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:6px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#16a34a"></div>Jalan Kabupaten</div>
|
||
|
||
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px; margin-top:6px">Kavling</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #f59e0b; background:rgba(245,158,11,0.22)"></div>SHM</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #2563eb; background:rgba(37,99,235,0.15)"></div>HGB</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #16a34a; background:rgba(22,163,74,0.15)"></div>HGU</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:6px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #dc2626; background:rgba(220,38,38,0.15)"></div>HP</div>
|
||
|
||
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px; margin-top:6px">Jalan Rusak</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #f97316; background:#f97316"></div>< 1 bulan</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #ca8a04; background:#facc15"></div>3-12 bulan</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #64748b; background:#94a3b8"></div>> 1 tahun</div>
|
||
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #991b1b; background:#ef4444"></div>⚡ Berulang rusak</div>
|
||
</div>
|
||
|
||
<!-- Filter/Display Card -->
|
||
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
|
||
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Tampilkan</h4>
|
||
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; margin-bottom:5px; color:#475569">
|
||
<input type="checkbox" id="show-jalan" checked onchange="toggleLayerGroup('jalan')"> Ruas Jalan
|
||
</label>
|
||
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; margin-bottom:5px; color:#475569">
|
||
<input type="checkbox" id="show-parsil" checked onchange="toggleLayerGroup('parsil')"> Kavling Tanah
|
||
</label>
|
||
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; color:#475569">
|
||
<input type="checkbox" id="show-rusak" checked onchange="toggleLayerGroup('rusak')"> Jalan Rusak
|
||
</label>
|
||
</div>
|
||
|
||
<!-- Base Layer Toggle -->
|
||
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
|
||
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>Peta Dasar</h4>
|
||
<div style="display:flex; gap:5px">
|
||
<button type="button" id="jt-base-map" style="flex:1;padding:6px;border:1px solid #1d4ed8;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:#1d4ed8;color:#fff" onclick="if(map.hasLayer(baseSat))map.removeLayer(baseSat);if(!map.hasLayer(baseMap))baseMap.addTo(map);this.style.background='#1d4ed8';this.style.color='#fff';this.style.borderColor='#1d4ed8';document.getElementById('jt-base-sat').style.background='#fff';document.getElementById('jt-base-sat').style.color='#475569';document.getElementById('jt-base-sat').style.borderColor='#e2e8f0'">Peta</button>
|
||
<button type="button" id="jt-base-sat" style="flex:1;padding:6px;border:1px solid #e2e8f0;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:#fff;color:#475569" onclick="if(map.hasLayer(baseMap))map.removeLayer(baseMap);if(!map.hasLayer(baseSat))baseSat.addTo(map);this.style.background='#1d4ed8';this.style.color='#fff';this.style.borderColor='#1d4ed8';document.getElementById('jt-base-map').style.background='#fff';document.getElementById('jt-base-map').style.color='#475569';document.getElementById('jt-base-map').style.borderColor='#e2e8f0'">Satelit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- SIDE PANEL -->
|
||
<div id="side-panel">
|
||
<div class="sp-card">
|
||
<div class="sp-head">
|
||
<div class="sp-icon" id="sp-icon">🛣</div>
|
||
<div><div class="sp-title" id="sp-title">Edit Data</div><div class="sp-sub" id="sp-sub">—</div></div>
|
||
<button class="sp-close" onclick="closePanel()">✕</button>
|
||
</div>
|
||
<div class="sp-tabs" id="sp-tabs">
|
||
<div class="sp-tab active" id="tab-data" onclick="switchTab('data')">📋 Data</div>
|
||
<div class="sp-tab" id="tab-geo" onclick="switchTab('geo')">📐 Geometri</div>
|
||
</div>
|
||
|
||
<div class="sp-body" id="pane-data">
|
||
<input type="hidden" id="sp-id">
|
||
<input type="hidden" id="sp-type">
|
||
<div class="sp-field">
|
||
<label class="sp-label" id="sp-lbl-nama">Nama</label>
|
||
<input class="sp-input" type="text" id="sp-nama" placeholder="Masukkan nama…">
|
||
</div>
|
||
<div id="sp-parsil-extra" style="display:none">
|
||
<div class="sp-row">
|
||
<div class="sp-field"><label class="sp-label">Nama Pemilik</label><input class="sp-input" type="text" id="sp-pemilik" placeholder="Nama pemilik…"></div>
|
||
<div class="sp-field"><label class="sp-label">No. Sertifikat</label><input class="sp-input" type="text" id="sp-nosert" placeholder="No. sertifikat…"></div>
|
||
</div>
|
||
<div class="sp-field"><label class="sp-label">Keterangan</label><textarea class="sp-input" id="sp-ket" rows="2" placeholder="Keterangan (opsional)…" style="resize:vertical"></textarea></div>
|
||
</div>
|
||
<div id="sp-rusak-extra" style="display:none">
|
||
<div class="sp-row" style="grid-template-columns:1fr 1fr; gap:11px">
|
||
<div class="sp-field"><label class="sp-label">Tanggal</label><input class="sp-input" type="date" id="sp-tanggal"></div>
|
||
<div class="sp-field">
|
||
<label class="sp-label">Koordinat</label>
|
||
<div style="display:flex; gap:6px">
|
||
<input class="sp-input" type="text" id="sp-koordinat-rusak" readonly style="flex:1; min-width:0">
|
||
<button type="button" onclick="startPointDrag()" class="photo-btn" style="background:#eff6ff; color:#1d4ed8; border-color:#bfdbfe; margin-top:0; padding:8px 10px; font-size:11px; white-space:nowrap; flex-shrink:0">📍 Geser</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="sp-field"><label class="sp-label">Keterangan</label><textarea class="sp-input" id="sp-ket-rusak" rows="2" style="resize:vertical"></textarea></div>
|
||
<div class="sp-field">
|
||
<label class="sp-label">Foto</label>
|
||
<img id="sp-foto-preview" src="" alt="foto">
|
||
<div class="photo-btns">
|
||
<button class="photo-btn" onclick="document.getElementById('sp-foto-file').click()">🖼 Ganti Foto</button>
|
||
<button class="photo-btn" id="sp-foto-clear" onclick="spClearFoto()" style="display:none">🗑 Hapus</button>
|
||
</div>
|
||
<input type="file" id="sp-foto-file" accept="image/*" style="display:none" onchange="spHandleFoto(this)">
|
||
</div>
|
||
</div>
|
||
<div id="sp-status-wrap" class="sp-field">
|
||
<label class="sp-label" id="sp-lbl-status">Status</label>
|
||
<select class="sp-input" id="sp-status"></select>
|
||
</div>
|
||
<div id="sp-ukuran-wrap" class="sp-field">
|
||
<label class="sp-label">Ukuran <span class="sp-auto">Otomatis</span></label>
|
||
<input class="sp-input" type="text" id="sp-ukuran" readonly>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp-body" id="pane-geo" style="display:none">
|
||
<div class="geo-hint"><strong>🖱 Edit Geometri</strong>Pilih mode lalu interaksi dengan shape di peta. Klik lagi untuk nonaktifkan.</div>
|
||
<div class="geo-toolbar">
|
||
<button class="geo-btn" id="geo-btn-vertex" onclick="setGeoMode('vertex')"><span class="geo-btn-icon">🔵</span><div><div class="geo-btn-title">Drag Titik</div><div class="geo-btn-desc">Geser titik individual untuk ubah bentuk</div></div></button>
|
||
<button class="geo-btn" id="geo-btn-move" onclick="setGeoMode('move')"><span class="geo-btn-icon">✋</span><div><div class="geo-btn-title">Geser Seluruh Shape</div><div class="geo-btn-desc">Klik-tahan peta untuk pindahkan keseluruhan</div></div></button>
|
||
<button class="geo-btn" id="geo-btn-add" onclick="setGeoMode('add')"><span class="geo-btn-icon">➕</span><div><div class="geo-btn-title">Tambah Titik</div><div class="geo-btn-desc">Klik titik hijau di tengah segmen untuk sisipkan</div></div></button>
|
||
<button class="geo-btn" id="geo-btn-del" onclick="setGeoMode('del')"><span class="geo-btn-icon">🗑</span><div><div class="geo-btn-title">Hapus Titik</div><div class="geo-btn-desc">Klik titik merah untuk menghapusnya</div></div></button>
|
||
</div>
|
||
<div class="geo-note">Perubahan disimpan saat klik <b style="color:#a5b4fc">💾 Simpan</b>.</div>
|
||
</div>
|
||
|
||
<div class="sp-foot">
|
||
<button class="sp-save" id="sp-save-btn" onclick="submitEdit()">💾 Simpan</button>
|
||
<button class="sp-cancel" onclick="closePanel()">Batal</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- FLOATING GEO EDIT BAR -->
|
||
<div id="geo-edit-bar" style="display:none; position:fixed; top:24px; left:50%; transform:translateX(-50%); z-index:99999; background:#ffffff; padding:12px 20px; border-radius:12px; box-shadow:0 10px 25px rgba(0,0,0,0.15); border:1px solid #e2e8f0; align-items:center; gap:16px; font-family:'Inter',sans-serif;">
|
||
<div style="font-size:13px; font-weight:700; color:#1e293b; display:flex; align-items:center; gap:6px; white-space:nowrap">
|
||
<span id="geo-edit-title-icon">📐</span>
|
||
<span id="geo-edit-title-text">Edit Geometri</span>
|
||
</div>
|
||
<div style="width:1px; height:24px; background:#e2e8f0" id="geo-edit-divider-1"></div>
|
||
<div style="display:flex; gap:6px" id="geo-edit-modes">
|
||
<button type="button" onclick="setGeoMode('vertex')" class="geo-btn-mini active" id="gbm-vertex">🔵 Drag Titik</button>
|
||
<button type="button" onclick="setGeoMode('move')" class="geo-btn-mini" id="gbm-move">✋ Geser Shape</button>
|
||
<button type="button" onclick="setGeoMode('add')" class="geo-btn-mini" id="gbm-add">➕ Tambah</button>
|
||
<button type="button" onclick="setGeoMode('del')" class="geo-btn-mini" id="gbm-del">🗑️ Hapus</button>
|
||
</div>
|
||
<div style="width:1px; height:24px; background:#e2e8f0"></div>
|
||
<div style="display:flex; gap:8px">
|
||
<button type="button" onclick="exitFloatingGeoEdit(true)" style="background:#1d4ed8; color:white; border:none; padding:8px 16px; border-radius:8px; font-size:12px; font-weight:700; cursor:pointer; transition:all 0.15s">Selesai</button>
|
||
<button type="button" onclick="exitFloatingGeoEdit(false)" style="background:#f1f5f9; color:#475569; border:1px solid #cbd5e1; padding:8px 16px; border-radius:8px; font-size:12px; font-weight:600; cursor:pointer; transition:all 0.15s">Batal</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal simpan jalan / kavling -->
|
||
<div class="modal-overlay" id="form-modal">
|
||
<div class="modal-card">
|
||
<div class="modal-header">
|
||
<div class="modal-icon" id="form-icon">🛣</div>
|
||
<div>
|
||
<div class="modal-title" id="form-title">Simpan Data</div>
|
||
<div class="modal-sub" id="form-sub">Lengkapi informasi di bawah ini</div>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label" id="label-nama">Nama</label>
|
||
<input class="field-input" type="text" id="inp-nama" placeholder="Masukkan nama…">
|
||
</div>
|
||
<div id="parsil-extra-fields" style="display:none">
|
||
<div class="field-row">
|
||
<div class="field-group">
|
||
<label class="field-label">Nama Pemilik</label>
|
||
<input class="field-input" type="text" id="inp-pemilik" placeholder="Nama pemilik…">
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Nomor Sertifikat</label>
|
||
<input class="field-input" type="text" id="inp-nosert" placeholder="No. sertifikat…">
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Keterangan</label>
|
||
<textarea class="field-input" id="inp-keterangan" rows="2" placeholder="Keterangan tambahan (opsional)…" style="resize:vertical"></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label" id="label-status">Status</label>
|
||
<select class="field-input" id="inp-status"></select>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label" id="label-ukuran">Ukuran <span class="readonly-tag">Otomatis</span></label>
|
||
<input class="field-input" type="text" id="inp-ukuran" readonly>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-primary" type="button" onclick="saveData()">Simpan Data</button>
|
||
<button class="btn-ghost" type="button" onclick="closeForm()">Batal</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal jalan rusak -->
|
||
<div class="modal-overlay" id="rusak-modal">
|
||
<div class="modal-card">
|
||
<div class="modal-header">
|
||
<div class="modal-icon rusak">⚠</div>
|
||
<div>
|
||
<div class="modal-title">Input Jalan Rusak</div>
|
||
<div class="modal-sub" id="rusak-sub">Titik kerusakan jalan</div>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Nama Pelapor <span style="font-size:10px;color:#94a3b8;text-transform:none;letter-spacing:0">(kosongkan = Anonym)</span></label>
|
||
<input class="field-input" type="text" id="rusak-pelapor" placeholder="Nama pelapor (opsional)…">
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Nama Jalan <span class="readonly-tag">Otomatis</span></label>
|
||
<input class="field-input" type="text" id="rusak-nama-jalan" readonly placeholder="Terdeteksi dari lokasi titik…">
|
||
</div>
|
||
<div class="field-row">
|
||
<div class="field-group">
|
||
<label class="field-label">Tanggal <span class="readonly-tag">Otomatis</span></label>
|
||
<input class="field-input" type="date" id="rusak-tanggal">
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Koordinat</label>
|
||
<input class="field-input" type="text" id="rusak-koordinat" readonly>
|
||
<div id="gps-badge" class="gps-badge" style="display:none">📍 GPS aktif</div>
|
||
</div>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Keterangan</label>
|
||
<textarea class="field-input" id="rusak-ket" rows="2" placeholder="Deskripsi kerusakan (opsional)…" style="resize:vertical"></textarea>
|
||
</div>
|
||
<div class="field-group">
|
||
<label class="field-label">Foto Dokumentasi</label>
|
||
<div class="photo-upload-area" onclick="document.getElementById('foto-cam').click()">
|
||
<div class="pu-icon">📷</div>
|
||
<div class="pu-text">Klik untuk ambil foto</div>
|
||
<div class="pu-sub">JPG / PNG · maks 5 MB</div>
|
||
</div>
|
||
<div class="photo-btns">
|
||
<button class="photo-btn" type="button" onclick="document.getElementById('foto-file').click()">🖼 Galeri</button>
|
||
<button class="photo-btn" type="button" onclick="document.getElementById('foto-cam').click()">📷 Kamera</button>
|
||
<button class="photo-btn" type="button" id="foto-clear-btn" onclick="clearFoto()" style="display:none">🗑 Hapus</button>
|
||
</div>
|
||
<input type="file" id="foto-file" accept="image/*" style="display:none" onchange="handleFoto(this)">
|
||
<input type="file" id="foto-cam" accept="image/*" capture="environment" style="display:none" onchange="handleFoto(this)">
|
||
<img id="photo-preview" src="" alt="preview">
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn-primary" type="button" id="rusak-save-btn" onclick="saveRusak()">Simpan Titik</button>
|
||
<button class="btn-ghost" type="button" onclick="closeRusakModal()">Batal</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php include 'includes/confirm-modal.php'; ?>
|
||
<script>
|
||
/* ══ MAP ══════════════════════════════════════════════ */
|
||
var map=L.map('map',{zoomControl:false,maxZoom:22}).setView([-0.0263,109.3425],13);
|
||
L.control.zoom({position:'bottomright'}).addTo(map);
|
||
var baseMap=L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© <a href="https://www.openstreetmap.org/">OSM</a>',maxZoom:22,maxNativeZoom:19}).addTo(map);
|
||
var baseSat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:22,maxNativeZoom:19});
|
||
|
||
map.createPane('rusakPane');
|
||
map.getPane('rusakPane').style.zIndex=450;
|
||
|
||
var jalanLayers={},parsilLayers={},rusakLayers={};
|
||
var jalanData={},parsilData={},rusakData={};
|
||
var WARNA_JALAN={'Jalan Nasional':'#e63946','Jalan Provinsi':'#f59e0b','Jalan Kabupaten':'#16a34a'};
|
||
var WARNA_PARSIL={'SHM':{color:'#f59e0b',fillColor:'#f59e0b',fillOpacity:.22,weight:2},'HGB':{color:'#2563eb',fillColor:'#2563eb',fillOpacity:.18,weight:2},'HGU':{color:'#16a34a',fillColor:'#16a34a',fillOpacity:.18,weight:2},'HP':{color:'#dc2626',fillColor:'#dc2626',fillOpacity:.18,weight:2}};
|
||
|
||
function parseCoords(raw){
|
||
try{var g=typeof raw==='string'?JSON.parse(raw):raw;
|
||
if(g&&g.type==='LineString')return g.coordinates.map(function(c){return[c[1],c[0]];});
|
||
if(g&&g.type==='Polygon') return g.coordinates[0].map(function(c){return[c[1],c[0]];});
|
||
if(Array.isArray(g)) return g.map(function(p){return[p[0],p[1]];});
|
||
}catch(e){}return[];
|
||
}
|
||
|
||
function updateStats(d){
|
||
['tj','tp','tr'].forEach(function(k){var e=document.getElementById('s-'+k);if(e&&d[k]!==undefined)e.textContent=d[k];});
|
||
var tj = parseInt(document.getElementById('s-tj').textContent)||0;
|
||
var tp = parseInt(document.getElementById('s-tp').textContent)||0;
|
||
var tr = parseInt(document.getElementById('s-tr').textContent)||0;
|
||
var tot = document.getElementById('s-total');
|
||
if (tot) tot.textContent = tj + tp + tr;
|
||
}
|
||
function toggleLayerGroup(type) {
|
||
var show = document.getElementById('show-' + type).checked;
|
||
var layers = type === 'jalan' ? jalanLayers : (type === 'parsil' ? parsilLayers : rusakLayers);
|
||
Object.values(layers).forEach(function(l) {
|
||
if (show) {
|
||
map.addLayer(l);
|
||
} else {
|
||
map.removeLayer(l);
|
||
}
|
||
});
|
||
}
|
||
function h(s){return(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');}
|
||
|
||
/* ══ POPUP BUILDERS ─── defined BEFORE PHP data loops ══ */
|
||
function badgeJ(st){var c={'Jalan Nasional':'#fef2f2;color:#dc2626','Jalan Provinsi':'#fffbeb;color:#b45309','Jalan Kabupaten':'#f0fdf4;color:#15803d'};return'<span style="display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px;background:'+(c[st]||'#f3f4f6;color:#374151')+'">'+st+'</span>';}
|
||
function badgeP(st){var c={'SHM':'#fffbeb;color:#b45309','HGB':'#eff6ff;color:#1d4ed8','HGU':'#f0fdf4;color:#15803d','HP':'#fef2f2;color:#dc2626'};return'<span style="display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px;background:'+(c[st]||'#f3f4f6;color:#374151')+'">'+st+'</span>';}
|
||
|
||
function makeJalanPopup(id,nama,status,pjg){
|
||
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
|
||
'<span style="background:#eff6ff;color:#2563eb;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span>' +
|
||
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
|
||
'</div>';
|
||
var lenRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
|
||
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><line x1="5" y1="9" x2="19" y2="9"/><line x1="5" y1="15" x2="19" y2="15"/><line x1="9" y1="5" x2="9" y2="19"/><line x1="15" y1="5" x2="15" y2="19"/></svg>' +
|
||
'<span>Panjang: <b>' + pjg + ' m</b></span>' +
|
||
'</div>';
|
||
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + badgeJ(status) + lenRow + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'jalan\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusJalan('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
|
||
}
|
||
function makeParsilPopup(id,nama,pemilik,nosert,ket,status,luas){
|
||
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
|
||
'<span style="background:#f0fdf4;color:#16a34a;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>' +
|
||
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
|
||
'</div>';
|
||
var areaRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
|
||
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>' +
|
||
'<span>Luas: <b>' + luas + ' m²</b></span>' +
|
||
'</div>';
|
||
var extra=(pemilik?'<div style="font-size:11.5px;color:#64748b;margin-bottom:3px">Pemilik: <b>'+h(pemilik)+'</b></div>':'')+(nosert?'<div style="font-size:11.5px;color:#64748b;margin-bottom:3px">No.Sert: <b>'+h(nosert)+'</b></div>':'')+(ket?'<div style="margin-top:6px;padding:6px 8px;background:#f8fafc;border-radius:6px;font-size:11px;color:#475569;border-left:2px solid #cbd5e1;line-height:1.4">'+h(ket)+'</div>':'');
|
||
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + badgeP(status) + areaRow + extra + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'parsil\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusParsil('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
|
||
}
|
||
function makeRusakPopup(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring){
|
||
var fHtml=foto?'<div style="position:relative;margin-top:10px;border-radius:8px;overflow:hidden;border:1px solid #e2e8f0;box-shadow:0 2px 6px rgba(0,0,0,0.06)"><img src="'+foto+'" style="width:100%;display:block;max-height:120px;object-fit:cover;transition:transform 0.2s" onmouseover="this.style.transform=\'scale(1.03)\'" onmouseout="this.style.transform=\'scale(1)\'"></div>':'';
|
||
var ageBadge=recurring?'<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:12px;font-size:9.5px;font-weight:700;background:#fef2f2;color:#dc2626;border:1px solid #fee2e2;margin-bottom:6px"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="3" style="flex-shrink:0"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Berulang Rusak</span><br>':'';
|
||
|
||
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
|
||
'<span style="background:#fee2e2;color:#ef4444;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>' +
|
||
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
|
||
'</div>';
|
||
|
||
var dateRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
|
||
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>' +
|
||
'<span>' + h(tgl) + '</span>' +
|
||
'</div>';
|
||
|
||
var coordRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
|
||
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>' +
|
||
'<span style="font-family:monospace;letter-spacing:-0.2px">' + parseFloat(lat).toFixed(6) + ', ' + parseFloat(lng).toFixed(6) + '</span>' +
|
||
'</div>';
|
||
|
||
var ketRow = ket ? '<div style="margin-top:6px;padding:6px 8px;background:#f8fafc;border-radius:6px;font-size:11px;color:#475569;border-left:2px solid #cbd5e1;line-height:1.4">' + h(ket) + '</div>' : '';
|
||
|
||
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + ageBadge + dateRow + coordRow + ketRow + fHtml + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'rusak\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusRusak('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
|
||
}
|
||
function getRusakIcon(ageClass,recurring){
|
||
var colors={recent:'#f97316',medium:'#facc15',old:'#94a3b8'};
|
||
var borders={recent:'white',medium:'#92400e',old:'#334155'};
|
||
var bg=recurring?'#ef4444':(colors[ageClass]||colors.recent);
|
||
var bd=recurring?'white':(borders[ageClass]||'white');
|
||
var sz=recurring?18:15;
|
||
var shadow=recurring?'0 0 0 3px rgba(239,68,68,.35),0 2px 8px rgba(0,0,0,.4)':'0 2px 8px rgba(0,0,0,.3)';
|
||
return L.divIcon({
|
||
html:'<div style="width:'+sz+'px;height:'+sz+'px;border-radius:50%;background:'+bg+';border:2.5px solid '+bd+';box-shadow:'+shadow+'"></div>',
|
||
className:'',iconAnchor:[Math.round(sz/2),Math.round(sz/2)]
|
||
});
|
||
}
|
||
function makeRusakLayer(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring){
|
||
ageClass=ageClass||'recent';recurring=recurring||false;
|
||
var icon=getRusakIcon(ageClass,recurring);
|
||
var layer=L.marker([lat,lng],{icon:icon,pane:'rusakPane'}).addTo(map);
|
||
layer.bindPopup(makeRusakPopup(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring),{maxWidth:250});
|
||
layer.on('click',function(){lnavHighlightFromMap('rusak',id);});
|
||
return layer;
|
||
}
|
||
|
||
/* ══ LOAD DATA FROM PHP ═══════════════════════════════ */
|
||
<?php
|
||
$jln=$conn->query("SELECT * FROM jalan");
|
||
while($r=$jln->fetch_assoc()):
|
||
$id=$r['id'];$nama=addslashes($r['nama_jalan']);$status=addslashes($r['status_jalan']);
|
||
$pjg=number_format($r['panjang_meter'],1);$coords=addslashes($r['koordinat']);
|
||
?>
|
||
(function(){
|
||
var id=<?=$id?>,nama='<?=$nama?>',status='<?=$status?>',pjg='<?=$pjg?>';
|
||
var ll=parseCoords('<?=$coords?>');
|
||
var layer=L.polyline(ll,{color:WARNA_JALAN[status]||'#999',weight:5,opacity:.9}).addTo(map);
|
||
layer.bindPopup(makeJalanPopup(id,nama,status,pjg),{maxWidth:280});
|
||
layer.on('click',function(){lnavHighlightFromMap('jalan',id);});
|
||
jalanLayers[id]=layer; jalanData[id]={id:id,nama:nama,status:status,pjg:pjg,ll:ll};
|
||
})();
|
||
<?php endwhile;?>
|
||
|
||
<?php
|
||
$prs=$conn->query("SELECT * FROM parsil");
|
||
while($r=$prs->fetch_assoc()):
|
||
$id=$r['id'];$nama=addslashes($r['nama_parsil']);$pemilik=addslashes($r['nama_pemilik']);
|
||
$nosert=addslashes($r['nomor_sertifikat']);$ket=addslashes($r['keterangan']);
|
||
$status=addslashes($r['status_kepemilikan']);$luas=number_format($r['luas_m2'],1);$coords=addslashes($r['koordinat']);
|
||
?>
|
||
(function(){
|
||
var id=<?=$id?>,nama='<?=$nama?>',pemilik='<?=$pemilik?>',nosert='<?=$nosert?>',ket='<?=$ket?>',status='<?=$status?>',luas='<?=$luas?>';
|
||
var ll=parseCoords('<?=$coords?>');
|
||
var opt=WARNA_PARSIL[status]||{color:'#999',fillColor:'#999',fillOpacity:.2,weight:2};
|
||
var layer=L.polygon(ll,opt).addTo(map);
|
||
layer.bindPopup(makeParsilPopup(id,nama,pemilik,nosert,ket,status,luas),{maxWidth:280});
|
||
layer.on('click',function(){lnavHighlightFromMap('parsil',id);});
|
||
parsilLayers[id]=layer; parsilData[id]={id:id,nama:nama,pemilik:pemilik,nosert:nosert,ket:ket,status:status,luas:luas,ll:ll};
|
||
})();
|
||
<?php endwhile;?>
|
||
|
||
<?php
|
||
$repeatMapS=[];
|
||
$rRepeatS=$conn->query("SELECT ROUND(latitude,4) as la, ROUND(longitude,4) as lo, COUNT(*) as cnt FROM jalan_rusak GROUP BY la,lo HAVING cnt>2");
|
||
while($rr2=$rRepeatS->fetch_assoc()) $repeatMapS[round($rr2['la'],4).','.round($rr2['lo'],4)]=(int)$rr2['cnt'];
|
||
|
||
$rsk=$conn->query("SELECT * FROM jalan_rusak ORDER BY tanggal DESC");
|
||
while($r=$rsk->fetch_assoc()):
|
||
$id=$r['id'];$nama=addslashes($r['nama']);$tgl=addslashes($r['tanggal']);
|
||
$ket=addslashes($r['keterangan']??'');$lat=(float)$r['latitude'];$lng=(float)$r['longitude'];
|
||
$foto=addslashes($r['foto']??'');
|
||
$key=round($lat,4).','.round($lng,4);
|
||
$recurring=isset($repeatMapS[$key])?'true':'false';
|
||
$tglDate=new DateTime($r['tanggal']);
|
||
$now=new DateTime();
|
||
$diffDays=$now->diff($tglDate)->days;
|
||
if($diffDays<=30) $ageClass='recent';
|
||
elseif($diffDays<=365) $ageClass='medium';
|
||
else $ageClass='old';
|
||
?>
|
||
(function(){
|
||
var id=<?=$id?>,nama='<?=$nama?>',tgl='<?=$tgl?>',lat=<?=$lat?>,lng=<?=$lng?>,ket='<?=$ket?>',foto='<?=$foto?>';
|
||
var layer=makeRusakLayer(id,nama,tgl,lat,lng,ket,foto,'<?=$ageClass?>',<?=$recurring?>);
|
||
rusakLayers[id]=layer; rusakData[id]={id:id,nama:nama,tgl:tgl,lat:lat,lng:lng,ket:ket,foto:foto,age:'<?=$ageClass?>',recurring:<?=$recurring?>};
|
||
})();
|
||
<?php endwhile;?>
|
||
|
||
/* ══ DELETE ═══════════════════════════════════════════ */
|
||
function hapusJalan(id){showConfirm({title: 'Hapus Jalan?', message: 'Hapus permanen data jalan ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_jalan&id='+id).then(function(r){return r.json();}).then(function(d){if(jalanLayers[id]){map.removeLayer(jalanLayers[id]);delete jalanLayers[id];delete jalanData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Data jalan dihapus.','success');});});}
|
||
function hapusParsil(id){showConfirm({title: 'Hapus Kavling?', message: 'Hapus permanen data kavling ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_parsil&id='+id).then(function(r){return r.json();}).then(function(d){if(parsilLayers[id]){map.removeLayer(parsilLayers[id]);delete parsilLayers[id];delete parsilData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Data kavling dihapus.','success');});});}
|
||
function hapusRusak(id){showConfirm({title: 'Hapus Jalan Rusak?', message: 'Hapus permanen titik jalan rusak ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_rusak&id='+id).then(function(r){return r.json();}).then(function(d){if(rusakLayers[id]){map.removeLayer(rusakLayers[id]);delete rusakLayers[id];delete rusakData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Titik jalan rusak dihapus.','success');});});}
|
||
|
||
/* ══ SIDE PANEL ═══════════════════════════════════════ */
|
||
var activeId=null,activeType=null,activeTab='data';
|
||
var spFotoBase64='';
|
||
|
||
function el(id){return document.getElementById(id);}
|
||
|
||
function openEdit(type,id){
|
||
map.closePopup();activeId=id;activeType=type;
|
||
|
||
if(type==='jalan'){
|
||
var d=jalanData[id];
|
||
el('sp-icon').textContent='🛣';el('sp-icon').className='sp-icon jalan';
|
||
el('sp-title').textContent='Edit Jalan';el('sp-sub').textContent=d.nama;
|
||
el('sp-lbl-nama').textContent='Nama Jalan';el('sp-lbl-status').textContent='Status Jalan';
|
||
el('sp-nama').value=d.nama;el('sp-ukuran').value=d.pjg+' m';
|
||
el('sp-parsil-extra').style.display='none';el('sp-rusak-extra').style.display='none';
|
||
el('sp-status-wrap').style.display='';el('sp-ukuran-wrap').style.display='';
|
||
var sel=el('sp-status');sel.innerHTML='<option>Jalan Nasional</option><option>Jalan Provinsi</option><option>Jalan Kabupaten</option>';sel.value=d.status;
|
||
editLL=d.ll.map(function(p){return[p[0],p[1]];});
|
||
originalLL=d.ll.map(function(p){return[p[0],p[1]];});
|
||
el('tab-geo').style.display='';
|
||
} else if(type==='parsil'){
|
||
var d=parsilData[id];
|
||
el('sp-icon').textContent='🏘';el('sp-icon').className='sp-icon parsil';
|
||
el('sp-title').textContent='Edit Kavling';el('sp-sub').textContent=d.nama;
|
||
el('sp-lbl-nama').textContent='Nama / No. Parsil';el('sp-lbl-status').textContent='Status Kepemilikan';
|
||
el('sp-nama').value=d.nama;el('sp-pemilik').value=d.pemilik;el('sp-nosert').value=d.nosert;el('sp-ket').value=d.ket;
|
||
el('sp-ukuran').value=d.luas+' m²';
|
||
el('sp-parsil-extra').style.display='block';el('sp-rusak-extra').style.display='none';
|
||
el('sp-status-wrap').style.display='';el('sp-ukuran-wrap').style.display='';
|
||
var sel=el('sp-status');sel.innerHTML='<option value="SHM">SHM</option><option value="HGB">HGB</option><option value="HGU">HGU</option><option value="HP">HP</option>';sel.value=d.status;
|
||
editLL=d.ll.map(function(p){return[p[0],p[1]];});
|
||
originalLL=d.ll.map(function(p){return[p[0],p[1]];});
|
||
el('tab-geo').style.display='';
|
||
} else {
|
||
var d=rusakData[id];
|
||
el('sp-icon').textContent='⚠';el('sp-icon').className='sp-icon rusak';
|
||
el('sp-title').textContent='Edit Jalan Rusak';el('sp-sub').textContent=d.nama;
|
||
el('sp-lbl-nama').textContent='Nama / Deskripsi';
|
||
el('sp-nama').value=d.nama;
|
||
el('sp-tanggal').value=d.tgl;
|
||
el('sp-koordinat-rusak').value=d.lat.toFixed(6)+', '+d.lng.toFixed(6);
|
||
el('sp-ket-rusak').value=d.ket;
|
||
el('sp-parsil-extra').style.display='none';el('sp-rusak-extra').style.display='block';
|
||
el('sp-status-wrap').style.display='none';el('sp-ukuran-wrap').style.display='none';
|
||
el('tab-geo').style.display='none';
|
||
originalLat=d.lat;
|
||
originalLng=d.lng;
|
||
spFotoBase64=d.foto||'';
|
||
if(d.foto){el('sp-foto-preview').src=d.foto;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';}
|
||
else{el('sp-foto-preview').style.display='none';el('sp-foto-clear').style.display='none';}
|
||
}
|
||
|
||
switchTab('data');
|
||
el('side-panel').classList.add('open');
|
||
setTimeout(function(){el('sp-nama').focus();},200);
|
||
}
|
||
function closePanel(){clearGeo();el('side-panel').classList.remove('open');activeId=null;activeType=null;}
|
||
function switchTab(tab){
|
||
if (tab === 'geo') {
|
||
el('side-panel').classList.remove('open');
|
||
el('geo-edit-bar').style.display = 'flex';
|
||
el('geo-edit-modes').style.display = 'flex';
|
||
el('geo-edit-title-text').textContent = 'Edit Geometri: ' + el('sp-sub').textContent;
|
||
setGeoMode('vertex');
|
||
initGeo();
|
||
} else {
|
||
activeTab=tab;
|
||
el('tab-data').className='sp-tab active';
|
||
el('tab-geo').className ='sp-tab';
|
||
el('pane-data').style.display='';
|
||
el('pane-geo').style.display ='none';
|
||
clearGeo();
|
||
}
|
||
}
|
||
|
||
function exitGeoEdit(saveChanges){
|
||
if (saveChanges) {
|
||
if (activeType === 'jalan') {
|
||
var totalDist = 0;
|
||
for (var i = 0; i < editLL.length - 1; i++) {
|
||
var p1 = L.latLng(editLL[i]), p2 = L.latLng(editLL[i+1]);
|
||
totalDist += p1.distanceTo(p2);
|
||
}
|
||
el('sp-ukuran').value = totalDist.toFixed(1) + ' m';
|
||
} else if (activeType === 'parsil') {
|
||
el('sp-ukuran').value = 'Diperbarui setelah disimpan';
|
||
}
|
||
showToast('Geometri diperbarui sementara di peta. Klik Simpan untuk menyimpan ke database.', 'success');
|
||
} else {
|
||
editLL = originalLL.map(function(p){return [p[0], p[1]];});
|
||
updateGhost();
|
||
}
|
||
clearGeo();
|
||
el('geo-edit-bar').style.display = 'none';
|
||
el('side-panel').classList.add('open');
|
||
|
||
activeTab='data';
|
||
el('tab-data').className='sp-tab active';
|
||
el('tab-geo').className ='sp-tab';
|
||
el('pane-data').style.display='';
|
||
el('pane-geo').style.display ='none';
|
||
}
|
||
|
||
function startPointDrag() {
|
||
el('side-panel').classList.remove('open');
|
||
var marker = rusakLayers[activeId];
|
||
if (marker) {
|
||
marker.dragging.enable();
|
||
marker.bindTooltip("Seret saya ke lokasi baru!", {permanent:true, direction:'top'}).openTooltip();
|
||
|
||
var bar = el('geo-edit-bar');
|
||
bar.style.display = 'flex';
|
||
el('geo-edit-modes').style.display = 'none';
|
||
el('geo-edit-title-text').textContent = 'Geser Penanda Jalan Rusak';
|
||
}
|
||
}
|
||
|
||
function exitPointDrag(save) {
|
||
var marker = rusakLayers[activeId];
|
||
if (marker) {
|
||
marker.dragging.disable();
|
||
marker.unbindTooltip();
|
||
if (save) {
|
||
var newPos = marker.getLatLng();
|
||
rusakData[activeId].lat = newPos.lat;
|
||
rusakData[activeId].lng = newPos.lng;
|
||
el('sp-koordinat-rusak').value = newPos.lat.toFixed(6) + ', ' + newPos.lng.toFixed(6);
|
||
showToast('Lokasi baru tersimpan sementara.', 'success');
|
||
} else {
|
||
marker.setLatLng([originalLat, originalLng]);
|
||
rusakData[activeId].lat = originalLat;
|
||
rusakData[activeId].lng = originalLng;
|
||
}
|
||
}
|
||
el('geo-edit-bar').style.display = 'none';
|
||
el('side-panel').classList.add('open');
|
||
}
|
||
|
||
function exitFloatingGeoEdit(save) {
|
||
if (activeType === 'rusak') {
|
||
exitPointDrag(save);
|
||
} else {
|
||
exitGeoEdit(save);
|
||
}
|
||
}
|
||
|
||
function spHandleFoto(input){var f=input.files[0];if(!f)return;if(f.size>5*1024*1024){showToast('Foto maks 5MB!','error');return;}var r=new FileReader();r.onload=function(e){var img=new Image();img.onload=function(){var maxDim=1280,w=img.width,h=img.height;if(w>h&&w>maxDim){h=Math.round(h*maxDim/w);w=maxDim;}else if(h>=w&&h>maxDim){w=Math.round(w*maxDim/h);h=maxDim;}var c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);spFotoBase64=c.toDataURL('image/jpeg',0.7);el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.onerror=function(){spFotoBase64=e.target.result;el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.src=e.target.result;};r.readAsDataURL(f);}
|
||
function spClearFoto(){spFotoBase64='';el('sp-foto-preview').style.display='none';el('sp-foto-file').value='';el('sp-foto-clear').style.display='none';}
|
||
|
||
/* ══ GEOMETRY EDIT ═══════════════════════════════════ */
|
||
var editLL=[],originalLL=[],originalLat=null,originalLng=null,ghostLayer=null,vtxMarkers=[],midMarkers=[],geoMode=null;
|
||
var moveActive=false,movePrev=null;
|
||
|
||
function getColor(){var st=el('sp-status').value;return activeType==='jalan'?WARNA_JALAN[st]||'#6366f1':(WARNA_PARSIL[st]||{color:'#6366f1'}).color;}
|
||
function initGeo(){
|
||
clearGeo();
|
||
var col=getColor();
|
||
if (activeType === 'jalan' && jalanLayers[activeId]) {
|
||
map.removeLayer(jalanLayers[activeId]);
|
||
} else if (activeType === 'parsil' && parsilLayers[activeId]) {
|
||
map.removeLayer(parsilLayers[activeId]);
|
||
}
|
||
ghostLayer=(activeType==='jalan'?L.polyline:L.polygon)(editLL,{color:col,fillColor:col,fillOpacity:.08,weight:3,dashArray:'8,5',interactive:false}).addTo(map);
|
||
}
|
||
function clearGeo(){
|
||
if(ghostLayer){map.removeLayer(ghostLayer);ghostLayer=null;}
|
||
vtxMarkers.forEach(function(m){map.removeLayer(m);});vtxMarkers=[];
|
||
midMarkers.forEach(function(m){map.removeLayer(m);});midMarkers=[];
|
||
stopMove();
|
||
map.off('click', onMapClickForAdd);
|
||
setGeoBtn(null);
|
||
geoMode=null;
|
||
map.dragging.enable();
|
||
if (activeType === 'jalan' && jalanLayers[activeId]) {
|
||
jalanLayers[activeId].addTo(map);
|
||
jalanLayers[activeId].setLatLngs(editLL);
|
||
} else if (activeType === 'parsil' && parsilLayers[activeId]) {
|
||
parsilLayers[activeId].addTo(map);
|
||
parsilLayers[activeId].setLatLngs(editLL);
|
||
}
|
||
}
|
||
function updateGhost(){if(ghostLayer)ghostLayer.setLatLngs(editLL);}
|
||
function onMapClickForAdd(e) {
|
||
if (geoMode === 'add') {
|
||
editLL.push([e.latlng.lat, e.latlng.lng]);
|
||
updateGhost();
|
||
renderVtx();
|
||
}
|
||
}
|
||
function renderVtx(){
|
||
vtxMarkers.forEach(function(m){map.removeLayer(m);});vtxMarkers=[];
|
||
midMarkers.forEach(function(m){map.removeLayer(m);});midMarkers=[];
|
||
editLL.forEach(function(ll,i){
|
||
var isDel=geoMode==='del';
|
||
var m=L.circleMarker([ll[0],ll[1]],{radius:isDel?7:6,color:isDel?'#ef4444':'#6366f1',fillColor:isDel?'#fca5a5':'white',fillOpacity:1,weight:2.5}).addTo(map);
|
||
if(geoMode==='vertex') enableDrag(m,i);
|
||
if(isDel)(function(idx){m.on('click',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);var min=activeType==='jalan'?2:3;if(editLL.length<=min){showToast('Titik sudah minimal!','error');return;}editLL.splice(idx,1);updateGhost();renderVtx();});})(i);
|
||
vtxMarkers.push(m);
|
||
});
|
||
if(geoMode==='add'){var n=editLL.length;var loop=activeType==='parsil';for(var i=0;i<(loop?n:n-1);i++)(function(i){var j=(i+1)%n,mlat=(editLL[i][0]+editLL[j][0])/2,mlng=(editLL[i][1]+editLL[j][1])/2;var mm=L.circleMarker([mlat,mlng],{radius:5,color:'#10b981',fillColor:'#d1fae5',fillOpacity:1,weight:2}).addTo(map);mm.on('click',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);editLL.splice(i+1,0,[mlat,mlng]);updateGhost();renderVtx();});midMarkers.push(mm);})(i);}
|
||
}
|
||
function enableDrag(marker,idx){
|
||
marker.on('mousedown',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);map.dragging.disable();
|
||
var onM=function(ev){editLL[idx]=[ev.latlng.lat,ev.latlng.lng];marker.setLatLng(ev.latlng);updateGhost();};
|
||
var onU=function(){map.dragging.enable();map.off('mousemove',onM);map.off('mouseup',onU);};
|
||
map.on('mousemove',onM);map.on('mouseup',onU);});
|
||
}
|
||
function startMove(){map.dragging.disable();map._mh=function(e){moveActive=true;movePrev=e.latlng;var oM=function(ev){if(!moveActive||!movePrev)return;var dl=ev.latlng.lat-movePrev.lat,dg=ev.latlng.lng-movePrev.lng;editLL=editLL.map(function(p){return[p[0]+dl,p[1]+dg];});movePrev=ev.latlng;updateGhost();vtxMarkers.forEach(function(m,i){m.setLatLng([editLL[i][0],editLL[i][1]]);});};var oU=function(){moveActive=false;movePrev=null;map.off('mousemove',oM);map.off('mouseup',oU);};map.on('mousemove',oM);map.on('mouseup',oU);};map.on('mousedown',map._mh);map.getContainer().style.cursor='grab';}
|
||
function stopMove(){if(map._mh){map.off('mousedown',map._mh);map._mh=null;}map.dragging.enable();map.getContainer().style.cursor='';}
|
||
function setGeoMode(m){
|
||
if(geoMode===m){
|
||
geoMode=null;
|
||
setGeoBtn(null);
|
||
vtxMarkers.forEach(function(mk){map.removeLayer(mk);});
|
||
vtxMarkers=[];
|
||
midMarkers.forEach(function(mk){map.removeLayer(mk);});
|
||
midMarkers=[];
|
||
stopMove();
|
||
map.off('click', onMapClickForAdd);
|
||
return;
|
||
}
|
||
stopMove();
|
||
map.off('click', onMapClickForAdd);
|
||
geoMode=m;
|
||
setGeoBtn(m);
|
||
renderVtx();
|
||
if(m==='move')startMove();
|
||
if(m==='add'){
|
||
map.on('click', onMapClickForAdd);
|
||
}
|
||
}
|
||
function setGeoBtn(m){['vertex','move','add','del'].forEach(function(k){var b=el('geo-btn-'+k);if(b)b.className='geo-btn'+(m===k?' active':''); var bm=el('gbm-'+k); if(bm)bm.className='geo-btn-mini'+(m===k?' active':'');});}
|
||
|
||
/* ══ SUBMIT EDIT ══════════════════════════════════════ */
|
||
function submitEdit(){
|
||
if(!activeId)return;
|
||
var nama=el('sp-nama').value.trim();
|
||
if(!nama){showToast('Nama tidak boleh kosong!','error');return;}
|
||
var btn=el('sp-save-btn');btn.textContent='⏳ Menyimpan…';btn.disabled=true;
|
||
|
||
if(activeType==='rusak'){
|
||
var fd=new FormData();
|
||
fd.append('action','edit_rusak');fd.append('id',activeId);
|
||
fd.append('nama',nama);fd.append('tanggal',el('sp-tanggal').value);
|
||
fd.append('keterangan',el('sp-ket-rusak').value.trim());
|
||
fd.append('foto',spFotoBase64);
|
||
fd.append('latitude',rusakData[activeId].lat);
|
||
fd.append('longitude',rusakData[activeId].lng);
|
||
fetch('api.php?action=edit_rusak&id='+activeId,{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(d){
|
||
btn.textContent='💾 Simpan';btn.disabled=false;
|
||
if(d.error){showToast('Error: '+d.error,'error');return;}
|
||
updateStats(d);
|
||
var rd=rusakData[activeId];
|
||
rd.nama=nama;rd.tgl=el('sp-tanggal').value;rd.ket=el('sp-ket-rusak').value.trim();rd.foto=spFotoBase64;
|
||
rusakLayers[activeId].setPopupContent(makeRusakPopup(activeId,rd.nama,rd.tgl,rd.lat,rd.lng,rd.ket,rd.foto,rd.age||'recent',rd.recurring||false));
|
||
el('sp-sub').textContent=nama;
|
||
closePanel(); // Close modal on success!
|
||
showToast('Data jalan rusak diperbarui!','success');
|
||
}).catch(function(){btn.textContent='💾 Simpan';btn.disabled=false;showToast('Koneksi gagal!','error');});
|
||
return;
|
||
}
|
||
|
||
var status=el('sp-status').value;
|
||
var coords=JSON.stringify(editLL);
|
||
var url='api.php?action=edit_'+(activeType==='jalan'?'jalan':'parsil')+'&id='+activeId+'&nama='+encodeURIComponent(nama)+'&status='+encodeURIComponent(status)+'&koordinat='+encodeURIComponent(coords);
|
||
if(activeType==='parsil'){url+='&pemilik='+encodeURIComponent(el('sp-pemilik').value.trim())+'&nosert='+encodeURIComponent(el('sp-nosert').value.trim())+'&keterangan='+encodeURIComponent(el('sp-ket').value.trim());}
|
||
|
||
fetch(url).then(function(r){return r.json();}).then(function(d){
|
||
btn.textContent='💾 Simpan';btn.disabled=false;
|
||
if(d.error){showToast('Error: '+d.error,'error');return;}
|
||
updateStats(d);
|
||
if(activeType==='jalan'){
|
||
var np=d.panjang!=null?parseFloat(d.panjang).toFixed(1):el('sp-ukuran').value.replace(/[^0-9.]/g,'');
|
||
jalanLayers[activeId].setLatLngs(editLL);jalanLayers[activeId].setStyle({color:WARNA_JALAN[status]||'#999'});
|
||
jalanLayers[activeId].setPopupContent(makeJalanPopup(activeId,nama,status,np));
|
||
jalanData[activeId]=Object.assign(jalanData[activeId],{nama:nama,status:status,pjg:np,ll:editLL.map(function(p){return[p[0],p[1]];})});
|
||
el('sp-ukuran').value=np+' m';
|
||
} else {
|
||
var pm=el('sp-pemilik').value.trim(),pn=el('sp-nosert').value.trim(),pk=el('sp-ket').value.trim();
|
||
var nl=d.luas!=null?parseFloat(d.luas).toFixed(1):el('sp-ukuran').value.replace(/[^0-9.]/g,'');
|
||
parsilLayers[activeId].setLatLngs(editLL);parsilLayers[activeId].setStyle(WARNA_PARSIL[status]||{});
|
||
parsilLayers[activeId].setPopupContent(makeParsilPopup(activeId,nama,pm,pn,pk,status,nl));
|
||
parsilData[activeId]=Object.assign(parsilData[activeId],{nama:nama,status:status,pemilik:pm,nosert:pn,ket:pk,luas:nl,ll:editLL.map(function(p){return[p[0],p[1]];})});
|
||
el('sp-ukuran').value=nl+' m²';
|
||
}
|
||
el('sp-sub').textContent=nama;if(activeTab==='geo')updateGhost();
|
||
showToast('Data berhasil disimpan!','success');
|
||
}).catch(function(){btn.textContent='💾 Simpan';btn.disabled=false;showToast('Koneksi gagal!','error');});
|
||
}
|
||
|
||
/* ══ TOAST ════════════════════════════════════════════ */
|
||
function showToast(msg,type){
|
||
var old=document.getElementById('__t');if(old)old.remove();
|
||
var t=document.createElement('div');t.id='__t';t.textContent=msg;
|
||
t.style.cssText='position:fixed;bottom:28px;left:50%;transform:translateX(-50%);z-index:99999;padding:10px 22px;border-radius:24px;font-size:13px;font-weight:600;font-family:Inter,sans-serif;box-shadow:0 4px 20px rgba(0,0,0,.14);transition:opacity .3s;white-space:nowrap;'+(type==='success'?'background:#f0fdf4;color:#15803d;border:1px solid #86efac;':'background:#fef2f2;color:#dc2626;border:1px solid #fca5a5;');
|
||
document.body.appendChild(t);
|
||
setTimeout(function(){t.style.opacity='0';setTimeout(function(){t.remove();},300);},2800);
|
||
}
|
||
|
||
/* ══ LEFT NAV BAR ════════════════════════════════════ */
|
||
var lnavTipe='semua', lnavStatus='', lnavPmin=0, lnavPmax=0;
|
||
var lnavSearchTimer=null;
|
||
var lnavActiveId=null, lnavActiveType=null;
|
||
|
||
// Semua data dikumpulkan setelah load (dipopulate oleh PHP loop di atas)
|
||
// jalanData, parsilData, rusakData sudah tersedia
|
||
|
||
function lnavResetStatusChips(){
|
||
lnavStatus='';
|
||
document.querySelectorAll('.filter-chip[data-group="status"]').forEach(function(c){c.classList.remove('active');});
|
||
document.querySelectorAll('.filter-chip[data-group="status"][data-val=""]').forEach(function(c){c.classList.add('active');});
|
||
}
|
||
|
||
function lnavUpdateFilterPanels(){
|
||
var t=lnavTipe;
|
||
var jalanBlock=document.getElementById('lnav-jalan-block');
|
||
var parsilBlock=document.getElementById('lnav-parsil-block');
|
||
var panjangRow=document.getElementById('lnav-panjang-row');
|
||
if(jalanBlock) jalanBlock.style.display=(t==='semua'||t==='jalan')?'flex':'none';
|
||
if(parsilBlock) parsilBlock.style.display=(t==='semua'||t==='parsil')?'flex':'none';
|
||
if(panjangRow) panjangRow.style.display=(t==='parsil'||t==='rusak')?'none':'flex';
|
||
}
|
||
|
||
function lnavChip(el){
|
||
var g=el.getAttribute('data-group'), v=el.getAttribute('data-val');
|
||
if(g==='tipe'){
|
||
document.querySelectorAll('.filter-seg-btn[data-group="tipe"]').forEach(function(c){c.classList.remove('active');});
|
||
el.classList.add('active');
|
||
lnavTipe=v;
|
||
lnavResetStatusChips();
|
||
lnavUpdateFilterPanels();
|
||
}
|
||
if(g==='status'){
|
||
document.querySelectorAll('.filter-chip[data-group="status"]').forEach(function(c){c.classList.remove('active');});
|
||
el.classList.add('active');
|
||
lnavStatus=v;
|
||
}
|
||
lnavRender();
|
||
}
|
||
|
||
lnavUpdateFilterPanels();
|
||
|
||
// Close side panel when clicking outside the sp-card
|
||
var spEl = document.getElementById('side-panel');
|
||
if (spEl) {
|
||
spEl.addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closePanel();
|
||
}
|
||
});
|
||
}
|
||
|
||
var lnavInputEl = document.getElementById('lnav-input');
|
||
if (lnavInputEl) {
|
||
lnavInputEl.addEventListener('input', function(){
|
||
clearTimeout(lnavSearchTimer);
|
||
lnavSearchTimer=setTimeout(lnavRender, 200);
|
||
});
|
||
}
|
||
var lnavPminEl = document.getElementById('lnav-pmin');
|
||
var lnavPmaxEl = document.getElementById('lnav-pmax');
|
||
if (lnavPminEl) lnavPminEl.addEventListener('input', function(){clearTimeout(lnavSearchTimer);lnavSearchTimer=setTimeout(lnavRender,300);});
|
||
if (lnavPmaxEl) lnavPmaxEl.addEventListener('input', function(){clearTimeout(lnavSearchTimer);lnavSearchTimer=setTimeout(lnavRender,300);});
|
||
|
||
function lnavRender(){
|
||
var inputEl=document.getElementById('lnav-input');
|
||
if(!inputEl)return;
|
||
var q=inputEl.value.trim().toLowerCase();
|
||
lnavPmin=parseFloat(document.getElementById('lnav-pmin').value)||0;
|
||
lnavPmax=parseFloat(document.getElementById('lnav-pmax').value)||0;
|
||
var results=[];
|
||
|
||
var jalanStatuses=['Jalan Nasional','Jalan Provinsi','Jalan Kabupaten'];
|
||
var parsilStatuses=['SHM','HGB','HGU','HP'];
|
||
|
||
// ── Jalan ──
|
||
if(lnavTipe==='semua'||lnavTipe==='jalan'){
|
||
Object.values(jalanData).forEach(function(d){
|
||
if(lnavStatus && parsilStatuses.indexOf(lnavStatus)>=0) return;
|
||
if(lnavStatus && d.status!==lnavStatus) return;
|
||
if(lnavPmin>0 && parseFloat(d.pjg)<lnavPmin) return;
|
||
if(lnavPmax>0 && parseFloat(d.pjg)>lnavPmax) return;
|
||
if(q && d.nama.toLowerCase().indexOf(q)<0) return;
|
||
results.push({type:'jalan',id:d.id,icon:'🛣',name:d.nama,meta:d.status+' · '+parseFloat(d.pjg).toFixed(0)+' m',badge:'Jalan',badgeCls:'lnav-badge-jalan'});
|
||
});
|
||
}
|
||
|
||
// ── Parsil ──
|
||
if(lnavTipe==='semua'||lnavTipe==='parsil'){
|
||
Object.values(parsilData).forEach(function(d){
|
||
if(lnavStatus && jalanStatuses.indexOf(lnavStatus)>=0) return;
|
||
if(lnavStatus && d.status!==lnavStatus) return;
|
||
if(q && d.nama.toLowerCase().indexOf(q)<0 && (!d.pemilik||d.pemilik.toLowerCase().indexOf(q)<0)) return;
|
||
results.push({type:'parsil',id:d.id,icon:'🏘',name:d.nama,meta:d.status+(d.pemilik?' · '+d.pemilik:''),badge:'Kavling',badgeCls:'lnav-badge-parsil'});
|
||
});
|
||
}
|
||
|
||
// ── Rusak ──
|
||
if(lnavTipe==='semua'||lnavTipe==='rusak'){
|
||
Object.values(rusakData).forEach(function(d){
|
||
if(q && d.nama.toLowerCase().indexOf(q)<0) return;
|
||
results.push({type:'rusak',id:d.id,icon:'⚠',name:d.nama,meta:'📅 '+d.tgl,badge:'Rusak',badgeCls:'lnav-badge-rusak'});
|
||
});
|
||
}
|
||
|
||
// Sort: jalan asc nama, parsil asc nama, rusak desc tgl
|
||
results.sort(function(a,b){
|
||
if(a.type!==b.type) return a.type<b.type?-1:1;
|
||
return a.name.localeCompare(b.name,'id');
|
||
});
|
||
|
||
// Count
|
||
var countEl=document.getElementById('lnav-count');
|
||
countEl.textContent=results.length+' data ditemukan';
|
||
|
||
var list=document.getElementById('lnav-list');
|
||
if(results.length===0){
|
||
list.innerHTML='<div class="lnav-empty">😕 Tidak ada data<br>yang sesuai filter</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML=results.map(function(r){
|
||
var isActive=(lnavActiveId===r.id && lnavActiveType===r.type);
|
||
return '<div class="lnav-item'+(isActive?' active':'')+'" data-id="'+r.id+'" data-type="'+r.type+'" onclick="lnavFlyTo(this)">'
|
||
+'<span class="lnav-item-icon">'+r.icon+'</span>'
|
||
+'<div class="lnav-item-body">'
|
||
+'<div class="lnav-item-name">'+h(r.name)+'</div>'
|
||
+'<div class="lnav-item-meta">'+h(r.meta)+'</div>'
|
||
+'</div>'
|
||
+'<span class="lnav-item-badge '+r.badgeCls+'">'+r.badge+'</span>'
|
||
+'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
function lnavFlyTo(el){
|
||
var type = el.getAttribute('data-type');
|
||
var id = parseInt(el.getAttribute('data-id'), 10);
|
||
document.querySelectorAll('.lnav-item').forEach(function(i){i.classList.remove('active');});
|
||
el.classList.add('active');
|
||
lnavActiveId=id; lnavActiveType=type;
|
||
|
||
try{
|
||
if(type==='jalan'){
|
||
var d=jalanData[id]; if(!d)return;
|
||
var ll=d.ll; if(!ll||!ll.length)return;
|
||
map.flyToBounds(L.polyline(ll).getBounds().pad(.35),{maxZoom:17,duration:.9});
|
||
setTimeout(function(){if(jalanLayers[id])jalanLayers[id].openPopup();},1000);
|
||
} else if(type==='parsil'){
|
||
var d=parsilData[id]; if(!d)return;
|
||
var ll=d.ll; if(!ll||!ll.length)return;
|
||
map.flyToBounds(L.polygon(ll).getBounds().pad(.35),{maxZoom:18,duration:.9});
|
||
setTimeout(function(){if(parsilLayers[id])parsilLayers[id].openPopup();},1000);
|
||
} else if(type==='rusak'){
|
||
var d=rusakData[id]; if(!d)return;
|
||
map.flyTo([d.lat,d.lng],17,{duration:.9});
|
||
setTimeout(function(){if(rusakLayers[id])rusakLayers[id].openPopup();},1000);
|
||
}
|
||
}catch(e){console.error('lnavFlyTo error:',e);}
|
||
}
|
||
|
||
// Highlight sidebar saat klik layer di peta
|
||
function lnavHighlightFromMap(type, id){
|
||
if(!document.getElementById('lnav-list'))return;
|
||
lnavActiveId=id; lnavActiveType=type;
|
||
document.querySelectorAll('.lnav-item').forEach(function(i){
|
||
var match=(parseInt(i.getAttribute('data-id'))===id && i.getAttribute('data-type')===type);
|
||
i.classList.toggle('active', match);
|
||
if(match) i.scrollIntoView({behavior:'smooth',block:'nearest'});
|
||
});
|
||
}
|
||
|
||
if (document.getElementById('lnav-list')) {
|
||
setTimeout(function(){ lnavRender(); }, 100);
|
||
}
|
||
</script>
|
||
<?php if ($mode === 'input'): ?>
|
||
<script src="assets/jalan-input.js?v=<?= @filemtime(__DIR__ . '/assets/jalan-input.js') ?: time() ?>"></script>
|
||
<?php endif; ?>
|
||
</body>
|
||
</html>
|