c274bb6c3a
# Conflicts: # 01/index.html
7331 lines
234 KiB
HTML
7331 lines
234 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 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>
|
||
|
||
<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 — 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 & 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>
|
||
</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> Masuk ke Sistem
|
||
</a>
|
||
<p class="lp-note"><i class="fas fa-circle-check"></i> Statistik & 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, kemiskinan: [], masjid: [],
|
||
featureRefs: { kemiskinan: {}, masjid: {} },
|
||
kkLayerRefs: {}, blankRefs: {}, useSatellite: false, geometryEdit: null, activeCard: 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: '© 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 © Esri', maxZoom: 19
|
||
});
|
||
const layers = {
|
||
kemiskinan: L.layerGroup().addTo(map),
|
||
masjid: L.layerGroup().addTo(map),
|
||
blankspot: L.layerGroup().addTo(map)
|
||
};
|
||
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,
|
||
}, { 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 < 3) 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]]);
|
||
|
||
}
|
||
|
||
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},'${done ? 'Belum Ditangani' : 'Sudah Ditangani'}')">
|
||
${done ? 'Batal' : 'Selesai'}
|
||
</button>
|
||
<button class="pbtn" style="background:#ede9fe;color:#7c3aed" title="Data Warga" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, ''')}')"><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, ''')}')"><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, ''')}')"><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, ''')}')"><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);
|
||
|
||
// 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'].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',
|
||
};
|
||
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);
|
||
});
|
||
map.on('dblclick', function (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]
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
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(); 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 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 cancelDrawing() {
|
||
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 () {
|
||
|
||
// Kemiskinan butuh data Masjid untuk hitung Blank Spot (merah/hijau)
|
||
// Jadi pastikan loadMasjid selesai dulu baru loadKemiskinan
|
||
loadMasjid().then(() => {
|
||
loadKemiskinan();
|
||
});
|
||
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/* ── 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, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
function escapeText(value) {
|
||
return String(value || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||
}
|
||
|
||
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 & 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},'${done ? 'Belum Ditangani' : 'Sudah Ditangani'}')">
|
||
${done ? 'Batal' : 'Selesai'}
|
||
</button>
|
||
<button class="pbtn" style="background:#ede9fe;color:#7c3aed" title="Data Warga" onclick="openWargaModal(${d.id},'${d.nama_kk.replace(/'/g, ''')}')"><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, ''')}')"><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, ''')}')"><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, ''')}')"><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, '"')}" 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, '<')}</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, '<')}</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 & 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} • 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, '"')}">
|
||
</div>
|
||
<div class="km-field">
|
||
<label>Penanggung Jawab <span class="req">*</span></label>
|
||
<input id="e-m-pic" value="${(d.pic_masjid || '').replace(/"/g, '"')}">
|
||
</div>
|
||
|
||
<div class="km-section" style="color:#0f6250;border-color:#ccfbf1;">ALAMAT & JANGKAUAN</div>
|
||
<div class="km-field">
|
||
<label>Alamat Lengkap <span class="req">*</span></label>
|
||
<textarea id="e-m-alamat">${(d.alamat_masjid || '').replace(/</g, '<')}</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, '"')}" data-hub="${d.hubungan_keluarga || ''}" data-tgl="${d.tanggal_lahir || ''}" data-pend="${(d.pendidikan || '').replace(/"/g, '"')}" data-peny="${(d.riwayat_penyakit || '').replace(/"/g, '"')}" 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 + ' • ' : ''}${d.tanggal_lahir ? 'Lahir: ' + d.tanggal_lahir + ' • ' : ''}${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 & 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> 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> 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> 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> 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> |