1218 lines
58 KiB
PHP
1218 lines
58 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; }
|
|
.badge-zone {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 3px 8px; border-radius: 6px; font-size: 10px; font-weight: 700; margin-top: 6px;
|
|
}
|
|
.badge-in { background: #ecfdf5; color: #059669; }
|
|
.badge-out { background: #fdf2f8; color: #db2777; }
|
|
.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;
|
|
}
|
|
.choice-group {
|
|
display: flex; gap: 4px; margin-bottom: 12px;
|
|
background: #f1f5f9; padding: 3px; border-radius: 8px;
|
|
}
|
|
.btn-choice {
|
|
flex: 1; padding: 8px 4px; cursor: pointer; font-size: 11px;
|
|
border: none; border-radius: 6px; background: transparent;
|
|
color: #64748b; font-weight: 600;
|
|
display: flex; flex-direction: column; align-items: center; gap: 4px;
|
|
}
|
|
.btn-choice.active { background: white; color: #0f172a; box-shadow: 0 2px 4px rgba(0,0,0,0.06); }
|
|
#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-rumibadah { background: #ecfdf5; color: #10b981; }
|
|
.icon-miskin { background: #ede9fe; color: #8b5cf6; }
|
|
.icon-lapor { background: #fff1f2; color: #e11d48; }
|
|
|
|
.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; }
|
|
|
|
.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-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">3</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 Sosial & Warga</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(16,185,129,0.1);border-color:rgba(16,185,129,0.25);" onclick="openDrawer('masjid')">
|
|
<i class="fa-solid fa-place-of-worship" style="color:#10b981;"></i>
|
|
<span style="color:#10b981;">Rumah Ibadah</span>
|
|
</button>
|
|
<button class="qa-btn" style="background:rgba(139,92,246,0.1);border-color:rgba(139,92,246,0.25);" onclick="openDrawer('miskin')">
|
|
<i class="fa-solid fa-house-user" style="color:#8b5cf6;"></i>
|
|
<span style="color:#8b5cf6;">Warga</span>
|
|
</button>
|
|
<button class="qa-btn" style="background:rgba(225,29,72,0.1);border-color:rgba(225,29,72,0.25);" onclick="openDrawer('lapor')">
|
|
<i class="fa-solid fa-triangle-exclamation" style="color:#e11d48;"></i>
|
|
<span style="color:#e11d48;">Laporan</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- LAYER TOGGLES -->
|
|
<div class="lp-section">Data Spasial</div>
|
|
|
|
<div class="lp-row" onclick="toggleLayerNew('masjid')">
|
|
<div class="lp-icon icon-rumibadah"><i class="fa-solid fa-place-of-worship"></i></div>
|
|
<div class="lp-info">
|
|
<div class="lp-name">Rumah Ibadah</div>
|
|
<div class="lp-desc">Titik lokasi rumah ibadah</div>
|
|
</div>
|
|
<button class="lp-add-btn" style="background:rgba(16,185,129,0.12);border-color:rgba(16,185,129,0.3);color:#10b981;"
|
|
onclick="event.stopPropagation();openDrawer('masjid')" title="Tambah Rumah Ibadah">
|
|
<i class="fa-solid fa-plus"></i>
|
|
</button>
|
|
<label class="lp-toggle" onclick="event.stopPropagation()">
|
|
<input type="checkbox" checked id="chk-masjid" onchange="toggleLayerNew('masjid')">
|
|
<span class="lp-slider"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="lp-row" onclick="toggleLayerNew('miskin')">
|
|
<div class="lp-icon icon-miskin"><i class="fa-solid fa-house-user"></i></div>
|
|
<div class="lp-info">
|
|
<div class="lp-name">Warga Miskin</div>
|
|
<div class="lp-desc">Sebaran penduduk miskin</div>
|
|
</div>
|
|
<button class="lp-add-btn" style="background:rgba(139,92,246,0.12);border-color:rgba(139,92,246,0.3);color:#8b5cf6;"
|
|
onclick="event.stopPropagation();openDrawer('miskin')" title="Tambah Warga">
|
|
<i class="fa-solid fa-plus"></i>
|
|
</button>
|
|
<label class="lp-toggle" onclick="event.stopPropagation()">
|
|
<input type="checkbox" checked id="chk-miskin" onchange="toggleLayerNew('miskin')">
|
|
<span class="lp-slider"></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="lp-row" onclick="toggleLayerNew('lapor')">
|
|
<div class="lp-icon icon-lapor"><i class="fa-solid fa-bell"></i></div>
|
|
<div class="lp-info">
|
|
<div class="lp-name">Pengaduan Warga</div>
|
|
<div class="lp-desc">Laporan & aduan masyarakat</div>
|
|
</div>
|
|
<button class="lp-add-btn" style="background:rgba(225,29,72,0.12);border-color:rgba(225,29,72,0.3);color:#e11d48;"
|
|
onclick="event.stopPropagation();openDrawer('lapor')" title="Buat Laporan">
|
|
<i class="fa-solid fa-plus"></i>
|
|
</button>
|
|
<label class="lp-toggle" onclick="event.stopPropagation()">
|
|
<input type="checkbox" checked id="chk-lapor" onchange="toggleLayerNew('lapor')">
|
|
<span class="lp-slider"></span>
|
|
</label>
|
|
</div>
|
|
|
|
</div><!-- /lp-scroll -->
|
|
|
|
<div class="lp-footer">
|
|
<div class="lp-footer-txt"><span id="footer-count">3</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/@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 masjidGroup = L.layerGroup().addTo(map);
|
|
var miskinGroup = L.layerGroup().addTo(map);
|
|
var laporGroup = L.layerGroup().addTo(map);
|
|
|
|
var layerMap = {
|
|
masjid: masjidGroup,
|
|
miskin: miskinGroup,
|
|
lapor: laporGroup
|
|
};
|
|
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 = {
|
|
masjid: {
|
|
title: 'Tambah Rumah Ibadah',
|
|
icon: 'fa-solid fa-place-of-worship',
|
|
color: '#10b981',
|
|
action: 'simpan_rumah_ibadah.php'
|
|
},
|
|
miskin: {
|
|
title: 'Tambah Data Warga',
|
|
icon: 'fa-solid fa-house-user',
|
|
color: '#8b5cf6',
|
|
action: 'simpan_miskin.php'
|
|
},
|
|
lapor: {
|
|
title: 'Kirim Laporan Cepat',
|
|
icon: 'fa-solid fa-triangle-exclamation',
|
|
color: '#e11d48',
|
|
action: 'simpan_laporan_cepat.php'
|
|
}
|
|
};
|
|
|
|
var currentDrawerKey = null;
|
|
var lastClickedLatLng = null;
|
|
|
|
map.on('click', function(e) {
|
|
lastClickedLatLng = e.latlng;
|
|
if (!document.getElementById('drawer-panel').classList.contains('open')) {
|
|
renderForm('miskin', 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 === 'masjid') {
|
|
html += field('Nama Rumah Ibadah', 'nama_rumah_ibadah', 'text', true, 'Contoh: Nama Rumah Ibadah');
|
|
html += field('Nama PIC / Penanggungjawab', 'nama_pic', 'text', true, 'Nama penanggung jawab');
|
|
html += field('Radius Jangkauan (meter)', 'radius_meter', 'number', false, '500');
|
|
html += coordHint();
|
|
html += coordFields(lat, lng);
|
|
}
|
|
else if (key === 'miskin') {
|
|
html += field('Nama Kepala Keluarga', 'nama_kk', 'text', true, 'Nama lengkap KK');
|
|
html += field('Alamat Lengkap', 'alamat', 'text', true, 'Alamat tempat tinggal');
|
|
html += '<div class="df-row2">';
|
|
html += field('Jumlah Anggota', 'anggota_keluarga', 'number', true, '4');
|
|
html += field('Umur KK', 'umur', 'number', true, '35');
|
|
html += '</div>';
|
|
html += field('Tanggal Lahir KK', 'tanggal_lahir', 'date', false, '');
|
|
html += selectField('Pendidikan Terakhir', 'pendidikan_terakhir', true, [
|
|
{val:'Tidak Sekolah',label:'Tidak Sekolah'},
|
|
{val:'SD',label:'SD'},
|
|
{val:'SMP',label:'SMP'},
|
|
{val:'SMA/SMK',label:'SMA / SMK'},
|
|
{val:'D3',label:'D3'},
|
|
{val:'S1',label:'S1'},
|
|
]);
|
|
html += field('Riwayat Penyakit', 'riwayat_penyakit', 'text', false, 'Contoh: Hipertensi, Diabetes, -');
|
|
html += coordHint();
|
|
html += coordFields(lat, lng);
|
|
}
|
|
else if (key === 'lapor') {
|
|
html += field('Nama Pelapor', 'nama_pelapor', 'text', true, 'Nama lengkap');
|
|
html += field('Kontak WhatsApp', 'kontak_pelapor', 'text', true, '08xxxxxxxxxx');
|
|
html += selectField('Kategori Laporan', 'kategori', true, [
|
|
{val:'Jalan Rusak', label:'Jalan Rusak'},
|
|
{val:'Banjir', label:'Banjir'},
|
|
{val:'Sampah', label:'Sampah'},
|
|
{val:'Penerangan Jalan', label:'Penerangan Jalan'},
|
|
{val:'Lainnya', label:'Lainnya'},
|
|
]);
|
|
html += '<div class="df-group">';
|
|
html += '<label class="df-label">Deskripsi Laporan <span class="req">*</span></label>';
|
|
html += '<textarea class="df-textarea" name="deskripsi_laporan" rows="4" required ' +
|
|
'placeholder="Ceritakan masalah secara detail..."></textarea>';
|
|
html += '</div>';
|
|
html += coordHint();
|
|
html += coordFields(lat, lng);
|
|
}
|
|
|
|
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';
|
|
});
|
|
}
|
|
|
|
/* ===================== KLIK PETA (form popup) ===================== */
|
|
function updateRadiusData(event, form) {
|
|
event.preventDefault();
|
|
var formData = new FormData(form);
|
|
var targetUrl = formData.has('id_warga') ? 'update_radius_warga.php' : 'update_radius.php';
|
|
fetch(targetUrl, { method: 'POST', body: formData })
|
|
.then(function(res){ return res.text(); })
|
|
.then(function(){ alert('Radius spasial diperbarui!'); window.location.reload(); });
|
|
return false;
|
|
}
|
|
|
|
function renderForm(type, lat, lng) {
|
|
var html =
|
|
'<div class="form-title"><i class="fa-solid fa-map-pin"></i> Tambah Titik Atribut</div>' +
|
|
'<div class="choice-group">' +
|
|
'<button onclick="renderForm(\'masjid\',' + lat + ',' + lng + ')" class="btn-choice ' + (type==='masjid'?'active':'') + '"><i class="fa-solid fa-place-of-worship"></i>Rumah Ibadah</button>' +
|
|
'<button onclick="renderForm(\'miskin\',' + lat + ',' + lng + ')" class="btn-choice ' + (type==='miskin'?'active':'') + '"><i class="fa-solid fa-house-user"></i>Warga</button>' +
|
|
'<button onclick="renderForm(\'lapor\',' + lat + ',' + lng + ')" class="btn-choice ' + (type==='lapor'?'active':'') + '"><i class="fa-solid fa-triangle-exclamation"></i>Lapor</button>' +
|
|
'</div><div id="form-container">';
|
|
|
|
if (type === 'masjid') {
|
|
html +=
|
|
'<form action="simpan_rumah_ibadah.php" method="POST">' +
|
|
'<label>Nama Rumah Ibadah</label><input name="nama_rumah_ibadah" required placeholder="Nama rumah ibadah">' +
|
|
'<label>Nama PIC / Penanggungjawab</label><input name="nama_pic" required placeholder="Nama penanggungjawab">' +
|
|
'<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 Rumah Ibadah</button></form>';
|
|
} else if (type === 'miskin') {
|
|
html +=
|
|
'<form action="simpan_miskin.php" method="POST">' +
|
|
'<label>Nama Kepala Keluarga</label><input name="nama_kk" required>' +
|
|
'<label>Alamat</label><input name="alamat" required>' +
|
|
'<label>Anggota Keluarga</label><input type="number" name="anggota_keluarga" required min="1">' +
|
|
'<label>Umur KK</label><input type="number" name="umur" required min="1">' +
|
|
'<label>Tanggal Lahir KK</label><input type="date" name="tanggal_lahir">' +
|
|
'<label>Pendidikan Terakhir</label>' +
|
|
'<select name="pendidikan_terakhir">' +
|
|
'<option value="Tidak Sekolah">Tidak Sekolah</option>' +
|
|
'<option value="SD">SD</option>' +
|
|
'<option value="SMP">SMP</option>' +
|
|
'<option value="SMA/SMK">SMA/SMK</option>' +
|
|
'<option value="D3">D3</option>' +
|
|
'<option value="S1">S1</option>' +
|
|
'</select>' +
|
|
'<label>Riwayat Penyakit</label>' +
|
|
'<input name="riwayat_penyakit" placeholder="Contoh: Hipertensi, Diabetes, -">' +
|
|
'<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 Warga</button></form>';
|
|
} else if (type === 'lapor') {
|
|
html +=
|
|
'<form action="simpan_laporan_cepat.php" method="POST">' +
|
|
'<label>Nama Pelapor</label><input name="nama_pelapor" required>' +
|
|
'<label>Kontak (WhatsApp)</label><input name="kontak_pelapor" required placeholder="08xxxxxxxxxx">' +
|
|
'<label>Kategori Laporan</label>' +
|
|
'<select name="kategori" required>' +
|
|
'<option value="Jalan Rusak">Jalan Rusak</option>' +
|
|
'<option value="Banjir">Banjir</option>' +
|
|
'<option value="Sampah">Sampah</option>' +
|
|
'<option value="Penerangan">Penerangan Jalan</option>' +
|
|
'<option value="Lainnya">Lainnya</option>' +
|
|
'</select>' +
|
|
'<label>Deskripsi Laporan</label>' +
|
|
'<textarea name="deskripsi_laporan" rows="3" required placeholder="Ceritakan masalah secara detail..."></textarea>' +
|
|
'<input type="hidden" name="latitude" value="' + lat + '">' +
|
|
'<input type="hidden" name="longitude" value="' + lng + '">' +
|
|
'<button class="btn-submit" style="background:#e11d48;" type="submit">' +
|
|
'<i class="fa-solid fa-paper-plane"></i> Kirim Laporan</button></form>';
|
|
}
|
|
|
|
html += '</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 RUMAH IBADAH
|
|
// =========================================================================
|
|
$masjids = [];
|
|
$q = mysqli_query($conn, "SELECT * FROM masjid 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;
|
|
$rad = !empty($d['radius_meter']) ? (int)$d['radius_meter'] : 500;
|
|
$masjids[] = ['lat'=>$lat,'lng'=>$lng,'radius'=>$rad];
|
|
$nama_masjid = htmlspecialchars($d['nama_masjid'] ?? 'Rumah Ibadah', ENT_QUOTES);
|
|
$nama_pic = htmlspecialchars($d['nama_pic'] ?? '-', ENT_QUOTES);
|
|
$id = (int)$d['id'];
|
|
$popup = "<div class='form-title'><i class='fa-solid fa-place-of-worship'></i> {$nama_masjid}</div>"
|
|
. "<div class='info-box'><strong>PIC:</strong> {$nama_pic}<br><strong>Jangkauan:</strong> {$rad} Meter"
|
|
. "<hr style='margin:6px 0;border:0;border-top:1px solid #e2e8f0;'>"
|
|
. "<form onsubmit='return updateRadiusData(event,this)'><input type='hidden' name='id' value='{$id}'>"
|
|
. "<div style='display:flex;gap:4px;'><input type='number' name='radius_baru' value='{$rad}' style='width:80px;padding:4px;font-size:11px;' required>"
|
|
. "<button type='submit' style='background:#10b981;color:white;border:0;padding:4px 8px;border-radius:4px;cursor:pointer;'>Set</button></div></form></div>"
|
|
. "<div class='action-group'>"
|
|
. "<a href='edit_rumah_ibadah.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
|
|
. "<a href='hapus_rumah_ibadah.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Data Rumah Ibadah?\")'>Hapus</a></div>";
|
|
$popupJs = json_encode($popup);
|
|
echo "<script>
|
|
L.circle([$lat,$lng],{radius:$rad,color:'#10b981',weight:1.5,fillOpacity:0.1}).addTo(masjidGroup);
|
|
L.marker([$lat,$lng],{icon:createCustomIcon('fa-solid fa-place-of-worship','#10b981')}).bindPopup($popupJs).addTo(masjidGroup);
|
|
</script>\n";
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 2. DATA PENDUDUK MISKIN
|
|
// =========================================================================
|
|
$q = mysqli_query($conn, "SELECT * FROM penduduk_miskin 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;
|
|
$dalamRadius = false;
|
|
foreach ($masjids as $m) {
|
|
$R = 6371000; $dLat = deg2rad($lat-$m['lat']); $dLng = deg2rad($lng-$m['lng']);
|
|
$a = sin($dLat/2)*sin($dLat/2)+cos(deg2rad($m['lat']))*cos(deg2rad($lat))*sin($dLng/2)*sin($dLng/2);
|
|
if (($R*2*atan2(sqrt($a),sqrt(1-$a))) <= $m['radius']) { $dalamRadius = true; break; }
|
|
}
|
|
$hexColor = $dalamRadius ? '#6366f1' : '#db2777';
|
|
$zoneText = $dalamRadius
|
|
? "<span class='badge-zone badge-in'><i class='fa-solid fa-circle-check'></i> Terjangkau Radius</span>"
|
|
: "<span class='badge-zone badge-out'><i class='fa-solid fa-circle-xmark'></i> Luar Radius</span>";
|
|
$nama_kk = htmlspecialchars($d['nama_kk'] ?? 'Warga', ENT_QUOTES);
|
|
$alamat = htmlspecialchars($d['alamat'] ?? '-', ENT_QUOTES);
|
|
$penyakit = htmlspecialchars($d['riwayat_penyakit'] ?? '-', ENT_QUOTES);
|
|
$tgl_lahir = !empty($d['tanggal_lahir']) ? date('d M Y', strtotime($d['tanggal_lahir'])) : '-';
|
|
$rad_warga = !empty($d['radius_warga']) ? (int)$d['radius_warga'] : 300;
|
|
$id = (int)$d['id'];
|
|
$popup = "<div class='form-title'><i class='fa-solid fa-house-user'></i> {$nama_kk}</div>"
|
|
. "<div class='info-box'>"
|
|
. "<strong>Alamat:</strong> {$alamat}<br>"
|
|
. "<strong>Tgl Lahir:</strong> {$tgl_lahir}<br>"
|
|
. "<strong>Riwayat Penyakit:</strong> {$penyakit}<br>"
|
|
. $zoneText
|
|
. "<hr style='margin:6px 0;border:0;border-top:1px solid #e2e8f0;'>"
|
|
. "<form onsubmit='return updateRadiusData(event,this)'><input type='hidden' name='id_warga' value='{$id}'>"
|
|
. "<div style='display:flex;gap:4px;'><input type='number' name='radius_baru_warga' value='{$rad_warga}' style='width:80px;padding:4px;font-size:11px;' required>"
|
|
. "<button type='submit' style='background:#10b981;color:white;border:0;padding:4px 8px;border-radius:4px;cursor:pointer;'>Set</button></div></form></div>"
|
|
. "<div class='action-group'>"
|
|
. "<a href='edit_miskin.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
|
|
. "<a href='hapus_miskin.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Data?\")'>Hapus</a></div>"
|
|
. "<button onclick=\"map.closePopup();renderForm('lapor',{$lat},{$lng});\" class='btn-submit' style='background:#e11d48;margin-top:8px;'>"
|
|
. "<i class='fa-solid fa-triangle-exclamation'></i> Buat Laporan</button>";
|
|
$popupJs = json_encode($popup);
|
|
echo "<script>
|
|
L.circle([$lat,$lng],{radius:$rad_warga,color:'$hexColor',weight:1,fillOpacity:0.04}).addTo(miskinGroup);
|
|
L.marker([$lat,$lng],{icon:createCustomIcon('fa-solid fa-house-user','$hexColor')}).bindPopup($popupJs).addTo(miskinGroup);
|
|
</script>\n";
|
|
}
|
|
}
|
|
|
|
// =========================================================================
|
|
// 3. DATA LAPORAN CEPAT
|
|
// =========================================================================
|
|
$q = mysqli_query($conn, "SELECT * FROM laporan_cepat ORDER BY tgl_lapor 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;
|
|
$nama_pelapor = htmlspecialchars($d['nama_pelapor'] ?? 'Anonim', ENT_QUOTES);
|
|
$kategori = htmlspecialchars($d['kategori'] ?? '-', ENT_QUOTES);
|
|
$deskripsi = htmlspecialchars(substr($d['deskripsi'] ?? '-', 0, 100), ENT_QUOTES);
|
|
$status = $d['status'] ?? 'Masuk';
|
|
$statusColor = ['Masuk'=>'#d97706','Diproses'=>'#2563eb','Selesai'=>'#16a34a'][$status] ?? '#64748b';
|
|
$tgl = date('d M Y', strtotime($d['tgl_lapor']));
|
|
$id = (int)$d['id'];
|
|
$popup = "<div class='form-title' style='color:#e11d48;'><i class='fa-solid fa-triangle-exclamation'></i> Laporan Cepat</div>"
|
|
. "<div class='info-box'>"
|
|
. "<strong>Pelapor:</strong> {$nama_pelapor}<br>"
|
|
. "<strong>Kategori:</strong> {$kategori}<br>"
|
|
. "<strong>Laporan:</strong> {$deskripsi}<br>"
|
|
. "<strong>Tanggal:</strong> {$tgl}<br>"
|
|
. "<span style='display:inline-flex;align-items:center;gap:4px;padding:2px 8px;background:#f8fafc;border-radius:6px;font-size:10px;font-weight:700;color:{$statusColor};margin-top:4px;'>"
|
|
. "<i class='fa-solid fa-circle' style='font-size:6px;'></i> {$status}</span>"
|
|
. "</div>"
|
|
. "<div class='action-group'>"
|
|
. "<a href='edit_pengaduan.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
|
|
. "<a href='hapus_pengaduan.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Laporan?\")'><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-triangle-exclamation','#e11d48')}).bindPopup($popupJs).addTo(laporGroup);</script>\n";
|
|
}
|
|
}
|
|
|
|
// Fallback tabel pengaduan_warga lama
|
|
$q2 = mysqli_query($conn, "SHOW TABLES LIKE 'pengaduan_warga'");
|
|
if ($q2 && mysqli_num_rows($q2) > 0) {
|
|
$q = mysqli_query($conn, "SELECT * FROM pengaduan_warga 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;
|
|
$nama_pelapor = htmlspecialchars($d['nama_pelapor'] ?? 'Anonim', ENT_QUOTES);
|
|
$aduan = htmlspecialchars($d['deskripsi_laporan'] ?? '-', ENT_QUOTES);
|
|
$id = (int)$d['id'];
|
|
$popup = "<div class='form-title' style='color:#e11d48;'><i class='fa-solid fa-triangle-exclamation'></i> Laporan Warga</div>"
|
|
. "<div class='info-box'><strong>Pelapor:</strong> {$nama_pelapor}<br><strong>Aduan:</strong> {$aduan}</div>"
|
|
. "<div class='action-group'>"
|
|
. "<a href='edit_pengaduan.php?id={$id}' class='btn-action btn-edit'><i class='fa-solid fa-pen-to-square'></i> Edit</a>"
|
|
. "<a href='hapus_pengaduan.php?id={$id}' class='btn-action btn-delete' onclick='return confirm(\"Hapus Laporan?\")'><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-triangle-exclamation','#e11d48')}).bindPopup($popupJs).addTo(laporGroup);</script>\n";
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
</body>
|
|
</html>
|