2379 lines
90 KiB
PHP
2379 lines
90 KiB
PHP
<?php
|
||
session_start();
|
||
require 'config.php';
|
||
|
||
$isAdmin = isset($_SESSION['admin']);
|
||
|
||
$poor = [];
|
||
$res = mysqli_query($conn, "SELECT id, name, nik, family_size, income, rt_rw, lat, lng FROM poor_residents");
|
||
if ($res) {
|
||
while ($row = mysqli_fetch_assoc($res)) {
|
||
$poor[] = $row;
|
||
}
|
||
}
|
||
|
||
$bufferRadius = 500;
|
||
$res = mysqli_query($conn, "SELECT setting_value FROM settings WHERE setting_key = 'buffer_radius'");
|
||
if ($res && $row = mysqli_fetch_assoc($res)) {
|
||
$bufferRadius = (int) $row['setting_value'];
|
||
}
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>SIG Kemiskinan & Jangkauan Bantuan - WebGIS</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--primary: #6366f1;
|
||
--primary-dark: #4f46e5;
|
||
--primary-light: #a5b4fc;
|
||
--success: #10b981;
|
||
--warning: #f59e0b;
|
||
--danger: #ef4444;
|
||
--info: #3b82f6;
|
||
--bg: #f1f5f9;
|
||
--sidebar-bg: #0f172a;
|
||
--sidebar-hover: #1e293b;
|
||
--sidebar-active: #6366f1;
|
||
--card: #ffffff;
|
||
--text: #1e293b;
|
||
--text-secondary: #64748b;
|
||
--border: #e2e8f0;
|
||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||
--shadow-lg: 0 10px 40px rgba(0, 0, 0, 0.12);
|
||
--radius: 12px;
|
||
--radius-sm: 8px;
|
||
--radius-xs: 6px;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
overflow: hidden;
|
||
height: 100vh;
|
||
}
|
||
|
||
.app-layout {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
width: 340px;
|
||
background: var(--sidebar-bg);
|
||
color: white;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
transition: width 0.3s, transform 0.3s;
|
||
z-index: 1000;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sidebar.collapsed {
|
||
width: 0;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px;
|
||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
}
|
||
|
||
.sidebar-header .logo {
|
||
width: 44px;
|
||
height: 44px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
backdrop-filter: blur(10px);
|
||
}
|
||
|
||
.sidebar-header .logo svg {
|
||
width: 24px;
|
||
height: 24px;
|
||
fill: white;
|
||
}
|
||
|
||
.sidebar-header h2 {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.sidebar-header small {
|
||
font-size: 11px;
|
||
opacity: 0.8;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
padding: 12px;
|
||
display: flex;
|
||
gap: 4px;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar {
|
||
height: 6px;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.sidebar-nav::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.35);
|
||
}
|
||
|
||
.nav-item {
|
||
flex: 1 0 auto;
|
||
padding: 10px 8px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
border-radius: var(--radius-sm);
|
||
transition: all 0.2s;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
border: none;
|
||
background: none;
|
||
font-family: inherit;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.nav-item:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
color: white;
|
||
}
|
||
|
||
.nav-item.active {
|
||
background: rgba(99, 102, 241, 0.3);
|
||
color: var(--primary-light);
|
||
}
|
||
|
||
.nav-item.hidden {
|
||
display: none;
|
||
}
|
||
|
||
.sidebar-content {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 0;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||
}
|
||
|
||
.sidebar-content::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.sidebar-content::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.sidebar-content::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 10px;
|
||
border: 2px solid transparent;
|
||
background-clip: content-box;
|
||
}
|
||
|
||
.sidebar-content::-webkit-scrollbar-thumb:hover {
|
||
background: rgba(255, 255, 255, 0.35);
|
||
background-clip: content-box;
|
||
}
|
||
|
||
.section {
|
||
display: none;
|
||
}
|
||
|
||
.section.active {
|
||
display: block;
|
||
animation: fadeSlideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes fadeSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.section-title {
|
||
padding: 16px 20px 12px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #64748b;
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
}
|
||
|
||
.stats-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
padding: 0 16px 12px;
|
||
}
|
||
|
||
.stat-mini {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-radius: var(--radius-sm);
|
||
padding: 14px 12px;
|
||
text-align: center;
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
transition: all 0.2s;
|
||
animation: fadeScale 0.5s ease backwards;
|
||
}
|
||
|
||
@keyframes fadeScale {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.stat-mini:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.stat-mini .stat-val {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
}
|
||
|
||
.stat-mini .stat-lbl {
|
||
font-size: 10px;
|
||
color: #94a3b8;
|
||
margin-top: 4px;
|
||
font-weight: 500;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.stat-mini.green .stat-val {
|
||
color: #34d399;
|
||
}
|
||
|
||
.stat-mini.red .stat-val {
|
||
color: #f87171;
|
||
}
|
||
|
||
.stat-mini.blue .stat-val {
|
||
color: #60a5fa;
|
||
}
|
||
|
||
.stat-mini.yellow .stat-val {
|
||
color: #fbbf24;
|
||
}
|
||
|
||
.analysis-mini {
|
||
margin: 0 16px 12px;
|
||
padding: 16px;
|
||
border-radius: var(--radius-sm);
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
|
||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.analysis-mini:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.analysis-mini h4 {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.analysis-mini p {
|
||
font-size: 10px;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.analysis-mini .a-val {
|
||
font-size: 26px;
|
||
font-weight: 800;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.dist-table {
|
||
width: calc(100% - 32px);
|
||
margin: 0 16px 16px;
|
||
border-collapse: collapse;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.dist-table td {
|
||
padding: 5px 8px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
color: #cbd5e1;
|
||
transition: background 0.2s;
|
||
}
|
||
|
||
.dist-table tr {
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.dist-table tr:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.dist-table .dist-name {
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.dist-table .dist-count {
|
||
text-align: right;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.dist-table .dist-pct {
|
||
text-align: right;
|
||
color: #94a3b8;
|
||
font-size: 10px;
|
||
width: 45px;
|
||
}
|
||
|
||
.dist-bar-bg {
|
||
display: inline-block;
|
||
width: 50px;
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-radius: 3px;
|
||
vertical-align: middle;
|
||
margin-left: 6px;
|
||
}
|
||
|
||
.dist-bar-fill {
|
||
display: block;
|
||
height: 100%;
|
||
border-radius: 3px;
|
||
transition: width 0.6s ease;
|
||
}
|
||
|
||
.legend-wrap {
|
||
padding: 8px 16px 16px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 8px;
|
||
font-size: 12px;
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.legend-dot {
|
||
width: 14px;
|
||
height: 14px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.legend-rect {
|
||
width: 24px;
|
||
height: 14px;
|
||
border-radius: 3px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.legend-line {
|
||
width: 24px;
|
||
height: 3px;
|
||
border-radius: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.layer-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 16px;
|
||
cursor: pointer;
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.layer-item:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
|
||
.layer-switch {
|
||
width: 38px;
|
||
height: 20px;
|
||
border-radius: 10px;
|
||
background: #334155;
|
||
position: relative;
|
||
transition: background 0.2s;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.layer-switch.on {
|
||
background: var(--success);
|
||
}
|
||
|
||
.layer-switch::after {
|
||
content: '';
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: white;
|
||
position: absolute;
|
||
top: 2px;
|
||
left: 2px;
|
||
transition: transform 0.2s;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.layer-switch.on::after {
|
||
transform: translateX(18px);
|
||
}
|
||
|
||
.layer-name {
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: #e2e8f0;
|
||
}
|
||
|
||
.layer-count {
|
||
font-size: 11px;
|
||
color: #64748b;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.buffer-ctrl {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 10px 16px;
|
||
margin: 0 12px 8px;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.buffer-ctrl label {
|
||
font-size: 12px;
|
||
color: #94a3b8;
|
||
flex: 1;
|
||
}
|
||
|
||
.buffer-ctrl input {
|
||
width: 64px;
|
||
padding: 5px 8px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
border-radius: 6px;
|
||
color: white;
|
||
font-size: 12px;
|
||
font-family: inherit;
|
||
text-align: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.buffer-ctrl input:focus {
|
||
border-color: var(--primary);
|
||
background: rgba(99, 102, 241, 0.1);
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
||
}
|
||
|
||
.buffer-ctrl span {
|
||
font-size: 11px;
|
||
color: #64748b;
|
||
}
|
||
|
||
.sidebar-form {
|
||
padding: 0 16px 16px;
|
||
}
|
||
|
||
.sidebar-form .fg {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.sidebar-form .fg label {
|
||
display: block;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
margin-bottom: 5px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.sidebar-form .fg input,
|
||
.sidebar-form .fg select {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: var(--radius-xs);
|
||
color: white;
|
||
font-size: 13px;
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.sidebar-form .fg input::placeholder {
|
||
color: #475569;
|
||
}
|
||
|
||
.sidebar-form .fg input:focus,
|
||
.sidebar-form .fg select:focus {
|
||
border-color: var(--primary);
|
||
background: rgba(99, 102, 241, 0.1);
|
||
outline: none;
|
||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.15);
|
||
}
|
||
|
||
.sidebar-form .fg select option {
|
||
background: #1e293b;
|
||
color: white;
|
||
}
|
||
|
||
.input-error {
|
||
border-color: #ef4444 !important;
|
||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15) !important;
|
||
}
|
||
|
||
.error-msg {
|
||
color: #f87171;
|
||
font-size: 10px;
|
||
margin-top: 3px;
|
||
display: none;
|
||
}
|
||
|
||
.error-msg.show {
|
||
display: block;
|
||
animation: fadeSlideIn 0.2s ease;
|
||
}
|
||
|
||
.coord-display {
|
||
padding: 10px 12px;
|
||
background: rgba(99, 102, 241, 0.1);
|
||
border: 1px dashed rgba(99, 102, 241, 0.3);
|
||
border-radius: var(--radius-xs);
|
||
font-size: 12px;
|
||
color: var(--primary-light);
|
||
font-family: 'Courier New', monospace;
|
||
text-align: center;
|
||
margin-bottom: 14px;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.coord-display.picked {
|
||
border-color: var(--success);
|
||
background: rgba(16, 185, 129, 0.1);
|
||
color: #34d399;
|
||
border-style: solid;
|
||
}
|
||
|
||
.btn-pick-location {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 2px dashed rgba(99, 102, 241, 0.4);
|
||
background: rgba(99, 102, 241, 0.08);
|
||
color: var(--primary-light);
|
||
border-radius: var(--radius-sm);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-family: inherit;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.btn-pick-location:hover {
|
||
background: rgba(99, 102, 241, 0.15);
|
||
border-color: var(--primary);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-pick-location.picking {
|
||
background: rgba(239, 68, 68, 0.15);
|
||
border-color: var(--danger);
|
||
color: #fca5a5;
|
||
animation: pulse-border 1.5s infinite;
|
||
}
|
||
|
||
@keyframes pulse-border {
|
||
|
||
0%,
|
||
100% {
|
||
border-color: var(--danger);
|
||
}
|
||
|
||
50% {
|
||
border-color: #f87171;
|
||
}
|
||
}
|
||
|
||
.btn-save-data {
|
||
width: 100%;
|
||
padding: 13px;
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
color: white;
|
||
border: none;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-family: inherit;
|
||
box-shadow: 0 4px 16px rgba(99, 102, 241, 0.3);
|
||
}
|
||
|
||
.btn-save-data:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 8px 25px rgba(99, 102, 241, 0.5);
|
||
}
|
||
|
||
.btn-save-data:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.btn-reset-form {
|
||
width: 100%;
|
||
padding: 10px;
|
||
background: transparent;
|
||
color: #64748b;
|
||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||
border-radius: var(--radius-sm);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
font-family: inherit;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.btn-reset-form:hover {
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: #94a3b8;
|
||
}
|
||
|
||
.data-list {
|
||
padding: 0 16px 16px;
|
||
}
|
||
|
||
.data-list-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border-radius: var(--radius-sm);
|
||
margin-bottom: 6px;
|
||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||
transition: all 0.2s;
|
||
animation: fadeSlideIn 0.3s ease backwards;
|
||
}
|
||
|
||
.data-list-item:hover {
|
||
background: rgba(255, 255, 255, 0.08);
|
||
transform: translateX(2px);
|
||
}
|
||
|
||
.data-list-icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.data-list-icon.poor {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
}
|
||
|
||
.data-list-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.data-list-name {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: #e2e8f0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.data-list-sub {
|
||
font-size: 11px;
|
||
color: #64748b;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.btn-full-table {
|
||
display: block;
|
||
width: calc(100% - 32px);
|
||
margin: 0 16px 16px;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.2));
|
||
border: 1px solid rgba(99, 102, 241, 0.3);
|
||
color: var(--primary-light);
|
||
border-radius: var(--radius-sm);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
text-align: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-full-table:hover {
|
||
background: rgba(99, 102, 241, 0.3);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.map-wrapper {
|
||
flex: 1;
|
||
position: relative;
|
||
}
|
||
|
||
#map {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.topbar {
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 999;
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
justify-content: center;
|
||
}
|
||
|
||
.topbar-btn {
|
||
padding: 8px 14px;
|
||
background: var(--card);
|
||
border: none;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow-lg);
|
||
transition: all 0.2s;
|
||
font-family: inherit;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.topbar-btn:hover {
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.topbar-btn.active {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.admin-controls {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 14px;
|
||
z-index: 999;
|
||
display: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.admin-controls.show {
|
||
display: flex;
|
||
}
|
||
|
||
.admin-badge {
|
||
padding: 6px 12px;
|
||
background: var(--success);
|
||
color: white;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
box-shadow: var(--shadow-lg);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
}
|
||
|
||
.admin-badge .dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, 0.7);
|
||
animation: pulse-dot 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot {
|
||
|
||
0%,
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
opacity: 0.4;
|
||
}
|
||
}
|
||
|
||
.logout-btn {
|
||
padding: 6px 12px;
|
||
background: var(--danger);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 20px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow-lg);
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
text-decoration: none;
|
||
display: inline-block;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: #dc2626;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.toggle-sidebar-btn {
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 14px;
|
||
z-index: 999;
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 12px;
|
||
background: var(--card);
|
||
border: none;
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow-lg);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.toggle-sidebar-btn:hover {
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.toggle-sidebar-btn svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
fill: var(--text-secondary);
|
||
}
|
||
|
||
.picking-banner {
|
||
position: absolute;
|
||
top: 70px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 1000;
|
||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||
color: white;
|
||
padding: 10px 20px;
|
||
border-radius: 24px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.4);
|
||
display: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
animation: bounceIn 0.3s ease;
|
||
}
|
||
|
||
.picking-banner.show {
|
||
display: flex;
|
||
}
|
||
|
||
@keyframes bounceIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(-50%) scale(0.9);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(-50%) scale(1);
|
||
}
|
||
}
|
||
|
||
.picking-banner .cancel-pick {
|
||
padding: 4px 10px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: none;
|
||
border-radius: 12px;
|
||
color: white;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.picking-banner .cancel-pick:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.info-panel {
|
||
position: absolute;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 999;
|
||
background: var(--card);
|
||
border-radius: var(--radius);
|
||
padding: 14px 22px;
|
||
box-shadow: var(--shadow-lg);
|
||
display: none;
|
||
max-width: 460px;
|
||
width: calc(100% - 32px);
|
||
}
|
||
|
||
.info-panel.show {
|
||
display: block;
|
||
animation: slideUp 0.3s ease;
|
||
}
|
||
|
||
.info-panel h4 {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 2px;
|
||
}
|
||
|
||
.info-panel p {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
@keyframes slideUp {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(-50%) translateY(10px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
}
|
||
|
||
.toast-container {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
z-index: 3000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toast {
|
||
padding: 12px 18px;
|
||
border-radius: var(--radius-sm);
|
||
color: white;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
box-shadow: var(--shadow-lg);
|
||
animation: slideIn 0.3s ease;
|
||
min-width: 260px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(40px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
|
||
.toast.success {
|
||
background: var(--success);
|
||
}
|
||
|
||
.toast.error {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.toast.info {
|
||
background: var(--info);
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
border-radius: var(--radius) !important;
|
||
box-shadow: var(--shadow-lg) !important;
|
||
}
|
||
|
||
.leaflet-popup-content {
|
||
margin: 12px 16px !important;
|
||
font-family: 'Inter', sans-serif !important;
|
||
font-size: 13px !important;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.popup-info {
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
margin-bottom: 3px;
|
||
}
|
||
|
||
.popup-status {
|
||
display: inline-block;
|
||
padding: 3px 10px;
|
||
border-radius: 12px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: white;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.popup-status.covered {
|
||
background: var(--success);
|
||
}
|
||
|
||
.popup-status.uncovered {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.popup-actions {
|
||
margin-top: 10px;
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn-popup {
|
||
padding: 5px 12px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
color: white;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-popup.danger {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.btn-popup.danger:hover {
|
||
background: #dc2626;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-popup.info {
|
||
background: var(--info);
|
||
}
|
||
|
||
.btn-popup.info:hover {
|
||
background: #2563eb;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.map-picking-mode {
|
||
cursor: crosshair !important;
|
||
}
|
||
|
||
.map-picking-mode .leaflet-interactive {
|
||
cursor: crosshair !important;
|
||
}
|
||
|
||
.map-picking-mode .leaflet-overlay-pane,
|
||
.map-picking-mode .leaflet-marker-pane {
|
||
pointer-events: none !important;
|
||
}
|
||
|
||
.modal-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 5000;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.modal-overlay.show {
|
||
display: flex;
|
||
animation: fadeIn 0.2s ease;
|
||
}
|
||
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.modal-dialog {
|
||
background: var(--sidebar-bg);
|
||
border-radius: var(--radius);
|
||
padding: 24px;
|
||
width: 90vw;
|
||
max-width: 800px;
|
||
max-height: 85vh;
|
||
overflow-y: auto;
|
||
color: white;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||
animation: modalSlideIn 0.3s ease;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||
}
|
||
|
||
.modal-dialog::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.modal-dialog::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.modal-dialog::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
@keyframes modalSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: scale(0.95) translateY(10px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
}
|
||
|
||
.modal-dialog h3 {
|
||
margin-bottom: 20px;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.modal-dialog .form-group {
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.modal-dialog label {
|
||
display: block;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: #94a3b8;
|
||
margin-bottom: 5px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.modal-dialog input,
|
||
.modal-dialog select {
|
||
width: 100%;
|
||
padding: 10px 12px;
|
||
background: rgba(255, 255, 255, 0.08);
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
border-radius: var(--radius-xs);
|
||
color: white;
|
||
font-size: 13px;
|
||
font-family: inherit;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.modal-dialog input:focus,
|
||
.modal-dialog select:focus {
|
||
border-color: var(--primary);
|
||
background: rgba(99, 102, 241, 0.1);
|
||
outline: none;
|
||
}
|
||
|
||
.modal-dialog select option {
|
||
background: #1e293b;
|
||
color: white;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.modal-actions button {
|
||
flex: 1;
|
||
padding: 10px;
|
||
border-radius: var(--radius-sm);
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
border: none;
|
||
font-size: 13px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.btn-save-modal {
|
||
background: var(--primary);
|
||
color: white;
|
||
}
|
||
|
||
.btn-save-modal:hover {
|
||
background: var(--primary-dark);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-cancel-modal {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.btn-cancel-modal:hover {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.table-responsive {
|
||
width: 100%;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
|
||
}
|
||
|
||
.table-responsive::-webkit-scrollbar {
|
||
height: 6px;
|
||
}
|
||
|
||
.table-responsive::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
.table-responsive::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border-radius: 10px;
|
||
}
|
||
|
||
.data-table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.data-table th,
|
||
.data-table td {
|
||
padding: 10px 12px;
|
||
text-align: left;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||
color: #cbd5e1;
|
||
}
|
||
|
||
.data-table th {
|
||
color: #94a3b8;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
font-size: 11px;
|
||
letter-spacing: 0.5px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--sidebar-bg);
|
||
z-index: 1;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.data-table th:hover {
|
||
color: white;
|
||
}
|
||
|
||
.data-table th .sort-icon {
|
||
margin-left: 4px;
|
||
font-size: 9px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.data-table th.sorted .sort-icon {
|
||
opacity: 1;
|
||
color: var(--primary-light);
|
||
}
|
||
|
||
.data-table tr {
|
||
transition: background 0.15s;
|
||
}
|
||
|
||
.data-table tr:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
|
||
.status-badge {
|
||
display: inline-block;
|
||
padding: 3px 8px;
|
||
border-radius: 12px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: white;
|
||
}
|
||
|
||
.status-badge.covered {
|
||
background: var(--success);
|
||
}
|
||
|
||
.status-badge.uncovered {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.btn-table-action {
|
||
padding: 4px 8px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
font-family: inherit;
|
||
color: white;
|
||
transition: all 0.2s;
|
||
margin-right: 3px;
|
||
}
|
||
|
||
.btn-table-action.edit {
|
||
background: var(--info);
|
||
}
|
||
|
||
.btn-table-action.edit:hover {
|
||
background: #2563eb;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.btn-table-action.delete {
|
||
background: var(--danger);
|
||
}
|
||
|
||
.btn-table-action.delete:hover {
|
||
background: #dc2626;
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.sidebar {
|
||
position: fixed;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 320px;
|
||
z-index: 2000;
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.sidebar.mobile-open {
|
||
transform: translateX(0);
|
||
width: 320px;
|
||
}
|
||
|
||
.mobile-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 1999;
|
||
background: rgba(0, 0, 0, 0.4);
|
||
display: none;
|
||
}
|
||
|
||
.mobile-overlay.show {
|
||
display: block;
|
||
}
|
||
|
||
.topbar {
|
||
top: 8px;
|
||
gap: 4px;
|
||
}
|
||
|
||
.topbar-btn {
|
||
padding: 6px 10px;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.stats-row {
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
.modal-dialog {
|
||
width: 95vw;
|
||
padding: 16px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.topbar-btn {
|
||
padding: 5px 8px;
|
||
font-size: 9px;
|
||
gap: 3px;
|
||
}
|
||
|
||
.topbar {
|
||
gap: 3px;
|
||
}
|
||
|
||
.sidebar-nav {
|
||
gap: 2px;
|
||
}
|
||
|
||
.nav-item {
|
||
padding: 8px 6px;
|
||
font-size: 10px;
|
||
}
|
||
}
|
||
|
||
.loading-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10000;
|
||
background: rgba(15, 23, 42, 0.85);
|
||
backdrop-filter: blur(6px);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.5s;
|
||
opacity: 1;
|
||
}
|
||
|
||
.loading-overlay.hidden {
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.loading-spinner {
|
||
width: 60px;
|
||
height: 60px;
|
||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||
border-top-color: var(--primary);
|
||
border-radius: 50%;
|
||
animation: spin 0.8s linear infinite;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.loading-text {
|
||
color: var(--primary-light);
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div class="loading-overlay" id="loadingOverlay">
|
||
<div class="loading-spinner"></div>
|
||
<div class="loading-text">Memuat data...</div>
|
||
</div>
|
||
<div class="toast-container" id="toastContainer"></div>
|
||
<div class="mobile-overlay" id="mobileOverlay" onclick="toggleMobileSidebar()"></div>
|
||
<div class="app-layout" id="appLayout">
|
||
<div class="sidebar" id="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="logo"><svg viewBox="0 0 24 24">
|
||
<path
|
||
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z" />
|
||
</svg></div>
|
||
<div>
|
||
<h2>WebGIS Kemiskinan</h2><small>Kota Pontianak, Kalimantan Barat</small>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-nav" id="sidebarNav">
|
||
<button class="nav-item active" onclick="switchTab('dashboard')" data-tab="dashboard">📊
|
||
Dashboard</button>
|
||
<button class="nav-item" onclick="switchTab('layers')" data-tab="layers">🗂️ Layer</button>
|
||
<button class="nav-item" onclick="switchTab('data')" data-tab="data">📋 Data</button>
|
||
<button class="nav-item <?= $isAdmin ? '' : 'hidden' ?>" id="tabAddData" onclick="switchTab('adddata')"
|
||
data-tab="adddata">➕ Tambah</button>
|
||
<button class="nav-item" onclick="switchTab('analysis')" data-tab="analysis">🔍 Analisis</button>
|
||
</div>
|
||
<div class="sidebar-content" id="sidebarContent"></div>
|
||
</div>
|
||
<div class="map-wrapper">
|
||
<div id="map"></div>
|
||
<button class="toggle-sidebar-btn" onclick="toggleSidebar()" title="Toggle Panel"><svg viewBox="0 0 24 24">
|
||
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z" />
|
||
</svg></button>
|
||
<div class="topbar">
|
||
<button class="topbar-btn active" id="btnBuffer" onclick="toggleLayerBtn('buffer')">🟣 Buffer</button>
|
||
<button class="topbar-btn active" id="btnDensity" onclick="toggleLayerBtn('density')">🟢
|
||
Kerapatan</button>
|
||
<button class="topbar-btn active" id="btnFacilities" onclick="toggleLayerBtn('facilities')">🏥
|
||
Fasilitas</button>
|
||
<button class="topbar-btn active" id="btnRoads" onclick="toggleLayerBtn('roads')">🛣️ Jalan</button>
|
||
<button class="topbar-btn active" id="btnWorship" onclick="toggleLayerBtn('worship')">🕌 Ibadah</button>
|
||
<button class="topbar-btn active" id="btnKecamatan" onclick="toggleLayerBtn('kecamatan')">🟧
|
||
Kecamatan</button>
|
||
</div>
|
||
<div class="admin-controls <?= $isAdmin ? 'show' : '' ?>" id="adminControls">
|
||
<div class="admin-badge"><span class="dot"></span> Admin</div>
|
||
<a href="logout.php" class="logout-btn">Keluar</a>
|
||
</div>
|
||
<div class="picking-banner" id="pickingBanner">📍 Klik pada peta untuk memilih lokasi <button
|
||
class="cancel-pick" onclick="cancelPicking()">Batal</button></div>
|
||
<div class="info-panel" id="infoPanel">
|
||
<h4 id="infoTitle">Informasi</h4>
|
||
<p id="infoText">Klik pada elemen peta untuk melihat detail</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="editModal">
|
||
<div class="modal-dialog">
|
||
<h3 id="editModalTitle">✏️ Edit Data</h3>
|
||
<div id="editModalBody"></div>
|
||
<div class="modal-actions"><button class="btn-cancel-modal" onclick="closeEditModal()">Batal</button><button
|
||
class="btn-save-modal" id="btnSaveEdit">💾 Simpan</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="confirmModal">
|
||
<div class="modal-dialog">
|
||
<h3 id="confirmModalTitle">Konfirmasi</h3>
|
||
<p id="confirmModalText" style="color:#cbd5e1; font-size:13px; margin-bottom:20px;"></p>
|
||
<div class="modal-actions">
|
||
<button class="btn-cancel-modal" onclick="closeConfirmModal()">Batal</button>
|
||
<button class="btn-save-modal" id="btnConfirmYes" style="background:var(--danger);">Ya,
|
||
Lanjutkan</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="fullTableModal">
|
||
<div class="modal-dialog">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:16px;">
|
||
<h3 style="margin:0;">📋 Tabel Data Lengkap</h3>
|
||
<button class="btn-cancel-modal" style="padding:6px 12px; font-size:12px;"
|
||
onclick="closeFullTableModal()">✕ Tutup</button>
|
||
</div>
|
||
<div class="table-responsive" id="fullTableContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const IS_ADMIN = <?= $isAdmin ? 'true' : 'false' ?>;
|
||
const POOR_RESIDENTS = <?= json_encode($poor) ?>;
|
||
let kecamatanPolygons = [];
|
||
|
||
const APP_STATE = {
|
||
isAdmin: IS_ADMIN,
|
||
pickingMode: false,
|
||
pickedLat: null,
|
||
pickedLng: null,
|
||
activeTab: 'dashboard',
|
||
layers: { buffer: true, poorPoints: true, facilities: true, roads: true, worship: true, density: true, kecamatan: true },
|
||
bufferRadius: <?= $bufferRadius ?>,
|
||
map: null,
|
||
layerGroups: {},
|
||
pickupMarker: null,
|
||
currentEditId: null,
|
||
confirmCallback: null,
|
||
sortColumn: null,
|
||
sortDirection: 'asc'
|
||
};
|
||
|
||
const layerOrder = ['kecamatan', 'density', 'buffer', 'roads', 'poorPoints', 'facilities', 'worship'];
|
||
|
||
function updateLayerOrder() {
|
||
for (let i = 0; i < layerOrder.length; i++) {
|
||
const name = layerOrder[i];
|
||
if (APP_STATE.layers[name] && APP_STATE.layerGroups[name] && typeof APP_STATE.layerGroups[name].bringToFront === 'function') {
|
||
APP_STATE.layerGroups[name].bringToFront();
|
||
}
|
||
}
|
||
}
|
||
function openEditModal(data) {
|
||
const modal = document.getElementById('editModal');
|
||
document.getElementById('editModalTitle').textContent = '✏️ Edit Data Penduduk Miskin';
|
||
document.getElementById('editModalBody').innerHTML = `
|
||
<div class="form-group"><label>Nama Lengkap</label><input type="text" id="edit_name" value="${escapeHtml(data.name)}"><span class="error-msg" id="err_edit_name">Nama wajib diisi</span></div>
|
||
<div class="form-group"><label>NIK</label><input type="text" id="edit_nik" value="${escapeHtml(data.nik || '')}" maxlength="16"><span class="error-msg" id="err_edit_nik">NIK harus 16 digit angka</span></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||
<div class="form-group"><label>Jumlah Keluarga</label><input type="number" id="edit_family" value="${data.family_size}" min="1"><span class="error-msg" id="err_edit_family">Minimal 1</span></div>
|
||
<div class="form-group"><label>Penghasilan (Rp)</label><input type="number" id="edit_income" value="${data.income}" min="0"><span class="error-msg" id="err_edit_income">Harus angka</span></div>
|
||
</div>
|
||
<div class="form-group"><label>RT / RW</label><input type="text" id="edit_rtrw" value="${escapeHtml(data.rt_rw || '')}"></div>
|
||
`;
|
||
document.getElementById('btnSaveEdit').onclick = function () {
|
||
if (validateEditForm()) {
|
||
showConfirm('Simpan perubahan data ini?', savePoorEdit);
|
||
}
|
||
};
|
||
APP_STATE.currentEditId = data.id;
|
||
modal.classList.add('show');
|
||
}
|
||
|
||
function closeEditModal() { document.getElementById('editModal').classList.remove('show'); APP_STATE.currentEditId = null; }
|
||
function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(text).replace(/[&<>"']/g, m => map[m]); }
|
||
|
||
function validateEditForm() {
|
||
let valid = true;
|
||
const name = document.getElementById('edit_name');
|
||
const nik = document.getElementById('edit_nik');
|
||
const family = document.getElementById('edit_family');
|
||
const income = document.getElementById('edit_income');
|
||
document.querySelectorAll('.error-msg').forEach(e => e.classList.remove('show'));
|
||
document.querySelectorAll('.input-error').forEach(e => e.classList.remove('input-error'));
|
||
if (!name.value.trim()) {
|
||
document.getElementById('err_edit_name').classList.add('show');
|
||
name.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
const nikVal = nik.value.trim();
|
||
if (nikVal && (!/^\d{16}$/.test(nikVal))) {
|
||
document.getElementById('err_edit_nik').classList.add('show');
|
||
nik.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
if (!family.value || parseInt(family.value) < 1) {
|
||
document.getElementById('err_edit_family').classList.add('show');
|
||
family.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
if (income.value === '' || isNaN(parseInt(income.value)) || parseInt(income.value) < 0) {
|
||
document.getElementById('err_edit_income').classList.add('show');
|
||
income.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
return valid;
|
||
}
|
||
|
||
async function savePoorEdit() {
|
||
const id = APP_STATE.currentEditId;
|
||
const name = document.getElementById('edit_name').value.trim();
|
||
const formData = new FormData();
|
||
formData.append('id', id);
|
||
formData.append('name', name);
|
||
formData.append('nik', document.getElementById('edit_nik').value);
|
||
formData.append('family_size', document.getElementById('edit_family').value);
|
||
formData.append('income', document.getElementById('edit_income').value);
|
||
formData.append('rt_rw', document.getElementById('edit_rtrw').value);
|
||
const old = POOR_RESIDENTS.find(p => p.id == id);
|
||
formData.append('lat', old.lat);
|
||
formData.append('lng', old.lng);
|
||
|
||
try {
|
||
const response = await fetch('edit_poor.php', { method: 'POST', body: formData });
|
||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||
showToast('✅ Data berhasil diupdate!', 'success');
|
||
closeEditModal();
|
||
location.reload();
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
console.error('Edit error:', err);
|
||
}
|
||
}
|
||
|
||
function showConfirm(message, callback) {
|
||
document.getElementById('confirmModalText').textContent = message;
|
||
document.getElementById('confirmModal').classList.add('show');
|
||
APP_STATE.confirmCallback = callback;
|
||
document.getElementById('btnConfirmYes').onclick = function () {
|
||
const cb = APP_STATE.confirmCallback;
|
||
closeConfirmModal();
|
||
if (cb) {
|
||
cb();
|
||
}
|
||
};
|
||
}
|
||
|
||
function closeConfirmModal() {
|
||
document.getElementById('confirmModal').classList.remove('show');
|
||
APP_STATE.confirmCallback = null;
|
||
}
|
||
|
||
function getDensityColor(count, total) {
|
||
if (total === 0 || count === 0) return '#1a5e1a';
|
||
const ratio = count / total;
|
||
const colors = ['#1a5e1a', '#2e7d32', '#4caf50', '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', '#f44336', '#b71c1c'];
|
||
const idx = Math.min(Math.floor(ratio * 10), 9);
|
||
return colors[idx];
|
||
}
|
||
|
||
function showToast(msg, type = 'info') { const c = document.getElementById('toastContainer'); if (!c) return; const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = msg; c.appendChild(t); setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(40px)'; setTimeout(() => t.remove(), 300); }, 3500); }
|
||
function showInfo(title, text) { const panel = document.getElementById('infoPanel'); document.getElementById('infoTitle').textContent = title; document.getElementById('infoText').textContent = text; panel.classList.add('show'); clearTimeout(panel._timeout); panel._timeout = setTimeout(() => { panel.classList.remove('show'); }, 5000); }
|
||
|
||
function initMap() {
|
||
const loadingEl = document.getElementById('loadingOverlay');
|
||
loadingEl.classList.remove('hidden');
|
||
|
||
if (APP_STATE.map) { APP_STATE.map.remove(); APP_STATE.map = null; APP_STATE.layerGroups = {}; }
|
||
APP_STATE.map = L.map('map', { center: [-0.045, 109.340], zoom: 13, zoomControl: false });
|
||
L.control.zoom({ position: 'bottomright' }).addTo(APP_STATE.map);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(APP_STATE.map);
|
||
['buffer', 'poorPoints', 'facilities', 'roads', 'worship', 'density', 'kecamatan'].forEach(k => { APP_STATE.layerGroups[k] = L.layerGroup().addTo(APP_STATE.map); });
|
||
|
||
const promises = [
|
||
loadFacilitiesLayer(),
|
||
loadRoadsLayer(),
|
||
loadWorshipLayer(),
|
||
loadKecamatanLayer(),
|
||
loadBufferLayer(),
|
||
loadPoorPointsLayer()
|
||
];
|
||
|
||
Promise.all(promises).then(() => {
|
||
loadingEl.classList.add('hidden');
|
||
setTimeout(() => { loadingEl.style.display = 'none'; }, 500);
|
||
updateLayerOrder();
|
||
}).catch(() => {
|
||
loadingEl.classList.add('hidden');
|
||
setTimeout(() => { loadingEl.style.display = 'none'; }, 500);
|
||
});
|
||
|
||
APP_STATE.map.on('click', function (e) { if (APP_STATE.pickingMode) pickLocation(e.latlng); });
|
||
['btnBuffer', 'btnDensity', 'btnFacilities', 'btnRoads', 'btnWorship', 'btnKecamatan'].forEach(id => { const btn = document.getElementById(id); if (btn) btn.classList.add('active'); });
|
||
refreshSidebar();
|
||
}
|
||
|
||
function loadKecamatanLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.kecamatan;
|
||
g.clearLayers();
|
||
fetch('json/Kecamatan_Pontianak.json')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const filteredFeatures = data.features.filter(f =>
|
||
f.geometry.type === 'Polygon' || f.geometry.type === 'MultiPolygon'
|
||
);
|
||
const cleanData = {
|
||
type: 'FeatureCollection',
|
||
features: filteredFeatures
|
||
};
|
||
L.geoJSON(cleanData, {
|
||
style: { color: '#ff9800', weight: 2, fillOpacity: 0, dashArray: '4' }
|
||
}).addTo(g);
|
||
kecamatanPolygons = filteredFeatures;
|
||
return loadDensityLayer();
|
||
})
|
||
.then(() => resolve())
|
||
.catch(err => {
|
||
console.error('Gagal memuat kecamatan:', err);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
function loadBufferLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.buffer; g.clearLayers();
|
||
fetch('json/Ibadah_Pontianak.json').then(r => r.json()).then(data => {
|
||
const pts = data.features.map(f => f.geometry.coordinates);
|
||
if (!pts.length) { resolve(); return; }
|
||
const buffers = pts.map(c => turf.buffer(turf.point(c), APP_STATE.bufferRadius, { units: 'meters' }));
|
||
let merged = buffers[0];
|
||
for (let i = 1; i < buffers.length; i++) { try { merged = turf.union(merged, buffers[i]); } catch (e) { } }
|
||
L.geoJSON(merged, { style: { color: '#8b5cf6', weight: 2, fillColor: '#8b5cf6', fillOpacity: 0.12, dashArray: '6' } }).addTo(g);
|
||
resolve();
|
||
}).catch(err => { console.error('Gagal memuat ibadah:', err); resolve(); });
|
||
});
|
||
}
|
||
|
||
function loadPoorPointsLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.poorPoints; g.clearLayers();
|
||
if (!POOR_RESIDENTS.length) { resolve(); return; }
|
||
fetch('json/Ibadah_Pontianak.json').then(r => r.json()).then(data => {
|
||
const pts = data.features.map(f => f.geometry.coordinates);
|
||
const buffers = pts.map(c => turf.buffer(turf.point(c), APP_STATE.bufferRadius, { units: 'meters' }));
|
||
POOR_RESIDENTS.forEach(p => {
|
||
const lat = parseFloat(p.lat), lng = parseFloat(p.lng);
|
||
if (isNaN(lat) || isNaN(lng)) return;
|
||
const covered = buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b));
|
||
const color = covered ? '#10b981' : '#ef4444';
|
||
const icon = L.divIcon({ className: '', html: `<div style="width:20px;height:20px;border-radius:50%;background:${color};border:3px solid white;box-shadow:0 2px 8px rgba(0,0,0,0.3);transition: transform 0.2s"></div>`, iconSize: [20, 20], iconAnchor: [10, 10] });
|
||
const marker = L.marker([lat, lng], { icon });
|
||
marker.bindPopup(`<div class="popup-title">${escapeHtml(p.name)}</div><div class="popup-info">NIK: ${escapeHtml(p.nik || '-')}</div><div class="popup-info">RT/RW: ${escapeHtml(p.rt_rw || '-')}</div><div class="popup-info">Keluarga: ${Number(p.family_size)} orang</div><div class="popup-info">Penghasilan: Rp ${Number(p.income).toLocaleString('id-ID')}/bulan</div><div class="popup-status ${covered ? 'covered' : 'uncovered'}">${covered ? '✓ Dalam Jangkauan' : '✗ Di Luar Jangkauan'}</div>${APP_STATE.isAdmin ? `<div class="popup-actions"><button class="btn-popup info" onclick="editPoor(${p.id})">✏️ Edit</button><button class="btn-popup danger" onclick="confirmDeletePoor(${p.id})">🗑️ Hapus</button></div>` : ''}`);
|
||
marker.on('click', () => showInfo(p.name, `Rp ${Number(p.income).toLocaleString('id-ID')}/bln`));
|
||
g.addLayer(marker);
|
||
});
|
||
resolve();
|
||
}).catch(err => { console.error('Gagal memuat ibadah:', err); resolve(); });
|
||
});
|
||
}
|
||
|
||
function loadFacilitiesLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.facilities; g.clearLayers();
|
||
const tc = { hospital: '#ef4444', clinic: '#f59e0b', school: '#3b82f6' };
|
||
const ti = { hospital: '🏥', clinic: '🏪', school: '🏫' };
|
||
fetch('json/Fasilitas_Pontianak.json').then(r => r.json()).then(data => {
|
||
data.features.forEach(f => {
|
||
const [lng, lat] = f.geometry.coordinates;
|
||
if (!lat || !lng) return;
|
||
const type = f.properties.amenity || 'other';
|
||
const name = f.properties.name || type;
|
||
L.marker([lat, lng], { icon: L.divIcon({ className: '', html: `<div style="width:34px;height:34px;border-radius:50%;background:${tc[type] || '#999'};display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.3);font-size:16px;color:white">${ti[type] || '📍'}</div>`, iconSize: [34, 34], iconAnchor: [17, 17] }) }).bindPopup(`<div class="popup-title">${name}</div><div class="popup-info">Tipe: ${type}</div>`).addTo(g);
|
||
});
|
||
resolve();
|
||
}).catch(err => { console.error('Gagal memuat fasilitas:', err); resolve(); });
|
||
});
|
||
}
|
||
|
||
function loadRoadsLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.roads; g.clearLayers();
|
||
fetch('json/Jalan_Pontianak.json').then(r => r.json()).then(data => {
|
||
L.geoJSON(data, { style: f => { const t = f.properties.highway || 'other'; const s = { motorway: { color: '#ef4444', weight: 4 }, trunk: { color: '#f59e0b', weight: 4 }, primary: { color: '#fbbf24', weight: 3 }, secondary: { color: '#94a3b8', weight: 2 }, tertiary: { color: '#94a3b8', weight: 2 }, residential: { color: '#999', weight: 1.5 } }; return s[t] || { color: '#ccc', weight: 1 }; }, onEachFeature: (f, l) => l.bindPopup(`<div class="popup-title">${f.properties.name || 'Jalan'}</div><div class="popup-info">${f.properties.highway || ''}</div>`) }).addTo(g);
|
||
resolve();
|
||
}).catch(err => { console.error('Gagal memuat jalan:', err); resolve(); });
|
||
});
|
||
}
|
||
|
||
function loadWorshipLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.worship; g.clearLayers();
|
||
const religionIcons = { muslim: '🕌', christian: '⛪', buddhist: '☸️', hindu: '🛕' };
|
||
const religionColors = { muslim: '#10b981', christian: '#3b82f6', buddhist: '#f59e0b', hindu: '#8b5cf6' };
|
||
const religionNames = { muslim: 'Masjid', christian: 'Gereja', buddhist: 'Vihara', hindu: 'Pura' };
|
||
fetch('json/Ibadah_Pontianak.json').then(r => r.json()).then(data => {
|
||
data.features.forEach(f => {
|
||
const [lng, lat] = f.geometry.coordinates;
|
||
if (!lat || !lng) return;
|
||
const religion = f.properties.religion || 'other';
|
||
const name = f.properties.name || religionNames[religion] || 'Tempat Ibadah';
|
||
const iconEmoji = religionIcons[religion] || '🕌';
|
||
const borderColor = religionColors[religion] || '#999';
|
||
L.marker([lat, lng], {
|
||
icon: L.divIcon({ className: '', html: `<div style="width:30px;height:30px;border-radius:50%;background:white;border:3px solid ${borderColor};display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,0.2);font-size:14px">${iconEmoji}</div>`, iconSize: [30, 30], iconAnchor: [15, 15] })
|
||
}).bindPopup(`<div class="popup-title">${name}</div><div class="popup-info">Jenis: ${religion}</div>`).addTo(g);
|
||
});
|
||
resolve();
|
||
}).catch(err => { console.error('Gagal memuat ibadah:', err); resolve(); });
|
||
});
|
||
}
|
||
|
||
function loadDensityLayer() {
|
||
return new Promise((resolve) => {
|
||
const g = APP_STATE.layerGroups.density;
|
||
g.clearLayers();
|
||
if (!kecamatanPolygons.length) { resolve(); return; }
|
||
fetch('json/Ibadah_Pontianak.json')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
const pts = data.features.map(f => f.geometry.coordinates);
|
||
const buffers = pts.map(c => turf.buffer(turf.point(c), APP_STATE.bufferRadius, { units: 'meters' }));
|
||
const total = POOR_RESIDENTS.filter(p => {
|
||
const lat = parseFloat(p.lat), lng = parseFloat(p.lng);
|
||
if (isNaN(lat) || isNaN(lng)) return false;
|
||
return !buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b));
|
||
}).length;
|
||
kecamatanPolygons.forEach((poly, idx) => {
|
||
let count = 0;
|
||
POOR_RESIDENTS.forEach(p => {
|
||
const lat = parseFloat(p.lat), lng = parseFloat(p.lng);
|
||
if (isNaN(lat) || isNaN(lng)) return;
|
||
const pt = turf.point([lng, lat]);
|
||
if (!buffers.some(b => turf.booleanPointInPolygon(pt, b)) && turf.booleanPointInPolygon(pt, poly)) count++;
|
||
});
|
||
const color = getDensityColor(count, total);
|
||
const layer = L.geoJSON(poly, {
|
||
style: { color: color, weight: 2, fillColor: color, fillOpacity: 0.4 }
|
||
});
|
||
layer.bindPopup(`Titik Miskin (Blm Terjangkau): ${count} (${total > 0 ? ((count / total) * 100).toFixed(1) : 0}%)`);
|
||
g.addLayer(layer);
|
||
});
|
||
resolve();
|
||
})
|
||
.catch(err => {
|
||
console.error('Gagal density:', err);
|
||
resolve();
|
||
});
|
||
});
|
||
}
|
||
|
||
function toggleLayerBtn(name) {
|
||
const currentState = APP_STATE.layers[name];
|
||
const newState = !currentState;
|
||
APP_STATE.layers[name] = newState;
|
||
if (name === 'worship') {
|
||
APP_STATE.layers.buffer = newState;
|
||
const bufferBtn = document.getElementById('btnBuffer');
|
||
if (bufferBtn) bufferBtn.classList.toggle('active', newState);
|
||
if (newState) APP_STATE.map.addLayer(APP_STATE.layerGroups.buffer);
|
||
else APP_STATE.map.removeLayer(APP_STATE.layerGroups.buffer);
|
||
}
|
||
const btnMap = { buffer: 'btnBuffer', density: 'btnDensity', facilities: 'btnFacilities', roads: 'btnRoads', worship: 'btnWorship', kecamatan: 'btnKecamatan' };
|
||
const btn = document.getElementById(btnMap[name]);
|
||
if (btn) btn.classList.toggle('active', newState);
|
||
const lg = APP_STATE.layerGroups[name];
|
||
if (lg) {
|
||
if (newState) APP_STATE.map.addLayer(lg);
|
||
else APP_STATE.map.removeLayer(lg);
|
||
}
|
||
updateLayerOrder();
|
||
refreshSidebar();
|
||
}
|
||
|
||
function toggleLayer(name) {
|
||
const currentState = APP_STATE.layers[name];
|
||
const newState = !currentState;
|
||
APP_STATE.layers[name] = newState;
|
||
if (name === 'worship') {
|
||
APP_STATE.layers.buffer = newState;
|
||
const bufferBtn = document.getElementById('btnBuffer');
|
||
if (bufferBtn) bufferBtn.classList.toggle('active', newState);
|
||
if (newState) APP_STATE.map.addLayer(APP_STATE.layerGroups.buffer);
|
||
else APP_STATE.map.removeLayer(APP_STATE.layerGroups.buffer);
|
||
}
|
||
const btnMap = { buffer: 'btnBuffer', density: 'btnDensity', facilities: 'btnFacilities', roads: 'btnRoads', worship: 'btnWorship', kecamatan: 'btnKecamatan' };
|
||
const btn = document.getElementById(btnMap[name]);
|
||
if (btn) btn.classList.toggle('active', newState);
|
||
const lg = APP_STATE.layerGroups[name];
|
||
if (lg) {
|
||
if (newState) APP_STATE.map.addLayer(lg);
|
||
else APP_STATE.map.removeLayer(lg);
|
||
}
|
||
updateLayerOrder();
|
||
refreshSidebar();
|
||
}
|
||
|
||
function updateBufferRadius(val) { APP_STATE.bufferRadius = parseInt(val) || 500; localStorage.setItem('bufferRadius', APP_STATE.bufferRadius); if (APP_STATE.isAdmin) { const fd = new FormData(); fd.append('key', 'buffer_radius'); fd.append('value', APP_STATE.bufferRadius); fetch('update_settings.php', { method: 'POST', body: fd }); } loadBufferLayer(); loadPoorPointsLayer(); loadDensityLayer(); refreshSidebar(); showToast(`Radius buffer: ${APP_STATE.bufferRadius}m`, 'info'); }
|
||
|
||
function startPicking() {
|
||
if (!APP_STATE.isAdmin) { showToast('Hanya admin!', 'error'); return; }
|
||
APP_STATE.pickingMode = true;
|
||
document.getElementById('pickingBanner').classList.add('show');
|
||
document.getElementById('map').classList.add('map-picking-mode');
|
||
// Nonaktifkan interaksi semua elemen interaktif Leaflet
|
||
document.querySelectorAll('.leaflet-interactive').forEach(el => {
|
||
el.style.pointerEvents = 'none';
|
||
});
|
||
showToast('Klik peta untuk memilih lokasi', 'info');
|
||
if (window.innerWidth <= 768) toggleMobileSidebar();
|
||
}
|
||
|
||
function cancelPicking() {
|
||
APP_STATE.pickingMode = false;
|
||
document.getElementById('pickingBanner').classList.remove('show');
|
||
document.getElementById('map').classList.remove('map-picking-mode');
|
||
// Kembalikan interaksi
|
||
document.querySelectorAll('.leaflet-interactive').forEach(el => {
|
||
el.style.pointerEvents = '';
|
||
});
|
||
refreshSidebar();
|
||
}
|
||
|
||
function pickLocation(latlng) {
|
||
APP_STATE.pickingMode = false;
|
||
APP_STATE.pickedLat = latlng.lat;
|
||
APP_STATE.pickedLng = latlng.lng;
|
||
document.getElementById('pickingBanner').classList.remove('show');
|
||
document.getElementById('map').classList.remove('map-picking-mode');
|
||
// Kembalikan interaksi
|
||
document.querySelectorAll('.leaflet-interactive').forEach(el => {
|
||
el.style.pointerEvents = '';
|
||
});
|
||
if (APP_STATE.pickupMarker) APP_STATE.map.removeLayer(APP_STATE.pickupMarker);
|
||
APP_STATE.pickupMarker = L.marker([latlng.lat, latlng.lng], {
|
||
icon: L.divIcon({
|
||
className: '',
|
||
html: '<div style="width:24px;height:24px;border-radius:50%;background:#6366f1;border:3px solid white;box-shadow:0 2px 12px rgba(99,102,241,0.5);animation: pulse-dot 2s infinite"></div>',
|
||
iconSize: [24, 24],
|
||
iconAnchor: [12, 12]
|
||
})
|
||
}).addTo(APP_STATE.map);
|
||
showToast('Lokasi dipilih!', 'success');
|
||
refreshSidebar();
|
||
}
|
||
function toggleSidebar() { const sb = document.getElementById('sidebar'); if (window.innerWidth <= 768) { sb.classList.toggle('mobile-open'); document.getElementById('mobileOverlay').classList.toggle('show', sb.classList.contains('mobile-open')); } else { sb.classList.toggle('collapsed'); } }
|
||
function toggleMobileSidebar() { document.getElementById('sidebar').classList.remove('mobile-open'); document.getElementById('mobileOverlay').classList.remove('show'); }
|
||
function switchTab(tab) { if (tab === 'adddata' && !APP_STATE.isAdmin) { showToast('Hanya admin!', 'info'); return; } APP_STATE.activeTab = tab; document.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n.dataset.tab === tab)); refreshSidebar(); }
|
||
|
||
function zoomToKecamatan(index) {
|
||
if (index >= 0 && index < kecamatanPolygons.length) {
|
||
const feature = kecamatanPolygons[index];
|
||
const layer = L.geoJSON(feature);
|
||
APP_STATE.map.fitBounds(layer.getBounds(), { padding: [50, 50], animate: true, duration: 0.5 });
|
||
}
|
||
}
|
||
|
||
function sortTableData(data, column) {
|
||
if (APP_STATE.sortColumn === column) {
|
||
APP_STATE.sortDirection = APP_STATE.sortDirection === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
APP_STATE.sortColumn = column;
|
||
APP_STATE.sortDirection = 'asc';
|
||
}
|
||
return [...data].sort((a, b) => {
|
||
let valA = a[column] ?? '';
|
||
let valB = b[column] ?? '';
|
||
if (column === 'family_size' || column === 'income') {
|
||
valA = Number(valA);
|
||
valB = Number(valB);
|
||
}
|
||
if (valA < valB) return APP_STATE.sortDirection === 'asc' ? -1 : 1;
|
||
if (valA > valB) return APP_STATE.sortDirection === 'asc' ? 1 : -1;
|
||
return 0;
|
||
});
|
||
}
|
||
|
||
function openFullTableModal() {
|
||
fetch('json/Ibadah_Pontianak.json').then(r => r.json()).then(data => {
|
||
const pts = data.features.map(f => f.geometry.coordinates);
|
||
const buffers = pts.map(c => turf.buffer(turf.point(c), APP_STATE.bufferRadius, { units: 'meters' }));
|
||
const tableData = POOR_RESIDENTS.map(p => {
|
||
const lat = parseFloat(p.lat), lng = parseFloat(p.lng);
|
||
const covered = !isNaN(lat) && !isNaN(lng) && buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b));
|
||
return {
|
||
id: p.id,
|
||
name: p.name,
|
||
nik: p.nik || '-',
|
||
family_size: Number(p.family_size),
|
||
income: Number(p.income),
|
||
rt_rw: p.rt_rw || '-',
|
||
covered: covered
|
||
};
|
||
});
|
||
const sortedData = sortTableData(tableData, APP_STATE.sortColumn || 'name');
|
||
const getSortIcon = (col) => {
|
||
if (APP_STATE.sortColumn !== col) return ' ↕';
|
||
return APP_STATE.sortDirection === 'asc' ? ' ↑' : ' ↓';
|
||
};
|
||
const getSortClass = (col) => APP_STATE.sortColumn === col ? 'sorted' : '';
|
||
const rows = sortedData.map(p => {
|
||
const statusClass = p.covered ? 'covered' : 'uncovered';
|
||
const statusText = p.covered ? 'Terjangkau' : 'Belum';
|
||
const incomeFormatted = 'Rp ' + p.income.toLocaleString('id-ID');
|
||
return `<tr>
|
||
<td>${escapeHtml(p.name)}</td>
|
||
<td>${escapeHtml(p.nik)}</td>
|
||
<td>${p.family_size}</td>
|
||
<td>${incomeFormatted}</td>
|
||
<td>${escapeHtml(p.rt_rw)}</td>
|
||
<td><span class="status-badge ${statusClass}">${statusText}</span></td>
|
||
${APP_STATE.isAdmin ? `<td>
|
||
<button class="btn-table-action edit" onclick="editPoor(${p.id})">✏️</button>
|
||
<button class="btn-table-action delete" onclick="confirmDeletePoor(${p.id})">🗑️</button>
|
||
</td>` : '<td>-</td>'}
|
||
</tr>`;
|
||
}).join('');
|
||
document.getElementById('fullTableContent').innerHTML = `
|
||
<table class="data-table">
|
||
<thead>
|
||
<tr>
|
||
<th onclick="APP_STATE.sortColumn='name';openFullTableModal()" class="${getSortClass('name')}">Nama<span class="sort-icon">${getSortIcon('name')}</span></th>
|
||
<th onclick="APP_STATE.sortColumn='nik';openFullTableModal()" class="${getSortClass('nik')}">NIK<span class="sort-icon">${getSortIcon('nik')}</span></th>
|
||
<th onclick="APP_STATE.sortColumn='family_size';openFullTableModal()" class="${getSortClass('family_size')}">Keluarga<span class="sort-icon">${getSortIcon('family_size')}</span></th>
|
||
<th onclick="APP_STATE.sortColumn='income';openFullTableModal()" class="${getSortClass('income')}">Penghasilan<span class="sort-icon">${getSortIcon('income')}</span></th>
|
||
<th>RT/RW</th>
|
||
<th>Status</th>
|
||
${APP_STATE.isAdmin ? '<th>Aksi</th>' : '<th></th>'}
|
||
</tr>
|
||
</thead>
|
||
<tbody>${rows}</tbody>
|
||
</table>`;
|
||
document.getElementById('fullTableModal').classList.add('show');
|
||
});
|
||
}
|
||
|
||
function closeFullTableModal() {
|
||
document.getElementById('fullTableModal').classList.remove('show');
|
||
}
|
||
|
||
function refreshSidebar() {
|
||
const sc = document.getElementById('sidebarContent');
|
||
if (!sc) return;
|
||
fetch('json/Ibadah_Pontianak.json').then(r => r.json()).then(data => {
|
||
const pts = data.features.map(f => f.geometry.coordinates);
|
||
const buffers = pts.map(c => turf.buffer(turf.point(c), APP_STATE.bufferRadius, { units: 'meters' }));
|
||
const totalPts = POOR_RESIDENTS.filter(p => { const lat = parseFloat(p.lat), lng = parseFloat(p.lng); if (isNaN(lat) || isNaN(lng)) return false; return !buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b)); }).length;
|
||
const totalKecamatan = kecamatanPolygons.length;
|
||
let covered = 0, uncovered = 0;
|
||
POOR_RESIDENTS.forEach(p => { const lat = parseFloat(p.lat), lng = parseFloat(p.lng); if (isNaN(lat) || isNaN(lng)) return; if (buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b))) covered++; else uncovered++; });
|
||
const vp = {};
|
||
POOR_RESIDENTS.forEach(p => { const lat = parseFloat(p.lat), lng = parseFloat(p.lng); if (isNaN(lat) || isNaN(lng)) return; const pt = turf.point([lng, lat]); if (!buffers.some(b => turf.booleanPointInPolygon(pt, b))) { kecamatanPolygons.forEach((poly, idx) => { if (turf.booleanPointInPolygon(pt, poly)) vp[idx] = (vp[idx] || 0) + 1; }); } });
|
||
|
||
let html = '';
|
||
if (APP_STATE.activeTab === 'dashboard') {
|
||
const un = POOR_RESIDENTS.filter(p => { const lat = parseFloat(p.lat), lng = parseFloat(p.lng); if (isNaN(lat) || isNaN(lng)) return false; return !buffers.some(b => turf.booleanPointInPolygon(turf.point([lng, lat]), b)); });
|
||
const totalFam = un.reduce((s, p) => s + Number(p.family_size), 0);
|
||
const totalIncome = un.reduce((s, p) => s + Number(p.income), 0);
|
||
const avgInc = totalPts > 0 ? Math.round(totalIncome / totalPts) : 0;
|
||
|
||
let distRows = kecamatanPolygons.map((poly, idx) => {
|
||
const c = vp[idx] || 0;
|
||
const pct = totalPts > 0 ? ((c / totalPts) * 100).toFixed(1) : 0;
|
||
const barWidth = totalPts > 0 ? (c / totalPts) * 100 : 0;
|
||
const barColor = getDensityColor(c, totalPts);
|
||
return `<tr onclick="zoomToKecamatan(${idx})" style="cursor:pointer;"><td class="dist-name">Kec. ${idx + 1}</td><td class="dist-count">${c}</td><td class="dist-pct">${pct}% <span class="dist-bar-bg"><span class="dist-bar-fill" style="width:${barWidth}%;background:${barColor}"></span></span></td></tr>`;
|
||
}).join('');
|
||
|
||
html = `<div class="section active">
|
||
<div class="section-title">Ringkasan Data</div>
|
||
<div class="stats-row">
|
||
<div class="stat-mini red" style="animation-delay:0.05s"><div class="stat-val">${totalPts}</div><div class="stat-lbl">Titik Miskin</div></div>
|
||
<div class="stat-mini green" style="animation-delay:0.1s"><div class="stat-val">${covered}</div><div class="stat-lbl">Terjangkau</div></div>
|
||
<div class="stat-mini yellow" style="animation-delay:0.15s"><div class="stat-val">${uncovered}</div><div class="stat-lbl">Belum</div></div>
|
||
<div class="stat-mini blue" style="animation-delay:0.2s"><div class="stat-val">${totalKecamatan}</div><div class="stat-lbl">Kecamatan</div></div>
|
||
</div>
|
||
<div class="analysis-mini"><h4>Total Jiwa Terdampak</h4><div class="a-val">${totalFam.toLocaleString('id-ID')}</div></div>
|
||
<div class="analysis-mini" style="background:linear-gradient(135deg,rgba(236,72,153,0.2),rgba(244,114,182,0.2));border-color:rgba(236,72,153,0.3)"><h4>Rata-rata Penghasilan</h4><div class="a-val">Rp ${avgInc.toLocaleString('id-ID')}</div></div>
|
||
<div class="section-title">Distribusi (Blm Terjangkau)</div>
|
||
<table class="dist-table">${distRows}</table>
|
||
</div>`;
|
||
} else if (APP_STATE.activeTab === 'layers') {
|
||
html = `<div class="section active">
|
||
<div class="buffer-ctrl"><label>Radius Buffer</label><input type="number" value="${APP_STATE.bufferRadius}" min="100" max="5000" step="100" onchange="updateBufferRadius(this.value)"><span>meter</span></div>
|
||
<div class="section-title">Kontrol Layer</div>
|
||
${[{ key: 'kecamatan', label: 'Batas Kecamatan', count: 'garis' }, { key: 'density', label: 'Kerapatan', count: `${totalKecamatan} kec` }, { key: 'buffer', label: 'Zona Buffer', count: `${APP_STATE.bufferRadius}m` }, { key: 'poorPoints', label: 'Titik Miskin', count: `${totalPts} titik` }, { key: 'facilities', label: 'Fasilitas', count: 'data' }, { key: 'roads', label: 'Jalan', count: 'data' }, { key: 'worship', label: 'Ibadah', count: 'data' }].map(l => `<div class="layer-item" onclick="toggleLayer('${l.key}')"><div class="layer-switch ${APP_STATE.layers[l.key] ? 'on' : ''}"></div><span class="layer-name">${l.label}</span><span class="layer-count">${l.count}</span></div>`).join('')}
|
||
<div class="section-title">Legenda</div>
|
||
<div class="legend-wrap">
|
||
<div class="legend-item"><div class="legend-dot" style="background:#10b981;"></div> Titik Terjangkau</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background:#ef4444;"></div> Titik Belum Terjangkau</div>
|
||
<div class="legend-item"><div class="legend-rect" style="background:rgba(139,92,246,0.3);border:2px dashed #8b5cf6;"></div> Zona Buffer</div>
|
||
<div class="legend-item"><div class="legend-line" style="background:#ff9800;height:2px;"></div> Batas Kecamatan</div>
|
||
<div class="legend-item"><div class="legend-dot" style="background:#6366f1;border:3px solid white;"></div> Lokasi Dipilih</div>
|
||
<div class="legend-item"><div class="legend-rect" style="background:var(--success);opacity:0.4;"></div> Kerapatan Rendah</div>
|
||
<div class="legend-item"><div class="legend-rect" style="background:var(--danger);opacity:0.4;"></div> Kerapatan Tinggi</div>
|
||
</div>
|
||
</div>`;
|
||
} else if (APP_STATE.activeTab === 'data') {
|
||
const maxShow = 5;
|
||
const displayed = POOR_RESIDENTS.slice(0, maxShow);
|
||
const remaining = POOR_RESIDENTS.length - maxShow;
|
||
|
||
let listItems = displayed.map(p => {
|
||
const incomeFormatted = 'Rp ' + Number(p.income).toLocaleString('id-ID');
|
||
return `<div class="data-list-item">
|
||
<div class="data-list-icon poor">👤</div>
|
||
<div class="data-list-info">
|
||
<div class="data-list-name">${escapeHtml(p.name)}</div>
|
||
<div class="data-list-sub">${incomeFormatted}</div>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
if (remaining > 0) {
|
||
listItems += `<div class="data-list-item" style="justify-content:center; color:#94a3b8; font-weight:500;">
|
||
+ ${remaining} penduduk lainnya
|
||
</div>`;
|
||
}
|
||
|
||
html = `<div class="section active">
|
||
<div class="section-title">Data Penduduk</div>
|
||
<div class="data-list">${listItems}</div>
|
||
<button class="btn-full-table" onclick="openFullTableModal()">📋 Lihat Tabel Lengkap</button>
|
||
</div>`;
|
||
} else if (APP_STATE.activeTab === 'adddata') {
|
||
if (!APP_STATE.isAdmin) { APP_STATE.activeTab = 'dashboard'; refreshSidebar(); return; }
|
||
const hasPicked = APP_STATE.pickedLat !== null;
|
||
html = `<div class="section active">
|
||
<div class="section-title">Tambah Penduduk Miskin</div>
|
||
<div class="sidebar-form">
|
||
<div class="coord-display${hasPicked ? ' picked' : ''}" id="coordDisplay">${hasPicked ? `📍 ${APP_STATE.pickedLat.toFixed(6)}, ${APP_STATE.pickedLng.toFixed(6)}` : 'Belum memilih lokasi'}</div>
|
||
<button class="btn-pick-location${APP_STATE.pickingMode ? ' picking' : ''}" onclick="startPicking()">${hasPicked ? '✅ Klik untuk ubah' : '📍 Pilih Lokasi'}</button>
|
||
|
||
<div class="fg"><label>Nama</label><input type="text" id="f_name" placeholder="Nama lengkap"><span class="error-msg" id="err_f_name">Nama wajib diisi</span></div>
|
||
<div class="fg"><label>NIK</label><input type="text" id="f_nik" maxlength="16" placeholder="16 digit angka"><span class="error-msg" id="err_f_nik">NIK harus 16 digit angka</span></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
||
<div class="fg"><label>Keluarga</label><input type="number" id="f_family" value="1" min="1"><span class="error-msg" id="err_f_family">Minimal 1</span></div>
|
||
<div class="fg"><label>Penghasilan (Rp)</label><input type="number" id="f_income" value="1000000" min="0"><span class="error-msg" id="err_f_income">Harus angka</span></div>
|
||
</div>
|
||
<div class="fg"><label>RT/RW</label><input type="text" id="f_rtrw" placeholder="Contoh: 001/002"></div>
|
||
|
||
<button class="btn-save-data" onclick="validateAndSave()" ${!hasPicked ? 'disabled' : ''}>💾 Simpan</button>
|
||
<button class="btn-reset-form" onclick="resetAddForm()">🔄 Reset</button>
|
||
</div>
|
||
</div>`;
|
||
} else if (APP_STATE.activeTab === 'analysis') {
|
||
const totalAll = POOR_RESIDENTS.length;
|
||
html = `<div class="section active">
|
||
<div class="analysis-mini"><h4>Cakupan Bantuan</h4><div class="a-val">${totalAll > 0 ? ((covered / totalAll) * 100).toFixed(1) : 0}%</div></div>
|
||
<div class="analysis-mini"><h4>Di Luar Jangkauan</h4><div class="a-val">${uncovered}</div></div>
|
||
<div class="section-title">Rekomendasi</div>
|
||
<div style="padding:0 16px"><div style="padding:12px;background:rgba(255,255,255,0.05);border-radius:8px;font-size:11px;color:#cbd5e1;line-height:1.9">✅ Perluas buffer dengan pos bantuan baru<br>✅ Fokus pada kecamatan dengan kerapatan tinggi<br>✅ Tambah rumah ibadah di area kosong</div></div>
|
||
</div>`;
|
||
}
|
||
sc.innerHTML = html;
|
||
}).catch(err => { console.error('Gagal refresh:', err); sc.innerHTML = '<div style="color:#f87171;padding:20px;">Gagal memuat data. Periksa Console.</div>'; });
|
||
}
|
||
|
||
function validateAddForm() {
|
||
let valid = true;
|
||
const name = document.getElementById('f_name');
|
||
const nik = document.getElementById('f_nik');
|
||
const family = document.getElementById('f_family');
|
||
const income = document.getElementById('f_income');
|
||
document.querySelectorAll('.error-msg').forEach(e => e.classList.remove('show'));
|
||
document.querySelectorAll('.input-error').forEach(e => e.classList.remove('input-error'));
|
||
if (!name.value.trim()) {
|
||
document.getElementById('err_f_name').classList.add('show');
|
||
name.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
const nikVal = nik.value.trim();
|
||
if (nikVal && !/^\d{16}$/.test(nikVal)) {
|
||
document.getElementById('err_f_nik').classList.add('show');
|
||
nik.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
if (!family.value || parseInt(family.value) < 1) {
|
||
document.getElementById('err_f_family').classList.add('show');
|
||
family.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
if (income.value === '' || isNaN(parseInt(income.value)) || parseInt(income.value) < 0) {
|
||
document.getElementById('err_f_income').classList.add('show');
|
||
income.classList.add('input-error');
|
||
valid = false;
|
||
}
|
||
return valid;
|
||
}
|
||
|
||
function validateAndSave() {
|
||
console.log('validateAndSave called');
|
||
if (validateAddForm()) {
|
||
console.log('Form valid, showing confirm');
|
||
showConfirm('Simpan data penduduk baru ini?', savePoorData);
|
||
} else {
|
||
console.log('Form invalid');
|
||
}
|
||
}
|
||
|
||
function editPoor(id) { const data = POOR_RESIDENTS.find(p => p.id == id); if (data) openEditModal(data); }
|
||
|
||
function confirmDeletePoor(id) {
|
||
showConfirm('Hapus data ini? Tindakan tidak dapat dibatalkan.', function () {
|
||
deletePoor(id);
|
||
});
|
||
}
|
||
|
||
function resetAddForm() { APP_STATE.pickedLat = APP_STATE.pickedLng = null; if (APP_STATE.pickupMarker) { APP_STATE.map.removeLayer(APP_STATE.pickupMarker); APP_STATE.pickupMarker = null; } refreshSidebar(); showToast('Form direset', 'info'); }
|
||
function savePoorData() {
|
||
console.log('savePoorData executed');
|
||
if (!APP_STATE.isAdmin) return;
|
||
const name = document.getElementById('f_name')?.value.trim();
|
||
if (!name) { showToast('Nama harus diisi!', 'error'); return; }
|
||
if (!APP_STATE.pickedLat) { showToast('Pilih lokasi!', 'error'); return; }
|
||
|
||
const fd = new FormData();
|
||
fd.append('name', name);
|
||
fd.append('nik', document.getElementById('f_nik')?.value || '');
|
||
fd.append('family_size', document.getElementById('f_family')?.value || 1);
|
||
fd.append('income', document.getElementById('f_income')?.value || 0);
|
||
fd.append('rt_rw', document.getElementById('f_rtrw')?.value || '-');
|
||
fd.append('lat', APP_STATE.pickedLat);
|
||
fd.append('lng', APP_STATE.pickedLng);
|
||
|
||
console.log('Sending fetch...');
|
||
fetch('save_poor.php', { method: 'POST', body: fd })
|
||
.then(response => {
|
||
console.log('Response:', response);
|
||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||
showToast('✅ Data berhasil disimpan!', 'success');
|
||
location.reload();
|
||
})
|
||
.catch(err => {
|
||
showToast('Error: ' + err.message, 'error');
|
||
console.error('Save error:', err);
|
||
});
|
||
}
|
||
function deletePoor(id) {
|
||
if (!APP_STATE.isAdmin) return;
|
||
fetch('delete_poor.php', { method: 'POST', body: new URLSearchParams({ id }) })
|
||
.then(response => {
|
||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||
showToast('✅ Data berhasil dihapus!', 'success');
|
||
location.reload();
|
||
})
|
||
.catch(err => {
|
||
showToast('Error: ' + err.message, 'error');
|
||
console.error('Delete error:', err);
|
||
});
|
||
}
|
||
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') { if (APP_STATE.pickingMode) cancelPicking(); closeEditModal(); closeConfirmModal(); closeFullTableModal(); } });
|
||
initMap();
|
||
if (APP_STATE.isAdmin) showToast('Selamat datang, Admin!', 'success');
|
||
else showToast('Mode pengunjung — <a href="login.php" style="color:white;text-decoration:underline;">Login</a>', 'info');
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|