Files
pontianak-poverty-webgis/index.php
T

2379 lines
90 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }; 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>