Files
2026-06-08 17:18:31 +07:00

7483 lines
540 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SiPetaSos ΓÇö Sistem Informasi Peta Sosial</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap"
rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #f8fafc;
--panel: #ffffff;
--panel2: #f1f5f9;
--border: #e2e8f0;
--navy: #0f172a;
--text: #1e293b;
--muted: #64748b;
--muted2: #94a3b8;
--green: #10b981;
--green-light: #d1fae5;
--red: #ef4444;
--red-light: #fee2e2;
--amber: #f59e0b;
--blue: #3b82f6;
--blue-light: #dbeafe;
--teal: #14b8a6;
--teal-light: #ccfbf1;
--purple: #8b5cf6;
--purple-light: #ede9fe;
}
body {
font-family: 'Inter', sans-serif;
background: var(--bg);
color: var(--text);
display: flex;
height: 100vh;
overflow: hidden;
}
@keyframes riseIn {
from {
opacity: 0;
transform: translateY(12px)
}
to {
opacity: 1;
transform: translateY(0)
}
}
@keyframes pulseGlow {
0% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
}
70% {
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
}
}
@keyframes popupIn {
from {
opacity: 0;
transform: translateY(10px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
/* ΓöÇΓöÇ SIDEBAR ΓöÇΓöÇ */
#sidebar {
width: 360px;
min-width: 360px;
background: #ffffff;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
box-shadow: 0 0 30px rgba(0, 0, 0, 0.05);
z-index: 1000;
overflow: hidden;
}
.brand {
padding: 18px 20px 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.brand-row {
display: flex;
align-items: center;
gap: 10px;
}
.brand-icon {
width: 36px;
height: 36px;
background: var(--blue);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 16px;
animation: pulseGlow 2.5s ease-out infinite;
flex-shrink: 0;
}
.brand-title {
font-family: 'Space Grotesk', sans-serif;
font-size: 18px;
font-weight: 700;
color: var(--navy);
letter-spacing: -0.3px;
line-height: 1.1;
}
.brand-sub {
font-size: 10px;
color: var(--muted);
font-weight: 500;
display: block;
margin-top: 3px;
}
.sec {
padding: 16px 16px 10px;
flex-shrink: 0;
}
.sec-lbl {
font-size: 10px;
font-weight: 800;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
padding: 0 4px;
}
.mode-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.mbtn {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 14px 8px;
background: #fff;
border: 1.5px solid var(--border);
border-radius: 12px;
cursor: pointer;
color: var(--navy);
transition: all 0.2s;
font-size: 12px;
font-weight: 700;
text-decoration: none;
}
.mbtn:hover {
border-color: var(--blue);
background: var(--blue-light);
color: var(--blue);
transform: translateY(-1px);
}
.mbtn i {
font-size: 18px;
color: var(--muted);
transition: 0.2s;
}
.mbtn:hover i {
color: var(--blue);
}
.mbtn.active {
border-color: var(--blue);
background: var(--blue-light);
}
.mbtn.active i {
color: var(--blue);
}
.list-wrap {
flex: 1;
overflow-y: auto;
padding: 0 12px;
}
.list-wrap::-webkit-scrollbar {
width: 4px;
}
.list-wrap::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
}
.leg-sec {
padding: 0 20px 18px;
flex-shrink: 0;
}
.leg-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.leg-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 600;
color: var(--navy);
}
.leg-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
/* ΓöÇΓöÇ MAIN CONTAINER ΓöÇΓöÇ */
#main-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
background: var(--bg);
overflow: hidden;
}
/* ΓöÇΓöÇ TOPBAR ΓöÇΓöÇ */
.app-topbar {
height: 64px;
min-height: 64px;
background: #ffffff;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 0 20px;
z-index: 900;
flex-shrink: 0;
}
.public-brand {
display: flex;
align-items: center;
gap: 10px;
min-width: 200px;
}
.brand-icon-mini {
width: 30px;
height: 30px;
border-radius: 9px;
display: grid;
place-items: center;
background: var(--blue);
color: #fff;
font-size: 13px;
}
.public-brand-text {
display: flex;
flex-direction: column;
}
.public-brand-title {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
font-size: 15px;
color: var(--navy);
}
.public-brand-sub {
font-size: 10px;
color: var(--muted);
}
.public-only {
display: none !important;
}
body.public-mode .public-only {
display: flex !important;
}
body.public-mode .admin-only {
display: none !important;
}
.map-search {
flex: 1;
max-width: 380px;
display: flex;
align-items: center;
}
.map-search input {
width: 100%;
height: 38px;
border: 1.5px solid var(--border);
border-right: none;
border-radius: 10px 0 0 10px;
padding: 0 14px;
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.map-search input:focus {
border-color: var(--blue);
}
.map-search button {
height: 38px;
padding: 0 14px;
border: 1.5px solid var(--border);
border-left: none;
border-radius: 0 10px 10px 0;
background: #fff;
cursor: pointer;
color: var(--muted);
transition: all 0.2s;
}
.map-search button:hover {
background: var(--blue-light);
color: var(--blue);
border-color: var(--blue);
}
.top-actions {
display: flex;
align-items: center;
gap: 8px;
}
.top-btn {
height: 38px;
padding: 0 14px;
border: 1.5px solid var(--border);
background: #fff;
color: var(--navy);
border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 7px;
font-weight: 700;
font-size: 13px;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.top-btn:hover {
border-color: var(--blue);
background: var(--blue-light);
color: var(--blue);
}
.top-btn.primary {
background: var(--blue);
border-color: var(--blue);
color: #fff;
}
.top-btn.primary:hover {
background: #2563eb;
}
.top-btn.teal {
background: linear-gradient(135deg, #0f766e, #14b8a6);
border-color: transparent;
color: #fff;
}
.top-btn.blue {
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
border-color: transparent;
color: #fff;
}
.icon-btn {
width: 38px;
height: 38px;
border: 1.5px solid var(--border);
background: #fff;
color: var(--navy);
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
position: relative;
transition: all 0.2s;
}
.icon-btn:hover {
border-color: var(--blue);
background: var(--blue-light);
color: var(--blue);
}
.ib-badge {
position: absolute;
top: -6px;
right: -6px;
min-width: 16px;
height: 16px;
border-radius: 999px;
padding: 0 4px;
background: var(--red);
color: #fff;
font-size: 10px;
font-weight: 800;
display: grid;
place-items: center;
}
.tb-divider {
width: 1px;
height: 24px;
background: var(--border);
}
.user-avatar-btn {
height: 38px;
padding: 0 12px;
border: 1.5px solid var(--border);
background: #fff;
color: var(--navy);
border-radius: 10px;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.user-avatar-btn:hover {
border-color: var(--blue);
}
.user-avatar {
width: 26px;
height: 26px;
border-radius: 8px;
display: grid;
place-items: center;
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
color: #fff;
font-size: 11px;
font-weight: 800;
}
.user-caret {
font-size: 10px;
color: var(--muted);
}
.add-menu,
.user-menu {
position: relative;
}
.add-dropdown,
.user-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 220px;
background: #fff;
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 18px 50px rgba(15, 23, 42, .14);
padding: 8px;
display: none;
z-index: 1500;
animation: riseIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.add-menu.open .add-dropdown,
.user-menu.open .user-dropdown {
display: block;
}
.add-dropdown-head {
padding: 8px 10px;
color: var(--muted);
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
}
.add-item,
.ud-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-radius: 9px;
color: var(--navy);
text-decoration: none;
cursor: pointer;
font-size: 13px;
font-weight: 700;
transition: background 0.15s;
}
.add-item:hover,
.ud-item:hover {
background: var(--panel2);
}
.add-item small {
display: block;
color: var(--muted);
font-weight: 500;
margin-top: 2px;
}
.ai-icon {
width: 32px;
height: 32px;
border-radius: 9px;
display: grid;
place-items: center;
flex: 0 0 auto;
}
.ud-header {
padding: 10px;
}
.ud-name {
font-weight: 800;
color: var(--navy);
font-size: 14px;
}
.ud-role {
color: var(--muted);
font-size: 12px;
margin-top: 2px;
}
.ud-sep {
height: 1px;
background: var(--border);
margin: 4px 0;
}
.ud-item.danger {
color: #b91c1c;
}
/* ΓöÇΓöÇ PUBLIC BANNER ΓöÇΓöÇ */
#public-banner {
display: none;
align-items: center;
gap: 10px;
background: linear-gradient(90deg, #1e3a8a, #1d4ed8);
color: #fff;
font-size: 13px;
font-weight: 600;
padding: 10px 20px;
z-index: 910;
flex-shrink: 0;
}
#public-banner.show {
display: flex;
}
#public-banner a {
color: #93c5fd;
font-weight: 800;
}
body.public-mode #sidebar {
display: none;
}
/* ΓöÇΓöÇ MAP WRAPPER ΓöÇΓöÇ */
#map-wrapper {
flex: 1;
position: relative;
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
z-index: 1;
background: #e2e8f0;
}
/* ΓöÇΓöÇ STATS STRIP ΓöÇΓöÇ */
.stats-strip {
position: absolute;
top: 14px;
left: 14px;
display: flex;
gap: 8px;
flex-wrap: wrap;
z-index: 500;
pointer-events: none;
}
.stat-pill {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, .94);
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px 12px;
box-shadow: 0 4px 18px rgba(15, 23, 42, .08);
pointer-events: auto;
}
.stat-pill i {
width: 30px;
height: 30px;
border-radius: 8px;
display: grid;
place-items: center;
color: #fff;
font-size: 13px;
}
.stat-pill strong {
display: block;
color: var(--navy);
font-size: 15px;
line-height: 1;
}
.stat-pill small {
display: block;
color: var(--muted);
font-size: 10px;
margin-top: 2px;
}
.map-chip {
position: absolute;
left: 14px;
bottom: 16px;
z-index: 500;
border: 1px solid var(--border);
background: #fff;
color: var(--navy);
border-radius: 999px;
padding: 8px 14px;
display: inline-flex;
align-items: center;
gap: 8px;
font-weight: 700;
font-size: 13px;
cursor: pointer;
box-shadow: 0 4px 18px rgba(15, 23, 42, .1);
transition: all 0.2s;
}
.map-chip:hover {
box-shadow: 0 8px 24px rgba(15, 23, 42, .15);
transform: translateY(-1px);
}
/* ΓöÇΓöÇ SIDE PANEL ΓöÇΓöÇ */
.side-panel,
.locked-panel {
position: absolute;
top: 14px;
right: 14px;
bottom: 14px;
width: min(360px, calc(100% - 28px));
background: #fff;
border: 1px solid var(--border);
border-radius: 14px;
box-shadow: 0 18px 50px rgba(15, 23, 42, .14);
z-index: 700;
display: none;
overflow: hidden;
}
.side-panel.show,
.side-panel.open {
display: flex;
flex-direction: column;
}
.locked-panel.show {
display: block;
}
.panel-head {
padding: 16px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.panel-head h3 {
margin: 0;
display: flex;
align-items: center;
justify-content: space-between;
color: var(--navy);
font-size: 16px;
font-weight: 800;
}
.panel-head p {
margin: 4px 0 0;
color: var(--muted);
font-size: 12px;
}
.close-panel-btn {
border: 0;
background: var(--panel2);
width: 28px;
height: 28px;
border-radius: 8px;
cursor: pointer;
color: var(--navy);
display: grid;
place-items: center;
}
.close-panel-btn:hover {
background: var(--red-light);
color: var(--red);
}
.panel-list {
padding: 12px;
overflow-y: auto;
flex: 1;
}
.panel-list::-webkit-scrollbar {
width: 4px;
}
.panel-list::-webkit-scrollbar-thumb {
background: #e2e8f0;
border-radius: 4px;
}
/* ΓöÇΓöÇ LOCKED PANEL ΓöÇΓöÇ */
.lp-header {
padding: 24px 20px;
text-align: center;
background: linear-gradient(140deg, #1e3a8a 0%, #2563eb 60%, #3b82f6 100%);
color: #fff;
}
.lp-icon-wrap {
width: 48px;
height: 48px;
margin: 0 auto 12px;
border-radius: 14px;
display: grid;
place-items: center;
background: rgba(255, 255, 255, .18);
font-size: 20px;
}
.lp-divider {
height: 1px;
background: var(--border);
}
.lp-title {
font-size: 20px;
font-weight: 800;
}
.lp-header-sub {
font-size: 12px;
opacity: .82;
margin-top: 4px;
}
.lp-body {
padding: 18px;
overflow-y: auto;
}
.lp-desc {
color: var(--muted);
line-height: 1.55;
margin: 0 0 14px;
font-size: 13px;
}
.lp-features {
padding: 0;
margin: 0 0 16px;
list-style: none;
display: grid;
gap: 10px;
}
.lp-features li {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
color: var(--navy);
font-size: 13px;
}
.feat-icon {
width: 32px;
height: 32px;
border-radius: 9px;
display: grid;
place-items: center;
flex-shrink: 0;
}
.lp-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-radius: 12px;
background: var(--navy);
color: #fff;
text-decoration: none;
padding: 12px 16px;
font-weight: 800;
font-size: 14px;
transition: all 0.2s;
}
.lp-btn:hover {
background: #1e293b;
transform: translateY(-1px);
}
.lp-note {
color: var(--muted);
font-size: 12px;
text-align: center;
margin-top: 10px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.lp-note i {
color: var(--green);
}
/* ΓöÇΓöÇ KK CARD (SIDE PANEL) ΓöÇΓöÇ */
.kk-card {
background: #fff;
border: 1.5px solid var(--border);
border-radius: 12px;
padding: 14px;
margin-bottom: 10px;
transition: all 0.2s;
animation: riseIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.kk-card:hover {
border-color: var(--blue);
box-shadow: 0 6px 20px rgba(59, 130, 246, .12);
transform: translateY(-1px);
}
.kk-top {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
}
.kk-avatar {
width: 38px;
height: 38px;
border-radius: 10px;
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.kk-name {
font-weight: 800;
font-size: 14px;
color: var(--navy);
line-height: 1.2;
}
.kk-sub {
font-size: 11px;
color: var(--muted);
margin-top: 2px;
}
.kk-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed var(--border);
}
.kk-row:last-of-type {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.kk-row span {
color: var(--muted);
font-weight: 600;
}
.kk-row strong {
color: var(--navy);
font-weight: 700;
text-align: right;
}
.kk-badge {
font-size: 10px;
font-weight: 800;
padding: 3px 8px;
border-radius: 6px;
}
.badge-extreme {
background: var(--red-light);
color: #991b1b;
}
.badge-poor {
background: #ffedd5;
color: #9a3412;
}
.badge-vulnerable {
background: #fef3c7;
color: #92400e;
}
.badge-handled {
background: var(--green-light);
color: #065f46;
}
.badge-pending {
background: #f1f5f9;
color: var(--muted);
}
.blank-warning {
color: #dc2626 !important;
font-weight: 800 !important;
background: var(--red-light);
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
margin-top: 6px;
border-bottom: none;
}
.kk-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.kk-actions select {
flex: 1;
padding: 6px 10px;
border: 1.5px solid var(--border);
border-radius: 8px;
font: 600 11px 'Inter', sans-serif;
color: var(--navy);
outline: none;
background: #fff;
cursor: pointer;
transition: 0.2s;
}
.kk-actions select:focus {
border-color: var(--blue);
}
.mini-btn {
width: 32px;
height: 32px;
border: 1.5px solid var(--border);
border-radius: 8px;
background: #fff;
color: var(--muted);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: 0.2s;
}
.mini-btn:hover {
border-color: var(--blue);
background: var(--blue-light);
color: var(--blue);
}
/* ΓöÇΓöÇ CARD ΓöÇΓöÇ */
.card {
background: #fff;
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 14px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
animation: riseIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.card:hover {
border-color: var(--blue);
box-shadow: 0 4px 16px rgba(59, 130, 246, .1);
transform: translateY(-1px);
}
.card.active {
border-color: var(--blue);
box-shadow: 0 4px 16px rgba(59, 130, 246, .15);
}
.card-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 4px;
}
.card-name {
font-weight: 800;
font-size: 13px;
color: var(--navy);
}
.card-sub {
font-size: 11px;
color: var(--muted);
margin-top: 2px;
}
.card-badge {
font-size: 10px;
font-weight: 700;
padding: 3px 8px;
border-radius: 6px;
flex-shrink: 0;
}
/* ΓöÇΓöÇ LEAFLET OVERRIDES ΓöÇΓöÇ */
.leaflet-control-zoom a {
background: #fff !important;
color: var(--navy) !important;
border: 1px solid var(--border) !important;
font-weight: 700 !important;
}
.leaflet-bar {
box-shadow: 0 4px 16px rgba(0, 0, 0, .08);
border: none !important;
border-radius: 10px !important;
overflow: hidden;
}
.leaflet-control-attribution {
font-size: 10px !important;
background: rgba(255, 255, 255, .8) !important;
border-top-left-radius: 6px;
}
.leaflet-popup {
animation: popupIn 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}
.leaflet-control-layers-base {
display: none !important;
}
.leaflet-control-layers-separator {
display: none !important;
}
.leaflet-control-layers {
border: 1px solid var(--border) !important;
border-radius: 14px !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, .08) !important;
overflow: hidden;
}
.leaflet-control-layers-expanded {
padding: 16px !important;
background: rgba(255, 255, 255, .97) !important;
}
.leaflet-control-layers-overlays {
display: flex;
flex-direction: column;
gap: 6px;
}
.leaflet-control-layers label {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: var(--navy);
font-weight: 700;
margin: 0;
padding: 8px 10px;
background: #f8fafc;
border-radius: 9px;
cursor: pointer;
transition: all 0.2s;
}
.leaflet-control-layers label:hover {
background: var(--blue-light);
}
.leaflet-control-layers-overlays input[type=checkbox] {
width: 16px;
height: 16px;
cursor: pointer;
}
.lc-group-title {
color: var(--muted);
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .5px;
margin: 0 0 6px;
padding: 0 2px;
}
.lc-item {
display: inline-flex;
align-items: center;
gap: 8px;
}
.lc-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.lc-dot-kemiskinan {
background: var(--amber);
}
.lc-dot-masjid {
background: var(--teal);
}
.lc-text strong {
display: block;
font-size: 12px;
}
.lc-text small {
display: block;
color: var(--muted);
font-size: 10px;
}
/* ΓöÇΓöÇ HINT ΓöÇΓöÇ */
#hint {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(15, 23, 42, .9);
color: #fff;
padding: 10px 18px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
z-index: 2000;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
white-space: nowrap;
}
#hint.show {
opacity: 1;
}
/* ΓöÇΓöÇ MODERN MARKER ΓöÇΓöÇ */
.modern-marker {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.modern-marker .pulse {
position: absolute;
width: 100%;
height: 100%;
background: var(--marker-color);
border-radius: 50%;
opacity: 0.4;
animation: markerPulse 2s infinite ease-out;
}
.modern-marker .pin-body {
position: relative;
width: 32px;
height: 32px;
background: var(--marker-color);
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
z-index: 2;
border: 2px solid #fff;
}
.modern-marker .icon-container {
transform: rotate(45deg);
color: #fff;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
}
.modern-marker .pin-label {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.95);
color: var(--navy);
padding: 3px 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
pointer-events: none;
opacity: 0;
transition: opacity 0.2s, top 0.2s;
border: 1px solid var(--border);
z-index: 3;
}
.modern-marker:hover .pin-label {
opacity: 1;
top: -30px;
}
.path-glow { filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.35)); }
.parsil-glow { filter: drop-shadow(0px 2px 6px rgba(0,0,0,0.25)); }
@keyframes markerPulse {
0% {
transform: scale(0.8);
opacity: 0.6;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
/* ΓöÇΓöÇ POPUP ΓöÇΓöÇ */
.km-popup {
min-width: 260px;
font: 13px 'Inter', sans-serif;
}
.km-title {
font-size: 15px;
font-weight: 800;
color: var(--navy);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.km-grid {
display: grid;
gap: 6px;
margin-bottom: 10px;
}
.km-row {
display: flex;
gap: 6px;
}
.km-label {
width: 110px;
font-weight: 700;
color: var(--muted);
font-size: 11px;
flex-shrink: 0;
}
.km-val {
flex: 1;
font-weight: 600;
color: var(--navy);
font-size: 12px;
}
.km-field {
margin-bottom: 10px;
}
.km-field label {
display: block;
font-weight: 700;
font-size: 11px;
color: var(--muted);
margin-bottom: 4px;
}
.km-field select {
width: 100%;
padding: 7px 10px;
border: 1.5px solid var(--border);
border-radius: 8px;
font: 600 12px 'Inter', sans-serif;
outline: none;
}
.km-field select:focus {
border-color: var(--blue);
}
.km-actions {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.pbtn {
flex: 1;
min-width: 60px;
padding: 8px 8px;
border: none;
border-radius: 8px;
font: 700 11px 'Inter', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
transition: all 0.2s;
}
.pbtn-g {
background: var(--green-light);
color: #065f46;
}
.pbtn-g:hover {
background: var(--green);
color: #fff;
}
.pbtn-r {
background: var(--red-light);
color: #991b1b;
}
.pbtn-r:hover {
background: var(--red);
color: #fff;
}
.pbtn-b {
background: var(--blue-light);
color: #1e40af;
}
.pbtn-b:hover {
background: var(--blue);
color: #fff;
}
.pbtn-y {
background: #fef3c7;
color: #92400e;
}
.pbtn-y:hover {
background: var(--amber);
color: #fff;
}
.kk-popup-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5px;
padding: 10px 12px 12px;
}
.kk-popup-actions .pbtn {
flex: unset;
min-width: unset;
padding: 7px 4px;
font-size: 11px;
border-radius: 7px;
}
.kk-popup-actions .pbtn:last-child:nth-child(3n+1) {
grid-column: 1 / -1;
}
/* ΓöÇΓöÇ LEAFLET POPUP WRAPPER ΓöÇΓöÇ */
.leaflet-popup-content-wrapper {
padding: 0 !important;
border-radius: 14px !important;
overflow: hidden !important;
box-shadow: 0 20px 60px rgba(15,23,42,.18) !important;
border: 1px solid var(--border) !important;
}
.leaflet-popup-content {
margin: 0 !important;
width: auto !important;
min-width: 260px !important;
}
.leaflet-popup-tip-container { margin-top: -1px; }
/* Sembunyikan X default Leaflet ΓÇö semua popup pakai custom close */
.leaflet-popup-close-button {
display: none !important;
}
/* ΓöÇΓöÇ POPUP INFO (.pi) ΓöÇΓöÇ */
.pi {
font-family: 'Inter', sans-serif;
min-width: 260px;
max-width: 310px;
}
.pi-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px 12px;
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
color: #fff;
}
.pi-header-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(255,255,255,.18);
display: grid;
place-items: center;
font-size: 15px;
flex-shrink: 0;
}
.pi-header-info { flex: 1; min-width: 0; }
.pi-header-name {
font-weight: 800;
font-size: 14px;
line-height: 1.2;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pi-header-sub {
font-size: 11px;
opacity: .8;
margin-top: 2px;
}
.pi-close {
width: 26px;
height: 26px;
border: none;
border-radius: 7px;
background: rgba(255,255,255,.18);
color: #fff;
cursor: pointer;
display: grid;
place-items: center;
font-size: 12px;
transition: background .15s;
flex-shrink: 0;
margin-left: auto;
}
.pi-close:hover { background: rgba(255,255,255,.32); }
.pi-close-alt {
width: 26px;
height: 26px;
border: none;
border-radius: 7px;
background: #f1f5f9;
color: var(--muted);
cursor: pointer;
display: grid;
place-items: center;
font-size: 12px;
transition: background .15s;
flex-shrink: 0;
margin-left: auto;
}
.pi-close-alt:hover { background: #e2e8f0; color: var(--red); }
.pi-body {
padding: 10px 14px 6px;
display: flex;
flex-direction: column;
gap: 4px;
}
.pi-row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
padding: 4px 0;
border-bottom: 1px dashed #f1f5f9;
font-size: 12px;
}
.pi-row:last-child { border-bottom: none; }
.pi-k {
color: var(--muted);
font-weight: 600;
flex-shrink: 0;
}
.pi-v {
font-weight: 700;
color: var(--navy);
text-align: right;
word-break: break-word;
}
.pi-badge {
display: inline-block;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 999px;
letter-spacing: .2px;
}
.pi-badge.extreme { background:#fee2e2; color:#991b1b; }
.pi-badge.poor { background:#ffedd5; color:#9a3412; }
.pi-badge.vulnerable { background:#fef3c7; color:#92400e; }
/* ΓöÇΓöÇ ACTION ROW ΓöÇΓöÇ */
.act-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
padding: 10px 12px 12px;
border-top: 1px solid #f1f5f9;
}
/* ΓöÇΓöÇ MODAL FORM (.kk-modal) ΓöÇΓöÇ */
.kk-modal {
font-family: 'Inter', sans-serif;
min-width: 290px;
max-width: 360px;
}
.km-head {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px 12px;
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
color: #fff;
}
.km-icon {
width: 32px;
height: 32px;
border-radius: 9px;
background: rgba(255,255,255,.2);
display: grid;
place-items: center;
font-size: 14px;
flex-shrink: 0;
}
.km-title {
font-weight: 800;
font-size: 14px;
flex: 1;
line-height: 1.2;
}
.km-close {
width: 26px;
height: 26px;
border: none;
border-radius: 7px;
background: rgba(255,255,255,.18);
color: #fff;
cursor: pointer;
display: grid;
place-items: center;
font-size: 12px;
transition: background .15s;
flex-shrink: 0;
}
.km-close:hover { background: rgba(255,255,255,.3); }
.km-body {
padding: 14px;
max-height: 60vh;
overflow-y: auto;
}
.km-body::-webkit-scrollbar { width: 4px; }
.km-body::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 4px; }
.km-section {
font-size: 10px;
font-weight: 800;
color: var(--blue);
text-transform: uppercase;
letter-spacing: 1px;
margin: 12px 0 8px;
padding-bottom: 6px;
border-bottom: 1.5px solid #dbeafe;
}
.km-section:first-child { margin-top: 0; }
.km-field {
margin-bottom: 10px;
}
.km-field label {
display: block;
font-weight: 700;
font-size: 11px;
color: var(--muted);
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: .4px;
}
.km-field input,
.km-field textarea,
.km-field select {
width: 100%;
padding: 8px 11px;
border: 1.5px solid var(--border);
border-radius: 9px;
font: 600 12px 'Inter', sans-serif;
color: var(--navy);
outline: none;
background: #fff;
transition: border-color .2s, box-shadow .2s;
}
.km-field textarea {
resize: vertical;
min-height: 56px;
line-height: 1.5;
}
.km-field input:focus,
.km-field textarea:focus,
.km-field select:focus {
border-color: var(--blue);
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
}
.km-field input::placeholder,
.km-field textarea::placeholder { color: #94a3b8; font-weight: 500; }
.km-gps-box {
border-radius: 10px;
overflow: hidden;
margin-bottom: 10px;
background: linear-gradient(135deg, #1e3a8a, #2563eb);
}
.km-gps-row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
flex-wrap: wrap;
}
.km-gps-row span { color: rgba(255,255,255,.7); font-size: 11px; font-weight: 600; }
.km-gps-row strong { color: #fff; font-size: 13px; font-weight: 700; }
.km-gps-row i { color: rgba(255,255,255,.8); width: 16px; text-align: center; }
.km-btn {
padding: 9px 16px;
border: none;
border-radius: 9px;
font: 700 12px 'Inter', sans-serif;
cursor: pointer;
transition: all .18s;
}
.km-btn-save {
background: var(--blue);
color: #fff;
flex: 1;
}
.km-btn-save:hover { background: #1d4ed8; transform: translateY(-1px); }
.km-btn-cancel {
background: var(--panel2);
color: var(--navy);
}
.km-btn-cancel:hover { background: #e2e8f0; }
.km-foot {
display: flex;
gap: 8px;
padding: 10px 14px 14px;
border-top: 1px solid var(--border);
}
.req { color: var(--red); margin-left: 2px; }
.km-hint {
color: var(--muted);
font-weight: 500;
font-size: 10px;
margin-left: 4px;
}
.hint-txt {
font-size: 10px;
color: var(--muted);
margin-top: 6px;
font-style: italic;
}
.km-vuln-bar {
background: #f1f5f9;
border-radius: 8px;
padding: 10px 12px;
margin-top: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.km-vuln-score {
font-size: 22px;
font-weight: 900;
color: var(--navy);
min-width: 36px;
text-align: center;
}
.km-vuln-label {
font-size: 11px;
color: var(--muted);
font-weight: 600;
}
.km-vuln-cat {
font-weight: 800;
font-size: 13px;
}
.km-checkbox-row {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
cursor: pointer;
user-select: none;
}
.km-checkbox-row input[type=checkbox] {
width: 15px;
height: 15px;
cursor: pointer;
accent-color: var(--blue);
}
.km-checkbox-row span {
font-size: 12px;
font-weight: 600;
color: var(--navy);
}
/* ΓöÇΓöÇ FORM EXTRAS ΓöÇΓöÇ */
.km-checks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
margin-top: 4px;
}
.km-check {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 8px;
border: 1.5px solid var(--border);
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
color: var(--navy);
transition: all .15s;
user-select: none;
background: #fff;
}
.km-check:hover { border-color: var(--blue); background: #eff6ff; }
.km-check input[type=checkbox] {
width: 14px;
height: 14px;
accent-color: var(--blue);
cursor: pointer;
flex-shrink: 0;
}
.km-gps-item {
display: flex;
align-items: center;
gap: 6px;
}
.km-gps-item span { color: rgba(255,255,255,.7); font-size: 10px; font-weight: 600; }
.km-gps-item strong { color: #fff; font-size: 12px; font-weight: 700; font-family: monospace; }
.km-gps-item i { color: rgba(255,255,255,.8); }
/* ΓöÇΓöÇ VULNERABILITY BOX ΓöÇΓöÇ */
.km-vuln-box {
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
border: 1.5px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 6px 0 10px;
}
.kvb-left { display: flex; flex-direction: column; gap: 3px; }
.kvb-label {
font-size: 10px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .5px;
}
.kvb-cat {
font-size: 13px;
font-weight: 800;
color: var(--navy);
}
.kvb-score {
font-size: 28px;
font-weight: 900;
color: var(--blue);
line-height: 1;
}
/* ΓöÇΓöÇ BLANK SPOT POPUP ΓöÇΓöÇ */
.pi h4 {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 800;
color: var(--navy);
padding: 14px 14px 8px;
margin: 0;
border-bottom: 1px solid #f1f5f9;
}
/* ΓöÇΓöÇ MASJID FORM GPS MAP ΓöÇΓöÇ */
#m-map, #e-m-map, #k-map, #e-k-map {
height: 110px;
border-radius: 8px;
overflow: hidden;
background: #e2e8f0;
}
/* ΓöÇΓöÇ GPS BOX INNER TEXT ΓöÇΓöÇ */
.km-addr-row {
display: flex;
align-items: flex-start;
gap: 7px;
padding: 8px 12px 10px;
border-top: 1px solid rgba(255,255,255,.12);
}
.km-addr-row span,
.km-addr-row * {
color: #ffffff !important;
font-size: 12px;
font-weight: 500;
line-height: 1.45;
}
.km-gps-lbl {
font-size: 10px !important;
font-weight: 700 !important;
color: rgba(255,255,255,.65) !important;
letter-spacing: .8px;
text-transform: uppercase;
}
.km-gps-val {
font-size: 13px !important;
font-weight: 800 !important;
color: #ffffff !important;
font-family: 'Courier New', monospace;
}
.km-gps-row .km-gps-item {
display: flex;
flex-direction: column;
gap: 1px;
}
/* ΓöÇΓöÇ DRAWING CONTROLS ΓöÇΓöÇ */
.coord-row {
display: flex;
gap: 8px;
margin-top: 8px;
}
.coord-row input {
flex: 1;
}
.lm-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .45);
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
}
.lm-box {
background: #fff;
border-radius: 18px;
width: 460px;
max-width: 94vw;
max-height: 88vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 60px rgba(0, 0, 0, .18);
overflow: hidden;
animation: riseIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.lm-head {
display: flex;
align-items: center;
gap: 12px;
padding: 18px 20px;
border-bottom: 1px solid var(--border);
}
.lm-head-icon {
width: 38px;
height: 38px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 16px;
flex-shrink: 0;
}
.lm-head-title {
font-size: 16px;
font-weight: 800;
color: var(--navy);
flex: 1;
}
.lm-head-close {
width: 30px;
height: 30px;
border: 0;
background: var(--panel2);
border-radius: 9px;
cursor: pointer;
font-size: 14px;
display: grid;
place-items: center;
}
.lm-head-close:hover {
background: var(--red-light);
color: var(--red);
}
.lm-body {
flex: 1;
overflow-y: auto;
padding: 18px 20px;
}
.lm-field {
margin-bottom: 14px;
}
.lm-field label {
display: block;
font-weight: 700;
font-size: 12px;
color: var(--navy);
margin-bottom: 6px;
}
.lm-field input,
.lm-field select,
.lm-field textarea {
width: 100%;
padding: 10px 12px;
border: 1.5px solid var(--border);
border-radius: 10px;
font: 500 13px 'Inter', sans-serif;
color: var(--navy);
outline: none;
transition: border-color 0.2s;
background: var(--bg);
}
.lm-field input:focus,
.lm-field select:focus,
.lm-field textarea:focus {
border-color: var(--blue);
background: #fff;
}
.lm-field textarea {
min-height: 80px;
resize: vertical;
}
.lm-submit {
width: 100%;
padding: 12px;
border: none;
border-radius: 12px;
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
color: #fff;
font: 700 14px 'Inter', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.lm-submit:hover {
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, .3);
}
.lm-report-card {
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px 14px;
margin-bottom: 10px;
background: #fff;
}
.lm-rc-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
}
.lm-rc-name {
font-size: 13px;
font-weight: 800;
color: var(--navy);
}
.lm-rc-time {
font-size: 11px;
color: var(--muted);
}
.lm-rc-desc {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
margin-bottom: 8px;
}
.lm-rc-footer {
display: flex;
align-items: center;
justify-content: space-between;
}
.lm-status {
padding: 3px 10px;
border-radius: 6px;
font-size: 11px;
font-weight: 800;
}
.lm-status.menunggu {
background: #fef3c7;
color: #d97706;
}
.lm-status.diproses {
background: var(--blue-light);
color: #1d4ed8;
}
.lm-status.selesai {
background: var(--green-light);
color: #065f46;
}
.lm-rc-actions {
display: flex;
gap: 6px;
}
.lm-rc-btn {
border: none;
border-radius: 6px;
padding: 4px 10px;
font: 700 11px 'Inter', sans-serif;
cursor: pointer;
}
.lm-rc-btn.selesai-btn {
background: var(--green-light);
color: #065f46;
}
.lm-rc-btn.selesai-btn:hover {
background: var(--green);
color: #fff;
}
.lm-rc-btn.hapus-btn {
background: var(--red-light);
color: #991b1b;
}
.lm-rc-btn.hapus-btn:hover {
background: var(--red);
color: #fff;
}
/* ΓöÇΓöÇ HISTORI BANTUAN MODAL ΓöÇΓöÇ */
.hb-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, .55);
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
animation: riseIn .2s ease;
}
.hb-box {
background: #fff;
border-radius: 18px;
width: 460px;
max-width: 95vw;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 60px rgba(0, 0, 0, .18);
overflow: hidden;
}
.hb-head {
display: flex;
align-items: center;
gap: 10px;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
background: linear-gradient(135deg, #1e3a8a, #2563eb);
color: #fff;
}
.hb-head-icon {
width: 36px;
height: 36px;
border-radius: 12px;
background: rgba(255, 255, 255, .18);
display: grid;
place-items: center;
font-size: 16px;
}
.hb-head-title {
font-size: 15px;
font-weight: 800;
flex: 1;
}
.hb-head-close {
width: 28px;
height: 28px;
border-radius: 8px;
border: none;
background: rgba(255, 255, 255, .18);
color: #fff;
cursor: pointer;
display: grid;
place-items: center;
font-size: 13px;
}
.hb-head-close:hover {
background: rgba(255, 255, 255, .32);
}
.hb-body {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.hb-body::-webkit-scrollbar {
width: 4px;
}
.hb-body::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
}
.hb-form {
background: var(--bg);
border: 1.5px solid var(--border);
border-radius: 14px;
padding: 14px;
margin-bottom: 16px;
}
.hb-form-title {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--blue);
margin-bottom: 10px;
}
.hb-form input,
.hb-form select,
.hb-form textarea {
width: 100%;
padding: 7px 10px;
border: 1.5px solid var(--border);
border-radius: 8px;
font: 500 12px 'Inter', sans-serif;
color: var(--navy);
background: #fff;
outline: none;
}
.hb-form input:focus,
.hb-form select:focus,
.hb-form textarea:focus {
border-color: var(--blue);
}
.hb-submit {
width: 100%;
padding: 9px;
border: none;
border-radius: 9px;
background: var(--blue);
color: #fff;
font: 700 12px 'Inter', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 8px;
}
.hb-submit:hover {
background: #1d4ed8;
}
.hb-list-title {
font-size: 11px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: .8px;
color: var(--muted);
margin-bottom: 10px;
}
.hb-item {
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px 14px;
margin-bottom: 8px;
background: #fff;
}
.hb-item-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
}
.hb-item-jenis {
font-size: 13px;
font-weight: 700;
color: var(--navy);
}
.hb-item-tgl {
font-size: 11px;
color: var(--muted);
font-weight: 600;
}
.hb-item-desc {
font-size: 12px;
color: var(--muted);
line-height: 1.5;
}
.hb-item-del {
border: none;
background: var(--red-light);
color: #991b1b;
border-radius: 6px;
padding: 4px 9px;
font: 700 11px 'Inter', sans-serif;
cursor: pointer;
margin-top: 8px;
}
.hb-item-del:hover {
background: var(--red);
color: #fff;
}
/* ΓöÇΓöÇ WALIKOTA BADGE ΓöÇΓöÇ */
.badge-walikota {
background: linear-gradient(135deg, #7c3aed, #a78bfa);
color: #fff;
font-size: 10px;
font-weight: 800;
padding: 3px 8px;
border-radius: 6px;
}
/* ΓöÇΓöÇ LAPORAN MASYARAKAT ΓöÇΓöÇ */
.lm-overlay {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, .6);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: riseIn .25s ease;
backdrop-filter: blur(4px);
}
.lm-box {
background: #fff;
border-radius: 18px;
width: 500px;
max-width: 95vw;
max-height: 90vh;
display: flex;
flex-direction: column;
box-shadow: 0 24px 60px rgba(0, 0, 0, .2);
overflow: hidden;
}
.lm-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
padding: 20px 24px;
border-bottom: 1px solid var(--border);
background: #fff;
}
.lm-head h3 {
margin: 0;
font-size: 18px;
font-weight: 800;
color: var(--navy);
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.lm-head h3 i {
color: var(--blue);
}
.lm-head p {
margin: 4px 0 0;
color: var(--muted);
font-size: 13px;
width: calc(100% - 40px);
}
.lm-close-btn {
position: absolute;
top: 20px;
right: 20px;
width: 32px;
height: 32px;
border: none;
background: var(--panel2);
color: var(--muted);
border-radius: 10px;
cursor: pointer;
display: grid;
place-items: center;
transition: 0.2s;
}
.lm-close-btn:hover {
background: var(--red-light);
color: var(--red);
}
.lm-body {
padding: 0;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
}
.lm-tabs {
display: flex;
padding: 0 20px;
border-bottom: 1px solid var(--border);
background: #f8fafc;
}
.lm-tab {
flex: 1;
padding: 14px;
text-align: center;
border: none;
background: transparent;
font: 600 13px 'Inter', sans-serif;
color: var(--muted);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.lm-tab:hover {
color: var(--navy);
}
.lm-tab.active {
color: var(--blue);
border-bottom-color: var(--blue);
background: #fff;
font-weight: 700;
}
.lm-section {
display: none;
padding: 24px;
}
.lm-section.active {
display: block;
}
.lm-field {
margin-bottom: 16px;
}
.lm-field label {
display: block;
font-weight: 700;
font-size: 12px;
color: var(--navy);
margin-bottom: 6px;
}
.lm-field input,
.lm-field textarea,
.lm-field select {
width: 100%;
padding: 10px 14px;
border: 1.5px solid var(--border);
border-radius: 10px;
font: 500 13px 'Inter', sans-serif;
color: var(--navy);
outline: none;
transition: border-color 0.2s;
background: #fff;
}
.lm-field input:focus,
.lm-field textarea:focus,
.lm-field select:focus {
border-color: var(--blue);
}
.lm-field textarea {
min-height: 100px;
resize: vertical;
}
.lm-submit {
width: 100%;
padding: 14px;
border: none;
border-radius: 12px;
background: var(--blue);
color: #fff;
font: 700 14px 'Inter', sans-serif;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: 0.2s;
margin-top: 10px;
}
.lm-submit:hover {
background: #2563eb;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(59, 130, 246, .2);
}
/* ΓöÇΓöÇ EXPORT MODAL ΓöÇΓöÇ */
.exp-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, .5);
z-index: 9000;
display: flex;
align-items: center;
justify-content: center;
animation: riseIn .25s ease;
}
.exp-box {
background: #fff;
border-radius: 18px;
width: 420px;
max-width: 95vw;
box-shadow: 0 24px 60px rgba(0, 0, 0, .2);
overflow: hidden;
animation: riseIn .25s ease;
}
.exp-head {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid var(--border);
background: var(--navy);
color: #fff;
}
.exp-head-icon {
width: 34px;
height: 34px;
border-radius: 10px;
background: rgba(255, 255, 255, .15);
display: grid;
place-items: center;
font-size: 15px;
}
.exp-head-title {
font-size: 15px;
font-weight: 800;
flex: 1;
}
.exp-head-close {
width: 28px;
height: 28px;
border-radius: 8px;
border: none;
background: rgba(255, 255, 255, .15);
color: #fff;
cursor: pointer;
display: grid;
place-items: center;
}
.exp-head-close:hover {
background: rgba(255, 255, 255, .28);
}
.exp-body {
padding: 20px;
display: grid;
gap: 12px;
}
.exp-row {
display: flex;
align-items: center;
gap: 14px;
padding: 14px;
background: var(--bg);
border: 1.5px solid var(--border);
border-radius: 14px;
cursor: pointer;
transition: all 0.2s;
}
.exp-row:hover {
border-color: var(--blue);
background: var(--blue-light);
}
.exp-row-icon {
width: 42px;
height: 42px;
border-radius: 12px;
display: grid;
place-items: center;
font-size: 18px;
flex-shrink: 0;
}
.exp-row-text strong {
display: block;
font-size: 14px;
font-weight: 800;
color: var(--navy);
}
.exp-row-text small {
color: var(--muted);
font-size: 12px;
}
/* ΓöÇΓöÇ BLANK SPOT ΓöÇΓöÇ */
.blank-spot-circle {
stroke: #64748b;
stroke-width: 1.5;
fill: rgba(100, 116, 139, 0.06);
stroke-dasharray: 6 4;
}
/* ΓöÇΓöÇ RESPONSIVE ΓöÇΓöÇ */
@media (max-width: 1024px) {
#sidebar {
width: 280px;
min-width: 280px;
}
}
@media (max-width: 760px) {
body {
flex-direction: column;
overflow-y: auto;
height: auto;
}
#sidebar {
width: 100%;
min-width: 0;
}
#main-container {
min-height: 75vh;
}
.stats-strip {
display: none;
}
}
/* ΓöÇΓöÇ CHANGE PASSWORD MODAL ΓöÇΓöÇ */
#cp-modal {
position: fixed;
inset: 0;
background: rgba(15,23,42,.55);
backdrop-filter: blur(4px);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
animation: riseIn .2s ease;
}
#cp-modal.hidden { display: none; }
.cp-box {
background: #fff;
border-radius: 20px;
width: 420px;
max-width: 95vw;
box-shadow: 0 28px 80px rgba(15,23,42,.25);
overflow: hidden;
animation: riseIn .25s cubic-bezier(.16,1,.3,1);
}
.cp-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 22px;
background: linear-gradient(135deg,#1e3a8a,#2563eb);
color: #fff;
}
.cp-head-left { display:flex; align-items:center; gap:10px; }
.cp-head-icon {
width: 34px; height: 34px; border-radius: 10px;
background: rgba(255,255,255,.15);
display: grid; place-items: center;
font-size: 15px;
}
.cp-head h3 { font-size:15px; font-weight:800; }
.cp-head p { font-size:11px; opacity:.7; margin-top:1px; }
.cp-close {
width:30px; height:30px; border-radius:8px;
background: rgba(255,255,255,.15);
border: none; color: #fff; cursor: pointer;
display: grid; place-items: center; font-size:14px;
transition: background .15s;
}
.cp-close:hover { background: rgba(255,255,255,.3); }
.cp-body { padding: 24px; }
.cp-field { margin-bottom: 16px; }
.cp-field label {
display:block; font-size:11px; font-weight:800;
text-transform:uppercase; letter-spacing:.5px;
color: #0f172a; margin-bottom:6px;
}
.cp-input-wrap { position: relative; }
.cp-input-wrap .cp-ico {
position:absolute; left:13px; top:50%; transform:translateY(-50%);
color: #94a3b8; font-size:13px; pointer-events:none;
}
.cp-input-wrap input {
width:100%; padding:12px 42px;
background:#f1f5f9; border:1.5px solid #e2e8f0;
border-radius:11px; font:500 14px 'Inter',sans-serif;
color:#1e293b; outline:none;
transition: border-color .2s, box-shadow .2s, background .2s;
}
.cp-input-wrap input::placeholder { color:#94a3b8; font-weight:400; }
.cp-input-wrap input:focus {
background:#fff; border-color:#3b82f6;
box-shadow:0 0 0 4px rgba(59,130,246,.1);
}
.cp-eye {
position:absolute; right:12px; top:50%; transform:translateY(-50%);
background:none; border:none; cursor:pointer;
color:#94a3b8; font-size:13px; padding:4px;
transition: color .2s;
}
.cp-eye:hover { color:#1e293b; }
.cp-strength { height:4px; border-radius:4px; background:#e2e8f0; margin-top:8px; overflow:hidden; }
.cp-strength-fill { height:100%; border-radius:4px; width:0; transition:width .3s, background .3s; }
.cp-strength-lbl { font-size:11px; color:#94a3b8; margin-top:4px; font-weight:600; }
.cp-alert {
display:none; align-items:center; gap:9px;
padding:12px 14px; border-radius:11px;
font-size:13px; font-weight:600; margin-bottom:16px;
}
.cp-alert.show { display:flex; }
.cp-alert.err { background:#fff1f2; border:1.5px solid #fecdd3; color:#be123c; }
.cp-alert.ok { background:#f0fdf4; border:1.5px solid #bbf7d0; color:#15803d; }
.cp-submit {
width:100%; padding:13px;
background: linear-gradient(135deg,#1d4ed8,#3b82f6);
border:none; border-radius:11px;
color:#fff; font:700 14px 'Inter',sans-serif;
cursor:pointer; display:flex; align-items:center;
justify-content:center; gap:8px;
box-shadow: 0 6px 18px rgba(37,99,235,.28);
transition: all .25s;
}
.cp-submit:hover { transform:translateY(-2px); box-shadow:0 12px 28px rgba(37,99,235,.38); }
.cp-submit:disabled { opacity:.6; transform:none; cursor:not-allowed; }
.cp-footer {
display:flex; align-items:center; justify-content:center;
gap:6px; margin-top:14px;
font-size:11px; color:#94a3b8;
}
.cp-footer i { color:#10b981; }
/* ΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉ
UI ENHANCEMENT ΓÇö SiPetaSos Premium Look
ΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉ */
/* ΓöÇΓöÇ STAT PILLS ΓÇö Glassmorphism upgrade ΓöÇΓöÇ */
.stat-pill {
background: rgba(255,255,255,.96) !important;
border: none !important;
border-radius: 14px !important;
padding: 9px 14px !important;
box-shadow: 0 4px 24px rgba(15,23,42,.1), 0 1px 4px rgba(15,23,42,.06) !important;
gap: 10px !important;
transition: transform .2s, box-shadow .2s;
}
.stat-pill:hover { transform: translateY(-2px); box-shadow: 0 8px 32px rgba(15,23,42,.14) !important; }
.stat-pill i {
width: 34px !important; height: 34px !important;
border-radius: 10px !important;
font-size: 14px !important;
}
.stat-pill strong { font-size: 17px !important; font-weight: 800 !important; }
.stat-pill small { font-size: 10px !important; font-weight: 600 !important; color: #94a3b8 !important; letter-spacing:.3px; }
/* ΓöÇΓöÇ HINT BAR ΓÇö Modern pill ΓöÇΓöÇ */
#hint {
background: linear-gradient(135deg, rgba(15,23,42,.92), rgba(30,58,138,.88)) !important;
backdrop-filter: blur(8px) !important;
border: 1px solid rgba(255,255,255,.12) !important;
padding: 12px 22px !important;
border-radius: 999px !important;
font-size: 13px !important;
font-weight: 700 !important;
letter-spacing: .2px;
box-shadow: 0 8px 32px rgba(15,23,42,.25) !important;
}
/* ΓöÇΓöÇ POPUP FORM .pf ΓÇö Complete redesign ΓöÇΓöÇ */
.pf {
font-family: 'Inter', sans-serif;
padding: 0;
min-width: 260px;
}
.pf h4 {
display: flex !important;
align-items: center !important;
gap: 9px !important;
font-size: 14px !important;
font-weight: 800 !important;
color: #fff !important;
padding: 13px 16px !important;
margin: 0 !important;
border-bottom: none !important;
border-radius: 0;
}
.pf h4 i { font-size: 15px !important; opacity: .9; }
/* Color the h4 based on icon color */
.pf h4:has(.fa-gas-pump) { background: linear-gradient(135deg,#064e3b,#059669); }
.pf h4:has(.fa-road) { background: linear-gradient(135deg,#1e3a8a,#2563eb); }
.pf h4:has(.fa-vector-square){ background: linear-gradient(135deg,#78350f,#d97706); }
.pf label {
display: block;
font-size: 10px !important;
font-weight: 800 !important;
text-transform: uppercase;
letter-spacing: .6px;
color: #64748b !important;
margin: 12px 16px 5px !important;
}
.pf input, .pf select, .pf textarea {
display: block;
box-sizing: border-box !important;
width: auto !important;
margin: 0 16px;
padding: 9px 12px !important;
border: 1.5px solid #e2e8f0 !important;
border-radius: 9px !important;
font: 600 13px 'Inter',sans-serif !important;
color: #1e293b !important;
background: #f8fafc !important;
outline: none;
transition: border-color .2s, box-shadow .2s, background .2s;
}
.pf textarea {
resize: vertical;
min-height: 60px;
font-family: monospace !important;
font-size: 11px !important;
font-weight: 500 !important;
}
.pf input:focus, .pf select:focus, .pf textarea:focus {
border-color: #3b82f6 !important;
background: #fff !important;
box-shadow: 0 0 0 3px rgba(59,130,246,.12) !important;
}
.pf select { cursor: pointer; }
.pf .coord-row {
display: flex; gap: 8px;
margin: 0 16px !important;
padding: 0 !important;
}
.pf .coord-row input {
flex: 1; margin: 0 !important;
width: auto !important;
background: #f1f5f9 !important;
color: #64748b !important;
font-family: monospace !important;
font-size: 12px !important;
text-align: center;
cursor: default;
}
.pf .hint-txt {
font-size: 11px !important;
color: #94a3b8 !important;
margin: 6px 16px 0 !important;
display: flex; align-items: center; gap: 5px;
}
.pf .hint-txt::before { content: 'ΓÜí'; font-size: 10px; }
.pf .pbtn {
display: block !important;
box-sizing: border-box !important;
width: auto !important;
margin: 12px 16px 14px !important;
padding: 10px !important;
border-radius: 10px !important;
font: 700 13px 'Inter',sans-serif !important;
border: none !important;
cursor: pointer !important;
text-align: center;
transition: all .2s !important;
}
.pf .pbtn-g {
background: linear-gradient(135deg,#059669,#10b981) !important;
color: #fff !important;
box-shadow: 0 4px 14px rgba(16,185,129,.3);
}
.pf .pbtn-b {
background: linear-gradient(135deg,#1d4ed8,#3b82f6) !important;
color: #fff !important;
box-shadow: 0 4px 14px rgba(37,99,235,.3);
}
.pf .pbtn-y {
background: linear-gradient(135deg,#d97706,#f59e0b) !important;
color: #fff !important;
box-shadow: 0 4px 14px rgba(217,119,6,.3);
}
.pf .pbtn:hover { transform: translateY(-2px) !important; filter: brightness(1.06); }
/* ΓöÇΓöÇ LEAFLET LAYER CONTROL ΓÇö OPSIONAL section ΓöÇΓöÇ */
.lc-group-title {
font-size: 10px !important;
font-weight: 900 !important;
letter-spacing: 1px !important;
text-transform: uppercase !important;
color: #1e293b !important;
padding: 10px 10px 4px !important;
border-top: 1px solid #f1f5f9;
margin-top: 4px !important;
}
.lc-group-title:first-child { border-top: none; margin-top: 0; }
/* ΓöÇΓöÇ TOPBAR search upgrade ΓöÇΓöÇ */
.map-search input {
border-radius: 10px 0 0 10px !important;
font-family: 'Inter', sans-serif !important;
font-size: 13px !important;
}
.map-search button {
border-radius: 0 10px 10px 0 !important;
}
/* ΓöÇΓöÇ ADD DROPDOWN upgrade ΓöÇΓöÇ */
.add-dropdown { border-radius: 16px !important; }
.add-item {
border-radius: 10px !important;
margin: 2px 4px !important;
transition: background .15s, transform .15s !important;
}
.add-item:hover { transform: translateX(3px) !important; }
.ai-icon { border-radius: 10px !important; font-size: 15px !important; }
.add-item span:last-child { font-size: 13px !important; font-weight: 700 !important; }
.add-item small { color: #94a3b8 !important; font-weight: 500 !important; font-size: 11px !important; }
/* ΓöÇΓöÇ SIDEBAR brand upgrade ΓöÇΓöÇ */
.brand-title { font-size: 17px !important; }
.brand-sub {
font-size: 10px !important;
color: #94a3b8 !important;
margin-top: 2px !important;
display: block;
font-weight: 500 !important;
}
/* ΓöÇΓöÇ USER DROPDOWN upgrade ΓöÇΓöÇ */
.user-dropdown { border-radius: 16px !important; min-width: 200px !important; }
.ud-item { border-radius: 10px !important; margin: 1px 4px !important; }
.ud-header { border-bottom: 1px solid #f1f5f9; padding-bottom: 10px !important; }
.ud-name { font-size: 13px !important; }
/* ΓöÇΓöÇ SCROLLBAR refinement ΓöÇΓöÇ */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 999px; }
::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
/* ΓöÇΓöÇ FULL TABLE MODAL CSS ΓöÇΓöÇ */
.ftm-overlay {
position: fixed; inset: 0; z-index: 10000;
background: rgba(15, 23, 42, 0.75);
backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
padding: 20px;
animation: fadeIn .25s ease;
}
.ftm-box {
background: var(--bg); border-radius: 16px;
width: 100%; max-width: 1200px; max-height: 90vh;
display: flex; flex-direction: column;
box-shadow: 0 25px 60px rgba(0,0,0,0.25);
overflow: hidden;
animation: riseIn .3s cubic-bezier(.16,1,.3,1);
}
.ftm-head {
display: flex; align-items: center; justify-content: space-between;
padding: 20px 24px; background: #fff; border-bottom: 1px solid var(--border);
}
.ftm-title { font-size: 20px; font-weight: 700; color: var(--navy); }
.ftm-sub { font-size: 13px; color: var(--muted); margin-top: 4px; }
.ftm-close {
width: 32px; height: 32px; border-radius: 8px; border: none;
background: var(--panel2); color: var(--muted); cursor: pointer;
display: flex; align-items: center; justify-content: center; font-size: 16px;
transition: .2s;
}
.ftm-close:hover { background: #fee2e2; color: #dc2626; }
.ftm-body {
flex: 1; overflow: auto; padding: 24px; background: var(--bg);
}
.ftm-filters {
display: flex; gap: 12px; margin-bottom: 16px; align-items: center;
}
.ftm-filter-select {
padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
font-family: inherit; font-size: 13px; background: #fff; outline: none;
}
.ftm-table-card {
background: #fff; border-radius: 12px; border: 1px solid var(--border);
box-shadow: 0 4px 16px rgba(0,0,0,0.03); overflow: auto;
}
.ftm-table-card table { width: 100%; border-collapse: collapse; }
.ftm-table-card th {
text-align: left; padding: 14px 16px; font-size: 12px; font-weight: 700;
color: var(--muted); text-transform: uppercase; border-bottom: 1px solid var(--border);
background: var(--panel-soft); position: sticky; top: 0; z-index: 10;
}
.ftm-table-card td { padding: 14px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
.ftm-table-card tr:last-child td { border-bottom: none; }
.ftm-table-card tr:hover td { background: var(--panel-soft); }
.ftm-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
.ftm-btn-icon { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 15px; transition: .2s; }
.ftm-btn-icon:hover { color: var(--blue); }
/* ΓöÇΓöÇ LOGIN MODAL ΓöÇΓöÇ */
.lm-overlay {
position: fixed; inset: 0; z-index: 12000;
background: rgba(15,23,42,.82); backdrop-filter: blur(10px);
display: flex; align-items: center; justify-content: center;
animation: fadeIn .2s ease;
}
.lm-box {
background: #fff; border-radius: 20px; width: 420px; max-width: 95vw;
box-shadow: 0 32px 80px rgba(0,0,0,.3);
overflow: hidden; animation: riseIn .3s cubic-bezier(.16,1,.3,1);
}
.lm-head {
background: linear-gradient(135deg,#1e3a8a,#2563eb);
padding: 28px 32px 24px; text-align: center; color: #fff;
}
.lm-head-icon { font-size: 36px; margin-bottom: 10px; }
.lm-head-title { font-size: 22px; font-weight: 800; }
.lm-head-sub { font-size: 13px; opacity: .8; margin-top: 4px; }
.lm-body { padding: 28px 32px; }
.lm-field { margin-bottom: 16px; }
.lm-field label { display: block; font-size: 12px; font-weight: 700; color: var(--muted); margin-bottom: 6px; letter-spacing: .5px; text-transform: uppercase; }
.lm-input-wrap { position: relative; }
.lm-input-wrap .lm-ico { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--muted); font-size: 14px; }
.lm-input-wrap input { width: 100%; padding: 12px 14px 12px 40px; border: 1.5px solid var(--border); border-radius: 10px; font: 500 14px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; box-sizing: border-box; }
.lm-input-wrap input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.12); }
.lm-eye { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: var(--muted); font-size: 14px; }
.lm-submit { width: 100%; padding: 13px; border: none; border-radius: 12px; background: linear-gradient(135deg,#1e3a8a,#2563eb); color: #fff; font: 700 14px 'Plus Jakarta Sans',sans-serif; cursor: pointer; box-shadow: 0 6px 20px rgba(37,99,235,.3); transition: .2s; margin-top: 4px; }
.lm-submit:hover { transform: translateY(-1px); box-shadow: 0 8px 28px rgba(37,99,235,.4); }
.lm-submit:disabled { opacity: .6; cursor: not-allowed; transform: none; }
.lm-alert { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; margin-bottom: 16px; }
.lm-alert.error { background: #fee2e2; color: #dc2626; }
.lm-close-btn { position: absolute; top: 14px; right: 14px; background: rgba(255,255,255,.15); border: none; border-radius: 8px; width: 30px; height: 30px; cursor: pointer; color: #fff; font-size: 14px; display: flex; align-items: center; justify-content: center; }
/* ΓöÇΓöÇ PROFIL MODAL ΓöÇΓöÇ */
.prf-overlay {
position: fixed; inset: 0; z-index: 12000;
background: rgba(15,23,42,.75); backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center; padding: 20px;
animation: fadeIn .2s ease;
}
.prf-box {
background: #fff; border-radius: 20px; width: 480px; max-width: 95vw;
box-shadow: 0 28px 70px rgba(0,0,0,.25);
overflow: hidden; animation: riseIn .3s cubic-bezier(.16,1,.3,1);
}
.prf-head {
background: linear-gradient(135deg,#0f172a,#1e3a8a);
padding: 28px 28px 60px; text-align: center; position: relative;
}
.prf-avatar {
width: 80px; height: 80px; border-radius: 50%; margin: 0 auto 12px;
background: linear-gradient(135deg,#3b82f6,#6366f1);
display: flex; align-items: center; justify-content: center;
font-size: 32px; font-weight: 800; color: #fff;
border: 3px solid rgba(255,255,255,.3);
}
.prf-head-name { font-size: 18px; font-weight: 700; color: #fff; }
.prf-head-role { font-size: 12px; color: rgba(255,255,255,.7); margin-top: 4px; }
.prf-body { padding: 0 28px 28px; margin-top: -30px; }
.prf-card { background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.prf-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); }
.prf-row:last-child { border-bottom: none; padding-bottom: 0; }
.prf-label { font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; }
.prf-value { font-size: 14px; font-weight: 600; color: var(--navy); }
.prf-edit-row { display: flex; gap: 8px; align-items: center; }
.prf-input { padding: 8px 12px; border: 1.5px solid var(--border); border-radius: 8px; font: 600 14px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; }
.prf-input:focus { border-color: var(--blue); }
.prf-save-btn { padding: 8px 14px; background: var(--blue); color: #fff; border: none; border-radius: 8px; font: 700 12px 'Plus Jakarta Sans',sans-serif; cursor: pointer; }
.prf-close { position: absolute; top: 14px; right: 14px; background: rgba(255,255,255,.15); border: none; border-radius: 8px; width: 30px; height: 30px; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; }
/* ΓöÇΓöÇ KELOLA AKUN MODAL (Users) ΓöÇΓöÇ */
.ua-role-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; }
.ua-role-admin { background: #dbeafe; color: #1d4ed8; }
.ua-role-operator { background: #f0fdf4; color: #15803d; }
.ua-form { background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
.ua-form-title { font-size: 14px; font-weight: 700; color: var(--navy); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
.ua-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.ua-field label { display: block; font-size: 11px; font-weight: 700; color: var(--muted); margin-bottom: 5px; text-transform: uppercase; }
.ua-field input, .ua-field select { width: 100%; padding: 9px 12px; border: 1.5px solid var(--border); border-radius: 8px; font: 500 13px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; box-sizing: border-box; }
.ua-field input:focus, .ua-field select:focus { border-color: var(--blue); }
.ua-submit { padding: 10px 20px; background: var(--blue); color: #fff; border: none; border-radius: 8px; font: 700 13px 'Plus Jakarta Sans',sans-serif; cursor: pointer; margin-top: 14px; }
.ua-avatar { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; font: 700 13px 'Space Grotesk',sans-serif; color: #fff; }
</style>
</head>
<body>
<div id="sidebar">
<div class="brand">
<div class="brand-row">
<div class="brand-icon"><i class="fas fa-map-location-dot"></i></div>
<div class="brand-title">SiPeta<span style="color:var(--blue)">Sos</span></div>
</div>
<div class="brand-sub">Sistem Informasi Peta Sosial & Rumah Ibadah</div>
</div>
<div class="list-wrap">
<div style="padding:20px 20px 10px">
<a href="#" onclick="openDataWargaModal(); return false;"
style="display:flex;align-items:center;gap:10px;background:linear-gradient(135deg,#3b82f6,#6366f1);border-radius:14px;padding:20px 20px 10px;text-decoration:none;color:white;font-weight:600;font-size:14px;box-shadow: 0 8px 30px rgba(0,0,0,0.04);transition:0.2s">
<i class="fas fa-users" style="font-size:18px"></i>
<div>
<div>Lihat Data Warga</div>
<div style="font-weight:400;font-size:11px;opacity:0.85;margin-top:2px">Daftar & kelola anggota keluarga
</div>
</div>
<i class="fas fa-arrow-right" style="margin-left:auto;font-size:12px"></i>
</a>
</div>
<div style="padding:0 20px 20px">
<a href="#" onclick="openDataMasjidModal(); return false;"
style="display:flex;align-items:center;gap:10px;background:linear-gradient(135deg,#14b8a6,#0ea5e9);border-radius:14px;padding:20px 20px 10px;text-decoration:none;color:white;font-weight:600;font-size:14px;box-shadow: 0 8px 30px rgba(0,0,0,0.04);transition:0.2s">
<i class="fas fa-place-of-worship" style="font-size:18px"></i>
<div>
<div>Data Rumah Ibadah</div>
<div style="font-weight:400;font-size:11px;opacity:0.85;margin-top:2px">Kelola fasilitas ibadah</div>
</div>
<i class="fas fa-arrow-right" style="margin-left:auto;font-size:12px"></i>
</a>
</div>
<div id="list-masjid" style="display:none;"><span id="cnt-masjid">0</span></div>
<div id="list-kemiskinan" style="display:none;"><span id="cnt-kemiskinan">0</span></div>
<div id="list-spbu" style="display:none;"><span id="cnt-spbu">0</span></div>
<div id="list-jalan" style="display:none;"><span id="cnt-jalan">0</span></div>
<div id="list-parsil" style="display:none;"><span id="cnt-parsil">0</span></div>
</div>
<div class="leg-sec">
<div class="sec-lbl" style="margin-bottom:10px">Legenda Peta</div>
<div class="leg-grid">
<div class="leg-item">
<div class="leg-dot" style="background:var(--amber)"></div>Kemiskinan
</div>
<div class="leg-item">
<div class="leg-dot" style="background:var(--teal)"></div>Rumah Ibadah
</div>
</div>
</div>
</div>
<div id="main-container">
<!-- Public Mode Banner -->
<div id="public-banner">
<i class="fas fa-lock"></i>
<span>Mode Publik &mdash; Data KK miskin tidak ditampilkan.
<a href="#" onclick="openLoginModal(); return false;">Login</a> untuk akses penuh termasuk peta, daftar, dan pencarian data.
</span>
</div>
<div class="app-topbar">
<div class="public-brand public-only">
<span class="brand-icon-mini"><i class="fas fa-map-location-dot"></i></span>
<div class="public-brand-text">
<span class="public-brand-title">SiPeta<span style="color:var(--blue)">Sos</span></span>
<span class="public-brand-sub">Sistem Informasi Peta Sosial &amp; Rumah Ibadah</span>
</div>
</div>
<div class="map-search">
<input id="map-search" placeholder="Cari nama atau alamat..." oninput="renderDashboard()" onkeydown="if(event.key === 'Enter') runSearch()">
<button onclick="runSearch()"><i class="fas fa-search"></i></button>
</div>
<div class="top-actions">
<!-- Γ₧ò Add Data dropdown (admin) -->
<div class="add-menu admin-only" id="add-menu">
<button class="top-btn" onclick="toggleAddMenu()">
<i class="fas fa-plus"></i> Tambah
<i class="fas fa-chevron-down" style="font-size:10px;margin-left:2px;"></i>
</button>
<div class="add-dropdown">
<div class="add-dropdown-head">Pilih Jenis Data</div>
<div class="add-item" onclick="setMode('kemiskinan');closeAddMenu()">
<span class="ai-icon" style="background:#fee2e2;color:#ef4444"><i class="fas fa-users"></i></span>
<span>Data KK Miskin <small>Tambah keluarga miskin</small></span>
</div>
<div class="add-item" onclick="setMode('masjid');closeAddMenu()">
<span class="ai-icon" style="background:#d1fae5;color:#059669"><i class="fas fa-place-of-worship"></i></span>
<span>Rumah Ibadah <small>Tambah lokasi ibadah</small></span>
</div>
<div style="height:1px;background:#e2e8f0;margin:4px 4px"></div>
<div style="padding:6px 10px 2px;font-size:10px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.6px">Opsional</div>
<div class="add-item" onclick="setMode('spbu');closeAddMenu()">
<span class="ai-icon" style="background:#dcfce7;color:#16a34a"><i class="fas fa-gas-pump"></i></span>
<span>SPBU <small>Tambah titik SPBU</small></span>
</div>
<div class="add-item" onclick="setMode('jalan');closeAddMenu()">
<span class="ai-icon" style="background:#dbeafe;color:#2563eb"><i class="fas fa-road"></i></span>
<span>Jalan <small>Gambar ruas jalan</small></span>
</div>
<div class="add-item" onclick="setMode('parsil');closeAddMenu()">
<span class="ai-icon" style="background:#fef3c7;color:#d97706"><i class="fas fa-vector-square"></i></span>
<span>Parsil / Lahan <small>Gambar area persil</small></span>
</div>
</div>
</div>
<!-- Icon buttons -->
<button class="icon-btn admin-only" onclick="focusBlankSpot()" title="Blank Spot">
<i class="fas fa-triangle-exclamation"></i>
<span class="ib-badge" id="top-blank-count">0</span>
</button>
<button class="icon-btn admin-only" onclick="downloadReport()" title="Unduh Laporan CSV">
<i class="fas fa-download"></i>
</button>
<button class="icon-btn" onclick="openLaporanModal()" title="Laporan Masyarakat" id="btn-laporan-top">
<i class="fas fa-bullhorn"></i>
<span class="ib-badge" id="laporan-badge" style="display:none">0</span>
</button>
<!-- Data Panel (primary CTA) -->
<button class="top-btn primary admin-only" onclick="togglePanel()">
<i class="fas fa-table-list"></i> Data Panel
</button>
<!-- Divider + User avatar dropdown -->
<div class="tb-divider admin-only"></div>
<div class="user-menu admin-only" id="user-menu">
<button class="user-avatar-btn" onclick="toggleUserMenu()">
<div class="user-avatar" id="topbar-avatar">AD</div>
<span id="topbar-uname">Admin</span>
<i class="fas fa-chevron-down user-caret"></i>
</button>
<div class="user-dropdown">
<div class="ud-header">
<div class="ud-name" id="dd-nama">Admin</div>
<div class="ud-role" id="dd-role">Administrator</div>
</div>
<div class="ud-sep"></div>
<a href="#" onclick="openProfilModal(); return false;" class="ud-item"><i class="fas fa-circle-user"></i> Profil Saya</a>
<div class="ud-item" onclick="openChangePassword()"><i class="fas fa-key"></i> Ganti Sandi</div>
<a href="#" onclick="openKelolaAkunModal(); return false;" class="ud-item" id="dd-users-link"><i class="fas fa-users-gear"></i> Kelola Akun</a>
<div class="ud-sep"></div>
<div class="ud-item danger" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Keluar</div>
</div>
</div>
<!-- Public mode buttons -->
<button class="top-btn public-only" onclick="showLockedPanel()"><i class="fas fa-list"></i> Panel</button>
<a href="#" onclick="openLoginModal(); return false;" class="top-btn primary public-only" id="topbar-login-btn"
style="text-decoration:none;display:none;">
<i class="fas fa-sign-in-alt"></i> Login
</a>
</div>
</div>
<div id="map-wrapper">
<div id="map"></div>
<div class="stats-strip" id="stats-strip"></div>
<button class="map-chip" onclick="toggleSatellite()"><i class="fas fa-globe"></i> <span id="sat-label">Mode
Satelit</span></button>
<aside class="side-panel" id="side-panel">
<div class="panel-head">
<h3><span>Data KK Miskin (<span id="panel-count">0</span>)</span> <button class="close-panel-btn"
onclick="togglePanel()"><i class="fas fa-times"></i></button></h3>
<p>Detail dan status penanganan KK miskin</p>
</div>
<div class="panel-list" id="panel-list"></div>
</aside>
<!-- Locked Panel (public mode) -->
<div class="locked-panel" id="locked-panel">
<!-- Gradient header -->
<div class="lp-header">
<div class="lp-icon-wrap"><i class="fas fa-shield-halved"></i></div>
<div class="lp-title">Area Terproteksi</div>
<div class="lp-header-sub">Login diperlukan untuk akses penuh</div>
</div>
<div class="lp-divider"></div>
<!-- Body -->
<div class="lp-body">
<p class="lp-desc">Daftar KK miskin dan detail informasi hanya dapat diakses oleh pengguna yang telah masuk ke
sistem.</p>
<ul class="lp-features">
<li>
<span class="feat-icon" style="background:#FFEDD5;color:#2563eb"><i class="fas fa-users"></i></span>
Daftar lengkap KK miskin
</li>
<li>
<span class="feat-icon" style="background:#d1fae5;color:#059669"><i class="fas fa-map-pin"></i></span>
Titik peta interaktif
</li>
<li>
<span class="feat-icon" style="background:#ede9fe;color:#7c3aed"><i class="fas fa-file-lines"></i></span>
Ekspor laporan data
</li>
<li>
<span class="feat-icon" style="background:#ffedd5;color:#ea580c"><i
class="fas fa-triangle-exclamation"></i></span>
Monitoring blank spot
</li>
</ul>
<a href="#" onclick="openLoginModal(); return false;" class="lp-btn">
<i class="fas fa-right-to-bracket"></i>&ensp;Masuk ke Sistem
</a>
<p class="lp-note"><i class="fas fa-circle-check"></i>&ensp;Statistik &amp; titik peta dapat dilihat tanpa
login.</p>
</div>
</div>
</div>
</div>
<div id="hint"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.11.0/proj4.min.js"></script>
<script>
// Auth check
const IS_LOGGED_IN = sessionStorage.getItem('gis_logged_in') === 'true';
if (!IS_LOGGED_IN) {
document.body.classList.add('public-mode');
const banner = document.getElementById('public-banner');
if (banner) banner.classList.add('show');
const lockedPanel = document.getElementById('locked-panel');
if (lockedPanel) lockedPanel.classList.add('show');
const loginBtn = document.getElementById('topbar-login-btn');
if (loginBtn) loginBtn.style.display = 'flex';
} else {
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const nama = gisUser.nama_lengkap || gisUser.username || 'Admin';
const uname = gisUser.username || 'admin';
const role = gisUser.role || 'admin';
const roleLabel = role === 'walikota' ? 'Walikota Pontianak' : role === 'superadmin' ? 'Super Admin' : 'Administrator';
const initials = nama.split(' ').slice(0, 2).map(w => w[0]).join('').toUpperCase() || 'AD';
const el = id => document.getElementById(id);
if (el('topbar-avatar')) el('topbar-avatar').textContent = initials;
if (el('topbar-uname')) el('topbar-uname').textContent = uname;
if (el('dd-nama')) el('dd-nama').textContent = nama;
if (el('dd-role')) el('dd-role').textContent = roleLabel;
}
function logout() {
sessionStorage.removeItem('gis_logged_in');
sessionStorage.removeItem('gis_user');
window.location.href = 'index.html';
}
function toggleUserMenu() {
const menu = document.getElementById('user-menu');
if (menu) menu.classList.toggle('open');
}
function toggleAddMenu() {
const menu = document.getElementById('add-menu');
if (menu) menu.classList.toggle('open');
}
function closeAddMenu() {
const menu = document.getElementById('add-menu');
if (menu) menu.classList.remove('open');
}
document.addEventListener('click', function (e) {
const userMenu = document.getElementById('user-menu');
if (userMenu && !userMenu.contains(e.target)) userMenu.classList.remove('open');
const addMenu = document.getElementById('add-menu');
if (addMenu && !addMenu.contains(e.target)) addMenu.classList.remove('open');
});
function showLockedPanel() {
const lockedPanel = document.getElementById('locked-panel');
if (lockedPanel) lockedPanel.classList.add('show');
}
/* STATE */
const state = {
mode: null, spbu: [], jalan: [], parsil: [], kemiskinan: [], masjid: [],
featureRefs: { spbu: {}, jalan: {}, parsil: {}, kemiskinan: {}, masjid: {} },
kkLayerRefs: {}, blankRefs: {}, useSatellite: false, geometryEdit: null, activeCard: null,
spbuFilterVis: { open24: true, non24: true }, drawingLine: false, drawingPoly: false,
tempPoints: [], tempMarkers: [], tempPolyline: null, tempPolygon: null
};
/* MAP */
const map = L.map('map', { zoomControl: false, preferCanvas: true }).setView([-0.0263, 109.3425], 14);
L.control.zoom({ position: 'bottomright' }).addTo(map);
const streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors', maxZoom: 19
}).addTo(map);
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri', maxZoom: 19
});
const layers = {
kemiskinan: L.layerGroup().addTo(map),
masjid: L.layerGroup().addTo(map),
blankspot: L.layerGroup().addTo(map),
// Layer opsional ΓÇö OFF by default (tidak .addTo(map))
spbu24: L.layerGroup(),
spbuNon24: L.layerGroup(),
jalan: L.layerGroup(),
parsil: L.layerGroup()
};
const layersControl = L.control.layers(null, {
'<span class="lc-item"><span class="lc-dot lc-dot-kemiskinan"></span><span class="lc-text"><strong>Kemiskinan</strong><small>Data KK miskin</small></span></span>': layers.kemiskinan,
'<span class="lc-item"><span class="lc-dot lc-dot-masjid"></span><span class="lc-text"><strong>Rumah Ibadah</strong><small>Fasilitas ibadah</small></span></span>': layers.masjid,
'<span class="lc-item"><span class="lc-dot" style="background:#64748b"></span><span class="lc-text"><strong>Blank Spot</strong><small>Di luar radius rumah ibadah</small></span></span>': layers.blankspot,
'<span class="lc-item"><span class="lc-dot" style="background:#16a34a"></span><span class="lc-text"><strong>SPBU 24 Jam</strong><small>Buka 24 jam</small></span></span>': layers.spbu24,
'<span class="lc-item"><span class="lc-dot" style="background:#dc2626"></span><span class="lc-text"><strong>SPBU Non-24</strong><small>Tidak 24 jam</small></span></span>': layers.spbuNon24,
'<span class="lc-item"><span class="lc-dot" style="background:#2563eb"></span><span class="lc-text"><strong>Jalan</strong><small>Jaringan jalan</small></span></span>': layers.jalan,
'<span class="lc-item"><span class="lc-dot" style="background:#d97706"></span><span class="lc-text"><strong>Parsil / Lahan</strong><small>Data persil tanah</small></span></span>': layers.parsil,
}, { collapsed: false, position: 'topright' }).addTo(map);
decorateLayersControl();
function decorateLayersControl() {
const container = layersControl.getContainer();
if (!container) return;
const overlays = container.querySelector('.leaflet-control-layers-overlays');
if (!overlays) return;
const labels = Array.from(overlays.querySelectorAll('label'));
if (labels.length < 6) return;
overlays.innerHTML = '';
const addGroup = (title, items, color) => {
const head = document.createElement('div');
head.className = 'lc-group-title';
head.style.cursor = 'pointer';
head.style.display = 'flex';
head.style.justifyContent = 'space-between';
head.style.alignItems = 'center';
head.style.userSelect = 'none';
const titleSpan = document.createElement('span');
titleSpan.textContent = title;
if (color) titleSpan.style.color = color;
const iconSpan = document.createElement('i');
iconSpan.className = 'fa-solid fa-chevron-up';
iconSpan.style.transition = 'transform 0.3s ease';
iconSpan.style.fontSize = '12px';
if (color) iconSpan.style.color = color;
head.appendChild(titleSpan);
head.appendChild(iconSpan);
overlays.appendChild(head);
const content = document.createElement('div');
content.style.overflow = 'hidden';
content.style.transition = 'max-height 0.3s ease';
content.style.maxHeight = '1000px';
items.forEach(item => content.appendChild(item));
overlays.appendChild(content);
let isOpen = true;
head.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
isOpen = !isOpen;
if (isOpen) {
content.style.maxHeight = '1000px';
iconSpan.style.transform = 'rotate(0deg)';
} else {
content.style.maxHeight = '0px';
iconSpan.style.transform = 'rotate(180deg)';
}
});
};
addGroup('Tematik Utama', [labels[0], labels[1], labels[2]]);
addGroup('Opsional', [labels[3], labels[4], labels[5], labels[6]], '#64748b');
}
const statusStoreKey = 'gis-kemiskinan-status';
function getStatusMap() {
try { return JSON.parse(localStorage.getItem(statusStoreKey) || '{}'); }
catch (_) { return {}; }
}
function getKKStatus(id) {
// Cek dari state data terlebih dahulu (dari DB)
const d = state.kemiskinan.find(x => String(x.id) === String(id));
if (d && d.status_penanganan) return d.status_penanganan;
// Fallback ke localStorage
return getStatusMap()[String(id)] || 'Belum Ditangani';
}
function setKKStatus(id, value) {
// Update state
const d = state.kemiskinan.find(x => String(x.id) === String(id));
if (d) d.status_penanganan = value;
// Update localStorage
const mapStatus = getStatusMap();
mapStatus[String(id)] = value;
localStorage.setItem(statusStoreKey, JSON.stringify(mapStatus));
// Simpan ke database
fetch('api/update_status_kemiskinan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id), status: value })
}).catch(() => console.warn('Gagal simpan status ke DB'));
renderDashboard();
refreshType('kemiskinan');
}
// ΓöÇΓöÇ TOAST NOTIFICATION ΓöÇΓöÇ
function showToast(msg, type) {
let toast = document.getElementById('gis-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'gis-toast';
toast.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);padding:10px 20px;border-radius:999px;font:700 13px Inter,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.3s;opacity:0;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.15)';
document.body.appendChild(toast);
}
const colors = {
success: { bg: '#059669', color: '#fff' },
info: { bg: '#2563eb', color: '#fff' },
warning: { bg: '#f59e0b', color: '#fff' }
};
const c = colors[type] || colors.success;
toast.style.background = c.bg;
toast.style.color = c.color;
toast.textContent = msg;
toast.style.opacity = '1';
clearTimeout(toast._t);
toast._t = setTimeout(() => { toast.style.opacity = '0'; }, 2200);
}
function setKKStatusQuick(id, value) {
// Update state
const d2 = state.kemiskinan.find(x => String(x.id) === String(id));
if (d2) d2.status_penanganan = value;
// Update localStorage
const mapStatus = getStatusMap();
mapStatus[String(id)] = value;
localStorage.setItem(statusStoreKey, JSON.stringify(mapStatus));
// Simpan ke database
fetch('api/update_status_kemiskinan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id), status: value })
}).then(r => r.json()).then(res => {
if (res.status !== 'success') {
console.warn('Gagal simpan status:', res.message);
showToast('⚠️ Gagal simpan ke database', 'warning');
} else {
const label = value === 'Diproses' ? '🔄 Status: Diproses' : value === 'Sudah Ditangani' ? '✅ Status: Sudah Ditangani' : '↩️ Status: Belum Ditangani';
showToast(label, value === 'Diproses' ? 'info' : value === 'Sudah Ditangani' ? 'success' : 'warning');
}
}).catch(() => {
console.warn('Gagal simpan status ke DB');
showToast('⚠️ Tidak terhubung ke database', 'warning');
});
renderDashboard();
const d = state.kemiskinan.find(x => String(x.id) === String(id));
if (!d) return;
const ref = state.featureRefs['kemiskinan'][String(id)];
if (!ref) return;
const status = value;
const done = status === 'Sudah Ditangani';
const blank = isBlankSpot(d);
const markerColor = blank ? '#dc2626' : '#16a34a';
const coverIcon = blank ? '<i class="fas fa-circle-xmark" style="color:#dc2626"></i>' : '<i class="fas fa-circle-check" style="color:#16a34a"></i>';
const nearest = nearestRumahIbadah(d);
const namaRI = nearest ? nearest.nama_masjid : '-';
const jarakRI = nearest ? Math.round(nearest.distance) + ' m' : '-';
const cat = getCategory(d);
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const role = gisUser.role || '';
const canManage = role !== 'walikota';
const actionsHtml = canManage ? `<div class="act-row kk-popup-actions">
<button class="pbtn pbtn-y" onclick="openEditKemiskinan(${d.id})">Edit</button>
<button class="pbtn pbtn-b" onclick="setKKStatusQuick(${d.id},'Diproses')">Proses</button>
<button class="pbtn ${done ? 'pbtn-b' : 'pbtn-g'}" onclick="setKKStatusQuick(${d.id},&apos;${done ? 'Belum Ditangani' : 'Sudah Ditangani'}&apos;)">
${done ? 'Batal' : 'Selesai'}
</button>
<button class="pbtn" style="background:#ede9fe;color:#7c3aed" title="Data Warga" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-users-cog"></i></button>
<button class="pbtn" style="background:#FFEDD5;color:#1e3a8a" title="Histori Bantuan" onclick="openHistoriBantuan(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-history"></i></button>
<button class="pbtn pbtn-r" onclick="deleteFeature('kemiskinan',${d.id})">Hapus</button>
</div>` : `<div class="act-row kk-popup-actions" style="grid-template-columns:1fr 1fr">
<button class="pbtn" style="background:#ede9fe;color:#7c3aed;padding:8px" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-users-cog"></i> Data Warga</button>
<button class="pbtn" style="background:#FFEDD5;color:#1e3a8a;padding:8px" onclick="openHistoriBantuan(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-history"></i> Histori Bantuan</button>
</div>`;
const catBadgeClass = cat.key === 'extreme' ? 'extreme' : cat.key === 'poor' ? 'poor' : 'vulnerable';
const statusColor = status === 'Sudah Ditangani' ? '#059669' : status === 'Diproses' ? '#2563eb' : '#64748b';
const privatePopup = `<div class="pi">
<div class="pi-header">
<div class="pi-header-icon"><i class="fas fa-users"></i></div>
<div class="pi-header-info">
<div class="pi-header-name">${d.nama_kk}</div>
<div class="pi-header-sub"><span class="pi-badge ${catBadgeClass}" style="font-size:10px;padding:2px 7px">${cat.label}</span></div>
</div>
<button class="pi-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="pi-body">
<div class="pi-row"><span class="pi-k">Jumlah Jiwa</span><span class="pi-v">${d.jumlah_kk} orang</span></div>
<div class="pi-row"><span class="pi-k">Alamat</span><span class="pi-v" style="max-width:140px">${d.alamat}</span></div>
<div class="pi-row"><span class="pi-k">Status</span><span class="pi-v" style="color:${statusColor}">${status}</span></div>
<div class="pi-row"><span class="pi-k">Rumah Ibadah</span><span class="pi-v" style="max-width:130px">${namaRI}</span></div>
<div class="pi-row"><span class="pi-k">Jarak</span><span class="pi-v">${jarakRI}</span></div>
<div class="pi-row"><span class="pi-k">Cakupan</span><span class="pi-v">${coverIcon} ${blank ? 'Blank Spot' : 'Terjangkau'}</span></div>
</div>
${actionsHtml}
</div>`;
if (ref.setPopupContent) {
ref.setPopupContent(privatePopup);
}
}
function getCategory(d) {
const jumlah = Number(d.jumlah_kk) || 1;
if (jumlah >= 5) return { key: 'extreme', label: 'Miskin Ekstrem', color: '#dc2626', badge: 'badge-extreme' };
if (jumlah >= 3) return { key: 'poor', label: 'Miskin', color: '#ea580c', badge: 'badge-poor' };
return { key: 'vulnerable', label: 'Rentan Miskin', color: '#16a34a', badge: 'badge-vulnerable' };
}
function nearestRumahIbadah(d) {
if (!state.masjid.length) return null;
const here = L.latLng(d.lat, d.lng);
return state.masjid
.map(m => ({ ...m, distance: here.distanceTo(L.latLng(m.lat, m.lng)) }))
.sort((a, b) => a.distance - b.distance)[0];
}
function isBlankSpot(d) {
const nearest = nearestRumahIbadah(d);
if (!nearest) return true;
return nearest.distance > (Number(nearest.radius) || 500);
}
function rumahIbadahStats(d) {
const center = L.latLng(d.lat, d.lng);
const radius = Number(d.radius) || 500;
const covered = state.kemiskinan
.map(k => ({ ...k, distance: center.distanceTo(L.latLng(k.lat, k.lng)) }))
.filter(k => k.distance <= radius)
.sort((a, b) => a.distance - b.distance);
return {
total: covered.length,
extreme: covered.filter(k => getCategory(k).key === 'extreme').length,
process: covered.filter(k => getKKStatus(k.id) === 'Diproses').length,
handled: covered.filter(k => getKKStatus(k.id) === 'Sudah Ditangani').length,
sample: covered.slice(0, 3)
};
}
function vulnerabilityScore(d) {
const nearest = nearestRumahIbadah(d);
const distanceRisk = nearest && nearest.distance > nearest.radius ? 16 : 0;
return Math.min(100, Math.round(55 + ((Number(d.jumlah_kk) || 1) * 7) + distanceRisk));
}
function filteredKemiskinan() {
const q = (document.getElementById('map-search')?.value || '').trim().toLowerCase();
return state.kemiskinan.filter(d => {
const text = `${d.nama_kk} ${d.alamat}`.toLowerCase();
return !q || text.includes(q);
});
}
function renderBlankSpots() {
layers.blankspot.clearLayers();
state.blankRefs = {};
state.kemiskinan.filter(isBlankSpot).forEach(d => {
const marker = L.circleMarker([d.lat, d.lng], {
radius: 7,
color: '#64748b',
fillColor: '#64748b',
fillOpacity: 0.9,
weight: 2
}).bindPopup(IS_LOGGED_IN
? `<div class="pi"><h4><i class="fas fa-triangle-exclamation" style="color:#64748b"></i>Blank Spot</h4><div class="pi-row"><span class="pi-k">KK</span><span class="pi-v">${d.nama_kk}</span></div><div class="pi-row"><span class="pi-k">Status</span><span class="pi-v">Di luar radius rumah ibadah</span></div></div>`
: `<div class="pi"><h4><i class="fas fa-triangle-exclamation" style="color:#64748b"></i>Blank Spot</h4><div class="pi-row"><span class="pi-k">Status</span><span class="pi-v">Area prioritas</span></div><div class="pi-row"><span class="pi-k">Detail</span><span class="pi-v">Login diperlukan</span></div></div>`,
{ autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] });
marker.addTo(layers.blankspot);
state.blankRefs[String(d.id)] = marker;
});
}
function renderDashboard() {
renderBlankSpots();
const total = state.kemiskinan.length;
const extreme = state.kemiskinan.filter(d => getCategory(d).key === 'extreme').length;
const handled = state.kemiskinan.filter(d => getKKStatus(d.id) === 'Sudah Ditangani').length;
const blank = state.kemiskinan.filter(isBlankSpot).length;
const stats = [
['fa-users', total, 'Total KK', '#dc2626'],
['fa-fire', extreme, 'Ekstrem', '#ea580c'],
['fa-check', handled, 'Tertangani', '#16a34a'],
['fa-place-of-worship', state.masjid.length, 'Rumah Ibadah', '#2563eb'],
['fa-location-dot', blank, 'Blank Spot', '#64748b']
];
const statsEl = document.getElementById('stats-strip');
if (statsEl) {
statsEl.innerHTML = stats.map(([icon, num, label, color]) => `<div class="stat-pill"><i class="fas ${icon}" style="background:${color}"></i><span><strong>${num}</strong><small>${label}</small></span></div>`).join('');
}
const topBlank = document.getElementById('top-blank-count');
if (topBlank) topBlank.textContent = blank;
const panel = document.getElementById('panel-list');
const count = document.getElementById('panel-count');
if (!panel || !count) return;
const data = filteredKemiskinan();
count.textContent = data.length;
panel.innerHTML = data.map(d => {
const cat = getCategory(d);
const status = getKKStatus(d.id);
const statusClass = status === 'Sudah Ditangani' ? 'badge-handled' : 'badge-pending';
const nearest = nearestRumahIbadah(d);
const worship = nearest ? nearest.nama_masjid : '-';
const blankText = isBlankSpot(d) ? '<div class="kk-row blank-warning">BLANK SPOT</div>' : '';
return `<div class="kk-card">
<div class="kk-top"><div class="kk-avatar"><i class="fas fa-user"></i></div><div><div class="kk-name">${d.nama_kk}</div><div class="kk-sub">${d.alamat} · ${d.jumlah_kk} jiwa</div></div></div>
<div class="kk-row"><span>Kategori</span><strong><span class="kk-badge ${cat.badge}">${cat.label}</span></strong></div>
<div class="kk-row"><span>Skor Kerentanan</span><strong>${vulnerabilityScore(d)}/100</strong></div>
<div class="kk-row"><span>Status</span><strong><span class="kk-badge ${statusClass}">${status}</span></strong></div>
<div class="kk-row"><span>Rumah Ibadah</span><strong>${worship}</strong></div>
${blankText}
<div class="kk-actions">
<button class="mini-btn" onclick="focusFeature('kemiskinan',${d.id})"><i class="fas fa-eye"></i></button>
<select onchange="setKKStatus(${d.id},this.value)">
<option ${status === 'Belum Ditangani' ? 'selected' : ''}>Belum Ditangani</option>
<option ${status === 'Diproses' ? 'selected' : ''}>Diproses</option>
<option ${status === 'Sudah Ditangani' ? 'selected' : ''}>Sudah Ditangani</option>
</select>
</div>
</div>`;
}).join('');
}
function applyLayerFilters() {
refreshType('kemiskinan');
if (document.getElementById('flt-worship')?.checked) map.addLayer(layers.masjid); else map.removeLayer(layers.masjid);
if (document.getElementById('flt-blank')?.checked) map.addLayer(layers.blankspot); else map.removeLayer(layers.blankspot);
}
function runSearch() {
const q = (document.getElementById('map-search')?.value || '').trim().toLowerCase();
if (!q) return;
// 1. Cari Masjid
const fMasjid = state.masjid.find(d => (d.nama_masjid || '').toLowerCase().includes(q));
if (fMasjid) return focusFeature('masjid', fMasjid.id);
// 2. Cari Data Warga (Kemiskinan)
const fWarga = state.kemiskinan.find(d => (`${d.nama_kk} ${d.alamat}`).toLowerCase().includes(q));
if (fWarga) return focusFeature('kemiskinan', fWarga.id);
// 3. Cari SPBU
const fSpbu = state.spbu.find(d => (d.nama || '').toLowerCase().includes(q));
if (fSpbu) return focusFeature('spbu', fSpbu.id);
// 4. Cari Jalan
const fJalan = state.jalan.find(d => (d.nama || '').toLowerCase().includes(q));
if (fJalan) return focusFeature('jalan', fJalan.id);
// 5. Cari Parsil
const fParsil = state.parsil.find(d => (`${d.nama} ${d.pemilik}`).toLowerCase().includes(q));
if (fParsil) return focusFeature('parsil', fParsil.id);
// Jika tidak ketemu
alert('Data "' + q + '" tidak ditemukan.');
}
function focusBlankSpot() {
const first = state.kemiskinan.find(isBlankSpot);
if (first) focusFeature('kemiskinan', first.id);
}
function togglePanel() {
const panel = document.getElementById('side-panel');
if (panel) {
if (panel.classList.contains('open')) {
panel.classList.remove('open');
setTimeout(() => panel.style.pointerEvents = 'none', 400);
} else {
panel.style.pointerEvents = 'auto';
panel.classList.add('open');
}
}
}
function toggleSatellite() {
state.useSatellite = !state.useSatellite;
if (state.useSatellite) {
map.removeLayer(streetLayer);
satelliteLayer.addTo(map);
} else {
map.removeLayer(satelliteLayer);
streetLayer.addTo(map);
}
const label = document.getElementById('sat-label');
if (label) label.textContent = state.useSatellite ? 'Jalan' : 'Satelit';
}
function downloadReport() {
const rows = [
['Nama KK', 'Jumlah Jiwa', 'Alamat', 'Kategori', 'Skor Kerentanan', 'Status', 'Rumah Ibadah Terdekat', 'Jarak Meter', 'Blank Spot']
];
state.kemiskinan.forEach(d => {
const nearest = nearestRumahIbadah(d);
rows.push([
d.nama_kk,
d.jumlah_kk,
d.alamat,
getCategory(d).label,
vulnerabilityScore(d),
getKKStatus(d.id),
nearest ? nearest.nama_masjid : '',
nearest ? Math.round(nearest.distance) : '',
isBlankSpot(d) ? 'Ya' : 'Tidak'
]);
});
const csv = rows.map(row => row.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'laporan-kemiskinan-rumah-ibadah.csv';
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
const jColor = { 'Nasional': '#ef4444', 'Provinsi': '#f59e0b', 'Kabupaten': '#3b82f6' };
const pColor = { 'SHM': '#3b82f6', 'HGB': '#8b5cf6', 'HGU': '#f59e0b', 'HP': '#10b981' };
const kecColorMap = {
'Pontianak Timur': '#ef6b73',
'Pontianak Selatan': '#f0c86a',
'Pontianak Utara': '#e8c45d',
'Pontianak Barat': '#8f6cc6',
'Pontianak Kota': '#83be86',
'Pontianak Tenggara': '#eec8d8'
};
const utm49s = '+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs';
const wgs84 = 'WGS84';
function utm49sToLngLat(easting, northing) {
// Fallback konversi UTM 49S -> WGS84 ketika proj4 tidak tersedia.
const a = 6378137.0;
const f = 1 / 298.257223563;
const e2 = f * (2 - f);
const ep2 = e2 / (1 - e2);
const k0 = 0.9996;
const falseEasting = 500000;
const falseNorthing = 10000000;
const zone = 49;
const x = Number(easting) - falseEasting;
const y = Number(northing) - falseNorthing;
const m = y / k0;
const mu = m / (a * (1 - e2 / 4 - (3 * e2 * e2) / 64 - (5 * e2 * e2 * e2) / 256));
const e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2));
const j1 = (3 * e1 / 2) - (27 * Math.pow(e1, 3) / 32);
const j2 = (21 * e1 * e1 / 16) - (55 * Math.pow(e1, 4) / 32);
const j3 = (151 * Math.pow(e1, 3) / 96);
const j4 = (1097 * Math.pow(e1, 4) / 512);
const fp = mu + j1 * Math.sin(2 * mu) + j2 * Math.sin(4 * mu) + j3 * Math.sin(6 * mu) + j4 * Math.sin(8 * mu);
const sinFp = Math.sin(fp);
const cosFp = Math.cos(fp);
const tanFp = Math.tan(fp);
const c1 = ep2 * cosFp * cosFp;
const t1 = tanFp * tanFp;
const n1 = a / Math.sqrt(1 - e2 * sinFp * sinFp);
const r1 = (a * (1 - e2)) / Math.pow(1 - e2 * sinFp * sinFp, 1.5);
const d = x / (n1 * k0);
const lat = fp - (n1 * tanFp / r1) * (
(d * d) / 2 -
((5 + 3 * t1 + 10 * c1 - 4 * c1 * c1 - 9 * ep2) * Math.pow(d, 4)) / 24 +
((61 + 90 * t1 + 298 * c1 + 45 * t1 * t1 - 252 * ep2 - 3 * c1 * c1) * Math.pow(d, 6)) / 720
);
const lon0 = ((zone - 1) * 6 - 180 + 3) * Math.PI / 180;
const lon = lon0 + (
d -
((1 + 2 * t1 + c1) * Math.pow(d, 3)) / 6 +
((5 - 2 * c1 + 28 * t1 - 3 * c1 * c1 + 8 * ep2 + 24 * t1 * t1) * Math.pow(d, 5)) / 120
) / cosFp;
return [lon * 180 / Math.PI, lat * 180 / Math.PI];
}
function isProjectedCoordinate(coord) {
if (!Array.isArray(coord) || coord.length < 2) return false;
return Math.abs(coord[0]) > 180 || Math.abs(coord[1]) > 90;
}
function convertProjectedPointToLngLat(coord) {
if (!Array.isArray(coord) || coord.length < 2) return coord;
if (isProjectedCoordinate(coord)) {
if (typeof proj4 === 'function') {
try {
const converted = proj4(utm49s, wgs84, [Number(coord[0]), Number(coord[1])]);
if (Array.isArray(converted) && Number.isFinite(converted[0]) && Number.isFinite(converted[1])) {
return [converted[0], converted[1]];
}
} catch (_) {
// fallback ke konversi manual
}
}
return utm49sToLngLat(coord[0], coord[1]);
}
return [Number(coord[0]), Number(coord[1])];
}
function reprojectGeoJSONCoordinates(coords) {
if (!Array.isArray(coords) || coords.length === 0) return coords;
if (typeof coords[0] === 'number') return convertProjectedPointToLngLat(coords);
return coords.map(reprojectGeoJSONCoordinates);
}
function normalizeAdminKecamatanGeoJSON(geojson) {
return {
...geojson,
features: (geojson.features || []).map(feature => ({
...feature,
geometry: {
...feature.geometry,
coordinates: reprojectGeoJSONCoordinates(feature.geometry.coordinates)
}
}))
};
}
function getKecamatanColor(name) {
return kecColorMap[name] || '#d9b26f';
}
function loadAdminKecamatan() {
fetch('Admin_Kecamatan.json')
.then(res => res.json())
.then(raw => {
const geojson = normalizeAdminKecamatanGeoJSON(raw);
const kecLayer = L.geoJSON(geojson, {
interactive: false,
style: feature => {
const name = (feature.properties && feature.properties.Ket) || 'Kecamatan';
const fill = getKecamatanColor(name);
return {
color: '#232831',
weight: 3,
fillColor: fill,
fillOpacity: 0.4
};
}
});
kecLayer.addTo(layers.kecamatan);
kecLayer.bringToBack();
const bounds = kecLayer.getBounds();
if (bounds && bounds.isValid()) {
map.fitBounds(bounds, { padding: [24, 24], maxZoom: 13 });
}
// log removed
})
.catch(err => {});
}
/* ΓöÇΓöÇ MODE ΓöÇΓöÇ */
function setMode(m) {
cancelDrawing();
state.mode = state.mode === m ? null : m;
['kemiskinan', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(x => {
const el = document.getElementById('btn-' + x);
if (el) el.className = 'mbtn';
});
const hint = document.getElementById('hint');
hint.classList.remove('show');
if (state.mode) {
const msgs = {
kemiskinan: 'Klik peta untuk menambah data Kemiskinan',
masjid: 'Klik peta untuk menambah titik Rumah Ibadah',
spbu: 'Klik peta untuk menambah titik SPBU',
jalan: 'Klik untuk gambar jalan ΓÇö klik ganda untuk selesai',
parsil: 'Klik untuk gambar parsil ΓÇö klik titik awal atau dbl-klik selesai',
line: 'Klik untuk gambar jalan ΓÇö klik ganda untuk selesai',
poly: 'Klik untuk gambar parsil ΓÇö klik titik awal atau dbl-klik selesai',
};
hint.textContent = msgs[m] || 'Klik peta';
hint.classList.add('show');
map.getContainer().style.cursor = 'crosshair';
} else {
map.getContainer().style.cursor = '';
}
}
/* ΓöÇΓöÇ MAP EVENTS ΓöÇΓöÇ */
map.on('click', function (e) {
if (window._kkPickingLocation) {
finishKemiskinanLocationPick(e);
return;
}
if (!state.mode) return;
if (state.mode === 'kemiskinan') handleKemiskinanClick(e);
else if (state.mode === 'masjid') handleMasjidClick(e);
else if (state.mode === 'spbu') handlePointClick(e);
else if (state.mode === 'jalan') handleLineClick(e);
else if (state.mode === 'parsil') handlePolyClick(e);
});
map.on('dblclick', function (e) {
if ((state.mode === 'jalan' || state.mode === 'line') && state.drawingLine) finishLine(e);
if ((state.mode === 'parsil' || state.mode === 'poly') && state.drawingPoly) finishPoly(e);
if (['jalan','parsil','line','poly'].includes(state.mode)) L.DomEvent.stopPropagation(e);
});
/* ── MODERN MARKER ICON ── */
function createModernMarker(color, iconClass, label) {
const html = `
<div class="modern-marker" style="--marker-color: ${color}">
<div class="pulse"></div>
<div class="pin-body">
<div class="icon-container">
<i class="${iconClass}"></i>
</div>
</div>
<div class="pin-label">${label}</div>
</div>
`;
return L.divIcon({
html: html,
className: '',
iconSize: [40, 40],
iconAnchor: [20, 38],
popupAnchor: [0, -42]
});
}
/* ΓöÇΓöÇ POINT SPBU ΓöÇΓöÇ */
window.updateTempMarkerIcon = function() {
if (!window._tmpM) return;
const s = document.getElementById('p-status') ? document.getElementById('p-status').value : 'Ya';
const color = s === 'Ya' ? '#10b981' : '#ef4444';
window._tmpM.setIcon(createModernMarker(color, 'fas fa-gas-pump', 'SPBU Baru'));
};
function handlePointClick(e) {
const icon = createModernMarker('#10b981', 'fas fa-gas-pump', 'SPBU Baru');
const tmpM = L.marker(e.latlng, { icon, draggable: true }).addTo(map);
const upd = ll => {
const a = document.getElementById('p-lat'), b = document.getElementById('p-lng');
if (a) a.value = ll.lat.toFixed(6);
if (b) b.value = ll.lng.toFixed(6);
};
tmpM.on('dragend', ev => upd(ev.target.getLatLng()));
const form = `<div class="pf">
<h4><i class="fas fa-gas-pump" style="color:var(--green)"></i> Tambah SPBU</h4>
<label>Nama SPBU</label>
<input id="p-nama" placeholder="Pertamina Ahmad Yani">
<label>Nomor SPBU</label>
<input id="p-no" placeholder="64.xxx.xx">
<label>Status Operasional</label>
<select id="p-status" onchange="window.updateTempMarkerIcon()">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak Buka 24 Jam</option>
</select>
<div class="coord-row">
<input id="p-lat" value="${e.latlng.lat.toFixed(6)}" readonly>
<input id="p-lng" value="${e.latlng.lng.toFixed(6)}" readonly>
</div>
<div class="hint-txt">* Geser marker untuk menyesuaikan posisi</div>
<button class="pbtn pbtn-g" onclick="savePoint()">Simpan SPBU</button>
</div>`;
tmpM.bindPopup(form, { maxWidth: 280, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).openPopup();
tmpM.on('popupclose', () => { if (map.hasLayer(tmpM)) map.removeLayer(tmpM); });
window._tmpM = tmpM;
}
function savePoint() {
const nama = document.getElementById('p-nama').value.trim();
const no = document.getElementById('p-no').value.trim();
const status = document.getElementById('p-status').value;
const lat = parseFloat(document.getElementById('p-lat').value);
const lng = parseFloat(document.getElementById('p-lng').value);
if (!nama || !no) {
alert('Lengkapi nama dan nomor SPBU!');
return;
}
fetch("api/save_data.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
nama: nama,
no: no,
status: status,
lat: lat,
lng: lng
})
})
.then(res => res.json())
.then(response => {
if (response.status == "success") {
const d = {
id: response.id || Date.now(),
nama,
no,
status,
lat,
lng
};
state.spbu.push(d);
renderSPBU(d);
updateCount('spbu', state.spbu.length);
addCard(
'spbu',
d.id,
nama,
'No: ' + no,
status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam',
status === 'Ya' ? '#1a6b3c' : '#b93328'
);
if (window._tmpM) {
map.closePopup();
map.removeLayer(window._tmpM);
window._tmpM = null;
}
setMode(null);
ensureLayerVisible('spbu', d.id);
focusFeature('spbu', d.id);
} else {
alert("Gagal menyimpan: " + (response.message || "Periksa database"));
}
})
.catch(err => {
console.error(err);
alert("Terjadi error jaringan");
});
}
function renderSPBU(d) {
const color = d.status === 'Ya' ? '#10b981' : '#ef4444';
const icon = createModernMarker(color, 'fas fa-gas-pump', d.nama);
const m = L.marker([d.lat, d.lng], { icon, draggable: true });
m.bindPopup(`<div class="pi">
<h4><i class="fas fa-gas-pump" style="color:${color}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
<div class="pi-body">
<div class="pi-row"><span class="pi-k">No. SPBU</span><span class="pi-v">${d.no}</span></div>
<div class="pi-row"><span class="pi-k">Status</span><span class="pi-v" style="color:${color}">${d.status === 'Ya' ? '&#10004; Buka 24 Jam' : '&#10006; Tidak Buka 24 Jam'}</span></div>
<div class="pi-row"><span class="pi-k">Koordinat</span><span class="pi-v" style="font-size:11px;font-family:monospace">${d.lat.toFixed(5)}, ${d.lng.toFixed(5)}</span></div>
</div>
<div class="act-row">
<button class="pbtn pbtn-y" onclick="openEditSpbu(${d.id})">Edit</button>
<button class="pbtn pbtn-r" onclick="deleteFeature('spbu',${d.id})">Hapus</button>
</div>
</div>`, { autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] });
m.addTo(d.status === 'Ya' ? layers.spbu24 : layers.spbuNon24);
state.featureRefs.spbu[String(d.id)] = m;
}
/* ── JALAN ── */
function handleLineClick(e) {
if (!state.drawingLine) state.drawingLine = true;
state.tempPoints.push(e.latlng);
const vm = L.circleMarker(e.latlng, { radius: 4, color: '#e74c3c', fillColor: '#e74c3c', fillOpacity: 1, weight: 2 }).addTo(map);
state.tempMarkers.push(vm);
if (state.tempPolyline) map.removeLayer(state.tempPolyline);
state.tempPolyline = L.polyline(state.tempPoints, { color: '#e74c3c', weight: 3, dashArray: '7,5', opacity: 0.8 }).addTo(map);
document.getElementById('hint').textContent = `📍 ${state.tempPoints.length} titik — klik ganda untuk selesai`;
}
function finishLine(e) {
if (state.tempPoints.length < 2) { alert('Minimal 2 titik!'); return; }
const pts = [...state.tempPoints];
const len = calcLen(pts);
window._tempLinePts = pts;
window._tempLineLen = len;
clearTemp();
L.popup({ closeOnClick: false, keepInView: true, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
.setLatLng(pts[Math.floor(pts.length / 2)])
.setContent(`<div class="pf">
<h4><i class="fas fa-road"></i> Tambah Jalan</h4>
<label>Nama Jalan</label>
<input id="j-nama" placeholder="Jl. Ahmad Yani">
<label>Status Jalan</label>
<select id="j-status">
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten" selected>Jalan Kabupaten</option>
</select>
<div style="display:flex;align-items:center;gap:10px;margin:10px 16px 4px;padding:9px 12px;background:#eff6ff;border-radius:9px;border:1px solid #dbeafe">
<i class="fas fa-ruler-horizontal" style="color:#2563eb;font-size:13px"></i>
<span style="font-size:11px;color:#64748b;font-weight:600">Panjang terukur:</span>
<span style="font-size:14px;font-weight:800;color:#1d4ed8;margin-left:auto">${fmtLen(len)}</span>
</div>
<button class="pbtn pbtn-b" onclick="saveLine()">Simpan Jalan</button>
</div>`).addTo(map);
}
function saveLine() {
const pts = window._tempLinePts || [];
const len = window._tempLineLen || 0;
const nama = document.getElementById('j-nama').value.trim();
const status = document.getElementById('j-status').value;
if (!nama) {
alert('Isi nama jalan!');
return;
}
fetch("api/save_jalan.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
nama: nama,
status: status,
panjang: len,
koordinat: pts
})
})
.then(res => res.json())
.then(response => {
if (response.status == "success") {
const d = {
id: response.id || Date.now(),
nama,
status,
pts,
len
};
state.jalan.push(d);
renderJalan(d);
updateCount('jalan', state.jalan.length);
addCard(
'jalan',
d.id,
nama,
status + ' · ' + fmtLen(len),
status,
jColor[status]
);
map.closePopup();
setMode(null);
ensureLayerVisible('jalan', d.id);
focusFeature('jalan', d.id);
} else {
// log removed
alert("Gagal simpan jalan");
}
})
.catch(err => {
// log removed
alert("Error simpan jalan");
});
}
function renderJalan(d) {
const pts = normalizeLinePoints(d.pts);
if (pts.length < 2) {
// log removed
return;
}
const c = jColor[d.status] || '#ef4444';
const w =
d.status === 'Nasional' ? 6 :
d.status === 'Provinsi' ? 5 : 4;
const line = L.polyline(pts, {
color: c,
weight: w,
opacity: 0.95,
lineCap: 'round',
lineJoin: 'round',
className: 'path-glow'
})
.bindPopup(`
<div class="pi">
<h4><i class="fas fa-road" style="color:${c}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
<div class="pi-body">
<div class="pi-row">
<span class="pi-k">Status</span>
<span class="pi-v">${d.status}</span>
</div>
<div class="pi-row">
<span class="pi-k">Panjang</span>
<span class="pi-v">${fmtLen(d.len)}</span>
</div>
</div>
<div class="act-row">
<button class="pbtn pbtn-y" onclick="openEditJalan(${d.id})">Edit</button>
<button class="pbtn pbtn-r" onclick="deleteFeature('jalan',${d.id})">Hapus</button>
</div>
</div>
`, { autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
.addTo(layers.jalan);
state.featureRefs.jalan[String(d.id)] = line;
}
function normalizeLinePoints(raw) {
let pts = raw;
if (typeof pts === 'string') {
try {
pts = JSON.parse(pts);
} catch (_) {
return [];
}
}
// Kompatibilitas data lama: koordinat sempat tersimpan sebagai JSON string di dalam JSON string.
if (typeof pts === 'string') {
try {
pts = JSON.parse(pts);
} catch (_) {
return [];
}
}
if (!Array.isArray(pts) || pts.length === 0) return [];
if (Array.isArray(pts[0])) {
return pts
.map(p => [parseFloat(p[0]), parseFloat(p[1])])
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
}
if (pts[0] && typeof pts[0] === 'object' && pts[0].lat !== undefined) {
return pts
.map(p => [parseFloat(p.lat), parseFloat(p.lng)])
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
}
return [];
}
function normalizePolyPoints(raw) {
let pts = raw;
if (typeof pts === 'string') {
try {
pts = JSON.parse(pts);
} catch (_) {
return [];
}
}
if (typeof pts === 'string') {
try {
pts = JSON.parse(pts);
} catch (_) {
return [];
}
}
if (!Array.isArray(pts) || pts.length === 0) return [];
if (Array.isArray(pts[0])) {
return pts
.map(p => [parseFloat(p[0]), parseFloat(p[1])])
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
}
if (pts[0] && typeof pts[0] === 'object' && pts[0].lat !== undefined) {
return pts
.map(p => [parseFloat(p.lat), parseFloat(p.lng)])
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
}
return [];
}
/* ── PARSIL ── */
function handlePolyClick(e) {
if (!state.drawingPoly) state.drawingPoly = true;
if (state.tempPoints.length >= 3) {
const dist = map.distance(e.latlng, state.tempPoints[0]);
if (dist < 15) { finishPoly(null); return; }
}
state.tempPoints.push(e.latlng);
const vm = L.circleMarker(e.latlng, { radius: 4, color: '#1a5276', fillColor: '#1a5276', fillOpacity: 1, weight: 2 }).addTo(map);
state.tempMarkers.push(vm);
if (state.tempPolygon) map.removeLayer(state.tempPolygon);
if (state.tempPolyline) map.removeLayer(state.tempPolyline);
if (state.tempPoints.length >= 2)
state.tempPolyline = L.polyline([...state.tempPoints, state.tempPoints[0]],
{ color: '#1a5276', weight: 2, dashArray: '5,4', opacity: 0.7 }).addTo(map);
if (state.tempPoints.length >= 3)
state.tempPolygon = L.polygon(state.tempPoints,
{ color: '#1a5276', fillColor: '#1a5276', fillOpacity: 0.1, weight: 2, dashArray: '5,4' }).addTo(map);
document.getElementById('hint').textContent =
`🗺️ ${state.tempPoints.length} titik — klik titik awal atau dbl-klik selesai`;
}
function finishPoly(e) {
if (state.tempPoints.length < 3) { alert('Minimal 3 titik!'); return; }
const pts = [...state.tempPoints];
const area = calcArea(pts);
window._tempPolyPts = pts;
window._tempPolyArea = area;
clearTemp();
const center = L.polygon(pts).getBounds().getCenter();
L.popup({ closeOnClick: false, keepInView: true, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
.setLatLng(center)
.setContent(`<div class="pf">
<h4><i class="fas fa-vector-square"></i> Tambah Parsil / Lahan</h4>
<label>Nama / ID Parsil</label>
<input id="ps-nama" placeholder="Parsil A-01">
<label>Pemilik</label>
<input id="ps-pemilik" placeholder="Nama pemilik">
<label>Status Sertifikat</label>
<select id="ps-status">
<option value="SHM">SHM &mdash; Hak Milik</option>
<option value="HGB">HGB &mdash; Hak Guna Bangunan</option>
<option value="Sewa">Sewa / Kontrak</option>
<option value="Lainnya">Lainnya</option>
</select>
<div style="display:flex;align-items:center;gap:10px;margin:10px 16px 4px;padding:9px 12px;background:#fffbeb;border-radius:9px;border:1px solid #fde68a">
<i class="fas fa-ruler-combined" style="color:#d97706;font-size:13px"></i>
<span style="font-size:11px;color:#64748b;font-weight:600">Luas terukur:</span>
<span style="font-size:14px;font-weight:800;color:#d97706;margin-left:auto">${fmtArea(area)}</span>
</div>
<button class="pbtn pbtn-y" onclick="saveParsil()">Simpan Parsil</button>
</div>`).addTo(map);
}
function saveParsil() {
const pts = window._tempPolyPts || [];
const area = window._tempPolyArea || 0;
const nama = document.getElementById('ps-nama').value.trim();
const pemilik = document.getElementById('ps-pemilik').value.trim();
const status = document.getElementById('ps-status').value;
if (!nama) {
alert('Isi nama parsil!');
return;
}
fetch("api/save_parsil.php", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
nama: nama,
pemilik: pemilik,
status: status,
luas: area,
koordinat: pts
})
})
.then(res => res.json())
.then(response => {
if (response.status == "success") {
const parsil = {
id: response.id || Date.now(),
nama,
pemilik,
status,
pts,
area
};
state.parsil.push(parsil);
renderParsil(parsil);
updateCount('parsil', state.parsil.length);
addCard(
'parsil',
parsil.id,
parsil.nama,
parsil.status,
parsil.status,
'#1a5276'
);
map.closePopup();
setMode(null);
ensureLayerVisible('parsil', parsil.id);
focusFeature('parsil', parsil.id);
}
else {
// log removed
alert("Gagal simpan parsil");
}
})
.catch(err => {});
}
function renderParsil(d) {
const pts = normalizePolyPoints(d.pts);
if (pts.length < 3) {
// log removed
return;
}
const c = pColor[d.status];
const poly = L.polygon(pts, {
color: c,
fillColor: c,
fillOpacity: 0.25,
weight: 3,
dashArray: '8, 6',
lineCap: 'round',
lineJoin: 'round',
className: 'parsil-glow'
})
.bindPopup(`
<div class="pi">
<h4><i class="fas fa-draw-polygon" style="color:${c}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
<div class="pi-body">
<div class="pi-row">
<span class="pi-k">Pemilik</span>
<span class="pi-v">${d.pemilik || '-'}</span>
</div>
<div class="pi-row">
<span class="pi-k">Status</span>
<span class="pi-v">${d.status}</span>
</div>
<div class="pi-row">
<span class="pi-k">Luas</span>
<span class="pi-v">${fmtArea(d.area)}</span>
</div>
</div>
<div class="act-row">
<button class="pbtn pbtn-y" onclick="openEditParsil(${d.id})">Edit</button>
<button class="pbtn pbtn-r" onclick="deleteFeature('parsil',${d.id})">Hapus</button>
</div>
</div>
`)
.addTo(layers.parsil);
state.featureRefs.parsil[String(d.id)] = poly;
}
function getById(type, id) {
return state[type].find(x => String(x.id) === String(id));
}
function pointsToText(points) {
return points.map(p => `${Number(p[0]).toFixed(6)},${Number(p[1]).toFixed(6)}`).join('\n');
}
function textToPoints(text, minPoints) {
const rows = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
const pts = [];
for (const row of rows) {
const parts = row.split(',').map(x => x.trim());
if (parts.length !== 2) return null;
const lat = parseFloat(parts[0]);
const lng = parseFloat(parts[1]);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
pts.push([lat, lng]);
}
if (pts.length < minPoints) return null;
return pts;
}
function toLatLngObjects(pts) {
return pts.map(p => ({ lat: parseFloat(p[0]), lng: parseFloat(p[1]) }));
}
function makeVertexIcon() {
return L.divIcon({
className: '',
html: '<div style="width:12px;height:12px;border-radius:50%;background:#fdfdfd;border:2px solid #1b3a5c;box-shadow:0 4px 12px rgba(15,23,42,.25);"></div>',
iconSize: [16, 16],
iconAnchor: [8, 8]
});
}
function syncEditTextarea(type, points) {
const id = type === 'jalan' ? 'e-jalan-pts' : 'e-parsil-pts';
const el = document.getElementById(id);
if (el) el.value = pointsToText(points);
}
function stopGeometryEdit() {
if (!state.geometryEdit) return;
const g = state.geometryEdit;
if (g.overlay && map.hasLayer(g.overlay)) map.removeLayer(g.overlay);
(g.vertexMarkers || []).forEach(m => { if (map.hasLayer(m)) map.removeLayer(m); });
if (g.onMove) map.off('mousemove', g.onMove);
if (g.onUp) map.off('mouseup', g.onUp);
if (g.onMapClick) map.off('click', g.onMapClick);
map.dragging.enable();
const ref = state.featureRefs[g.type][String(g.id)];
if (ref && ref.setStyle) {
if (g.type === 'jalan') ref.setStyle({ opacity: 0.9, interactive: true });
if (g.type === 'parsil') ref.setStyle({ opacity: 1, fillOpacity: 0.18, interactive: true });
}
state.geometryEdit = null;
const hint = document.getElementById('hint');
hint.classList.remove('show');
}
function startGeometryEdit(type, id) {
if (type !== 'jalan' && type !== 'parsil') return;
const d = getById(type, id);
if (!d) return;
const points = type === 'jalan' ? normalizeLinePoints(d.pts) : normalizePolyPoints(d.pts);
const minPts = type === 'jalan' ? 2 : 3;
if (points.length < minPts) {
alert('Koordinat belum valid untuk diedit di peta');
return;
}
stopGeometryEdit();
setMode(null);
ensureLayerVisible(type);
const ref = state.featureRefs[type][String(id)];
if (ref && ref.setStyle) {
if (type === 'jalan') ref.setStyle({ opacity: 0.2, interactive: false });
if (type === 'parsil') ref.setStyle({ opacity: 0.2, fillOpacity: 0.05, interactive: false });
}
const overlay = type === 'jalan'
? L.polyline(points, { color: '#1b3a5c', weight: 4, dashArray: '8,6', opacity: 0.95 }).addTo(map)
: L.polygon(points, { color: '#1b3a5c', weight: 3, dashArray: '8,6', fillColor: '#1b3a5c', fillOpacity: 0.12 }).addTo(map);
const g = {
type,
id,
points: points.map(p => [p[0], p[1]]),
overlay,
vertexMarkers: [],
draggingAll: false,
lastLatLng: null,
onMove: null,
onUp: null,
onMapClick: null
};
const redraw = () => {
if (g.type === 'jalan') g.overlay.setLatLngs(g.points);
else g.overlay.setLatLngs(g.points);
g.vertexMarkers.forEach((m, i) => m.setLatLng(g.points[i]));
syncEditTextarea(g.type, g.points);
};
const icon = makeVertexIcon();
g.points.forEach((p, i) => {
const vm = L.marker(p, { draggable: true, icon, zIndexOffset: 1000 }).addTo(map);
vm.on('drag', ev => {
const ll = ev.target.getLatLng();
g.points[i] = [ll.lat, ll.lng];
redraw();
});
g.vertexMarkers.push(vm);
});
const startDragAll = (latlng) => {
g.draggingAll = true;
g.lastLatLng = latlng;
map.dragging.disable();
};
g.overlay.on('mousedown', e => {
L.DomEvent.stopPropagation(e);
startDragAll(e.latlng);
});
g.onMapClick = (e) => {
if (e.originalEvent && e.originalEvent.shiftKey) {
startDragAll(e.latlng);
}
};
g.onMove = (e) => {
if (!g.draggingAll || !g.lastLatLng) return;
const dLat = e.latlng.lat - g.lastLatLng.lat;
const dLng = e.latlng.lng - g.lastLatLng.lng;
g.points = g.points.map(p => [p[0] + dLat, p[1] + dLng]);
g.lastLatLng = e.latlng;
redraw();
};
g.onUp = () => {
g.draggingAll = false;
g.lastLatLng = null;
map.dragging.enable();
};
map.on('click', g.onMapClick);
map.on('mousemove', g.onMove);
map.on('mouseup', g.onUp);
state.geometryEdit = g;
syncEditTextarea(type, g.points);
const hint = document.getElementById('hint');
hint.textContent = 'Mode edit geometri aktif: drag titik vertex atau drag shape untuk geser keseluruhan.';
hint.classList.add('show');
}
function refreshType(type) {
if (state.geometryEdit && state.geometryEdit.type === type) stopGeometryEdit();
if (type === 'spbu') {
layers.spbu24.clearLayers();
layers.spbuNon24.clearLayers();
} else {
layers[type].clearLayers();
}
state.featureRefs[type] = {};
if (type === 'kemiskinan') state.kkLayerRefs = {};
const list = document.getElementById('list-' + type);
if (list) list.querySelectorAll('.dcard').forEach(x => x.remove());
state[type].forEach(d => {
if (type === 'spbu') {
renderSPBU(d);
addCard('spbu', d.id, d.nama, 'No: ' + d.no, d.status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam', d.status === 'Ya' ? '#1a6b3c' : '#b93328');
return;
}
if (type === 'jalan') {
renderJalan(d);
addCard('jalan', d.id, d.nama, d.status + ' · ' + fmtLen(d.len), d.status, jColor[d.status] || '#e74c3c');
return;
}
if (type === 'kemiskinan') {
renderKemiskinan(d);
addCard('kemiskinan', d.id, d.nama_kk, 'Jumlah KK: ' + d.jumlah_kk, 'Kemiskinan', '#9f6500');
return;
}
if (type === 'masjid') {
renderMasjid(d);
addCard('masjid', d.id, d.nama_masjid, d.pic_masjid, 'Rumah Ibadah', '#0f6250');
return;
}
renderParsil(d);
addCard('parsil', d.id, d.nama, d.status, d.status, '#1a5276');
});
updateCount(type, state[type].length);
renderDashboard();
}
function openEditSpbu(id) {
const d = getById('spbu', id);
if (!d) return;
const ref = state.featureRefs.spbu[String(id)];
const ll = ref && ref.getLatLng ? ref.getLatLng() : { lat: d.lat, lng: d.lng };
const html = `<div class="pf">
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit SPBU</h4>
<label>Nama SPBU</label>
<input id="e-spbu-nama" value="${(d.nama || '').replace(/"/g, '&quot;')}">
<label>Nomor SPBU</label>
<input id="e-spbu-no" value="${(d.no || '').replace(/"/g, '&quot;')}">
<label>Status Operasional</label>
<select id="e-spbu-status">
<option value="Ya" ${d.status === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
<option value="Tidak" ${d.status === 'Tidak' ? 'selected' : ''}>Tidak Buka 24 Jam</option>
</select>
<label>Latitude</label>
<input id="e-spbu-lat" value="${Number(ll.lat).toFixed(6)}">
<label>Longitude</label>
<input id="e-spbu-lng" value="${Number(ll.lng).toFixed(6)}">
<button class="pbtn pbtn-y" onclick="saveEditSpbu(${id})">Simpan Perubahan</button>
</div>`;
map.closePopup();
L.popup({ maxWidth: 290, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng([ll.lat, ll.lng]).setContent(html).openOn(map);
}
function saveEditSpbu(id) {
const nama = document.getElementById('e-spbu-nama').value.trim();
const no = document.getElementById('e-spbu-no').value.trim();
const status = document.getElementById('e-spbu-status').value;
const lat = parseFloat(document.getElementById('e-spbu-lat').value);
const lng = parseFloat(document.getElementById('e-spbu-lng').value);
if (!nama || !no || !Number.isFinite(lat) || !Number.isFinite(lng)) {
alert('Data SPBU belum valid');
return;
}
fetch('api/update_data.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, no, status, lat, lng })
})
.then(res => res.json())
.then(resp => {
if (resp.status !== 'success') {
alert('Gagal update SPBU');
return;
}
const d = getById('spbu', id);
if (!d) return;
d.nama = nama; d.no = no; d.status = status; d.lat = lat; d.lng = lng;
refreshType('spbu');
map.closePopup();
focusFeature('spbu', id);
})
.catch(() => alert('Error update SPBU'));
}
function openEditJalan(id) {
if (state.geometryEdit && (state.geometryEdit.type !== 'jalan' || String(state.geometryEdit.id) !== String(id))) {
stopGeometryEdit();
}
const d = getById('jalan', id);
if (!d) return;
const pts = normalizeLinePoints(d.pts);
const ref = state.featureRefs.jalan[String(id)];
const center = ref && ref.getBounds ? ref.getBounds().getCenter() : map.getCenter();
const html = `<div class="pf" style="width:290px">
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit Jalan</h4>
<label>Nama Jalan</label>
<input id="e-jalan-nama" value="${(d.nama || '').replace(/"/g, '&quot;')}">
<label>Status Jalan</label>
<select id="e-jalan-status">
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Jalan Nasional</option>
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Jalan Provinsi</option>
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Jalan Kabupaten</option>
</select>
<label>Koordinat Lokasi (satu baris: lat,lng)</label>
<textarea id="e-jalan-pts">${pointsToText(pts)}</textarea>
<button class="pbtn pbtn-b" onclick="startGeometryEdit('jalan',${id})">Edit Vertex / Drag di Peta</button>
<button class="pbtn pbtn-y" onclick="saveEditJalan(${id})">Simpan Perubahan</button>
</div>`;
map.closePopup();
L.popup({ maxWidth: 330, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng(center).setContent(html).openOn(map);
}
function saveEditJalan(id) {
if (state.geometryEdit && state.geometryEdit.type === 'jalan' && String(state.geometryEdit.id) === String(id)) {
syncEditTextarea('jalan', state.geometryEdit.points);
}
const nama = document.getElementById('e-jalan-nama').value.trim();
const status = document.getElementById('e-jalan-status').value;
const pts = textToPoints(document.getElementById('e-jalan-pts').value, 2);
if (!nama || !pts) {
alert('Data jalan atau koordinat belum valid');
return;
}
const len = calcLen(pts);
fetch('api/update_jalan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, status, panjang: len, koordinat: toLatLngObjects(pts) })
})
.then(res => res.json())
.then(resp => {
if (resp.status !== 'success') {
alert('Gagal update jalan');
return;
}
const d = getById('jalan', id);
if (!d) return;
d.nama = nama; d.status = status; d.len = len; d.pts = pts;
stopGeometryEdit();
refreshType('jalan');
map.closePopup();
focusFeature('jalan', id);
})
.catch(() => alert('Error update jalan'));
}
function openEditParsil(id) {
if (state.geometryEdit && (state.geometryEdit.type !== 'parsil' || String(state.geometryEdit.id) !== String(id))) {
stopGeometryEdit();
}
const d = getById('parsil', id);
if (!d) return;
const pts = normalizePolyPoints(d.pts);
const ref = state.featureRefs.parsil[String(id)];
const center = ref && ref.getBounds ? ref.getBounds().getCenter() : map.getCenter();
const html = `<div class="pf" style="width:290px">
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit Parsil</h4>
<label>Nama / ID Parsil</label>
<input id="e-parsil-nama" value="${(d.nama || '').replace(/"/g, '&quot;')}">
<label>Pemilik</label>
<input id="e-parsil-pemilik" value="${(d.pemilik || '').replace(/"/g, '&quot;')}">
<label>Status Sertifikat</label>
<select id="e-parsil-status">
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM – Hak Milik</option>
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB – Hak Guna Bangunan</option>
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU – Hak Guna Usaha</option>
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP – Hak Pakai</option>
</select>
<label>Koordinat Lokasi (satu baris: lat,lng)</label>
<textarea id="e-parsil-pts">${pointsToText(pts)}</textarea>
<button class="pbtn pbtn-b" onclick="startGeometryEdit('parsil',${id})">Edit Vertex / Drag di Peta</button>
<button class="pbtn pbtn-y" onclick="saveEditParsil(${id})">Simpan Perubahan</button>
</div>`;
map.closePopup();
L.popup({ maxWidth: 330, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng(center).setContent(html).openOn(map);
}
function saveEditParsil(id) {
if (state.geometryEdit && state.geometryEdit.type === 'parsil' && String(state.geometryEdit.id) === String(id)) {
syncEditTextarea('parsil', state.geometryEdit.points);
}
const nama = document.getElementById('e-parsil-nama').value.trim();
const pemilik = document.getElementById('e-parsil-pemilik').value.trim();
const status = document.getElementById('e-parsil-status').value;
const pts = textToPoints(document.getElementById('e-parsil-pts').value, 3);
if (!nama || !pts) {
alert('Data parsil atau koordinat belum valid');
return;
}
const area = calcArea(toLatLngObjects(pts));
fetch('api/update_parsil.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, pemilik, status, luas: area, koordinat: toLatLngObjects(pts) })
})
.then(res => res.json())
.then(resp => {
if (resp.status !== 'success') {
alert('Gagal update parsil');
return;
}
const d = getById('parsil', id);
if (!d) return;
d.nama = nama; d.pemilik = pemilik; d.status = status; d.area = area; d.pts = pts;
stopGeometryEdit();
refreshType('parsil');
map.closePopup();
focusFeature('parsil', id);
})
.catch(() => alert('Error update parsil'));
}
function deleteFeature(type, id) {
if (state.geometryEdit && state.geometryEdit.type === type && String(state.geometryEdit.id) === String(id)) {
stopGeometryEdit();
}
const label = type === 'spbu' ? 'SPBU' : type === 'jalan' ? 'Jalan' : type === 'kemiskinan' ? 'Kemiskinan' : type === 'masjid' ? 'Rumah Ibadah' : 'Parsil';
if (!confirm(`Hapus data ${label} ini?`)) return;
const ep = {
spbu: 'api/delete_data.php',
jalan: 'api/delete_jalan.php',
parsil: 'api/delete_parsil.php',
kemiskinan: 'api/delete_kemiskinan.php',
masjid: 'api/delete_masjid.php'
};
fetch(ep[type], {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(resp => {
if (resp.status !== 'success') {
alert('Gagal menghapus data');
return;
}
state[type] = state[type].filter(x => String(x.id) !== String(id));
if (state.activeCard === 'card-' + type + '-' + id) state.activeCard = null;
refreshType(type);
map.closePopup();
})
.catch(() => alert('Error hapus data'));
}
/* ── HELPERS ── */
function calcLen(pts) {
let t = 0;
for (let i = 1; i < pts.length; i++)t += L.latLng(pts[i - 1]).distanceTo(L.latLng(pts[i]));
return t;
}
function fmtLen(m) { return m >= 1000 ? (m / 1000).toFixed(2) + ' km' : Math.round(m) + ' m' }
function calcArea(pts) {
let a = 0; const n = pts.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
const la1 = pts[i].lat * Math.PI / 180, la2 = pts[j].lat * Math.PI / 180;
const dl = (pts[j].lng - pts[i].lng) * Math.PI / 180;
a += Math.sin(dl) * (2 + Math.sin(la1) + Math.sin(la2));
}
return Math.abs(a * 6378137 * 6378137 / 2);
}
function fmtArea(m) { return m >= 10000 ? (m / 10000).toFixed(2) + ' ha' : Math.round(m) + ' m²' }
function clearTemp() {
state.tempPoints = []; state.drawingLine = false; state.drawingPoly = false;
state.tempMarkers.forEach(m => map.removeLayer(m)); state.tempMarkers = [];
if (state.tempPolyline) { map.removeLayer(state.tempPolyline); state.tempPolyline = null; }
if (state.tempPolygon) { map.removeLayer(state.tempPolygon); state.tempPolygon = null; }
}
function cancelDrawing() {
clearTemp();
if (window._tmpM) { map.removeLayer(window._tmpM); window._tmpM = null; }
if (window._tmpC) { map.removeLayer(window._tmpC); window._tmpC = null; }
}
function ensureLayerVisible(type, id) {
if (type === 'spbu') {
const d = id !== undefined ? getById('spbu', id) : null;
if (d) {
const targetLayer = d.status === 'Ya' ? layers.spbu24 : layers.spbuNon24;
map.addLayer(targetLayer);
} else {
if (state.spbuFilterVis.open24) map.addLayer(layers.spbu24);
if (state.spbuFilterVis.non24) map.addLayer(layers.spbuNon24);
}
return;
}
map.addLayer(layers[type]);
}
function setActiveCard(type, id) {
if (state.activeCard) {
const prev = document.getElementById(state.activeCard);
if (prev) prev.style.boxShadow = '';
}
const cardId = 'card-' + type + '-' + id;
const cur = document.getElementById(cardId);
if (cur) {
cur.style.boxShadow = '0 0 0 2px rgba(27,58,92,0.28), 0 2px 8px rgba(0,0,0,0.1)';
state.activeCard = cardId;
}
}
function focusFeature(type, id) {
const ref = state.featureRefs[type][String(id)];
if (!ref) return;
map.invalidateSize();
ensureLayerVisible(type, id);
setActiveCard(type, id);
if (ref.getLatLng) {
map.flyTo(ref.getLatLng(), 17, { duration: 0.7 });
} else if (ref.getBounds) {
map.fitBounds(ref.getBounds(), { padding: [40, 40], maxZoom: 17 });
}
if (ref.openPopup) {
setTimeout(() => ref.openPopup(), 800);
}
}
function updateCount(type, n) {
const el = document.getElementById('cnt-' + type);
if (el) el.textContent = n;
}
function addCard(type, id, name, sub, badge, color) {
const c = document.getElementById('list-' + type);
const el = document.createElement('div');
el.className = 'dcard'; el.id = 'card-' + type + '-' + id;
el.tabIndex = 0;
el.innerHTML = `<div style="position:absolute;left:0;top:0;bottom:0;width:3px;background:${color};border-radius:8px 0 0 8px"></div>
<div class="dc-name">${name}</div>
<div class="dc-sub">${sub}</div>
<div class="dc-badge" style="background:${color}20;color:${color}">${badge}</div>`;
el.onclick = () => focusFeature(type, id);
el.onkeydown = (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
focusFeature(type, id);
}
};
if (!c) { console.warn('addCard: container list-' + type + ' not found'); return; }
c.appendChild(el);
}
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
stopGeometryEdit();
cancelDrawing();
setMode(null);
}
});
document.addEventListener("DOMContentLoaded", function () {
loadSPBU();
loadJalan();
loadParsil();
// Kemiskinan butuh data Masjid untuk hitung Blank Spot (merah/hijau)
// Jadi pastikan loadMasjid selesai dulu baru loadKemiskinan
loadMasjid().then(() => {
loadKemiskinan();
});
});
function loadSPBU() {
fetch("api/read_data.php")
.then(res => res.json())
.then(data => {
data.forEach(d => {
const spbu = {
id: d.id,
nama: d.nama,
no: d.no,
status: d.status,
lat: parseFloat(d.lat),
lng: parseFloat(d.lng)
};
state.spbu.push(spbu);
renderSPBU(spbu);
updateCount('spbu', state.spbu.length);
addCard(
'spbu',
spbu.id,
spbu.nama,
'No: ' + spbu.no,
spbu.status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam',
spbu.status === 'Ya' ? '#1a6b3c' : '#b93328'
);
});
})
.catch(err => {});
}
function loadJalan() {
fetch("api/read_jalan.php")
.then(res => res.json())
.then(data => {
data.forEach(d => {
const jalan = {
id: d.id,
nama: d.nama,
status: d.status,
len: parseFloat(d.panjang),
pts: d.koordinat
};
state.jalan.push(jalan);
renderJalan(jalan);
updateCount('jalan', state.jalan.length);
addCard(
'jalan',
jalan.id,
jalan.nama,
jalan.status,
jalan.status,
'#e74c3c'
);
});
})
.catch(err => {});
}
function loadParsil() {
fetch("api/read_parsil.php")
.then(res => res.json())
.then(data => {
data.forEach(d => {
const parsil = {
id: d.id,
nama: d.nama,
pemilik: d.pemilik,
status: d.status,
area: parseFloat(d.luas),
pts: d.koordinat
};
state.parsil.push(parsil);
renderParsil(parsil);
updateCount('parsil', state.parsil.length);
addCard(
'parsil',
parsil.id,
parsil.nama,
parsil.status,
parsil.status,
'#1a5276'
);
});
})
.catch(err => {});
}
/* ── KEMISKINAN ── */
function getKemiskinanDraft() {
return {
nama_kk: document.getElementById('k-nama')?.value || '',
jumlah_kk: document.getElementById('k-jumlah')?.value || '1',
alamat: document.getElementById('k-alamat')?.value || ''
};
}
function escapeAttr(value) {
return String(value || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function escapeText(value) {
return String(value || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function setKemiskinanLatLng(ll) {
const lat = document.getElementById('k-lat');
const lng = document.getElementById('k-lng');
const latDisp = document.getElementById('k-lat-display');
const lngDisp = document.getElementById('k-lng-display');
if (lat) lat.value = ll.lat.toFixed(6);
if (lng) lng.value = ll.lng.toFixed(6);
if (latDisp) latDisp.textContent = ll.lat.toFixed(6);
if (lngDisp) lngDisp.textContent = ll.lng.toFixed(6);
}
function setKemiskinanAddress(text) {
const alamat = document.getElementById('k-alamat');
const addrLabel = document.getElementById('k-addr-label');
const val = text || '';
if (alamat) alamat.value = val;
if (addrLabel && val) addrLabel.textContent = val;
}
function reverseGeocodeKemiskinan(ll) {
const alamat = document.getElementById('k-alamat');
const addrLabel = document.getElementById('k-addr-label');
const latDisp = document.getElementById('k-lat-display');
const lngDisp = document.getElementById('k-lng-display');
if (latDisp) latDisp.textContent = ll.lat.toFixed(6);
if (lngDisp) lngDisp.textContent = ll.lng.toFixed(6);
if (addrLabel) addrLabel.textContent = 'Mengambil alamat...';
if (alamat && !alamat.value.trim()) alamat.value = 'Mengambil alamat...';
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(ll.lat)}&lon=${encodeURIComponent(ll.lng)}&zoom=18&addressdetails=1`;
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(res => res.ok ? res.json() : Promise.reject())
.then(data => setKemiskinanAddress(data.display_name || `${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}`))
.catch(() => {
if (alamat && alamat.value === 'Mengambil alamat...') alamat.value = '';
alert('Alamat otomatis belum bisa diambil. Silakan isi alamat manual.');
});
}
function startKemiskinanLocationPick() {
window._kkDraft = getKemiskinanDraft();
window._kkPickingLocation = true;
if (window._tmpM && map.hasLayer(window._tmpM)) map.removeLayer(window._tmpM);
window._tmpM = null;
map.closePopup();
map.getContainer().style.cursor = 'crosshair';
const hint = document.getElementById('hint');
if (hint) {
hint.textContent = 'Klik lokasi KK di peta untuk mengisi latitude, longitude, dan alamat.';
hint.classList.add('show');
}
}
function finishKemiskinanLocationPick(e) {
window._kkPickingLocation = false;
const draft = window._kkDraft || {};
window._kkDraft = null;
const hint = document.getElementById('hint');
if (hint) hint.classList.remove('show');
handleKemiskinanClick(e, draft, true);
}
function handleKemiskinanClick(e, draft = {}, autoAddress = false) {
const icon = createModernMarker('#f59e0b', 'fas fa-users', 'KK Miskin Baru');
const tmpM = L.marker(e.latlng, { icon, draggable: true }).addTo(map);
const upd = ll => {
setKemiskinanLatLng(ll);
setKemiskinanAddress('');
reverseGeocodeKemiskinan(ll);
};
tmpM.on('dragend', ev => upd(ev.target.getLatLng()));
// Vulnerability estimator (called on input change)
window._kkVulnUpdate = function () {
const jumlah = parseInt(document.getElementById('k-jumlah')?.value || 1);
const kondisi = document.getElementById('k-kondisi')?.value || 'Sedang';
const airBersih = document.getElementById('k-air')?.checked ? 0 : 8;
const listrik = document.getElementById('k-listrik')?.checked ? 0 : 6;
const kesehatan = document.getElementById('k-kesehatan')?.checked ? 0 : 7;
const sekolah = document.getElementById('k-sekolah')?.checked ? 0 : 5;
const kondisiScore = { Baik: 0, Sedang: 5, Buruk: 12, 'Tidak Layak': 20 }[kondisi] || 5;
const jmlScore = Math.min(jumlah * 5, 25);
const score = Math.min(100, kondisiScore + jmlScore + airBersih + listrik + kesehatan + sekolah);
const cat = score >= 70 ? { label: 'Miskin Ekstrem', color: '#dc2626' } : score >= 45 ? { label: 'Miskin', color: '#ea580c' } : { label: 'Rentan Miskin', color: '#16a34a' };
const el = document.getElementById('k-vuln-score');
const cl = document.getElementById('k-vuln-cat');
if (el) el.textContent = score;
if (cl) { cl.textContent = cat.label; cl.style.color = cat.color; }
};
const form = `<div class="kk-modal">
<div class="km-head">
<span class="km-icon"><i class="fas fa-user-plus"></i></span>
<span class="km-title">Tambah KK Miskin</span>
<button type="button" class="km-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="km-body">
<div class="km-section">DATA KEPALA KELUARGA</div>
<div class="km-field">
<label>Nama Kepala Keluarga <span class="req">*</span></label>
<input id="k-nama" placeholder="Nama lengkap" value="${escapeAttr(draft.nama_kk)}">
</div>
<div class="km-field">
<label>Jumlah Jiwa <span class="req">*</span></label>
<input type="number" id="k-jumlah" value="${escapeAttr(draft.jumlah_kk || '1')}" min="1" oninput="window._kkVulnUpdate()">
</div>
<div class="km-field">
<label>Tanggal Lahir</label>
<input type="date" id="k-tgl-lahir">
</div>
<div class="km-field">
<label>Pendidikan Terakhir</label>
<select id="k-pendidikan">
<option value="">-- Pilih Pendidikan --</option>
<option value="Tidak Sekolah">Tidak Sekolah</option>
<option value="SD/Sederajat">SD/Sederajat</option>
<option value="SMP/Sederajat">SMP/Sederajat</option>
<option value="SMA/Sederajat">SMA/Sederajat</option>
<option value="D3/S1">D3/S1</option>
<option value="S2/S3">S2/S3</option>
</select>
</div>
<div class="km-field">
<label>Alamat <span class="req">*</span></label>
<input id="k-alamat" placeholder="Alamat" value="${escapeAttr(draft.alamat)}">
</div>
<div class="km-section">KONDISI TEMPAT TINGGAL &amp; LAYANAN DASAR</div>
<div class="km-field">
<label>Kondisi Rumah</label>
<select id="k-kondisi" onchange="window._kkVulnUpdate()">
<option value="Baik">Baik</option>
<option value="Sedang" selected>Sedang</option>
<option value="Buruk">Buruk</option>
<option value="Tidak Layak">Tidak Layak</option>
</select>
</div>
<div class="km-field">
<label>Akses Layanan Dasar <span class="km-hint">(centang jika tersedia)</span></label>
<div class="km-checks">
<label class="km-check"><input type="checkbox" id="k-air" onchange="window._kkVulnUpdate()"> Air Bersih</label>
<label class="km-check"><input type="checkbox" id="k-listrik" onchange="window._kkVulnUpdate()"> Listrik</label>
<label class="km-check"><input type="checkbox" id="k-kesehatan" onchange="window._kkVulnUpdate()"> Layanan Kesehatan</label>
<label class="km-check"><input type="checkbox" id="k-sekolah" onchange="window._kkVulnUpdate()"> Anak Putus Sekolah</label>
</div>
</div>
<div class="km-section">LOKASI GPS</div>
<div class="km-gps-box">
<div class="km-gps-row">
<div class="km-gps-item">
<span class="km-gps-lbl">LAT</span>
<span class="km-gps-val" id="k-lat-display">${e.latlng.lat.toFixed(6)}</span>
</div>
<div class="km-gps-sep"></div>
<div class="km-gps-item">
<span class="km-gps-lbl">LNG</span>
<span class="km-gps-val" id="k-lng-display">${e.latlng.lng.toFixed(6)}</span>
</div>
</div>
<div class="km-addr-row" id="k-addr-row">
<i class="fas fa-location-dot" style="color:#EA580C;font-size:13px;flex-shrink:0;margin-top:1px;"></i>
<span id="k-addr-label" style="font-size:12px;color:#ffffff !important;line-height:1.45;font-weight:500;letter-spacing:0.1px;">Mengambil alamat...</span>
</div>
</div>
<input type="hidden" id="k-lat" value="${e.latlng.lat.toFixed(7)}">
<input type="hidden" id="k-lng" value="${e.latlng.lng.toFixed(7)}">
<button type="button" class="km-loc-btn" onclick="startKemiskinanLocationPick()">
<i class="fas fa-map-pin"></i> Pilih Lokasi di Peta
</button>
<div class="km-field" style="margin-top:14px">
<label>Riwayat Penyakit</label>
<textarea id="k-riwayat" placeholder="Contoh: Hipertensi, Diabetes, dll." rows="2"></textarea>
</div>
<div class="km-field">
<label>Surveyor</label>
<input id="k-surveyor" placeholder="Nama petugas survei">
</div>
<div class="km-field">
<label>Keterangan Tambahan</label>
<textarea id="k-keterangan" placeholder="Catatan khusus..." rows="2"></textarea>
</div>
<div class="km-vuln-box">
<div class="kvb-left">
<span class="kvb-label">Estimasi Indeks Kerentanan:</span>
<span class="kvb-cat" id="k-vuln-cat">Rentan Miskin</span>
</div>
<span class="kvb-score" id="k-vuln-score">0</span>
</div>
<div class="km-actions">
<button type="button" class="km-btn km-btn-cancel" onclick="map.closePopup()">Batal</button>
<button type="button" class="km-btn km-btn-save" onclick="saveKemiskinan()">
<i class="fas fa-floppy-disk"></i> Simpan Data
</button>
</div>
</div>
</div>`;
tmpM.bindPopup(form, { maxWidth: 280, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).openPopup();
tmpM.on('popupclose', () => { if (map.hasLayer(tmpM)) map.removeLayer(tmpM); });
window._tmpM = tmpM;
if (autoAddress || !draft.alamat) reverseGeocodeKemiskinan(e.latlng);
}
function saveKemiskinan() {
const nama_kk = (document.getElementById('k-nama')?.value || '').trim();
const jumlah_kk = (document.getElementById('k-jumlah')?.value || '1').trim();
const alamat = (document.getElementById('k-alamat')?.value || '').trim();
const lat = parseFloat(document.getElementById('k-lat')?.value || 0);
const lng = parseFloat(document.getElementById('k-lng')?.value || 0);
const kondisi_rumah = document.getElementById('k-kondisi')?.value || 'Sedang';
const akses_air_bersih = document.getElementById('k-air')?.checked ? 1 : 0;
const akses_listrik = document.getElementById('k-listrik')?.checked ? 1 : 0;
const akses_kesehatan = document.getElementById('k-kesehatan')?.checked ? 1 : 0;
const anak_putus_sekolah = document.getElementById('k-sekolah')?.checked ? 1 : 0;
const surveyor = (document.getElementById('k-surveyor')?.value || '').trim();
const keterangan = (document.getElementById('k-keterangan')?.value || '').trim();
const tanggal_lahir = (document.getElementById('k-tgl-lahir')?.value || '').trim();
const pendidikan = (document.getElementById('k-pendidikan')?.value || '').trim();
const riwayat_penyakit = (document.getElementById('k-riwayat')?.value || '').trim();
if (!nama_kk || !jumlah_kk || !alamat) {
alert('Lengkapi data: Nama, Jumlah Jiwa, dan Alamat wajib diisi!');
return;
}
fetch("api/save_kemiskinan.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
nama_kk, jumlah_kk, alamat, lat, lng,
kondisi_rumah, akses_air_bersih, akses_listrik, akses_kesehatan,
anak_putus_sekolah, surveyor, keterangan,
tanggal_lahir, pendidikan, riwayat_penyakit
})
})
.then(res => res.json())
.then(response => {
if (response.status == "success") {
const d = {
id: response.id || Date.now(), nama_kk, jumlah_kk: parseInt(jumlah_kk),
alamat, lat, lng, kondisi_rumah, akses_air_bersih, akses_listrik,
akses_kesehatan, anak_putus_sekolah, surveyor, keterangan,
tanggal_lahir, pendidikan, riwayat_penyakit
};
state.kemiskinan.push(d);
renderKemiskinan(d);
updateCount('kemiskinan', state.kemiskinan.length);
addCard('kemiskinan', d.id, nama_kk, 'Jumlah KK: ' + jumlah_kk, 'Kemiskinan', '#9f6500');
renderDashboard();
if (window._tmpM) { map.closePopup(); map.removeLayer(window._tmpM); window._tmpM = null; }
setMode(null);
ensureLayerVisible('kemiskinan', d.id);
focusFeature('kemiskinan', d.id);
} else {
alert("Gagal simpan: " + (response.message || 'Error'));
}
})
.catch(err => { alert("Terjadi error"); });
}
function renderKemiskinan(d) {
const status = getKKStatus(d.id);
const done = status === 'Sudah Ditangani';
// Warna marker: HIJAU = dalam radius masjid, MERAH = blank spot
const blank = isBlankSpot(d);
const markerColor = blank ? '#dc2626' : '#16a34a';
const coverIcon = blank
? '<i class="fas fa-circle-xmark" style="color:#dc2626"></i>'
: '<i class="fas fa-circle-check" style="color:#16a34a"></i>';
const coverLabel = blank ? 'Di luar radius rumah ibadah' : 'Dalam radius rumah ibadah';
const nearest = nearestRumahIbadah(d);
const namaRI = nearest ? nearest.nama_masjid : '-';
const jarakRI = nearest ? Math.round(nearest.distance) + ' m' : '-';
const cat = getCategory(d);
const visible = {
extreme: document.getElementById('flt-extreme')?.checked !== false,
poor: document.getElementById('flt-poor')?.checked !== false,
vulnerable: document.getElementById('flt-vulnerable')?.checked !== false
};
if (!visible[cat.key]) return;
const icon = createModernMarker(markerColor, 'fas fa-users', d.nama_kk);
const m = L.marker([d.lat, d.lng], { icon, draggable: IS_LOGGED_IN });
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const role = gisUser.role || '';
const canManage = role !== 'walikota';
const actionsHtml = canManage ? `<div class="act-row kk-popup-actions">
<button class="pbtn pbtn-y" onclick="openEditKemiskinan(${d.id})">Edit</button>
<button class="pbtn pbtn-b" onclick="setKKStatusQuick(${d.id},'Diproses')">Proses</button>
<button class="pbtn ${done ? 'pbtn-b' : 'pbtn-g'}" onclick="setKKStatusQuick(${d.id},&apos;${done ? 'Belum Ditangani' : 'Sudah Ditangani'}&apos;)">
${done ? 'Batal' : 'Selesai'}
</button>
<button class="pbtn" style="background:#ede9fe;color:#7c3aed" title="Data Warga" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-users-cog"></i></button>
<button class="pbtn" style="background:#FFEDD5;color:#1e3a8a" title="Histori Bantuan" onclick="openHistoriBantuan(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-history"></i></button>
<button class="pbtn pbtn-r" onclick="deleteFeature('kemiskinan',${d.id})">Hapus</button>
</div>` : `<div class="act-row kk-popup-actions" style="grid-template-columns:1fr 1fr">
<button class="pbtn" style="background:#ede9fe;color:#7c3aed;padding:8px" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-users-cog"></i> Data Warga</button>
<button class="pbtn" style="background:#FFEDD5;color:#1e3a8a;padding:8px" onclick="openHistoriBantuan(${d.id},'${d.nama_kk.replace(/'/g, '&#39;')}')"><i class="fas fa-history"></i> Histori Bantuan</button>
</div>`;
const catBadgeClass = cat.key === 'extreme' ? 'extreme' : cat.key === 'poor' ? 'poor' : 'vulnerable';
const statusColor = status === 'Sudah Ditangani' ? '#059669' : status === 'Diproses' ? '#2563eb' : '#64748b';
const privatePopup = `<div class="pi">
<div class="pi-header">
<div class="pi-header-icon"><i class="fas fa-users"></i></div>
<div class="pi-header-info">
<div class="pi-header-name">${d.nama_kk}</div>
<div class="pi-header-sub"><span class="pi-badge ${catBadgeClass}" style="font-size:10px;padding:2px 7px">${cat.label}</span></div>
</div>
<button class="pi-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="pi-body">
<div class="pi-row"><span class="pi-k">Jumlah Jiwa</span><span class="pi-v">${d.jumlah_kk} orang</span></div>
<div class="pi-row"><span class="pi-k">Alamat</span><span class="pi-v" style="max-width:140px">${d.alamat}</span></div>
<div class="pi-row"><span class="pi-k">Status</span><span class="pi-v" style="color:${statusColor}">${status}</span></div>
<div class="pi-row"><span class="pi-k">Rumah Ibadah</span><span class="pi-v" style="max-width:130px">${namaRI}</span></div>
<div class="pi-row"><span class="pi-k">Jarak</span><span class="pi-v">${jarakRI}</span></div>
<div class="pi-row"><span class="pi-k">Cakupan</span><span class="pi-v">${coverIcon} ${blank ? 'Blank Spot' : 'Terjangkau'}</span></div>
</div>
${actionsHtml}
</div>`;
const publicPopup = `<div class="pi">
<div class="pi-header" style="background:var(--navy-light)">
<div class="pi-header-icon"><i class="fas fa-lock"></i></div>
<div class="pi-header-info">
<div class="pi-header-name">Data KK Terbatas</div>
<div class="pi-header-sub">Login diperlukan</div>
</div>
<button class="pi-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="pi-body">
<div class="pi-row"><span class="pi-k">Kategori</span><span class="pi-v">${cat.label}</span></div>
<div class="pi-row"><span class="pi-k">Cakupan</span><span class="pi-v">${coverIcon} ${blank ? 'Blank Spot' : 'Terjangkau'}</span></div>
</div>
<div class="act-row">
<a class="pbtn pbtn-b" style="text-decoration:none;text-align:center" href="#" onclick="openLoginModal(); return false;"><i class="fas fa-sign-in-alt"></i> Login Admin</a>
</div>
</div>`;
m.bindPopup(IS_LOGGED_IN ? privatePopup : publicPopup, { autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] });
m.addTo(layers.kemiskinan);
state.featureRefs.kemiskinan[String(d.id)] = m;
state.kkLayerRefs[String(d.id)] = m;
}
function loadKemiskinan() {
return fetch("api/read_kemiskinan.php").then(res => res.json()).then(data => {
data.forEach(d => {
const k = { id: d.id, nama_kk: d.nama_kk, jumlah_kk: parseInt(d.jumlah_kk), alamat: d.alamat, status_penanganan: d.status_penanganan || 'Belum Ditangani', lat: parseFloat(d.lat), lng: parseFloat(d.lng), kondisi_rumah: d.kondisi_rumah, akses_air_bersih: parseInt(d.akses_air_bersih || 0), akses_listrik: parseInt(d.akses_listrik || 0), akses_kesehatan: parseInt(d.akses_kesehatan || 0), anak_putus_sekolah: parseInt(d.anak_putus_sekolah || 0), tanggal_lahir: d.tanggal_lahir || '', pendidikan: d.pendidikan || '', riwayat_penyakit: d.riwayat_penyakit || '' };
state.kemiskinan.push(k);
renderKemiskinan(k);
updateCount('kemiskinan', state.kemiskinan.length);
addCard('kemiskinan', k.id, k.nama_kk, 'Jumlah KK: ' + k.jumlah_kk, 'Kemiskinan', '#9f6500');
});
renderDashboard();
if (state.masjid.length) refreshType('masjid');
}).catch(err => {});
}
function openEditKemiskinan(id) {
const d = getById('kemiskinan', id);
if (!d) return;
const existing = document.getElementById('ek-overlay');
if (existing) existing.remove();
const pendOpts = ['', 'Tidak Sekolah', 'SD/Sederajat', 'SMP/Sederajat', 'SMA/Sederajat', 'D3/S1', 'S2/S3'];
const pendSelects = pendOpts.map(v => `<option value="${v}" ${d.pendidikan === v ? 'selected' : ''}>${v || '-- Pilih Pendidikan --'}</option>`).join('');
const curStatus = getKKStatus(id);
const overlay = document.createElement('div');
overlay.className = 'hb-overlay';
overlay.id = 'ek-overlay';
overlay.innerHTML = `
<div class="hb-box" style="max-width:520px">
<div class="hb-head" style="background:linear-gradient(135deg,#f59e0b,#d97706)">
<div class="hb-head-icon"><i class="fas fa-user-edit"></i></div>
<div style="flex:1;min-width:0">
<div class="hb-head-title">Edit Data KK Miskin</div>
<div style="font-size:11px;opacity:.8;margin-top:1px">${escapeText(d.nama_kk)}</div>
</div>
<button class="hb-head-close" onclick="document.getElementById('ek-overlay').remove()">
<i class="fas fa-times"></i>
</button>
</div>
<div class="hb-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
<div class="km-field" style="grid-column:1/-1">
<label>Nama Kepala Keluarga *</label>
<input id="ek-nama" value="${(d.nama_kk || '').replace(/"/g, '&quot;')}" placeholder="Nama lengkap">
</div>
<div class="km-field">
<label>Jumlah Jiwa *</label>
<input type="number" id="ek-jumlah" value="${d.jumlah_kk}" min="1">
</div>
<div class="km-field">
<label>Tanggal Lahir</label>
<input type="date" id="ek-tgl-lahir" value="${d.tanggal_lahir || ''}">
</div>
<div class="km-field" style="grid-column:1/-1">
<label>Pendidikan Terakhir</label>
<select id="ek-pendidikan">${pendSelects}</select>
</div>
<div class="km-field" style="grid-column:1/-1">
<label>Alamat *</label>
<textarea id="ek-alamat" rows="2" style="resize:vertical">${(d.alamat || '').replace(/</g, '&lt;')}</textarea>
</div>
<div class="km-field" style="grid-column:1/-1">
<label>Riwayat Penyakit</label>
<textarea id="ek-riwayat" rows="2" style="resize:vertical">${(d.riwayat_penyakit || '').replace(/</g, '&lt;')}</textarea>
</div>
<div class="km-field">
<label>Latitude</label>
<input id="ek-lat" value="${Number(d.lat).toFixed(6)}" style="font-family:monospace;font-size:12px">
</div>
<div class="km-field">
<label>Longitude</label>
<input id="ek-lng" value="${Number(d.lng).toFixed(6)}" style="font-family:monospace;font-size:12px">
</div>
<div class="km-field" style="grid-column:1/-1">
<label>Status Penanganan</label>
<select id="ek-status">
<option value="Belum Ditangani" ${curStatus === 'Belum Ditangani' ? 'selected' : ''}>Belum Ditangani</option>
<option value="Diproses" ${curStatus === 'Diproses' ? 'selected' : ''}>Diproses</option>
<option value="Sudah Ditangani" ${curStatus === 'Sudah Ditangani' ? 'selected' : ''}>Sudah Ditangani</option>
</select>
</div>
<button class="hb-submit" style="grid-column:1/-1;background:linear-gradient(135deg,#f59e0b,#d97706)" onclick="saveEditKemiskinan(${id})">
<i class="fas fa-save"></i> Simpan Perubahan
</button>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
// Close on backdrop click
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function saveEditKemiskinan(id) {
const nama_kk = document.getElementById('ek-nama')?.value.trim();
const jumlah_kk = document.getElementById('ek-jumlah')?.value.trim();
const alamat = document.getElementById('ek-alamat')?.value.trim();
const lat = parseFloat(document.getElementById('ek-lat')?.value);
const lng = parseFloat(document.getElementById('ek-lng')?.value);
const status = document.getElementById('ek-status')?.value || getKKStatus(id);
const tanggal_lahir = document.getElementById('ek-tgl-lahir')?.value || '';
const pendidikan = document.getElementById('ek-pendidikan')?.value || '';
const riwayat_penyakit = document.getElementById('ek-riwayat')?.value.trim() || '';
if (!nama_kk || !jumlah_kk || !alamat || !Number.isFinite(lat) || !Number.isFinite(lng)) {
alert('Lengkapi data: Nama, Jumlah Jiwa, dan Alamat wajib diisi!'); return;
}
const btn = document.querySelector('#ek-overlay .hb-submit');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Menyimpan...'; }
fetch('api/update_kemiskinan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama_kk, jumlah_kk, alamat, lat, lng, tanggal_lahir, pendidikan, riwayat_penyakit })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') {
alert('Gagal menyimpan: ' + (resp.message || 'Error'));
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> Simpan Perubahan'; }
return;
}
const d = getById('kemiskinan', id); if (!d) return;
d.nama_kk = nama_kk; d.jumlah_kk = parseInt(jumlah_kk); d.alamat = alamat; d.lat = lat; d.lng = lng;
d.tanggal_lahir = tanggal_lahir; d.pendidikan = pendidikan; d.riwayat_penyakit = riwayat_penyakit;
d.status_penanganan = status;
const mapStatus = getStatusMap();
mapStatus[String(id)] = status;
localStorage.setItem(statusStoreKey, JSON.stringify(mapStatus));
fetch('api/update_status_kemiskinan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id), status })
}).catch(() => {});
renderDashboard();
refreshType('kemiskinan');
map.closePopup();
document.getElementById('ek-overlay')?.remove();
showToast('✅ Data berhasil disimpan', 'success');
focusFeature('kemiskinan', id);
}).catch(() => {
alert('Error jaringan, coba lagi');
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-save"></i> Simpan Perubahan'; }
});
}
/* ── MASJID ── */
function setMasjidAddress(text) {
const alamat = document.getElementById('m-alamat');
const addrLabel = document.getElementById('m-addr-label');
const val = text || '';
if (alamat) alamat.value = val;
if (addrLabel && val) addrLabel.textContent = val;
}
function reverseGeocodeMasjid(ll) {
const alamat = document.getElementById('m-alamat');
const addrLabel = document.getElementById('m-addr-label');
const latDisp = document.getElementById('m-lat-display');
const lngDisp = document.getElementById('m-lng-display');
if (latDisp) latDisp.textContent = ll.lat.toFixed(6);
if (lngDisp) lngDisp.textContent = ll.lng.toFixed(6);
if (addrLabel) addrLabel.textContent = 'Mengambil alamat...';
if (alamat && !alamat.value.trim()) alamat.value = 'Mengambil alamat...';
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(ll.lat)}&lon=${encodeURIComponent(ll.lng)}&zoom=18&addressdetails=1`;
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(res => res.ok ? res.json() : Promise.reject())
.then(data => setMasjidAddress(data.display_name || `${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}`))
.catch(() => {
if (alamat && alamat.value === 'Mengambil alamat...') alamat.value = '';
if (addrLabel) addrLabel.textContent = 'Gagal mengambil alamat otomatis';
});
}
function handleMasjidClick(e, draft = {}, autoAddress = false) {
const icon = createModernMarker('#0f6250', 'fas fa-mosque', 'Masjid Baru');
const tmpM = L.marker(e.latlng, { icon, draggable: true }).addTo(map);
const tmpC = L.circle(e.latlng, { radius: 500, color: '#0f6250', fillColor: '#0f6250', fillOpacity: 0.1, weight: 1 }).addTo(map);
const upd = ll => {
const a = document.getElementById('m-lat'), b = document.getElementById('m-lng');
const latDisp = document.getElementById('m-lat-display'), lngDisp = document.getElementById('m-lng-display');
if (a) a.value = ll.lat.toFixed(6);
if (b) b.value = ll.lng.toFixed(6);
if (latDisp) latDisp.textContent = ll.lat.toFixed(6);
if (lngDisp) lngDisp.textContent = ll.lng.toFixed(6);
tmpC.setLatLng(ll);
setMasjidAddress('');
reverseGeocodeMasjid(ll);
};
tmpM.on('dragend', ev => upd(ev.target.getLatLng()));
const form = `<div class="kk-modal">
<div class="km-head">
<span class="km-icon" style="background:#D1FAE5;color:#0f6250"><i class="fas fa-place-of-worship"></i></span>
<span class="km-title">Tambah Rumah Ibadah</span>
<button type="button" class="km-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="km-body">
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">PROFIL RUMAH IBADAH</div>
<div class="km-field">
<label>Nama Rumah Ibadah <span class="req">*</span></label>
<input id="m-nama" placeholder="Contoh: Masjid Raya Mujahidin" value="${draft.nama_masjid || ''}">
</div>
<div class="km-field">
<label>Penanggung Jawab <span class="req">*</span></label>
<input id="m-pic" placeholder="Nama PIC/Pengurus" value="${draft.pic_masjid || ''}">
</div>
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">ALAMAT &amp; JANGKAUAN</div>
<div class="km-field">
<label>Alamat Lengkap <span class="req">*</span></label>
<textarea id="m-alamat" placeholder="Alamat lengkap">${draft.alamat_masjid || ''}</textarea>
</div>
<div class="km-field">
<label>Radius Pelayanan (Meter): <span id="m-rad-val" style="color:#0f6250;font-weight:800;">500</span>m</label>
<input type="range" id="m-radius" min="100" max="5000" step="100" value="500" style="width:100%;margin-bottom:8px;accent-color:#0f6250;" oninput="document.getElementById('m-rad-val').innerText=this.value; window._tmpC.setRadius(parseInt(this.value));">
</div>
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">LOKASI GPS</div>
<div class="km-gps-box" style="background:linear-gradient(135deg,#064e3b,#0f6250);">
<div class="km-gps-row">
<div class="km-gps-item">
<span class="km-gps-lbl">LAT</span>
<span class="km-gps-val" id="m-lat-display">${e.latlng.lat.toFixed(6)}</span>
</div>
<div class="km-gps-sep"></div>
<div class="km-gps-item">
<span class="km-gps-lbl">LNG</span>
<span class="km-gps-val" id="m-lng-display">${e.latlng.lng.toFixed(6)}</span>
</div>
</div>
<div class="km-addr-row" id="m-addr-row">
<i class="fas fa-location-dot" style="color:#6ee7b7;font-size:13px;flex-shrink:0;margin-top:1px;"></i>
<span id="m-addr-label" style="font-size:12px;color:#ffffff !important;line-height:1.45;font-weight:500;letter-spacing:0.1px;">Mengambil alamat...</span>
</div>
</div>
<input type="hidden" id="m-lat" value="${e.latlng.lat.toFixed(6)}">
<input type="hidden" id="m-lng" value="${e.latlng.lng.toFixed(6)}">
<div class="hint-txt" style="margin-top:4px;text-align:center;">* Geser marker untuk update alamat otomatis</div>
<div class="km-actions">
<button type="button" class="km-btn km-btn-cancel" onclick="map.closePopup()">Batal</button>
<button type="button" class="km-btn km-btn-save" style="background:linear-gradient(135deg,#0f6250,#14b8a6);" onclick="saveMasjid()">
<i class="fas fa-floppy-disk"></i> Simpan Data
</button>
</div>
</div>
</div>`;
tmpM.bindPopup(form, { maxWidth: 340, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).openPopup();
tmpM.on('popupclose', () => {
if (map.hasLayer(tmpM)) map.removeLayer(tmpM);
if (map.hasLayer(tmpC)) map.removeLayer(tmpC);
});
window._tmpM = tmpM;
window._tmpC = tmpC;
if (autoAddress || !draft.alamat_masjid) reverseGeocodeMasjid(e.latlng);
}
function saveMasjid() {
const nama_masjid = document.getElementById('m-nama').value.trim();
const pic_masjid = document.getElementById('m-pic').value.trim();
const alamat_masjid = document.getElementById('m-alamat').value.trim();
const radius = document.getElementById('m-radius').value;
const lat = parseFloat(document.getElementById('m-lat').value);
const lng = parseFloat(document.getElementById('m-lng').value);
if (!nama_masjid || !pic_masjid || !alamat_masjid) {
alert('Lengkapi semua data Rumah Ibadah!');
return;
}
fetch("api/save_masjid.php", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nama_masjid, pic_masjid, alamat_masjid, radius, lat, lng })
}).then(res => res.json()).then(response => {
if (response.status == "success") {
const d = { id: response.id || Date.now(), nama_masjid, pic_masjid, alamat_masjid, radius: parseInt(radius), lat, lng };
state.masjid.push(d);
renderMasjid(d);
updateCount('masjid', state.masjid.length);
addCard('masjid', d.id, nama_masjid, d.pic_masjid, 'Rumah Ibadah', '#0f6250');
renderDashboard();
if (window._tmpM) { map.closePopup(); map.removeLayer(window._tmpM); window._tmpM = null; }
if (window._tmpC) { map.removeLayer(window._tmpC); window._tmpC = null; }
setMode(null);
} else { alert("Gagal simpan database"); }
}).catch(err => { alert("Terjadi error"); });
}
function renderMasjid(d) {
const color = '#0f6250';
const summary = rumahIbadahStats(d);
const icon = createModernMarker(color, 'fas fa-place-of-worship', d.nama_masjid);
const m = L.marker([d.lat, d.lng], { icon, draggable: IS_LOGGED_IN });
const circle = L.circle([d.lat, d.lng], { radius: d.radius, color: color, fillColor: color, fillOpacity: 0.1, weight: 1 });
const grp = L.featureGroup([circle, m]);
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const role = gisUser.role || '';
const canManage = IS_LOGGED_IN && role !== 'walikota';
const adminActions = canManage ? `<div class="act-row">
<button class="pbtn pbtn-y" onclick="openEditMasjid(${d.id})">Edit</button>
<button class="pbtn pbtn-r" onclick="deleteFeature('masjid',${d.id})">Hapus</button>
</div>` : '';
m.bindPopup(`<div class="pi">
<div class="pi-header" style="background:linear-gradient(135deg,#064e3b,#0f6250)">
<div class="pi-header-icon"><i class="fas fa-place-of-worship"></i></div>
<div class="pi-header-info">
<div class="pi-header-name">${d.nama_masjid}</div>
<div class="pi-header-sub"><i class="fas fa-user" style="font-size:10px"></i> ${d.pic_masjid} &bull; Radius ${d.radius} m</div>
</div>
<button class="pi-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="pi-body">
<div class="pi-row"><span class="pi-k">Alamat</span><span class="pi-v" style="max-width:140px">${d.alamat_masjid}</span></div>
<div class="pi-row"><span class="pi-k">Penanggung Jawab</span><span class="pi-v">${d.pic_masjid}</span></div>
<div class="pi-row"><span class="pi-k">Radius Layanan</span><span class="pi-v">${d.radius} m</span></div>
</div>
` + (IS_LOGGED_IN ? `
<div style="padding:8px 12px 4px;font-size:10px;font-weight:800;color:#0f6250;text-transform:uppercase;letter-spacing:.6px;border-top:1px solid #e2e8f0;">
<i class="fas fa-users" style="margin-right:4px;"></i>KK Miskin Dalam Radius
</div>
<div class="pi-body" style="padding-top:4px">
<div class="pi-row"><span class="pi-k">Total KK</span><span class="pi-v">${summary.total}</span></div>
<div class="pi-row"><span class="pi-k" style="color:#dc2626">Miskin Ekstrem</span><span class="pi-v" style="color:#dc2626">${summary.extreme}</span></div>
<div class="pi-row"><span class="pi-k" style="color:#2563eb">Sedang Diproses</span><span class="pi-v" style="color:#2563eb">${summary.process}</span></div>
<div class="pi-row"><span class="pi-k" style="color:#059669">Sudah Ditangani</span><span class="pi-v" style="color:#059669">${summary.handled}</span></div>
` + (summary.sample.length > 0
? '<div style="font-size:10px;font-weight:700;color:#64748b;margin:6px 0 2px;text-transform:uppercase;letter-spacing:.5px;">KK Terdekat</div>'
+ summary.sample.map(k => '<div class="pi-row"><span class="pi-k">' + k.nama_kk + '</span><span class="pi-v" style="color:#64748b">' + Math.round(k.distance) + ' m</span></div>').join('')
: '<div style="font-size:12px;color:#94a3b8;padding:4px 0;text-align:center;">Tidak ada KK dalam radius ini</div>'
) + `
</div>` : '') + `
${adminActions}
</div>`);
grp.addTo(layers.masjid);
state.featureRefs.masjid[String(d.id)] = grp;
}
function loadMasjid() {
return fetch("api/read_masjid.php").then(res => res.json()).then(data => {
data.forEach(d => {
const m = { id: d.id, nama_masjid: d.nama_masjid, pic_masjid: d.pic_masjid, alamat_masjid: d.alamat_masjid, radius: parseInt(d.radius), lat: parseFloat(d.lat), lng: parseFloat(d.lng) };
state.masjid.push(m);
renderMasjid(m);
updateCount('masjid', state.masjid.length);
addCard('masjid', m.id, m.nama_masjid, m.pic_masjid, 'Rumah Ibadah', '#0f6250');
});
renderDashboard();
}).catch(err => {});
}
function openEditMasjid(id) {
const d = getById('masjid', id);
if (!d) return;
const html = `<div class="kk-modal">
<div class="km-head">
<span class="km-icon" style="background:#D1FAE5;color:#0f6250"><i class="fas fa-edit"></i></span>
<span class="km-title">Edit Rumah Ibadah</span>
<button type="button" class="km-close" onclick="map.closePopup()"><i class="fas fa-times"></i></button>
</div>
<div class="km-body">
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">PROFIL RUMAH IBADAH</div>
<div class="km-field">
<label>Nama Rumah Ibadah <span class="req">*</span></label>
<input id="e-m-nama" value="${(d.nama_masjid || '').replace(/"/g, '&quot;')}">
</div>
<div class="km-field">
<label>Penanggung Jawab <span class="req">*</span></label>
<input id="e-m-pic" value="${(d.pic_masjid || '').replace(/"/g, '&quot;')}">
</div>
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">ALAMAT &amp; JANGKAUAN</div>
<div class="km-field">
<label>Alamat Lengkap <span class="req">*</span></label>
<textarea id="e-m-alamat">${(d.alamat_masjid || '').replace(/</g, '&lt;')}</textarea>
</div>
<div class="km-field">
<label>Radius Pelayanan (Meter): <span id="e-m-rad-val" style="color:#0f6250;font-weight:800;">${d.radius}</span>m</label>
<input type="range" id="e-m-radius" min="100" max="5000" step="100" value="${d.radius}" style="width:100%;margin-bottom:8px;accent-color:#0f6250;" oninput="document.getElementById('e-m-rad-val').innerText=this.value">
</div>
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">LOKASI GPS</div>
<div class="km-gps-box" style="background:linear-gradient(135deg,#064e3b,#0f6250);">
<div class="km-gps-row" style="padding:10px 12px;">
<div class="km-gps-item">
<span class="km-gps-lbl">LATITUDE</span>
<input id="e-m-lat" value="${Number(d.lat).toFixed(6)}" style="background:transparent;border:none;color:#fff;font-family:'Space Grotesk',sans-serif;font-size:14px;font-weight:700;text-align:center;width:100%;outline:none;">
</div>
<div class="km-gps-sep" style="height:32px;"></div>
<div class="km-gps-item">
<span class="km-gps-lbl">LONGITUDE</span>
<input id="e-m-lng" value="${Number(d.lng).toFixed(6)}" style="background:transparent;border:none;color:#fff;font-family:'Space Grotesk',sans-serif;font-size:14px;font-weight:700;text-align:center;width:100%;outline:none;">
</div>
</div>
</div>
<div class="km-actions">
<button type="button" class="km-btn km-btn-cancel" onclick="map.closePopup()">Batal</button>
<button type="button" class="km-btn km-btn-save" style="background:linear-gradient(135deg,#0f6250,#14b8a6);" onclick="saveEditMasjid(${id})">
<i class="fas fa-check"></i> Simpan
</button>
</div>
</div>
</div>`;
map.closePopup();
L.popup({ maxWidth: 340, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng([d.lat, d.lng]).setContent(html).openOn(map);
}
function saveEditMasjid(id) {
const nama_masjid = document.getElementById('e-m-nama').value.trim();
const pic_masjid = document.getElementById('e-m-pic').value.trim();
const alamat_masjid = document.getElementById('e-m-alamat').value.trim();
const radius = document.getElementById('e-m-radius').value;
const lat = parseFloat(document.getElementById('e-m-lat').value);
const lng = parseFloat(document.getElementById('e-m-lng').value);
if (!nama_masjid || !pic_masjid || !alamat_masjid || !Number.isFinite(lat) || !Number.isFinite(lng)) {
alert('Data belum valid'); return;
}
fetch('api/update_masjid.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama_masjid, pic_masjid, alamat_masjid, radius, lat, lng })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal update'); return; }
const d = getById('masjid', id); if (!d) return;
d.nama_masjid = nama_masjid; d.pic_masjid = pic_masjid; d.alamat_masjid = alamat_masjid; d.radius = parseInt(radius); d.lat = lat; d.lng = lng;
refreshType('masjid');
map.closePopup();
focusFeature('masjid', id);
}).catch(() => alert('Error update data'));
}
/* ΓöÇΓöÇ HISTORI BANTUAN MODAL ΓöÇΓöÇ */
function openHistoriBantuan(kkId, kkNama) {
const existing = document.getElementById('hb-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'hb-overlay';
overlay.id = 'hb-overlay';
overlay.innerHTML = `
<div class="hb-box">
<div class="hb-head">
<div class="hb-head-icon"><i class="fas fa-hand-holding-heart"></i></div>
<div style="flex:1;min-width:0">
<div class="hb-head-title">Histori Bantuan</div>
<div style="font-size:11px;opacity:.75;margin-top:1px">${kkNama}</div>
</div>
<button class="hb-head-close" onclick="document.getElementById('hb-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="hb-body">
<div class="hb-form" id="hb-form-tambah" style="display:none">
<div class="hb-form-title">+ Tambah Bantuan Baru</div>
<div class="km-field" style="margin-bottom:8px">
<label>Jenis Bantuan *</label>
<input id="hb-jenis" placeholder="Contoh: Sembako, PKH, BPNT, dll">
</div>
<div class="km-field" style="margin-bottom:8px">
<label>Tanggal *</label>
<input type="date" id="hb-tanggal" value="${new Date().toISOString().split('T')[0]}">
</div>
<div class="km-field" style="margin-bottom:8px">
<label>Keterangan</label>
<textarea id="hb-ket" rows="2" placeholder="Catatan tambahan..."></textarea>
</div>
<button class="hb-submit" onclick="saveHistoriBantuan(${kkId})">
<i class="fas fa-plus"></i> Simpan Bantuan
</button>
</div>
<div class="hb-list-title">Riwayat Bantuan</div>
<div id="hb-list"><div class="hb-empty"><i class="fas fa-spinner fa-spin"></i> Memuat...</div></div>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
if (gisUser.role !== 'walikota') {
document.getElementById('hb-form-tambah').style.display = 'block';
}
loadHistoriBantuan(kkId);
}
function loadHistoriBantuan(kkId) {
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const canManage = gisUser.role !== 'walikota';
fetch(`api/read_histori_bantuan.php?kemiskinan_id=${kkId}`)
.then(res => res.json())
.then(data => {
const list = document.getElementById('hb-list');
if (!list) return;
if (!Array.isArray(data) || data.length === 0) {
list.innerHTML = '<div class="hb-empty"><i class="fas fa-inbox"></i><br>Belum ada histori bantuan</div>';
return;
}
list.innerHTML = data.map(h => `
<div class="hb-item" id="hb-item-${h.id}">
<div class="hb-item-top">
<div>
<div class="hb-item-jenis">${h.jenis_bantuan}</div>
<div class="hb-item-tgl"><i class="fas fa-calendar-alt"></i> ${h.tanggal}</div>
</div>
${canManage ? `<button class="hb-item-del" onclick="deleteHistoriBantuan(${h.id},${kkId})"><i class="fas fa-trash"></i></button>` : ''}
</div>
${h.keterangan ? `<div class="hb-item-ket">${h.keterangan}</div>` : ''}
</div>
`).join('');
})
.catch(() => {
const list = document.getElementById('hb-list');
if (list) list.innerHTML = '<div class="hb-empty" style="color:#ef4444">Gagal memuat data</div>';
});
}
function saveHistoriBantuan(kkId) {
const jenis = (document.getElementById('hb-jenis')?.value || '').trim();
const tanggal = (document.getElementById('hb-tanggal')?.value || '').trim();
const ket = (document.getElementById('hb-ket')?.value || '').trim();
if (!jenis || !tanggal) {
alert('Jenis bantuan dan tanggal wajib diisi!');
return;
}
fetch('api/save_histori_bantuan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ kemiskinan_id: kkId, jenis_bantuan: jenis, tanggal, keterangan: ket })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal simpan: ' + (resp.message || 'Error')); return; }
document.getElementById('hb-jenis').value = '';
document.getElementById('hb-ket').value = '';
loadHistoriBantuan(kkId);
// update status KK menjadi Diproses jika masih belum ditangani
if (getKKStatus(kkId) === 'Belum Ditangani') setKKStatus(kkId, 'Diproses');
}).catch(() => alert('Error menyimpan histori'));
}
function deleteHistoriBantuan(histId, kkId) {
if (!confirm('Hapus histori bantuan ini?')) return;
fetch('api/delete_histori_bantuan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: histId })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal hapus'); return; }
const el = document.getElementById('hb-item-' + histId);
if (el) el.remove();
// cek apakah list kosong
const list = document.getElementById('hb-list');
if (list && !list.querySelector('.hb-item')) {
list.innerHTML = '<div class="hb-empty"><i class="fas fa-inbox"></i><br>Belum ada histori bantuan</div>';
}
}).catch(() => alert('Error hapus histori'));
}
/* ΓöÇΓöÇ LAPORAN MASYARAKAT MODAL ΓöÇΓöÇ */
let _laporanCache = [];
function openLaporanModal() {
const existing = document.getElementById('lm-overlay');
if (existing) existing.remove();
const isAdmin = IS_LOGGED_IN;
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const role = gisUser.role || '';
const canManage = isAdmin && role !== 'walikota';
const overlay = document.createElement('div');
overlay.className = 'lm-overlay';
overlay.id = 'lm-overlay';
overlay.innerHTML = `
<div class="lm-box">
<div class="lm-head">
<h3><i class="fas fa-bullhorn"></i> Laporan Masyarakat</h3>
<p>Jalur pelaporan cepat masalah kemiskinan di Kota Pontianak</p>
<button class="lm-close-btn" onclick="document.getElementById('lm-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="lm-body">
<div class="lm-tabs">
<button class="lm-tab active" onclick="switchLaporanTab('form',this)">📝 Kirim Laporan</button>
${isAdmin ? `<button class="lm-tab" onclick="switchLaporanTab('daftar',this)">📋 Daftar Laporan</button>` : ''}
</div>
<div class="lm-section active" id="lm-tab-form">
<div class="lm-field">
<label>Nama Pelapor <span style="color:#ef4444">*</span></label>
<input id="lm-nama" placeholder="Nama lengkap Anda">
</div>
<div class="lm-field">
<label>Nomor Kontak (opsional)</label>
<input id="lm-kontak" placeholder="No. HP / WhatsApp">
</div>
<div class="lm-field">
<label>Deskripsi Laporan <span style="color:#ef4444">*</span></label>
<textarea id="lm-deskripsi" placeholder="Ceritakan kondisi keluarga miskin yang perlu diperhatikan, lokasi, dll..."></textarea>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="lm-field">
<label>Latitude</label>
<input id="lm-lat" placeholder="Klik peta lalu salin" type="number" step="any">
</div>
<div class="lm-field">
<label>Longitude</label>
<input id="lm-lng" placeholder="Klik peta lalu salin" type="number" step="any">
</div>
</div>
<p style="font-size:11px;color:var(--muted);margin-bottom:14px;">
<i class="fas fa-info-circle" style="color:var(--blue)"></i>
Koordinat opsional. Klik kanan di peta untuk mendapatkan koordinat lokasi.
</p>
<button class="lm-submit" onclick="submitLaporan()">
<i class="fas fa-paper-plane"></i> Kirim Laporan
</button>
<div id="lm-success" style="display:none;margin-top:14px;padding:12px 14px;background:#d1fae5;border-radius:14px;color:#065f46;font-size:13px;font-weight:600;text-align:center;">
<i class="fas fa-check-circle"></i> Laporan berhasil dikirim! Terima kasih.
</div>
</div>
${isAdmin ? `<div class="lm-section" id="lm-tab-daftar">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<span style="font-size:12px;font-weight:700;color:var(--muted)" id="lm-count-label">Memuat...</span>
<button onclick="loadDaftarLaporan()" style="border:1px solid var(--border);background:#fff;border-radius:8px;padding:5px 10px;font-size:11px;font-weight:700;cursor:pointer;color:var(--navy)"><i class="fas fa-sync"></i> Refresh</button>
</div>
<div id="lm-daftar-list"><div style="text-align:center;padding:20px;color:var(--muted)"><i class="fas fa-spinner fa-spin"></i> Memuat...</div></div>
</div>` : ''}
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
if (isAdmin) loadDaftarLaporan();
}
function switchLaporanTab(tab, btn) {
document.querySelectorAll('.lm-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.lm-section').forEach(s => s.classList.remove('active'));
if (btn) btn.classList.add('active');
const sec = document.getElementById('lm-tab-' + tab);
if (sec) sec.classList.add('active');
if (tab === 'daftar') loadDaftarLaporan();
}
function submitLaporan() {
const nama_pelapor = (document.getElementById('lm-nama')?.value || '').trim();
const kontak = (document.getElementById('lm-kontak')?.value || '').trim();
const deskripsi_laporan = (document.getElementById('lm-deskripsi')?.value || '').trim();
const lat = parseFloat(document.getElementById('lm-lat')?.value || 0);
const lng = parseFloat(document.getElementById('lm-lng')?.value || 0);
if (!nama_pelapor || !deskripsi_laporan) {
alert('Nama pelapor dan deskripsi laporan wajib diisi!');
return;
}
fetch('api/save_laporan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama_pelapor, kontak, deskripsi_laporan, lat, lng })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal kirim laporan: ' + (resp.message || '')); return; }
document.getElementById('lm-nama').value = '';
document.getElementById('lm-kontak').value = '';
document.getElementById('lm-deskripsi').value = '';
document.getElementById('lm-lat').value = '';
document.getElementById('lm-lng').value = '';
const sukses = document.getElementById('lm-success');
if (sukses) { sukses.style.display = 'block'; setTimeout(() => sukses.style.display = 'none', 4000); }
updateLaporanBadge();
}).catch(() => alert('Terjadi kesalahan saat mengirim laporan'));
}
function loadDaftarLaporan() {
const list = document.getElementById('lm-daftar-list');
const label = document.getElementById('lm-count-label');
if (!list) return;
list.innerHTML = '<div style="text-align:center;padding:20px;color:var(--muted)"><i class="fas fa-spinner fa-spin"></i> Memuat...</div>';
fetch('api/read_laporan.php')
.then(res => res.json())
.then(data => {
_laporanCache = data;
if (label) label.textContent = `${data.length} laporan ditemukan`;
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const canManage = gisUser.role !== 'walikota';
if (!data.length) {
list.innerHTML = '<div style="text-align:center;padding:24px;color:var(--muted)"><i class="fas fa-inbox"></i><br>Belum ada laporan masuk</div>';
return;
}
list.innerHTML = data.map(r => `
<div class="lm-report-card">
<div class="lm-rc-top">
<div>
<div class="lm-rc-name"><i class="fas fa-user-circle" style="color:var(--blue)"></i> ${r.nama_pelapor}</div>
<div class="lm-rc-time">${r.kontak ? '📞 ' + r.kontak + ' · ' : ''}${r.created_at}</div>
</div>
<span class="lm-status ${r.status}">${r.status}</span>
</div>
<div class="lm-rc-desc">${r.deskripsi_laporan}</div>
${r.lat && r.lat != 0 ? `<div style="font-size:11px;color:var(--muted);margin-bottom:8px"><i class="fas fa-map-pin"></i> ${parseFloat(r.lat).toFixed(5)}, ${parseFloat(r.lng).toFixed(5)}</div>` : ''}
<div class="lm-rc-footer">
<div class="lm-rc-actions">
${canManage && r.status !== 'selesai' ? `<button class="lm-rc-btn selesai-btn" onclick="updateStatusLaporan(${r.id},'selesai')"><i class="fas fa-check"></i> Selesai</button>` : ''}
${canManage && r.status === 'menunggu' ? `<button class="lm-rc-btn" style="background:#FFEDD5;color:#2563eb" onclick="updateStatusLaporan(${r.id},'diproses')"><i class="fas fa-cog"></i> Proses</button>` : ''}
${canManage ? `<button class="lm-rc-btn hapus-btn" onclick="hapusLaporan(${r.id})"><i class="fas fa-trash"></i></button>` : ''}
</div>
</div>
</div>
`).join('');
updateLaporanBadge();
})
.catch(() => {
list.innerHTML = '<div style="text-align:center;padding:20px;color:#ef4444">Gagal memuat laporan</div>';
});
}
function updateStatusLaporan(id, status) {
fetch('api/update_laporan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal update status'); return; }
loadDaftarLaporan();
}).catch(() => alert('Error update status laporan'));
}
function hapusLaporan(id) {
if (!confirm('Hapus laporan ini?')) return;
fetch('api/delete_laporan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
}).then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal hapus'); return; }
loadDaftarLaporan();
}).catch(() => alert('Error hapus laporan'));
}
function updateLaporanBadge() {
const badge = document.getElementById('laporan-badge');
if (!badge || !IS_LOGGED_IN) return;
fetch('api/read_laporan.php').then(r => r.json()).then(data => {
const menunggu = data.filter(r => r.status === 'menunggu').length;
if (menunggu > 0) {
badge.textContent = menunggu;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
}).catch(() => { });
}
// Load badge saat halaman pertama kali
if (IS_LOGGED_IN) setTimeout(updateLaporanBadge, 1500);
/* ΓöÇΓöÇ DATA WARGA MODAL ΓöÇΓöÇ */
function openWargaModal(kkId, kkNama) {
const existing = document.getElementById('dw-overlay');
if (existing) existing.remove();
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const role = gisUser.role || '';
const canManage = role !== 'walikota' && IS_LOGGED_IN;
const overlay = document.createElement('div');
overlay.className = 'hb-overlay';
overlay.id = 'dw-overlay';
overlay.innerHTML = `
<div class="hb-box" style="max-width:700px">
<div class="hb-head">
<div class="hb-head-icon"><i class="fas fa-users-cog"></i></div>
<div style="flex:1;min-width:0">
<div class="hb-head-title">Data Anggota Keluarga</div>
<div style="font-size:11px;opacity:.75;margin-top:1px">${kkNama}</div>
</div>
<button class="hb-head-close" onclick="document.getElementById('dw-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="hb-body">
${canManage ? `
<div class="hb-form" id="dw-form-tambah" style="display:none;grid-template-columns:1fr 1fr;gap:10px">
<div style="grid-column:1/-1" class="hb-form-title">+ Tambah/Edit Warga</div>
<input type="hidden" id="dw-id" value="">
<div class="km-field">
<label>NIK</label>
<input id="dw-nik" placeholder="Opsional">
</div>
<div class="km-field">
<label>Nama Warga *</label>
<input id="dw-nama" placeholder="Nama Lengkap">
</div>
<div class="km-field">
<label>Hubungan Keluarga *</label>
<select id="dw-hubungan">
<option value="Kepala Keluarga">Kepala Keluarga</option>
<option value="Istri">Istri</option>
<option value="Anak">Anak</option>
<option value="Lainnya">Lainnya</option>
</select>
</div>
<div class="km-field">
<label>Tanggal Lahir</label>
<input type="date" id="dw-tgl">
</div>
<div class="km-field">
<label>Pendidikan</label>
<input id="dw-pend" placeholder="SD/SMP/SMA/dll">
</div>
<div class="km-field">
<label>Riwayat Penyakit</label>
<input id="dw-peny" placeholder="Asma, Jantung, dll">
</div>
<button style="grid-column:1/-1" class="hb-submit" onclick="saveWarga(${kkId})">
<i class="fas fa-save"></i> Simpan Data Warga
</button>
</div>
<button class="pbtn pbtn-y" style="margin-bottom:15px;width:100%" onclick="
var frm=document.getElementById('dw-form-tambah');
frm.style.display='grid';
document.getElementById('dw-id').value='';
document.getElementById('dw-nik').value='';
document.getElementById('dw-nama').value='';
document.getElementById('dw-hubungan').value='Kepala Keluarga';
document.getElementById('dw-tgl').value='';
document.getElementById('dw-pend').value='';
document.getElementById('dw-peny').value='';
">
<i class="fas fa-plus"></i> Tambah Anggota Keluarga
</button>
` : ''}
<div class="hb-list-title">Daftar Anggota Keluarga</div>
<div id="dw-list"><div class="hb-empty"><i class="fas fa-spinner fa-spin"></i> Memuat...</div></div>
</div>
</div>
`;
document.body.appendChild(overlay);
loadWarga(kkId, canManage);
}
function loadWarga(kkId, canManage) {
const list = document.getElementById('dw-list');
fetch('api/read_warga.php?kemiskinan_id=' + kkId)
.then(res => res.json())
.then(resp => {
if (resp.status !== 'success') { list.innerHTML = '<div class="hb-empty">Gagal memuat data</div>'; return; }
if (resp.data.length === 0) {
list.innerHTML = '<div class="hb-empty"><i class="fas fa-users-slash"></i><br>Belum ada data anggota keluarga</div>';
return;
}
let html = '';
resp.data.forEach(d => {
const hbId = d.id;
const editBtn = canManage ? `<button class="pbtn pbtn-y" style="padding:4px 8px;font-size:12px" onclick="editWarga(${hbId})"><i class="fas fa-pen"></i></button>
<button class="pbtn pbtn-r" style="padding:4px 8px;font-size:12px" onclick="deleteWarga(${hbId}, ${kkId})"><i class="fas fa-trash"></i></button>` : '';
html += `<div class="hb-item" id="dw-item-${hbId}" data-nik="${d.nik || ''}" data-nama="${(d.nama_warga || '').replace(/"/g, '&quot;')}" data-hub="${d.hubungan_keluarga || ''}" data-tgl="${d.tanggal_lahir || ''}" data-pend="${(d.pendidikan || '').replace(/"/g, '&quot;')}" data-peny="${(d.riwayat_penyakit || '').replace(/"/g, '&quot;')}" style="display:flex;justify-content:space-between;align-items:center">
<div style="flex:1">
<div style="font-weight:600;color:var(--dark)">${d.nama_warga} <span class="pi-badge poor" style="font-size:10px">${d.hubungan_keluarga || ''}</span></div>
<div style="font-size:12px;color:#64748b;margin-top:4px">
${d.nik ? 'NIK: ' + d.nik + ' &bull; ' : ''}${d.tanggal_lahir ? 'Lahir: ' + d.tanggal_lahir + ' &bull; ' : ''}${d.pendidikan ? 'Pend: ' + d.pendidikan : ''}
</div>
${d.riwayat_penyakit ? '<div style="font-size:12px;color:#dc2626;margin-top:2px"><i class="fas fa-notes-medical"></i> ' + d.riwayat_penyakit + '</div>' : ''}
</div>
${canManage ? '<div style="display:flex;gap:5px;margin-left:10px">' + editBtn + '</div>' : ''}
</div>`;
});
list.innerHTML = html;
})
.catch(() => list.innerHTML = '<div class="hb-empty">Gagal memuat data</div>');
}
function saveWarga(kkId) {
const id = document.getElementById('dw-id').value;
const nik = document.getElementById('dw-nik').value.trim();
const nama = document.getElementById('dw-nama').value.trim();
const hubungan = document.getElementById('dw-hubungan').value;
const tgl = document.getElementById('dw-tgl').value;
const pend = document.getElementById('dw-pend').value.trim();
const peny = document.getElementById('dw-peny').value.trim();
if (!nama || !hubungan) { alert('Nama dan Hubungan Keluarga wajib diisi!'); return; }
const endpoint = id ? 'api/update_warga.php' : 'api/save_warga.php';
const payload = id
? { id, nik, nama_warga: nama, hubungan_keluarga: hubungan, tanggal_lahir: tgl, pendidikan: pend, riwayat_penyakit: peny }
: { kemiskinan_id: kkId, nik, nama_warga: nama, hubungan_keluarga: hubungan, tanggal_lahir: tgl, pendidikan: pend, riwayat_penyakit: peny };
fetch(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })
.then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal simpan: ' + (resp.message || 'Error')); return; }
document.getElementById('dw-form-tambah').style.display = 'none';
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const canManage = (gisUser.role || '') !== 'walikota' && IS_LOGGED_IN;
loadWarga(kkId, canManage);
}).catch(() => alert('Error menyimpan warga'));
}
function editWarga(id) {
const el = document.getElementById('dw-item-' + id);
if (!el) return;
document.getElementById('dw-form-tambah').style.display = 'grid';
document.getElementById('dw-id').value = id;
document.getElementById('dw-nik').value = el.dataset.nik || '';
document.getElementById('dw-nama').value = el.dataset.nama || '';
document.getElementById('dw-hubungan').value = el.dataset.hub || 'Kepala Keluarga';
document.getElementById('dw-tgl').value = el.dataset.tgl || '';
document.getElementById('dw-pend').value = el.dataset.pend || '';
document.getElementById('dw-peny').value = el.dataset.peny || '';
document.getElementById('dw-form-tambah').scrollIntoView({ behavior: 'smooth' });
}
function deleteWarga(id, kkId) {
if (!confirm('Hapus anggota keluarga ini?')) return;
fetch('api/delete_warga.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) })
.then(res => res.json()).then(resp => {
if (resp.status !== 'success') { alert('Gagal hapus'); return; }
const el = document.getElementById('dw-item-' + id);
if (el) el.remove();
const list = document.getElementById('dw-list');
if (list && !list.querySelector('.hb-item')) {
list.innerHTML = '<div class="hb-empty"><i class="fas fa-users-slash"></i><br>Belum ada data anggota keluarga</div>';
}
}).catch(() => alert('Error hapus warga'));
}
/* ΓöÇΓöÇ FULL DATA WARGA MODAL ΓöÇΓöÇ */
let _allDataWarga = [];
function openDataWargaModal() {
const existing = document.getElementById('ftm-warga-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'ftm-overlay';
overlay.id = 'ftm-warga-overlay';
overlay.innerHTML = `
<div class="ftm-box">
<div class="ftm-head">
<div>
<div class="ftm-title">Data Warga</div>
<div class="ftm-sub">Daftar seluruh individu yang terdata dalam program bantuan sosial</div>
</div>
<button class="ftm-close" onclick="document.getElementById('ftm-warga-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="ftm-body">
<div class="ftm-filters">
<select class="ftm-filter-select" id="ftm-filter-kategori" onchange="renderDataWargaTable()">
<option value="">Semua Kategori</option>
<option value="Sangat Miskin">Sangat Miskin</option>
<option value="Miskin">Miskin</option>
<option value="Hampir Miskin">Hampir Miskin</option>
</select>
<select class="ftm-filter-select" id="ftm-filter-penyakit" onchange="renderDataWargaTable()">
<option value="">Semua Kondisi</option>
<option value="Sehat">Sehat / Tidak Ada Penyakit</option>
<option value="Sakit">Ada Penyakit</option>
</select>
</div>
<div class="ftm-table-card">
<table>
<thead>
<tr>
<th>Nama Warga</th>
<th>Kepala Keluarga / Alamat</th>
<th>Kategori KK</th>
<th>Usia / Tgl Lahir</th>
<th>Pendidikan</th>
<th>Penyakit</th>
<th>Bantuan</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="ftm-warga-tbody">
<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--muted)"><i class="fas fa-spinner fa-spin"></i> Memuat data...</td></tr>
</tbody>
</table>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
fetch('api/read_semua_warga.php')
.then(r => r.json())
.then(res => {
if(res.status === 'success') {
_allDataWarga = res.data;
renderDataWargaTable();
} else {
document.getElementById('ftm-warga-tbody').innerHTML = '<tr><td colspan="8" style="text-align:center;color:red">Gagal memuat data</td></tr>';
}
})
.catch(err => {
document.getElementById('ftm-warga-tbody').innerHTML = '<tr><td colspan="8" style="text-align:center;color:red">Terjadi kesalahan jaringan</td></tr>';
});
}
function renderDataWargaTable() {
const tbody = document.getElementById('ftm-warga-tbody');
if(!tbody) return;
const catFilter = document.getElementById('ftm-filter-kategori').value;
const sickFilter = document.getElementById('ftm-filter-penyakit').value;
let filtered = _allDataWarga.filter(d => {
let matchCat = catFilter ? d.kategori_label === catFilter : true;
let isSakit = d.riwayat_penyakit && d.riwayat_penyakit.trim() !== '' && d.riwayat_penyakit.toLowerCase() !== 'tidak ada';
let matchSick = true;
if(sickFilter === 'Sehat') matchSick = !isSakit;
if(sickFilter === 'Sakit') matchSick = isSakit;
return matchCat && matchSick;
});
if(filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;padding:30px;color:var(--muted)">Tidak ada data ditemukan</td></tr>';
return;
}
let html = '';
filtered.forEach(d => {
const isSakit = d.riwayat_penyakit && d.riwayat_penyakit.trim() !== '' && d.riwayat_penyakit.toLowerCase() !== 'tidak ada';
const healthHtml = isSakit
? `<div style="font-size:13px;color:var(--red)">${d.riwayat_penyakit}</div>`
: `<div style="font-size:13px;color:var(--green)"><i class="fas fa-check"></i> Sehat</div>`;
const pend = (d.pendidikan && d.pendidikan.trim() !== '') ? d.pendidikan : '-';
html += `<tr>
<td>
<div style="font-weight:600;color:var(--navy)">${d.nama_warga} <span style="font-size:10px;color:var(--muted);font-weight:400">(${d.hubungan_keluarga || 'Anggota'})</span></div>
<div style="font-size:12px;color:var(--muted);margin-top:2px">${d.nik || 'NIK Tidak Ada'}</div>
</td>
<td>
<div style="font-size:13px;font-weight:600;color:var(--text)">KK: ${d.nama_kk || '-'}</div>
<div style="font-size:12px;color:var(--muted);margin-top:2px;max-width:200px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${d.alamat || '-'}</div>
</td>
<td>
<span class="ftm-badge" style="color:${d.kategori_color};border:1px solid ${d.kategori_color}33;background:${d.kategori_color}11">${d.kategori_label}</span>
</td>
<td>
<div style="font-size:13px;font-weight:600;color:var(--text)">${d.usia_label}</div>
<div style="font-size:12px;color:var(--muted);margin-top:2px">${d.tgl_lahir_format}</div>
</td>
<td style="font-size:13px">${pend}</td>
<td>${healthHtml}</td>
<td>
<div style="font-size:13px;font-weight:600;color:${d.jumlah_bantuan > 0 ? 'var(--green)' : 'var(--muted)'}">${d.jumlah_bantuan}x</div>
</td>
<td>
<button class="ftm-btn-icon" title="Lihat di Peta" onclick="document.getElementById('ftm-warga-overlay').remove(); focusFeature('kemiskinan', ${d.kemiskinan_id})">
<i class="fas fa-map-marked-alt"></i>
</button>
</td>
</tr>`;
});
tbody.innerHTML = html;
}
/* ΓöÇΓöÇ FULL DATA MASJID MODAL ΓöÇΓöÇ */
let _allDataMasjid = [];
function openDataMasjidModal() {
const existing = document.getElementById('ftm-masjid-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'ftm-overlay';
overlay.id = 'ftm-masjid-overlay';
overlay.innerHTML = `
<div class="ftm-box">
<div class="ftm-head">
<div>
<div class="ftm-title">Data Rumah Ibadah</div>
<div class="ftm-sub">Daftar fasilitas ibadah beserta area jangkauannya</div>
</div>
<button class="ftm-close" onclick="document.getElementById('ftm-masjid-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="ftm-body">
<div class="ftm-filters">
<input type="text" id="ftm-search-masjid" class="ftm-filter-select" placeholder="Cari nama atau pengurus..." onkeyup="renderDataMasjidTable()" style="width:250px">
</div>
<div class="ftm-table-card">
<table>
<thead>
<tr>
<th>Nama Fasilitas</th>
<th>Penanggung Jawab</th>
<th>Alamat Lengkap</th>
<th>Radius Jangkauan</th>
<th>Terakhir Diubah</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="ftm-masjid-tbody">
<tr><td colspan="6" style="text-align:center;padding:40px;color:var(--muted)"><i class="fas fa-spinner fa-spin"></i> Memuat data...</td></tr>
</tbody>
</table>
</div>
</div>
</div>`;
document.body.appendChild(overlay);
fetch('api/read_masjid.php')
.then(r => r.json())
.then(res => {
if(Array.isArray(res)) _allDataMasjid = res;
else if (res.status === 'success' && res.data) _allDataMasjid = res.data;
else _allDataMasjid = [];
renderDataMasjidTable();
})
.catch(err => {
document.getElementById('ftm-masjid-tbody').innerHTML = '<tr><td colspan="6" style="text-align:center;color:red">Terjadi kesalahan jaringan</td></tr>';
});
}
function renderDataMasjidTable() {
const tbody = document.getElementById('ftm-masjid-tbody');
if(!tbody) return;
const searchVal = document.getElementById('ftm-search-masjid').value.toLowerCase();
let filtered = _allDataMasjid.filter(d => {
if(!searchVal) return true;
return (d.nama_masjid && d.nama_masjid.toLowerCase().includes(searchVal)) ||
(d.pic_masjid && d.pic_masjid.toLowerCase().includes(searchVal));
});
if(filtered.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:30px;color:var(--muted)">Tidak ada data ditemukan</td></tr>';
return;
}
let html = '';
filtered.forEach(d => {
const radiusHtml = `<span class="ftm-badge" style="color:#059669;border:1px solid #10b981;background:#D1FAE5">${d.radius || 0} meter</span>`;
let tgl_format = '-';
if(d.updated_at) {
const date = new Date(d.updated_at);
tgl_format = date.toLocaleDateString('id-ID', { year: 'numeric', month: 'short', day: 'numeric' });
}
html += `<tr>
<td>
<div style="font-weight:600;color:var(--navy)">${d.nama_masjid}</div>
<div style="font-size:12px;color:var(--muted);margin-top:2px"><i class="fas fa-place-of-worship" style="font-size:10px;margin-right:4px"></i>Rumah Ibadah</div>
</td>
<td>
<div style="font-size:13px;font-weight:600;color:var(--text)">${d.pic_masjid || '-'}</div>
</td>
<td>
<div style="font-size:13px;color:var(--muted);max-width:250px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${d.alamat_masjid || ''}">${d.alamat_masjid || '-'}</div>
</td>
<td>${radiusHtml}</td>
<td style="font-size:13px;color:var(--muted)">${tgl_format}</td>
<td>
<button class="ftm-btn-icon" title="Lihat di Peta" onclick="document.getElementById('ftm-masjid-overlay').remove(); focusFeature('masjid', ${d.id})">
<i class="fas fa-map-marked-alt"></i>
</button>
</td>
</tr>`;
});
tbody.innerHTML = html;
}
/* ΓöÇΓöÇ LOGIN MODAL ΓöÇΓöÇ */
function openLoginModal() {
const existing = document.getElementById('lm-overlay');
if (existing) existing.remove();
const el = document.createElement('div');
el.className = 'lm-overlay'; el.id = 'lm-overlay';
el.innerHTML = `
<div class="lm-box" style="position:relative">
<button class="lm-close-btn" onclick="document.getElementById('lm-overlay').remove()"><i class="fas fa-times"></i></button>
<div class="lm-head">
<div class="lm-head-icon"><i class="fas fa-map-location-dot"></i></div>
<div class="lm-head-title">SiPetaSos</div>
<div class="lm-head-sub">Sistem Informasi Peta Sosial &amp; Rumah Ibadah</div>
</div>
<div class="lm-body">
<div id="lm-alert" style="display:none" class="lm-alert error"><i class="fas fa-circle-exclamation"></i><span id="lm-alert-msg"></span></div>
<div class="lm-field">
<label>Username</label>
<div class="lm-input-wrap">
<i class="fas fa-user lm-ico"></i>
<input type="text" id="lm-user" placeholder="Masukkan username" autocomplete="username">
</div>
</div>
<div class="lm-field">
<label>Password</label>
<div class="lm-input-wrap">
<i class="fas fa-lock lm-ico"></i>
<input type="password" id="lm-pass" placeholder="Masukkan password" autocomplete="current-password" onkeydown="if(event.key==='Enter')doLogin()">
<button type="button" class="lm-eye" onclick="lmToggleEye()"><i class="fas fa-eye" id="lm-eye-icon"></i></button>
</div>
</div>
<button class="lm-submit" id="lm-btn" onclick="doLogin()"><i class="fas fa-right-to-bracket"></i>&ensp;Masuk ke Sistem</button>
</div>
</div>`;
document.body.appendChild(el);
el.addEventListener('click', e => { if (e.target === el) el.remove(); });
setTimeout(() => document.getElementById('lm-user')?.focus(), 100);
}
function lmToggleEye() {
const inp = document.getElementById('lm-pass');
const ico = document.getElementById('lm-eye-icon');
if (!inp) return;
inp.type = inp.type === 'password' ? 'text' : 'password';
ico.className = inp.type === 'password' ? 'fas fa-eye' : 'fas fa-eye-slash';
}
function doLogin() {
const username = document.getElementById('lm-user')?.value.trim();
const password = document.getElementById('lm-pass')?.value;
const btn = document.getElementById('lm-btn');
const alert = document.getElementById('lm-alert');
const alertMsg = document.getElementById('lm-alert-msg');
if (!username || !password) {
alert.style.display = 'flex'; alertMsg.textContent = 'Username dan password wajib diisi!'; return;
}
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>&ensp;Memproses...';
fetch('api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
}).then(r => r.json()).then(res => {
if (res.status === 'success') {
sessionStorage.setItem('gis_logged_in', 'true');
sessionStorage.setItem('gis_user', JSON.stringify(res.user));
document.getElementById('lm-overlay')?.remove();
window.location.reload();
} else {
alert.style.display = 'flex'; alertMsg.textContent = res.message || 'Login gagal';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-right-to-bracket"></i>&ensp;Masuk ke Sistem';
}
}).catch(() => {
alert.style.display = 'flex'; alertMsg.textContent = 'Terjadi kesalahan jaringan';
btn.disabled = false; btn.innerHTML = '<i class="fas fa-right-to-bracket"></i>&ensp;Masuk ke Sistem';
});
}
/* ΓöÇΓöÇ PROFIL MODAL ΓöÇΓöÇ */
function openProfilModal() {
const existing = document.getElementById('prf-overlay');
if (existing) existing.remove();
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const nama = gisUser.nama_lengkap || 'User';
const username = gisUser.username || '-';
const role = gisUser.role || '-';
const initial = nama.charAt(0).toUpperCase();
const roleLabel = role === 'admin' ? 'Administrator' : role === 'operator' ? 'Operator' : role === 'walikota' ? 'Walikota' : role;
const el = document.createElement('div');
el.className = 'prf-overlay'; el.id = 'prf-overlay';
el.innerHTML = `
<div class="prf-box">
<div class="prf-head" style="position:relative">
<button class="prf-close" onclick="document.getElementById('prf-overlay').remove()"><i class="fas fa-times"></i></button>
<div class="prf-avatar">${initial}</div>
<div class="prf-head-name" id="prf-disp-name">${nama}</div>
<div class="prf-head-role">${roleLabel}</div>
</div>
<div class="prf-body">
<div class="prf-card">
<div class="prf-row">
<span class="prf-label">Nama Lengkap</span>
<div class="prf-edit-row">
<span class="prf-value" id="prf-nama-val">${nama}</span>
<button class="prf-save-btn" onclick="prfToggleEdit()" id="prf-edit-btn" style="background:var(--panel2);color:var(--navy)"><i class="fas fa-pen"></i></button>
</div>
</div>
<div id="prf-edit-form" style="display:none;padding-top:12px">
<input class="prf-input" id="prf-nama-input" value="${nama}" style="width:100%;box-sizing:border-box" placeholder="Nama lengkap baru">
<div style="display:flex;gap:8px;margin-top:8px">
<button class="prf-save-btn" onclick="prfSaveName()"><i class="fas fa-floppy-disk"></i> Simpan</button>
<button class="prf-save-btn" onclick="prfToggleEdit()" style="background:var(--panel2);color:var(--navy)">Batal</button>
</div>
<div id="prf-save-msg" style="display:none;margin-top:8px;font-size:12px;color:var(--green);font-weight:600"><i class="fas fa-check"></i> Tersimpan!</div>
</div>
<div class="prf-row">
<span class="prf-label">Username</span>
<span class="prf-value">${username}</span>
</div>
<div class="prf-row">
<span class="prf-label">Role</span>
<span class="prf-value">${roleLabel}</span>
</div>
</div>
<button onclick="openChangePassword(); document.getElementById('prf-overlay').remove();" style="width:100%;padding:12px;border:1.5px solid var(--border);border-radius:10px;background:#fff;font:600 13px 'Plus Jakarta Sans',sans-serif;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:8px;color:var(--navy)">
<i class="fas fa-key"></i> Ganti Password
</button>
</div>
</div>`;
document.body.appendChild(el);
el.addEventListener('click', e => { if (e.target === el) el.remove(); });
}
function prfToggleEdit() {
const form = document.getElementById('prf-edit-form');
const msg = document.getElementById('prf-save-msg');
if (!form) return;
const isHidden = form.style.display === 'none';
form.style.display = isHidden ? 'block' : 'none';
if (msg) msg.style.display = 'none';
if (isHidden) document.getElementById('prf-nama-input')?.focus();
}
function prfSaveName() {
const input = document.getElementById('prf-nama-input');
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const nama = input?.value.trim();
if (!nama) return alert('Nama tidak boleh kosong');
fetch('api/update_profile.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: gisUser.username, nama_lengkap: nama })
}).then(r => r.json()).then(res => {
if (res.status === 'success') {
gisUser.nama_lengkap = nama;
sessionStorage.setItem('gis_user', JSON.stringify(gisUser));
const valEl = document.getElementById('prf-nama-val');
const dispName = document.getElementById('prf-disp-name');
if (valEl) valEl.textContent = nama;
if (dispName) dispName.textContent = nama;
const msgEl = document.getElementById('prf-save-msg');
if (msgEl) msgEl.style.display = 'block';
const nameEls = document.querySelectorAll('#dd-nama, .user-name-display');
nameEls.forEach(el => el.textContent = nama);
setTimeout(() => prfToggleEdit(), 1500);
} else { alert(res.message || 'Gagal menyimpan'); }
}).catch(() => alert('Terjadi kesalahan jaringan'));
}
/* ΓöÇΓöÇ KELOLA AKUN MODAL ΓöÇΓöÇ */
let _uaUsers = [];
let _uaEditId = null;
function openKelolaAkunModal() {
const existing = document.getElementById('ua-overlay');
if (existing) existing.remove();
const el = document.createElement('div');
el.className = 'ftm-overlay'; el.id = 'ua-overlay';
el.innerHTML = `
<div class="ftm-box" style="max-width:960px">
<div class="ftm-head">
<div>
<div class="ftm-title"><i class="fas fa-users-gear" style="color:var(--blue);margin-right:8px"></i>Kelola Akun Pengguna</div>
<div class="ftm-sub">Tambah, edit, dan hapus akun operator sistem</div>
</div>
<button class="ftm-close" onclick="document.getElementById('ua-overlay').remove()"><i class="fas fa-times"></i></button>
</div>
<div class="ftm-body">
<div class="ua-form">
<div class="ua-form-title"><i class="fas fa-user-plus" style="color:var(--blue)"></i><span id="ua-form-label">Tambah Akun Baru</span></div>
<div class="ua-grid">
<div class="ua-field"><label>Nama Lengkap <span style="color:red">*</span></label><input id="ua-nama" placeholder="Nama lengkap..."></div>
<div class="ua-field"><label>Username <span style="color:red">*</span></label><input id="ua-username" placeholder="Username..."></div>
<div class="ua-field"><label>Password <span id="ua-pass-hint" style="color:var(--muted);font-weight:400;text-transform:none">(wajib diisi)</span></label><input type="password" id="ua-pass" placeholder="Min. 6 karakter"></div>
<div class="ua-field"><label>Role</label>
<select id="ua-role">
<option value="operator">Operator</option>
<option value="admin">Admin</option>
</select>
</div>
</div>
<div style="display:flex;gap:8px;align-items:center;margin-top:2px">
<button class="ua-submit" onclick="uaSaveUser()"><i class="fas fa-floppy-disk"></i> <span id="ua-submit-label">Simpan Akun</span></button>
<button id="ua-cancel-btn" onclick="uaCancelEdit()" style="display:none;padding:10px 16px;border:1.5px solid var(--border);border-radius:8px;background:#fff;font:700 13px 'Plus Jakarta Sans',sans-serif;cursor:pointer"><i class="fas fa-xmark"></i> Batal</button>
</div>
<div id="ua-form-alert" style="display:none;margin-top:10px;font-size:13px;font-weight:600;padding:8px 12px;border-radius:8px"></div>
</div>
<div class="ftm-table-card">
<table>
<thead><tr>
<th>Pengguna</th><th>Username</th><th>Role</th><th>Dibuat</th><th>Aksi</th>
</tr></thead>
<tbody id="ua-tbody">
<tr><td colspan="5" style="text-align:center;padding:32px;color:var(--muted)"><i class="fas fa-spinner fa-spin"></i> Memuat...</td></tr>
</tbody>
</table>
</div>
</div>
</div>`;
document.body.appendChild(el);
el.addEventListener('click', e => { if (e.target === el) el.remove(); });
uaLoadUsers();
}
function uaLoadUsers() {
fetch('api/get_users.php').then(r => r.json()).then(data => {
_uaUsers = data;
uaRenderTable();
}).catch(() => {
const tb = document.getElementById('ua-tbody');
if (tb) tb.innerHTML = '<tr><td colspan="5" style="text-align:center;color:red">Gagal memuat data</td></tr>';
});
}
function uaRenderTable() {
const tb = document.getElementById('ua-tbody');
if (!tb) return;
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const colors = ['#3b82f6','#8b5cf6','#ec4899','#f59e0b','#10b981'];
if (!_uaUsers.length) {
tb.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:24px;color:var(--muted)">Tidak ada pengguna</td></tr>';
return;
}
tb.innerHTML = _uaUsers.map((u, i) => {
const col = colors[i % colors.length];
const initial = (u.nama_lengkap || u.username).charAt(0).toUpperCase();
const roleHtml = u.role === 'admin'
? `<span class="ua-role-badge ua-role-admin"><i class="fas fa-shield-halved"></i> Admin</span>`
: `<span class="ua-role-badge ua-role-operator"><i class="fas fa-user-gear"></i> Operator</span>`;
const tgl = u.created_at ? new Date(u.created_at).toLocaleDateString('id-ID', { year:'numeric', month:'short', day:'numeric' }) : '-';
const isSelf = u.username === gisUser.username;
return `<tr>
<td><div style="display:flex;align-items:center;gap:10px">
<div class="ua-avatar" style="background:${col}">${initial}</div>
<div>
<div style="font-weight:600;color:var(--navy)">${u.nama_lengkap}${isSelf ? ' <span style="font-size:10px;background:#eff6ff;color:var(--blue);padding:1px 6px;border-radius:99px;font-weight:700">Saya</span>' : ''}</div>
</div>
</div></td>
<td style="font-size:13px;color:var(--muted)">${u.username}</td>
<td>${roleHtml}</td>
<td style="font-size:13px;color:var(--muted)">${tgl}</td>
<td><div style="display:flex;gap:6px">
<button onclick="uaEditUser(${u.id})" style="padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:#fff;font-size:12px;font-weight:700;cursor:pointer;color:var(--navy)"><i class="fas fa-pen"></i></button>
${!isSelf && u.id !== 1 ? `<button onclick="uaDeleteUser(${u.id},'${u.nama_lengkap}')" style="padding:6px 12px;border:none;border-radius:6px;background:#fee2e2;font-size:12px;font-weight:700;cursor:pointer;color:#dc2626"><i class="fas fa-trash"></i></button>` : ''}
</div></td>
</tr>`;
}).join('');
}
function uaEditUser(id) {
const u = _uaUsers.find(x => x.id == id);
if (!u) return;
_uaEditId = id;
document.getElementById('ua-nama').value = u.nama_lengkap;
document.getElementById('ua-username').value = u.username;
document.getElementById('ua-username').disabled = true;
document.getElementById('ua-pass').value = '';
document.getElementById('ua-role').value = u.role;
document.getElementById('ua-form-label').textContent = 'Edit Akun';
document.getElementById('ua-submit-label').textContent = 'Update Akun';
document.getElementById('ua-pass-hint').textContent = '(kosongkan jika tidak ubah)';
document.getElementById('ua-cancel-btn').style.display = 'inline-flex';
document.getElementById('ua-nama').focus();
}
function uaCancelEdit() {
_uaEditId = null;
document.getElementById('ua-nama').value = '';
document.getElementById('ua-username').value = '';
document.getElementById('ua-username').disabled = false;
document.getElementById('ua-pass').value = '';
document.getElementById('ua-role').value = 'operator';
document.getElementById('ua-form-label').textContent = 'Tambah Akun Baru';
document.getElementById('ua-submit-label').textContent = 'Simpan Akun';
document.getElementById('ua-pass-hint').textContent = '(wajib diisi)';
document.getElementById('ua-cancel-btn').style.display = 'none';
document.getElementById('ua-form-alert').style.display = 'none';
}
function uaShowAlert(msg, ok) {
const el = document.getElementById('ua-form-alert');
if (!el) return;
el.style.display = 'block';
el.style.background = ok ? '#f0fdf4' : '#fee2e2';
el.style.color = ok ? '#15803d' : '#dc2626';
el.innerHTML = `<i class="fas fa-${ok ? 'check' : 'circle-exclamation'}"></i> ${msg}`;
if (ok) setTimeout(() => { el.style.display = 'none'; }, 2500);
}
function uaSaveUser() {
const nama = document.getElementById('ua-nama')?.value.trim();
const username = document.getElementById('ua-username')?.value.trim();
const pass = document.getElementById('ua-pass')?.value;
const role = document.getElementById('ua-role')?.value;
if (_uaEditId) {
fetch('api/update_user.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: _uaEditId, nama_lengkap: nama, role, password: pass })
}).then(r => r.json()).then(res => {
if (res.status === 'success') {
uaShowAlert('Akun berhasil diperbarui!', true);
uaCancelEdit(); uaLoadUsers();
} else { uaShowAlert(res.message || 'Gagal update', false); }
}).catch(() => uaShowAlert('Terjadi kesalahan jaringan', false));
} else {
fetch('api/create_user.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama_lengkap: nama, username, password: pass, role })
}).then(r => r.json()).then(res => {
if (res.status === 'success') {
uaShowAlert('Akun berhasil dibuat!', true);
uaCancelEdit(); uaLoadUsers();
} else { uaShowAlert(res.message || 'Gagal membuat akun', false); }
}).catch(() => uaShowAlert('Terjadi kesalahan jaringan', false));
}
}
function uaDeleteUser(id, nama) {
if (!confirm(`Hapus akun "${nama}"? Tindakan ini tidak dapat dibatalkan.`)) return;
fetch('api/delete_user.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
}).then(r => r.json()).then(res => {
if (res.status === 'success') { uaLoadUsers(); }
else { alert(res.message || 'Gagal menghapus'); }
}).catch(() => alert('Terjadi kesalahan jaringan'));
}
/* ΓöÇΓöÇ CHANGE PASSWORD MODAL ΓöÇΓöÇ */
function openChangePassword() {
// Close user dropdown first
const menu = document.getElementById('user-menu');
if (menu) menu.classList.remove('open');
// Reset form
const form = document.getElementById('cp-form');
if (form) form.reset();
const fill = document.getElementById('cp-strength-fill');
const lbl = document.getElementById('cp-strength-lbl');
if (fill) { fill.style.width='0'; fill.style.background=''; }
if (lbl) lbl.textContent = '';
document.getElementById('cp-err').classList.remove('show');
document.getElementById('cp-ok').classList.remove('show');
const btn = document.getElementById('cp-submit-btn');
btn.innerHTML = '<i class="fas fa-floppy-disk"></i> Simpan Password Baru';
btn.style.background = '';
btn.disabled = false;
document.getElementById('cp-modal').classList.remove('hidden');
}
function closeChangePassword() {
document.getElementById('cp-modal').classList.add('hidden');
}
function cpToggleEye(inputId, iconId) {
const inp = document.getElementById(inputId);
const ico = document.getElementById(iconId);
if (inp.type === 'password') {
inp.type = 'text';
ico.classList.replace('fa-eye','fa-eye-slash');
} else {
inp.type = 'password';
ico.classList.replace('fa-eye-slash','fa-eye');
}
}
function cpCheckStrength(val) {
const fill = document.getElementById('cp-strength-fill');
const lbl = document.getElementById('cp-strength-lbl');
if (!val) { fill.style.width='0'; lbl.textContent=''; return; }
let s = 0;
if (val.length >= 6) s++;
if (val.length >= 10) s++;
if (/[A-Z]/.test(val)) s++;
if (/[0-9]/.test(val)) s++;
if (/[^A-Za-z0-9]/.test(val)) s++;
const lv = [
{pct:'20%',bg:'#ef4444',txt:'Sangat Lemah'},
{pct:'40%',bg:'#f97316',txt:'Lemah'},
{pct:'60%',bg:'#f59e0b',txt:'Sedang'},
{pct:'80%',bg:'#3b82f6',txt:'Kuat'},
{pct:'100%',bg:'#10b981',txt:'Sangat Kuat'},
][Math.max(0,s-1)];
fill.style.width = lv.pct;
fill.style.background = lv.bg;
lbl.textContent = lv.txt;
lbl.style.color = lv.bg;
}
document.getElementById('cp-form').addEventListener('submit', async function(e) {
e.preventDefault();
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
const uname = gisUser.username || '';
const oldP = document.getElementById('cp-old').value;
const newP = document.getElementById('cp-new').value;
const conP = document.getElementById('cp-con').value;
const err = document.getElementById('cp-err');
const errTxt = document.getElementById('cp-err-text');
const ok = document.getElementById('cp-ok');
const btn = document.getElementById('cp-submit-btn');
err.classList.remove('show'); ok.classList.remove('show');
if (!uname) {
errTxt.textContent = 'Sesi tidak ditemukan. Silakan login ulang.';
return err.classList.add('show');
}
if (newP.length < 6) {
errTxt.textContent = 'Password baru minimal 6 karakter.';
return err.classList.add('show');
}
if (newP !== conP) {
errTxt.textContent = 'Konfirmasi password tidak cocok.';
return err.classList.add('show');
}
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Menyimpan...';
try {
const res = await fetch('api/change_password.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ username: uname, old_password: oldP, new_password: newP })
});
const data = await res.json();
if (data.status === 'success') {
ok.classList.add('show');
document.getElementById('cp-form').reset();
document.getElementById('cp-strength-fill').style.width = '0';
document.getElementById('cp-strength-lbl').textContent = '';
btn.innerHTML = '<i class="fas fa-check"></i> Tersimpan!';
btn.style.background = 'linear-gradient(135deg,#059669,#10b981)';
setTimeout(() => {
closeChangePassword();
}, 1800);
} else {
errTxt.textContent = data.message || 'Gagal mengubah password.';
err.classList.add('show');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-floppy-disk"></i> Simpan Password Baru';
btn.style.background = '';
}
} catch {
errTxt.textContent = 'Tidak dapat terhubung ke server.';
err.classList.add('show');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-floppy-disk"></i> Simpan Password Baru';
btn.style.background = '';
}
});
// Close modal on backdrop click
document.getElementById('cp-modal').addEventListener('click', function(e) {
if (e.target === this) closeChangePassword();
});
</script>
<!-- ΓöÇΓöÇ CHANGE PASSWORD MODAL ΓöÇΓöÇ -->
<div id="cp-modal" class="hidden">
<div class="cp-box">
<div class="cp-head">
<div class="cp-head-left">
<div class="cp-head-icon"><i class="fas fa-key"></i></div>
<div>
<h3>Ganti Sandi</h3>
<p>Perbarui password akun Anda</p>
</div>
</div>
<button class="cp-close" onclick="closeChangePassword()"><i class="fas fa-times"></i></button>
</div>
<div class="cp-body">
<div class="cp-alert err" id="cp-err"><i class="fas fa-circle-exclamation"></i><span id="cp-err-text">Error</span></div>
<div class="cp-alert ok" id="cp-ok"><i class="fas fa-circle-check"></i><span>Password berhasil diubah!</span></div>
<form id="cp-form" novalidate>
<div class="cp-field">
<label>Password Lama</label>
<div class="cp-input-wrap">
<i class="fas fa-lock cp-ico"></i>
<input type="password" id="cp-old" placeholder="Masukkan password lama" required>
<button type="button" class="cp-eye" onclick="cpToggleEye('cp-old','cp-eye0')"><i class="fas fa-eye" id="cp-eye0"></i></button>
</div>
</div>
<div class="cp-field">
<label>Password Baru <span style="color:#94a3b8;font-size:10px;text-transform:none;letter-spacing:0;font-weight:500;">(min. 6 karakter)</span></label>
<div class="cp-input-wrap">
<i class="fas fa-lock cp-ico"></i>
<input type="password" id="cp-new" placeholder="Password baru" required oninput="cpCheckStrength(this.value)">
<button type="button" class="cp-eye" onclick="cpToggleEye('cp-new','cp-eye1')"><i class="fas fa-eye" id="cp-eye1"></i></button>
</div>
<div class="cp-strength"><div class="cp-strength-fill" id="cp-strength-fill"></div></div>
<div class="cp-strength-lbl" id="cp-strength-lbl"></div>
</div>
<div class="cp-field">
<label>Konfirmasi Password Baru</label>
<div class="cp-input-wrap">
<i class="fas fa-lock cp-ico"></i>
<input type="password" id="cp-con" placeholder="Ulangi password baru" required>
<button type="button" class="cp-eye" onclick="cpToggleEye('cp-con','cp-eye2')"><i class="fas fa-eye" id="cp-eye2"></i></button>
</div>
</div>
<button type="submit" class="cp-submit" id="cp-submit-btn">
<i class="fas fa-floppy-disk"></i> Simpan Password Baru
</button>
</form>
<p class="cp-footer"><i class="fas fa-lock"></i> Perubahan berlaku segera setelah disimpan</p>
</div>
</div>
</div>
</body>
</html>