7483 lines
540 KiB
HTML
7483 lines
540 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SiPetaSos ΓÇö Sistem Informasi Peta Sosial</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@600;700&display=swap"
|
|
rel="stylesheet">
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
:root {
|
|
--bg: #f8fafc;
|
|
--panel: #ffffff;
|
|
--panel2: #f1f5f9;
|
|
--border: #e2e8f0;
|
|
--navy: #0f172a;
|
|
--text: #1e293b;
|
|
--muted: #64748b;
|
|
--muted2: #94a3b8;
|
|
--green: #10b981;
|
|
--green-light: #d1fae5;
|
|
--red: #ef4444;
|
|
--red-light: #fee2e2;
|
|
--amber: #f59e0b;
|
|
--blue: #3b82f6;
|
|
--blue-light: #dbeafe;
|
|
--teal: #14b8a6;
|
|
--teal-light: #ccfbf1;
|
|
--purple: #8b5cf6;
|
|
--purple-light: #ede9fe;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
display: flex;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
@keyframes riseIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(12px)
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0)
|
|
}
|
|
}
|
|
|
|
@keyframes pulseGlow {
|
|
0% {
|
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
|
}
|
|
|
|
70% {
|
|
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
|
}
|
|
|
|
100% {
|
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
|
}
|
|
}
|
|
|
|
@keyframes popupIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(10px) scale(0.95);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0) scale(1);
|
|
}
|
|
}
|
|
|
|
/* ΓöÇΓöÇ SIDEBAR ΓöÇΓöÇ */
|
|
#sidebar {
|
|
width: 360px;
|
|
min-width: 360px;
|
|
background: #ffffff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid var(--border);
|
|
box-shadow: 0 0 30px rgba(0, 0, 0, 0.05);
|
|
z-index: 1000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.brand {
|
|
padding: 18px 20px 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.brand-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.brand-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
background: var(--blue);
|
|
border-radius: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-size: 16px;
|
|
animation: pulseGlow 2.5s ease-out infinite;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.brand-title {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--navy);
|
|
letter-spacing: -0.3px;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.brand-sub {
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
font-weight: 500;
|
|
display: block;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.sec {
|
|
padding: 16px 16px 10px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sec-lbl {
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 10px;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.mode-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.mbtn {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 14px 8px;
|
|
background: #fff;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
color: var(--navy);
|
|
transition: all 0.2s;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.mbtn:hover {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
color: var(--blue);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.mbtn i {
|
|
font-size: 18px;
|
|
color: var(--muted);
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.mbtn:hover i {
|
|
color: var(--blue);
|
|
}
|
|
|
|
.mbtn.active {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
}
|
|
|
|
.mbtn.active i {
|
|
color: var(--blue);
|
|
}
|
|
|
|
.list-wrap {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 12px;
|
|
}
|
|
|
|
.list-wrap::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.list-wrap::-webkit-scrollbar-thumb {
|
|
background: #e2e8f0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.leg-sec {
|
|
padding: 0 20px 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.leg-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 8px;
|
|
}
|
|
|
|
.leg-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.leg-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MAIN CONTAINER ΓöÇΓöÇ */
|
|
#main-container {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
background: var(--bg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ TOPBAR ΓöÇΓöÇ */
|
|
.app-topbar {
|
|
height: 64px;
|
|
min-height: 64px;
|
|
background: #ffffff;
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 14px;
|
|
padding: 0 20px;
|
|
z-index: 900;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.public-brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 200px;
|
|
}
|
|
|
|
.brand-icon-mini {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 9px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--blue);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.public-brand-text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.public-brand-title {
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 15px;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.public-brand-sub {
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.public-only {
|
|
display: none !important;
|
|
}
|
|
|
|
body.public-mode .public-only {
|
|
display: flex !important;
|
|
}
|
|
|
|
body.public-mode .admin-only {
|
|
display: none !important;
|
|
}
|
|
|
|
.map-search {
|
|
flex: 1;
|
|
max-width: 380px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.map-search input {
|
|
width: 100%;
|
|
height: 38px;
|
|
border: 1.5px solid var(--border);
|
|
border-right: none;
|
|
border-radius: 10px 0 0 10px;
|
|
padding: 0 14px;
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.map-search input:focus {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.map-search button {
|
|
height: 38px;
|
|
padding: 0 14px;
|
|
border: 1.5px solid var(--border);
|
|
border-left: none;
|
|
border-radius: 0 10px 10px 0;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
color: var(--muted);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.map-search button:hover {
|
|
background: var(--blue-light);
|
|
color: var(--blue);
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.top-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.top-btn {
|
|
height: 38px;
|
|
padding: 0 14px;
|
|
border: 1.5px solid var(--border);
|
|
background: #fff;
|
|
color: var(--navy);
|
|
border-radius: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.top-btn:hover {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
color: var(--blue);
|
|
}
|
|
|
|
.top-btn.primary {
|
|
background: var(--blue);
|
|
border-color: var(--blue);
|
|
color: #fff;
|
|
}
|
|
|
|
.top-btn.primary:hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
.top-btn.teal {
|
|
background: linear-gradient(135deg, #0f766e, #14b8a6);
|
|
border-color: transparent;
|
|
color: #fff;
|
|
}
|
|
|
|
.top-btn.blue {
|
|
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
|
|
border-color: transparent;
|
|
color: #fff;
|
|
}
|
|
|
|
.icon-btn {
|
|
width: 38px;
|
|
height: 38px;
|
|
border: 1.5px solid var(--border);
|
|
background: #fff;
|
|
color: var(--navy);
|
|
border-radius: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
position: relative;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.icon-btn:hover {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
color: var(--blue);
|
|
}
|
|
|
|
.ib-badge {
|
|
position: absolute;
|
|
top: -6px;
|
|
right: -6px;
|
|
min-width: 16px;
|
|
height: 16px;
|
|
border-radius: 999px;
|
|
padding: 0 4px;
|
|
background: var(--red);
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.tb-divider {
|
|
width: 1px;
|
|
height: 24px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.user-avatar-btn {
|
|
height: 38px;
|
|
padding: 0 12px;
|
|
border: 1.5px solid var(--border);
|
|
background: #fff;
|
|
color: var(--navy);
|
|
border-radius: 10px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.user-avatar-btn:hover {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 8px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
|
|
color: #fff;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.user-caret {
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.add-menu,
|
|
.user-menu {
|
|
position: relative;
|
|
}
|
|
|
|
.add-dropdown,
|
|
.user-dropdown {
|
|
position: absolute;
|
|
top: calc(100% + 8px);
|
|
right: 0;
|
|
min-width: 220px;
|
|
background: #fff;
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
box-shadow: 0 18px 50px rgba(15, 23, 42, .14);
|
|
padding: 8px;
|
|
display: none;
|
|
z-index: 1500;
|
|
animation: riseIn 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.add-menu.open .add-dropdown,
|
|
.user-menu.open .user-dropdown {
|
|
display: block;
|
|
}
|
|
|
|
.add-dropdown-head {
|
|
padding: 8px 10px;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.add-item,
|
|
.ud-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px;
|
|
border-radius: 9px;
|
|
color: var(--navy);
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.add-item:hover,
|
|
.ud-item:hover {
|
|
background: var(--panel2);
|
|
}
|
|
|
|
.add-item small {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-weight: 500;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.ai-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 9px;
|
|
display: grid;
|
|
place-items: center;
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.ud-header {
|
|
padding: 10px;
|
|
}
|
|
|
|
.ud-name {
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
font-size: 14px;
|
|
}
|
|
|
|
.ud-role {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.ud-sep {
|
|
height: 1px;
|
|
background: var(--border);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.ud-item.danger {
|
|
color: #b91c1c;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ PUBLIC BANNER ΓöÇΓöÇ */
|
|
#public-banner {
|
|
display: none;
|
|
align-items: center;
|
|
gap: 10px;
|
|
background: linear-gradient(90deg, #1e3a8a, #1d4ed8);
|
|
color: #fff;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
padding: 10px 20px;
|
|
z-index: 910;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#public-banner.show {
|
|
display: flex;
|
|
}
|
|
|
|
#public-banner a {
|
|
color: #93c5fd;
|
|
font-weight: 800;
|
|
}
|
|
|
|
body.public-mode #sidebar {
|
|
display: none;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MAP WRAPPER ΓöÇΓöÇ */
|
|
#map-wrapper {
|
|
flex: 1;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
#map {
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ STATS STRIP ΓöÇΓöÇ */
|
|
.stats-strip {
|
|
position: absolute;
|
|
top: 14px;
|
|
left: 14px;
|
|
display: flex;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
z-index: 500;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.stat-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
background: rgba(255, 255, 255, .94);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 8px 12px;
|
|
box-shadow: 0 4px 18px rgba(15, 23, 42, .08);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.stat-pill i {
|
|
width: 30px;
|
|
height: 30px;
|
|
border-radius: 8px;
|
|
display: grid;
|
|
place-items: center;
|
|
color: #fff;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.stat-pill strong {
|
|
display: block;
|
|
color: var(--navy);
|
|
font-size: 15px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.stat-pill small {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.map-chip {
|
|
position: absolute;
|
|
left: 14px;
|
|
bottom: 16px;
|
|
z-index: 500;
|
|
border: 1px solid var(--border);
|
|
background: #fff;
|
|
color: var(--navy);
|
|
border-radius: 999px;
|
|
padding: 8px 14px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 700;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
box-shadow: 0 4px 18px rgba(15, 23, 42, .1);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.map-chip:hover {
|
|
box-shadow: 0 8px 24px rgba(15, 23, 42, .15);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* ΓöÇΓöÇ SIDE PANEL ΓöÇΓöÇ */
|
|
.side-panel,
|
|
.locked-panel {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 14px;
|
|
bottom: 14px;
|
|
width: min(360px, calc(100% - 28px));
|
|
background: #fff;
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
box-shadow: 0 18px 50px rgba(15, 23, 42, .14);
|
|
z-index: 700;
|
|
display: none;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.side-panel.show,
|
|
.side-panel.open {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.locked-panel.show {
|
|
display: block;
|
|
}
|
|
|
|
.panel-head {
|
|
padding: 16px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.panel-head h3 {
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
color: var(--navy);
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.panel-head p {
|
|
margin: 4px 0 0;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.close-panel-btn {
|
|
border: 0;
|
|
background: var(--panel2);
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
color: var(--navy);
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.close-panel-btn:hover {
|
|
background: var(--red-light);
|
|
color: var(--red);
|
|
}
|
|
|
|
.panel-list {
|
|
padding: 12px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.panel-list::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.panel-list::-webkit-scrollbar-thumb {
|
|
background: #e2e8f0;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ LOCKED PANEL ΓöÇΓöÇ */
|
|
.lp-header {
|
|
padding: 24px 20px;
|
|
text-align: center;
|
|
background: linear-gradient(140deg, #1e3a8a 0%, #2563eb 60%, #3b82f6 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
.lp-icon-wrap {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto 12px;
|
|
border-radius: 14px;
|
|
display: grid;
|
|
place-items: center;
|
|
background: rgba(255, 255, 255, .18);
|
|
font-size: 20px;
|
|
}
|
|
|
|
.lp-divider {
|
|
height: 1px;
|
|
background: var(--border);
|
|
}
|
|
|
|
.lp-title {
|
|
font-size: 20px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.lp-header-sub {
|
|
font-size: 12px;
|
|
opacity: .82;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.lp-body {
|
|
padding: 18px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.lp-desc {
|
|
color: var(--muted);
|
|
line-height: 1.55;
|
|
margin: 0 0 14px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.lp-features {
|
|
padding: 0;
|
|
margin: 0 0 16px;
|
|
list-style: none;
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.lp-features li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-weight: 700;
|
|
color: var(--navy);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.feat-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 9px;
|
|
display: grid;
|
|
place-items: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.lp-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
border-radius: 12px;
|
|
background: var(--navy);
|
|
color: #fff;
|
|
text-decoration: none;
|
|
padding: 12px 16px;
|
|
font-weight: 800;
|
|
font-size: 14px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.lp-btn:hover {
|
|
background: #1e293b;
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.lp-note {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
text-align: center;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.lp-note i {
|
|
color: var(--green);
|
|
}
|
|
|
|
|
|
/* ΓöÇΓöÇ KK CARD (SIDE PANEL) ΓöÇΓöÇ */
|
|
.kk-card {
|
|
background: #fff;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 14px;
|
|
margin-bottom: 10px;
|
|
transition: all 0.2s;
|
|
animation: riseIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.kk-card:hover {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 6px 20px rgba(59, 130, 246, .12);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.kk-top {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.kk-avatar {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, #f1f5f9, #e2e8f0);
|
|
color: var(--muted);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.kk-name {
|
|
font-weight: 800;
|
|
font-size: 14px;
|
|
color: var(--navy);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.kk-sub {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.kk-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 12px;
|
|
margin-bottom: 8px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px dashed var(--border);
|
|
}
|
|
|
|
.kk-row:last-of-type {
|
|
border-bottom: none;
|
|
margin-bottom: 0;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.kk-row span {
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.kk-row strong {
|
|
color: var(--navy);
|
|
font-weight: 700;
|
|
text-align: right;
|
|
}
|
|
|
|
.kk-badge {
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
padding: 3px 8px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.badge-extreme {
|
|
background: var(--red-light);
|
|
color: #991b1b;
|
|
}
|
|
|
|
.badge-poor {
|
|
background: #ffedd5;
|
|
color: #9a3412;
|
|
}
|
|
|
|
.badge-vulnerable {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.badge-handled {
|
|
background: var(--green-light);
|
|
color: #065f46;
|
|
}
|
|
|
|
.badge-pending {
|
|
background: #f1f5f9;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.blank-warning {
|
|
color: #dc2626 !important;
|
|
font-weight: 800 !important;
|
|
background: var(--red-light);
|
|
padding: 4px 8px;
|
|
border-radius: 6px;
|
|
display: inline-block;
|
|
margin-top: 6px;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.kk-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.kk-actions select {
|
|
flex: 1;
|
|
padding: 6px 10px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 8px;
|
|
font: 600 11px 'Inter', sans-serif;
|
|
color: var(--navy);
|
|
outline: none;
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.kk-actions select:focus {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.mini-btn {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
color: var(--muted);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.mini-btn:hover {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
color: var(--blue);
|
|
}
|
|
|
|
/* ΓöÇΓöÇ CARD ΓöÇΓöÇ */
|
|
.card {
|
|
background: #fff;
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 12px 14px;
|
|
margin-bottom: 8px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
animation: riseIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.card:hover {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 4px 16px rgba(59, 130, 246, .1);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.card.active {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 4px 16px rgba(59, 130, 246, .15);
|
|
}
|
|
|
|
.card-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card-name {
|
|
font-weight: 800;
|
|
font-size: 13px;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.card-sub {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.card-badge {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
padding: 3px 8px;
|
|
border-radius: 6px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ LEAFLET OVERRIDES ΓöÇΓöÇ */
|
|
.leaflet-control-zoom a {
|
|
background: #fff !important;
|
|
color: var(--navy) !important;
|
|
border: 1px solid var(--border) !important;
|
|
font-weight: 700 !important;
|
|
}
|
|
|
|
.leaflet-bar {
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, .08);
|
|
border: none !important;
|
|
border-radius: 10px !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.leaflet-control-attribution {
|
|
font-size: 10px !important;
|
|
background: rgba(255, 255, 255, .8) !important;
|
|
border-top-left-radius: 6px;
|
|
}
|
|
|
|
.leaflet-popup {
|
|
animation: popupIn 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.leaflet-control-layers-base {
|
|
display: none !important;
|
|
}
|
|
|
|
.leaflet-control-layers-separator {
|
|
display: none !important;
|
|
}
|
|
|
|
.leaflet-control-layers {
|
|
border: 1px solid var(--border) !important;
|
|
border-radius: 14px !important;
|
|
box-shadow: 0 8px 30px rgba(0, 0, 0, .08) !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.leaflet-control-layers-expanded {
|
|
padding: 16px !important;
|
|
background: rgba(255, 255, 255, .97) !important;
|
|
}
|
|
|
|
.leaflet-control-layers-overlays {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.leaflet-control-layers label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
color: var(--navy);
|
|
font-weight: 700;
|
|
margin: 0;
|
|
padding: 8px 10px;
|
|
background: #f8fafc;
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.leaflet-control-layers label:hover {
|
|
background: var(--blue-light);
|
|
}
|
|
|
|
.leaflet-control-layers-overlays input[type=checkbox] {
|
|
width: 16px;
|
|
height: 16px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.lc-group-title {
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
margin: 0 0 6px;
|
|
padding: 0 2px;
|
|
}
|
|
|
|
.lc-item {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.lc-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.lc-dot-kemiskinan {
|
|
background: var(--amber);
|
|
}
|
|
|
|
.lc-dot-masjid {
|
|
background: var(--teal);
|
|
}
|
|
|
|
.lc-text strong {
|
|
display: block;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.lc-text small {
|
|
display: block;
|
|
color: var(--muted);
|
|
font-size: 10px;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ HINT ΓöÇΓöÇ */
|
|
#hint {
|
|
position: fixed;
|
|
bottom: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(15, 23, 42, .9);
|
|
color: #fff;
|
|
padding: 10px 18px;
|
|
border-radius: 999px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
z-index: 2000;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.3s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#hint.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
|
|
/* ΓöÇΓöÇ MODERN MARKER ΓöÇΓöÇ */
|
|
.modern-marker {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
|
|
.modern-marker .pulse {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--marker-color);
|
|
border-radius: 50%;
|
|
opacity: 0.4;
|
|
animation: markerPulse 2s infinite ease-out;
|
|
}
|
|
|
|
.modern-marker .pin-body {
|
|
position: relative;
|
|
width: 32px;
|
|
height: 32px;
|
|
background: var(--marker-color);
|
|
border-radius: 50% 50% 50% 0;
|
|
transform: rotate(-45deg);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
|
z-index: 2;
|
|
border: 2px solid #fff;
|
|
}
|
|
|
|
.modern-marker .icon-container {
|
|
transform: rotate(45deg);
|
|
color: #fff;
|
|
font-size: 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.modern-marker .pin-label {
|
|
position: absolute;
|
|
top: -24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(255, 255, 255, 0.95);
|
|
color: var(--navy);
|
|
padding: 3px 8px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
pointer-events: none;
|
|
opacity: 0;
|
|
transition: opacity 0.2s, top 0.2s;
|
|
border: 1px solid var(--border);
|
|
z-index: 3;
|
|
}
|
|
|
|
.modern-marker:hover .pin-label {
|
|
opacity: 1;
|
|
top: -30px;
|
|
}
|
|
|
|
.path-glow { filter: drop-shadow(0px 2px 4px rgba(0,0,0,0.35)); }
|
|
.parsil-glow { filter: drop-shadow(0px 2px 6px rgba(0,0,0,0.25)); }
|
|
|
|
@keyframes markerPulse {
|
|
0% {
|
|
transform: scale(0.8);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
100% {
|
|
transform: scale(1.6);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* ΓöÇΓöÇ POPUP ΓöÇΓöÇ */
|
|
.km-popup {
|
|
min-width: 260px;
|
|
font: 13px 'Inter', sans-serif;
|
|
}
|
|
|
|
.km-title {
|
|
font-size: 15px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
margin-bottom: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.km-grid {
|
|
display: grid;
|
|
gap: 6px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.km-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.km-label {
|
|
width: 110px;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.km-val {
|
|
flex: 1;
|
|
font-weight: 600;
|
|
color: var(--navy);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.km-field {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.km-field label {
|
|
display: block;
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.km-field select {
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 8px;
|
|
font: 600 12px 'Inter', sans-serif;
|
|
outline: none;
|
|
}
|
|
|
|
.km-field select:focus {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.km-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.pbtn {
|
|
flex: 1;
|
|
min-width: 60px;
|
|
padding: 8px 8px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font: 700 11px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.pbtn-g {
|
|
background: var(--green-light);
|
|
color: #065f46;
|
|
}
|
|
|
|
.pbtn-g:hover {
|
|
background: var(--green);
|
|
color: #fff;
|
|
}
|
|
|
|
.pbtn-r {
|
|
background: var(--red-light);
|
|
color: #991b1b;
|
|
}
|
|
|
|
.pbtn-r:hover {
|
|
background: var(--red);
|
|
color: #fff;
|
|
}
|
|
|
|
.pbtn-b {
|
|
background: var(--blue-light);
|
|
color: #1e40af;
|
|
}
|
|
|
|
.pbtn-b:hover {
|
|
background: var(--blue);
|
|
color: #fff;
|
|
}
|
|
|
|
.pbtn-y {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.pbtn-y:hover {
|
|
background: var(--amber);
|
|
color: #fff;
|
|
}
|
|
|
|
|
|
.kk-popup-actions {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 5px;
|
|
padding: 10px 12px 12px;
|
|
}
|
|
|
|
.kk-popup-actions .pbtn {
|
|
flex: unset;
|
|
min-width: unset;
|
|
padding: 7px 4px;
|
|
font-size: 11px;
|
|
border-radius: 7px;
|
|
}
|
|
|
|
.kk-popup-actions .pbtn:last-child:nth-child(3n+1) {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ LEAFLET POPUP WRAPPER ΓöÇΓöÇ */
|
|
.leaflet-popup-content-wrapper {
|
|
padding: 0 !important;
|
|
border-radius: 14px !important;
|
|
overflow: hidden !important;
|
|
box-shadow: 0 20px 60px rgba(15,23,42,.18) !important;
|
|
border: 1px solid var(--border) !important;
|
|
}
|
|
.leaflet-popup-content {
|
|
margin: 0 !important;
|
|
width: auto !important;
|
|
min-width: 260px !important;
|
|
}
|
|
.leaflet-popup-tip-container { margin-top: -1px; }
|
|
|
|
/* Sembunyikan X default Leaflet ΓÇö semua popup pakai custom close */
|
|
.leaflet-popup-close-button {
|
|
display: none !important;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ POPUP INFO (.pi) ΓöÇΓöÇ */
|
|
.pi {
|
|
font-family: 'Inter', sans-serif;
|
|
min-width: 260px;
|
|
max-width: 310px;
|
|
}
|
|
|
|
.pi-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 14px 12px;
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
.pi-header-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 10px;
|
|
background: rgba(255,255,255,.18);
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 15px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pi-header-info { flex: 1; min-width: 0; }
|
|
|
|
.pi-header-name {
|
|
font-weight: 800;
|
|
font-size: 14px;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.pi-header-sub {
|
|
font-size: 11px;
|
|
opacity: .8;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.pi-close {
|
|
width: 26px;
|
|
height: 26px;
|
|
border: none;
|
|
border-radius: 7px;
|
|
background: rgba(255,255,255,.18);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 12px;
|
|
transition: background .15s;
|
|
flex-shrink: 0;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.pi-close:hover { background: rgba(255,255,255,.32); }
|
|
|
|
.pi-close-alt {
|
|
width: 26px;
|
|
height: 26px;
|
|
border: none;
|
|
border-radius: 7px;
|
|
background: #f1f5f9;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 12px;
|
|
transition: background .15s;
|
|
flex-shrink: 0;
|
|
margin-left: auto;
|
|
}
|
|
.pi-close-alt:hover { background: #e2e8f0; color: var(--red); }
|
|
|
|
.pi-body {
|
|
padding: 10px 14px 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.pi-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
gap: 8px;
|
|
padding: 4px 0;
|
|
border-bottom: 1px dashed #f1f5f9;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.pi-row:last-child { border-bottom: none; }
|
|
|
|
.pi-k {
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.pi-v {
|
|
font-weight: 700;
|
|
color: var(--navy);
|
|
text-align: right;
|
|
word-break: break-word;
|
|
}
|
|
|
|
.pi-badge {
|
|
display: inline-block;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
padding: 2px 8px;
|
|
border-radius: 999px;
|
|
letter-spacing: .2px;
|
|
}
|
|
|
|
.pi-badge.extreme { background:#fee2e2; color:#991b1b; }
|
|
.pi-badge.poor { background:#ffedd5; color:#9a3412; }
|
|
.pi-badge.vulnerable { background:#fef3c7; color:#92400e; }
|
|
|
|
/* ΓöÇΓöÇ ACTION ROW ΓöÇΓöÇ */
|
|
.act-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 6px;
|
|
padding: 10px 12px 12px;
|
|
border-top: 1px solid #f1f5f9;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MODAL FORM (.kk-modal) ΓöÇΓöÇ */
|
|
.kk-modal {
|
|
font-family: 'Inter', sans-serif;
|
|
min-width: 290px;
|
|
max-width: 360px;
|
|
}
|
|
|
|
.km-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 14px 14px 12px;
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #2563eb 100%);
|
|
color: #fff;
|
|
}
|
|
|
|
.km-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 9px;
|
|
background: rgba(255,255,255,.2);
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.km-title {
|
|
font-weight: 800;
|
|
font-size: 14px;
|
|
flex: 1;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.km-close {
|
|
width: 26px;
|
|
height: 26px;
|
|
border: none;
|
|
border-radius: 7px;
|
|
background: rgba(255,255,255,.18);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 12px;
|
|
transition: background .15s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.km-close:hover { background: rgba(255,255,255,.3); }
|
|
|
|
.km-body {
|
|
padding: 14px;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.km-body::-webkit-scrollbar { width: 4px; }
|
|
.km-body::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 4px; }
|
|
|
|
.km-section {
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: var(--blue);
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin: 12px 0 8px;
|
|
padding-bottom: 6px;
|
|
border-bottom: 1.5px solid #dbeafe;
|
|
}
|
|
|
|
.km-section:first-child { margin-top: 0; }
|
|
|
|
.km-field {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.km-field label {
|
|
display: block;
|
|
font-weight: 700;
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
margin-bottom: 4px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .4px;
|
|
}
|
|
|
|
.km-field input,
|
|
.km-field textarea,
|
|
.km-field select {
|
|
width: 100%;
|
|
padding: 8px 11px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 9px;
|
|
font: 600 12px 'Inter', sans-serif;
|
|
color: var(--navy);
|
|
outline: none;
|
|
background: #fff;
|
|
transition: border-color .2s, box-shadow .2s;
|
|
}
|
|
|
|
.km-field textarea {
|
|
resize: vertical;
|
|
min-height: 56px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.km-field input:focus,
|
|
.km-field textarea:focus,
|
|
.km-field select:focus {
|
|
border-color: var(--blue);
|
|
box-shadow: 0 0 0 3px rgba(59,130,246,.12);
|
|
}
|
|
|
|
.km-field input::placeholder,
|
|
.km-field textarea::placeholder { color: #94a3b8; font-weight: 500; }
|
|
|
|
.km-gps-box {
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin-bottom: 10px;
|
|
background: linear-gradient(135deg, #1e3a8a, #2563eb);
|
|
}
|
|
|
|
.km-gps-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.km-gps-row span { color: rgba(255,255,255,.7); font-size: 11px; font-weight: 600; }
|
|
.km-gps-row strong { color: #fff; font-size: 13px; font-weight: 700; }
|
|
.km-gps-row i { color: rgba(255,255,255,.8); width: 16px; text-align: center; }
|
|
|
|
.km-btn {
|
|
padding: 9px 16px;
|
|
border: none;
|
|
border-radius: 9px;
|
|
font: 700 12px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
transition: all .18s;
|
|
}
|
|
|
|
.km-btn-save {
|
|
background: var(--blue);
|
|
color: #fff;
|
|
flex: 1;
|
|
}
|
|
|
|
.km-btn-save:hover { background: #1d4ed8; transform: translateY(-1px); }
|
|
|
|
.km-btn-cancel {
|
|
background: var(--panel2);
|
|
color: var(--navy);
|
|
}
|
|
|
|
.km-btn-cancel:hover { background: #e2e8f0; }
|
|
|
|
.km-foot {
|
|
display: flex;
|
|
gap: 8px;
|
|
padding: 10px 14px 14px;
|
|
border-top: 1px solid var(--border);
|
|
}
|
|
|
|
.req { color: var(--red); margin-left: 2px; }
|
|
|
|
.km-hint {
|
|
color: var(--muted);
|
|
font-weight: 500;
|
|
font-size: 10px;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.hint-txt {
|
|
font-size: 10px;
|
|
color: var(--muted);
|
|
margin-top: 6px;
|
|
font-style: italic;
|
|
}
|
|
|
|
.km-vuln-bar {
|
|
background: #f1f5f9;
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.km-vuln-score {
|
|
font-size: 22px;
|
|
font-weight: 900;
|
|
color: var(--navy);
|
|
min-width: 36px;
|
|
text-align: center;
|
|
}
|
|
|
|
.km-vuln-label {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.km-vuln-cat {
|
|
font-weight: 800;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.km-checkbox-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 0;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.km-checkbox-row input[type=checkbox] {
|
|
width: 15px;
|
|
height: 15px;
|
|
cursor: pointer;
|
|
accent-color: var(--blue);
|
|
}
|
|
|
|
.km-checkbox-row span {
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--navy);
|
|
}
|
|
|
|
/* ΓöÇΓöÇ FORM EXTRAS ΓöÇΓöÇ */
|
|
.km-checks {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 4px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.km-check {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
padding: 6px 8px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: var(--navy);
|
|
transition: all .15s;
|
|
user-select: none;
|
|
background: #fff;
|
|
}
|
|
|
|
.km-check:hover { border-color: var(--blue); background: #eff6ff; }
|
|
|
|
.km-check input[type=checkbox] {
|
|
width: 14px;
|
|
height: 14px;
|
|
accent-color: var(--blue);
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.km-gps-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.km-gps-item span { color: rgba(255,255,255,.7); font-size: 10px; font-weight: 600; }
|
|
.km-gps-item strong { color: #fff; font-size: 12px; font-weight: 700; font-family: monospace; }
|
|
.km-gps-item i { color: rgba(255,255,255,.8); }
|
|
|
|
/* ΓöÇΓöÇ VULNERABILITY BOX ΓöÇΓöÇ */
|
|
.km-vuln-box {
|
|
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 10px;
|
|
padding: 10px 14px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin: 6px 0 10px;
|
|
}
|
|
|
|
.kvb-left { display: flex; flex-direction: column; gap: 3px; }
|
|
|
|
.kvb-label {
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: .5px;
|
|
}
|
|
|
|
.kvb-cat {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.kvb-score {
|
|
font-size: 28px;
|
|
font-weight: 900;
|
|
color: var(--blue);
|
|
line-height: 1;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ BLANK SPOT POPUP ΓöÇΓöÇ */
|
|
.pi h4 {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 14px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
padding: 14px 14px 8px;
|
|
margin: 0;
|
|
border-bottom: 1px solid #f1f5f9;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MASJID FORM GPS MAP ΓöÇΓöÇ */
|
|
#m-map, #e-m-map, #k-map, #e-k-map {
|
|
height: 110px;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
background: #e2e8f0;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ GPS BOX INNER TEXT ΓöÇΓöÇ */
|
|
.km-addr-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 7px;
|
|
padding: 8px 12px 10px;
|
|
border-top: 1px solid rgba(255,255,255,.12);
|
|
}
|
|
|
|
.km-addr-row span,
|
|
.km-addr-row * {
|
|
color: #ffffff !important;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.km-gps-lbl {
|
|
font-size: 10px !important;
|
|
font-weight: 700 !important;
|
|
color: rgba(255,255,255,.65) !important;
|
|
letter-spacing: .8px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.km-gps-val {
|
|
font-size: 13px !important;
|
|
font-weight: 800 !important;
|
|
color: #ffffff !important;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
|
|
.km-gps-row .km-gps-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1px;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ DRAWING CONTROLS ΓöÇΓöÇ */
|
|
.coord-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.coord-row input {
|
|
flex: 1;
|
|
}
|
|
|
|
.lm-modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, .45);
|
|
z-index: 9000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.lm-box {
|
|
background: #fff;
|
|
border-radius: 18px;
|
|
width: 460px;
|
|
max-width: 94vw;
|
|
max-height: 88vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 24px 60px rgba(0, 0, 0, .18);
|
|
overflow: hidden;
|
|
animation: riseIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
.lm-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 18px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.lm-head-icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 12px;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 16px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.lm-head-title {
|
|
font-size: 16px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
flex: 1;
|
|
}
|
|
|
|
.lm-head-close {
|
|
width: 30px;
|
|
height: 30px;
|
|
border: 0;
|
|
background: var(--panel2);
|
|
border-radius: 9px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.lm-head-close:hover {
|
|
background: var(--red-light);
|
|
color: var(--red);
|
|
}
|
|
|
|
.lm-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 18px 20px;
|
|
}
|
|
|
|
.lm-field {
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.lm-field label {
|
|
display: block;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
color: var(--navy);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.lm-field input,
|
|
.lm-field select,
|
|
.lm-field textarea {
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 10px;
|
|
font: 500 13px 'Inter', sans-serif;
|
|
color: var(--navy);
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
background: var(--bg);
|
|
}
|
|
|
|
.lm-field input:focus,
|
|
.lm-field select:focus,
|
|
.lm-field textarea:focus {
|
|
border-color: var(--blue);
|
|
background: #fff;
|
|
}
|
|
|
|
.lm-field textarea {
|
|
min-height: 80px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.lm-submit {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
|
|
color: #fff;
|
|
font: 700 14px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.lm-submit:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px rgba(59, 130, 246, .3);
|
|
}
|
|
|
|
.lm-report-card {
|
|
border: 1px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 12px 14px;
|
|
margin-bottom: 10px;
|
|
background: #fff;
|
|
}
|
|
|
|
.lm-rc-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.lm-rc-name {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.lm-rc-time {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.lm-rc-desc {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
line-height: 1.5;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.lm-rc-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.lm-status {
|
|
padding: 3px 10px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.lm-status.menunggu {
|
|
background: #fef3c7;
|
|
color: #d97706;
|
|
}
|
|
|
|
.lm-status.diproses {
|
|
background: var(--blue-light);
|
|
color: #1d4ed8;
|
|
}
|
|
|
|
.lm-status.selesai {
|
|
background: var(--green-light);
|
|
color: #065f46;
|
|
}
|
|
|
|
.lm-rc-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
}
|
|
|
|
.lm-rc-btn {
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
font: 700 11px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.lm-rc-btn.selesai-btn {
|
|
background: var(--green-light);
|
|
color: #065f46;
|
|
}
|
|
|
|
.lm-rc-btn.selesai-btn:hover {
|
|
background: var(--green);
|
|
color: #fff;
|
|
}
|
|
|
|
.lm-rc-btn.hapus-btn {
|
|
background: var(--red-light);
|
|
color: #991b1b;
|
|
}
|
|
|
|
.lm-rc-btn.hapus-btn:hover {
|
|
background: var(--red);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ HISTORI BANTUAN MODAL ΓöÇΓöÇ */
|
|
.hb-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(15, 23, 42, .55);
|
|
z-index: 9000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: riseIn .2s ease;
|
|
}
|
|
|
|
.hb-box {
|
|
background: #fff;
|
|
border-radius: 18px;
|
|
width: 460px;
|
|
max-width: 95vw;
|
|
max-height: 85vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 24px 60px rgba(0, 0, 0, .18);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.hb-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: linear-gradient(135deg, #1e3a8a, #2563eb);
|
|
color: #fff;
|
|
}
|
|
|
|
.hb-head-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 12px;
|
|
background: rgba(255, 255, 255, .18);
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.hb-head-title {
|
|
font-size: 15px;
|
|
font-weight: 800;
|
|
flex: 1;
|
|
}
|
|
|
|
.hb-head-close {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: rgba(255, 255, 255, .18);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.hb-head-close:hover {
|
|
background: rgba(255, 255, 255, .32);
|
|
}
|
|
|
|
.hb-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px 20px;
|
|
}
|
|
|
|
.hb-body::-webkit-scrollbar {
|
|
width: 4px;
|
|
}
|
|
|
|
.hb-body::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.hb-form {
|
|
background: var(--bg);
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 14px;
|
|
padding: 14px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.hb-form-title {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: .8px;
|
|
color: var(--blue);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.hb-form input,
|
|
.hb-form select,
|
|
.hb-form textarea {
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 8px;
|
|
font: 500 12px 'Inter', sans-serif;
|
|
color: var(--navy);
|
|
background: #fff;
|
|
outline: none;
|
|
}
|
|
|
|
.hb-form input:focus,
|
|
.hb-form select:focus,
|
|
.hb-form textarea:focus {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.hb-submit {
|
|
width: 100%;
|
|
padding: 9px;
|
|
border: none;
|
|
border-radius: 9px;
|
|
background: var(--blue);
|
|
color: #fff;
|
|
font: 700 12px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.hb-submit:hover {
|
|
background: #1d4ed8;
|
|
}
|
|
|
|
.hb-list-title {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: .8px;
|
|
color: var(--muted);
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.hb-item {
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 12px 14px;
|
|
margin-bottom: 8px;
|
|
background: #fff;
|
|
}
|
|
|
|
.hb-item-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.hb-item-jenis {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.hb-item-tgl {
|
|
font-size: 11px;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.hb-item-desc {
|
|
font-size: 12px;
|
|
color: var(--muted);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.hb-item-del {
|
|
border: none;
|
|
background: var(--red-light);
|
|
color: #991b1b;
|
|
border-radius: 6px;
|
|
padding: 4px 9px;
|
|
font: 700 11px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
.hb-item-del:hover {
|
|
background: var(--red);
|
|
color: #fff;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ WALIKOTA BADGE ΓöÇΓöÇ */
|
|
.badge-walikota {
|
|
background: linear-gradient(135deg, #7c3aed, #a78bfa);
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
padding: 3px 8px;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
|
|
/* ΓöÇΓöÇ LAPORAN MASYARAKAT ΓöÇΓöÇ */
|
|
.lm-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(15, 23, 42, .6);
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: riseIn .25s ease;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
.lm-box {
|
|
background: #fff;
|
|
border-radius: 18px;
|
|
width: 500px;
|
|
max-width: 95vw;
|
|
max-height: 90vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 24px 60px rgba(0, 0, 0, .2);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.lm-head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 20px 24px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: #fff;
|
|
}
|
|
|
|
.lm-head h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
}
|
|
|
|
.lm-head h3 i {
|
|
color: var(--blue);
|
|
}
|
|
|
|
.lm-head p {
|
|
margin: 4px 0 0;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
width: calc(100% - 40px);
|
|
}
|
|
|
|
.lm-close-btn {
|
|
position: absolute;
|
|
top: 20px;
|
|
right: 20px;
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
background: var(--panel2);
|
|
color: var(--muted);
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
transition: 0.2s;
|
|
}
|
|
|
|
.lm-close-btn:hover {
|
|
background: var(--red-light);
|
|
color: var(--red);
|
|
}
|
|
|
|
.lm-body {
|
|
padding: 0;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.lm-tabs {
|
|
display: flex;
|
|
padding: 0 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: #f8fafc;
|
|
}
|
|
|
|
.lm-tab {
|
|
flex: 1;
|
|
padding: 14px;
|
|
text-align: center;
|
|
border: none;
|
|
background: transparent;
|
|
font: 600 13px 'Inter', sans-serif;
|
|
color: var(--muted);
|
|
cursor: pointer;
|
|
border-bottom: 2px solid transparent;
|
|
transition: 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.lm-tab:hover {
|
|
color: var(--navy);
|
|
}
|
|
|
|
.lm-tab.active {
|
|
color: var(--blue);
|
|
border-bottom-color: var(--blue);
|
|
background: #fff;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.lm-section {
|
|
display: none;
|
|
padding: 24px;
|
|
}
|
|
|
|
.lm-section.active {
|
|
display: block;
|
|
}
|
|
|
|
.lm-field {
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.lm-field label {
|
|
display: block;
|
|
font-weight: 700;
|
|
font-size: 12px;
|
|
color: var(--navy);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.lm-field input,
|
|
.lm-field textarea,
|
|
.lm-field select {
|
|
width: 100%;
|
|
padding: 10px 14px;
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 10px;
|
|
font: 500 13px 'Inter', sans-serif;
|
|
color: var(--navy);
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
background: #fff;
|
|
}
|
|
|
|
.lm-field input:focus,
|
|
.lm-field textarea:focus,
|
|
.lm-field select:focus {
|
|
border-color: var(--blue);
|
|
}
|
|
|
|
.lm-field textarea {
|
|
min-height: 100px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.lm-submit {
|
|
width: 100%;
|
|
padding: 14px;
|
|
border: none;
|
|
border-radius: 12px;
|
|
background: var(--blue);
|
|
color: #fff;
|
|
font: 700 14px 'Inter', sans-serif;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
transition: 0.2s;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.lm-submit:hover {
|
|
background: #2563eb;
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 6px 20px rgba(59, 130, 246, .2);
|
|
}
|
|
|
|
/* ΓöÇΓöÇ EXPORT MODAL ΓöÇΓöÇ */
|
|
.exp-modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, .5);
|
|
z-index: 9000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: riseIn .25s ease;
|
|
}
|
|
|
|
.exp-box {
|
|
background: #fff;
|
|
border-radius: 18px;
|
|
width: 420px;
|
|
max-width: 95vw;
|
|
box-shadow: 0 24px 60px rgba(0, 0, 0, .2);
|
|
overflow: hidden;
|
|
animation: riseIn .25s ease;
|
|
}
|
|
|
|
.exp-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid var(--border);
|
|
background: var(--navy);
|
|
color: #fff;
|
|
}
|
|
|
|
.exp-head-icon {
|
|
width: 34px;
|
|
height: 34px;
|
|
border-radius: 10px;
|
|
background: rgba(255, 255, 255, .15);
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 15px;
|
|
}
|
|
|
|
.exp-head-title {
|
|
font-size: 15px;
|
|
font-weight: 800;
|
|
flex: 1;
|
|
}
|
|
|
|
.exp-head-close {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: rgba(255, 255, 255, .15);
|
|
color: #fff;
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.exp-head-close:hover {
|
|
background: rgba(255, 255, 255, .28);
|
|
}
|
|
|
|
.exp-body {
|
|
padding: 20px;
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.exp-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
padding: 14px;
|
|
background: var(--bg);
|
|
border: 1.5px solid var(--border);
|
|
border-radius: 14px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.exp-row:hover {
|
|
border-color: var(--blue);
|
|
background: var(--blue-light);
|
|
}
|
|
|
|
.exp-row-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 12px;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 18px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.exp-row-text strong {
|
|
display: block;
|
|
font-size: 14px;
|
|
font-weight: 800;
|
|
color: var(--navy);
|
|
}
|
|
|
|
.exp-row-text small {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ BLANK SPOT ΓöÇΓöÇ */
|
|
.blank-spot-circle {
|
|
stroke: #64748b;
|
|
stroke-width: 1.5;
|
|
fill: rgba(100, 116, 139, 0.06);
|
|
stroke-dasharray: 6 4;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ RESPONSIVE ΓöÇΓöÇ */
|
|
@media (max-width: 1024px) {
|
|
#sidebar {
|
|
width: 280px;
|
|
min-width: 280px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 760px) {
|
|
body {
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
height: auto;
|
|
}
|
|
|
|
#sidebar {
|
|
width: 100%;
|
|
min-width: 0;
|
|
}
|
|
|
|
#main-container {
|
|
min-height: 75vh;
|
|
}
|
|
|
|
.stats-strip {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ΓöÇΓöÇ CHANGE PASSWORD MODAL ΓöÇΓöÇ */
|
|
#cp-modal {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(15,23,42,.55);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
animation: riseIn .2s ease;
|
|
}
|
|
#cp-modal.hidden { display: none; }
|
|
.cp-box {
|
|
background: #fff;
|
|
border-radius: 20px;
|
|
width: 420px;
|
|
max-width: 95vw;
|
|
box-shadow: 0 28px 80px rgba(15,23,42,.25);
|
|
overflow: hidden;
|
|
animation: riseIn .25s cubic-bezier(.16,1,.3,1);
|
|
}
|
|
.cp-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 18px 22px;
|
|
background: linear-gradient(135deg,#1e3a8a,#2563eb);
|
|
color: #fff;
|
|
}
|
|
.cp-head-left { display:flex; align-items:center; gap:10px; }
|
|
.cp-head-icon {
|
|
width: 34px; height: 34px; border-radius: 10px;
|
|
background: rgba(255,255,255,.15);
|
|
display: grid; place-items: center;
|
|
font-size: 15px;
|
|
}
|
|
.cp-head h3 { font-size:15px; font-weight:800; }
|
|
.cp-head p { font-size:11px; opacity:.7; margin-top:1px; }
|
|
.cp-close {
|
|
width:30px; height:30px; border-radius:8px;
|
|
background: rgba(255,255,255,.15);
|
|
border: none; color: #fff; cursor: pointer;
|
|
display: grid; place-items: center; font-size:14px;
|
|
transition: background .15s;
|
|
}
|
|
.cp-close:hover { background: rgba(255,255,255,.3); }
|
|
.cp-body { padding: 24px; }
|
|
.cp-field { margin-bottom: 16px; }
|
|
.cp-field label {
|
|
display:block; font-size:11px; font-weight:800;
|
|
text-transform:uppercase; letter-spacing:.5px;
|
|
color: #0f172a; margin-bottom:6px;
|
|
}
|
|
.cp-input-wrap { position: relative; }
|
|
.cp-input-wrap .cp-ico {
|
|
position:absolute; left:13px; top:50%; transform:translateY(-50%);
|
|
color: #94a3b8; font-size:13px; pointer-events:none;
|
|
}
|
|
.cp-input-wrap input {
|
|
width:100%; padding:12px 42px;
|
|
background:#f1f5f9; border:1.5px solid #e2e8f0;
|
|
border-radius:11px; font:500 14px 'Inter',sans-serif;
|
|
color:#1e293b; outline:none;
|
|
transition: border-color .2s, box-shadow .2s, background .2s;
|
|
}
|
|
.cp-input-wrap input::placeholder { color:#94a3b8; font-weight:400; }
|
|
.cp-input-wrap input:focus {
|
|
background:#fff; border-color:#3b82f6;
|
|
box-shadow:0 0 0 4px rgba(59,130,246,.1);
|
|
}
|
|
.cp-eye {
|
|
position:absolute; right:12px; top:50%; transform:translateY(-50%);
|
|
background:none; border:none; cursor:pointer;
|
|
color:#94a3b8; font-size:13px; padding:4px;
|
|
transition: color .2s;
|
|
}
|
|
.cp-eye:hover { color:#1e293b; }
|
|
.cp-strength { height:4px; border-radius:4px; background:#e2e8f0; margin-top:8px; overflow:hidden; }
|
|
.cp-strength-fill { height:100%; border-radius:4px; width:0; transition:width .3s, background .3s; }
|
|
.cp-strength-lbl { font-size:11px; color:#94a3b8; margin-top:4px; font-weight:600; }
|
|
.cp-alert {
|
|
display:none; align-items:center; gap:9px;
|
|
padding:12px 14px; border-radius:11px;
|
|
font-size:13px; font-weight:600; margin-bottom:16px;
|
|
}
|
|
.cp-alert.show { display:flex; }
|
|
.cp-alert.err { background:#fff1f2; border:1.5px solid #fecdd3; color:#be123c; }
|
|
.cp-alert.ok { background:#f0fdf4; border:1.5px solid #bbf7d0; color:#15803d; }
|
|
.cp-submit {
|
|
width:100%; padding:13px;
|
|
background: linear-gradient(135deg,#1d4ed8,#3b82f6);
|
|
border:none; border-radius:11px;
|
|
color:#fff; font:700 14px 'Inter',sans-serif;
|
|
cursor:pointer; display:flex; align-items:center;
|
|
justify-content:center; gap:8px;
|
|
box-shadow: 0 6px 18px rgba(37,99,235,.28);
|
|
transition: all .25s;
|
|
}
|
|
.cp-submit:hover { transform:translateY(-2px); box-shadow:0 12px 28px rgba(37,99,235,.38); }
|
|
.cp-submit:disabled { opacity:.6; transform:none; cursor:not-allowed; }
|
|
.cp-footer {
|
|
display:flex; align-items:center; justify-content:center;
|
|
gap:6px; margin-top:14px;
|
|
font-size:11px; color:#94a3b8;
|
|
}
|
|
.cp-footer i { color:#10b981; }
|
|
|
|
/* ΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉ
|
|
UI ENHANCEMENT ΓÇö SiPetaSos Premium Look
|
|
ΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉΓòÉ */
|
|
|
|
/* ΓöÇΓöÇ STAT PILLS ΓÇö Glassmorphism upgrade ΓöÇΓöÇ */
|
|
.stat-pill {
|
|
background: rgba(255,255,255,.96) !important;
|
|
border: none !important;
|
|
border-radius: 14px !important;
|
|
padding: 9px 14px !important;
|
|
box-shadow: 0 4px 24px rgba(15,23,42,.1), 0 1px 4px rgba(15,23,42,.06) !important;
|
|
gap: 10px !important;
|
|
transition: transform .2s, box-shadow .2s;
|
|
}
|
|
.stat-pill:hover { transform: translateY(-2px); box-shadow: 0 8px 32px rgba(15,23,42,.14) !important; }
|
|
.stat-pill i {
|
|
width: 34px !important; height: 34px !important;
|
|
border-radius: 10px !important;
|
|
font-size: 14px !important;
|
|
}
|
|
.stat-pill strong { font-size: 17px !important; font-weight: 800 !important; }
|
|
.stat-pill small { font-size: 10px !important; font-weight: 600 !important; color: #94a3b8 !important; letter-spacing:.3px; }
|
|
|
|
/* ΓöÇΓöÇ HINT BAR ΓÇö Modern pill ΓöÇΓöÇ */
|
|
#hint {
|
|
background: linear-gradient(135deg, rgba(15,23,42,.92), rgba(30,58,138,.88)) !important;
|
|
backdrop-filter: blur(8px) !important;
|
|
border: 1px solid rgba(255,255,255,.12) !important;
|
|
padding: 12px 22px !important;
|
|
border-radius: 999px !important;
|
|
font-size: 13px !important;
|
|
font-weight: 700 !important;
|
|
letter-spacing: .2px;
|
|
box-shadow: 0 8px 32px rgba(15,23,42,.25) !important;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ POPUP FORM .pf ΓÇö Complete redesign ΓöÇΓöÇ */
|
|
.pf {
|
|
font-family: 'Inter', sans-serif;
|
|
padding: 0;
|
|
min-width: 260px;
|
|
}
|
|
.pf h4 {
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
gap: 9px !important;
|
|
font-size: 14px !important;
|
|
font-weight: 800 !important;
|
|
color: #fff !important;
|
|
padding: 13px 16px !important;
|
|
margin: 0 !important;
|
|
border-bottom: none !important;
|
|
border-radius: 0;
|
|
}
|
|
.pf h4 i { font-size: 15px !important; opacity: .9; }
|
|
/* Color the h4 based on icon color */
|
|
.pf h4:has(.fa-gas-pump) { background: linear-gradient(135deg,#064e3b,#059669); }
|
|
.pf h4:has(.fa-road) { background: linear-gradient(135deg,#1e3a8a,#2563eb); }
|
|
.pf h4:has(.fa-vector-square){ background: linear-gradient(135deg,#78350f,#d97706); }
|
|
|
|
.pf label {
|
|
display: block;
|
|
font-size: 10px !important;
|
|
font-weight: 800 !important;
|
|
text-transform: uppercase;
|
|
letter-spacing: .6px;
|
|
color: #64748b !important;
|
|
margin: 12px 16px 5px !important;
|
|
}
|
|
.pf input, .pf select, .pf textarea {
|
|
display: block;
|
|
box-sizing: border-box !important;
|
|
width: auto !important;
|
|
margin: 0 16px;
|
|
padding: 9px 12px !important;
|
|
border: 1.5px solid #e2e8f0 !important;
|
|
border-radius: 9px !important;
|
|
font: 600 13px 'Inter',sans-serif !important;
|
|
color: #1e293b !important;
|
|
background: #f8fafc !important;
|
|
outline: none;
|
|
transition: border-color .2s, box-shadow .2s, background .2s;
|
|
}
|
|
.pf textarea {
|
|
resize: vertical;
|
|
min-height: 60px;
|
|
font-family: monospace !important;
|
|
font-size: 11px !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
.pf input:focus, .pf select:focus, .pf textarea:focus {
|
|
border-color: #3b82f6 !important;
|
|
background: #fff !important;
|
|
box-shadow: 0 0 0 3px rgba(59,130,246,.12) !important;
|
|
}
|
|
.pf select { cursor: pointer; }
|
|
|
|
.pf .coord-row {
|
|
display: flex; gap: 8px;
|
|
margin: 0 16px !important;
|
|
padding: 0 !important;
|
|
}
|
|
.pf .coord-row input {
|
|
flex: 1; margin: 0 !important;
|
|
width: auto !important;
|
|
background: #f1f5f9 !important;
|
|
color: #64748b !important;
|
|
font-family: monospace !important;
|
|
font-size: 12px !important;
|
|
text-align: center;
|
|
cursor: default;
|
|
}
|
|
|
|
.pf .hint-txt {
|
|
font-size: 11px !important;
|
|
color: #94a3b8 !important;
|
|
margin: 6px 16px 0 !important;
|
|
display: flex; align-items: center; gap: 5px;
|
|
}
|
|
.pf .hint-txt::before { content: 'ΓÜí'; font-size: 10px; }
|
|
|
|
.pf .pbtn {
|
|
display: block !important;
|
|
box-sizing: border-box !important;
|
|
width: auto !important;
|
|
margin: 12px 16px 14px !important;
|
|
padding: 10px !important;
|
|
border-radius: 10px !important;
|
|
font: 700 13px 'Inter',sans-serif !important;
|
|
border: none !important;
|
|
cursor: pointer !important;
|
|
text-align: center;
|
|
transition: all .2s !important;
|
|
}
|
|
.pf .pbtn-g {
|
|
background: linear-gradient(135deg,#059669,#10b981) !important;
|
|
color: #fff !important;
|
|
box-shadow: 0 4px 14px rgba(16,185,129,.3);
|
|
}
|
|
.pf .pbtn-b {
|
|
background: linear-gradient(135deg,#1d4ed8,#3b82f6) !important;
|
|
color: #fff !important;
|
|
box-shadow: 0 4px 14px rgba(37,99,235,.3);
|
|
}
|
|
.pf .pbtn-y {
|
|
background: linear-gradient(135deg,#d97706,#f59e0b) !important;
|
|
color: #fff !important;
|
|
box-shadow: 0 4px 14px rgba(217,119,6,.3);
|
|
}
|
|
.pf .pbtn:hover { transform: translateY(-2px) !important; filter: brightness(1.06); }
|
|
|
|
/* ΓöÇΓöÇ LEAFLET LAYER CONTROL ΓÇö OPSIONAL section ΓöÇΓöÇ */
|
|
.lc-group-title {
|
|
font-size: 10px !important;
|
|
font-weight: 900 !important;
|
|
letter-spacing: 1px !important;
|
|
text-transform: uppercase !important;
|
|
color: #1e293b !important;
|
|
padding: 10px 10px 4px !important;
|
|
border-top: 1px solid #f1f5f9;
|
|
margin-top: 4px !important;
|
|
}
|
|
.lc-group-title:first-child { border-top: none; margin-top: 0; }
|
|
|
|
/* ΓöÇΓöÇ TOPBAR search upgrade ΓöÇΓöÇ */
|
|
.map-search input {
|
|
border-radius: 10px 0 0 10px !important;
|
|
font-family: 'Inter', sans-serif !important;
|
|
font-size: 13px !important;
|
|
}
|
|
.map-search button {
|
|
border-radius: 0 10px 10px 0 !important;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ ADD DROPDOWN upgrade ΓöÇΓöÇ */
|
|
.add-dropdown { border-radius: 16px !important; }
|
|
.add-item {
|
|
border-radius: 10px !important;
|
|
margin: 2px 4px !important;
|
|
transition: background .15s, transform .15s !important;
|
|
}
|
|
.add-item:hover { transform: translateX(3px) !important; }
|
|
.ai-icon { border-radius: 10px !important; font-size: 15px !important; }
|
|
.add-item span:last-child { font-size: 13px !important; font-weight: 700 !important; }
|
|
.add-item small { color: #94a3b8 !important; font-weight: 500 !important; font-size: 11px !important; }
|
|
|
|
/* ΓöÇΓöÇ SIDEBAR brand upgrade ΓöÇΓöÇ */
|
|
.brand-title { font-size: 17px !important; }
|
|
.brand-sub {
|
|
font-size: 10px !important;
|
|
color: #94a3b8 !important;
|
|
margin-top: 2px !important;
|
|
display: block;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
/* ΓöÇΓöÇ USER DROPDOWN upgrade ΓöÇΓöÇ */
|
|
.user-dropdown { border-radius: 16px !important; min-width: 200px !important; }
|
|
.ud-item { border-radius: 10px !important; margin: 1px 4px !important; }
|
|
.ud-header { border-bottom: 1px solid #f1f5f9; padding-bottom: 10px !important; }
|
|
.ud-name { font-size: 13px !important; }
|
|
|
|
/* ΓöÇΓöÇ SCROLLBAR refinement ΓöÇΓöÇ */
|
|
::-webkit-scrollbar { width: 5px; height: 5px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 999px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #cbd5e1; }
|
|
|
|
/* ΓöÇΓöÇ FULL TABLE MODAL CSS ΓöÇΓöÇ */
|
|
.ftm-overlay {
|
|
position: fixed; inset: 0; z-index: 10000;
|
|
background: rgba(15, 23, 42, 0.75);
|
|
backdrop-filter: blur(8px);
|
|
display: flex; align-items: center; justify-content: center;
|
|
padding: 20px;
|
|
animation: fadeIn .25s ease;
|
|
}
|
|
.ftm-box {
|
|
background: var(--bg); border-radius: 16px;
|
|
width: 100%; max-width: 1200px; max-height: 90vh;
|
|
display: flex; flex-direction: column;
|
|
box-shadow: 0 25px 60px rgba(0,0,0,0.25);
|
|
overflow: hidden;
|
|
animation: riseIn .3s cubic-bezier(.16,1,.3,1);
|
|
}
|
|
.ftm-head {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 20px 24px; background: #fff; border-bottom: 1px solid var(--border);
|
|
}
|
|
.ftm-title { font-size: 20px; font-weight: 700; color: var(--navy); }
|
|
.ftm-sub { font-size: 13px; color: var(--muted); margin-top: 4px; }
|
|
.ftm-close {
|
|
width: 32px; height: 32px; border-radius: 8px; border: none;
|
|
background: var(--panel2); color: var(--muted); cursor: pointer;
|
|
display: flex; align-items: center; justify-content: center; font-size: 16px;
|
|
transition: .2s;
|
|
}
|
|
.ftm-close:hover { background: #fee2e2; color: #dc2626; }
|
|
.ftm-body {
|
|
flex: 1; overflow: auto; padding: 24px; background: var(--bg);
|
|
}
|
|
.ftm-filters {
|
|
display: flex; gap: 12px; margin-bottom: 16px; align-items: center;
|
|
}
|
|
.ftm-filter-select {
|
|
padding: 8px 12px; border: 1px solid var(--border); border-radius: 6px;
|
|
font-family: inherit; font-size: 13px; background: #fff; outline: none;
|
|
}
|
|
.ftm-table-card {
|
|
background: #fff; border-radius: 12px; border: 1px solid var(--border);
|
|
box-shadow: 0 4px 16px rgba(0,0,0,0.03); overflow: auto;
|
|
}
|
|
.ftm-table-card table { width: 100%; border-collapse: collapse; }
|
|
.ftm-table-card th {
|
|
text-align: left; padding: 14px 16px; font-size: 12px; font-weight: 700;
|
|
color: var(--muted); text-transform: uppercase; border-bottom: 1px solid var(--border);
|
|
background: var(--panel-soft); position: sticky; top: 0; z-index: 10;
|
|
}
|
|
.ftm-table-card td { padding: 14px 16px; font-size: 13px; border-bottom: 1px solid var(--border); }
|
|
.ftm-table-card tr:last-child td { border-bottom: none; }
|
|
.ftm-table-card tr:hover td { background: var(--panel-soft); }
|
|
.ftm-badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; }
|
|
.ftm-btn-icon { background: none; border: none; cursor: pointer; color: var(--muted); font-size: 15px; transition: .2s; }
|
|
.ftm-btn-icon:hover { color: var(--blue); }
|
|
|
|
/* ΓöÇΓöÇ LOGIN MODAL ΓöÇΓöÇ */
|
|
.lm-overlay {
|
|
position: fixed; inset: 0; z-index: 12000;
|
|
background: rgba(15,23,42,.82); backdrop-filter: blur(10px);
|
|
display: flex; align-items: center; justify-content: center;
|
|
animation: fadeIn .2s ease;
|
|
}
|
|
.lm-box {
|
|
background: #fff; border-radius: 20px; width: 420px; max-width: 95vw;
|
|
box-shadow: 0 32px 80px rgba(0,0,0,.3);
|
|
overflow: hidden; animation: riseIn .3s cubic-bezier(.16,1,.3,1);
|
|
}
|
|
.lm-head {
|
|
background: linear-gradient(135deg,#1e3a8a,#2563eb);
|
|
padding: 28px 32px 24px; text-align: center; color: #fff;
|
|
}
|
|
.lm-head-icon { font-size: 36px; margin-bottom: 10px; }
|
|
.lm-head-title { font-size: 22px; font-weight: 800; }
|
|
.lm-head-sub { font-size: 13px; opacity: .8; margin-top: 4px; }
|
|
.lm-body { padding: 28px 32px; }
|
|
.lm-field { margin-bottom: 16px; }
|
|
.lm-field label { display: block; font-size: 12px; font-weight: 700; color: var(--muted); margin-bottom: 6px; letter-spacing: .5px; text-transform: uppercase; }
|
|
.lm-input-wrap { position: relative; }
|
|
.lm-input-wrap .lm-ico { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--muted); font-size: 14px; }
|
|
.lm-input-wrap input { width: 100%; padding: 12px 14px 12px 40px; border: 1.5px solid var(--border); border-radius: 10px; font: 500 14px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; box-sizing: border-box; }
|
|
.lm-input-wrap input:focus { border-color: var(--blue); box-shadow: 0 0 0 3px rgba(37,99,235,.12); }
|
|
.lm-eye { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); background: none; border: none; cursor: pointer; color: var(--muted); font-size: 14px; }
|
|
.lm-submit { width: 100%; padding: 13px; border: none; border-radius: 12px; background: linear-gradient(135deg,#1e3a8a,#2563eb); color: #fff; font: 700 14px 'Plus Jakarta Sans',sans-serif; cursor: pointer; box-shadow: 0 6px 20px rgba(37,99,235,.3); transition: .2s; margin-top: 4px; }
|
|
.lm-submit:hover { transform: translateY(-1px); box-shadow: 0 8px 28px rgba(37,99,235,.4); }
|
|
.lm-submit:disabled { opacity: .6; cursor: not-allowed; transform: none; }
|
|
.lm-alert { display: flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px; font-size: 13px; font-weight: 600; margin-bottom: 16px; }
|
|
.lm-alert.error { background: #fee2e2; color: #dc2626; }
|
|
.lm-close-btn { position: absolute; top: 14px; right: 14px; background: rgba(255,255,255,.15); border: none; border-radius: 8px; width: 30px; height: 30px; cursor: pointer; color: #fff; font-size: 14px; display: flex; align-items: center; justify-content: center; }
|
|
|
|
/* ΓöÇΓöÇ PROFIL MODAL ΓöÇΓöÇ */
|
|
.prf-overlay {
|
|
position: fixed; inset: 0; z-index: 12000;
|
|
background: rgba(15,23,42,.75); backdrop-filter: blur(8px);
|
|
display: flex; align-items: center; justify-content: center; padding: 20px;
|
|
animation: fadeIn .2s ease;
|
|
}
|
|
.prf-box {
|
|
background: #fff; border-radius: 20px; width: 480px; max-width: 95vw;
|
|
box-shadow: 0 28px 70px rgba(0,0,0,.25);
|
|
overflow: hidden; animation: riseIn .3s cubic-bezier(.16,1,.3,1);
|
|
}
|
|
.prf-head {
|
|
background: linear-gradient(135deg,#0f172a,#1e3a8a);
|
|
padding: 28px 28px 60px; text-align: center; position: relative;
|
|
}
|
|
.prf-avatar {
|
|
width: 80px; height: 80px; border-radius: 50%; margin: 0 auto 12px;
|
|
background: linear-gradient(135deg,#3b82f6,#6366f1);
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 32px; font-weight: 800; color: #fff;
|
|
border: 3px solid rgba(255,255,255,.3);
|
|
}
|
|
.prf-head-name { font-size: 18px; font-weight: 700; color: #fff; }
|
|
.prf-head-role { font-size: 12px; color: rgba(255,255,255,.7); margin-top: 4px; }
|
|
.prf-body { padding: 0 28px 28px; margin-top: -30px; }
|
|
.prf-card { background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; }
|
|
.prf-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid var(--border); }
|
|
.prf-row:last-child { border-bottom: none; padding-bottom: 0; }
|
|
.prf-label { font-size: 12px; color: var(--muted); font-weight: 600; text-transform: uppercase; }
|
|
.prf-value { font-size: 14px; font-weight: 600; color: var(--navy); }
|
|
.prf-edit-row { display: flex; gap: 8px; align-items: center; }
|
|
.prf-input { padding: 8px 12px; border: 1.5px solid var(--border); border-radius: 8px; font: 600 14px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; }
|
|
.prf-input:focus { border-color: var(--blue); }
|
|
.prf-save-btn { padding: 8px 14px; background: var(--blue); color: #fff; border: none; border-radius: 8px; font: 700 12px 'Plus Jakarta Sans',sans-serif; cursor: pointer; }
|
|
.prf-close { position: absolute; top: 14px; right: 14px; background: rgba(255,255,255,.15); border: none; border-radius: 8px; width: 30px; height: 30px; cursor: pointer; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 14px; }
|
|
|
|
/* ΓöÇΓöÇ KELOLA AKUN MODAL (Users) ΓöÇΓöÇ */
|
|
.ua-role-badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 700; }
|
|
.ua-role-admin { background: #dbeafe; color: #1d4ed8; }
|
|
.ua-role-operator { background: #f0fdf4; color: #15803d; }
|
|
.ua-form { background: #f8fafc; border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
|
|
.ua-form-title { font-size: 14px; font-weight: 700; color: var(--navy); margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
|
|
.ua-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
.ua-field label { display: block; font-size: 11px; font-weight: 700; color: var(--muted); margin-bottom: 5px; text-transform: uppercase; }
|
|
.ua-field input, .ua-field select { width: 100%; padding: 9px 12px; border: 1.5px solid var(--border); border-radius: 8px; font: 500 13px 'Plus Jakarta Sans',sans-serif; outline: none; transition: .2s; box-sizing: border-box; }
|
|
.ua-field input:focus, .ua-field select:focus { border-color: var(--blue); }
|
|
.ua-submit { padding: 10px 20px; background: var(--blue); color: #fff; border: none; border-radius: 8px; font: 700 13px 'Plus Jakarta Sans',sans-serif; cursor: pointer; margin-top: 14px; }
|
|
.ua-avatar { width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; font: 700 13px 'Space Grotesk',sans-serif; color: #fff; }
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="sidebar">
|
|
<div class="brand">
|
|
<div class="brand-row">
|
|
<div class="brand-icon"><i class="fas fa-map-location-dot"></i></div>
|
|
<div class="brand-title">SiPeta<span style="color:var(--blue)">Sos</span></div>
|
|
</div>
|
|
<div class="brand-sub">Sistem Informasi Peta Sosial & Rumah Ibadah</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="list-wrap">
|
|
<div style="padding:20px 20px 10px">
|
|
<a href="#" onclick="openDataWargaModal(); return false;"
|
|
style="display:flex;align-items:center;gap:10px;background:linear-gradient(135deg,#3b82f6,#6366f1);border-radius:14px;padding:20px 20px 10px;text-decoration:none;color:white;font-weight:600;font-size:14px;box-shadow: 0 8px 30px rgba(0,0,0,0.04);transition:0.2s">
|
|
<i class="fas fa-users" style="font-size:18px"></i>
|
|
<div>
|
|
<div>Lihat Data Warga</div>
|
|
<div style="font-weight:400;font-size:11px;opacity:0.85;margin-top:2px">Daftar & kelola anggota keluarga
|
|
</div>
|
|
</div>
|
|
<i class="fas fa-arrow-right" style="margin-left:auto;font-size:12px"></i>
|
|
</a>
|
|
</div>
|
|
<div style="padding:0 20px 20px">
|
|
<a href="#" onclick="openDataMasjidModal(); return false;"
|
|
style="display:flex;align-items:center;gap:10px;background:linear-gradient(135deg,#14b8a6,#0ea5e9);border-radius:14px;padding:20px 20px 10px;text-decoration:none;color:white;font-weight:600;font-size:14px;box-shadow: 0 8px 30px rgba(0,0,0,0.04);transition:0.2s">
|
|
<i class="fas fa-place-of-worship" style="font-size:18px"></i>
|
|
<div>
|
|
<div>Data Rumah Ibadah</div>
|
|
<div style="font-weight:400;font-size:11px;opacity:0.85;margin-top:2px">Kelola fasilitas ibadah</div>
|
|
</div>
|
|
<i class="fas fa-arrow-right" style="margin-left:auto;font-size:12px"></i>
|
|
</a>
|
|
</div>
|
|
<div id="list-masjid" style="display:none;"><span id="cnt-masjid">0</span></div>
|
|
<div id="list-kemiskinan" style="display:none;"><span id="cnt-kemiskinan">0</span></div>
|
|
<div id="list-spbu" style="display:none;"><span id="cnt-spbu">0</span></div>
|
|
<div id="list-jalan" style="display:none;"><span id="cnt-jalan">0</span></div>
|
|
<div id="list-parsil" style="display:none;"><span id="cnt-parsil">0</span></div>
|
|
</div>
|
|
|
|
<div class="leg-sec">
|
|
<div class="sec-lbl" style="margin-bottom:10px">Legenda Peta</div>
|
|
<div class="leg-grid">
|
|
<div class="leg-item">
|
|
<div class="leg-dot" style="background:var(--amber)"></div>Kemiskinan
|
|
</div>
|
|
<div class="leg-item">
|
|
<div class="leg-dot" style="background:var(--teal)"></div>Rumah Ibadah
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="main-container">
|
|
<!-- Public Mode Banner -->
|
|
<div id="public-banner">
|
|
<i class="fas fa-lock"></i>
|
|
<span>Mode Publik — 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 style="padding:6px 10px 2px;font-size:10px;font-weight:800;color:#94a3b8;text-transform:uppercase;letter-spacing:.6px">Opsional</div>
|
|
<div class="add-item" onclick="setMode('spbu');closeAddMenu()">
|
|
<span class="ai-icon" style="background:#dcfce7;color:#16a34a"><i class="fas fa-gas-pump"></i></span>
|
|
<span>SPBU <small>Tambah titik SPBU</small></span>
|
|
</div>
|
|
<div class="add-item" onclick="setMode('jalan');closeAddMenu()">
|
|
<span class="ai-icon" style="background:#dbeafe;color:#2563eb"><i class="fas fa-road"></i></span>
|
|
<span>Jalan <small>Gambar ruas jalan</small></span>
|
|
</div>
|
|
<div class="add-item" onclick="setMode('parsil');closeAddMenu()">
|
|
<span class="ai-icon" style="background:#fef3c7;color:#d97706"><i class="fas fa-vector-square"></i></span>
|
|
<span>Parsil / Lahan <small>Gambar area persil</small></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Icon buttons -->
|
|
<button class="icon-btn admin-only" onclick="focusBlankSpot()" title="Blank Spot">
|
|
<i class="fas fa-triangle-exclamation"></i>
|
|
<span class="ib-badge" id="top-blank-count">0</span>
|
|
</button>
|
|
<button class="icon-btn admin-only" onclick="downloadReport()" title="Unduh Laporan CSV">
|
|
<i class="fas fa-download"></i>
|
|
</button>
|
|
<button class="icon-btn" onclick="openLaporanModal()" title="Laporan Masyarakat" id="btn-laporan-top">
|
|
<i class="fas fa-bullhorn"></i>
|
|
<span class="ib-badge" id="laporan-badge" style="display:none">0</span>
|
|
</button>
|
|
|
|
|
|
<!-- Data Panel (primary CTA) -->
|
|
<button class="top-btn primary admin-only" onclick="togglePanel()">
|
|
<i class="fas fa-table-list"></i> Data Panel
|
|
</button>
|
|
|
|
<!-- Divider + User avatar dropdown -->
|
|
<div class="tb-divider admin-only"></div>
|
|
<div class="user-menu admin-only" id="user-menu">
|
|
<button class="user-avatar-btn" onclick="toggleUserMenu()">
|
|
<div class="user-avatar" id="topbar-avatar">AD</div>
|
|
<span id="topbar-uname">Admin</span>
|
|
<i class="fas fa-chevron-down user-caret"></i>
|
|
</button>
|
|
<div class="user-dropdown">
|
|
<div class="ud-header">
|
|
<div class="ud-name" id="dd-nama">Admin</div>
|
|
<div class="ud-role" id="dd-role">Administrator</div>
|
|
</div>
|
|
<div class="ud-sep"></div>
|
|
<a href="#" onclick="openProfilModal(); return false;" class="ud-item"><i class="fas fa-circle-user"></i> Profil Saya</a>
|
|
<div class="ud-item" onclick="openChangePassword()"><i class="fas fa-key"></i> Ganti Sandi</div>
|
|
<a href="#" onclick="openKelolaAkunModal(); return false;" class="ud-item" id="dd-users-link"><i class="fas fa-users-gear"></i> Kelola Akun</a>
|
|
<div class="ud-sep"></div>
|
|
<div class="ud-item danger" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Keluar</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Public mode buttons -->
|
|
<button class="top-btn public-only" onclick="showLockedPanel()"><i class="fas fa-list"></i> Panel</button>
|
|
<a href="#" onclick="openLoginModal(); return false;" class="top-btn primary public-only" id="topbar-login-btn"
|
|
style="text-decoration:none;display:none;">
|
|
<i class="fas fa-sign-in-alt"></i> Login
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="map-wrapper">
|
|
<div id="map"></div>
|
|
<div class="stats-strip" id="stats-strip"></div>
|
|
<button class="map-chip" onclick="toggleSatellite()"><i class="fas fa-globe"></i> <span id="sat-label">Mode
|
|
Satelit</span></button>
|
|
|
|
<aside class="side-panel" id="side-panel">
|
|
<div class="panel-head">
|
|
<h3><span>Data KK Miskin (<span id="panel-count">0</span>)</span> <button class="close-panel-btn"
|
|
onclick="togglePanel()"><i class="fas fa-times"></i></button></h3>
|
|
<p>Detail dan status penanganan KK miskin</p>
|
|
</div>
|
|
<div class="panel-list" id="panel-list"></div>
|
|
</aside>
|
|
|
|
<!-- Locked Panel (public mode) -->
|
|
<div class="locked-panel" id="locked-panel">
|
|
<!-- Gradient header -->
|
|
<div class="lp-header">
|
|
<div class="lp-icon-wrap"><i class="fas fa-shield-halved"></i></div>
|
|
<div class="lp-title">Area Terproteksi</div>
|
|
<div class="lp-header-sub">Login diperlukan untuk akses penuh</div>
|
|
</div>
|
|
<div class="lp-divider"></div>
|
|
<!-- Body -->
|
|
<div class="lp-body">
|
|
<p class="lp-desc">Daftar KK miskin dan detail informasi hanya dapat diakses oleh pengguna yang telah masuk ke
|
|
sistem.</p>
|
|
<ul class="lp-features">
|
|
<li>
|
|
<span class="feat-icon" style="background:#FFEDD5;color:#2563eb"><i class="fas fa-users"></i></span>
|
|
Daftar lengkap KK miskin
|
|
</li>
|
|
<li>
|
|
<span class="feat-icon" style="background:#d1fae5;color:#059669"><i class="fas fa-map-pin"></i></span>
|
|
Titik peta interaktif
|
|
</li>
|
|
<li>
|
|
<span class="feat-icon" style="background:#ede9fe;color:#7c3aed"><i class="fas fa-file-lines"></i></span>
|
|
Ekspor laporan data
|
|
</li>
|
|
<li>
|
|
<span class="feat-icon" style="background:#ffedd5;color:#ea580c"><i
|
|
class="fas fa-triangle-exclamation"></i></span>
|
|
Monitoring blank spot
|
|
</li>
|
|
</ul>
|
|
<a href="#" onclick="openLoginModal(); return false;" class="lp-btn">
|
|
<i class="fas fa-right-to-bracket"></i> 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, spbu: [], jalan: [], parsil: [], kemiskinan: [], masjid: [],
|
|
featureRefs: { spbu: {}, jalan: {}, parsil: {}, kemiskinan: {}, masjid: {} },
|
|
kkLayerRefs: {}, blankRefs: {}, useSatellite: false, geometryEdit: null, activeCard: null,
|
|
spbuFilterVis: { open24: true, non24: true }, drawingLine: false, drawingPoly: false,
|
|
tempPoints: [], tempMarkers: [], tempPolyline: null, tempPolygon: null
|
|
};
|
|
|
|
/* MAP */
|
|
const map = L.map('map', { zoomControl: false, preferCanvas: true }).setView([-0.0263, 109.3425], 14);
|
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
|
const streetLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© 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),
|
|
// Layer opsional ΓÇö OFF by default (tidak .addTo(map))
|
|
spbu24: L.layerGroup(),
|
|
spbuNon24: L.layerGroup(),
|
|
jalan: L.layerGroup(),
|
|
parsil: L.layerGroup()
|
|
};
|
|
const layersControl = L.control.layers(null, {
|
|
'<span class="lc-item"><span class="lc-dot lc-dot-kemiskinan"></span><span class="lc-text"><strong>Kemiskinan</strong><small>Data KK miskin</small></span></span>': layers.kemiskinan,
|
|
'<span class="lc-item"><span class="lc-dot lc-dot-masjid"></span><span class="lc-text"><strong>Rumah Ibadah</strong><small>Fasilitas ibadah</small></span></span>': layers.masjid,
|
|
'<span class="lc-item"><span class="lc-dot" style="background:#64748b"></span><span class="lc-text"><strong>Blank Spot</strong><small>Di luar radius rumah ibadah</small></span></span>': layers.blankspot,
|
|
'<span class="lc-item"><span class="lc-dot" style="background:#16a34a"></span><span class="lc-text"><strong>SPBU 24 Jam</strong><small>Buka 24 jam</small></span></span>': layers.spbu24,
|
|
'<span class="lc-item"><span class="lc-dot" style="background:#dc2626"></span><span class="lc-text"><strong>SPBU Non-24</strong><small>Tidak 24 jam</small></span></span>': layers.spbuNon24,
|
|
'<span class="lc-item"><span class="lc-dot" style="background:#2563eb"></span><span class="lc-text"><strong>Jalan</strong><small>Jaringan jalan</small></span></span>': layers.jalan,
|
|
'<span class="lc-item"><span class="lc-dot" style="background:#d97706"></span><span class="lc-text"><strong>Parsil / Lahan</strong><small>Data persil tanah</small></span></span>': layers.parsil,
|
|
}, { collapsed: false, position: 'topright' }).addTo(map);
|
|
decorateLayersControl();
|
|
function decorateLayersControl() {
|
|
const container = layersControl.getContainer();
|
|
if (!container) return;
|
|
const overlays = container.querySelector('.leaflet-control-layers-overlays');
|
|
if (!overlays) return;
|
|
const labels = Array.from(overlays.querySelectorAll('label'));
|
|
if (labels.length < 6) return;
|
|
overlays.innerHTML = '';
|
|
const addGroup = (title, items, color) => {
|
|
const head = document.createElement('div');
|
|
head.className = 'lc-group-title';
|
|
head.style.cursor = 'pointer';
|
|
head.style.display = 'flex';
|
|
head.style.justifyContent = 'space-between';
|
|
head.style.alignItems = 'center';
|
|
head.style.userSelect = 'none';
|
|
|
|
const titleSpan = document.createElement('span');
|
|
titleSpan.textContent = title;
|
|
if (color) titleSpan.style.color = color;
|
|
|
|
const iconSpan = document.createElement('i');
|
|
iconSpan.className = 'fa-solid fa-chevron-up';
|
|
iconSpan.style.transition = 'transform 0.3s ease';
|
|
iconSpan.style.fontSize = '12px';
|
|
if (color) iconSpan.style.color = color;
|
|
|
|
head.appendChild(titleSpan);
|
|
head.appendChild(iconSpan);
|
|
overlays.appendChild(head);
|
|
|
|
const content = document.createElement('div');
|
|
content.style.overflow = 'hidden';
|
|
content.style.transition = 'max-height 0.3s ease';
|
|
content.style.maxHeight = '1000px';
|
|
|
|
items.forEach(item => content.appendChild(item));
|
|
overlays.appendChild(content);
|
|
|
|
let isOpen = true;
|
|
head.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
isOpen = !isOpen;
|
|
if (isOpen) {
|
|
content.style.maxHeight = '1000px';
|
|
iconSpan.style.transform = 'rotate(0deg)';
|
|
} else {
|
|
content.style.maxHeight = '0px';
|
|
iconSpan.style.transform = 'rotate(180deg)';
|
|
}
|
|
});
|
|
};
|
|
addGroup('Tematik Utama', [labels[0], labels[1], labels[2]]);
|
|
addGroup('Opsional', [labels[3], labels[4], labels[5], labels[6]], '#64748b');
|
|
}
|
|
|
|
const statusStoreKey = 'gis-kemiskinan-status';
|
|
function getStatusMap() {
|
|
try { return JSON.parse(localStorage.getItem(statusStoreKey) || '{}'); }
|
|
catch (_) { return {}; }
|
|
}
|
|
|
|
function getKKStatus(id) {
|
|
// Cek dari state data terlebih dahulu (dari DB)
|
|
const d = state.kemiskinan.find(x => String(x.id) === String(id));
|
|
if (d && d.status_penanganan) return d.status_penanganan;
|
|
// Fallback ke localStorage
|
|
return getStatusMap()[String(id)] || 'Belum Ditangani';
|
|
}
|
|
|
|
function setKKStatus(id, value) {
|
|
// Update state
|
|
const d = state.kemiskinan.find(x => String(x.id) === String(id));
|
|
if (d) d.status_penanganan = value;
|
|
// Update localStorage
|
|
const mapStatus = getStatusMap();
|
|
mapStatus[String(id)] = value;
|
|
localStorage.setItem(statusStoreKey, JSON.stringify(mapStatus));
|
|
// Simpan ke database
|
|
fetch('api/update_status_kemiskinan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: parseInt(id), status: value })
|
|
}).catch(() => console.warn('Gagal simpan status ke DB'));
|
|
renderDashboard();
|
|
refreshType('kemiskinan');
|
|
}
|
|
|
|
// ΓöÇΓöÇ TOAST NOTIFICATION ΓöÇΓöÇ
|
|
function showToast(msg, type) {
|
|
let toast = document.getElementById('gis-toast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'gis-toast';
|
|
toast.style.cssText = 'position:fixed;bottom:28px;left:50%;transform:translateX(-50%);padding:10px 20px;border-radius:999px;font:700 13px Inter,sans-serif;z-index:99999;pointer-events:none;transition:opacity 0.3s;opacity:0;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.15)';
|
|
document.body.appendChild(toast);
|
|
}
|
|
const colors = {
|
|
success: { bg: '#059669', color: '#fff' },
|
|
info: { bg: '#2563eb', color: '#fff' },
|
|
warning: { bg: '#f59e0b', color: '#fff' }
|
|
};
|
|
const c = colors[type] || colors.success;
|
|
toast.style.background = c.bg;
|
|
toast.style.color = c.color;
|
|
toast.textContent = msg;
|
|
toast.style.opacity = '1';
|
|
clearTimeout(toast._t);
|
|
toast._t = setTimeout(() => { toast.style.opacity = '0'; }, 2200);
|
|
}
|
|
|
|
function setKKStatusQuick(id, value) {
|
|
// Update state
|
|
const d2 = state.kemiskinan.find(x => String(x.id) === String(id));
|
|
if (d2) d2.status_penanganan = value;
|
|
// Update localStorage
|
|
const mapStatus = getStatusMap();
|
|
mapStatus[String(id)] = value;
|
|
localStorage.setItem(statusStoreKey, JSON.stringify(mapStatus));
|
|
// Simpan ke database
|
|
fetch('api/update_status_kemiskinan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: parseInt(id), status: value })
|
|
}).then(r => r.json()).then(res => {
|
|
if (res.status !== 'success') {
|
|
console.warn('Gagal simpan status:', res.message);
|
|
showToast('⚠️ Gagal simpan ke database', 'warning');
|
|
} else {
|
|
const label = value === 'Diproses' ? '🔄 Status: Diproses' : value === 'Sudah Ditangani' ? '✅ Status: Sudah Ditangani' : '↩️ Status: Belum Ditangani';
|
|
showToast(label, value === 'Diproses' ? 'info' : value === 'Sudah Ditangani' ? 'success' : 'warning');
|
|
}
|
|
}).catch(() => {
|
|
console.warn('Gagal simpan status ke DB');
|
|
showToast('⚠️ Tidak terhubung ke database', 'warning');
|
|
});
|
|
renderDashboard();
|
|
|
|
const d = state.kemiskinan.find(x => String(x.id) === String(id));
|
|
if (!d) return;
|
|
const ref = state.featureRefs['kemiskinan'][String(id)];
|
|
if (!ref) return;
|
|
|
|
const status = value;
|
|
const done = status === 'Sudah Ditangani';
|
|
const blank = isBlankSpot(d);
|
|
const markerColor = blank ? '#dc2626' : '#16a34a';
|
|
const coverIcon = blank ? '<i class="fas fa-circle-xmark" style="color:#dc2626"></i>' : '<i class="fas fa-circle-check" style="color:#16a34a"></i>';
|
|
const nearest = nearestRumahIbadah(d);
|
|
const namaRI = nearest ? nearest.nama_masjid : '-';
|
|
const jarakRI = nearest ? Math.round(nearest.distance) + ' m' : '-';
|
|
const cat = getCategory(d);
|
|
const gisUser = JSON.parse(sessionStorage.getItem('gis_user') || '{}');
|
|
const role = gisUser.role || '';
|
|
const canManage = role !== 'walikota';
|
|
|
|
const actionsHtml = canManage ? `<div class="act-row kk-popup-actions">
|
|
<button class="pbtn pbtn-y" onclick="openEditKemiskinan(${d.id})">Edit</button>
|
|
<button class="pbtn pbtn-b" onclick="setKKStatusQuick(${d.id},'Diproses')">Proses</button>
|
|
<button class="pbtn ${done ? 'pbtn-b' : 'pbtn-g'}" onclick="setKKStatusQuick(${d.id},'${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);
|
|
|
|
// 3. Cari SPBU
|
|
const fSpbu = state.spbu.find(d => (d.nama || '').toLowerCase().includes(q));
|
|
if (fSpbu) return focusFeature('spbu', fSpbu.id);
|
|
|
|
// 4. Cari Jalan
|
|
const fJalan = state.jalan.find(d => (d.nama || '').toLowerCase().includes(q));
|
|
if (fJalan) return focusFeature('jalan', fJalan.id);
|
|
|
|
// 5. Cari Parsil
|
|
const fParsil = state.parsil.find(d => (`${d.nama} ${d.pemilik}`).toLowerCase().includes(q));
|
|
if (fParsil) return focusFeature('parsil', fParsil.id);
|
|
|
|
// Jika tidak ketemu
|
|
alert('Data "' + q + '" tidak ditemukan.');
|
|
}
|
|
|
|
function focusBlankSpot() {
|
|
const first = state.kemiskinan.find(isBlankSpot);
|
|
if (first) focusFeature('kemiskinan', first.id);
|
|
}
|
|
|
|
function togglePanel() {
|
|
const panel = document.getElementById('side-panel');
|
|
if (panel) {
|
|
if (panel.classList.contains('open')) {
|
|
panel.classList.remove('open');
|
|
setTimeout(() => panel.style.pointerEvents = 'none', 400);
|
|
} else {
|
|
panel.style.pointerEvents = 'auto';
|
|
panel.classList.add('open');
|
|
}
|
|
}
|
|
}
|
|
|
|
function toggleSatellite() {
|
|
state.useSatellite = !state.useSatellite;
|
|
if (state.useSatellite) {
|
|
map.removeLayer(streetLayer);
|
|
satelliteLayer.addTo(map);
|
|
} else {
|
|
map.removeLayer(satelliteLayer);
|
|
streetLayer.addTo(map);
|
|
}
|
|
const label = document.getElementById('sat-label');
|
|
if (label) label.textContent = state.useSatellite ? 'Jalan' : 'Satelit';
|
|
}
|
|
|
|
function downloadReport() {
|
|
const rows = [
|
|
['Nama KK', 'Jumlah Jiwa', 'Alamat', 'Kategori', 'Skor Kerentanan', 'Status', 'Rumah Ibadah Terdekat', 'Jarak Meter', 'Blank Spot']
|
|
];
|
|
state.kemiskinan.forEach(d => {
|
|
const nearest = nearestRumahIbadah(d);
|
|
rows.push([
|
|
d.nama_kk,
|
|
d.jumlah_kk,
|
|
d.alamat,
|
|
getCategory(d).label,
|
|
vulnerabilityScore(d),
|
|
getKKStatus(d.id),
|
|
nearest ? nearest.nama_masjid : '',
|
|
nearest ? Math.round(nearest.distance) : '',
|
|
isBlankSpot(d) ? 'Ya' : 'Tidak'
|
|
]);
|
|
});
|
|
const csv = rows.map(row => row.map(v => `"${String(v).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'laporan-kemiskinan-rumah-ibadah.csv';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
a.remove();
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
const jColor = { 'Nasional': '#ef4444', 'Provinsi': '#f59e0b', 'Kabupaten': '#3b82f6' };
|
|
const pColor = { 'SHM': '#3b82f6', 'HGB': '#8b5cf6', 'HGU': '#f59e0b', 'HP': '#10b981' };
|
|
const kecColorMap = {
|
|
'Pontianak Timur': '#ef6b73',
|
|
'Pontianak Selatan': '#f0c86a',
|
|
'Pontianak Utara': '#e8c45d',
|
|
'Pontianak Barat': '#8f6cc6',
|
|
'Pontianak Kota': '#83be86',
|
|
'Pontianak Tenggara': '#eec8d8'
|
|
};
|
|
|
|
const utm49s = '+proj=utm +zone=49 +south +datum=WGS84 +units=m +no_defs';
|
|
const wgs84 = 'WGS84';
|
|
|
|
function utm49sToLngLat(easting, northing) {
|
|
// Fallback konversi UTM 49S -> WGS84 ketika proj4 tidak tersedia.
|
|
const a = 6378137.0;
|
|
const f = 1 / 298.257223563;
|
|
const e2 = f * (2 - f);
|
|
const ep2 = e2 / (1 - e2);
|
|
const k0 = 0.9996;
|
|
const falseEasting = 500000;
|
|
const falseNorthing = 10000000;
|
|
const zone = 49;
|
|
|
|
const x = Number(easting) - falseEasting;
|
|
const y = Number(northing) - falseNorthing;
|
|
|
|
const m = y / k0;
|
|
const mu = m / (a * (1 - e2 / 4 - (3 * e2 * e2) / 64 - (5 * e2 * e2 * e2) / 256));
|
|
|
|
const e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2));
|
|
|
|
const j1 = (3 * e1 / 2) - (27 * Math.pow(e1, 3) / 32);
|
|
const j2 = (21 * e1 * e1 / 16) - (55 * Math.pow(e1, 4) / 32);
|
|
const j3 = (151 * Math.pow(e1, 3) / 96);
|
|
const j4 = (1097 * Math.pow(e1, 4) / 512);
|
|
|
|
const fp = mu + j1 * Math.sin(2 * mu) + j2 * Math.sin(4 * mu) + j3 * Math.sin(6 * mu) + j4 * Math.sin(8 * mu);
|
|
|
|
const sinFp = Math.sin(fp);
|
|
const cosFp = Math.cos(fp);
|
|
const tanFp = Math.tan(fp);
|
|
|
|
const c1 = ep2 * cosFp * cosFp;
|
|
const t1 = tanFp * tanFp;
|
|
const n1 = a / Math.sqrt(1 - e2 * sinFp * sinFp);
|
|
const r1 = (a * (1 - e2)) / Math.pow(1 - e2 * sinFp * sinFp, 1.5);
|
|
const d = x / (n1 * k0);
|
|
|
|
const lat = fp - (n1 * tanFp / r1) * (
|
|
(d * d) / 2 -
|
|
((5 + 3 * t1 + 10 * c1 - 4 * c1 * c1 - 9 * ep2) * Math.pow(d, 4)) / 24 +
|
|
((61 + 90 * t1 + 298 * c1 + 45 * t1 * t1 - 252 * ep2 - 3 * c1 * c1) * Math.pow(d, 6)) / 720
|
|
);
|
|
|
|
const lon0 = ((zone - 1) * 6 - 180 + 3) * Math.PI / 180;
|
|
const lon = lon0 + (
|
|
d -
|
|
((1 + 2 * t1 + c1) * Math.pow(d, 3)) / 6 +
|
|
((5 - 2 * c1 + 28 * t1 - 3 * c1 * c1 + 8 * ep2 + 24 * t1 * t1) * Math.pow(d, 5)) / 120
|
|
) / cosFp;
|
|
|
|
return [lon * 180 / Math.PI, lat * 180 / Math.PI];
|
|
}
|
|
|
|
function isProjectedCoordinate(coord) {
|
|
if (!Array.isArray(coord) || coord.length < 2) return false;
|
|
return Math.abs(coord[0]) > 180 || Math.abs(coord[1]) > 90;
|
|
}
|
|
|
|
function convertProjectedPointToLngLat(coord) {
|
|
if (!Array.isArray(coord) || coord.length < 2) return coord;
|
|
|
|
if (isProjectedCoordinate(coord)) {
|
|
if (typeof proj4 === 'function') {
|
|
try {
|
|
const converted = proj4(utm49s, wgs84, [Number(coord[0]), Number(coord[1])]);
|
|
if (Array.isArray(converted) && Number.isFinite(converted[0]) && Number.isFinite(converted[1])) {
|
|
return [converted[0], converted[1]];
|
|
}
|
|
} catch (_) {
|
|
// fallback ke konversi manual
|
|
}
|
|
}
|
|
return utm49sToLngLat(coord[0], coord[1]);
|
|
}
|
|
|
|
return [Number(coord[0]), Number(coord[1])];
|
|
}
|
|
|
|
function reprojectGeoJSONCoordinates(coords) {
|
|
if (!Array.isArray(coords) || coords.length === 0) return coords;
|
|
if (typeof coords[0] === 'number') return convertProjectedPointToLngLat(coords);
|
|
return coords.map(reprojectGeoJSONCoordinates);
|
|
}
|
|
|
|
function normalizeAdminKecamatanGeoJSON(geojson) {
|
|
return {
|
|
...geojson,
|
|
features: (geojson.features || []).map(feature => ({
|
|
...feature,
|
|
geometry: {
|
|
...feature.geometry,
|
|
coordinates: reprojectGeoJSONCoordinates(feature.geometry.coordinates)
|
|
}
|
|
}))
|
|
};
|
|
}
|
|
|
|
function getKecamatanColor(name) {
|
|
return kecColorMap[name] || '#d9b26f';
|
|
}
|
|
|
|
function loadAdminKecamatan() {
|
|
fetch('Admin_Kecamatan.json')
|
|
.then(res => res.json())
|
|
.then(raw => {
|
|
const geojson = normalizeAdminKecamatanGeoJSON(raw);
|
|
const kecLayer = L.geoJSON(geojson, {
|
|
interactive: false,
|
|
style: feature => {
|
|
const name = (feature.properties && feature.properties.Ket) || 'Kecamatan';
|
|
const fill = getKecamatanColor(name);
|
|
return {
|
|
color: '#232831',
|
|
weight: 3,
|
|
fillColor: fill,
|
|
fillOpacity: 0.4
|
|
};
|
|
}
|
|
});
|
|
|
|
kecLayer.addTo(layers.kecamatan);
|
|
kecLayer.bringToBack();
|
|
|
|
const bounds = kecLayer.getBounds();
|
|
if (bounds && bounds.isValid()) {
|
|
map.fitBounds(bounds, { padding: [24, 24], maxZoom: 13 });
|
|
}
|
|
|
|
// log removed
|
|
})
|
|
.catch(err => {});
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MODE ΓöÇΓöÇ */
|
|
function setMode(m) {
|
|
cancelDrawing();
|
|
state.mode = state.mode === m ? null : m;
|
|
['kemiskinan', 'masjid', 'spbu', 'jalan', 'parsil'].forEach(x => {
|
|
const el = document.getElementById('btn-' + x);
|
|
if (el) el.className = 'mbtn';
|
|
});
|
|
const hint = document.getElementById('hint');
|
|
hint.classList.remove('show');
|
|
if (state.mode) {
|
|
const msgs = {
|
|
kemiskinan: 'Klik peta untuk menambah data Kemiskinan',
|
|
masjid: 'Klik peta untuk menambah titik Rumah Ibadah',
|
|
spbu: 'Klik peta untuk menambah titik SPBU',
|
|
jalan: 'Klik untuk gambar jalan ΓÇö klik ganda untuk selesai',
|
|
parsil: 'Klik untuk gambar parsil ΓÇö klik titik awal atau dbl-klik selesai',
|
|
line: 'Klik untuk gambar jalan ΓÇö klik ganda untuk selesai',
|
|
poly: 'Klik untuk gambar parsil ΓÇö klik titik awal atau dbl-klik selesai',
|
|
};
|
|
hint.textContent = msgs[m] || 'Klik peta';
|
|
hint.classList.add('show');
|
|
map.getContainer().style.cursor = 'crosshair';
|
|
} else {
|
|
map.getContainer().style.cursor = '';
|
|
}
|
|
}
|
|
|
|
/* ΓöÇΓöÇ MAP EVENTS ΓöÇΓöÇ */
|
|
map.on('click', function (e) {
|
|
if (window._kkPickingLocation) {
|
|
finishKemiskinanLocationPick(e);
|
|
return;
|
|
}
|
|
if (!state.mode) return;
|
|
if (state.mode === 'kemiskinan') handleKemiskinanClick(e);
|
|
else if (state.mode === 'masjid') handleMasjidClick(e);
|
|
else if (state.mode === 'spbu') handlePointClick(e);
|
|
else if (state.mode === 'jalan') handleLineClick(e);
|
|
else if (state.mode === 'parsil') handlePolyClick(e);
|
|
});
|
|
map.on('dblclick', function (e) {
|
|
if ((state.mode === 'jalan' || state.mode === 'line') && state.drawingLine) finishLine(e);
|
|
if ((state.mode === 'parsil' || state.mode === 'poly') && state.drawingPoly) finishPoly(e);
|
|
if (['jalan','parsil','line','poly'].includes(state.mode)) L.DomEvent.stopPropagation(e);
|
|
});
|
|
|
|
/* ── MODERN MARKER ICON ── */
|
|
function createModernMarker(color, iconClass, label) {
|
|
const html = `
|
|
<div class="modern-marker" style="--marker-color: ${color}">
|
|
<div class="pulse"></div>
|
|
<div class="pin-body">
|
|
<div class="icon-container">
|
|
<i class="${iconClass}"></i>
|
|
</div>
|
|
</div>
|
|
<div class="pin-label">${label}</div>
|
|
</div>
|
|
`;
|
|
return L.divIcon({
|
|
html: html,
|
|
className: '',
|
|
iconSize: [40, 40],
|
|
iconAnchor: [20, 38],
|
|
popupAnchor: [0, -42]
|
|
});
|
|
}
|
|
|
|
/* ΓöÇΓöÇ POINT SPBU ΓöÇΓöÇ */
|
|
window.updateTempMarkerIcon = function() {
|
|
if (!window._tmpM) return;
|
|
const s = document.getElementById('p-status') ? document.getElementById('p-status').value : 'Ya';
|
|
const color = s === 'Ya' ? '#10b981' : '#ef4444';
|
|
window._tmpM.setIcon(createModernMarker(color, 'fas fa-gas-pump', 'SPBU Baru'));
|
|
};
|
|
|
|
function handlePointClick(e) {
|
|
const icon = createModernMarker('#10b981', 'fas fa-gas-pump', 'SPBU Baru');
|
|
const tmpM = L.marker(e.latlng, { icon, draggable: true }).addTo(map);
|
|
const upd = ll => {
|
|
const a = document.getElementById('p-lat'), b = document.getElementById('p-lng');
|
|
if (a) a.value = ll.lat.toFixed(6);
|
|
if (b) b.value = ll.lng.toFixed(6);
|
|
};
|
|
tmpM.on('dragend', ev => upd(ev.target.getLatLng()));
|
|
const form = `<div class="pf">
|
|
<h4><i class="fas fa-gas-pump" style="color:var(--green)"></i> Tambah SPBU</h4>
|
|
<label>Nama SPBU</label>
|
|
<input id="p-nama" placeholder="Pertamina Ahmad Yani">
|
|
<label>Nomor SPBU</label>
|
|
<input id="p-no" placeholder="64.xxx.xx">
|
|
<label>Status Operasional</label>
|
|
<select id="p-status" onchange="window.updateTempMarkerIcon()">
|
|
<option value="Ya">Buka 24 Jam</option>
|
|
<option value="Tidak">Tidak Buka 24 Jam</option>
|
|
</select>
|
|
<div class="coord-row">
|
|
<input id="p-lat" value="${e.latlng.lat.toFixed(6)}" readonly>
|
|
<input id="p-lng" value="${e.latlng.lng.toFixed(6)}" readonly>
|
|
</div>
|
|
<div class="hint-txt">* Geser marker untuk menyesuaikan posisi</div>
|
|
<button class="pbtn pbtn-g" onclick="savePoint()">Simpan SPBU</button>
|
|
</div>`;
|
|
tmpM.bindPopup(form, { maxWidth: 280, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).openPopup();
|
|
tmpM.on('popupclose', () => { if (map.hasLayer(tmpM)) map.removeLayer(tmpM); });
|
|
window._tmpM = tmpM;
|
|
}
|
|
|
|
function savePoint() {
|
|
|
|
const nama = document.getElementById('p-nama').value.trim();
|
|
const no = document.getElementById('p-no').value.trim();
|
|
const status = document.getElementById('p-status').value;
|
|
const lat = parseFloat(document.getElementById('p-lat').value);
|
|
const lng = parseFloat(document.getElementById('p-lng').value);
|
|
|
|
if (!nama || !no) {
|
|
alert('Lengkapi nama dan nomor SPBU!');
|
|
return;
|
|
}
|
|
|
|
fetch("api/save_data.php", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
nama: nama,
|
|
no: no,
|
|
status: status,
|
|
lat: lat,
|
|
lng: lng
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
|
|
if (response.status == "success") {
|
|
|
|
const d = {
|
|
id: response.id || Date.now(),
|
|
nama,
|
|
no,
|
|
status,
|
|
lat,
|
|
lng
|
|
};
|
|
|
|
state.spbu.push(d);
|
|
|
|
renderSPBU(d);
|
|
|
|
updateCount('spbu', state.spbu.length);
|
|
|
|
addCard(
|
|
'spbu',
|
|
d.id,
|
|
nama,
|
|
'No: ' + no,
|
|
status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam',
|
|
status === 'Ya' ? '#1a6b3c' : '#b93328'
|
|
);
|
|
|
|
if (window._tmpM) {
|
|
map.closePopup();
|
|
map.removeLayer(window._tmpM);
|
|
window._tmpM = null;
|
|
}
|
|
|
|
setMode(null);
|
|
ensureLayerVisible('spbu', d.id);
|
|
focusFeature('spbu', d.id);
|
|
|
|
} else {
|
|
alert("Gagal menyimpan: " + (response.message || "Periksa database"));
|
|
}
|
|
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert("Terjadi error jaringan");
|
|
});
|
|
|
|
}
|
|
|
|
function renderSPBU(d) {
|
|
const color = d.status === 'Ya' ? '#10b981' : '#ef4444';
|
|
const icon = createModernMarker(color, 'fas fa-gas-pump', d.nama);
|
|
const m = L.marker([d.lat, d.lng], { icon, draggable: true });
|
|
m.bindPopup(`<div class="pi">
|
|
<h4><i class="fas fa-gas-pump" style="color:${color}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
|
|
<div class="pi-body">
|
|
<div class="pi-row"><span class="pi-k">No. SPBU</span><span class="pi-v">${d.no}</span></div>
|
|
<div class="pi-row"><span class="pi-k">Status</span><span class="pi-v" style="color:${color}">${d.status === 'Ya' ? '✔ Buka 24 Jam' : '✖ Tidak Buka 24 Jam'}</span></div>
|
|
<div class="pi-row"><span class="pi-k">Koordinat</span><span class="pi-v" style="font-size:11px;font-family:monospace">${d.lat.toFixed(5)}, ${d.lng.toFixed(5)}</span></div>
|
|
</div>
|
|
<div class="act-row">
|
|
<button class="pbtn pbtn-y" onclick="openEditSpbu(${d.id})">Edit</button>
|
|
<button class="pbtn pbtn-r" onclick="deleteFeature('spbu',${d.id})">Hapus</button>
|
|
</div>
|
|
</div>`, { autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] });
|
|
m.addTo(d.status === 'Ya' ? layers.spbu24 : layers.spbuNon24);
|
|
state.featureRefs.spbu[String(d.id)] = m;
|
|
}
|
|
|
|
/* ── JALAN ── */
|
|
function handleLineClick(e) {
|
|
if (!state.drawingLine) state.drawingLine = true;
|
|
state.tempPoints.push(e.latlng);
|
|
const vm = L.circleMarker(e.latlng, { radius: 4, color: '#e74c3c', fillColor: '#e74c3c', fillOpacity: 1, weight: 2 }).addTo(map);
|
|
state.tempMarkers.push(vm);
|
|
if (state.tempPolyline) map.removeLayer(state.tempPolyline);
|
|
state.tempPolyline = L.polyline(state.tempPoints, { color: '#e74c3c', weight: 3, dashArray: '7,5', opacity: 0.8 }).addTo(map);
|
|
document.getElementById('hint').textContent = `📍 ${state.tempPoints.length} titik — klik ganda untuk selesai`;
|
|
}
|
|
|
|
function finishLine(e) {
|
|
if (state.tempPoints.length < 2) { alert('Minimal 2 titik!'); return; }
|
|
const pts = [...state.tempPoints];
|
|
const len = calcLen(pts);
|
|
window._tempLinePts = pts;
|
|
window._tempLineLen = len;
|
|
clearTemp();
|
|
L.popup({ closeOnClick: false, keepInView: true, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
|
|
.setLatLng(pts[Math.floor(pts.length / 2)])
|
|
.setContent(`<div class="pf">
|
|
<h4><i class="fas fa-road"></i> Tambah Jalan</h4>
|
|
<label>Nama Jalan</label>
|
|
<input id="j-nama" placeholder="Jl. Ahmad Yani">
|
|
<label>Status Jalan</label>
|
|
<select id="j-status">
|
|
<option value="Nasional">Jalan Nasional</option>
|
|
<option value="Provinsi">Jalan Provinsi</option>
|
|
<option value="Kabupaten" selected>Jalan Kabupaten</option>
|
|
</select>
|
|
<div style="display:flex;align-items:center;gap:10px;margin:10px 16px 4px;padding:9px 12px;background:#eff6ff;border-radius:9px;border:1px solid #dbeafe">
|
|
<i class="fas fa-ruler-horizontal" style="color:#2563eb;font-size:13px"></i>
|
|
<span style="font-size:11px;color:#64748b;font-weight:600">Panjang terukur:</span>
|
|
<span style="font-size:14px;font-weight:800;color:#1d4ed8;margin-left:auto">${fmtLen(len)}</span>
|
|
</div>
|
|
<button class="pbtn pbtn-b" onclick="saveLine()">Simpan Jalan</button>
|
|
</div>`).addTo(map);
|
|
}
|
|
|
|
function saveLine() {
|
|
const pts = window._tempLinePts || [];
|
|
const len = window._tempLineLen || 0;
|
|
|
|
const nama = document.getElementById('j-nama').value.trim();
|
|
const status = document.getElementById('j-status').value;
|
|
|
|
if (!nama) {
|
|
alert('Isi nama jalan!');
|
|
return;
|
|
}
|
|
|
|
fetch("api/save_jalan.php", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
nama: nama,
|
|
status: status,
|
|
panjang: len,
|
|
koordinat: pts
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
|
|
if (response.status == "success") {
|
|
|
|
const d = {
|
|
id: response.id || Date.now(),
|
|
nama,
|
|
status,
|
|
pts,
|
|
len
|
|
};
|
|
|
|
state.jalan.push(d);
|
|
|
|
renderJalan(d);
|
|
|
|
updateCount('jalan', state.jalan.length);
|
|
|
|
addCard(
|
|
'jalan',
|
|
d.id,
|
|
nama,
|
|
status + ' · ' + fmtLen(len),
|
|
status,
|
|
jColor[status]
|
|
);
|
|
|
|
map.closePopup();
|
|
setMode(null);
|
|
ensureLayerVisible('jalan', d.id);
|
|
focusFeature('jalan', d.id);
|
|
|
|
} else {
|
|
|
|
// log removed
|
|
alert("Gagal simpan jalan");
|
|
|
|
}
|
|
|
|
})
|
|
.catch(err => {
|
|
// log removed
|
|
alert("Error simpan jalan");
|
|
});
|
|
|
|
}
|
|
function renderJalan(d) {
|
|
|
|
const pts = normalizeLinePoints(d.pts);
|
|
if (pts.length < 2) {
|
|
// log removed
|
|
return;
|
|
}
|
|
|
|
const c = jColor[d.status] || '#ef4444';
|
|
|
|
const w =
|
|
d.status === 'Nasional' ? 6 :
|
|
d.status === 'Provinsi' ? 5 : 4;
|
|
|
|
const line = L.polyline(pts, {
|
|
color: c,
|
|
weight: w,
|
|
opacity: 0.95,
|
|
lineCap: 'round',
|
|
lineJoin: 'round',
|
|
className: 'path-glow'
|
|
})
|
|
.bindPopup(`
|
|
<div class="pi">
|
|
<h4><i class="fas fa-road" style="color:${c}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
|
|
|
|
<div class="pi-body">
|
|
<div class="pi-row">
|
|
<span class="pi-k">Status</span>
|
|
<span class="pi-v">${d.status}</span>
|
|
</div>
|
|
<div class="pi-row">
|
|
<span class="pi-k">Panjang</span>
|
|
<span class="pi-v">${fmtLen(d.len)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="act-row">
|
|
<button class="pbtn pbtn-y" onclick="openEditJalan(${d.id})">Edit</button>
|
|
<button class="pbtn pbtn-r" onclick="deleteFeature('jalan',${d.id})">Hapus</button>
|
|
</div>
|
|
|
|
</div>
|
|
`, { autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
|
|
.addTo(layers.jalan);
|
|
|
|
state.featureRefs.jalan[String(d.id)] = line;
|
|
|
|
}
|
|
|
|
function normalizeLinePoints(raw) {
|
|
|
|
let pts = raw;
|
|
|
|
if (typeof pts === 'string') {
|
|
try {
|
|
pts = JSON.parse(pts);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Kompatibilitas data lama: koordinat sempat tersimpan sebagai JSON string di dalam JSON string.
|
|
if (typeof pts === 'string') {
|
|
try {
|
|
pts = JSON.parse(pts);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(pts) || pts.length === 0) return [];
|
|
|
|
if (Array.isArray(pts[0])) {
|
|
return pts
|
|
.map(p => [parseFloat(p[0]), parseFloat(p[1])])
|
|
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
}
|
|
|
|
if (pts[0] && typeof pts[0] === 'object' && pts[0].lat !== undefined) {
|
|
return pts
|
|
.map(p => [parseFloat(p.lat), parseFloat(p.lng)])
|
|
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
function normalizePolyPoints(raw) {
|
|
|
|
let pts = raw;
|
|
|
|
if (typeof pts === 'string') {
|
|
try {
|
|
pts = JSON.parse(pts);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (typeof pts === 'string') {
|
|
try {
|
|
pts = JSON.parse(pts);
|
|
} catch (_) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(pts) || pts.length === 0) return [];
|
|
|
|
if (Array.isArray(pts[0])) {
|
|
return pts
|
|
.map(p => [parseFloat(p[0]), parseFloat(p[1])])
|
|
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
}
|
|
|
|
if (pts[0] && typeof pts[0] === 'object' && pts[0].lat !== undefined) {
|
|
return pts
|
|
.map(p => [parseFloat(p.lat), parseFloat(p.lng)])
|
|
.filter(p => Number.isFinite(p[0]) && Number.isFinite(p[1]));
|
|
}
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
/* ── PARSIL ── */
|
|
function handlePolyClick(e) {
|
|
if (!state.drawingPoly) state.drawingPoly = true;
|
|
if (state.tempPoints.length >= 3) {
|
|
const dist = map.distance(e.latlng, state.tempPoints[0]);
|
|
if (dist < 15) { finishPoly(null); return; }
|
|
}
|
|
state.tempPoints.push(e.latlng);
|
|
const vm = L.circleMarker(e.latlng, { radius: 4, color: '#1a5276', fillColor: '#1a5276', fillOpacity: 1, weight: 2 }).addTo(map);
|
|
state.tempMarkers.push(vm);
|
|
if (state.tempPolygon) map.removeLayer(state.tempPolygon);
|
|
if (state.tempPolyline) map.removeLayer(state.tempPolyline);
|
|
if (state.tempPoints.length >= 2)
|
|
state.tempPolyline = L.polyline([...state.tempPoints, state.tempPoints[0]],
|
|
{ color: '#1a5276', weight: 2, dashArray: '5,4', opacity: 0.7 }).addTo(map);
|
|
if (state.tempPoints.length >= 3)
|
|
state.tempPolygon = L.polygon(state.tempPoints,
|
|
{ color: '#1a5276', fillColor: '#1a5276', fillOpacity: 0.1, weight: 2, dashArray: '5,4' }).addTo(map);
|
|
document.getElementById('hint').textContent =
|
|
`🗺️ ${state.tempPoints.length} titik — klik titik awal atau dbl-klik selesai`;
|
|
}
|
|
|
|
function finishPoly(e) {
|
|
if (state.tempPoints.length < 3) { alert('Minimal 3 titik!'); return; }
|
|
const pts = [...state.tempPoints];
|
|
const area = calcArea(pts);
|
|
window._tempPolyPts = pts;
|
|
window._tempPolyArea = area;
|
|
clearTemp();
|
|
const center = L.polygon(pts).getBounds().getCenter();
|
|
L.popup({ closeOnClick: false, keepInView: true, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] })
|
|
.setLatLng(center)
|
|
.setContent(`<div class="pf">
|
|
<h4><i class="fas fa-vector-square"></i> Tambah Parsil / Lahan</h4>
|
|
<label>Nama / ID Parsil</label>
|
|
<input id="ps-nama" placeholder="Parsil A-01">
|
|
<label>Pemilik</label>
|
|
<input id="ps-pemilik" placeholder="Nama pemilik">
|
|
<label>Status Sertifikat</label>
|
|
<select id="ps-status">
|
|
<option value="SHM">SHM — Hak Milik</option>
|
|
<option value="HGB">HGB — Hak Guna Bangunan</option>
|
|
<option value="Sewa">Sewa / Kontrak</option>
|
|
<option value="Lainnya">Lainnya</option>
|
|
</select>
|
|
<div style="display:flex;align-items:center;gap:10px;margin:10px 16px 4px;padding:9px 12px;background:#fffbeb;border-radius:9px;border:1px solid #fde68a">
|
|
<i class="fas fa-ruler-combined" style="color:#d97706;font-size:13px"></i>
|
|
<span style="font-size:11px;color:#64748b;font-weight:600">Luas terukur:</span>
|
|
<span style="font-size:14px;font-weight:800;color:#d97706;margin-left:auto">${fmtArea(area)}</span>
|
|
</div>
|
|
<button class="pbtn pbtn-y" onclick="saveParsil()">Simpan Parsil</button>
|
|
</div>`).addTo(map);
|
|
}
|
|
|
|
function saveParsil() {
|
|
const pts = window._tempPolyPts || [];
|
|
const area = window._tempPolyArea || 0;
|
|
|
|
const nama = document.getElementById('ps-nama').value.trim();
|
|
const pemilik = document.getElementById('ps-pemilik').value.trim();
|
|
const status = document.getElementById('ps-status').value;
|
|
|
|
if (!nama) {
|
|
alert('Isi nama parsil!');
|
|
return;
|
|
}
|
|
|
|
fetch("api/save_parsil.php", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
nama: nama,
|
|
pemilik: pemilik,
|
|
status: status,
|
|
luas: area,
|
|
koordinat: pts
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(response => {
|
|
|
|
if (response.status == "success") {
|
|
|
|
const parsil = {
|
|
id: response.id || Date.now(),
|
|
nama,
|
|
pemilik,
|
|
status,
|
|
pts,
|
|
area
|
|
};
|
|
|
|
state.parsil.push(parsil);
|
|
|
|
renderParsil(parsil);
|
|
|
|
updateCount('parsil', state.parsil.length);
|
|
|
|
addCard(
|
|
'parsil',
|
|
parsil.id,
|
|
parsil.nama,
|
|
parsil.status,
|
|
parsil.status,
|
|
'#1a5276'
|
|
);
|
|
|
|
map.closePopup();
|
|
setMode(null);
|
|
ensureLayerVisible('parsil', parsil.id);
|
|
focusFeature('parsil', parsil.id);
|
|
|
|
}
|
|
|
|
else {
|
|
// log removed
|
|
alert("Gagal simpan parsil");
|
|
}
|
|
|
|
})
|
|
.catch(err => {});
|
|
}
|
|
|
|
function renderParsil(d) {
|
|
|
|
const pts = normalizePolyPoints(d.pts);
|
|
if (pts.length < 3) {
|
|
// log removed
|
|
return;
|
|
}
|
|
|
|
const c = pColor[d.status];
|
|
|
|
const poly = L.polygon(pts, {
|
|
color: c,
|
|
fillColor: c,
|
|
fillOpacity: 0.25,
|
|
weight: 3,
|
|
dashArray: '8, 6',
|
|
lineCap: 'round',
|
|
lineJoin: 'round',
|
|
className: 'parsil-glow'
|
|
})
|
|
.bindPopup(`
|
|
<div class="pi">
|
|
<h4><i class="fas fa-draw-polygon" style="color:${c}"></i>${d.nama}<button class="pi-close-alt" onclick="map.closePopup()"><i class="fas fa-times"></i></button></h4>
|
|
|
|
<div class="pi-body">
|
|
<div class="pi-row">
|
|
<span class="pi-k">Pemilik</span>
|
|
<span class="pi-v">${d.pemilik || '-'}</span>
|
|
</div>
|
|
<div class="pi-row">
|
|
<span class="pi-k">Status</span>
|
|
<span class="pi-v">${d.status}</span>
|
|
</div>
|
|
<div class="pi-row">
|
|
<span class="pi-k">Luas</span>
|
|
<span class="pi-v">${fmtArea(d.area)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="act-row">
|
|
<button class="pbtn pbtn-y" onclick="openEditParsil(${d.id})">Edit</button>
|
|
<button class="pbtn pbtn-r" onclick="deleteFeature('parsil',${d.id})">Hapus</button>
|
|
</div>
|
|
|
|
</div>
|
|
`)
|
|
.addTo(layers.parsil);
|
|
|
|
state.featureRefs.parsil[String(d.id)] = poly;
|
|
|
|
}
|
|
|
|
function getById(type, id) {
|
|
return state[type].find(x => String(x.id) === String(id));
|
|
}
|
|
|
|
function pointsToText(points) {
|
|
return points.map(p => `${Number(p[0]).toFixed(6)},${Number(p[1]).toFixed(6)}`).join('\n');
|
|
}
|
|
|
|
function textToPoints(text, minPoints) {
|
|
const rows = text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
|
|
const pts = [];
|
|
for (const row of rows) {
|
|
const parts = row.split(',').map(x => x.trim());
|
|
if (parts.length !== 2) return null;
|
|
const lat = parseFloat(parts[0]);
|
|
const lng = parseFloat(parts[1]);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
|
|
pts.push([lat, lng]);
|
|
}
|
|
if (pts.length < minPoints) return null;
|
|
return pts;
|
|
}
|
|
|
|
function toLatLngObjects(pts) {
|
|
return pts.map(p => ({ lat: parseFloat(p[0]), lng: parseFloat(p[1]) }));
|
|
}
|
|
|
|
function makeVertexIcon() {
|
|
return L.divIcon({
|
|
className: '',
|
|
html: '<div style="width:12px;height:12px;border-radius:50%;background:#fdfdfd;border:2px solid #1b3a5c;box-shadow:0 4px 12px rgba(15,23,42,.25);"></div>',
|
|
iconSize: [16, 16],
|
|
iconAnchor: [8, 8]
|
|
});
|
|
}
|
|
|
|
function syncEditTextarea(type, points) {
|
|
const id = type === 'jalan' ? 'e-jalan-pts' : 'e-parsil-pts';
|
|
const el = document.getElementById(id);
|
|
if (el) el.value = pointsToText(points);
|
|
}
|
|
|
|
function stopGeometryEdit() {
|
|
if (!state.geometryEdit) return;
|
|
|
|
const g = state.geometryEdit;
|
|
|
|
if (g.overlay && map.hasLayer(g.overlay)) map.removeLayer(g.overlay);
|
|
(g.vertexMarkers || []).forEach(m => { if (map.hasLayer(m)) map.removeLayer(m); });
|
|
|
|
if (g.onMove) map.off('mousemove', g.onMove);
|
|
if (g.onUp) map.off('mouseup', g.onUp);
|
|
if (g.onMapClick) map.off('click', g.onMapClick);
|
|
|
|
map.dragging.enable();
|
|
|
|
const ref = state.featureRefs[g.type][String(g.id)];
|
|
if (ref && ref.setStyle) {
|
|
if (g.type === 'jalan') ref.setStyle({ opacity: 0.9, interactive: true });
|
|
if (g.type === 'parsil') ref.setStyle({ opacity: 1, fillOpacity: 0.18, interactive: true });
|
|
}
|
|
|
|
state.geometryEdit = null;
|
|
const hint = document.getElementById('hint');
|
|
hint.classList.remove('show');
|
|
}
|
|
|
|
function startGeometryEdit(type, id) {
|
|
if (type !== 'jalan' && type !== 'parsil') return;
|
|
|
|
const d = getById(type, id);
|
|
if (!d) return;
|
|
|
|
const points = type === 'jalan' ? normalizeLinePoints(d.pts) : normalizePolyPoints(d.pts);
|
|
const minPts = type === 'jalan' ? 2 : 3;
|
|
if (points.length < minPts) {
|
|
alert('Koordinat belum valid untuk diedit di peta');
|
|
return;
|
|
}
|
|
|
|
stopGeometryEdit();
|
|
setMode(null);
|
|
ensureLayerVisible(type);
|
|
|
|
const ref = state.featureRefs[type][String(id)];
|
|
if (ref && ref.setStyle) {
|
|
if (type === 'jalan') ref.setStyle({ opacity: 0.2, interactive: false });
|
|
if (type === 'parsil') ref.setStyle({ opacity: 0.2, fillOpacity: 0.05, interactive: false });
|
|
}
|
|
|
|
const overlay = type === 'jalan'
|
|
? L.polyline(points, { color: '#1b3a5c', weight: 4, dashArray: '8,6', opacity: 0.95 }).addTo(map)
|
|
: L.polygon(points, { color: '#1b3a5c', weight: 3, dashArray: '8,6', fillColor: '#1b3a5c', fillOpacity: 0.12 }).addTo(map);
|
|
|
|
const g = {
|
|
type,
|
|
id,
|
|
points: points.map(p => [p[0], p[1]]),
|
|
overlay,
|
|
vertexMarkers: [],
|
|
draggingAll: false,
|
|
lastLatLng: null,
|
|
onMove: null,
|
|
onUp: null,
|
|
onMapClick: null
|
|
};
|
|
|
|
const redraw = () => {
|
|
if (g.type === 'jalan') g.overlay.setLatLngs(g.points);
|
|
else g.overlay.setLatLngs(g.points);
|
|
|
|
g.vertexMarkers.forEach((m, i) => m.setLatLng(g.points[i]));
|
|
syncEditTextarea(g.type, g.points);
|
|
};
|
|
|
|
const icon = makeVertexIcon();
|
|
g.points.forEach((p, i) => {
|
|
const vm = L.marker(p, { draggable: true, icon, zIndexOffset: 1000 }).addTo(map);
|
|
vm.on('drag', ev => {
|
|
const ll = ev.target.getLatLng();
|
|
g.points[i] = [ll.lat, ll.lng];
|
|
redraw();
|
|
});
|
|
g.vertexMarkers.push(vm);
|
|
});
|
|
|
|
const startDragAll = (latlng) => {
|
|
g.draggingAll = true;
|
|
g.lastLatLng = latlng;
|
|
map.dragging.disable();
|
|
};
|
|
|
|
g.overlay.on('mousedown', e => {
|
|
L.DomEvent.stopPropagation(e);
|
|
startDragAll(e.latlng);
|
|
});
|
|
|
|
g.onMapClick = (e) => {
|
|
if (e.originalEvent && e.originalEvent.shiftKey) {
|
|
startDragAll(e.latlng);
|
|
}
|
|
};
|
|
|
|
g.onMove = (e) => {
|
|
if (!g.draggingAll || !g.lastLatLng) return;
|
|
const dLat = e.latlng.lat - g.lastLatLng.lat;
|
|
const dLng = e.latlng.lng - g.lastLatLng.lng;
|
|
g.points = g.points.map(p => [p[0] + dLat, p[1] + dLng]);
|
|
g.lastLatLng = e.latlng;
|
|
redraw();
|
|
};
|
|
|
|
g.onUp = () => {
|
|
g.draggingAll = false;
|
|
g.lastLatLng = null;
|
|
map.dragging.enable();
|
|
};
|
|
|
|
map.on('click', g.onMapClick);
|
|
map.on('mousemove', g.onMove);
|
|
map.on('mouseup', g.onUp);
|
|
|
|
state.geometryEdit = g;
|
|
syncEditTextarea(type, g.points);
|
|
|
|
const hint = document.getElementById('hint');
|
|
hint.textContent = 'Mode edit geometri aktif: drag titik vertex atau drag shape untuk geser keseluruhan.';
|
|
hint.classList.add('show');
|
|
}
|
|
|
|
function refreshType(type) {
|
|
if (state.geometryEdit && state.geometryEdit.type === type) stopGeometryEdit();
|
|
if (type === 'spbu') {
|
|
layers.spbu24.clearLayers();
|
|
layers.spbuNon24.clearLayers();
|
|
} else {
|
|
layers[type].clearLayers();
|
|
}
|
|
state.featureRefs[type] = {};
|
|
if (type === 'kemiskinan') state.kkLayerRefs = {};
|
|
const list = document.getElementById('list-' + type);
|
|
if (list) list.querySelectorAll('.dcard').forEach(x => x.remove());
|
|
|
|
state[type].forEach(d => {
|
|
if (type === 'spbu') {
|
|
renderSPBU(d);
|
|
addCard('spbu', d.id, d.nama, 'No: ' + d.no, d.status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam', d.status === 'Ya' ? '#1a6b3c' : '#b93328');
|
|
return;
|
|
}
|
|
if (type === 'jalan') {
|
|
renderJalan(d);
|
|
addCard('jalan', d.id, d.nama, d.status + ' · ' + fmtLen(d.len), d.status, jColor[d.status] || '#e74c3c');
|
|
return;
|
|
}
|
|
if (type === 'kemiskinan') {
|
|
renderKemiskinan(d);
|
|
addCard('kemiskinan', d.id, d.nama_kk, 'Jumlah KK: ' + d.jumlah_kk, 'Kemiskinan', '#9f6500');
|
|
return;
|
|
}
|
|
if (type === 'masjid') {
|
|
renderMasjid(d);
|
|
addCard('masjid', d.id, d.nama_masjid, d.pic_masjid, 'Rumah Ibadah', '#0f6250');
|
|
return;
|
|
}
|
|
renderParsil(d);
|
|
addCard('parsil', d.id, d.nama, d.status, d.status, '#1a5276');
|
|
});
|
|
|
|
updateCount(type, state[type].length);
|
|
renderDashboard();
|
|
}
|
|
|
|
function openEditSpbu(id) {
|
|
const d = getById('spbu', id);
|
|
if (!d) return;
|
|
const ref = state.featureRefs.spbu[String(id)];
|
|
const ll = ref && ref.getLatLng ? ref.getLatLng() : { lat: d.lat, lng: d.lng };
|
|
const html = `<div class="pf">
|
|
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit SPBU</h4>
|
|
<label>Nama SPBU</label>
|
|
<input id="e-spbu-nama" value="${(d.nama || '').replace(/"/g, '"')}">
|
|
<label>Nomor SPBU</label>
|
|
<input id="e-spbu-no" value="${(d.no || '').replace(/"/g, '"')}">
|
|
<label>Status Operasional</label>
|
|
<select id="e-spbu-status">
|
|
<option value="Ya" ${d.status === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
|
|
<option value="Tidak" ${d.status === 'Tidak' ? 'selected' : ''}>Tidak Buka 24 Jam</option>
|
|
</select>
|
|
<label>Latitude</label>
|
|
<input id="e-spbu-lat" value="${Number(ll.lat).toFixed(6)}">
|
|
<label>Longitude</label>
|
|
<input id="e-spbu-lng" value="${Number(ll.lng).toFixed(6)}">
|
|
<button class="pbtn pbtn-y" onclick="saveEditSpbu(${id})">Simpan Perubahan</button>
|
|
</div>`;
|
|
map.closePopup();
|
|
L.popup({ maxWidth: 290, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng([ll.lat, ll.lng]).setContent(html).openOn(map);
|
|
}
|
|
|
|
function saveEditSpbu(id) {
|
|
const nama = document.getElementById('e-spbu-nama').value.trim();
|
|
const no = document.getElementById('e-spbu-no').value.trim();
|
|
const status = document.getElementById('e-spbu-status').value;
|
|
const lat = parseFloat(document.getElementById('e-spbu-lat').value);
|
|
const lng = parseFloat(document.getElementById('e-spbu-lng').value);
|
|
|
|
if (!nama || !no || !Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
alert('Data SPBU belum valid');
|
|
return;
|
|
}
|
|
|
|
fetch('api/update_data.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, nama, no, status, lat, lng })
|
|
})
|
|
.then(res => res.json())
|
|
.then(resp => {
|
|
if (resp.status !== 'success') {
|
|
alert('Gagal update SPBU');
|
|
return;
|
|
}
|
|
const d = getById('spbu', id);
|
|
if (!d) return;
|
|
d.nama = nama; d.no = no; d.status = status; d.lat = lat; d.lng = lng;
|
|
refreshType('spbu');
|
|
map.closePopup();
|
|
focusFeature('spbu', id);
|
|
})
|
|
.catch(() => alert('Error update SPBU'));
|
|
}
|
|
|
|
function openEditJalan(id) {
|
|
if (state.geometryEdit && (state.geometryEdit.type !== 'jalan' || String(state.geometryEdit.id) !== String(id))) {
|
|
stopGeometryEdit();
|
|
}
|
|
const d = getById('jalan', id);
|
|
if (!d) return;
|
|
const pts = normalizeLinePoints(d.pts);
|
|
const ref = state.featureRefs.jalan[String(id)];
|
|
const center = ref && ref.getBounds ? ref.getBounds().getCenter() : map.getCenter();
|
|
const html = `<div class="pf" style="width:290px">
|
|
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit Jalan</h4>
|
|
<label>Nama Jalan</label>
|
|
<input id="e-jalan-nama" value="${(d.nama || '').replace(/"/g, '"')}">
|
|
<label>Status Jalan</label>
|
|
<select id="e-jalan-status">
|
|
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Jalan Nasional</option>
|
|
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Jalan Provinsi</option>
|
|
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Jalan Kabupaten</option>
|
|
</select>
|
|
<label>Koordinat Lokasi (satu baris: lat,lng)</label>
|
|
<textarea id="e-jalan-pts">${pointsToText(pts)}</textarea>
|
|
<button class="pbtn pbtn-b" onclick="startGeometryEdit('jalan',${id})">Edit Vertex / Drag di Peta</button>
|
|
<button class="pbtn pbtn-y" onclick="saveEditJalan(${id})">Simpan Perubahan</button>
|
|
</div>`;
|
|
map.closePopup();
|
|
L.popup({ maxWidth: 330, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng(center).setContent(html).openOn(map);
|
|
}
|
|
|
|
function saveEditJalan(id) {
|
|
if (state.geometryEdit && state.geometryEdit.type === 'jalan' && String(state.geometryEdit.id) === String(id)) {
|
|
syncEditTextarea('jalan', state.geometryEdit.points);
|
|
}
|
|
const nama = document.getElementById('e-jalan-nama').value.trim();
|
|
const status = document.getElementById('e-jalan-status').value;
|
|
const pts = textToPoints(document.getElementById('e-jalan-pts').value, 2);
|
|
if (!nama || !pts) {
|
|
alert('Data jalan atau koordinat belum valid');
|
|
return;
|
|
}
|
|
|
|
const len = calcLen(pts);
|
|
|
|
fetch('api/update_jalan.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, nama, status, panjang: len, koordinat: toLatLngObjects(pts) })
|
|
})
|
|
.then(res => res.json())
|
|
.then(resp => {
|
|
if (resp.status !== 'success') {
|
|
alert('Gagal update jalan');
|
|
return;
|
|
}
|
|
const d = getById('jalan', id);
|
|
if (!d) return;
|
|
d.nama = nama; d.status = status; d.len = len; d.pts = pts;
|
|
stopGeometryEdit();
|
|
refreshType('jalan');
|
|
map.closePopup();
|
|
focusFeature('jalan', id);
|
|
})
|
|
.catch(() => alert('Error update jalan'));
|
|
}
|
|
|
|
function openEditParsil(id) {
|
|
if (state.geometryEdit && (state.geometryEdit.type !== 'parsil' || String(state.geometryEdit.id) !== String(id))) {
|
|
stopGeometryEdit();
|
|
}
|
|
const d = getById('parsil', id);
|
|
if (!d) return;
|
|
const pts = normalizePolyPoints(d.pts);
|
|
const ref = state.featureRefs.parsil[String(id)];
|
|
const center = ref && ref.getBounds ? ref.getBounds().getCenter() : map.getCenter();
|
|
const html = `<div class="pf" style="width:290px">
|
|
<h4><i class="fas fa-edit" style="color:var(--amber)"></i> Edit Parsil</h4>
|
|
<label>Nama / ID Parsil</label>
|
|
<input id="e-parsil-nama" value="${(d.nama || '').replace(/"/g, '"')}">
|
|
<label>Pemilik</label>
|
|
<input id="e-parsil-pemilik" value="${(d.pemilik || '').replace(/"/g, '"')}">
|
|
<label>Status Sertifikat</label>
|
|
<select id="e-parsil-status">
|
|
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM – Hak Milik</option>
|
|
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB – Hak Guna Bangunan</option>
|
|
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU – Hak Guna Usaha</option>
|
|
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP – Hak Pakai</option>
|
|
</select>
|
|
<label>Koordinat Lokasi (satu baris: lat,lng)</label>
|
|
<textarea id="e-parsil-pts">${pointsToText(pts)}</textarea>
|
|
<button class="pbtn pbtn-b" onclick="startGeometryEdit('parsil',${id})">Edit Vertex / Drag di Peta</button>
|
|
<button class="pbtn pbtn-y" onclick="saveEditParsil(${id})">Simpan Perubahan</button>
|
|
</div>`;
|
|
map.closePopup();
|
|
L.popup({ maxWidth: 330, autoPanPaddingTopLeft: [0, 80], autoPanPaddingBottomRight: [0, 20] }).setLatLng(center).setContent(html).openOn(map);
|
|
}
|
|
|
|
function saveEditParsil(id) {
|
|
if (state.geometryEdit && state.geometryEdit.type === 'parsil' && String(state.geometryEdit.id) === String(id)) {
|
|
syncEditTextarea('parsil', state.geometryEdit.points);
|
|
}
|
|
const nama = document.getElementById('e-parsil-nama').value.trim();
|
|
const pemilik = document.getElementById('e-parsil-pemilik').value.trim();
|
|
const status = document.getElementById('e-parsil-status').value;
|
|
const pts = textToPoints(document.getElementById('e-parsil-pts').value, 3);
|
|
if (!nama || !pts) {
|
|
alert('Data parsil atau koordinat belum valid');
|
|
return;
|
|
}
|
|
|
|
const area = calcArea(toLatLngObjects(pts));
|
|
|
|
fetch('api/update_parsil.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, nama, pemilik, status, luas: area, koordinat: toLatLngObjects(pts) })
|
|
})
|
|
.then(res => res.json())
|
|
.then(resp => {
|
|
if (resp.status !== 'success') {
|
|
alert('Gagal update parsil');
|
|
return;
|
|
}
|
|
const d = getById('parsil', id);
|
|
if (!d) return;
|
|
d.nama = nama; d.pemilik = pemilik; d.status = status; d.area = area; d.pts = pts;
|
|
stopGeometryEdit();
|
|
refreshType('parsil');
|
|
map.closePopup();
|
|
focusFeature('parsil', id);
|
|
})
|
|
.catch(() => alert('Error update parsil'));
|
|
}
|
|
|
|
function deleteFeature(type, id) {
|
|
if (state.geometryEdit && state.geometryEdit.type === type && String(state.geometryEdit.id) === String(id)) {
|
|
stopGeometryEdit();
|
|
}
|
|
const label = type === 'spbu' ? 'SPBU' : type === 'jalan' ? 'Jalan' : type === 'kemiskinan' ? 'Kemiskinan' : type === 'masjid' ? 'Rumah Ibadah' : 'Parsil';
|
|
if (!confirm(`Hapus data ${label} ini?`)) return;
|
|
|
|
const ep = {
|
|
spbu: 'api/delete_data.php',
|
|
jalan: 'api/delete_jalan.php',
|
|
parsil: 'api/delete_parsil.php',
|
|
kemiskinan: 'api/delete_kemiskinan.php',
|
|
masjid: 'api/delete_masjid.php'
|
|
};
|
|
|
|
fetch(ep[type], {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
})
|
|
.then(res => res.json())
|
|
.then(resp => {
|
|
if (resp.status !== 'success') {
|
|
alert('Gagal menghapus data');
|
|
return;
|
|
}
|
|
state[type] = state[type].filter(x => String(x.id) !== String(id));
|
|
if (state.activeCard === 'card-' + type + '-' + id) state.activeCard = null;
|
|
refreshType(type);
|
|
map.closePopup();
|
|
})
|
|
.catch(() => alert('Error hapus data'));
|
|
}
|
|
|
|
/* ── HELPERS ── */
|
|
function calcLen(pts) {
|
|
let t = 0;
|
|
for (let i = 1; i < pts.length; i++)t += L.latLng(pts[i - 1]).distanceTo(L.latLng(pts[i]));
|
|
return t;
|
|
}
|
|
function fmtLen(m) { return m >= 1000 ? (m / 1000).toFixed(2) + ' km' : Math.round(m) + ' m' }
|
|
|
|
function calcArea(pts) {
|
|
let a = 0; const n = pts.length;
|
|
for (let i = 0; i < n; i++) {
|
|
const j = (i + 1) % n;
|
|
const la1 = pts[i].lat * Math.PI / 180, la2 = pts[j].lat * Math.PI / 180;
|
|
const dl = (pts[j].lng - pts[i].lng) * Math.PI / 180;
|
|
a += Math.sin(dl) * (2 + Math.sin(la1) + Math.sin(la2));
|
|
}
|
|
return Math.abs(a * 6378137 * 6378137 / 2);
|
|
}
|
|
function fmtArea(m) { return m >= 10000 ? (m / 10000).toFixed(2) + ' ha' : Math.round(m) + ' m²' }
|
|
|
|
function clearTemp() {
|
|
state.tempPoints = []; state.drawingLine = false; state.drawingPoly = false;
|
|
state.tempMarkers.forEach(m => map.removeLayer(m)); state.tempMarkers = [];
|
|
if (state.tempPolyline) { map.removeLayer(state.tempPolyline); state.tempPolyline = null; }
|
|
if (state.tempPolygon) { map.removeLayer(state.tempPolygon); state.tempPolygon = null; }
|
|
}
|
|
function cancelDrawing() {
|
|
clearTemp();
|
|
if (window._tmpM) { map.removeLayer(window._tmpM); window._tmpM = null; }
|
|
if (window._tmpC) { map.removeLayer(window._tmpC); window._tmpC = null; }
|
|
}
|
|
function ensureLayerVisible(type, id) {
|
|
if (type === 'spbu') {
|
|
const d = id !== undefined ? getById('spbu', id) : null;
|
|
if (d) {
|
|
const targetLayer = d.status === 'Ya' ? layers.spbu24 : layers.spbuNon24;
|
|
map.addLayer(targetLayer);
|
|
} else {
|
|
if (state.spbuFilterVis.open24) map.addLayer(layers.spbu24);
|
|
if (state.spbuFilterVis.non24) map.addLayer(layers.spbuNon24);
|
|
}
|
|
return;
|
|
}
|
|
|
|
map.addLayer(layers[type]);
|
|
}
|
|
|
|
function setActiveCard(type, id) {
|
|
if (state.activeCard) {
|
|
const prev = document.getElementById(state.activeCard);
|
|
if (prev) prev.style.boxShadow = '';
|
|
}
|
|
const cardId = 'card-' + type + '-' + id;
|
|
const cur = document.getElementById(cardId);
|
|
if (cur) {
|
|
cur.style.boxShadow = '0 0 0 2px rgba(27,58,92,0.28), 0 2px 8px rgba(0,0,0,0.1)';
|
|
state.activeCard = cardId;
|
|
}
|
|
}
|
|
|
|
function focusFeature(type, id) {
|
|
const ref = state.featureRefs[type][String(id)];
|
|
if (!ref) return;
|
|
|
|
map.invalidateSize();
|
|
ensureLayerVisible(type, id);
|
|
setActiveCard(type, id);
|
|
|
|
if (ref.getLatLng) {
|
|
map.flyTo(ref.getLatLng(), 17, { duration: 0.7 });
|
|
} else if (ref.getBounds) {
|
|
map.fitBounds(ref.getBounds(), { padding: [40, 40], maxZoom: 17 });
|
|
}
|
|
|
|
if (ref.openPopup) {
|
|
setTimeout(() => ref.openPopup(), 800);
|
|
}
|
|
}
|
|
|
|
function updateCount(type, n) {
|
|
const el = document.getElementById('cnt-' + type);
|
|
if (el) el.textContent = n;
|
|
}
|
|
function addCard(type, id, name, sub, badge, color) {
|
|
const c = document.getElementById('list-' + type);
|
|
const el = document.createElement('div');
|
|
el.className = 'dcard'; el.id = 'card-' + type + '-' + id;
|
|
el.tabIndex = 0;
|
|
el.innerHTML = `<div style="position:absolute;left:0;top:0;bottom:0;width:3px;background:${color};border-radius:8px 0 0 8px"></div>
|
|
<div class="dc-name">${name}</div>
|
|
<div class="dc-sub">${sub}</div>
|
|
<div class="dc-badge" style="background:${color}20;color:${color}">${badge}</div>`;
|
|
el.onclick = () => focusFeature(type, id);
|
|
el.onkeydown = (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
focusFeature(type, id);
|
|
}
|
|
};
|
|
if (!c) { console.warn('addCard: container list-' + type + ' not found'); return; }
|
|
c.appendChild(el);
|
|
}
|
|
|
|
document.addEventListener('keydown', e => {
|
|
if (e.key === 'Escape') {
|
|
stopGeometryEdit();
|
|
cancelDrawing();
|
|
setMode(null);
|
|
}
|
|
});
|
|
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
|
|
loadSPBU();
|
|
loadJalan();
|
|
loadParsil();
|
|
|
|
// Kemiskinan butuh data Masjid untuk hitung Blank Spot (merah/hijau)
|
|
// Jadi pastikan loadMasjid selesai dulu baru loadKemiskinan
|
|
loadMasjid().then(() => {
|
|
loadKemiskinan();
|
|
});
|
|
|
|
});
|
|
|
|
function loadSPBU() {
|
|
|
|
fetch("api/read_data.php")
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
data.forEach(d => {
|
|
|
|
const spbu = {
|
|
id: d.id,
|
|
nama: d.nama,
|
|
no: d.no,
|
|
status: d.status,
|
|
lat: parseFloat(d.lat),
|
|
lng: parseFloat(d.lng)
|
|
};
|
|
|
|
state.spbu.push(spbu);
|
|
|
|
renderSPBU(spbu);
|
|
|
|
updateCount('spbu', state.spbu.length);
|
|
|
|
addCard(
|
|
'spbu',
|
|
spbu.id,
|
|
spbu.nama,
|
|
'No: ' + spbu.no,
|
|
spbu.status === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam',
|
|
spbu.status === 'Ya' ? '#1a6b3c' : '#b93328'
|
|
);
|
|
|
|
});
|
|
|
|
})
|
|
.catch(err => {});
|
|
|
|
}
|
|
|
|
function loadJalan() {
|
|
|
|
fetch("api/read_jalan.php")
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
data.forEach(d => {
|
|
|
|
const jalan = {
|
|
id: d.id,
|
|
nama: d.nama,
|
|
status: d.status,
|
|
len: parseFloat(d.panjang),
|
|
pts: d.koordinat
|
|
};
|
|
|
|
state.jalan.push(jalan);
|
|
|
|
renderJalan(jalan);
|
|
|
|
updateCount('jalan', state.jalan.length);
|
|
|
|
addCard(
|
|
'jalan',
|
|
jalan.id,
|
|
jalan.nama,
|
|
jalan.status,
|
|
jalan.status,
|
|
'#e74c3c'
|
|
);
|
|
|
|
});
|
|
|
|
})
|
|
.catch(err => {});
|
|
|
|
}
|
|
|
|
function loadParsil() {
|
|
|
|
fetch("api/read_parsil.php")
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
|
|
data.forEach(d => {
|
|
|
|
const parsil = {
|
|
id: d.id,
|
|
nama: d.nama,
|
|
pemilik: d.pemilik,
|
|
status: d.status,
|
|
area: parseFloat(d.luas),
|
|
pts: d.koordinat
|
|
};
|
|
|
|
state.parsil.push(parsil);
|
|
|
|
renderParsil(parsil);
|
|
|
|
updateCount('parsil', state.parsil.length);
|
|
|
|
addCard(
|
|
'parsil',
|
|
parsil.id,
|
|
parsil.nama,
|
|
parsil.status,
|
|
parsil.status,
|
|
'#1a5276'
|
|
);
|
|
|
|
});
|
|
|
|
})
|
|
.catch(err => {});
|
|
}
|
|
|
|
/* ── KEMISKINAN ── */
|
|
function getKemiskinanDraft() {
|
|
return {
|
|
nama_kk: document.getElementById('k-nama')?.value || '',
|
|
jumlah_kk: document.getElementById('k-jumlah')?.value || '1',
|
|
alamat: document.getElementById('k-alamat')?.value || ''
|
|
};
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return String(value || '').replace(/&/g, '&').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>
|
|
|
|
|