Files
UAS_WEBGIS_AnderiSaputra_D1…/infrastruktur/index.php
T
Anderi Saputra adec71e763 first commit
2026-06-10 17:00:19 +07:00

3028 lines
136 KiB
PHP

<?php
session_start();
include 'koneksi.php';
$isAdmin = isset($_SESSION['username']) ? true : false;
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS — Infrastruktur Kota Pontianak</title>
<meta name="description" content="Sistem Informasi Geografis Infrastruktur SPBU, Jalan, dan Kavling Kota Pontianak">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--font: 'Plus Jakarta Sans', sans-serif;
--bg: #ffffff;
--surface: #f8f9fb;
--border: #e4e7ec;
--text: #1a202c;
--text-2: #5a6478;
--text-3: #9aa3b2;
--primary: #ea580c;
--primary-hover: #c2410c;
--primary-light: #fff7ed;
--primary-border: #fed7aa;
--green: #16a34a;
--red: #dc2626;
--orange: #ea580c;
--purple: #7c3aed;
--teal: #0891b2;
--shadow-sm: 0 1px 3px rgba(0,0,0,.08);
--shadow: 0 4px 16px rgba(0,0,0,.10), 0 2px 6px rgba(0,0,0,.07);
--shadow-lg: 0 12px 40px rgba(0,0,0,.15), 0 4px 12px rgba(0,0,0,.08);
--radius: 12px;
--radius-sm: 8px;
--radius-xs: 6px;
}
html, body { height: 100%; font-family: var(--font); font-size: 14px; color: var(--text); }
#map { height: 100vh; z-index: 0; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }
/* ── TOP SEARCH BAR ── */
#topbar {
position: fixed; top: 60px; left: 50%; transform: translateX(-50%);
z-index: 1100; display: flex; align-items: center; gap: 8px;
background: var(--bg); border-radius: var(--radius);
padding: 8px 14px; box-shadow: var(--shadow);
width: 520px; max-width: 92vw;
border: 1px solid var(--border);
}
#topbar .search-icon { color: var(--text-3); font-size: 15px; flex-shrink: 0; }
#searchInput {
flex: 1; border: none; outline: none; font-size: 13.5px;
font-family: var(--font); background: transparent; color: var(--text);
}
#searchInput::placeholder { color: var(--text-3); }
#searchFilter {
border: 1px solid var(--border); border-radius: var(--radius-xs);
padding: 4px 8px; font-size: 12px; background: var(--surface);
cursor: pointer; font-family: var(--font); color: var(--text-2); outline: none;
}
#btnSearch {
background: var(--primary); color: white; border: none; border-radius: var(--radius-xs);
padding: 6px 14px; cursor: pointer; font-size: 12.5px; font-weight: 600;
font-family: var(--font); white-space: nowrap; transition: background .18s;
}
#btnSearch:hover { background: var(--primary-hover); }
/* ── SEARCH RESULTS ── */
#searchResults {
position: fixed; top: 108px; left: 50%; transform: translateX(-50%);
background: var(--bg); border-radius: var(--radius-sm);
box-shadow: var(--shadow); border: 1px solid var(--border);
z-index: 1090; width: 520px; max-width: 92vw;
max-height: 300px; overflow-y: auto; display: none;
}
.search-item {
padding: 10px 14px; cursor: pointer;
border-bottom: 1px solid var(--border);
display: flex; align-items: center; gap: 10px; transition: background .12s;
}
.search-item:hover { background: var(--surface); }
.search-item:last-child { border-bottom: none; }
.search-badge {
font-size: 10.5px; padding: 2px 8px; border-radius: 20px;
color: white; font-weight: 700; white-space: nowrap;
}
.badge-spbu { background: var(--green); }
.badge-jalan { background: var(--orange); }
.badge-kavling { background: var(--purple); }
.badge-rumah { background: var(--teal); }
.badge-tempat-ibadah { background: var(--purple); }
/* ── DRAW HINT ── */
#drawHint {
position: fixed; top: 114px; left: 50%; transform: translateX(-50%);
background: rgba(26,32,44,.88); backdrop-filter: blur(8px);
color: white; padding: 8px 18px; border-radius: var(--radius-sm);
font-size: 12.5px; z-index: 1050; display: none; pointer-events: none;
font-weight: 500;
}
/* ── PICKING INDICATOR ── */
#pickingIndicator {
position: fixed; top: 114px; left: 50%; transform: translateX(-50%);
z-index: 1051; display: none; pointer-events: none;
background: #7c3aed; color: white;
padding: 8px 18px; border-radius: 30px;
font-size: 12.5px; font-weight: 700;
box-shadow: 0 4px 16px rgba(124,58,237,.45);
animation: pickPulse 1.2s ease-in-out infinite;
}
@keyframes pickPulse {
0%,100% { opacity:1; transform: translateX(-50%) scale(1); }
50% { opacity:.7; transform: translateX(-50%) scale(1.04); }
}
/* ── LEFT LEGEND — KECAMATAN ── */
#legendKecamatan {
position: fixed; bottom: 90px; left: 12px; z-index: 1000;
background: var(--bg); border-radius: var(--radius);
padding: 12px 14px; box-shadow: var(--shadow);
border: 1px solid var(--border); min-width: 160px; max-width: 200px;
font-size: 12px; max-height: 260px;
}
#legendKecamatan h4 {
font-size: 11px; font-weight: 700; color: var(--text-2);
text-transform: uppercase; letter-spacing: .6px; margin-bottom: 8px;
}
#legendKecamatanList { overflow-y: auto; max-height: 200px; display: flex; flex-direction: column; gap: 4px; }
.kec-legend-item { display: flex; align-items: center; gap: 7px; }
.kec-legend-swatch { width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; border: 1.5px solid rgba(0,0,0,.18); }
/* ── HEADER BRAND ── */
#headerBrand {
position: fixed; top: 0; left: 0; right: 0; height: 50px;
background: linear-gradient(90deg, #29201d 0%, #4a2418 18%, #713017 38%, #9a3b12 60%, #c24a0c 80%, #ea580c 100%);
z-index: 1300; display: flex; align-items: center;
padding: 0 16px; gap: 12px;
box-shadow: 0 2px 16px rgba(15,23,42,.55);
}
.hb-logo { font-size: 22px; flex-shrink: 0; }
.hb-title-wrap { flex: 1; min-width: 0; }
.hb-title {
font-size: 13.5px; font-weight: 700; color: #fff;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.3;
}
.hb-sub {
font-size: 10px; color: rgba(255,255,255,.6);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; line-height: 1.2;
}
.hb-badge {
background: rgba(255,255,255,.13); border: 1px solid rgba(255,255,255,.22);
border-radius: 20px; padding: 4px 12px;
font-size: 10.5px; color: rgba(255,255,255,.9); font-weight: 600;
white-space: nowrap; flex-shrink: 0; backdrop-filter: blur(4px);
}
.hb-divider { width: 1px; height: 24px; background: rgba(255,255,255,.2); flex-shrink: 0; }
/* ── BASEMAP SWITCHER ── */
#basemapSwitcher {
position: fixed; bottom: 88px; left: 12px; z-index: 1001;
background: var(--bg); border-radius: var(--radius);
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
padding: 8px 8px 6px; min-width: 130px;
}
.bs-label {
font-size: 9.5px; font-weight: 700; color: var(--text-3);
text-transform: uppercase; letter-spacing: .8px;
text-align: center; margin-bottom: 6px;
}
.bs-row { display: flex; gap: 5px; }
.basemap-btn {
display: flex; flex-direction: column; align-items: center; gap: 4px;
background: var(--surface); border: 2px solid var(--border);
border-radius: var(--radius-xs); padding: 5px 6px; cursor: pointer;
transition: all .2s; font-family: var(--font); flex: 1;
}
.basemap-btn:hover { border-color: #fdba74; background: var(--primary-light); }
.basemap-btn.active { border-color: var(--primary); background: var(--primary-light); }
.basemap-btn.active .bs-name { color: var(--primary); font-weight: 700; }
.bs-thumb {
width: 100%; height: 36px; border-radius: 4px;
overflow: hidden; border: 1px solid rgba(0,0,0,.1);
transition: box-shadow .18s; position: relative;
}
.basemap-btn.active .bs-thumb { box-shadow: 0 0 0 2px var(--primary); }
.bs-street {
background: linear-gradient(135deg, #f5f1e8 0%, #ede8da 50%, #e0d9c8 100%);
}
.bs-street::after {
content: ''; position: absolute; inset: 0;
background:
linear-gradient(to bottom, transparent 44%, #d4c87a 44%, #d4c87a 56%, transparent 56%),
linear-gradient(to right, transparent 28%, rgba(255,255,255,.85) 28%, rgba(255,255,255,.85) 42%, transparent 42%),
linear-gradient(135deg, #b8d4a8 0%, #b8d4a8 30%, transparent 30%);
}
.bs-satellite {
background: linear-gradient(135deg, #1a3d10 0%, #2e5c1e 35%, #1c3e0d 65%, #3a6828 100%);
}
.bs-satellite::after {
content: ''; position: absolute; inset: 0;
background:
radial-gradient(ellipse at 25% 35%, rgba(80,130,60,.5) 0%, transparent 45%),
radial-gradient(ellipse at 72% 65%, rgba(50,90,40,.45) 0%, transparent 38%),
linear-gradient(60deg, rgba(255,230,160,.06) 0%, transparent 55%);
}
.bs-name {
font-size: 10px; font-weight: 600; color: var(--text-2);
white-space: nowrap; transition: color .18s;
}
/* ── RIGHT LEGEND ── */
#legend {
position: fixed; top: 50px; right: 0; bottom: 0; z-index: 1000;
background: var(--bg); border-radius: 0;
padding: 18px 16px; box-shadow: -2px 0 16px rgba(0,0,0,.08);
border: none; border-left: 1px solid var(--border); width: 250px;
overflow-y: auto;
}
.legend-header {
background: linear-gradient(90deg, #9a3412 0%, #ea580c 100%);
color: #fff;
padding: 14px 16px;
margin: -18px -16px 14px -16px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border);
}
.legend-header h4 {
font-size: 12px;
font-weight: 700;
color: #fff !important;
margin: 0 !important;
text-transform: uppercase;
letter-spacing: .6px;
display: flex;
align-items: center;
gap: 6px;
}
.legend-section { margin-bottom: 10px; }
.legend-title {
font-weight: 700; font-size: 10.5px; color: var(--text-3); margin-bottom: 5px;
border-bottom: 1px solid var(--border); padding-bottom: 3px;
text-transform: uppercase; letter-spacing: .5px;
}
.legend-item {
display: flex; align-items: center; gap: 8px; margin-bottom: 4px;
cursor: pointer; padding: 3px 5px; border-radius: var(--radius-xs);
transition: background .12s; font-size: 12px;
}
.legend-item:hover { background: var(--surface); }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; border: 2px solid rgba(0,0,0,.15); flex-shrink: 0; }
.legend-line { width: 22px; height: 4px; border-radius: 2px; flex-shrink: 0; }
.legend-polygon { width: 16px; height: 13px; border-radius: 3px; border: 2px solid rgba(0,0,0,.2); flex-shrink: 0; opacity: .8; }
.legend-circle { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--primary); background: rgba(234,88,12,.15); flex-shrink: 0; }
.legend-mosque { width: 16px; height: 16px; flex-shrink: 0; background: #7c3aed; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 9px; }
.legend-sep { height: 1px; background: var(--border); margin: 8px 0; }
.legend-reset { display: flex; justify-content: center; margin-top: 6px; }
.legend-reset span {
font-size: 11.5px; color: var(--primary); font-weight: 700; cursor: pointer;
padding: 4px 10px; border-radius: var(--radius-xs); transition: background .12s;
border: 1px solid var(--primary-border);
}
.legend-reset span:hover { background: var(--primary-light); }
/* ── MODE BAR ── */
#modeBar {
position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
z-index: 1250; display: flex; gap: 5px; background: var(--bg);
border-radius: var(--radius); padding: 7px;
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
flex-wrap: wrap; justify-content: center; max-width: 96vw;
}
.modeBtn {
padding: 7px 13px; border: none; border-radius: var(--radius-xs);
cursor: pointer; font-size: 12.5px; font-weight: 600;
transition: all .2s; background: var(--surface); color: var(--text-2);
font-family: var(--font); white-space: nowrap;
}
.modeBtn:hover { background: #e8ecf3; color: var(--text); }
.modeBtn.active { color: white; }
.modeBtn#btnModeView.active { background: var(--primary); }
.modeBtn#btnModeSPBU.active { background: var(--green); }
.modeBtn#btnModeJalan.active { background: var(--orange); }
.modeBtn#btnModeKavling.active { background: var(--purple); }
.modeBtn#btnModeTempatIbadah.active { background: #7c3aed; }
.modeBtn#btnModeRumah.active { background: var(--teal); }
/* ── FORMS ── */
.form-popup {
position: fixed; top: 116px; right: 266px;
background: var(--bg); padding: 18px; border-radius: var(--radius);
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
display: none; z-index: 1200; width: 290px;
max-height: calc(100vh - 200px); overflow-y: auto;
}
.form-popup h3 {
margin-bottom: 14px; font-size: 14px; color: var(--text); font-weight: 700;
display: flex; align-items: center; gap: 7px;
padding-bottom: 10px; border-bottom: 1px solid var(--border);
cursor: grab; user-select: none;
}
.form-popup h3:active {
cursor: grabbing;
}
.form-popup label { font-size: 11.5px; color: var(--text-2); display: block; margin-bottom: 3px; font-weight: 600; }
.form-popup input, .form-popup select, .form-popup textarea {
width: 100%; margin-bottom: 10px; padding: 8px 10px;
border-radius: var(--radius-xs); border: 1px solid var(--border);
font-size: 13px; font-family: var(--font); outline: none;
transition: border-color .15s; color: var(--text); background: var(--bg);
}
.form-popup input:focus, .form-popup select:focus, .form-popup textarea:focus { border-color: var(--primary); box-shadow: 0 0 0 3px rgba(234,88,12,.1); }
.form-popup input[readonly], .form-popup textarea[readonly] { background: var(--surface); color: var(--text-2); }
.form-row { display: flex; gap: 8px; }
.form-row .btn { flex: 1; }
/* ── MASJID PANEL ── */
#panelTempatIbadah {
position: fixed; top: 116px; right: 266px;
background: var(--bg); padding: 18px; border-radius: var(--radius);
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
display: none; z-index: 1200; width: 300px;
max-height: calc(100vh - 200px); overflow-y: auto;
}
#panelTempatIbadah h3 {
font-size: 14px; font-weight: 700; margin-bottom: 14px;
color: var(--text); display: flex; align-items: center; gap: 7px;
padding-bottom: 10px; border-bottom: 1px solid var(--border);
cursor: grab; user-select: none;
}
#panelTempatIbadah h3:active {
cursor: grabbing;
}
.panel-section-title {
font-size: 11px; font-weight: 700; color: var(--text-3);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; margin-top: 12px;
}
.radius-row { display: flex; gap: 6px; align-items: center; margin-bottom: 8px; }
.radius-row select, .radius-row input[type=number] {
flex: 1; padding: 7px 8px; border-radius: var(--radius-xs);
border: 1px solid var(--border); font-size: 12.5px;
font-family: var(--font); outline: none; color: var(--text);
}
input[type=range] { width: 100%; accent-color: #7c3aed; cursor: pointer; margin-bottom: 4px; }
.radius-val-label { text-align: center; font-size: 12.5px; font-weight: 700; color: #7c3aed; margin-bottom: 8px; }
.loc-btn {
width: 100%; padding: 9px; border-radius: var(--radius-xs); border: none;
font-size: 12.5px; font-weight: 600; cursor: pointer; font-family: var(--font);
margin-bottom: 7px; transition: all .18s; display: flex; align-items: center;
justify-content: center; gap: 6px;
}
.loc-btn-gps { background: var(--primary-light); color: var(--primary); border: 1.5px solid var(--primary-border); }
.loc-btn-gps:hover { background: #ffedd5; }
.loc-btn-map { background: #f5f3ff; color: #7c3aed; border: 1.5px solid #ddd6fe; }
.loc-btn-map:hover { background: #ede9fe; }
.coord-display {
background: var(--surface); border-radius: var(--radius-xs); padding: 8px 10px;
font-size: 11.5px; color: var(--text-2); margin-bottom: 10px;
border: 1px solid var(--border); font-family: monospace; min-height: 34px;
}
/* ── MASJID LIST ── */
#tempatIbadahListWrap {
margin-top: 12px; border-top: 1px solid var(--border); padding-top: 10px;
}
#tempatIbadahListWrap h5 {
font-size: 11px; font-weight: 700; color: var(--text-3);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 8px;
}
.tempat-ibadah-list-item {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-xs); padding: 8px 10px; margin-bottom: 6px;
display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.tempat-ibadah-list-info { flex: 1; min-width: 0; }
.tempat-ibadah-list-name { font-size: 12.5px; font-weight: 700; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tempat-ibadah-list-radius { font-size: 11px; color: var(--text-3); margin-top: 1px; }
.tempat-ibadah-list-actions { display: flex; gap: 4px; flex-shrink: 0; }
.btn-icon {
width: 26px; height: 26px; border: none; border-radius: 5px; cursor: pointer;
font-size: 12px; display: flex; align-items: center; justify-content: center;
transition: opacity .15s;
}
.btn-icon:hover { opacity: .8; }
.btn-icon-fly { background: #ffedd5; color: var(--primary); }
.btn-icon-del { background: #fee2e2; color: var(--red); }
/* ── BUTTONS ── */
.btn {
padding: 8px 14px; border: none; border-radius: var(--radius-xs);
cursor: pointer; font-size: 12.5px; font-weight: 600; font-family: var(--font);
transition: all .18s;
}
.btn-save { background: #16a34a; color: white; }
.btn-save:hover { background: #15803d; }
.btn-cancel { background: #dc2626; color: white; }
.btn-cancel:hover { background: #b91c1c; }
.btn-primary { background: var(--primary); color: white; }
.btn-primary:hover { background: var(--primary-hover); }
.btn-purple { background: #7c3aed; color: white; }
.btn-purple:hover { background: #6d28d9; }
.btn-orange { background: var(--orange); color: white; }
.btn-orange:hover { background: #c2410c; }
.btn-teal { background: var(--teal); color: white; }
.btn-teal:hover { background: #0e7490; }
/* ── FINISH / UNDO ── */
#btnFinishDraw {
position: fixed; bottom: 76px; right: 266px; z-index: 1050; display: none;
background: var(--primary); color: white; border: none; border-radius: var(--radius-sm);
padding: 10px 20px; font-size: 13.5px; font-weight: 700; cursor: pointer;
box-shadow: var(--shadow); font-family: var(--font);
}
#btnFinishDraw:hover { background: var(--primary-hover); }
#btnUndoDraw {
position: fixed; bottom: 76px; right: 424px; z-index: 1050; display: none;
background: var(--orange); color: white; border: none; border-radius: var(--radius-sm);
padding: 10px 20px; font-size: 13.5px; font-weight: 700; cursor: pointer;
box-shadow: var(--shadow); font-family: var(--font);
}
#btnUndoDraw:hover { background: #c2410c; }
/* ── NOTIFICATION ── */
#notif {
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
background: var(--green); color: white; padding: 10px 24px;
border-radius: var(--radius-sm); display: none; z-index: 2000;
font-size: 13px; font-weight: 600; box-shadow: var(--shadow);
pointer-events: none; white-space: nowrap;
}
/* ── POPUP ── */
.leaflet-popup-content-wrapper {
border-radius: var(--radius-sm) !important;
box-shadow: var(--shadow) !important;
border: 1px solid var(--border) !important;
}
.leaflet-popup-content { font-size: 13px; min-width: 190px; font-family: var(--font); }
.leaflet-popup-content b { font-size: 13.5px; color: var(--text); }
.popup-actions { margin-top: 9px; display: flex; gap: 6px; }
.popup-actions button {
flex: 1; padding: 6px; border: none; border-radius: 5px;
cursor: pointer; font-size: 11.5px; font-weight: 700; font-family: var(--font);
transition: opacity .15s;
}
.popup-actions button:hover { opacity: .85; }
.btn-hapus { background: var(--red); color: white; }
.btn-edit { background: var(--primary); color: white; }
.popup-status-in { color: #15803d; font-weight: 700; font-size: 12.5px; margin: 4px 0; }
.popup-status-out { color: var(--red); font-weight: 700; font-size: 12.5px; margin: 4px 0; }
.popup-addr { font-size: 11.5px; color: var(--text-2); margin: 3px 0; line-height: 1.5; }
.btn-detail-rumah {
width: 100%; margin-top: 8px; padding: 7px 10px;
background: linear-gradient(90deg, #c2410c 0%, #ea580c 100%);
color: white; border: none; border-radius: 6px;
font-size: 11.5px; font-weight: 700; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 5px;
box-shadow: 0 2px 4px rgba(234,88,12,.2);
transition: transform 0.15s, opacity 0.15s;
font-family: var(--font);
}
.btn-detail-rumah:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* ── DETAIL RUMAH MODAL ── */
#detailRumahModal {
position: fixed; inset: 0; background: rgba(0,0,0,.45);
z-index: 2200; display: none; align-items: center; justify-content: center;
backdrop-filter: blur(2px);
}
#detailRumahModal.show { display: flex; }
.detail-modal-box {
background: var(--bg); border-radius: var(--radius);
width: 480px; max-width: 90%; box-shadow: var(--shadow-xl); border: 1px solid var(--border);
overflow: hidden; animation: modalFadeIn 0.25s ease-out;
display: flex; flex-direction: column;
}
@keyframes modalFadeIn {
from { transform: scale(0.95); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
.detail-modal-header {
background: linear-gradient(90deg, #9a3412 0%, #ea580c 100%);
padding: 15px 20px; color: white; display: flex; justify-content: space-between; align-items: center;
}
.detail-modal-header h3 { margin: 0; font-size: 14.5px; font-weight: 700; color: white; display: flex; align-items: center; gap: 8px; }
.detail-modal-header .close-btn {
background: transparent; border: none; color: white; font-size: 22px; cursor: pointer; line-height: 1; opacity: 0.8; transition: opacity 0.15s;
}
.detail-modal-header .close-btn:hover { opacity: 1; }
.detail-modal-body {
padding: 20px; max-height: calc(100vh - 160px); overflow-y: auto;
}
.detail-section-title {
font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--primary);
border-bottom: 2px solid var(--border); padding-bottom: 6px; margin: 16px 0 10px 0;
display: flex; align-items: center; gap: 6px;
}
.detail-section-title:first-of-type { margin-top: 0; }
.detail-grid {
display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;
}
.detail-item {
display: flex; flex-direction: column;
}
.detail-item.full-width {
grid-column: span 2;
}
.detail-label {
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-3); font-weight: 700; margin-bottom: 4px;
}
.detail-value {
font-size: 13px; color: var(--text); font-weight: 500; background: var(--surface); padding: 8px 10px; border-radius: 6px; border: 1px solid var(--border);
min-height: 33px; display: flex; align-items: center; word-break: break-word;
}
.detail-badge-in {
display: inline-flex; align-items: center; gap: 4px; background: #dcfce7; color: #166534; font-weight: 700; padding: 4px 8px; border-radius: 4px; font-size: 11px;
}
.detail-badge-out {
display: inline-flex; align-items: center; gap: 4px; background: #fee2e2; color: #991b1b; font-weight: 700; padding: 4px 8px; border-radius: 4px; font-size: 11px;
}
.rumah-bantuan-item-hover:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0,0,0,0.08) !important;
border-color: #cbd5e1 !important;
}
/* ── EDIT MODAL ── */
#editModal {
position: fixed; inset: 0; background: rgba(0,0,0,.42);
z-index: 2100; display: none; align-items: center; justify-content: center;
}
#editModal.show { display: flex; }
#editModalBox {
background: var(--bg); border-radius: var(--radius); padding: 22px 24px;
width: 330px; box-shadow: var(--shadow-lg); border: 1px solid var(--border);
}
#editModalBox h3 { font-size: 14.5px; color: var(--text); margin-bottom: 14px; font-weight: 700; }
#editModalBox label { font-size: 11.5px; color: var(--text-2); display: block; margin-bottom: 3px; font-weight: 600; }
#editModalBox input, #editModalBox select {
width: 100%; margin-bottom: 10px; padding: 8px 10px;
border-radius: var(--radius-xs); border: 1px solid var(--border); font-size: 13px;
font-family: var(--font); outline: none; color: var(--text);
}
#editModalBox input[readonly] { background: var(--surface); color: var(--text-3); }
.edit-hint {
background: var(--primary-light); border-left: 3px solid var(--primary);
padding: 8px 11px; border-radius: 4px; font-size: 11.5px;
color: #9a3412; margin-bottom: 12px; line-height: 1.6;
}
.modal-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 4px; }
.modal-actions .btn { flex: 1; min-width: 70px; }
/* ── CUSTOM MARKERS ── */
.tempat-ibadah-marker-icon {
background: #7c3aed; color: white; border-radius: 50% 50% 50% 0;
transform: rotate(-45deg); width: 36px; height: 36px;
display: flex; align-items: center; justify-content: center;
border: 3px solid white; box-shadow: 0 3px 10px rgba(0,0,0,.25); font-size: 15px;
}
.tempat-ibadah-marker-icon span { transform: rotate(45deg); display: block; }
.map-picking-tempat-ibadah { cursor: crosshair !important; }
/* ── STAT BADGE ── */
.stat-row {
display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap;
}
.stat-badge {
flex: 1; min-width: 60px; background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-xs); padding: 6px 8px; text-align: center;
}
.stat-badge .stat-num { font-size: 16px; font-weight: 700; color: var(--text); }
.stat-badge .stat-lbl { font-size: 10px; color: var(--text-3); margin-top: 1px; }
/* ── MASJID POPUP EDITOR ── */
.tempat-ibadah-popup-wrap { min-width: 220px; }
.tempat-ibadah-popup-wrap h4 { font-size: 13.5px; font-weight: 700; margin-bottom: 6px; }
.tempat-ibadah-popup-addr { font-size: 11.5px; color: var(--text-2); margin-bottom: 10px; line-height: 1.5; }
.re-label { font-size: 10.5px; color: var(--text-3); font-weight: 700; text-transform: uppercase; letter-spacing: .5px; margin-bottom: 3px; }
.re-val { text-align: center; font-size: 13px; font-weight: 700; color: #7c3aed; margin-bottom: 5px; }
/* ── PROFESSIONAL POLISH ── */
.leaflet-control-zoom {
border: none !important;
box-shadow: var(--shadow) !important;
}
.leaflet-control-zoom a {
border-radius: var(--radius-xs) !important;
font-weight: 700 !important;
color: var(--text) !important;
border-color: var(--border) !important;
}
.leaflet-control-zoom a:hover { background: var(--surface) !important; }
/* Scrollbar peta sidebar */
#panelTempatIbadah::-webkit-scrollbar { width: 4px; }
#panelTempatIbadah::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* Animasi panel masuk */
.form-popup, #panelTempatIbadah {
animation: none;
}
.form-popup[style*="block"], #panelTempatIbadah[style*="block"] {
animation: slideIn .18s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
/* ── DASHBOARD PANEL ── */
#dashboardPanel {
position: fixed; top: 116px; left: 12px; z-index: 1050;
background: var(--bg); border-radius: var(--radius);
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
width: 310px; display: none; overflow: hidden;
max-height: calc(100vh - 150px);
}
.dp-header {
background: linear-gradient(135deg,#9a3412,#ea580c);
color:#fff; padding:11px 14px; display:flex; align-items:center; justify-content:space-between;
}
.dp-header h3 { font-size:13px; font-weight:700; margin:0; }
.dp-close {
background:rgba(255,255,255,.2); border:none; color:#fff;
border-radius:50%; width:22px; height:22px; cursor:pointer;
font-size:14px; display:flex; align-items:center; justify-content:center;
}
.dp-body { padding:12px; overflow-y:auto; max-height:calc(100vh - 220px); }
.dp-grid { display:grid; grid-template-columns:1fr 1fr; gap:7px; margin-bottom:10px; }
.dp-card {
background:var(--surface); border:1px solid var(--border);
border-radius:var(--radius-xs); padding:9px; text-align:center;
}
.dp-card.green { border-color:#bbf7d0; background:#f0fdf4; }
.dp-card.red { border-color:#fecaca; background:#fef2f2; }
.dp-card.blue { border-color:#bfdbfe; background:#eff6ff; }
.dp-card.amber { border-color:#fde68a; background:#fffbeb; }
.dp-num { font-size:20px; font-weight:800; color:var(--text); line-height:1; }
.dp-card.green .dp-num { color:#16a34a; }
.dp-card.red .dp-num { color:#dc2626; }
.dp-card.blue .dp-num { color:#2563eb; }
.dp-card.amber .dp-num { color:#d97706; }
.dp-lbl { font-size:9.5px; color:var(--text-3); margin-top:3px; font-weight:700; text-transform:uppercase; letter-spacing:.4px; }
.dp-chart-wrap { position:relative; height:150px; margin:8px 0; }
.dp-sec { font-size:10px; font-weight:700; color:var(--text-3); text-transform:uppercase; letter-spacing:.5px; margin:10px 0 5px; border-top:1px solid var(--border); padding-top:9px; }
.dp-kec-row { display:flex; align-items:center; gap:5px; margin-bottom:4px; }
.dp-kec-bar-bg { flex:1; background:#f0f0f0; border-radius:3px; height:7px; overflow:hidden; }
.dp-kec-bar { height:7px; border-radius:3px; background:var(--primary); transition:width .4s; }
.dp-kec-lbl { font-size:10px; color:var(--text-2); min-width:70px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.dp-kec-cnt { font-size:10px; font-weight:700; color:var(--text); min-width:16px; text-align:right; }
/* ── FILTER PANEL ── */
#filterPanel {
position: fixed; top: 116px; left: 12px; z-index: 1050;
background: var(--bg); border-radius: var(--radius);
box-shadow: var(--shadow-lg); border: 1px solid var(--border);
width: 260px; display: none; overflow: hidden;
}
.fp-header {
background: linear-gradient(135deg,#7c3aed,#6d28d9);
color:#fff; padding:10px 14px; display:flex; align-items:center; justify-content:space-between;
}
.fp-header h3 { font-size:13px; font-weight:700; margin:0; }
.fp-close {
background:rgba(255,255,255,.2); border:none; color:#fff;
border-radius:50%; width:22px; height:22px; cursor:pointer; font-size:13px;
display:flex; align-items:center; justify-content:center;
}
.fp-body { padding:12px 14px; }
.fp-body label { font-size:11px; color:var(--text-2); display:block; margin-bottom:3px; font-weight:600; }
.fp-body select {
width:100%; margin-bottom:8px; padding:7px 9px;
border-radius:var(--radius-xs); border:1px solid var(--border);
font-size:12.5px; font-family:var(--font); outline:none; color:var(--text);
}
/* ── MODALS (QR, HISTORI, CSV) ── */
.big-modal {
position:fixed; inset:0; background:rgba(0,0,0,.5);
z-index:3000; display:none; align-items:center; justify-content:center;
}
.big-modal.show { display:flex; }
.modal-box {
background:var(--bg); border-radius:var(--radius); padding:22px 24px;
box-shadow:var(--shadow-lg); max-height:90vh; overflow-y:auto; width:380px;
}
.modal-box.sm { width:290px; }
.modal-box h3 { font-size:14px; font-weight:700; margin-bottom:12px; color:var(--text); display:flex; align-items:center; gap:7px; padding-bottom:10px; border-bottom:1px solid var(--border); }
.modal-close-row { display:flex; justify-content:flex-end; margin-top:14px; }
#qrCodeDiv { display:flex; justify-content:center; margin:14px 0; }
.histori-item {
border-left:3px solid var(--primary); padding:8px 10px; margin-bottom:8px;
background:var(--surface); border-radius:0 var(--radius-xs) var(--radius-xs) 0;
}
.hi-date { font-size:10px; font-weight:700; color:var(--text-3); }
.hi-jenis { font-size:12.5px; font-weight:700; color:var(--text); margin:2px 0; }
.hi-ket { font-size:11px; color:var(--text-2); }
.hi-petugas { font-size:10px; color:var(--text-3); margin-top:2px; }
.form-histori { margin-top:12px; border-top:1px solid var(--border); padding-top:12px; }
.form-histori label { font-size:11px; color:var(--text-2); display:block; margin-bottom:3px; font-weight:600; }
.form-histori input,.form-histori select,.form-histori textarea {
width:100%; margin-bottom:8px; padding:7px 9px;
border-radius:var(--radius-xs); border:1px solid var(--border);
font-size:12.5px; font-family:var(--font); outline:none; color:var(--text);
}
.csv-hint { background:#f0fdf4; border:1px solid #bbf7d0; border-radius:var(--radius-xs); padding:9px 11px; font-size:10.5px; font-family:monospace; color:#166534; margin-bottom:12px; word-break:break-all; line-height:1.6; }
/* ── ALERT BADGE ── */
#alertBadge {
position:fixed; top:62px; right:266px; z-index:1400; display:none;
}
.alert-pill {
background:#dc2626; color:#fff; border-radius:20px; padding:5px 12px;
font-size:11.5px; font-weight:700; box-shadow:var(--shadow); cursor:pointer;
display:flex; align-items:center; gap:6px;
animation:apulse 2s ease-in-out infinite;
}
@keyframes apulse { 0%,100%{box-shadow:0 0 0 0 rgba(220,38,38,.4)} 50%{box-shadow:0 0 0 8px rgba(220,38,38,0)} }
/* ── MODEBAR ENHANCEMENTS ── */
.modebar-sep { width:1px; height:22px; background:var(--border); align-self:center; margin:0 2px; flex-shrink:0; }
.modeBtn-tool { font-size:11.5px !important; padding:6px 10px !important; }
.modeBtn-tool:hover { background:var(--primary-light) !important; color:var(--primary) !important; }
.modeBtn-tool.heatmap-on { background:#ea580c !important; color:#fff !important; }
/* ── KATEGORI KEMISKINAN BADGE ── */
.kat-badge {
display:inline-block; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px; margin-left:4px;
}
.kat-sm { background:#fee2e2; color:#dc2626; }
.kat-m { background:#fef3c7; color:#b45309; }
.kat-hm { background:#dbeafe; color:#1d4ed8; }
</style>
</head>
<body>
<!-- HEADER BRAND -->
<div id="headerBrand">
<span class="hb-logo">🏗️</span>
<div class="hb-title-wrap">
<div class="hb-title">WebGIS Infrastruktur</div>
<div class="hb-sub">Sistem Informasi Geografis SPBU, Jalan, Kavling • Kota Pontianak</div>
</div>
<div class="hb-divider"></div>
<?php if ($isAdmin): ?>
<span class="hb-badge" onclick="exportInfrastrukturExcel()" style="background:#16a34a; border-color:#22c55e; cursor:pointer; display:inline-flex; align-items:center; gap:6px; transition:all 0.2s;" onmouseover="this.style.background='#15803d'; this.style.transform='scale(1.05)';" onmouseout="this.style.background='#16a34a'; this.style.transform='scale(1)';">📊 Unduh Laporan Excel</span>
<div class="hb-divider"></div>
<span class="hb-badge" style="background:#ea580c;border-color:#f97316;">
🏗️ Admin Infrastruktur
</span>
<div class="hb-divider"></div>
<a href="logout.php" class="hb-badge" style="background:#dc2626;border-color:#ef4444;text-decoration:none;cursor:pointer;">🚪 Keluar</a>
<?php else: ?>
<a href="login.php" class="hb-badge" style="background:#ea580c;border-color:#f97316;text-decoration:none;cursor:pointer;">🔑 Masuk Admin</a>
<?php endif; ?>
</div>
<div id="map"></div>
<!-- TOP SEARCH BAR -->
<div id="topbar">
<span class="search-icon">🔍</span>
<input type="text" id="searchInput" placeholder="Cari SPBU, Jalan, Kavling..."
oninput="liveSearch(this.value)"
onkeydown="if(event.key==='Enter')doSearch()"
onfocus="onSearchFocus()">
<select id="searchFilter" onchange="onFilterChange(this.value)">
<option value="all">Semua</option>
<option value="spbu">SPBU</option>
<option value="jalan">Jalan</option>
<option value="kavling">Kavling</option>
</select>
<button id="btnSearch" onclick="doSearch()">Cari</button>
</div>
<div id="searchResults"></div>
<div id="drawHint"></div>
<div id="pickingIndicator">📍 Klik pada peta untuk menentukan lokasi tempat ibadah</div>
<!-- LEGENDA KECAMATAN -->
<div id="legendKecamatan">
<h4>🗺️ Kecamatan</h4>
<div id="legendKecamatanList"></div>
</div>
<!-- LEGENDA UTAMA -->
<div id="legend">
<div class="legend-header">
<h4>🏗️ Legenda Infrastruktur</h4>
</div>
<div class="legend-section">
<div class="legend-title">⛽ SPBU</div>
<div class="legend-item" onclick="filterLayer('spbu','Ya')">
<div class="legend-dot" style="background:#16a34a;"></div><span>SPBU 24 Jam</span>
</div>
<div class="legend-item" onclick="filterLayer('spbu','Tidak')">
<div class="legend-dot" style="background:#dc2626;"></div><span>Tidak 24 Jam</span>
</div>
</div>
<div class="legend-section">
<div class="legend-title">🛣️ Jalan</div>
<div class="legend-item" onclick="filterLayer('jalan','Nasional')">
<div class="legend-line" style="background:#c0392b;"></div><span>Jalan Nasional</span>
</div>
<div class="legend-item" onclick="filterLayer('jalan','Provinsi')">
<div class="legend-line" style="background:#ea580c;"></div><span>Jalan Provinsi</span>
</div>
<div class="legend-item" onclick="filterLayer('jalan','Kabupaten')">
<div class="legend-line" style="background:#ca8a04;"></div><span>Jalan Kabupaten</span>
</div>
</div>
<div class="legend-section">
<div class="legend-title">🏘️ Kavling</div>
<div class="legend-item" onclick="filterLayer('kavling','SHM')">
<div class="legend-polygon" style="background:#3b82f6;"></div><span>SHM (Hak Milik)</span>
</div>
<div class="legend-item" onclick="filterLayer('kavling','HGB')">
<div class="legend-polygon" style="background:#22c55e;"></div><span>HGB (Hak Guna Bangunan)</span>
</div>
<div class="legend-item" onclick="filterLayer('kavling','HGU')">
<div class="legend-polygon" style="background:#a855f7;"></div><span>HGU (Hak Guna Usaha)</span>
</div>
<div class="legend-item" onclick="filterLayer('kavling','HP')">
<div class="legend-polygon" style="background:#ef4444;"></div><span>HP (Hak Pakai)</span>
</div>
</div>
<div class="legend-sep"></div>
<div class="legend-title" style="margin-bottom:6px;">🗺️ Tampilan Peta</div>
<div style="display:flex;gap:5px;margin-bottom:10px;">
<button class="basemap-btn active" id="btnMapStreet" onclick="switchBasemap('street')" style="flex:1;">
<div class="bs-thumb bs-street"></div>
<span class="bs-name">Peta</span>
</button>
<button class="basemap-btn" id="btnMapSatellite" onclick="switchBasemap('satellite')" style="flex:1;">
<div class="bs-thumb bs-satellite"></div>
<span class="bs-name">Satelit</span>
</button>
</div>
<div class="legend-reset">
<span onclick="resetFilter()">↺ Tampilkan Semua</span>
</div>
</div>
<!-- MODE BAR -->
<div id="modeBar">
<button class="modeBtn active" id="btnModeView" onclick="setMode('view')" title="Mode Lihat (Pilih item)">👁️ Lihat</button>
<div class="modebar-sep"></div>
<?php if ($isAdmin): ?>
<button class="modeBtn" id="btnModeSPBU" onclick="setMode('spbu')" title="Tambah Titik SPBU">⛽ SPBU</button>
<button class="modeBtn" id="btnModeJalan" onclick="setMode('jalan')" title="Gambar Garis Jalan">🛣️ Jalan</button>
<button class="modeBtn" id="btnModeKavling" onclick="setMode('kavling')" title="Gambar Area Kavling">🏡 Kavling</button>
<?php endif; ?>
</div>
<!-- FORM SPBU -->
<div id="formSPBU" class="form-popup">
<h3>⛽ Tambah SPBU</h3>
<label>Nama SPBU</label>
<input type="text" id="spbu_nama" placeholder="Masukkan nama SPBU">
<label>No WhatsApp</label>
<input type="text" id="spbu_wa" placeholder="08xxxxxxxxxx">
<label>Buka 24 Jam?</label>
<select id="spbu_buka">
<option value="Ya">Ya</option>
<option value="Tidak">Tidak</option>
</select>
<div class="form-row">
<button class="btn btn-save" onclick="simpanSPBU()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupFormSPBU()">Batal</button>
</div>
</div>
<!-- FORM JALAN -->
<div id="formJalan" class="form-popup">
<h3>🛣️ Simpan Data Jalan</h3>
<label>Nama Jalan</label>
<input type="text" id="jalan_nama" placeholder="Nama jalan">
<label>Status Jalan</label>
<select id="jalan_status">
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten">Jalan Kabupaten</option>
</select>
<label>Panjang Jalan (otomatis)</label>
<input type="text" id="jalan_panjang" readonly placeholder="Dihitung otomatis (meter)">
<div class="form-row">
<button class="btn btn-save" onclick="simpanJalan()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupFormJalan()">Batal</button>
</div>
</div>
<!-- FORM KAVLING -->
<div id="formKavling" class="form-popup">
<h3>🏘️ Simpan Data Kavling</h3>
<label>Nama/No Kavling</label>
<input type="text" id="kavling_nama" placeholder="Nama atau nomor kavling">
<label>Status Kepemilikan</label>
<select id="kavling_status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
<label>Luas Tanah (otomatis)</label>
<input type="text" id="kavling_luas" readonly placeholder="Dihitung otomatis (m²)">
<div class="form-row">
<button class="btn btn-save" onclick="simpanKavling()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupFormKavling()">Batal</button>
</div>
</div>
<!-- PANEL TEMPAT IBADAH (MULTI) -->
<div id="panelTempatIbadah">
<h3>🕌 Pengaturan Tempat Ibadah &amp; Bantuan</h3>
<div class="panel-section-title">📍 Lokasi Tempat Ibadah Baru</div>
<label style="font-size:11.5px;color:#5a6478;font-weight:600;display:block;margin-bottom:5px;">Nama Tempat Ibadah</label>
<input type="text" id="tempat_ibadah_nama" placeholder="Nama tempat ibadah"
style="width:100%;padding:7px 9px;border-radius:6px;border:1px solid #e4e7ec;font-size:12.5px;margin-bottom:8px;font-family:var(--font);outline:none;">
<label style="font-size:11.5px;color:#5a6478;font-weight:600;display:block;margin-bottom:5px;">Tipe Tempat Ibadah</label>
<select id="tempat_ibadah_tipe" style="width:100%;padding:7px 9px;border-radius:6px;border:1px solid #e4e7ec;font-size:12.5px;margin-bottom:8px;font-family:var(--font);outline:none;">
<option value="Masjid">Masjid</option>
<option value="Gereja">Gereja</option>
<option value="Pura">Pura</option>
<option value="Vihara">Vihara</option>
<option value="Klenteng">Klenteng</option>
</select>
<button class="loc-btn loc-btn-gps" onclick="getLokasiGPS()">📡 Gunakan GPS / Geolocation</button>
<button class="loc-btn loc-btn-map" onclick="startPickTempatIbadahOnMap()">🖱️ Pilih Titik di Peta</button>
<div class="coord-display" id="tempatIbadahCoordDisplay">Belum ada koordinat dipilih.</div>
<label style="font-size:11.5px;color:#5a6478;font-weight:600;display:block;margin-bottom:3px;">Alamat (otomatis)</label>
<textarea id="tempatIbadahAlamatDisplay" rows="2" readonly placeholder="Alamat otomatis setelah koordinat dipilih"
style="width:100%;margin-bottom:10px;padding:8px 10px;border-radius:6px;border:1px solid #e4e7ec;font-size:11.5px;font-family:var(--font);resize:none;outline:none;background:#f8f9fb;color:#5a6478;"></textarea>
<div class="panel-section-title">🔵 Radius Bantuan</div>
<div class="radius-val-label" id="radiusLabel">500 meter</div>
<input type="range" id="radiusSlider" min="100" max="5000" step="100" value="500" oninput="onRadiusChange(this.value)">
<div class="radius-row">
<select id="radiusDropdown" onchange="onRadiusDropdown(this.value)">
<option value="500">500 m</option>
<option value="1000">1 km</option>
<option value="2000">2 km</option>
<option value="3000">3 km</option>
</select>
<input type="number" id="radiusManual" placeholder="meter" min="50" max="20000"
oninput="onRadiusChange(this.value)"
style="width:90px;padding:7px 8px;border-radius:6px;border:1px solid #e4e7ec;font-size:12.5px;font-family:var(--font);outline:none;">
<span style="font-size:12px;color:#9aa3b2;align-self:center;white-space:nowrap;">m</span>
</div>
<div class="form-row" style="margin-top:10px;">
<button class="btn btn-purple" onclick="terapkanTempatIbadah()">✅ Terapkan</button>
<button class="btn btn-cancel" onclick="tutupPanelTempatIbadah()">Tutup</button>
</div>
<!-- Daftar Tempat Ibadah -->
<div id="tempatIbadahListWrap" style="display:none;">
<h5>🕌 Daftar Tempat Ibadah Terdaftar</h5>
<div id="tempatIbadahListContainer"></div>
<div class="stat-row" id="statRow">
<div class="stat-badge">
<div class="stat-num" id="statTotal">0</div>
<div class="stat-lbl">Total Rumah</div>
</div>
<div class="stat-badge" style="border-color:#bbf7d0;">
<div class="stat-num" id="statIn" style="color:#16a34a;">0</div>
<div class="stat-lbl">Dapat Bantuan</div>
</div>
<div class="stat-badge" style="border-color:#fecaca;">
<div class="stat-num" id="statOut" style="color:#dc2626;">0</div>
<div class="stat-lbl">Belum Terjangkau</div>
</div>
</div>
</div>
</div>
<!-- FORM RUMAH BANTUAN -->
<div id="formRumah" class="form-popup">
<h3>🏠 Tambah Rumah Bantuan</h3>
<label>Nama Pemilik / Kepala Keluarga</label>
<input type="text" id="rumah_nama" placeholder="Nama kepala keluarga">
<label>Umur Kepala Keluarga (Tahun)</label>
<input type="number" id="rumah_umur_kk" min="0" placeholder="Contoh: 45">
<label>Pekerjaan</label>
<select id="rumah_pekerjaan" onchange="togglePekerjaanLainnya(this.value)">
<option value="">-- Pilih Pekerjaan --</option>
<option value="Buruh">Buruh</option>
<option value="Nelayan">Nelayan</option>
<option value="Petani">Petani</option>
<option value="Tidak Bekerja">Tidak Bekerja</option>
<option value="Pedagang">Pedagang</option>
<option value="Ojek">Ojek</option>
<option value="Lainnya">Lainnya</option>
</select>
<input type="text" id="rumah_pekerjaan_lainnya" placeholder="Sebutkan pekerjaan lainnya..." style="display:none; margin-top:-5px; margin-bottom:10px;">
<label>Penghasilan Perbulan</label>
<select id="rumah_penghasilan">
<option value="">-- Pilih Penghasilan --</option>
<option value="< 500 ribu">&lt; 500 ribu</option>
<option value="500 ribu - 1 juta">500 ribu - 1 juta</option>
<option value="1 - 2 juta">1 - 2 juta</option>
<option value="> 2 juta">&gt; 2 juta</option>
</select>
<label>Jumlah Anggota Keluarga</label>
<input type="number" id="rumah_jumlah_anggota" min="1" value="1">
<label>Status Rumah</label>
<select id="rumah_status_rumah">
<option value="">-- Pilih Status Kepemilikan --</option>
<option value="Milik Sendiri">Milik Sendiri</option>
<option value="Kontrak">Kontrak</option>
<option value="Menumpang">Menumpang</option>
</select>
<label>Alamat (otomatis)</label>
<textarea id="rumah_alamat" rows="3" readonly placeholder="Mengambil alamat..."
style="width:100%;margin-bottom:10px;padding:8px 10px;border-radius:6px;border:1px solid #e4e7ec;font-size:12px;font-family:var(--font);resize:none;outline:none;background:#f8f9fb;color:#5a6478;"></textarea>
<label>Keterangan</label>
<input type="text" id="rumah_ket" placeholder="Keterangan tambahan">
<div class="form-row">
<button class="btn btn-teal" onclick="simpanRumah()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupFormRumah()">Batal</button>
</div>
</div>
<button id="btnUndoDraw" onclick="handleUndo()">↩ Undo</button>
<button id="btnFinishDraw" onclick="handleFinish()">✓ Selesai</button>
<div id="notif"></div>
<!-- DETAIL RUMAH MODAL -->
<div id="detailRumahModal">
<div class="detail-modal-box">
<div class="detail-modal-header">
<h3>🏠 Rincian Detail Rumah Bantuan</h3>
<button class="close-btn" onclick="tutupDetailRumahModal()">&times;</button>
</div>
<div class="detail-modal-body" id="detailRumahModalBody">
<!-- Content populated dynamically via JS -->
</div>
</div>
</div>
<!-- DETAIL BANTUAN TEMPAT IBADAH MODAL -->
<div id="detailBantuanTempatIbadahModal" style="position: fixed; inset: 0; background: rgba(0,0,0,.45); z-index: 2200; display: none; align-items: center; justify-content: center; backdrop-filter: blur(2px);">
<div class="detail-modal-box" style="width: 520px; max-width: 90%; max-height: 85vh;">
<div class="detail-modal-header" style="background: linear-gradient(90deg, #059669 0%, #10b981 100%);">
<h3 style="margin:0; font-size:14.5px; font-weight:700; color:white; display:flex; align-items:center; gap:8px;">
<span id="detailBantuanTIHeaderEmoji">🕌</span>
<span id="detailBantuanTITitle">Rincian Penerima Bantuan</span>
</h3>
<button class="close-btn" onclick="tutupDetailBantuanTempatIbadahModal()" style="background: transparent; border: none; color: white; font-size: 22px; cursor: pointer; line-height: 1; opacity: 0.8; transition: opacity 0.15s;">&times;</button>
</div>
<div class="detail-modal-body" id="detailBantuanTempatIbadahModalBody" style="padding: 20px; overflow-y: auto;">
<!-- Content populated dynamically via JS -->
</div>
</div>
</div>
<!-- EDIT MODAL -->
<div id="editModal">
<div id="editModalBox">
<h3 id="editModalTitle">Edit Data</h3>
<div id="editJalanForm" style="display:none;">
<div class="edit-hint">Ubah nama atau status, ATAU klik <b>Perbaiki</b> untuk menggambar rute baru.</div>
<label>Nama Jalan</label>
<input type="text" id="edit_jalan_nama">
<label>Status Jalan</label>
<select id="edit_jalan_status">
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten">Jalan Kabupaten</option>
</select>
<label>Panjang Jalan</label>
<input type="text" id="edit_jalan_panjang" readonly>
<div class="modal-actions">
<button class="btn btn-orange" onclick="startEditGeom('jalan')">Perbaiki</button>
<button class="btn btn-save" onclick="simpanEditJalan()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupEditModal()">Batal</button>
</div>
</div>
<div id="editKavlingForm" style="display:none;">
<div class="edit-hint">Ubah nama atau status, ATAU klik <b>Perbaiki</b> untuk menggambar batas baru.</div>
<label>Nama Kavling</label>
<input type="text" id="edit_kavling_nama">
<label>Status Kepemilikan</label>
<select id="edit_kavling_status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
<label>Luas Tanah</label>
<input type="text" id="edit_kavling_luas" readonly>
<div class="modal-actions">
<button class="btn btn-orange" onclick="startEditGeom('kavling')">Perbaiki</button>
<button class="btn btn-save" onclick="simpanEditKavling()">Simpan</button>
<button class="btn btn-cancel" onclick="tutupEditModal()">Batal</button>
</div>
</div>
</div>
</div>
<script>
const IS_ADMIN = <?php echo $isAdmin ? 'true' : 'false'; ?>;
const IS_KEMISKINAN = false;
const IS_INFRASTRUKTUR = true;
// =====================================================================
// MAP INIT
// =====================================================================
const map = L.map('map', { zoomControl: false }).setView([-0.0556148, 109.3485769], 13);
L.control.zoom({ position: 'bottomleft' }).addTo(map);
// BASEMAP LAYERS
const baseLayers = {
street: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© <a href="https://openstreetmap.org">OpenStreetMap</a> contributors'
}),
satellite: L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19,
attribution: '© Esri, Maxar, Earthstar Geographics, USDA FSA, USGS, Aerogrid, IGN'
})
};
// OVERLAY LABEL UNTUK TAMPILAN SATELLITE (Hibrida: menampilkan nama jalan, komplek, dan gang)
const satelliteLabels = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_only_labels/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap, © CARTO'
});
let currentBasemap = 'street';
baseLayers.street.addTo(map);
function switchBasemap(type) {
if (type === currentBasemap) return;
// Hapus basemap saat ini
map.removeLayer(baseLayers[currentBasemap]);
if (currentBasemap === 'satellite' && map.hasLayer(satelliteLabels)) {
map.removeLayer(satelliteLabels);
}
// Aktifkan basemap pilihan
baseLayers[type].addTo(map);
if (type === 'satellite') {
satelliteLabels.addTo(map);
}
// Pastikan layer data tetap di atas tile layer
Object.values(layerGroups || {}).forEach(lg => { if (map.hasLayer(lg)) { lg.remove(); lg.addTo(map); } });
currentBasemap = type;
document.querySelectorAll('.basemap-btn').forEach(b => b.classList.remove('active'));
document.getElementById(type === 'street' ? 'btnMapStreet' : 'btnMapSatellite').classList.add('active');
showNotif(type === 'street' ? '🗺️ Tampilan Peta Jalan' : '🛰️ Tampilan Satelit Hibrida (Dengan Nama Jalan)', '#ea580c');
}
// =====================================================================
// STATE — draw / edit
// =====================================================================
let currentMode = 'view';
let drawPoints = [];
let drawPolyline = null;
let drawPolygon = null;
let tempMarkers = [];
let previewMarker = null;
let latKlik, lngKlik;
let editingType = null;
let editingId = null;
let editingLayer = null;
let isRedrawing = false;
let editDrawPoints = [];
let editDrawPolyline = null;
let editDrawPolygon = null;
let editTempMarkers = [];
// =====================================================================
// STATE — TEMPAT IBADAH (MULTI-INSTANCE)
// =====================================================================
// Setiap tempat ibadah disimpan sebagai objek:
// { id, nama, tipe, alamat, lat, lng, radius, marker, circle }
let tempatIbadahInstances = []; // array semua tempat ibadah di peta
let pendingTempatIbadahLat = null;
let pendingTempatIbadahLng = null;
let pendingTempatIbadahAlamat = '';
let panelRadiusMeters = 500; // radius di panel untuk tempat ibadah BARU
let pickingTempatIbadah = false;
let pickPreviewMarker = null;
let previewCircle = null; // Preview radius saat tambah tempat ibadah baru
let pendingTempatIbadahMarker = null; // Marker sementara saat pilih titik
// =====================================================================
// STATE — RUMAH
// =====================================================================
let rumahLat = null;
let rumahLng = null;
const layerGroups = {
spbu: L.layerGroup().addTo(map),
jalan: L.layerGroup().addTo(map),
kavling: L.layerGroup().addTo(map),
kecamatan: L.layerGroup().addTo(map),
rumah: L.layerGroup().addTo(map)
};
let cachedData = { spbu: [], jalan: [], kavling: [], rumah: [] };
// =====================================================================
// KECAMATAN COLORS
// =====================================================================
const KECAMATAN_COLORS = [
'#e63946','#f4a261','#2a9d8f','#457b9d','#a8dadc',
'#e76f51','#264653','#6a4c93','#1982c4','#8ac926',
'#ff595e','#ffca3a','#6a994e','#bc4749','#0077b6',
'#023e8a','#e9c46a','#4cc9f0','#f77f00','#7b2d8b'
];
let kecamatanColorMap = {};
function getKecamatanColor(nama) {
if (!kecamatanColorMap[nama]) {
const idx = Object.keys(kecamatanColorMap).length % KECAMATAN_COLORS.length;
kecamatanColorMap[nama] = KECAMATAN_COLORS[idx];
}
return kecamatanColorMap[nama];
}
// =====================================================================
// UTILITIES
// =====================================================================
function makeElementDraggable(elmnt, header) {
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (header) {
header.onmousedown = dragMouseDown;
} else {
elmnt.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
const tagName = e.target.tagName.toLowerCase();
if (tagName === 'input' || tagName === 'select' || tagName === 'button' || tagName === 'textarea' || e.target.closest('.close-btn') || e.target.closest('.btn')) {
return;
}
e.preventDefault();
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
let newTop = elmnt.offsetTop - pos2;
let newLeft = elmnt.offsetLeft - pos1;
const rect = elmnt.getBoundingClientRect();
const maxX = window.innerWidth - rect.width;
const maxY = window.innerHeight - rect.height;
newLeft = Math.max(0, Math.min(newLeft, maxX));
newTop = Math.max(0, Math.min(newTop, maxY));
elmnt.style.top = newTop + "px";
elmnt.style.left = newLeft + "px";
elmnt.style.right = "auto";
elmnt.style.bottom = "auto";
}
function closeDragElement() {
document.onmouseup = null;
document.onmousemove = null;
}
}
function exportInfrastrukturExcel() {
// ── Sheet 1: SPBU ──
const statusBukaLabel = { 'Ya': 'Ya (Buka 24 Jam)', 'Tidak': 'Tidak (Jam Terbatas)' };
const spbuRows = cachedData.spbu.map((d, i) => ({
"No" : i + 1,
"Nama SPBU" : d.nama || "-",
"No WhatsApp / Kontak" : d.wa || "-",
"Status Operasional" : statusBukaLabel[d.buka] || d.buka || "-",
"Latitude" : d.lat ? parseFloat(d.lat).toFixed(8) : "-",
"Longitude" : d.lng ? parseFloat(d.lng).toFixed(8) : "-",
"Koordinat (untuk Google Maps)" : d.lat && d.lng ? `${parseFloat(d.lat).toFixed(6)}, ${parseFloat(d.lng).toFixed(6)}` : "-"
}));
// ── Sheet 2: Jalan ──
const statusJalanDesc = {
Nasional : 'Dikelola Pemerintah Pusat',
Provinsi : 'Dikelola Pemerintah Provinsi',
Kabupaten : 'Dikelola Pemerintah Kabupaten/Kota'
};
const jalanRows = cachedData.jalan.map((d, i) => ({
"No" : i + 1,
"Nama Jalan" : d.nama || "-",
"Klasifikasi Jalan" : d.status || "-",
"Keterangan Pengelolaan" : statusJalanDesc[d.status] || "-",
"Panjang Jalan" : d.panjang || "-"
}));
// ── Sheet 3: Kavling ──
const statusKavlingFull = {
SHM : 'Sertifikat Hak Milik (SHM)',
HGB : 'Sertifikat Hak Guna Bangunan (HGB)',
HGU : 'Sertifikat Hak Guna Usaha (HGU)',
HP : 'Sertifikat Hak Pakai (HP)'
};
const statusKavlingDesc = {
SHM : 'Hak kepemilikan penuh atas tanah',
HGB : 'Hak mendirikan dan memiliki bangunan di atas tanah',
HGU : 'Hak mengusahakan tanah milik negara',
HP : 'Hak memakai tanah milik negara/orang lain'
};
const kavlingRows = cachedData.kavling.map((d, i) => ({
"No" : i + 1,
"Nama Kavling / Lahan" : d.nama || "-",
"Status Kepemilikan" : statusKavlingFull[d.status] || d.status || "-",
"Keterangan Status" : statusKavlingDesc[d.status] || "-",
"Luas Tanah" : d.luas || "-"
}));
// ── Auto-fit column widths ──
function autoFitCols(ws, rows) {
if (!rows || rows.length === 0) return;
const headers = Object.keys(rows[0]);
const colsWidth = headers.map(h => ({ wch: Math.max(h.length + 4, 12) }));
rows.forEach(row => {
headers.forEach((h, idx) => {
const len = row[h] ? String(row[h]).length + 2 : 0;
if (len > colsWidth[idx].wch) colsWidth[idx].wch = len;
});
});
ws['!cols'] = colsWidth;
}
// ── Fallback if data empty ──
const spbuFinal = spbuRows.length > 0 ? spbuRows : [{"No":"", "Nama SPBU":"(Belum ada data)", "No WhatsApp / Kontak":"-", "Status Operasional":"-", "Latitude":"-", "Longitude":"-", "Koordinat (untuk Google Maps)":"-"}];
const jalanFinal = jalanRows.length > 0 ? jalanRows : [{"No":"", "Nama Jalan":"(Belum ada data)", "Klasifikasi Jalan":"-", "Keterangan Pengelolaan":"-", "Panjang Jalan":"-"}];
const kavlingFinal = kavlingRows.length > 0 ? kavlingRows : [{"No":"", "Nama Kavling / Lahan":"(Belum ada data)", "Status Kepemilikan":"-", "Keterangan Status":"-", "Luas Tanah":"-"}];
const wsSpbu = XLSX.utils.json_to_sheet(spbuFinal);
const wsJalan = XLSX.utils.json_to_sheet(jalanFinal);
const wsKavling = XLSX.utils.json_to_sheet(kavlingFinal);
autoFitCols(wsSpbu, spbuFinal);
autoFitCols(wsJalan, jalanFinal);
autoFitCols(wsKavling, kavlingFinal);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, wsSpbu, "Data SPBU");
XLSX.utils.book_append_sheet(workbook, wsJalan, "Data Jalan");
XLSX.utils.book_append_sheet(workbook, wsKavling, "Data Kavling");
const today = new Date();
const dateStr = `${today.getFullYear()}${String(today.getMonth()+1).padStart(2,'0')}${String(today.getDate()).padStart(2,'0')}`;
XLSX.writeFile(workbook, `Laporan_Infrastruktur_${dateStr}.xlsx`);
showNotif(`✅ Laporan diunduh! ${spbuRows.length} SPBU · ${jalanRows.length} Jalan · ${kavlingRows.length} Kavling`, "#16a34a");
}
function exportExcel() {
if (IS_INFRASTRUKTUR) {
exportInfrastrukturExcel();
} else {
exportRumahExcel();
}
}
function exportRumahExcel() {
if (!cachedData.rumah || cachedData.rumah.length === 0) {
showNotif("Tidak ada data rumah untuk diexport!", "#dc2626");
return;
}
const penerimaRows = [];
const tidakMenerimaRows = [];
let idxPenerima = 1;
let idxTidakMenerima = 1;
cachedData.rumah.forEach((r) => {
const result = isInsideAnyTempatIbadah(r.lat, r.lng);
const statusBantuan = result.inRadius ? "Mendapatkan Bantuan" : "Tidak Mendapatkan Bantuan";
const tempatIbadahNama = (result.inRadius && result.tempatIbadah) ? result.tempatIbadah.nama : "-";
const jarak = result.inRadius ? result.jarak + " m" : "-";
const rowData = {
"Nama Pemilik / KK": r.nama || "-",
"Umur KK (Tahun)": r.umur_kk ? r.umur_kk : "-",
"Pekerjaan": r.pekerjaan || "-",
"Penghasilan Perbulan": r.penghasilan || "-",
"Jumlah Anggota Keluarga": r.jumlah_anggota || 1,
"Status Rumah": r.status_rumah || "-",
"Alamat Lengkap": r.alamat || "-",
"Latitude": r.lat,
"Longitude": r.lng,
"Keterangan": r.ket || "-",
"Status Kelayakan Bantuan": statusBantuan,
"Tempat Ibadah Pemberi Bantuan": tempatIbadahNama,
"Jarak ke Tempat Ibadah": jarak
};
if (result.inRadius) {
penerimaRows.push({
"No": idxPenerima++,
...rowData
});
} else {
tidakMenerimaRows.push({
"No": idxTidakMenerima++,
...rowData
});
}
});
const worksheetPenerima = XLSX.utils.json_to_sheet(penerimaRows);
const worksheetTidakMenerima = XLSX.utils.json_to_sheet(tidakMenerimaRows);
// Auto-fit columns helper
function autoFitCols(ws, rows) {
if (rows.length === 0) return;
const colsWidth = [];
const headers = Object.keys(rows[0]);
headers.forEach(h => {
colsWidth.push({ wch: Math.max(h.length + 4, 10) });
});
rows.forEach(row => {
headers.forEach((h, idx) => {
const val = row[h] ? String(row[h]) : "";
if (val.length + 2 > colsWidth[idx].wch) {
colsWidth[idx].wch = val.length + 2;
}
});
});
ws['!cols'] = colsWidth;
}
autoFitCols(worksheetPenerima, penerimaRows);
autoFitCols(worksheetTidakMenerima, tidakMenerimaRows);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheetPenerima, "Penerima Bantuan");
XLSX.utils.book_append_sheet(workbook, worksheetTidakMenerima, "Tidak Menerima Bantuan");
XLSX.writeFile(workbook, "Laporan_Kelayakan_Bantuan_Rumah.xlsx");
showNotif("Laporan Excel berhasil diunduh!", "#16a34a");
}
function showNotif(text, color) {
const n = document.getElementById('notif');
n.textContent = text;
n.style.background = color || '#16a34a';
n.style.display = 'block';
setTimeout(() => { n.style.display = 'none'; }, 2800);
}
function setHint(text) {
const h = document.getElementById('drawHint');
if (text) { h.textContent = text; h.style.display = 'block'; }
else { h.style.display = 'none'; }
}
function fmtRadius(r) {
return r >= 1000 ? (r/1000).toFixed(r % 1000 === 0 ? 0 : 1) + ' km' : r + ' meter';
}
// =====================================================================
// MODE
// =====================================================================
function setMode(mode) {
if (!IS_ADMIN && mode !== 'view') {
showNotif('Anda harus login sebagai admin untuk menggunakan fitur ini!', '#dc2626');
return;
}
if (currentMode !== 'view' && !isRedrawing) cancelDraw();
if (mode !== 'tempat_ibadah') stopPickTempatIbadah();
currentMode = mode;
document.querySelectorAll('.modeBtn').forEach(b => b.classList.remove('active'));
const ids = {view:'btnModeView',spbu:'btnModeSPBU',jalan:'btnModeJalan',kavling:'btnModeKavling',tempat_ibadah:'btnModeTempatIbadah',rumah:'btnModeRumah'};
if (ids[mode] && document.getElementById(ids[mode])) document.getElementById(ids[mode]).classList.add('active');
document.getElementById('panelTempatIbadah').style.display = (mode === 'tempat_ibadah') ? 'block' : 'none';
if (mode === 'spbu') setHint('Klik kanan pada peta untuk menambah SPBU');
else if (mode === 'jalan') setHint('Klik untuk menambah titik jalan. Klik Selesai jika sudah.');
else if (mode === 'kavling') setHint('Klik untuk menambah titik kavling (min 3). Klik Selesai.');
else if (mode === 'tempat_ibadah') setHint(null);
else if (mode === 'rumah') setHint('Klik pada peta untuk menandai lokasi rumah kurang mampu.');
else setHint(null);
}
function cancelDraw() {
drawPoints = [];
if (drawPolyline) { map.removeLayer(drawPolyline); drawPolyline = null; }
if (drawPolygon) { map.removeLayer(drawPolygon); drawPolygon = null; }
tempMarkers.forEach(m => map.removeLayer(m)); tempMarkers = [];
if (previewMarker) { map.removeLayer(previewMarker); previewMarker = null; }
document.getElementById('btnFinishDraw').style.display = 'none';
document.getElementById('btnUndoDraw').style.display = 'none';
document.getElementById('formJalan').style.display = 'none';
document.getElementById('formKavling').style.display = 'none';
}
// =====================================================================
// MAP EVENTS
// =====================================================================
map.on('contextmenu', function(e) {
if (!IS_ADMIN) return;
if (currentMode !== 'spbu') return;
latKlik = e.latlng.lat; lngKlik = e.latlng.lng;
if (previewMarker) map.removeLayer(previewMarker);
previewMarker = L.marker([latKlik, lngKlik], { opacity: 0.5 }).addTo(map);
document.getElementById('formSPBU').style.display = 'block';
});
map.on('click', function(e) {
document.getElementById('searchResults').style.display = 'none';
if (!IS_ADMIN) return;
if (pickingTempatIbadah) {
pendingTempatIbadahLat = e.latlng.lat;
pendingTempatIbadahLng = e.latlng.lng;
pendingTempatIbadahAlamat = '';
document.getElementById('tempatIbadahCoordDisplay').textContent =
`Lat: ${pendingTempatIbadahLat.toFixed(6)}, Lng: ${pendingTempatIbadahLng.toFixed(6)}`;
document.getElementById('tempatIbadahAlamatDisplay').value = 'Mengambil alamat...';
// Tampilkan preview marker & circle segera
if (pendingTempatIbadahMarker) map.removeLayer(pendingTempatIbadahMarker);
const tipe = document.getElementById('tempat_ibadah_tipe').value;
pendingTempatIbadahMarker = L.marker([pendingTempatIbadahLat, pendingTempatIbadahLng], { icon: getIconTempatIbadah(tipe) }).addTo(map);
if (previewCircle) map.removeLayer(previewCircle);
previewCircle = L.circle([pendingTempatIbadahLat, pendingTempatIbadahLng], {
radius: panelRadiusMeters, color:'#ea580c', fillColor:'#f97316',
fillOpacity:0.1, weight:1.5, dashArray:'5,5'
}).addTo(map);
recheckAllRumah(); // Update status rumah live saat titik dipilih
stopPickTempatIbadah();
showNotif('Titik tempat ibadah dipilih. Mengambil alamat...', '#7c3aed');
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${pendingTempatIbadahLat}&lon=${pendingTempatIbadahLng}&zoom=18&addressdetails=1`)
.then(r => r.json())
.then(data => {
pendingTempatIbadahAlamat = data.display_name || '-';
document.getElementById('tempatIbadahAlamatDisplay').value = pendingTempatIbadahAlamat;
showNotif('Lokasi & alamat tempat ibadah siap. Klik Terapkan.', '#7c3aed');
})
.catch(() => { pendingTempatIbadahAlamat = '-'; document.getElementById('tempatIbadahAlamatDisplay').value = '-'; });
return;
}
if (currentMode === 'rumah') { rumahLat = e.latlng.lat; rumahLng = e.latlng.lng; openFormRumah(e.latlng); return; }
if (isRedrawing) { addEditDrawPoint(e.latlng); return; }
if (currentMode === 'jalan' || currentMode === 'kavling') addDrawPoint(e.latlng);
});
// Mousemove: cursor preview saat picking tempat ibadah
map.on('mousemove', function(e) {
if (!pickingTempatIbadah) return;
if (!pickPreviewMarker) {
const icon = L.divIcon({
className: '',
html: `<div style="width:18px;height:18px;border-radius:50%;background:#7c3aed;border:2.5px solid white;opacity:0.55;pointer-events:none;"></div>`,
iconSize:[18,18], iconAnchor:[9,9]
});
pickPreviewMarker = L.marker(e.latlng, { icon, interactive: false, zIndexOffset: 999 }).addTo(map);
} else {
pickPreviewMarker.setLatLng(e.latlng);
}
});
// =====================================================================
// DRAW POINTS
// =====================================================================
function addDrawPoint(latlng) {
drawPoints.push(latlng);
const m = L.circleMarker(latlng, { radius:5, color:'#ea580c', fillColor:'#ea580c', fillOpacity:1 }).addTo(map);
tempMarkers.push(m);
if (currentMode === 'jalan') {
if (drawPolyline) map.removeLayer(drawPolyline);
drawPolyline = L.polyline(drawPoints, { color:'#ea580c', weight:3, dashArray:'6,4' }).addTo(map);
document.getElementById('btnFinishDraw').style.display = drawPoints.length >= 2 ? 'block' : 'none';
document.getElementById('btnUndoDraw').style.display = 'block';
} else if (currentMode === 'kavling') {
if (drawPolygon) map.removeLayer(drawPolygon);
if (drawPoints.length >= 3)
drawPolygon = L.polygon(drawPoints, { color:'#7c3aed', fillColor:'#a855f7', fillOpacity:0.3, weight:2, dashArray:'6,4' }).addTo(map);
document.getElementById('btnFinishDraw').style.display = drawPoints.length >= 3 ? 'block' : 'none';
document.getElementById('btnUndoDraw').style.display = 'block';
}
}
function handleUndo() { isRedrawing ? undoEditDrawPoint() : undoLastPoint(); }
function handleFinish() { isRedrawing ? finishEditGeom() : finishDraw(); }
function undoLastPoint() {
if (!drawPoints.length) return;
drawPoints.pop();
const last = tempMarkers.pop(); if (last) map.removeLayer(last);
if (drawPolyline) { map.removeLayer(drawPolyline); drawPolyline = null; }
if (drawPolygon) { map.removeLayer(drawPolygon); drawPolygon = null; }
if (drawPoints.length >= 2 && currentMode === 'jalan')
drawPolyline = L.polyline(drawPoints, { color:'#ea580c', weight:3, dashArray:'6,4' }).addTo(map);
if (drawPoints.length >= 3 && currentMode === 'kavling')
drawPolygon = L.polygon(drawPoints, { color:'#7c3aed', fillColor:'#a855f7', fillOpacity:0.3, weight:2, dashArray:'6,4' }).addTo(map);
document.getElementById('btnFinishDraw').style.display =
(currentMode==='jalan'&&drawPoints.length>=2)||(currentMode==='kavling'&&drawPoints.length>=3) ? 'block' : 'none';
document.getElementById('btnUndoDraw').style.display = drawPoints.length >= 1 ? 'block' : 'none';
}
function finishDraw() {
if (currentMode === 'jalan') {
if (drawPoints.length < 2) { showNotif('Minimal 2 titik untuk jalan!', '#dc2626'); return; }
let p = 0;
for (let i = 1; i < drawPoints.length; i++) p += drawPoints[i-1].distanceTo(drawPoints[i]);
document.getElementById('jalan_panjang').value = Math.round(p) + ' meter';
document.getElementById('formJalan').style.display = 'block';
document.getElementById('btnFinishDraw').style.display = 'none';
document.getElementById('btnUndoDraw').style.display = 'none';
} else if (currentMode === 'kavling') {
if (drawPoints.length < 3) { showNotif('Minimal 3 titik untuk kavling!', '#dc2626'); return; }
document.getElementById('kavling_luas').value = Math.round(hitungLuasPolygon(drawPoints)) + ' m2';
document.getElementById('formKavling').style.display = 'block';
document.getElementById('btnFinishDraw').style.display = 'none';
document.getElementById('btnUndoDraw').style.display = 'none';
}
}
function hitungLuasPolygon(latlngs) {
const n = latlngs.length; if (n < 3) return 0;
let cx = 0, cy = 0;
latlngs.forEach(p => { cx += p.lat; cy += p.lng; });
cx /= n; cy /= n;
const origin = L.latLng(cx, cy);
const pts = latlngs.map(p => ({
x: L.latLng(cx, p.lng).distanceTo(origin) * (p.lng > cy ? 1 : -1),
y: L.latLng(p.lat, cy).distanceTo(origin) * (p.lat > cx ? 1 : -1)
}));
let area = 0;
for (let i = 0; i < n; i++) { const j = (i+1)%n; area += pts[i].x*pts[j].y - pts[j].x*pts[i].y; }
return Math.abs(area / 2);
}
// =====================================================================
// ICONS / COLORS
// =====================================================================
function getIconSPBU(buka) {
return new L.Icon({
iconUrl: buka === 'Ya'
? 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png'
: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25,41], iconAnchor: [12,41]
});
}
function getIconTempatIbadah(tipe) {
let emoji = '🕌';
if (tipe === 'Gereja') emoji = '⛪';
else if (tipe === 'Pura') emoji = '🛕';
else if (tipe === 'Vihara') emoji = '🛕';
else if (tipe === 'Klenteng') emoji = '⛩️';
return L.divIcon({
className: '',
html: `<div class="tempat-ibadah-marker-icon"><span>${emoji}</span></div>`,
iconSize: [36,36], iconAnchor: [18,36]
});
}
function getRumahIcon(inRadius) {
const color = inRadius ? '#16a34a' : '#dc2626';
return L.divIcon({
className: '',
html: `<div style="width:18px;height:18px;border-radius:50%;background:${color};border:2.5px solid white;box-shadow:0 2px 6px rgba(0,0,0,.3);"></div>`,
iconSize:[18,18], iconAnchor:[9,9]
});
}
function getColorJalan(s) { return {Nasional:'#c0392b',Provinsi:'#ea580c',Kabupaten:'#ca8a04'}[s]||'#888'; }
function getColorKavling(s) { return {SHM:'#3b82f6',HGB:'#22c55e',HGU:'#a855f7',HP:'#ef4444'}[s]||'#888'; }
// =====================================================================
// HAVERSINE
// =====================================================================
function haversineDistance(lat1, lng1, lat2, lng2) {
const R = 6371000;
const dLat = (lat2-lat1)*Math.PI/180, dLng = (lng2-lng1)*Math.PI/180;
const a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180)*Math.cos(lat2*Math.PI/180)*Math.sin(dLng/2)**2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}
function getRumahTerbantuForTempatIbadah(inst) {
const currentRadius = (inst._pendingRadius !== undefined && inst._pendingRadius !== null) ? inst._pendingRadius : inst.radius;
const list = [];
cachedData.rumah.forEach(r => {
const jarak = haversineDistance(r.lat, r.lng, inst.lat, inst.lng);
if (jarak <= currentRadius) {
list.push({ rumah: r, jarak: Math.round(jarak) });
}
});
list.sort((a, b) => a.jarak - b.jarak);
return list;
}
// =====================================================================
// MASJID — CEK APAKAH RUMAH DALAM SALAH SATU RADIUS
// =====================================================================
function isInsideAnyTempatIbadah(lat, lng) {
// Kembalikan {inRadius, tempatIbadahNama, jarak}
let best = null, bestJarak = Infinity;
// 1. Cek tempat ibadah yang sudah terdaftar (termasuk radius yang sedang digeser di popup)
for (const m of tempatIbadahInstances) {
const r = m._pendingRadius || m.radius;
const jarak = haversineDistance(lat, lng, m.lat, m.lng);
if (jarak <= r && jarak < bestJarak) {
bestJarak = jarak;
best = m;
}
}
// 2. Cek tempat ibadah "preview" yang baru dipilih di panel tapi belum "Terapkan"
if (pendingTempatIbadahLat !== null && pendingTempatIbadahLng !== null) {
const jarakPreview = haversineDistance(lat, lng, pendingTempatIbadahLat, pendingTempatIbadahLng);
if (jarakPreview <= panelRadiusMeters && jarakPreview < bestJarak) {
bestJarak = jarakPreview;
best = { nama: '(Tempat Ibadah Baru)', radius: panelRadiusMeters };
}
}
return { inRadius: best !== null, tempatIbadah: best, jarak: Math.round(bestJarak === Infinity ? 0 : bestJarak) };
}
// =====================================================================
// RECHECK SEMUA RUMAH (panggil setiap ada perubahan radius / masjid baru)
// =====================================================================
function recheckAllRumah() {
let totalIn = 0, totalOut = 0;
cachedData.rumah.forEach(d => {
const result = isInsideAnyTempatIbadah(d.lat, d.lng);
if (d._marker) {
d._marker.setIcon(getRumahIcon(result.inRadius));
d._marker.setPopupContent(popupRumah(d.id, d.nama, d.alamat, result));
}
if (result.inRadius) totalIn++; else totalOut++;
});
updateStatBadge(cachedData.rumah.length, totalIn, totalOut);
}
function updateStatBadge(total, inR, outR) {
const elT = document.getElementById('statTotal');
const elI = document.getElementById('statIn');
const elO = document.getElementById('statOut');
if (elT) elT.textContent = total;
if (elI) elI.textContent = inR;
if (elO) elO.textContent = outR;
}
// =====================================================================
// POPUPS
// =====================================================================
function popupSPBU(id, nama, wa, buka) {
const actions = (IS_ADMIN && IS_INFRASTRUKTUR) ? `<div class="popup-actions">
<button class="btn-hapus" onclick="hapusSPBU(${id})">🗑 Hapus</button>
<button class="btn-edit" onclick="editSPBU(${id})">✏ Geser</button>
</div>` : '';
return `<b>⛽ ${nama}</b><br>WA: ${wa}<br>24 Jam: ${buka}${actions}`;
}
function popupJalan(id, nama, status, panjang) {
const actions = (IS_ADMIN && IS_INFRASTRUKTUR) ? `<div class="popup-actions">
<button class="btn-hapus" onclick="hapusJalan(${id})">🗑 Hapus</button>
<button class="btn-edit" onclick="bukaEditJalan(${id})">✏ Edit</button>
</div>` : '';
return `<b>🛣️ ${nama}</b><br>Status: Jalan ${status}<br>Panjang: ${panjang}${actions}`;
}
function popupKavling(id, nama, status, luas) {
const actions = (IS_ADMIN && IS_INFRASTRUKTUR) ? `<div class="popup-actions">
<button class="btn-hapus" onclick="hapusKavling(${id})">🗑 Hapus</button>
<button class="btn-edit" onclick="bukaEditKavling(${id})">✏ Edit</button>
</div>` : '';
return `<b>🏘️ ${nama}</b><br>Status: ${status}<br>Luas: ${luas}${actions}`;
}
function popupRumah(id, nama, alamat, result) {
const inR = (typeof result === 'boolean') ? result : result.inRadius;
const tempatIbadahNama = (result && result.tempatIbadah) ? result.tempatIbadah.nama : '-';
const jarak = (result && result.jarak) ? result.jarak : 0;
const statusHtml = inR
? `<div class="popup-status-in">🟢 Mendapatkan Bantuan</div><div style="font-size:11px;color:#5a6478;">🕌 ${tempatIbadahNama} — ${jarak} m</div>`
: `<div class="popup-status-out">🔴 Tidak Mendapatkan Bantuan</div>`;
const detailBtn = `<button class="btn-detail-rumah" onclick="tampilDetailRumah(${id})">👁️ Lihat Lebih Lengkap</button>`;
const actions = (IS_ADMIN && IS_KEMISKINAN) ? `<div class="popup-actions">
<button class="btn-hapus" onclick="hapusRumah(${id})">🗑 Hapus</button>
</div>` : '';
return `<b>🏠 ${nama}</b>
<div class="popup-addr">${alamat || '-'}</div>
${statusHtml}
${detailBtn}
${actions}`;
}
function popupTempatIbadah(inst) {
let emoji = '🕌';
if (inst.tipe === 'Gereja') emoji = '⛪';
else if (inst.tipe === 'Pura') emoji = '🛕';
else if (inst.tipe === 'Vihara') emoji = '🛕';
else if (inst.tipe === 'Klenteng') emoji = '⛩️';
const housesInside = getRumahTerbantuForTempatIbadah(inst);
const adminControls = (IS_ADMIN && IS_KEMISKINAN) ? `
<div class="re-label">🔵 Radius Bantuan</div>
<div class="re-val" id="reVal_${inst.id}">${fmtRadius(inst.radius)}</div>
<input type="range" id="reSlide_${inst.id}" style="width:100%;accent-color:#7c3aed;cursor:pointer;margin-bottom:4px;"
min="100" max="5000" step="100" value="${inst.radius}"
oninput="onPopupRadiusSlide('${inst.id}', this.value)">
<div style="display:flex;gap:6px;margin-bottom:10px;">
<select id="reDrop_${inst.id}" style="flex:1;padding:6px 8px;border-radius:6px;border:1px solid #e4e7ec;font-size:12px;font-family:var(--font);outline:none;"
onchange="onPopupRadiusSlide('${inst.id}', this.value)">
<option value="500"${inst.radius==500?' selected':''}>500 m</option>
<option value="1000"${inst.radius==1000?' selected':''}>1 km</option>
<option value="2000"${inst.radius==2000?' selected':''}>2 km</option>
<option value="3000"${inst.radius==3000?' selected':''}>3 km</option>
</select>
<input type="number" id="reNum_${inst.id}" value="${inst.radius}" min="50" max="20000" placeholder="meter"
style="width:90px;padding:6px 8px;border-radius:6px;border:1px solid #e4e7ec;font-size:12px;font-family:var(--font);outline:none;"
oninput="onPopupRadiusSlide('${inst.id}', this.value)">
</div>
<div style="display:flex;gap:6px;">
<button style="flex:1;padding:7px;background:#7c3aed;color:white;border:none;border-radius:6px;font-size:12px;font-weight:700;cursor:pointer;"
onclick="applyPopupRadius('${inst.id}')">✅ Terapkan</button>
<button style="flex:1;padding:7px;background:#fee2e2;color:#dc2626;border:none;border-radius:6px;font-size:12px;font-weight:700;cursor:pointer;"
onclick="hapusTempatIbadah('${inst.id}')">🗑 Hapus</button>
</div>` : `
<div style="font-size:12px;color:#5a6478;margin-top:6px;">🔵 Radius: ${fmtRadius(inst.radius)}</div>`;
const infoBantuan = `
<div style="background:#f1f5f9;padding:10px;border-radius:8px;margin-bottom:10px;margin-top:10px;font-size:12px;border:1px solid #e2e8f0;">
<div style="font-weight:700;color:#1e293b;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between;">
<span>🏠 Rumah Dibantu:</span>
<span id="rumahDibantuCount_${inst.id}" style="color:#059669;font-weight:800;font-size:13px;">${housesInside.length}</span>
</div>
<button style="width:100%;padding:6px 8px;background:#059669;color:white;border:none;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:4px;transition:background 0.2s;"
onclick="showDetailBantuanTempatIbadah('${inst.id}')"
onmouseover="this.style.background='#047857'"
onmouseout="this.style.background='#059669'">
👁️ Lihat Detail Bantuan
</button>
</div>
`;
return `<div class="tempat-ibadah-popup-wrap">
<h4>${emoji} ${inst.nama}</h4>
<div style="font-size:11px;color:var(--text-3);margin-bottom:4px;">Tipe: ${inst.tipe}</div>
<div class="tempat-ibadah-popup-addr" style="margin-bottom:4px;">${inst.alamat || '-'}</div>
${infoBantuan}
${adminControls}
</div>`;
}
// =====================================================================
// TEMPAT IBADAH — PANEL RADIUS CHANGE (untuk tempat ibadah BARU)
// =====================================================================
function onRadiusChange(val) {
val = parseInt(val); if (isNaN(val) || val < 50) return;
panelRadiusMeters = val;
// Sync UI Panel
const s = document.getElementById('radiusSlider'); if(s) s.value = Math.min(val, 5000);
const m = document.getElementById('radiusManual'); if(m) m.value = val;
const d = document.getElementById('radiusDropdown');
if(d) {
const preset = [500,1000,2000,3000];
d.value = preset.reduce((a,b) => Math.abs(b-val)<Math.abs(a-val)?b:a);
}
document.getElementById('radiusLabel').textContent = fmtRadius(val);
// Update live preview circle jika ada
if (previewCircle) previewCircle.setRadius(val);
// Update status rumah live
recheckAllRumah();
}
function onRadiusDropdown(val) { onRadiusChange(parseInt(val)); }
// =====================================================================
// TEMPAT IBADAH — POPUP RADIUS CHANGE (untuk tempat ibadah yang SUDAH ADA)
// =====================================================================
function onPopupRadiusSlide(tempatIbadahId, val) {
val = parseInt(val); if (isNaN(val) || val < 50) return;
const inst = tempatIbadahInstances.find(m => String(m.id) === String(tempatIbadahId));
if (!inst) return;
// Update label di popup
const lbl = document.getElementById('reVal_' + tempatIbadahId);
if (lbl) lbl.textContent = fmtRadius(val);
// Sync input lainnya di popup
const s = document.getElementById('reSlide_' + tempatIbadahId); if(s) s.value = Math.min(val, 5000);
const n = document.getElementById('reNum_' + tempatIbadahId); if(n) n.value = val;
const d = document.getElementById('reDrop_' + tempatIbadahId);
if(d) {
const preset = [500,1000,2000,3000];
const closest = preset.reduce((a,b) => Math.abs(b-val)<Math.abs(a-val)?b:a);
d.value = closest;
}
// Update circle live di peta
inst.circle.setRadius(val);
// Simpan nilai sementara ke inst._pendingRadius
inst._pendingRadius = val;
// Update status rumah live saat slider digeser
recheckAllRumah();
// Update count live di popup
const countEl = document.getElementById('rumahDibantuCount_' + tempatIbadahId);
if (countEl) {
const list = getRumahTerbantuForTempatIbadah(inst);
countEl.textContent = list.length;
}
// Update detail modal jika sedang terbuka untuk tempat ibadah ini
if (String(activeDetailTempatIbadahId) === String(tempatIbadahId)) {
renderDetailBantuanTempatIbadah();
}
}
function applyPopupRadius(tempatIbadahId) {
const inst = tempatIbadahInstances.find(m => String(m.id) === String(tempatIbadahId));
if (!inst) return;
const newRadius = inst._pendingRadius || inst.radius;
inst.radius = newRadius;
inst._pendingRadius = null;
inst.circle.setRadius(newRadius);
// Rebuild popup dengan nilai baru
inst.marker.setPopupContent(popupTempatIbadah(inst));
recheckAllRumah();
updateTempatIbadahList();
// Simpan ke DB
saveTempatIbadahRadiusToDB(inst);
showNotif(`Radius ${inst.nama}: ${fmtRadius(newRadius)}`, '#7c3aed');
// Update detail modal jika sedang terbuka untuk tempat ibadah ini
if (String(activeDetailTempatIbadahId) === String(tempatIbadahId)) {
renderDetailBantuanTempatIbadah();
}
}
function saveTempatIbadahRadiusToDB(inst) {
const body = `id=${inst.id}&nama=${encodeURIComponent(inst.nama)}&tipe=${encodeURIComponent(inst.tipe)}&alamat=${encodeURIComponent(inst.alamat||'-')}&lat=${inst.lat}&lng=${inst.lng}&radius=${inst.radius}`;
fetch('save_tempat_ibadah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body })
.catch(() => {});
}
// =====================================================================
// TEMPAT IBADAH — GPS
// =====================================================================
function getLokasiGPS() {
if (!navigator.geolocation) { showNotif('Geolocation tidak didukung.', '#dc2626'); return; }
showNotif('Mengambil lokasi GPS...', '#ea580c');
navigator.geolocation.getCurrentPosition(
pos => {
pendingTempatIbadahLat = pos.coords.latitude;
pendingTempatIbadahLng = pos.coords.longitude;
document.getElementById('tempatIbadahCoordDisplay').textContent =
`Lat: ${pendingTempatIbadahLat.toFixed(6)}, Lng: ${pendingTempatIbadahLng.toFixed(6)}`;
document.getElementById('tempatIbadahAlamatDisplay').value = 'Mengambil alamat...';
map.setView([pendingTempatIbadahLat, pendingTempatIbadahLng], 16);
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${pendingTempatIbadahLat}&lon=${pendingTempatIbadahLng}&zoom=18&addressdetails=1`)
.then(r => r.json())
.then(data => {
pendingTempatIbadahAlamat = data.display_name || '-';
document.getElementById('tempatIbadahAlamatDisplay').value = pendingTempatIbadahAlamat;
showNotif('Lokasi GPS & alamat berhasil diambil!', '#16a34a');
})
.catch(() => { pendingTempatIbadahAlamat = '-'; document.getElementById('tempatIbadahAlamatDisplay').value = '-'; });
},
err => { showNotif('Gagal ambil GPS: ' + err.message, '#dc2626'); }
);
}
function startPickTempatIbadahOnMap() {
pickingTempatIbadah = true;
document.getElementById('map').classList.add('map-picking-tempat-ibadah');
document.getElementById('pickingIndicator').style.display = 'block';
showNotif('Klik titik lokasi tempat ibadah di peta', '#7c3aed');
}
function stopPickTempatIbadah() {
pickingTempatIbadah = false;
document.getElementById('map').classList.remove('map-picking-tempat-ibadah');
document.getElementById('pickingIndicator').style.display = 'none';
if (pickPreviewMarker) { map.removeLayer(pickPreviewMarker); pickPreviewMarker = null; }
}
// =====================================================================
// TEMPAT IBADAH — TERAPKAN (TAMBAH TEMPAT IBADAH BARU)
// =====================================================================
function terapkanTempatIbadah(idParam, namaParam, tipeParam, latParam, lngParam, radiusParam, alamatParam) {
const nama = namaParam || document.getElementById('tempat_ibadah_nama').value.trim() || 'Tempat Ibadah';
const tipe = tipeParam || document.getElementById('tempat_ibadah_tipe').value;
const lat = latParam ?? pendingTempatIbadahLat;
const lng = lngParam ?? pendingTempatIbadahLng;
const radius = radiusParam ?? panelRadiusMeters;
const alamat = alamatParam || pendingTempatIbadahAlamat || '-';
if (lat === null || lng === null) {
showNotif('Pilih lokasi tempat ibadah terlebih dahulu!', '#dc2626'); return;
}
// Buat unique id sementara (diganti dari DB jika berhasil save)
const tempId = idParam || ('m_' + Date.now());
const circle = L.circle([lat, lng], {
radius, color:'#ea580c', fillColor:'#f97316',
fillOpacity:0.04, weight:1.5, dashArray:'5,5', opacity:0.35
}).addTo(map);
const inst = { id: tempId, nama, tipe, alamat, lat, lng, radius, circle, marker: null, _pendingRadius: null };
tempatIbadahInstances.push(inst);
const marker = L.marker([lat, lng], { icon: getIconTempatIbadah(tipe), zIndexOffset: 1000 })
.addTo(map)
.bindPopup(() => popupTempatIbadah(inst), { maxWidth: 280, offset: [0, -36] });
inst.marker = marker;
marker.on('popupopen', () => circle.setStyle({ fillOpacity:0.15, opacity:1, weight:2.5, dashArray:'6,4' }));
marker.on('popupclose', () => circle.setStyle({ fillOpacity:0.04, opacity:0.35, weight:1.5, dashArray:'5,5' }));
recheckAllRumah();
updateTempatIbadahList();
map.setView([lat, lng], 15);
// Simpan ke DB jika bukan dari load
if (!idParam) {
saveTempatIbadahNewToDB(inst);
// Reset panel & preview
if (pendingTempatIbadahMarker) { map.removeLayer(pendingTempatIbadahMarker); pendingTempatIbadahMarker = null; }
if (previewCircle) { map.removeLayer(previewCircle); previewCircle = null; }
document.getElementById('tempat_ibadah_nama').value = '';
document.getElementById('tempatIbadahCoordDisplay').textContent = 'Belum ada koordinat dipilih.';
document.getElementById('tempatIbadahAlamatDisplay').value = '';
pendingTempatIbadahLat = null; pendingTempatIbadahLng = null; pendingTempatIbadahAlamat = '';
showNotif(`Tempat Ibadah "${nama}" berhasil diterapkan!`, '#7c3aed');
}
}
function saveTempatIbadahNewToDB(inst) {
const body = `nama=${encodeURIComponent(inst.nama)}&tipe=${encodeURIComponent(inst.tipe)}&alamat=${encodeURIComponent(inst.alamat)}&lat=${inst.lat}&lng=${inst.lng}&radius=${inst.radius}`;
fetch('save_tempat_ibadah.php', { method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'}, body })
.then(r => r.text())
.then(r => {
const parts = r.split('|');
if (parts[0] === 'ok' && parts[1]) {
const dbId = parseInt(parts[1]);
// Update id di instance
inst.id = dbId;
// Re-bind popup dengan id yang benar
inst.marker.setPopupContent(popupTempatIbadah(inst));
updateTempatIbadahList();
}
})
.catch(() => {});
}
// =====================================================================
// TEMPAT IBADAH — HAPUS
// =====================================================================
function hapusTempatIbadah(tempatIbadahId) {
if (!confirm('Hapus tempat ibadah ini?')) return;
const idx = tempatIbadahInstances.findIndex(m => String(m.id) === String(tempatIbadahId));
if (idx === -1) return;
const inst = tempatIbadahInstances[idx];
map.removeLayer(inst.marker);
map.removeLayer(inst.circle);
tempatIbadahInstances.splice(idx, 1);
recheckAllRumah();
updateTempatIbadahList();
// Hapus dari DB
fetch('delete_tempat_ibadah.php?id=' + tempatIbadahId).catch(() => {});
showNotif('Tempat ibadah dihapus', '#dc2626');
// Close detail modal jika sedang terbuka untuk tempat ibadah ini
if (String(activeDetailTempatIbadahId) === String(tempatIbadahId)) {
tutupDetailBantuanTempatIbadahModal();
}
}
// =====================================================================
// TEMPAT IBADAH — UPDATE DAFTAR DI PANEL
// =====================================================================
function updateTempatIbadahList() {
const wrap = document.getElementById('tempatIbadahListWrap');
const container = document.getElementById('tempatIbadahListContainer');
if (tempatIbadahInstances.length === 0) { wrap.style.display = 'none'; return; }
wrap.style.display = 'block';
container.innerHTML = tempatIbadahInstances.map(m => {
let emoji = '🕌';
if (m.tipe === 'Gereja') emoji = '⛪';
else if (m.tipe === 'Pura') emoji = '🛕';
else if (m.tipe === 'Vihara') emoji = '🛕';
else if (m.tipe === 'Klenteng') emoji = '⛩️';
return `
<div class="tempat-ibadah-list-item">
<div class="tempat-ibadah-list-info">
<div class="tempat-ibadah-list-name">${emoji} ${m.nama}</div>
<div class="tempat-ibadah-list-radius">Tipe: ${m.tipe} | Radius: ${fmtRadius(m.radius)}</div>
</div>
<div class="tempat-ibadah-list-actions">
<button class="btn-icon btn-icon-fly" title="Pergi ke lokasi" onclick="flyToTempatIbadah('${m.id}')">📍</button>
<button class="btn-icon btn-icon-del" title="Hapus" onclick="hapusTempatIbadah('${m.id}')">🗑</button>
</div>
</div>`;
}).join('');
recheckAllRumah();
}
function flyToTempatIbadah(tempatIbadahId) {
const inst = tempatIbadahInstances.find(m => String(m.id) === String(tempatIbadahId));
if (!inst) return;
map.setView([inst.lat, inst.lng], 15);
inst.marker.openPopup();
}
function tutupPanelTempatIbadah() {
stopPickTempatIbadah();
if (pendingTempatIbadahMarker) { map.removeLayer(pendingTempatIbadahMarker); pendingTempatIbadahMarker = null; }
if (previewCircle) { map.removeLayer(previewCircle); previewCircle = null; }
pendingTempatIbadahLat = null; pendingTempatIbadahLng = null;
document.getElementById('panelTempatIbadah').style.display = 'none';
recheckAllRumah(); // Kembalikan status rumah (karena preview hilang)
setMode('view');
}
// =====================================================================
// SPBU CRUD
// =====================================================================
function tutupFormSPBU() {
document.getElementById('formSPBU').style.display = 'none';
if (previewMarker) { map.removeLayer(previewMarker); previewMarker = null; }
}
function simpanSPBU() {
const nama = document.getElementById('spbu_nama').value.trim();
const wa = document.getElementById('spbu_wa').value.trim();
const buka = document.getElementById('spbu_buka').value;
if (!nama) { showNotif('Nama SPBU wajib diisi!', '#dc2626'); return; }
fetch('save_spbu.php', {
method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`nama=${encodeURIComponent(nama)}&wa=${encodeURIComponent(wa)}&buka=${buka}&lat=${latKlik}&lng=${lngKlik}`
}).then(r=>r.text()).then(r=>{
const id = r.split('|')[1];
const marker = L.marker([latKlik,lngKlik],{icon:getIconSPBU(buka)})
.bindPopup(popupSPBU(id,nama,wa,buka),{offset:[0,-35]});
layerGroups.spbu.addLayer(marker);
cachedData.spbu.push({id,nama,wa,buka,lat:latKlik,lng:lngKlik,_marker:marker});
if (previewMarker) { map.removeLayer(previewMarker); previewMarker=null; }
document.getElementById('formSPBU').style.display='none';
document.getElementById('spbu_nama').value=''; document.getElementById('spbu_wa').value='';
showNotif('SPBU berhasil disimpan!');
}).catch(()=>showNotif('Gagal menyimpan SPBU','#dc2626'));
}
function hapusSPBU(id) {
if (!confirm('Hapus data SPBU ini?')) return;
fetch('delete_spbu.php?id='+id).then(()=>{
layerGroups.spbu.eachLayer(l=>{
if (l instanceof L.Marker && l._popup && l.getPopup().getContent().includes(`hapusSPBU(${id})`))
layerGroups.spbu.removeLayer(l);
});
cachedData.spbu = cachedData.spbu.filter(d=>d.id!=id);
showNotif('SPBU dihapus');
});
}
function editSPBU(id) {
showNotif('Geser marker untuk memperbarui lokasi','#ea580c');
layerGroups.spbu.eachLayer(l=>{
if (l instanceof L.Marker && l._popup && l.getPopup().getContent().includes(`hapusSPBU(${id})`)) {
l.closePopup(); l.dragging.enable();
l.once('dragend',function(e){
fetch('update_spbu.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`id=${id}&lat=${e.target.getLatLng().lat}&lng=${e.target.getLatLng().lng}`
}).then(()=>{l.dragging.disable();showNotif('Lokasi SPBU diperbarui!');});
});
}
});
}
// =====================================================================
// JALAN CRUD
// =====================================================================
function tutupFormJalan() { document.getElementById('formJalan').style.display='none'; cancelDraw(); setMode('view'); }
function simpanJalan() {
const nama=document.getElementById('jalan_nama').value.trim();
const status=document.getElementById('jalan_status').value;
const panjang=document.getElementById('jalan_panjang').value;
if (!nama){showNotif('Nama jalan wajib diisi!','#dc2626');return;}
const geojson=JSON.stringify({type:'LineString',coordinates:drawPoints.map(p=>[p.lng,p.lat])});
fetch('save_jalan.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`nama=${encodeURIComponent(nama)}&status=${status}&panjang=${encodeURIComponent(panjang)}&geojson=${encodeURIComponent(geojson)}`
}).then(r=>r.text()).then(r=>{
const id=r.split('|')[1];
if(drawPolyline){map.removeLayer(drawPolyline);drawPolyline=null;}
tempMarkers.forEach(m=>map.removeLayer(m));tempMarkers=[];
const line=L.polyline(drawPoints.map(p=>[p.lat,p.lng]),{color:getColorJalan(status),weight:4})
.bindPopup(popupJalan(id,nama,status,panjang));
layerGroups.jalan.addLayer(line);
cachedData.jalan.push({id,nama,status,panjang,_layer:line});
drawPoints=[];
document.getElementById('formJalan').style.display='none';
document.getElementById('jalan_nama').value='';
showNotif('Jalan berhasil disimpan!');setMode('view');
}).catch(()=>showNotif('Gagal menyimpan jalan','#dc2626'));
}
function hapusJalan(id) {
if (!confirm('Hapus data jalan ini?')) return;
fetch('delete_jalan.php?id='+id).then(()=>{
layerGroups.jalan.eachLayer(l=>{
if(l._popup&&l.getPopup().getContent().includes(`hapusJalan(${id})`)) layerGroups.jalan.removeLayer(l);
});
cachedData.jalan=cachedData.jalan.filter(d=>d.id!=id);
showNotif('Jalan dihapus');
});
}
// EDIT JALAN
function bukaEditJalan(id) {
document.getElementById('editKavlingForm').style.display='none';
const data=cachedData.jalan.find(d=>d.id==id); if(!data)return;
editingType='jalan'; editingId=id; editingLayer=data._layer;
if(editingLayer)editingLayer.closePopup();
document.getElementById('edit_jalan_nama').value =data.nama;
document.getElementById('edit_jalan_status').value =data.status;
document.getElementById('edit_jalan_panjang').value=data.panjang;
document.getElementById('editJalanForm').style.display='block';
document.getElementById('editModal').classList.add('show');
}
function startEditGeom(type) {
showNotif(type==='jalan'?"Modifikasi rute: Klik peta untuk tambah titik atau Undo, lalu Selesai":"Modifikasi batas: Klik peta untuk tambah titik atau Undo, lalu Selesai","#ea580c");
document.getElementById('editModal').classList.remove('show');
isRedrawing=true;
// Ambil titik yang sudah ada agar bisa dimodifikasi (tambah/undo)
if (editingLayer) {
const rawPts = type === 'jalan' ? editingLayer.getLatLngs() : editingLayer.getLatLngs()[0];
// Pastikan kita mendapatkan array flat LatLng
editDrawPoints = Array.isArray(rawPts) ? [...rawPts] : [];
editingLayer.setStyle({opacity:0.2, fillOpacity:0.05});
// Non-aktifkan interaksi agar tidak menghalangi klik pada peta di bawahnya
if (editingLayer.getElement()) editingLayer.getElement().style.pointerEvents = 'none';
} else {
editDrawPoints = [];
}
editDrawPolyline=null; editDrawPolygon=null;
editTempMarkers.forEach(m=>map.removeLayer(m)); editTempMarkers=[];
// Tampilkan marker untuk titik-titik yang sudah ada
editDrawPoints.forEach(latlng => {
const m=L.circleMarker(latlng,{radius:5,color:'#a855f7',fillColor:'#a855f7',fillOpacity:1}).addTo(map);
editTempMarkers.push(m);
});
// Tampilkan preview garis/area jika sudah ada titiknya
if(type==='jalan' && editDrawPoints.length >= 2){
editDrawPolyline=L.polyline(editDrawPoints,{color:'#a855f7',weight:3,dashArray:'6,4'}).addTo(map);
} else if(type==='kavling' && editDrawPoints.length >= 3){
editDrawPolygon=L.polygon(editDrawPoints,{color:'#a855f7',fillColor:'#a855f7',fillOpacity:0.25,weight:2,dashArray:'6,4'}).addTo(map);
}
setHint(type==='jalan'?'Klik pada peta untuk menambah rute jalan, gunakan Undo untuk menghapus titik terakhir.':'Klik untuk menambah batas kavling, gunakan Undo untuk menghapus.');
document.getElementById('btnFinishDraw').style.display='block';
document.getElementById('btnUndoDraw').style.display=editDrawPoints.length>0?'block':'none';
}
function addEditDrawPoint(latlng) {
editDrawPoints.push(latlng);
const m=L.circleMarker(latlng,{radius:5,color:'#a855f7',fillColor:'#a855f7',fillOpacity:1}).addTo(map);
editTempMarkers.push(m);
if(editingType==='jalan'){
if(editDrawPolyline)map.removeLayer(editDrawPolyline);
editDrawPolyline=L.polyline(editDrawPoints,{color:'#a855f7',weight:3,dashArray:'6,4'}).addTo(map);
document.getElementById('btnFinishDraw').style.display=editDrawPoints.length>=2?'block':'none';
} else {
if(editDrawPolygon)map.removeLayer(editDrawPolygon);
if(editDrawPoints.length>=3)
editDrawPolygon=L.polygon(editDrawPoints,{color:'#a855f7',fillColor:'#a855f7',fillOpacity:0.25,weight:2,dashArray:'6,4'}).addTo(map);
document.getElementById('btnFinishDraw').style.display=editDrawPoints.length>=3?'block':'none';
}
document.getElementById('btnUndoDraw').style.display='block';
}
function undoEditDrawPoint() {
if(!editDrawPoints.length)return;
editDrawPoints.pop();
const last=editTempMarkers.pop(); if(last)map.removeLayer(last);
if(editDrawPolyline){map.removeLayer(editDrawPolyline);editDrawPolyline=null;}
if(editDrawPolygon){map.removeLayer(editDrawPolygon);editDrawPolygon=null;}
if(editDrawPoints.length>=2&&editingType==='jalan')
editDrawPolyline=L.polyline(editDrawPoints,{color:'#a855f7',weight:3,dashArray:'6,4'}).addTo(map);
if(editDrawPoints.length>=3&&editingType==='kavling')
editDrawPolygon=L.polygon(editDrawPoints,{color:'#a855f7',fillColor:'#a855f7',fillOpacity:0.25,weight:2,dashArray:'6,4'}).addTo(map);
document.getElementById('btnFinishDraw').style.display=
(editingType==='jalan'&&editDrawPoints.length>=2)||(editingType==='kavling'&&editDrawPoints.length>=3)?'block':'none';
}
function finishEditGeom() {
if(editDrawPoints.length===0 && editingLayer){
const rawPts = editingType==='jalan' ? editingLayer.getLatLngs() : editingLayer.getLatLngs()[0];
editDrawPoints = Array.isArray(rawPts) ? [...rawPts] : [];
}
if(editingType==='jalan'){
if(editDrawPoints.length<2){showNotif('Minimal 2 titik!','#dc2626');return;}
let p=0; for(let i=1;i<editDrawPoints.length;i++) p+=editDrawPoints[i-1].distanceTo(editDrawPoints[i]);
document.getElementById('edit_jalan_panjang').value=Math.round(p)+' meter';
} else {
if(editDrawPoints.length<3){showNotif('Minimal 3 titik!','#dc2626');return;}
document.getElementById('edit_kavling_luas').value=Math.round(hitungLuasPolygon(editDrawPoints))+' m2';
}
if(editDrawPolyline){map.removeLayer(editDrawPolyline);editDrawPolyline=null;}
if(editDrawPolygon){map.removeLayer(editDrawPolygon);editDrawPolygon=null;}
editTempMarkers.forEach(m=>map.removeLayer(m)); editTempMarkers=[];
setHint(null);
document.getElementById('btnFinishDraw').style.display='none';
document.getElementById('btnUndoDraw').style.display='none';
isRedrawing=false;
if(editingLayer) {
editingLayer.setStyle({opacity:1,fillOpacity:0.35});
if (editingLayer.getElement()) editingLayer.getElement().style.pointerEvents = 'auto';
}
document.getElementById('editModal').classList.add('show');
}
function tutupEditModal() {
document.getElementById('editModal').classList.remove('show');
isRedrawing=false; editDrawPoints=[];
if(editDrawPolyline){map.removeLayer(editDrawPolyline);editDrawPolyline=null;}
if(editDrawPolygon){map.removeLayer(editDrawPolygon);editDrawPolygon=null;}
editTempMarkers.forEach(m=>map.removeLayer(m)); editTempMarkers=[];
if(editingLayer) {
editingLayer.setStyle({opacity:1,fillOpacity:0.35});
if (editingLayer.getElement()) editingLayer.getElement().style.pointerEvents = 'auto';
}
setHint(null);
document.getElementById('btnFinishDraw').style.display='none';
document.getElementById('btnUndoDraw').style.display='none';
editingType=null; editingId=null; editingLayer=null;
}
function tampilDetailRumah(id) {
const d = cachedData.rumah.find(item => item.id == id);
if (!d) {
showNotif('Data rumah tidak ditemukan!', '#dc2626');
return;
}
const result = isInsideAnyTempatIbadah(d.lat, d.lng);
const inR = result.inRadius;
const tempatIbadahNama = result.tempatIbadah ? result.tempatIbadah.nama : '-';
const tipeIbadah = result.tempatIbadah ? result.tempatIbadah.tipe : '-';
const jarak = result.jarak ? result.jarak : 0;
const statusBantuanText = inR
? `<span class="detail-badge-in">🟢 Layak Mendapatkan Bantuan</span>`
: `<span class="detail-badge-out">🔴 Di Luar Jangkauan Bantuan</span>`;
let statusBantuanDetail = '';
if (inR) {
statusBantuanDetail = `
<div class="detail-item full-width">
<div class="detail-label">Rumah Ibadah Terdekat</div>
<div class="detail-value">🕌 ${tempatIbadahNama} (${tipeIbadah})</div>
</div>
<div class="detail-item">
<div class="detail-label">Jarak ke Rumah Ibadah</div>
<div class="detail-value">${jarak} Meter</div>
</div>
<div class="detail-item">
<div class="detail-label">Status Distribusi Bantuan</div>
<div class="detail-value" style="text-transform: capitalize; font-weight: 700; color: #16a34a;">
✅ Sudah Menerima Bantuan
</div>
</div>
`;
if (d.jenis_bantuan) {
statusBantuanDetail += `
<div class="detail-item full-width">
<div class="detail-label">Jenis Bantuan Yang Diperoleh</div>
<div class="detail-value">${d.jenis_bantuan}</div>
</div>
`;
}
} else {
statusBantuanDetail = `
<div class="detail-item">
<div class="detail-label">Status Distribusi Bantuan</div>
<div class="detail-value" style="text-transform: capitalize; font-weight: 700; color: #ea580c;">
⏳ Belum Menerima Bantuan
</div>
</div>
<div class="detail-item full-width">
<div class="detail-label">Keterangan Radius</div>
<div class="detail-value">Lokasi rumah berada di luar radius pelayanan seluruh rumah ibadah yang terdaftar.</div>
</div>
`;
}
const html = `
<div class="detail-section-title">👤 Data Kepala Keluarga & Demografi</div>
<div class="detail-grid">
<div class="detail-item full-width">
<div class="detail-label">Nama Kepala Keluarga (KK)</div>
<div class="detail-value" style="font-weight:700;">🏠 ${d.nama}</div>
</div>
<div class="detail-item">
<div class="detail-label">Umur Kepala Keluarga</div>
<div class="detail-value">${d.umur_kk ? d.umur_kk + ' Tahun' : '-'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Jumlah Anggota Keluarga</div>
<div class="detail-value">${d.jumlah_anggota ? d.jumlah_anggota + ' Orang' : '1 Orang'}</div>
</div>
</div>
<div class="detail-section-title">💼 Informasi Ekonomi & Kepemilikan</div>
<div class="detail-grid">
<div class="detail-item">
<div class="detail-label">Pekerjaan</div>
<div class="detail-value">${d.pekerjaan || '-'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Penghasilan Perbulan</div>
<div class="detail-value" style="color:var(--primary); font-weight:700;">${d.penghasilan || '-'}</div>
</div>
<div class="detail-item full-width">
<div class="detail-label">Status Kepemilikan Rumah</div>
<div class="detail-value">${d.status_rumah || '-'}</div>
</div>
</div>
<div class="detail-section-title">📍 Lokasi & Alamat</div>
<div class="detail-grid">
<div class="detail-item full-width">
<div class="detail-label">Alamat Lengkap</div>
<div class="detail-value">${d.alamat || '-'}</div>
</div>
<div class="detail-item">
<div class="detail-label">Latitude</div>
<div class="detail-value">${parseFloat(d.lat).toFixed(6)}</div>
</div>
<div class="detail-item">
<div class="detail-label">Longitude</div>
<div class="detail-value">${parseFloat(d.lng).toFixed(6)}</div>
</div>
<div class="detail-item full-width">
<div class="detail-label">Keterangan Tambahan</div>
<div class="detail-value">${d.ket || '-'}</div>
</div>
</div>
<div class="detail-section-title">🟢 Program & Status Bantuan Sosial</div>
<div class="detail-grid">
<div class="detail-item full-width">
<div class="detail-label">Status Kelayakan Bantuan</div>
<div style="margin-top: 4px;">${statusBantuanText}</div>
</div>
${statusBantuanDetail}
</div>
`;
document.getElementById('detailRumahModalBody').innerHTML = html;
document.getElementById('detailRumahModal').classList.add('show');
}
function tutupDetailRumahModal() {
document.getElementById('detailRumahModal').classList.remove('show');
document.getElementById('detailRumahModalBody').innerHTML = '';
}
let activeDetailTempatIbadahId = null;
function showDetailBantuanTempatIbadah(tempatIbadahId) {
activeDetailTempatIbadahId = tempatIbadahId;
renderDetailBantuanTempatIbadah();
document.getElementById('detailBantuanTempatIbadahModal').style.display = 'flex';
}
function tutupDetailBantuanTempatIbadahModal() {
activeDetailTempatIbadahId = null;
document.getElementById('detailBantuanTempatIbadahModal').style.display = 'none';
document.getElementById('detailBantuanTempatIbadahModalBody').innerHTML = '';
}
function renderDetailBantuanTempatIbadah() {
if (!activeDetailTempatIbadahId) return;
const inst = tempatIbadahInstances.find(m => String(m.id) === String(activeDetailTempatIbadahId));
if (!inst) return;
let emoji = '🕌';
if (inst.tipe === 'Gereja') emoji = '⛪';
else if (inst.tipe === 'Pura') emoji = '🛕';
else if (inst.tipe === 'Vihara') emoji = '🛕';
else if (inst.tipe === 'Klenteng') emoji = '⛩️';
document.getElementById('detailBantuanTIHeaderEmoji').textContent = emoji;
document.getElementById('detailBantuanTITitle').textContent = `Penerima Bantuan - ${inst.nama}`;
const currentRadius = inst._pendingRadius !== undefined && inst._pendingRadius !== null ? inst._pendingRadius : inst.radius;
const list = getRumahTerbantuForTempatIbadah(inst);
let html = `
<div style="margin-bottom:15px;">
<div style="font-size:13px; color:var(--text-2); margin-bottom:4px;">
<b>Tipe:</b> ${inst.tipe}
</div>
<div style="font-size:13px; color:var(--text-2); margin-bottom:4px;">
<b>Alamat:</b> ${inst.alamat || '-'}
</div>
<div style="font-size:13px; color:var(--text-2); margin-bottom:4px;">
<b>Radius Jangkauan Bantuan:</b> ${fmtRadius(currentRadius)}
</div>
<div style="font-size:13px; color:var(--text-2); margin-bottom:12px;">
<b>Total Rumah Dibantu:</b> <span style="font-weight:700; color:#059669;">${list.length} Rumah</span>
</div>
</div>
<div class="detail-section-title" style="margin-top:0;">📋 Daftar Rumah Penerima Bantuan</div>
`;
if (list.length === 0) {
html += `
<div style="text-align:center; padding:30px 10px; color:#5a6478; font-size:13px; background:#f8f9fb; border-radius:8px; border:1px dashed #e4e7ec;">
📭 Tidak ada rumah dalam radius bantuan ${fmtRadius(currentRadius)}.
</div>
`;
} else {
html += `<div style="display:flex; flex-direction:column; gap:10px; max-height: 400px; overflow-y: auto; padding-right: 4px;">`;
list.forEach((item, index) => {
const r = item.rumah;
const j = item.jarak;
html += `
<div style="background:white; border:1px solid #e4e7ec; border-radius:8px; padding:12px; display:flex; flex-direction:column; gap:4px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); transition: transform 0.2s, box-shadow 0.2s;" class="rumah-bantuan-item-hover">
<div style="display:flex; justify-content:space-between; align-items:flex-start; gap:8px;">
<div style="font-weight:700; font-size:13px; color:#1e293b; display:flex; align-items:center; gap:6px;">
<span style="background:#e0f2fe; color:#0369a1; border-radius:50%; width:20px; height:20px; display:inline-flex; align-items:center; justify-content:center; font-size:10px;">${index + 1}</span>
🏠 ${r.nama}
</div>
<span style="font-size:11px; background:#e0f5e9; color:#16a34a; padding:3px 8px; border-radius:12px; font-weight:700; white-space:nowrap; display:flex; align-items:center; gap:3px;">
✅ Sudah Menerima Bantuan
</span>
</div>
<div style="font-size:12px; color:#5a6478; margin-left: 26px;">
<b>Alamat:</b> ${r.alamat || '-'}
</div>
<div style="display:flex; justify-content:space-between; align-items:center; margin-top:6px; padding-top:6px; border-top:1px dashed #f1f5f9; font-size:11px; margin-left: 26px;">
<span style="color:#64748b; font-weight:600;">Jarak: <span style="color:#0f172a; font-weight:700;">${j} m</span></span>
<button style="background:transparent; border:none; color:#ea580c; cursor:pointer; font-weight:700; font-size:11px; padding:0; display:flex; align-items:center; gap:2px;"
onclick="tutupDetailBantuanTempatIbadahModal(); zoomToRumah(${r.id});">
📍 Lihat di Peta &rarr;
</button>
</div>
</div>
`;
});
html += `</div>`;
}
document.getElementById('detailBantuanTempatIbadahModalBody').innerHTML = html;
}
function zoomToRumah(id) {
const r = cachedData.rumah.find(x => x.id == id);
if (r && r._marker) {
map.setView(r._marker.getLatLng(), 18);
r._marker.openPopup();
}
}
function simpanEditJalan() {
const nama=document.getElementById('edit_jalan_nama').value.trim();
const status=document.getElementById('edit_jalan_status').value;
const panjang=document.getElementById('edit_jalan_panjang').value;
if(!nama){showNotif('Nama jalan wajib diisi!','#dc2626');return;}
const pts=editDrawPoints.length>0?editDrawPoints:editingLayer.getLatLngs();
const geojson=JSON.stringify({type:'LineString',coordinates:pts.map(p=>[p.lng,p.lat])});
fetch('update_jalan.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`id=${editingId}&nama=${encodeURIComponent(nama)}&status=${status}&panjang=${encodeURIComponent(panjang)}&geojson=${encodeURIComponent(geojson)}`
}).then(()=>{
if(editDrawPoints.length>0) editingLayer.setLatLngs(editDrawPoints.map(p=>[p.lat,p.lng]));
editingLayer.setStyle({color:getColorJalan(status),weight:4,opacity:1});
editingLayer.bindPopup(popupJalan(editingId,nama,status,panjang));
const d=cachedData.jalan.find(x=>x.id==editingId);
if(d){d.nama=nama;d.status=status;d.panjang=panjang;}
tutupEditModal(); showNotif('Jalan berhasil diupdate!');
}).catch(()=>showNotif('Gagal update jalan','#dc2626'));
}
// =====================================================================
// KAVLING CRUD
// =====================================================================
function tutupFormKavling() { document.getElementById('formKavling').style.display='none'; cancelDraw(); setMode('view'); }
function simpanKavling() {
const nama=document.getElementById('kavling_nama').value.trim();
const status=document.getElementById('kavling_status').value;
const luas=document.getElementById('kavling_luas').value;
if(!nama){showNotif('Nama kavling wajib diisi!','#dc2626');return;}
const geojson=JSON.stringify({type:'Polygon',coordinates:[drawPoints.map(p=>[p.lng,p.lat]).concat([[drawPoints[0].lng,drawPoints[0].lat]])]});
fetch('save_kavling.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`nama=${encodeURIComponent(nama)}&status=${status}&luas=${encodeURIComponent(luas)}&geojson=${encodeURIComponent(geojson)}`
}).then(r=>r.text()).then(r=>{
const id=r.split('|')[1];
if(drawPolygon){map.removeLayer(drawPolygon);drawPolygon=null;}
tempMarkers.forEach(m=>map.removeLayer(m));tempMarkers=[];
const color=getColorKavling(status);
const poly=L.polygon(drawPoints.map(p=>[p.lat,p.lng]),{color,fillColor:color,fillOpacity:0.35,weight:2})
.bindPopup(popupKavling(id,nama,status,luas));
layerGroups.kavling.addLayer(poly);
cachedData.kavling.push({id,nama,status,luas,_layer:poly});
drawPoints=[];
document.getElementById('formKavling').style.display='none';
document.getElementById('kavling_nama').value='';
showNotif('Kavling berhasil disimpan!');setMode('view');
}).catch(()=>showNotif('Gagal menyimpan kavling','#dc2626'));
}
function hapusKavling(id) {
if(!confirm('Hapus data kavling ini?'))return;
fetch('delete_kavling.php?id='+id).then(()=>{
layerGroups.kavling.eachLayer(l=>{
if(l._popup&&l.getPopup().getContent().includes(`hapusKavling(${id})`)) layerGroups.kavling.removeLayer(l);
});
cachedData.kavling=cachedData.kavling.filter(d=>d.id!=id);
showNotif('Kavling dihapus');
});
}
function bukaEditKavling(id) {
document.getElementById('editJalanForm').style.display='none';
const data=cachedData.kavling.find(d=>d.id==id); if(!data)return;
editingType='kavling'; editingId=id; editingLayer=data._layer;
if(editingLayer)editingLayer.closePopup();
document.getElementById('edit_kavling_nama').value =data.nama;
document.getElementById('edit_kavling_status').value=data.status;
document.getElementById('edit_kavling_luas').value =data.luas;
document.getElementById('editKavlingForm').style.display='block';
document.getElementById('editModal').classList.add('show');
}
function simpanEditKavling() {
const nama=document.getElementById('edit_kavling_nama').value.trim();
const status=document.getElementById('edit_kavling_status').value;
const luas=document.getElementById('edit_kavling_luas').value;
if(!nama){showNotif('Nama kavling wajib diisi!','#dc2626');return;}
const pts=editDrawPoints.length>0?editDrawPoints:editingLayer.getLatLngs()[0];
const geojson=JSON.stringify({type:'Polygon',coordinates:[pts.map(p=>[p.lng,p.lat]).concat([[pts[0].lng,pts[0].lat]])]});
fetch('update_kavling.php',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`id=${editingId}&nama=${encodeURIComponent(nama)}&status=${status}&luas=${encodeURIComponent(luas)}&geojson=${encodeURIComponent(geojson)}`
}).then(()=>{
const color=getColorKavling(status);
if(editDrawPoints.length>0) editingLayer.setLatLngs(editDrawPoints.map(p=>[p.lat,p.lng]));
editingLayer.setStyle({color,fillColor:color,fillOpacity:0.35,weight:2,opacity:1});
editingLayer.bindPopup(popupKavling(editingId,nama,status,luas));
const d=cachedData.kavling.find(x=>x.id==editingId);
if(d){d.nama=nama;d.status=status;d.luas=luas;}
tutupEditModal(); showNotif('Kavling berhasil diupdate!');
}).catch(()=>showNotif('Gagal update kavling','#dc2626'));
}
// =====================================================================
// RUMAH BANTUAN CRUD
// =====================================================================
function openFormRumah(latlng) {
if (previewMarker) map.removeLayer(previewMarker);
previewMarker = L.circleMarker([latlng.lat, latlng.lng], {
radius:8, color:'#0891b2', fillColor:'#22d3ee', fillOpacity:0.7, weight:2
}).addTo(map);
document.getElementById('rumah_nama').value='';
document.getElementById('rumah_umur_kk').value='';
document.getElementById('rumah_pekerjaan').value='';
document.getElementById('rumah_pekerjaan_lainnya').value='';
document.getElementById('rumah_pekerjaan_lainnya').style.display='none';
document.getElementById('rumah_penghasilan').value='';
document.getElementById('rumah_jumlah_anggota').value='1';
document.getElementById('rumah_status_rumah').value='';
document.getElementById('rumah_ket').value='';
document.getElementById('rumah_alamat').value='Mengambil alamat...';
document.getElementById('formRumah').style.display='block';
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${latlng.lat}&lon=${latlng.lng}&zoom=18&addressdetails=1`)
.then(r=>r.json())
.then(data => { document.getElementById('rumah_alamat').value=data.display_name||'-'; })
.catch(()=>{ document.getElementById('rumah_alamat').value='-'; });
}
function tutupFormRumah() {
document.getElementById('formRumah').style.display='none';
if (previewMarker){map.removeLayer(previewMarker);previewMarker=null;}
}
function togglePekerjaanLainnya(val) {
const inputLainnya = document.getElementById('rumah_pekerjaan_lainnya');
if (val === 'Lainnya') {
inputLainnya.style.display = 'block';
inputLainnya.focus();
} else {
inputLainnya.style.display = 'none';
inputLainnya.value = '';
}
}
function simpanRumah() {
const nama = document.getElementById('rumah_nama').value.trim();
const umur_kk = document.getElementById('rumah_umur_kk').value;
let pekerjaan = document.getElementById('rumah_pekerjaan').value;
if (pekerjaan === 'Lainnya') {
const pekerjaanLainnya = document.getElementById('rumah_pekerjaan_lainnya').value.trim();
if (pekerjaanLainnya) {
pekerjaan = pekerjaanLainnya;
}
}
const penghasilan = document.getElementById('rumah_penghasilan').value;
const jumlah_anggota = document.getElementById('rumah_jumlah_anggota').value;
const status_rumah = document.getElementById('rumah_status_rumah').value;
const alamat = document.getElementById('rumah_alamat').value;
const ket = document.getElementById('rumah_ket').value.trim();
if (!nama){ showNotif('Nama pemilik wajib diisi!','#dc2626'); return; }
const result = isInsideAnyTempatIbadah(rumahLat, rumahLng);
const bodyData = `nama=${encodeURIComponent(nama)}` +
`&alamat=${encodeURIComponent(alamat)}` +
`&keterangan=${encodeURIComponent(ket)}` +
`&lat=${rumahLat}` +
`&lng=${rumahLng}` +
`&umur_kk=${encodeURIComponent(umur_kk)}` +
`&pekerjaan=${encodeURIComponent(pekerjaan)}` +
`&penghasilan=${encodeURIComponent(penghasilan)}` +
`&jumlah_anggota=${encodeURIComponent(jumlah_anggota)}` +
`&status_rumah=${encodeURIComponent(status_rumah)}`;
fetch('save_rumah.php', {
method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},
body: bodyData
}).then(r=>r.text()).then(r=>{
const parts = r.split('|');
if (parts[0] !== 'success') {
showNotif('Gagal menyimpan rumah! ' + r, '#dc2626');
return;
}
const id = parts[1];
if(previewMarker){ map.removeLayer(previewMarker); previewMarker=null; }
const marker = L.marker([rumahLat,rumahLng], {icon:getRumahIcon(result.inRadius)})
.bindPopup(popupRumah(id, nama, alamat, result));
layerGroups.rumah.addLayer(marker);
cachedData.rumah.push({
id: id,
nama: nama,
alamat: alamat,
ket: ket,
lat: rumahLat,
lng: rumahLng,
umur_kk: umur_kk,
pekerjaan: pekerjaan,
penghasilan: penghasilan,
jumlah_anggota: jumlah_anggota,
status_rumah: status_rumah,
_marker: marker
});
document.getElementById('formRumah').style.display='none';
recheckAllRumah();
showNotif(result.inRadius ? '🟢 Rumah dalam radius bantuan!' : '🔴 Rumah di luar radius bantuan.', result.inRadius ? '#16a34a' : '#dc2626');
}).catch(()=>{
showNotif('Gagal menyimpan rumah! Cek koneksi database.', '#dc2626');
});
}
function hapusRumah(id) {
if(!confirm('Hapus data rumah ini?'))return;
fetch('delete_rumah.php?id='+id).catch(()=>{});
layerGroups.rumah.eachLayer(l=>{
if(l._popup&&l.getPopup().getContent().includes(`hapusRumah(${id})`)) layerGroups.rumah.removeLayer(l);
});
cachedData.rumah=cachedData.rumah.filter(d=>d.id!=id);
recheckAllRumah();
showNotif('Rumah dihapus');
}
// =====================================================================
// LOAD DATA
// =====================================================================
function loadAllData() {
fetch('load_spbu.php').then(r=>r.json()).then(data=>{
data.forEach(d=>{
const marker=L.marker([d.latitude,d.longitude],{icon:getIconSPBU(d.buka_24jam)})
.bindPopup(popupSPBU(d.id,d.nama_spbu,d.no_wa,d.buka_24jam),{offset:[0,-35]});
layerGroups.spbu.addLayer(marker);
cachedData.spbu.push({id:d.id,nama:d.nama_spbu,wa:d.no_wa,buka:d.buka_24jam,lat:d.latitude,lng:d.longitude,_marker:marker});
});
}).catch(e=>console.warn('load_spbu error',e));
fetch('load_jalan.php').then(r=>r.json()).then(data=>{
data.forEach(d=>{
try {
const geo = JSON.parse(d.geojson);
// Handle both plain Geometry {type:'LineString',...} and GeoJSON Feature {type:'Feature', geometry:{...}}
const geom = (geo.type === 'Feature') ? geo.geometry : geo;
const coords = geom.coordinates.map(c=>[c[1],c[0]]);
const line = L.polyline(coords,{color:getColorJalan(d.status_jalan),weight:4})
.bindPopup(popupJalan(d.id,d.nama_jalan,d.status_jalan,d.panjang_jalan));
layerGroups.jalan.addLayer(line);
cachedData.jalan.push({id:d.id,nama:d.nama_jalan,status:d.status_jalan,panjang:d.panjang_jalan,_layer:line});
} catch(err) {
console.warn('Error render jalan id=' + d.id, err);
}
});
}).catch(e=>console.warn('load_jalan error',e));
fetch('load_kavling.php').then(r=>r.json()).then(data=>{
data.forEach(d=>{
try {
const geo = JSON.parse(d.geojson);
// Handle both plain Geometry {type:'Polygon',...} and GeoJSON Feature {type:'Feature', geometry:{...}}
const geom = (geo.type === 'Feature') ? geo.geometry : geo;
const coords = geom.coordinates[0].map(c=>[c[1],c[0]]);
const color = getColorKavling(d.status_kepemilikan);
const poly = L.polygon(coords,{color,fillColor:color,fillOpacity:0.35,weight:2})
.bindPopup(popupKavling(d.id,d.nama_kavling,d.status_kepemilikan,d.luas_tanah));
layerGroups.kavling.addLayer(poly);
cachedData.kavling.push({id:d.id,nama:d.nama_kavling,status:d.status_kepemilikan,luas:d.luas_tanah,_layer:poly});
} catch(err) {
console.warn('Error render kavling id=' + d.id, err);
}
});
}).catch(e=>console.warn('load_kavling error',e));
}
// =====================================================================
// SEARCH
// =====================================================================
function onFilterChange(val) {
const q=document.getElementById('searchInput').value.trim();
if(val!=='all'&&q==='')showAllForType(val); else liveSearch(q);
}
function onSearchFocus() {
const val=document.getElementById('searchFilter').value;
const q=document.getElementById('searchInput').value.trim();
if(val!=='all'&&q==='')showAllForType(val);
}
function showAllForType(type) {
const results=[];
if(type==='spbu') cachedData.spbu.forEach(d=>results.push({type:'spbu',label:d.nama,sub:`24 Jam: ${d.buka}`,data:d}));
else if(type==='jalan') cachedData.jalan.forEach(d=>results.push({type:'jalan',label:d.nama,sub:`Jalan ${d.status} - ${d.panjang}`,data:d}));
else if(type==='kavling') cachedData.kavling.forEach(d=>results.push({type:'kavling',label:d.nama,sub:`${d.status} - ${d.luas}`,data:d}));
else if(type==='rumah') cachedData.rumah.forEach(d=>results.push({type:'rumah',label:d.nama,sub:d.alamat||'-',data:d}));
else if(type==='tempat_ibadah') tempatIbadahInstances.forEach(m=>results.push({type:'tempat_ibadah',label:m.nama,sub:`${m.tipe} - Radius: ${fmtRadius(m.radius)}`,data:m}));
renderSearchResults(results,results.length===0?`Belum ada data ${type}`:null);
}
function liveSearch(query) {
query=query.trim().toLowerCase();
const filter=document.getElementById('searchFilter').value;
const box=document.getElementById('searchResults');
if(!query&&filter==='all'){box.style.display='none';return;}
if(!query&&filter!=='all'){showAllForType(filter);return;}
const results=[];
if(filter==='all'||filter==='spbu') cachedData.spbu.forEach(d=>{if(d.nama.toLowerCase().includes(query))results.push({type:'spbu',label:d.nama,sub:`24 Jam: ${d.buka}`,data:d});});
if(filter==='all'||filter==='jalan') cachedData.jalan.forEach(d=>{if(d.nama.toLowerCase().includes(query))results.push({type:'jalan',label:d.nama,sub:`Jalan ${d.status} - ${d.panjang}`,data:d});});
if(filter==='all'||filter==='kavling') cachedData.kavling.forEach(d=>{if(d.nama.toLowerCase().includes(query))results.push({type:'kavling',label:d.nama,sub:`${d.status} - ${d.luas}`,data:d});});
if(filter==='all'||filter==='rumah') cachedData.rumah.forEach(d=>{if(d.nama.toLowerCase().includes(query))results.push({type:'rumah',label:d.nama,sub:d.alamat||'-',data:d});});
if(filter==='all'||filter==='tempat_ibadah') tempatIbadahInstances.forEach(m=>{if(m.nama.toLowerCase().includes(query))results.push({type:'tempat_ibadah',label:m.nama,sub:`${m.tipe} - Radius: ${fmtRadius(m.radius)}`,data:m});});
renderSearchResults(results);
}
function renderSearchResults(results,emptyMsg) {
const box=document.getElementById('searchResults');
if(results.length===0){
box.innerHTML=`<div class="search-item" style="color:#9aa3b2;cursor:default;">${emptyMsg||'Tidak ada hasil ditemukan'}</div>`;
} else {
box.innerHTML=results.map(r=>`
<div class="search-item">
<span class="search-badge badge-${r.type.replace('_','-')}">${r.type.toUpperCase().replace('_',' ')}</span>
<div>
<div style="font-weight:700;font-size:13px;">${r.label}</div>
<div style="font-size:11.5px;color:#9aa3b2;">${r.sub}</div>
</div>
</div>`).join('');
box.querySelectorAll('.search-item').forEach((el,i)=>{el.onclick=()=>focusResult(results[i]);});
}
box.style.display='block';
}
function doSearch(){liveSearch(document.getElementById('searchInput').value);}
function focusResult(r) {
document.getElementById('searchResults').style.display='none';
document.getElementById('searchInput').value=r.label;
if(r.type==='spbu'||r.type==='rumah'){
map.setView([r.data.lat,r.data.lng],17);
if(r.data._marker)r.data._marker.openPopup();
} else if(r.type==='tempat_ibadah'){
map.setView([r.data.lat,r.data.lng],15);
if(r.data.marker)r.data.marker.openPopup();
} else if(r.data._layer){
map.fitBounds(r.data._layer.getBounds(),{padding:[40,40]});
r.data._layer.openPopup();
}
setTimeout(()=>{
document.getElementById('searchInput').value='';
document.getElementById('searchFilter').value='all';
document.getElementById('searchResults').style.display='none';
},400);
}
// =====================================================================
// LEGEND FILTER
// =====================================================================
function filterLayer(type,value) {
if(type==='spbu'){
layerGroups.spbu.eachLayer(l=>{
const c=l._popup?l.getPopup().getContent():'';
const match=value==='Ya'?c.includes('24 Jam: Ya'):c.includes('24 Jam: Tidak');
l.setOpacity(match?1:0.15);
});
} else if(type==='jalan'){
layerGroups.jalan.eachLayer(l=>{
const c=l._popup?l.getPopup().getContent():'';
l.setStyle({opacity:c.includes(`Jalan ${value}`)?1:0.1});
});
} else if(type==='kavling'){
layerGroups.kavling.eachLayer(l=>{
const c=l._popup?l.getPopup().getContent():'';
l.setStyle(c.includes(`Status: ${value}`)?{opacity:1,fillOpacity:0.35}:{opacity:0.1,fillOpacity:0.05});
});
}
showNotif('Filter: '+value,'#ea580c');
}
function resetFilter(){
layerGroups.spbu.eachLayer(l=>l.setOpacity(1));
layerGroups.jalan.eachLayer(l=>l.setStyle({opacity:1}));
layerGroups.kavling.eachLayer(l=>l.setStyle({opacity:1,fillOpacity:0.35}));
showNotif('Semua layer ditampilkan','#16a34a');
}
// =====================================================================
// KEYBOARD
// =====================================================================
document.addEventListener('keydown',function(e){
if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='z'){
if(currentMode==='jalan'||currentMode==='kavling'||isRedrawing){e.preventDefault();handleUndo();}
}
});
// =====================================================================
// LOAD GEOJSON KECAMATAN
// =====================================================================
fetch('Admin_Kecamatan.json')
.then(res=>res.json())
.then(data=>{
L.geoJSON(data,{
style:function(feature){
const nama=feature.properties.keterangan||feature.properties.Ket||feature.properties.nama||feature.properties.KECAMATAN||'Unknown';
const color=getKecamatanColor(nama);
return {color,fillColor:color,fillOpacity:0.15,weight:2};
},
onEachFeature:function(feature,layer){
const nama=feature.properties.keterangan||feature.properties.Ket||feature.properties.nama||feature.properties.KECAMATAN||'Tidak diketahui';
layer.on('mouseover',()=>layer.setStyle({fillOpacity:0.15,weight:4}));
layer.on('mouseout', ()=>layer.setStyle({fillOpacity:0.15,weight:2}));
}
}).addTo(layerGroups.kecamatan);
buildKecamatanLegend();
})
.catch(err=>console.error('Gagal load JSON kecamatan:',err));
function buildKecamatanLegend() {
const list=document.getElementById('legendKecamatanList');
list.innerHTML='';
Object.entries(kecamatanColorMap).forEach(([nama,color])=>{
const item=document.createElement('div');
item.className='kec-legend-item';
item.innerHTML=`<div class="kec-legend-swatch" style="background:${color};border-color:${color};"></div>
<span style="font-size:11.5px;color:#1a202c;line-height:1.3;">${nama}</span>`;
list.appendChild(item);
});
}
// =====================================================================
// INIT
// =====================================================================
loadAllData();
// Terapkan draggable pada form popups & panel
(function() {
const panels = [
{ id: 'panelTempatIbadah', headerTag: 'h3' },
{ id: 'formSPBU', headerTag: 'h3' },
{ id: 'formJalan', headerTag: 'h3' },
{ id: 'formKavling', headerTag: 'h3' },
{ id: 'formRumah', headerTag: 'h3' }
];
panels.forEach(p => {
const el = document.getElementById(p.id);
if (el) {
const header = el.querySelector(p.headerTag);
makeElementDraggable(el, header);
}
});
})();
</script>
</body>
</html>