Files
2026-06-10 20:12:23 +07:00

1176 lines
54 KiB
PHP

<?php
session_start();
include 'koneksi.php';
if (!isset($_SESSION['user_id'])) {
header('Location: login.php'); exit;
}
$role = $_SESSION['user_role'] ?? 'masyarakat';
$user_nama = $_SESSION['user_nama'] ?? '';
$words = explode(' ', trim($user_nama));
$initials = '';
foreach (array_slice($words, 0, 2) as $w) $initials .= strtoupper(substr($w, 0, 1));
if (!$initials) $initials = 'U';
?>
<!DOCTYPE html>
<html lang="id" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* ===== RESET ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Plus Jakarta Sans', sans-serif; background: #f8fafc; overflow: hidden; }
/* ===== THEME VARIABLES ===== */
:root {
--nav-bg: #0f172a;
--nav-border: rgba(255,255,255,0.07);
--nav-text: #94a3b8;
--nav-text-hover: #f1f5f9;
--nav-brand: #ffffff;
--nav-surface: rgba(255,255,255,0.06);
--nav-surface-hover: rgba(255,255,255,0.10);
--dm-btn-bg: rgba(255,255,255,0.08);
--dm-btn-hover: rgba(255,255,255,0.13);
--dm-icon-color: #fbbf24;
}
[data-theme="light"] {
--nav-bg: #ffffff;
--nav-border: rgba(0,0,0,0.08);
--nav-text: #64748b;
--nav-text-hover: #0f172a;
--nav-brand: #0f172a;
--nav-surface: rgba(0,0,0,0.04);
--nav-surface-hover: rgba(0,0,0,0.07);
--dm-btn-bg: rgba(0,0,0,0.05);
--dm-btn-hover: rgba(0,0,0,0.09);
--dm-icon-color: #7c3aed;
}
/* ===== TOPBAR ===== */
#topnav {
position: fixed; top: 0; left: 0; right: 0; z-index: 10000;
background: var(--nav-bg);
border-bottom: 1px solid var(--nav-border);
height: 52px; padding: 0 20px;
display: flex; align-items: center; justify-content: space-between;
transition: background 0.3s, border-color 0.3s;
}
.nav-left { display: flex; align-items: center; gap: 14px; }
.nav-right { display: flex; align-items: center; gap: 3px; }
.nav-brand {
display: flex; align-items: center; gap: 9px;
font-size: 14px; font-weight: 800;
color: var(--nav-brand); text-decoration: none; white-space: nowrap;
}
.brand-icon {
width: 30px; height: 30px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
border-radius: 8px; display: flex; align-items: center;
justify-content: center; font-size: 13px; color: white; flex-shrink: 0;
}
.nav-divider { width: 1px; height: 20px; background: var(--nav-border); margin: 0 7px; flex-shrink: 0; }
.nav-role {
padding: 3px 10px; border-radius: 20px;
font-size: 10px; font-weight: 800; letter-spacing: 0.8px;
text-transform: uppercase; flex-shrink: 0;
}
.nav-role.walikota { background: rgba(251,191,36,0.15); color: #f59e0b; border: 1px solid rgba(251,191,36,0.25); }
.nav-role.admin { background: rgba(59,130,246,0.15); color: #60a5fa; border: 1px solid rgba(59,130,246,0.25); }
.nav-role.masyarakat { background: rgba(34,197,94,0.15); color: #4ade80; border: 1px solid rgba(34,197,94,0.25); }
[data-theme="light"] .nav-role.walikota { background:rgba(217,119,6,0.10); color:#d97706; border-color:rgba(217,119,6,0.2); }
[data-theme="light"] .nav-role.admin { background:rgba(37,99,235,0.08); color:#2563eb; border-color:rgba(37,99,235,0.2); }
[data-theme="light"] .nav-role.masyarakat { background:rgba(22,163,74,0.08); color:#16a34a; border-color:rgba(22,163,74,0.2); }
.nav-btn {
display: flex; align-items: center; gap: 6px; padding: 6px 11px; border-radius: 8px;
font-size: 12px; font-weight: 600; color: var(--nav-text); background: transparent;
border: none; cursor: pointer; text-decoration: none;
transition: background 0.15s, color 0.15s; white-space: nowrap;
}
.nav-btn i { font-size: 13px; }
.nav-btn:hover { background: var(--nav-surface-hover); color: var(--nav-text-hover); }
.nav-btn.laporan { color: #f59e0b; }
.nav-btn.laporan:hover { background: rgba(251,191,36,0.12); color: #fbbf24; }
.nav-btn.bantuan { color: #4ade80; }
.nav-btn.bantuan:hover { background: rgba(34,197,94,0.12); }
[data-theme="light"] .nav-btn.laporan { color: #d97706; }
[data-theme="light"] .nav-btn.bantuan { color: #16a34a; }
.notif-wrap { position: relative; display: inline-flex; }
.notif-dot {
position: absolute; top: 1px; right: 1px; width: 7px; height: 7px;
border-radius: 50%; background: #f59e0b;
box-shadow: 0 0 0 2px var(--nav-bg); animation: ping-dot 2s infinite;
pointer-events: none;
}
@keyframes ping-dot { 0%,100%{opacity:1} 50%{opacity:0.35} }
.user-chip {
display: flex; align-items: center; gap: 7px;
padding: 4px 10px 4px 4px;
background: var(--nav-surface); border: 1px solid var(--nav-border);
border-radius: 30px; cursor: default; flex-shrink: 0;
}
.user-avatar {
width: 26px; height: 26px; border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
display: flex; align-items: center; justify-content: center;
font-size: 9px; font-weight: 800; color: white; flex-shrink: 0;
}
.user-name { font-size: 12px; font-weight: 600; color: var(--nav-text-hover); white-space: nowrap; }
.nav-link-logout {
display: flex; align-items: center; gap: 6px; padding: 6px 11px;
border-radius: 8px; font-size: 12px; font-weight: 600; color: #f87171;
text-decoration: none; transition: background 0.15s; white-space: nowrap;
}
.nav-link-logout:hover { background: rgba(248,113,113,0.12); }
[data-theme="light"] .nav-link-logout { color: #dc2626; }
[data-theme="light"] .nav-link-logout:hover { background: rgba(220,38,38,0.08); }
.dm-btn {
width: 36px; height: 36px; border-radius: 10px;
background: var(--dm-btn-bg); border: 1px solid var(--nav-border);
display: flex; align-items: center; justify-content: center;
cursor: pointer; flex-shrink: 0; margin: 0 4px;
transition: background 0.2s, transform 0.15s;
}
.dm-btn:hover { background: var(--dm-btn-hover); }
.dm-btn:active { transform: scale(0.90); }
.dm-btn i { font-size: 14px; color: var(--dm-icon-color); transition: color 0.3s; }
/* ===== MAP ===== */
#map {
height: calc(100vh - 52px); width: 100%;
margin-top: 52px; position: relative; z-index: 1;
}
.leaflet-control-layers { display: none !important; }
.leaflet-popup { pointer-events: auto !important; }
/* ===== CUSTOM MARKER ===== */
.custom-div-icon { background: transparent; border: none; }
.pin-container {
position: relative; width: 32px; height: 42px;
filter: drop-shadow(0px 4px 6px rgba(15,23,42,0.35));
transition: transform 0.2s cubic-bezier(0.4,0,0.2,1);
}
.pin-container:hover { transform: scale(1.2) translateY(-4px); z-index: 9999 !important; }
.pin-icon-wrapper {
position: absolute; top: 0; left: 0; width: 32px; height: 32px;
display: flex; align-items: center; justify-content: center;
color: white; font-size: 13px; z-index: 2;
}
/* ===== POPUP ===== */
.leaflet-popup-content-wrapper { border-radius: 16px; box-shadow: 0 10px 25px -5px rgba(0,0,0,0.15); padding: 4px; }
.leaflet-popup-content { margin: 12px 14px; min-width: 260px; }
.form-title {
display: flex; align-items: center; gap: 8px;
font-weight: 700; font-size: 14px; color: #0f172a;
margin-bottom: 12px; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px;
}
.form-title i { color: #3b82f6; font-size: 15px; }
.info-box {
background: #f8fafc; padding: 12px; border-radius: 10px;
font-size: 12px; line-height: 1.8; color: #475569; border: 1px solid #edf2f7;
}
.info-box strong { color: #0f172a; font-weight: 600; }
.action-group { display: flex; gap: 6px; margin-top: 12px; flex-wrap: wrap; }
.btn-action {
text-align: center; padding: 8px; border-radius: 8px; font-size: 11px;
text-decoration: none; font-weight: 600; cursor: pointer;
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
transition: all 0.2s; width: 100%; flex: 1; min-width: 65px; border: none;
}
.btn-edit { background: #eff6ff; color: #2563eb; }
.btn-edit:hover { background: #dbeafe; }
.btn-delete { background: #fef2f2; color: #dc2626; }
.btn-delete:hover { background: #fee2e2; }
.btn-submit {
width: 100%; padding: 10px; background: #0f172a; color: white;
border: none; border-radius: 8px; cursor: pointer; margin-top: 8px;
font-size: 12px; font-weight: 600;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
#form-container input,
#form-container select,
#form-container textarea {
width: 100%; padding: 8px 10px; border: 1px solid #cbd5e1; border-radius: 8px;
margin-bottom: 8px; font-size: 12px; font-family: inherit; background: #fff; color: #0f172a;
}
#form-container label { font-size: 11px; color: #475569; font-weight: 600; display: block; margin-bottom: 4px; }
/* ===== LAYER PANEL ===== */
#layer-panel {
position: fixed; top: 72px; right: 20px; z-index: 9999; width: 308px;
font-family: 'Plus Jakarta Sans', sans-serif;
}
#layer-toggle-btn {
width: 100%; background: #0f172a; color: #f8fafc; border: none;
border-radius: 14px; padding: 13px 18px; font-size: 13px; font-weight: 700;
cursor: pointer; display: flex; align-items: center; justify-content: space-between;
box-shadow: 0 8px 24px -4px rgba(15,23,42,0.35);
transition: box-shadow 0.25s, transform 0.2s; user-select: none;
}
#layer-toggle-btn:hover { box-shadow: 0 12px 30px -4px rgba(15,23,42,0.45); transform: translateY(-1px); }
#layer-toggle-btn:active { transform: scale(0.98); }
.ltb-left { display: flex; align-items: center; gap: 10px; }
.ltb-dot {
width: 8px; height: 8px; border-radius: 50%; background: #22c55e;
box-shadow: 0 0 0 3px rgba(34,197,94,0.25); animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%,100% { box-shadow: 0 0 0 3px rgba(34,197,94,0.25); }
50% { box-shadow: 0 0 0 6px rgba(34,197,94,0.10); }
}
.ltb-chevron { font-size: 11px; opacity: 0.5; transition: transform 0.3s cubic-bezier(0.4,0,0.2,1); }
#layer-panel.open .ltb-chevron { transform: rotate(180deg); }
.ltb-count {
background: #3b82f6; color: white; font-size: 10px; font-weight: 800;
width: 20px; height: 20px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
}
#layer-body {
margin-top: 10px; background: #fff; border: 1px solid #e8edf5;
border-radius: 18px; overflow: hidden;
box-shadow: 0 20px 50px -10px rgba(15,23,42,0.18);
display: none;
}
#layer-body.open {
display: block;
animation: panelSlide 0.28s cubic-bezier(0.4,0,0.2,1);
}
@keyframes panelSlide {
from { opacity: 0; transform: translateY(-12px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.lp-header {
background: linear-gradient(135deg, #0f172a 0%, #1e3a5f 100%);
padding: 16px 18px 14px; display: flex; align-items: flex-end; justify-content: space-between;
}
.lp-header-title { font-size: 11px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase; color: rgba(255,255,255,0.5); }
.lp-header-sub { font-size: 14px; font-weight: 700; color: #fff; margin-top: 2px; }
.lp-header-badge {
background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15);
color: rgba(255,255,255,0.8); font-size: 10px; font-weight: 700;
padding: 4px 9px; border-radius: 20px;
}
.lp-scroll {
max-height: 430px; overflow-y: auto; padding: 14px 14px 10px;
scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent;
}
.lp-scroll::-webkit-scrollbar { width: 4px; }
.lp-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
.lp-section {
font-size: 10px; font-weight: 800; letter-spacing: 1.8px; text-transform: uppercase;
color: #94a3b8; margin: 14px 4px 8px;
display: flex; align-items: center; gap: 8px;
}
.lp-section::after { content: ''; flex: 1; height: 1px; background: #f1f5f9; }
.lp-section:first-child { margin-top: 2px; }
/* basemap cards */
.bm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 4px; }
.bm-card {
background: #f8fafc; border: 2px solid #e2e8f0; border-radius: 12px;
padding: 10px 12px; cursor: pointer; transition: all 0.2s;
display: flex; flex-direction: column; align-items: center; gap: 6px; position: relative;
}
.bm-card:hover { border-color: #93c5fd; background: #eff6ff; }
.bm-card.active { border-color: #3b82f6; background: #eff6ff; }
.bm-card.active::after {
content: '\f00c'; font-family: 'Font Awesome 6 Free'; font-weight: 900;
position: absolute; top: 6px; right: 8px; font-size: 9px; color: #3b82f6;
}
.bm-thumb { width: 100%; height: 44px; border-radius: 7px; overflow: hidden; }
.bm-thumb-osm { background: linear-gradient(135deg, #a8d5a2 0%, #f7f3e3 40%, #c8e6f5 100%); }
.bm-thumb-sat { background: linear-gradient(135deg, #2d4a1e 0%, #3d6b2d 40%, #1a3a0a 100%); }
.bm-thumb-osm-inner { width: 100%; height: 100%; position: relative; overflow: hidden; }
.bm-road { position: absolute; background: #fff; border-radius: 2px; opacity: 0.8; }
.bm-thumb-sat-inner {
width: 100%; height: 100%;
background:
repeating-linear-gradient(0deg, rgba(0,0,0,0.08) 0px, rgba(0,0,0,0.08) 1px, transparent 1px, transparent 12px),
repeating-linear-gradient(90deg, rgba(0,0,0,0.08) 0px, rgba(0,0,0,0.08) 1px, transparent 1px, transparent 14px);
}
.bm-label { font-size: 11px; font-weight: 700; color: #334155; }
/* Quick Add buttons */
.qa-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 7px; margin-bottom: 4px; }
.qa-btn {
border-radius: 11px; padding: 10px 6px 8px; cursor: pointer; border: 1.5px solid transparent;
display: flex; flex-direction: column; align-items: center; gap: 5px;
transition: all 0.18s; font-family: 'Plus Jakarta Sans', sans-serif;
}
.qa-btn:hover { transform: translateY(-2px); }
.qa-btn i { font-size: 15px; }
.qa-btn span { font-size: 10px; font-weight: 700; }
/* Layer rows */
.lp-row {
display: flex; align-items: center; gap: 10px; padding: 8px 8px;
border-radius: 11px; cursor: pointer; transition: background 0.15s, transform 0.15s;
margin-bottom: 3px; border: 1px solid transparent;
}
.lp-row:hover { background: #f8fafc; border-color: #e8edf5; }
.lp-row:active { transform: scale(0.99); }
.lp-icon {
width: 34px; height: 34px; border-radius: 10px;
display: flex; align-items: center; justify-content: center;
font-size: 13px; flex-shrink: 0; transition: transform 0.2s;
}
.lp-row:hover .lp-icon { transform: scale(1.1); }
.icon-spbu24 { background: #ecfdf5; color: #059669; }
.icon-spbunrm { background: #fef2f2; color: #ef4444; }
.icon-jalan { background: #fffbeb; color: #d97706; }
.icon-parsil { background: #f5f3ff; color: #7c3aed; }
.lp-info { flex: 1; min-width: 0; }
.lp-name { font-size: 12.5px; font-weight: 700; color: #1e293b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lp-desc { font-size: 10.5px; color: #94a3b8; margin-top: 1px; }
/* add btn per row */
.lp-add-btn {
width: 26px; height: 26px; border-radius: 7px; border: 1.5px solid transparent;
display: flex; align-items: center; justify-content: center;
font-size: 12px; cursor: pointer; flex-shrink: 0; transition: all 0.15s;
font-family: inherit;
}
/* toggle switch */
.lp-toggle { position: relative; width: 36px; height: 20px; flex-shrink: 0; }
.lp-toggle input { opacity: 0; width: 0; height: 0; }
.lp-slider {
position: absolute; inset: 0; background: #e2e8f0; border-radius: 20px;
cursor: pointer; transition: background 0.25s;
}
.lp-slider::before {
content: ''; position: absolute; width: 14px; height: 14px;
left: 3px; top: 3px; background: white; border-radius: 50%;
transition: transform 0.25s cubic-bezier(0.4,0,0.2,1);
box-shadow: 0 1px 4px rgba(0,0,0,0.2);
}
.lp-toggle input:checked + .lp-slider { background: #3b82f6; }
.lp-toggle input:checked + .lp-slider::before { transform: translateX(16px); }
/* footer */
.lp-footer {
padding: 11px 14px; border-top: 1px solid #f1f5f9;
display: flex; align-items: center; justify-content: space-between;
}
.lp-footer-txt { font-size: 11px; color: #94a3b8; }
.lp-footer-txt span { color: #3b82f6; font-weight: 700; }
.lp-btn-all {
font-size: 11px; font-weight: 700; color: #64748b;
background: #f1f5f9; border: none; border-radius: 7px;
padding: 5px 10px; cursor: pointer; transition: all 0.15s; font-family: inherit;
}
.lp-btn-all:hover { background: #e2e8f0; color: #0f172a; }
/* ===== DRAWER OVERLAY ===== */
#drawer-overlay {
position: fixed; inset: 0; z-index: 19998;
background: rgba(15,23,42,0.45);
backdrop-filter: blur(3px);
display: none; animation: fadeIn 0.2s ease;
}
#drawer-overlay.open { display: block; }
/* ===== DRAWER PANEL ===== */
#drawer-panel {
position: fixed; top: 52px; right: 0; bottom: 0; z-index: 19999;
width: 390px; background: #fff;
box-shadow: -20px 0 60px rgba(15,23,42,0.18);
display: flex; flex-direction: column;
transform: translateX(100%); transition: transform 0.3s cubic-bezier(0.4,0,0.2,1);
overflow: hidden;
}
#drawer-panel.open { transform: translateX(0); }
.drawer-header {
padding: 20px 22px 18px; flex-shrink: 0;
display: flex; align-items: flex-start; justify-content: space-between;
}
.drawer-header-text { flex: 1; }
.drawer-header-eyebrow {
font-size: 10px; font-weight: 800; letter-spacing: 2px; text-transform: uppercase;
color: rgba(255,255,255,0.65); margin-bottom: 3px;
}
.drawer-header-title { font-size: 17px; font-weight: 800; color: #fff; }
.drawer-header-icon { font-size: 26px; color: rgba(255,255,255,0.9); margin-right: 10px; }
.drawer-close-btn {
width: 34px; height: 34px; border-radius: 10px;
background: rgba(255,255,255,0.18); border: none; cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 14px; color: #fff; flex-shrink: 0; transition: background 0.15s;
}
.drawer-close-btn:hover { background: rgba(255,255,255,0.28); }
.drawer-body {
flex: 1; overflow-y: auto; padding: 20px 22px 24px;
scrollbar-width: thin; scrollbar-color: #cbd5e1 transparent;
}
.drawer-body::-webkit-scrollbar { width: 4px; }
.drawer-body::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
/* Form inside drawer */
.df-group { margin-bottom: 14px; }
.df-label {
font-size: 11px; font-weight: 700; color: #475569; margin-bottom: 5px; display: block;
}
.df-label .req { color: #ef4444; margin-left: 2px; }
.df-input, .df-select, .df-textarea {
width: 100%; padding: 9px 11px;
border: 1.5px solid #e2e8f0; border-radius: 9px;
font-size: 12.5px; font-family: 'Plus Jakarta Sans', sans-serif;
background: #fff; color: #0f172a; outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.df-input:focus, .df-select:focus, .df-textarea:focus {
border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.12);
}
.df-input.readonly { background: #f8fafc; color: #64748b; font-weight: 600; }
.df-textarea { resize: vertical; line-height: 1.6; }
.df-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
/* Geo coords textarea */
.df-geocoords {
width: 100%; padding: 9px 11px;
border: 1.5px solid #e2e8f0; border-radius: 9px;
font-size: 11px; font-family: monospace;
background: #f8fafc; color: #334155; outline: none;
transition: border-color 0.2s; resize: vertical; line-height: 1.5;
}
.df-geocoords:focus { border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.12); }
.df-hint {
margin-top: 5px; padding: 9px 11px; border-radius: 8px;
font-size: 11px; line-height: 1.6;
}
.df-hint-blue { background: #eff6ff; border: 1px solid #bfdbfe; color: #1e40af; }
.df-hint-yellow { background: #fffbeb; border: 1px solid #fde68a; color: #92400e; }
.df-divider { border: none; border-top: 1px solid #f1f5f9; margin: 18px 0; }
.df-submit-btn {
width: 100%; padding: 13px; border: none; border-radius: 11px;
font-size: 13px; font-weight: 700; cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 8px;
font-family: 'Plus Jakarta Sans', sans-serif;
transition: opacity 0.2s, transform 0.15s; color: #fff;
margin-top: 4px;
}
.df-submit-btn:hover { opacity: 0.9; transform: translateY(-1px); }
.df-submit-btn:active { transform: scale(0.98); }
.df-submit-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; }
/* success state */
.drawer-success {
flex: 1; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 12px; padding: 40px;
}
.drawer-success-icon {
width: 64px; height: 64px; border-radius: 20px;
display: flex; align-items: center; justify-content: center;
font-size: 26px; color: white;
}
.drawer-success-title { font-size: 18px; font-weight: 800; color: #0f172a; }
.drawer-success-desc { font-size: 13px; color: #64748b; }
/* toast */
#toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%) translateY(20px);
background: #0f172a; color: #fff; padding: 11px 22px; border-radius: 12px;
font-size: 13px; font-weight: 600; z-index: 99999;
box-shadow: 0 8px 30px rgba(0,0,0,0.3);
opacity: 0; transition: all 0.3s cubic-bezier(0.4,0,0.2,1); pointer-events: none;
display: flex; align-items: center; gap: 8px;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
</style>
</head>
<body>
<!-- ===== NAVBAR ===== -->
<div id="topnav">
<div class="nav-left">
<a href="#" class="nav-brand">
<div class="brand-icon"><i class="fa-solid fa-map-location-dot"></i></div>
WebGIS Pontianak
</a>
<span class="nav-role <?= $role ?>"><?= strtoupper($role) ?></span>
</div>
<div class="nav-right">
<?php if (in_array($role, ['admin','walikota'])): ?>
<div class="notif-wrap">
<a href="kelola_laporan.php" class="nav-btn laporan">
<i class="fa-solid fa-bell"></i> Kelola Laporan
</a>
<span class="notif-dot"></span>
</div>
<a href="histori_bantuan.php" class="nav-btn bantuan">
<i class="fa-solid fa-hand-holding-heart"></i> Histori Bantuan
</a>
<div class="nav-divider"></div>
<?php endif; ?>
<div class="user-chip">
<div class="user-avatar"><?= htmlspecialchars($initials) ?></div>
<span class="user-name"><?= htmlspecialchars($user_nama) ?></span>
</div>
<div class="nav-divider"></div>
<button class="dm-btn" id="dmBtn" onclick="toggleDarkMode()" title="Ganti tema">
<i class="fa-solid fa-moon" id="dmIcon"></i>
</button>
<a href="logout.php" class="nav-link-logout">
<i class="fa-solid fa-right-from-bracket"></i> Keluar
</a>
</div>
</div>
<!-- ===== LAYER PANEL ===== -->
<div id="layer-panel">
<button id="layer-toggle-btn" onclick="togglePanel()">
<div class="ltb-left">
<div class="ltb-dot"></div>
<span>Kontrol Lapisan</span>
<div class="ltb-count" id="active-count">4</div>
</div>
<i class="fa-solid fa-chevron-down ltb-chevron"></i>
</button>
<div id="layer-body">
<div class="lp-header">
<div>
<div class="lp-header-title">WebGIS</div>
<div class="lp-header-sub">Peta Infrastruktur</div>
</div>
<div class="lp-header-badge">Pontianak</div>
</div>
<div class="lp-scroll">
<!-- BASEMAP -->
<div class="lp-section">Peta Dasar</div>
<div class="bm-grid">
<div class="bm-card active" id="bm-osm" onclick="switchBasemapVisual('osm')">
<div class="bm-thumb bm-thumb-osm">
<div class="bm-thumb-osm-inner">
<div class="bm-road" style="width:70%;height:3px;top:35%;left:0;"></div>
<div class="bm-road" style="width:3px;height:70%;top:0;left:35%;"></div>
<div class="bm-road" style="width:50%;height:2px;top:60%;left:25%;opacity:0.5;"></div>
</div>
</div>
<div class="bm-label"><i class="fa-solid fa-map" style="margin-right:4px;color:#3b82f6;font-size:10px;"></i>Peta Jalan</div>
</div>
<div class="bm-card" id="bm-satellite" onclick="switchBasemapVisual('satellite')">
<div class="bm-thumb bm-thumb-sat"><div class="bm-thumb-sat-inner"></div></div>
<div class="bm-label"><i class="fa-solid fa-earth-asia" style="margin-right:4px;color:#6b7280;font-size:10px;"></i>Satelit</div>
</div>
</div>
<!-- QUICK ADD -->
<div class="lp-section">Tambah Data</div>
<div class="qa-grid">
<button class="qa-btn" style="background:rgba(5,150,105,0.1);border-color:rgba(5,150,105,0.25);" onclick="openDrawer('spbu')">
<i class="fa-solid fa-gas-pump" style="color:#059669;"></i>
<span style="color:#059669;">SPBU</span>
</button>
<button class="qa-btn" style="background:rgba(217,119,6,0.1);border-color:rgba(217,119,6,0.25);" onclick="openDrawer('jalan')">
<i class="fa-solid fa-road" style="color:#d97706;"></i>
<span style="color:#d97706;">Jalan</span>
</button>
<button class="qa-btn" style="background:rgba(124,58,237,0.1);border-color:rgba(124,58,237,0.25);" onclick="openDrawer('parsil')">
<i class="fa-solid fa-draw-polygon" style="color:#7c3aed;"></i>
<span style="color:#7c3aed;">Parsil</span>
</button>
</div>
<!-- LAYER TOGGLES -->
<div class="lp-section">Data Spasial</div>
<div class="lp-row" onclick="toggleLayerNew('spbu24')">
<div class="lp-icon icon-spbu24"><i class="fa-solid fa-gas-pump"></i></div>
<div class="lp-info">
<div class="lp-name">SPBU 24 Jam</div>
<div class="lp-desc">Pom bensin nonstop</div>
</div>
<button class="lp-add-btn" style="background:rgba(5,150,105,0.12);border-color:rgba(5,150,105,0.3);color:#059669;"
onclick="event.stopPropagation();openDrawer('spbu')" title="Tambah SPBU">
<i class="fa-solid fa-plus"></i>
</button>
<label class="lp-toggle" onclick="event.stopPropagation()">
<input type="checkbox" checked id="chk-spbu24" onchange="toggleLayerNew('spbu24')">
<span class="lp-slider"></span>
</label>
</div>
<div class="lp-row" onclick="toggleLayerNew('spbuNormal')">
<div class="lp-icon icon-spbunrm"><i class="fa-solid fa-gas-pump"></i></div>
<div class="lp-info">
<div class="lp-name">SPBU Reguler</div>
<div class="lp-desc">Pom bensin jam terbatas</div>
</div>
<button class="lp-add-btn" style="background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.3);color:#ef4444;"
onclick="event.stopPropagation();openDrawer('spbu')" title="Tambah SPBU">
<i class="fa-solid fa-plus"></i>
</button>
<label class="lp-toggle" onclick="event.stopPropagation()">
<input type="checkbox" checked id="chk-spbuNormal" onchange="toggleLayerNew('spbuNormal')">
<span class="lp-slider"></span>
</label>
</div>
<div class="lp-row" onclick="toggleLayerNew('jalan')">
<div class="lp-icon icon-jalan"><i class="fa-solid fa-road"></i></div>
<div class="lp-info">
<div class="lp-name">Manajemen Jalan</div>
<div class="lp-desc">Jaringan & status jalan</div>
</div>
<button class="lp-add-btn" style="background:rgba(217,119,6,0.12);border-color:rgba(217,119,6,0.3);color:#d97706;"
onclick="event.stopPropagation();openDrawer('jalan')" title="Tambah Jalan">
<i class="fa-solid fa-plus"></i>
</button>
<label class="lp-toggle" onclick="event.stopPropagation()">
<input type="checkbox" checked id="chk-jalan" onchange="toggleLayerNew('jalan')">
<span class="lp-slider"></span>
</label>
</div>
<div class="lp-row" onclick="toggleLayerNew('parsil')">
<div class="lp-icon icon-parsil"><i class="fa-solid fa-draw-polygon"></i></div>
<div class="lp-info">
<div class="lp-name">Parsil Kavling</div>
<div class="lp-desc">Batas & kepemilikan tanah</div>
</div>
<button class="lp-add-btn" style="background:rgba(124,58,237,0.12);border-color:rgba(124,58,237,0.3);color:#7c3aed;"
onclick="event.stopPropagation();openDrawer('parsil')" title="Tambah Parsil">
<i class="fa-solid fa-plus"></i>
</button>
<label class="lp-toggle" onclick="event.stopPropagation()">
<input type="checkbox" checked id="chk-parsil" onchange="toggleLayerNew('parsil')">
<span class="lp-slider"></span>
</label>
</div>
</div><!-- /lp-scroll -->
<div class="lp-footer">
<div class="lp-footer-txt"><span id="footer-count">4</span> lapisan aktif</div>
<button class="lp-btn-all" id="btn-all" onclick="toggleAllLayers()">Sembunyikan Semua</button>
</div>
</div><!-- /layer-body -->
</div><!-- /layer-panel -->
<!-- ===== MAP ===== -->
<div id="map"></div>
<!-- ===== DRAWER OVERLAY ===== -->
<div id="drawer-overlay" onclick="closeDrawer()"></div>
<!-- ===== DRAWER PANEL ===== -->
<div id="drawer-panel">
<div id="drawer-header" class="drawer-header">
<!-- filled by JS -->
</div>
<div id="drawer-body" class="drawer-body">
<!-- filled by JS -->
</div>
</div>
<!-- ===== TOAST ===== -->
<div id="toast">
<i class="fa-solid fa-circle-check" style="color:#22c55e;"></i>
<span id="toast-msg">Data berhasil disimpan!</span>
</div>
<!-- ===== SCRIPTS ===== -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<script>
/* ===================== DARK MODE ===================== */
(function(){
var saved = localStorage.getItem('webgis-theme') || 'dark';
document.documentElement.setAttribute('data-theme', saved);
updateDmIcon(saved);
})();
function toggleDarkMode() {
var html = document.documentElement;
var next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', next);
localStorage.setItem('webgis-theme', next);
updateDmIcon(next);
}
function updateDmIcon(theme) {
var icon = document.getElementById('dmIcon');
if (!icon) return;
icon.className = theme === 'dark' ? 'fa-solid fa-sun' : 'fa-solid fa-moon';
}
/* ===================== MAP INIT ===================== */
var osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 });
var satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { maxZoom: 19 });
var map = L.map('map', { layers: [osmLayer], zoomControl: true }).setView([-0.0263, 109.3425], 13);
function createCustomIcon(iconClass, pinColor) {
return L.divIcon({
html: '<div class="pin-container">' +
'<svg width="32" height="42" viewBox="0 0 32 42" xmlns="http://www.w3.org/2000/svg">' +
'<path d="M16 0C7.16 0 0 7.16 0 16c0 10.11 14.85 24.71 15.49 25.34.28.28.74.28 1.02 0C17.15 40.71 32 26.11 32 16 32 7.16 24.84 0 16 0z" fill="' + pinColor + '" stroke="#ffffff" stroke-width="2"/>' +
'</svg>' +
'<div class="pin-icon-wrapper"><i class="' + iconClass + '"></i></div>' +
'</div>',
className: 'custom-div-icon', iconSize: [32,42], iconAnchor: [16,42], popupAnchor: [0,-40]
});
}
var spbu24Group = L.layerGroup().addTo(map);
var spbuNormalGroup = L.layerGroup().addTo(map);
var jalanGroup = L.layerGroup().addTo(map);
var parsilGroup = L.layerGroup().addTo(map);
var layerMap = {
spbu24: spbu24Group, spbuNormal: spbuNormalGroup,
jalan: jalanGroup, parsil: parsilGroup
};
var allHidden = false;
/* ===================== LAYER PANEL ===================== */
function togglePanel() {
var panel = document.getElementById('layer-panel');
var body = document.getElementById('layer-body');
panel.classList.toggle('open');
body.classList.toggle('open');
}
function toggleLayerNew(name) {
var layer = layerMap[name];
var chk = document.getElementById('chk-' + name);
if (map.hasLayer(layer)) { map.removeLayer(layer); if (chk) chk.checked = false; }
else { map.addLayer(layer); if (chk) chk.checked = true; }
updateLayerCount();
}
function updateLayerCount() {
var count = Object.keys(layerMap).filter(function(n){ return map.hasLayer(layerMap[n]); }).length;
document.getElementById('active-count').textContent = count;
document.getElementById('footer-count').textContent = count;
document.getElementById('btn-all').textContent = count === 0 ? 'Tampilkan Semua' : 'Sembunyikan Semua';
allHidden = (count === 0);
}
function toggleAllLayers() {
allHidden = !allHidden;
Object.keys(layerMap).forEach(function(n) {
var chk = document.getElementById('chk-' + n);
if (allHidden) { map.removeLayer(layerMap[n]); if(chk) chk.checked = false; }
else { map.addLayer(layerMap[n]); if(chk) chk.checked = true; }
});
updateLayerCount();
}
function switchBasemap(type) {
if (type === 'osm') { map.removeLayer(satelliteLayer); map.addLayer(osmLayer); }
else { map.removeLayer(osmLayer); map.addLayer(satelliteLayer); }
}
function switchBasemapVisual(type) {
switchBasemap(type);
document.getElementById('bm-osm').classList.toggle('active', type === 'osm');
document.getElementById('bm-satellite').classList.toggle('active', type === 'satellite');
}
document.addEventListener('click', function(e) {
var panel = document.getElementById('layer-panel');
if (!panel.contains(e.target)) {
panel.classList.remove('open');
document.getElementById('layer-body').classList.remove('open');
}
});
/* ===================== TOAST ===================== */
function showToast(msg) {
var el = document.getElementById('toast');
document.getElementById('toast-msg').textContent = msg || 'Data berhasil disimpan!';
el.classList.add('show');
setTimeout(function(){ el.classList.remove('show'); }, 3200);
}
/* ===================== DRAWER ===================== */
var drawerConfigs = {
spbu: {
title: 'Tambah SPBU',
icon: 'fa-solid fa-gas-pump',
color: '#059669',
action: 'simpan_spbu.php'
},
jalan: {
title: 'Tambah Data Jalan',
icon: 'fa-solid fa-road',
color: '#d97706',
action: 'simpan_jalan.php',
isGeo: 'polyline'
},
parsil: {
title: 'Tambah Parsil Kavling',
icon: 'fa-solid fa-draw-polygon',
color: '#7c3aed',
action: 'simpan_parsil.php',
isGeo: 'polygon'
}
};
var currentDrawerKey = null;
var lastClickedLatLng = null;
map.on('click', function(e) {
lastClickedLatLng = e.latlng;
if (!document.getElementById('drawer-panel').classList.contains('open')) {
renderSpbuForm(e.latlng.lat, e.latlng.lng);
} else {
var latEl = document.getElementById('df-latitude');
var lngEl = document.getElementById('df-longitude');
if (latEl) latEl.value = e.latlng.lat.toFixed(6);
if (lngEl) lngEl.value = e.latlng.lng.toFixed(6);
showToast('Koordinat diperbarui dari klik peta');
}
});
function openDrawer(key, prefillLat, prefillLng) {
currentDrawerKey = key;
var cfg = drawerConfigs[key];
if (!cfg) return;
document.getElementById('layer-panel').classList.remove('open');
document.getElementById('layer-body').classList.remove('open');
document.getElementById('drawer-header').style.background =
'linear-gradient(135deg, ' + cfg.color + ' 0%, ' + cfg.color + 'bb 100%)';
document.getElementById('drawer-header').innerHTML =
'<div style="display:flex;align-items:center;gap:12px;flex:1;">' +
'<div style="width:42px;height:42px;border-radius:13px;background:rgba(255,255,255,0.2);' +
'display:flex;align-items:center;justify-content:center;">' +
'<i class="' + cfg.icon + '" style="font-size:18px;color:#fff;"></i>' +
'</div>' +
'<div>' +
'<div class="drawer-header-eyebrow">Tambah Data Baru</div>' +
'<div class="drawer-header-title">' + cfg.title + '</div>' +
'</div>' +
'</div>' +
'<button class="drawer-close-btn" onclick="closeDrawer()">' +
'<i class="fa-solid fa-xmark"></i>' +
'</button>';
var lat = prefillLat || (lastClickedLatLng ? lastClickedLatLng.lat.toFixed(6) : '');
var lng = prefillLng || (lastClickedLatLng ? lastClickedLatLng.lng.toFixed(6) : '');
document.getElementById('drawer-body').innerHTML = buildDrawerForm(key, cfg, lat, lng);
document.getElementById('drawer-overlay').classList.add('open');
document.getElementById('drawer-panel').classList.add('open');
}
function closeDrawer() {
document.getElementById('drawer-overlay').classList.remove('open');
document.getElementById('drawer-panel').classList.remove('open');
currentDrawerKey = null;
}
function buildDrawerForm(key, cfg, lat, lng) {
var html = '<form id="drawer-form" onsubmit="submitDrawerForm(event)">';
if (key === 'spbu') {
html += field('Nama SPBU', 'nama_spbu', 'text', true, 'Contoh: SPBU Jl. Ahmad Yani');
html += field('Nomor WhatsApp', 'no_whatsapp', 'text', true, '08xxxxxxxxxx');
html += selectField('Operasional', 'status_24jam', true, [
{val:'Ya', label:'24 Jam - Nonstop'},
{val:'Tidak', label:'Reguler - Jam Terbatas'}
]);
html += coordHint();
html += coordFields(lat, lng);
}
else if (key === 'jalan') {
html += field('Nama Jalan', 'nama_jalan', 'text', true, 'Contoh: Jl. Ahmad Yani');
html += selectField('Status Jalan', 'status_jalan', true, [
{val:'Jalan Nasional', label:'Jalan Nasional'},
{val:'Jalan Provinsi', label:'Jalan Provinsi'},
{val:'Jalan Kabupaten', label:'Jalan Kabupaten'},
]);
html += field('Panjang Jalan (meter)', 'panjang_jalan', 'number', false, 'Opsional');
html += '<hr class="df-divider">';
html += '<div class="df-group">';
html += '<label class="df-label"><i class="fa-solid fa-route" style="margin-right:5px;color:#d97706;"></i>Koordinat Polyline <span class="req">*</span></label>';
html += '<textarea class="df-geocoords" name="geojson_coords" rows="5" required ' +
'placeholder="[[109.3200,-0.0200],[109.3300,-0.0250],[109.3400,-0.0300]]"></textarea>';
html += '<div class="df-hint df-hint-yellow" style="margin-top:6px;">' +
'<i class="fa-solid fa-lightbulb" style="margin-right:5px;"></i>' +
'<strong>Tips:</strong> Gambar polyline di peta menggunakan tool draw (ikon garis di kiri peta), ' +
'lalu salin koordinat GeoJSON array dari popup yang muncul ke kolom di atas.</div>';
html += '</div>';
}
else if (key === 'parsil') {
html += field('Nama Pemilik Tanah', 'pemilik_tanah', 'text', true, 'Nama lengkap pemilik');
html += selectField('Status Kepemilikan', 'status_shm', true, [
{val:'SHM', label:'SHM - Sertifikat Hak Milik'},
{val:'HGB', label:'HGB - Hak Guna Bangunan'},
{val:'HGU', label:'HGU - Hak Guna Usaha'},
{val:'HP', label:'HP - Hak Pakai'},
]);
html += field('Luas Tanah (M²)', 'luas_tanah', 'number', false, 'Opsional');
html += '<hr class="df-divider">';
html += '<div class="df-group">';
html += '<label class="df-label"><i class="fa-solid fa-draw-polygon" style="margin-right:5px;color:#7c3aed;"></i>Koordinat Polygon <span class="req">*</span></label>';
html += '<textarea class="df-geocoords" name="geojson_coords" rows="5" required ' +
'placeholder="[[[109.320,-0.020],[109.325,-0.020],[109.325,-0.025],[109.320,-0.025],[109.320,-0.020]]]"></textarea>';
html += '<div class="df-hint df-hint-yellow" style="margin-top:6px;">' +
'<i class="fa-solid fa-lightbulb" style="margin-right:5px;"></i>' +
'<strong>Tips:</strong> Gambar polygon di peta menggunakan tool draw (ikon segi empat di kiri peta), ' +
'lalu salin koordinat GeoJSON dari popup ke kolom di atas. Titik pertama & terakhir harus sama.</div>';
html += '</div>';
}
html += '<input type="hidden" name="action_key" value="' + key + '">';
html += '<button type="submit" class="df-submit-btn" id="drawer-submit-btn" style="background:' + cfg.color + ';">' +
'<i class="fa-solid fa-floppy-disk"></i> Simpan Data' +
'</button>';
html += '</form>';
return html;
}
function field(label, name, type, required, placeholder) {
var req = required ? '<span class="req">*</span>' : '';
return '<div class="df-group">' +
'<label class="df-label">' + label + req + '</label>' +
'<input class="df-input" type="' + type + '" name="' + name + '" id="df-' + name + '" ' +
(required ? 'required ' : '') +
(placeholder ? 'placeholder="' + placeholder + '" ' : '') + '>' +
'</div>';
}
function selectField(label, name, required, options) {
var req = required ? '<span class="req">*</span>' : '';
var opts = '<option value="">Pilih...</option>';
options.forEach(function(o){ opts += '<option value="' + o.val + '">' + o.label + '</option>'; });
return '<div class="df-group">' +
'<label class="df-label">' + label + req + '</label>' +
'<select class="df-select" name="' + name + '" id="df-' + name + '" ' + (required ? 'required' : '') + '>' +
opts + '</select></div>';
}
function coordHint() {
return '<div class="df-hint df-hint-blue" style="margin-bottom:12px;">' +
'<i class="fa-solid fa-map-pin" style="margin-right:5px;"></i>' +
'Klik lokasi di peta untuk mengisi koordinat secara otomatis, atau isi manual di bawah.' +
'</div>';
}
function coordFields(lat, lng) {
return '<div class="df-row2">' +
'<div class="df-group"><label class="df-label">Latitude <span class="req">*</span></label>' +
'<input class="df-input" type="text" name="latitude" id="df-latitude" required placeholder="-0.0263" value="' + (lat||'') + '"></div>' +
'<div class="df-group"><label class="df-label">Longitude <span class="req">*</span></label>' +
'<input class="df-input" type="text" name="longitude" id="df-longitude" required placeholder="109.3425" value="' + (lng||'') + '"></div>' +
'</div>';
}
function submitDrawerForm(e) {
e.preventDefault();
var cfg = drawerConfigs[currentDrawerKey];
if (!cfg) return;
var btn = document.getElementById('drawer-submit-btn');
btn.disabled = true;
btn.innerHTML = '<i class="fa-solid fa-circle-notch fa-spin"></i> Menyimpan...';
var formData = new FormData(document.getElementById('drawer-form'));
fetch(cfg.action, { method: 'POST', body: formData })
.then(function(res) { return res.text(); })
.then(function() {
document.getElementById('drawer-body').innerHTML =
'<div class="drawer-success">' +
'<div class="drawer-success-icon" style="background:' + cfg.color + ';">' +
'<i class="fa-solid fa-check" style="font-size:26px;"></i></div>' +
'<div class="drawer-success-title">Data Tersimpan!</div>' +
'<div class="drawer-success-desc">Data berhasil ditambahkan ke peta.</div>' +
'<button onclick="closeDrawer();location.reload();" style="margin-top:16px;padding:10px 24px;' +
'background:' + cfg.color + ';color:#fff;border:none;border-radius:10px;' +
'font-size:13px;font-weight:700;cursor:pointer;font-family:inherit;">' +
'<i class="fa-solid fa-rotate" style="margin-right:6px;"></i>Muat Ulang Peta</button>' +
'</div>';
showToast('Data berhasil disimpan!');
})
.catch(function() {
btn.disabled = false;
btn.innerHTML = '<i class="fa-solid fa-floppy-disk"></i> Simpan Data';
showToast('Gagal menyimpan. Coba lagi.');
document.getElementById('toast').style.background = '#dc2626';
});
}
/* ===================== LEAFLET DRAW ===================== */
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
var drawControl = new L.Control.Draw({
position: 'topleft',
draw: {
polyline: { shapeOptions: { color: '#f59e0b', weight: 5 } },
polygon: { shapeOptions: { color: '#8b5cf6', fillOpacity: 0.3 } },
marker: false, circle: false, circlemarker: false, rectangle: false
},
edit: { featureGroup: drawnItems, remove: true }
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function(e) {
var layer = e.layer, type = e.layerType;
drawnItems.addLayer(layer);
if (type === 'polyline') {
var coords = layer.getLatLngs(), panjang = 0;
for (var i = 0; i < coords.length - 1; i++) panjang += coords[i].distanceTo(coords[i+1]);
panjang = panjang.toFixed(2);
var geojsonStr = JSON.stringify(layer.toGeoJSON().geometry.coordinates);
layer.bindPopup(
'<div class="form-title"><i class="fa-solid fa-road"></i> Tambah Data Jalan</div>' +
'<div id="form-container"><form action="simpan_jalan.php" method="POST">' +
'<label>Nama Jalan</label><input type="text" name="nama_jalan" required placeholder="Contoh: Jl. Ahmad Yani">' +
'<label>Status Jalan</label>' +
'<select name="status_jalan" required>' +
'<option value="Jalan Nasional">Jalan Nasional</option>' +
'<option value="Jalan Provinsi">Jalan Provinsi</option>' +
'<option value="Jalan Kabupaten">Jalan Kabupaten</option>' +
'</select>' +
'<label>Panjang Jalan (Meter)</label>' +
'<input type="text" name="panjang_jalan" value="' + panjang + '" readonly style="background:#f1f5f9;font-weight:bold;">' +
'<input type="hidden" name="geojson_coords" value=\'' + geojsonStr + '\'>' +
'<button type="submit" class="btn-submit"><i class="fa-solid fa-floppy-disk"></i> Simpan Jalan</button>' +
'</form></div>', { minWidth: 250 }
).openPopup();
} else if (type === 'polygon') {
var geojsonForm = layer.toGeoJSON(), luas = turf.area(geojsonForm).toFixed(2);
var geojsonStr = JSON.stringify(geojsonForm.geometry.coordinates);
layer.bindPopup(
'<div class="form-title"><i class="fa-solid fa-draw-polygon"></i> Tambah Parsil Kavling</div>' +
'<div id="form-container"><form action="simpan_parsil.php" method="POST">' +
'<label>Nama Pemilik</label><input type="text" name="pemilik_tanah" required placeholder="Contoh: Heri">' +
'<label>Status Kepemilikan</label>' +
'<select name="status_shm" required>' +
'<option value="SHM">SHM - Sertifikat Hak Milik</option>' +
'<option value="HGB">HGB - Hak Guna Bangunan</option>' +
'<option value="HGU">HGU - Hak Guna Usaha</option>' +
'<option value="HP">HP - Hak Pakai</option>' +
'</select>' +
'<label>Luas Wilayah (M²)</label>' +
'<input type="text" name="luas_tanah" value="' + luas + '" readonly style="background:#f1f5f9;font-weight:bold;">' +
'<input type="hidden" name="geojson_coords" value=\'' + geojsonStr + '\'>' +
'<button type="submit" class="btn-submit"><i class="fa-solid fa-floppy-disk"></i> Simpan Parsil</button>' +
'</form></div>', { minWidth: 250 }
).openPopup();
}
});
/* ===================== KLIK PETA (popup spbu cepat) ===================== */
function renderSpbuForm(lat, lng) {
var html =
'<div class="form-title"><i class="fa-solid fa-gas-pump"></i> Tambah SPBU</div>' +
'<div id="form-container">' +
'<form action="simpan_spbu.php" method="POST">' +
'<label>Nama SPBU</label><input name="nama_spbu" required placeholder="Nama SPBU">' +
'<label>WhatsApp</label><input name="no_whatsapp" required placeholder="08xxxxxxxxxx">' +
'<label>Operasional</label>' +
'<select name="status_24jam"><option value="Ya">24 Jam</option><option value="Tidak">Reguler</option></select>' +
'<input type="hidden" name="latitude" value="' + lat + '">' +
'<input type="hidden" name="longitude" value="' + lng + '">' +
'<button class="btn-submit" type="submit"><i class="fa-solid fa-floppy-disk"></i> Simpan SPBU</button>' +
'</form></div>';
L.popup({ maxWidth: 340 }).setLatLng([lat, lng]).setContent(html).openOn(map);
}
</script>
<?php
function ambilKoordinatBersih($row) {
$lat = !empty($row['latitude']) ? (float)$row['latitude'] : 0;
$lng = !empty($row['longitude']) ? (float)$row['longitude'] : 0;
return [$lat, $lng];
}
// =========================================================================
// 1. DATA SPBU
// =========================================================================
$q = mysqli_query($conn, "SELECT * FROM spbu ORDER BY id DESC");
if ($q && mysqli_num_rows($q) > 0) {
while ($d = mysqli_fetch_assoc($q)) {
list($lat, $lng) = ambilKoordinatBersih($d);
if ($lat == 0 || $lng == 0) continue;
$is24 = ($d['status_24jam'] ?? 'Tidak') == 'Ya';
$status = $is24 ? '24 Jam' : 'Reguler';
$hexColor = $is24 ? '#10b981' : '#ef4444';
$groupName = $is24 ? 'spbu24Group' : 'spbuNormalGroup';
$nama_spbu = htmlspecialchars($d['nama_spbu'] ?? 'SPBU', ENT_QUOTES);
$no_wa = htmlspecialchars($d['no_whatsapp'] ?? '-', ENT_QUOTES);
$id = (int)$d['id'];
$popup = "<div class='form-title'><i class='fa-solid fa-gas-pump'></i> {$nama_spbu}</div>"
. "<div class='info-box'><strong>WhatsApp:</strong> {$no_wa}<br><strong>Operasional:</strong> {$status}</div>"
. "<div class='action-group'>"
. "<a href='edit.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
. "<a href='hapus.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus SPBU?\")'><i class='fa-solid fa-trash'></i> Hapus</a>"
. "</div>";
$popupJs = json_encode($popup);
echo "<script>L.marker([$lat,$lng],{icon:createCustomIcon('fa-solid fa-gas-pump','$hexColor')}).bindPopup($popupJs).addTo($groupName);</script>\n";
}
}
// =========================================================================
// 2. DATA JALAN (POLYLINE)
// =========================================================================
$q = mysqli_query($conn, "SELECT * FROM jalan ORDER BY id DESC");
if ($q && mysqli_num_rows($q) > 0) {
while ($d = mysqli_fetch_assoc($q)) {
if (empty($d['geojson'])) continue;
$status = $d['status_jalan'] ?? 'Jalan Kabupaten';
$warna = $status=='Jalan Nasional' ? '#ef4444' : ($status=='Jalan Provinsi' ? '#f59e0b' : '#3b82f6');
$nama_jalan = htmlspecialchars($d['nama_jalan'] ?? 'Jalan', ENT_QUOTES);
$id = (int)$d['id'];
$popup = "<div class='form-title'><i class='fa-solid fa-road'></i> Lintasan Jalan</div>"
. "<div class='info-box'><strong>Nama:</strong> {$nama_jalan}<br><strong>Status:</strong> {$status}</div>"
. "<div class='action-group'>"
. "<a href='edit_jalan.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
. "<a href='hapus_jalan.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Data Jalan?\")'><i class='fa-solid fa-trash'></i> Hapus</a></div>";
$popupJs = json_encode($popup);
$geojson = $d['geojson'];
echo "<script>(function(){var c={$geojson};var lc=c.map(function(x){return[x[1],x[0]];});L.polyline(lc,{color:'$warna',weight:5,opacity:0.85}).bindPopup($popupJs).addTo(jalanGroup);})();</script>\n";
}
}
// =========================================================================
// 3. DATA PARSIL KAVLING (POLYGON)
// =========================================================================
$q = mysqli_query($conn, "SELECT * FROM parsil ORDER BY id DESC");
if ($q && mysqli_num_rows($q) > 0) {
while ($d = mysqli_fetch_assoc($q)) {
if (empty($d['geojson'])) continue;
$status = $d['status_kepemilikan'] ?? ($d['status_shm'] ?? 'SHM');
$warna = $status=='SHM' ? '#10b981' : '#8b5cf6';
$pemilik = htmlspecialchars($d['nama_pemilik'] ?? ($d['pemilik_tanah'] ?? 'Tanpa Nama'), ENT_QUOTES);
$id = (int)$d['id'];
$popup = "<div class='form-title'><i class='fa-solid fa-draw-polygon'></i> Informasi Kavling</div>"
. "<div class='info-box'><strong>Pemilik:</strong> {$pemilik}<br><strong>Status:</strong> {$status}</div>"
. "<div class='action-group'>"
. "<a href='edit_parsil.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
. "<a href='hapus_parsil.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Data Parsil?\")'><i class='fa-solid fa-trash'></i> Hapus</a></div>";
$popupJs = json_encode($popup);
$geojson = $d['geojson'];
echo "<script>(function(){var r={$geojson};var lp=r[0].map(function(x){return[x[1],x[0]];});L.polygon(lp,{color:'$warna',fillColor:'$warna',fillOpacity:0.35}).bindPopup($popupJs).addTo(parsilGroup);})();</script>\n";
}
}
?>
</body>
</html>