2398 lines
108 KiB
HTML
2398 lines
108 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta name="description" content="Website Pendata Kemiskinan – WebGIS berbasis partisipasi Rumah Ibadah">
|
||
<title>Website Pemberantas Kemiskinan</title>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-dark: #0f1117;
|
||
--bg-sidebar: #161b27;
|
||
--bg-card: #1e2538;
|
||
--bg-card-hover: #252d42;
|
||
--border: #2a3248;
|
||
--accent-blue: #3b82f6;
|
||
--accent-green: #10b981;
|
||
--accent-yellow: #f59e0b;
|
||
--accent-red: #ef4444;
|
||
--text-primary: #f1f5f9;
|
||
--text-secondary: #94a3b8;
|
||
--text-muted: #4b5563;
|
||
--radius-sm: 6px;
|
||
--radius-md: 10px;
|
||
--shadow: 0 4px 24px rgba(0, 0, 0, .4);
|
||
--kemiskinan: #f97316;
|
||
--masjid: #8b5cf6;
|
||
}
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', sans-serif;
|
||
background: var(--bg-dark);
|
||
color: var(--text-primary);
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#app-container {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#header-bar {
|
||
background: linear-gradient(135deg, #161b27 0%, #0f1117 100%);
|
||
border-bottom: 1px solid var(--border);
|
||
padding: 12px 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
/* ── Auth / User Badge ── */
|
||
.user-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: 1px;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.role-badge {
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 20px;
|
||
letter-spacing: .5px;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.role-badge-admin {
|
||
background: rgba(59, 130, 246, .2);
|
||
color: #60a5fa;
|
||
border: 1px solid rgba(59, 130, 246, .4);
|
||
}
|
||
|
||
.role-badge-pengurus {
|
||
background: rgba(139, 92, 246, .2);
|
||
color: #a78bfa;
|
||
border: 1px solid rgba(139, 92, 246, .4);
|
||
}
|
||
|
||
.role-badge-walikota {
|
||
background: rgba(249, 115, 22, .2);
|
||
color: #fb923c;
|
||
border: 1px solid rgba(249, 115, 22, .4);
|
||
}
|
||
|
||
.btn-login {
|
||
padding: 6px 16px;
|
||
background: linear-gradient(135deg, var(--accent-blue), #2563eb);
|
||
border: none;
|
||
border-radius: 6px;
|
||
color: #fff;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
font-family: 'Inter', sans-serif;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
text-decoration: none;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn-login:hover {
|
||
opacity: .9;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-lapor-header {
|
||
padding: 6px 14px;
|
||
background: rgba(239, 68, 68, .15);
|
||
border: 1px solid rgba(239, 68, 68, .35);
|
||
border-radius: 6px;
|
||
color: #f87171;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
font-family: 'Inter', sans-serif;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.btn-lapor-header:hover {
|
||
background: rgba(239, 68, 68, .25);
|
||
}
|
||
|
||
.btn-export {
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
background: rgba(59, 130, 246, .12);
|
||
border: 1px solid rgba(59, 130, 246, .3);
|
||
border-radius: 6px;
|
||
color: #60a5fa;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
font-family: 'Inter', sans-serif;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
margin: 0 14px 10px;
|
||
display: none;
|
||
}
|
||
|
||
.btn-export:hover {
|
||
background: rgba(59, 130, 246, .22);
|
||
}
|
||
|
||
.btn-logout {
|
||
padding: 6px 14px;
|
||
background: rgba(239, 68, 68, .12);
|
||
border: 1px solid rgba(239, 68, 68, .3);
|
||
border-radius: 6px;
|
||
color: #f87171;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
font-family: 'Inter', sans-serif;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
}
|
||
|
||
.btn-logout:hover {
|
||
background: rgba(239, 68, 68, .22);
|
||
border-color: rgba(239, 68, 68, .5);
|
||
}
|
||
|
||
/* ── Role-restricted element hiding ── */
|
||
.hide-for-role {
|
||
display: none !important;
|
||
}
|
||
|
||
.header-title {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.header-title h1 {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
margin: 0;
|
||
}
|
||
|
||
.status-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--accent-green);
|
||
}
|
||
|
||
.pulse-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background-color: var(--accent-green);
|
||
border-radius: 50%;
|
||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% {
|
||
transform: scale(0.95);
|
||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||
}
|
||
|
||
70% {
|
||
transform: scale(1);
|
||
box-shadow: 0 0 0 6px rgba(16, 185, 129, 0);
|
||
}
|
||
|
||
100% {
|
||
transform: scale(0.95);
|
||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||
}
|
||
}
|
||
|
||
#bottom-bar {
|
||
background: var(--bg-sidebar);
|
||
border-top: 1px solid var(--border);
|
||
padding: 8px 20px;
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
gap: 15px;
|
||
font-family: monospace;
|
||
font-size: 12px;
|
||
color: var(--text-secondary);
|
||
z-index: 1000;
|
||
}
|
||
|
||
.fab-container {
|
||
position: fixed;
|
||
bottom: 50px;
|
||
right: 30px;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column-reverse;
|
||
align-items: flex-end;
|
||
gap: 10px;
|
||
}
|
||
|
||
.fab-btn {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
background: linear-gradient(135deg, var(--accent-blue), #2563eb);
|
||
color: white;
|
||
border: none;
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.fab-btn:hover {
|
||
transform: scale(1.05);
|
||
background: linear-gradient(135deg, #60a5fa, var(--accent-blue));
|
||
}
|
||
|
||
.fab-menu {
|
||
display: none;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
align-items: flex-end;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.fab-container:hover .fab-menu,
|
||
.fab-menu.active {
|
||
display: flex;
|
||
}
|
||
|
||
.fab-item {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
color: var(--text-primary);
|
||
padding: 10px 16px;
|
||
border-radius: 20px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||
transition: all 0.2s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.fab-item:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: var(--accent-blue);
|
||
transform: translateX(-5px);
|
||
}
|
||
|
||
#sidebar {
|
||
width: 320px;
|
||
min-width: 320px;
|
||
background: var(--bg-sidebar);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
z-index: 999;
|
||
}
|
||
|
||
#map {
|
||
flex: 1;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.sidebar-header {
|
||
padding: 20px 18px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
background: linear-gradient(135deg, #1e2a45 0%, #161b27 100%);
|
||
}
|
||
|
||
.sidebar-header h1 {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
letter-spacing: .3px;
|
||
}
|
||
|
||
.sidebar-header p {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.sidebar-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
padding: 14px 14px 6px;
|
||
}
|
||
|
||
.btn-add {
|
||
display: none;
|
||
/* Disembunyikan karena diganti FAB */
|
||
}
|
||
|
||
.item-list {
|
||
padding: 0 10px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.item-card {
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-sm);
|
||
border: 1px solid var(--border);
|
||
padding: 10px 12px;
|
||
cursor: pointer;
|
||
transition: all .18s;
|
||
position: relative;
|
||
}
|
||
|
||
.item-card:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: var(--accent-blue);
|
||
transform: translateX(2px);
|
||
}
|
||
|
||
.item-card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 6px;
|
||
}
|
||
|
||
.item-card-name {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
flex: 1;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 9px;
|
||
font-weight: 700;
|
||
padding: 2px 7px;
|
||
border-radius: 20px;
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
|
||
|
||
.item-card-info {
|
||
font-size: 10px;
|
||
color: var(--text-secondary);
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.item-card-actions {
|
||
display: flex;
|
||
gap: 5px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.btn-sm {
|
||
flex: 1;
|
||
padding: 5px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: 'Inter', sans-serif;
|
||
transition: opacity .2s;
|
||
}
|
||
|
||
.btn-sm:hover {
|
||
opacity: .8;
|
||
}
|
||
|
||
.btn-zoom {
|
||
background: rgba(59, 130, 246, .2);
|
||
color: #60a5fa;
|
||
border: 1px solid rgba(59, 130, 246, .3);
|
||
}
|
||
|
||
.btn-edit {
|
||
background: rgba(245, 158, 11, .2);
|
||
color: #fbbf24;
|
||
border: 1px solid rgba(245, 158, 11, .3);
|
||
}
|
||
|
||
.btn-delete {
|
||
background: rgba(239, 68, 68, .2);
|
||
color: #f87171;
|
||
border: 1px solid rgba(239, 68, 68, .3);
|
||
}
|
||
|
||
.legend-box {
|
||
padding: 12px 14px;
|
||
border-top: 1px solid var(--border);
|
||
background: var(--bg-dark);
|
||
}
|
||
|
||
.legend-title {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 1px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 5px;
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.layer-info-box {
|
||
margin: 10px 14px;
|
||
padding: 12px;
|
||
background: rgba(59, 130, 246, .08);
|
||
border: 1px solid rgba(59, 130, 246, .25);
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.layer-info-box p {
|
||
font-size: 11px;
|
||
color: var(--text-secondary);
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.layer-info-box strong {
|
||
color: var(--accent-blue);
|
||
}
|
||
|
||
.divider {
|
||
height: 1px;
|
||
background: var(--border);
|
||
margin: 8px 0;
|
||
}
|
||
|
||
.count-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 0 14px 10px;
|
||
}
|
||
|
||
.count-card {
|
||
flex: 1;
|
||
background: var(--bg-card);
|
||
border-radius: var(--radius-sm);
|
||
padding: 10px;
|
||
text-align: center;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.count-card .num {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.count-card .lbl {
|
||
font-size: 10px;
|
||
color: var(--text-secondary);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
|
||
|
||
#map {
|
||
flex: 1;
|
||
height: 100vh;
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
background: var(--bg-card) !important;
|
||
border: 1px solid var(--border) !important;
|
||
border-radius: var(--radius-md) !important;
|
||
box-shadow: var(--shadow) !important;
|
||
color: var(--text-primary) !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.leaflet-popup-tip {
|
||
background: var(--bg-card) !important;
|
||
}
|
||
|
||
.leaflet-popup-close-button {
|
||
color: var(--text-secondary) !important;
|
||
top: 8px !important;
|
||
right: 8px !important;
|
||
}
|
||
|
||
.leaflet-popup-content {
|
||
margin: 0 !important;
|
||
}
|
||
|
||
.gis-popup {
|
||
padding: 16px;
|
||
min-width: 240px;
|
||
max-width: 300px;
|
||
}
|
||
|
||
.popup-title {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.popup-form label {
|
||
display: block;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
margin-bottom: 4px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.popup-form label:first-of-type {
|
||
margin-top: 0;
|
||
}
|
||
|
||
.popup-form input,
|
||
.popup-form select {
|
||
width: 100%;
|
||
padding: 8px 10px;
|
||
background: var(--bg-dark);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-sm);
|
||
color: var(--text-primary);
|
||
font-size: 12px;
|
||
font-family: 'Inter', sans-serif;
|
||
outline: none;
|
||
transition: border-color .2s;
|
||
}
|
||
|
||
.popup-form input:focus,
|
||
.popup-form select:focus {
|
||
border-color: var(--accent-blue);
|
||
}
|
||
|
||
.popup-actions {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.popup-btn {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: none;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
font-family: 'Inter', sans-serif;
|
||
cursor: pointer;
|
||
transition: all .2s;
|
||
}
|
||
|
||
.popup-btn:hover {
|
||
opacity: .85;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.popup-btn-save {
|
||
background: var(--accent-green);
|
||
color: #fff;
|
||
}
|
||
|
||
.popup-btn-edit {
|
||
background: var(--accent-yellow);
|
||
color: #000;
|
||
}
|
||
|
||
.popup-btn-delete {
|
||
background: var(--accent-red);
|
||
color: #fff;
|
||
}
|
||
|
||
.popup-btn-cancel {
|
||
background: var(--bg-dark);
|
||
color: var(--text-secondary);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 5px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 11px;
|
||
}
|
||
|
||
.info-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.info-label {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.info-value {
|
||
color: var(--text-primary);
|
||
font-weight: 600;
|
||
}
|
||
|
||
#toast {
|
||
position: fixed;
|
||
bottom: 30px;
|
||
right: 30px;
|
||
z-index: 10000;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.toast-item {
|
||
padding: 10px 18px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #fff;
|
||
box-shadow: var(--shadow);
|
||
animation: slideIn .3s ease;
|
||
max-width: 280px;
|
||
}
|
||
|
||
.toast-success {
|
||
background: #065f46;
|
||
border: 1px solid #10b981;
|
||
}
|
||
|
||
.toast-error {
|
||
background: #7f1d1d;
|
||
border: 1px solid #ef4444;
|
||
}
|
||
|
||
.toast-info {
|
||
background: #1e3a5f;
|
||
border: 1px solid #3b82f6;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(30px)
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0)
|
||
}
|
||
}
|
||
|
||
::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
|
||
::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
|
||
::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.loading-text {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
}
|
||
|
||
/* Custom Leaflet Layer Control dark style */
|
||
.leaflet-control-layers {
|
||
background: var(--bg-card) !important;
|
||
border: 1px solid var(--border) !important;
|
||
border-radius: var(--radius-md) !important;
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.leaflet-control-layers label {
|
||
color: var(--text-primary) !important;
|
||
font-size: 12px !important;
|
||
font-family: 'Inter', sans-serif !important;
|
||
}
|
||
|
||
.leaflet-control-layers-separator {
|
||
border-top-color: var(--border) !important;
|
||
}
|
||
|
||
.leaflet-control-layers-expanded {
|
||
padding: 10px 14px !important;
|
||
}
|
||
|
||
.leaflet-control-layers-toggle {
|
||
background-color: var(--bg-card) !important;
|
||
border-radius: var(--radius-sm) !important;
|
||
}
|
||
|
||
.btn-add-kemiskinan {
|
||
background: linear-gradient(135deg, #f97316, #c2410c);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-add-masjid {
|
||
background: linear-gradient(135deg, #8b5cf6, #5b21b6);
|
||
color: #fff;
|
||
}
|
||
|
||
.radius-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
.radius-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
color: var(--text-secondary);
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
}
|
||
|
||
.radius-val {
|
||
color: var(--masjid);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.radius-slider {
|
||
width: 100%;
|
||
accent-color: var(--masjid);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.radius-ticks {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 9px;
|
||
color: var(--text-muted);
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<div id="header-bar">
|
||
<div class="header-title">
|
||
<span style="font-size: 20px;">🏚️</span>
|
||
<h1>Website Pendata Kemiskinan</h1>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:12px;">
|
||
<div class="status-indicator">
|
||
<div class="pulse-dot"></div>
|
||
Sistem Online
|
||
</div>
|
||
<button onclick="aktifkanModeLapor()" class="btn-lapor-header"
|
||
style="border:1px solid rgba(239,68,68,.35);background:rgba(239,68,68,.15);">📢 Laporkan Warga
|
||
Miskin</button>
|
||
|
||
<!-- ADMIN ONLY: Riwayat Laporan Button -->
|
||
<button id="btn-riwayat-laporan" onclick="bukaRiwayatLaporan()"
|
||
style="display:none; position:relative; padding:10px 15px; border-radius:8px; border:1px solid rgba(255,255,255,0.1); background:rgba(0,0,0,0.3); color:white; cursor:pointer; font-weight:600;">
|
||
📋 Riwayat Laporan
|
||
<span id="notif-badge"
|
||
style="display:none; position:absolute; top:-5px; right:-5px; background:#ef4444; color:white; font-size:10px; font-weight:bold; padding:2px 6px; border-radius:10px; border:2px solid var(--bg-dark);">0</span>
|
||
</button>
|
||
<!-- User badge (tampil jika sudah login) -->
|
||
<div class="user-badge" id="user-badge" style="display:none;">
|
||
<div class="user-info">
|
||
<span class="user-name" id="user-name-display">—</span>
|
||
<span class="role-badge" id="user-role-badge">—</span>
|
||
</div>
|
||
<button class="btn-logout" onclick="logout()">🚪 Keluar</button>
|
||
</div>
|
||
<!-- Tombol login (tampil jika belum login) -->
|
||
<a href="login.html" class="btn-login" id="btn-header-login">🔐 Masuk</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="app-container">
|
||
<div id="sidebar">
|
||
<div class="sidebar-header">
|
||
<h1>🏚️ Sidebar Panel</h1>
|
||
<p>Filter & Ringkasan Statistik</p>
|
||
</div>
|
||
<div class="sidebar-body">
|
||
<div class="layer-info-box">
|
||
<p>💡 Gunakan <strong>Layers Control</strong> di pojok kanan atas peta untuk
|
||
menampilkan/menyembunyikan
|
||
layer berdasarkan jenis datanya.</p>
|
||
</div>
|
||
<div class="divider"></div>
|
||
<div class="section-title" style="color:var(--kemiskinan)">🏚️ Data Kemiskinan</div>
|
||
<div class="count-row" style="flex-direction:column; gap:6px;">
|
||
<div class="count-card"
|
||
style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid #ef4444; padding:8px 12px">
|
||
<div style="text-align:left"><strong style="color:#ef4444; font-size:12px;">🚨 Sangat
|
||
Miskin</strong>
|
||
<div class="lbl" id="stats-sm-detail" style="font-size:9px">Sudah: 0 | Belum: 0</div>
|
||
</div>
|
||
<div class="num" id="cnt-sm" style="font-size:16px; color:var(--text-primary)">0</div>
|
||
</div>
|
||
<div class="count-card"
|
||
style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid #f97316; padding:8px 12px">
|
||
<div style="text-align:left"><strong style="color:#f97316; font-size:12px;">⚠️ Miskin</strong>
|
||
<div class="lbl" id="stats-m-detail" style="font-size:9px">Sudah: 0 | Belum: 0</div>
|
||
</div>
|
||
<div class="num" id="cnt-m" style="font-size:16px; color:var(--text-primary)">0</div>
|
||
</div>
|
||
<div class="count-card"
|
||
style="display:flex; justify-content:space-between; align-items:center; border-left:4px solid #eab308; padding:8px 12px">
|
||
<div style="text-align:left"><strong style="color:#eab308; font-size:12px;">💡 Hampir
|
||
Miskin</strong>
|
||
<div class="lbl" id="stats-hm-detail" style="font-size:9px">Sudah: 0 | Belum: 0</div>
|
||
</div>
|
||
<div class="num" id="cnt-hm" style="font-size:16px; color:var(--text-primary)">0</div>
|
||
</div>
|
||
<div class="count-card" style="margin-top:4px; padding:6px">
|
||
<div class="num" id="cnt-kemiskinan" style="color:var(--text-primary); font-size:14px">0</div>
|
||
<div class="lbl">Total Keseluruhan</div>
|
||
</div>
|
||
</div>
|
||
<div class="item-list" id="list-kemiskinan">
|
||
<div class="loading-text">⏳ Memuat data...</div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
<div class="section-title" style="color:var(--masjid)">🕌 Rumah Ibadah</div>
|
||
<div class="count-row">
|
||
<div class="count-card">
|
||
<div class="num" id="cnt-masjid" style="color:var(--masjid)">0</div>
|
||
<div class="lbl">Total Masjid</div>
|
||
</div>
|
||
</div>
|
||
<div class="item-list" id="list-masjid">
|
||
<div class="loading-text">⏳ Memuat data...</div>
|
||
</div>
|
||
|
||
<div class="divider"></div>
|
||
<button class="btn-export" id="btn-export-csv" onclick="exportCSV()">📥 Export Data CSV (Excel)</button>
|
||
|
||
<div class="legend-box">
|
||
<div class="legend-title">Legenda Layer</div>
|
||
<div class="legend-item">
|
||
<div
|
||
style="width:14px;height:14px;border-radius:50%;background:var(--kemiskinan);flex-shrink:0">
|
||
</div>
|
||
Penduduk Miskin
|
||
</div>
|
||
<div class="legend-item">
|
||
<div style="width:14px;height:14px;border-radius:50%;background:var(--masjid);flex-shrink:0">
|
||
</div>
|
||
Rumah Ibadah + Radius Buffer
|
||
</div>
|
||
<div class="legend-item">
|
||
<div style="width:14px;height:14px;border-radius:50%;background:#ef4444;flex-shrink:0"></div>
|
||
Laporan Masyarakat
|
||
</div>
|
||
<div class="legend-item">
|
||
<div
|
||
style="width:14px;height:14px;border-radius:50%;background:linear-gradient(135deg,#f97316,#ef4444);flex-shrink:0">
|
||
</div>
|
||
Heatmap Kemiskinan
|
||
</div>
|
||
</div>
|
||
</div> <!-- End sidebar-body -->
|
||
</div> <!-- End sidebar -->
|
||
<div id="map"></div>
|
||
</div> <!-- End app-container -->
|
||
|
||
<!-- Modal Riwayat Bantuan -->
|
||
<div id="modal-bantuan"
|
||
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
|
||
<div
|
||
style="background:var(--bg-card); width:400px; max-width:90%; border-radius:12px; border:1px solid var(--border); box-shadow:0 10px 30px rgba(0,0,0,0.5); overflow:hidden;">
|
||
<div
|
||
style="padding:16px 20px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;">
|
||
<h3 style="margin:0; font-size:16px;" id="modal-bantuan-title">Riwayat Bantuan</h3>
|
||
<button onclick="document.getElementById('modal-bantuan').style.display='none'"
|
||
style="background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer;">×</button>
|
||
</div>
|
||
<div style="padding:20px; max-height:300px; overflow-y:auto;" id="modal-bantuan-list">
|
||
<div class="loading-text">Memuat histori...</div>
|
||
</div>
|
||
<div style="padding:16px 20px; border-top:1px solid var(--border); background:var(--bg-dark);"
|
||
id="form-bantuan-container">
|
||
<h4 style="margin:0 0 10px 0; font-size:13px; color:var(--text-secondary)">+ Tambah Riwayat Bantuan</h4>
|
||
<input type="hidden" id="bantuan-id-kemiskinan">
|
||
<input type="text" id="bantuan-pemberi" placeholder="Nama Pemberi (Mewakili)"
|
||
style="width:100%; margin-bottom:8px; padding:8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-card); color:white;">
|
||
<input type="text" id="bantuan-jenis" placeholder="Jenis Bantuan (Misal: Sembako)"
|
||
style="width:100%; margin-bottom:8px; padding:8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-card); color:white;">
|
||
<input type="date" id="bantuan-tanggal"
|
||
style="width:100%; margin-bottom:8px; padding:8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-card); color:white;"
|
||
value="">
|
||
<input type="text" id="bantuan-keterangan" placeholder="Keterangan tambahan (opsional)"
|
||
style="width:100%; margin-bottom:12px; padding:8px; border-radius:6px; border:1px solid var(--border); background:var(--bg-card); color:white;">
|
||
<button onclick="simpanRiwayatBantuan()"
|
||
style="width:100%; padding:10px; background:var(--kemiskinan); color:white; border:none; border-radius:6px; font-weight:bold; cursor:pointer;">Simpan
|
||
Data Bantuan</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Modal Riwayat Laporan -->
|
||
<div id="modal-riwayat-laporan"
|
||
style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.6); z-index:9999; align-items:center; justify-content:center;">
|
||
<div
|
||
style="background:var(--bg-card); width:600px; max-width:95%; border-radius:12px; border:1px solid var(--border); box-shadow:0 10px 30px rgba(0,0,0,0.5); overflow:hidden; display:flex; flex-direction:column; max-height:80vh;">
|
||
<div
|
||
style="padding:16px 20px; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center;">
|
||
<h3 style="margin:0; font-size:16px;">📋 Riwayat Laporan Masyarakat</h3>
|
||
<button onclick="document.getElementById('modal-riwayat-laporan').style.display='none'"
|
||
style="background:none;border:none;color:var(--text-muted);font-size:20px;cursor:pointer;">×</button>
|
||
</div>
|
||
<div style="padding:20px; overflow-y:auto; flex:1;" id="riwayat-laporan-list">
|
||
<div class="loading-text">Memuat laporan...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="bottom-bar">
|
||
<span>Total Aktif:</span>
|
||
<span id="bb-kemiskinan">Kemiskinan: 0</span> |
|
||
<span id="bb-masjid">Rumah Ibadah: 0</span>
|
||
</div>
|
||
|
||
<div class="fab-container" id="fab-container" style="display:none;">
|
||
<div class="fab-menu">
|
||
<div class="fab-item" style="color:#ef4444" onclick="aktifkanModeLapor()" id="fab-lapor">📢 Laporkan Warga
|
||
Miskin</div>
|
||
<div class="fab-item" style="color:var(--kemiskinan)" onclick="aktifkanModeKemiskinan()"
|
||
id="fab-kemiskinan">🏚️ Tambah Kemiskinan</div>
|
||
<div class="fab-item" style="color:var(--masjid)" onclick="aktifkanModeMasjid()" id="fab-masjid">🕌 Tambah
|
||
Rumah Ibadah</div>
|
||
</div>
|
||
<button class="fab-btn">
|
||
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"
|
||
stroke-linecap="round" stroke-linejoin="round">
|
||
<line x1="12" y1="5" x2="12" y2="19"></line>
|
||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
<div id="toast"></div>
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||
<script src="https://unpkg.com/leaflet.heat@0.2.0/dist/leaflet-heat.js"></script>
|
||
<script>
|
||
// ============================================================
|
||
// AUTENTIKASI & ROLE-BASED ACCESS CONTROL
|
||
// ============================================================
|
||
// Akses publik: peta bisa dilihat tanpa login
|
||
// Login hanya diperlukan untuk fitur CRUD
|
||
|
||
// Baca data user dari session (bisa null jika publik)
|
||
const currentUser = JSON.parse(sessionStorage.getItem('webgis_user') || 'null');
|
||
const userRole = currentUser ? currentUser.role : null;
|
||
const isLoggedIn = !!currentUser;
|
||
|
||
// Logout
|
||
function logout() {
|
||
if (!confirm('Yakin ingin keluar dari sistem?')) return;
|
||
sessionStorage.removeItem('webgis_user');
|
||
window.location.replace('login.html?logout=1');
|
||
}
|
||
|
||
// ── Tampilkan info user di header ──
|
||
function tampilkanUserInfo() {
|
||
const userBadge = document.getElementById('user-badge');
|
||
const loginBtn = document.getElementById('btn-header-login');
|
||
|
||
if (isLoggedIn) {
|
||
// Tampilkan user badge, sembunyikan tombol login
|
||
if (userBadge) userBadge.style.display = 'flex';
|
||
if (loginBtn) loginBtn.style.display = 'none';
|
||
|
||
const nameEl = document.getElementById('user-name-display');
|
||
const badgeEl = document.getElementById('user-role-badge');
|
||
if (nameEl) nameEl.textContent = currentUser.nama || currentUser.username || 'Pengguna';
|
||
if (badgeEl) {
|
||
const roleLabels = { admin: '🛡️ Admin', pengurus: '🕌 Pengurus Ibadah', walikota: '👑 Walikota' };
|
||
const roleCls = { admin: 'role-badge-admin', pengurus: 'role-badge-pengurus', walikota: 'role-badge-walikota' };
|
||
badgeEl.textContent = roleLabels[userRole] || userRole;
|
||
badgeEl.className = 'role-badge ' + (roleCls[userRole] || '');
|
||
}
|
||
} else {
|
||
// Publik: sembunyikan user badge, tampilkan tombol login
|
||
if (userBadge) userBadge.style.display = 'none';
|
||
if (loginBtn) loginBtn.style.display = 'inline-flex';
|
||
}
|
||
}
|
||
|
||
// ── Kontrol visibilitas elemen berdasarkan role ──
|
||
function terapkanRoleUI() {
|
||
const fabContainer = document.getElementById('fab-container');
|
||
const fabKemiskin = document.getElementById('fab-kemiskinan');
|
||
const fabMasjid = document.getElementById('fab-masjid');
|
||
const exportBtn = document.getElementById('btn-export-csv');
|
||
const fabLapor = document.getElementById('fab-lapor');
|
||
const btnLaporHeader = document.querySelector('.btn-lapor-header');
|
||
|
||
if (!isLoggedIn) {
|
||
// Publik: sembunyikan FAB CRUD (tapi link lapor tetap bisa dari header)
|
||
if (fabContainer) fabContainer.style.display = 'none';
|
||
if (btnLaporHeader) btnLaporHeader.style.display = 'inline-block';
|
||
return;
|
||
}
|
||
|
||
// Tampilkan FAB jika sudah login
|
||
if (fabContainer) fabContainer.style.display = 'flex';
|
||
|
||
if (userRole === 'admin') {
|
||
// Admin: semua fitur aktif + export + riwayat laporan
|
||
if (exportBtn) exportBtn.style.display = 'block';
|
||
const btnRiwayat = document.getElementById('btn-riwayat-laporan');
|
||
if (btnRiwayat) btnRiwayat.style.display = 'block';
|
||
|
||
// Hilangkan fitur lapor untuk admin (karena sudah ada fitur tambah kemiskinan)
|
||
if (fabLapor) fabLapor.style.display = 'none';
|
||
if (btnLaporHeader) btnLaporHeader.style.display = 'none';
|
||
} else if (userRole === 'pengurus') {
|
||
// Pengurus: hanya rumah ibadah + bantuan
|
||
if (fabKemiskin) fabKemiskin.style.display = 'none';
|
||
} else if (userRole === 'walikota') {
|
||
// Walikota: redirect ke dashboard khusus
|
||
window.location.replace('dashboard_walikota.html');
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Helper: cek apakah user boleh edit/hapus data tertentu
|
||
window.canEdit = function (dataType) {
|
||
if (!isLoggedIn) return false;
|
||
if (userRole === 'admin') return true;
|
||
if (userRole === 'pengurus' && dataType === 'masjid') return true;
|
||
return false;
|
||
};
|
||
|
||
// Helper: cek apakah user boleh tinjau laporan
|
||
window.canReviewLaporan = function () {
|
||
return isLoggedIn && userRole === 'admin';
|
||
};
|
||
|
||
// Konstanta API
|
||
const API_KEMISKINAN = 'api_kemiskinan.php';
|
||
const API_MASJID = 'api_masjid.php';
|
||
const API_BANTUAN = 'api_bantuan.php';
|
||
|
||
// ============================================================
|
||
// INISIALISASI PETA
|
||
// ============================================================
|
||
const map = L.map('map').setView([-0.0553, 109.3495], 13);
|
||
|
||
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors', maxZoom: 19
|
||
});
|
||
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||
attribution: 'Tiles © Esri', maxZoom: 19
|
||
});
|
||
|
||
osmLayer.addTo(map);
|
||
|
||
// ============================================================
|
||
// LAYER GROUPS
|
||
// ============================================================
|
||
const layerGroupKemiskinan = L.layerGroup(); // Kemiskinan
|
||
const layerGroupMasjid = L.layerGroup(); // Masjid + buffer
|
||
const layerGroupLaporan = L.layerGroup(); // Laporan Masyarakat
|
||
|
||
layerGroupKemiskinan.addTo(map);
|
||
layerGroupMasjid.addTo(map);
|
||
layerGroupLaporan.addTo(map);
|
||
|
||
// ============================================================
|
||
// BASE MAPS & OVERLAY MAPS untuk L.control.layers
|
||
// ============================================================
|
||
const baseMaps = {
|
||
'🗺️ OpenStreetMap': osmLayer,
|
||
'🛰️ Citra Satelit': satelliteLayer
|
||
};
|
||
|
||
// Heatmap layer
|
||
let heatLayer = null;
|
||
|
||
const overlayMaps = {
|
||
"<span style='color:#f97316;font-weight:700'>🏚️ Kemiskinan</span>": layerGroupKemiskinan,
|
||
"<span style='color:#8b5cf6;font-weight:700'>🕌 Rumah Ibadah</span>": layerGroupMasjid,
|
||
"<span style='color:#ef4444;font-weight:700'>📢 Laporan Warga</span>": layerGroupLaporan
|
||
};
|
||
|
||
const layerControl = L.control.layers(baseMaps, overlayMaps, {
|
||
collapsed: false,
|
||
position: 'topright'
|
||
}).addTo(map);
|
||
|
||
// ============================================================
|
||
// STORAGE MARKER
|
||
// ============================================================
|
||
const kemiskinanMarkers = {}; // { id: marker }
|
||
const masjidMarkers = {}; // { id: marker }
|
||
const masjidCircles = {}; // { id: L.circle }
|
||
const laporanMarkers = {}; // { id: marker }
|
||
let modeCurrent = null; // null | 'kemiskinan' | 'masjid'
|
||
|
||
// ============================================================
|
||
// LAPORAN MASYARAKAT
|
||
// ============================================================
|
||
const API_LAPOR = 'api_lapor.php';
|
||
|
||
const ikonLaporMarker = L.divIcon({
|
||
className: '',
|
||
html: `<div style="width:28px;height:28px;border-radius:50% 50% 50% 0;background:#ef4444;transform:rotate(-45deg);border:2px solid #7f1d1d;box-shadow:0 3px 10px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;"><span style="transform:rotate(45deg);font-size:12px;line-height:1">📢</span></div>`,
|
||
iconSize: [28, 28], iconAnchor: [14, 28]
|
||
});
|
||
|
||
function popupInfoLaporan(d) {
|
||
const statusColors = { 'Pending': '#eab308', 'Diproses': '#3b82f6', 'Diverifikasi': '#10b981', 'Ditolak': '#ef4444', 'Selesai': '#10b981' };
|
||
const sc = statusColors[d.status] || '#eab308';
|
||
const scText = d.status === 'Pending' ? '#000' : '#fff';
|
||
let badgeStatus = `<span style="background:${sc};color:${scText};padding:2px 6px;border-radius:4px;font-size:11px;font-weight:bold">${d.status}</span>`;
|
||
|
||
let btnAction = '';
|
||
if (canReviewLaporan() && d.status === 'Pending') {
|
||
btnAction = `
|
||
<div style="margin-top:10px; border-top:1px solid var(--border); padding-top:10px; display:flex; flex-direction:column; gap:5px;">
|
||
<button class="popup-btn popup-btn-save" onclick="updateStatusLaporan(${d.id}, 'Diproses')" style="width:100%">🔍 Tinjau / Proses</button>
|
||
<button class="popup-btn" style="width:100%;background:#10b981;color:#fff" onclick="verifikasiLaporan(${d.id})">✅ Verifikasi & Jadikan Data Resmi</button>
|
||
<button class="popup-btn popup-btn-delete" onclick="tolakLaporan(${d.id})" style="width:100%">❌ Tolak Laporan</button>
|
||
</div>`;
|
||
} else if (canReviewLaporan() && d.status === 'Diproses') {
|
||
btnAction = `
|
||
<div style="margin-top:10px; border-top:1px solid var(--border); padding-top:10px; display:flex; flex-direction:column; gap:5px;">
|
||
<button class="popup-btn" style="width:100%;background:#10b981;color:#fff" onclick="verifikasiLaporan(${d.id})">✅ Verifikasi & Jadikan Data Resmi</button>
|
||
<button class="popup-btn popup-btn-delete" onclick="tolakLaporan(${d.id})" style="width:100%">❌ Tolak Laporan</button>
|
||
</div>`;
|
||
}
|
||
|
||
return `
|
||
<div class="gis-popup">
|
||
<div class="popup-title" style="color:var(--accent-red)">📢 Laporan Masyarakat</div>
|
||
<div class="info-row"><span class="info-label">Nama/Pelapor</span><span class="info-value">${d.nama_pelapor}</span></div>
|
||
${d.nik ? `<div class="info-row"><span class="info-label">NIK</span><span class="info-value">${d.nik}</span></div>` : ''}
|
||
${d.no_wa ? `<div class="info-row"><span class="info-label">WhatsApp</span><span class="info-value">${d.no_wa}</span></div>` : ''}
|
||
<div class="info-row"><span class="info-label">Status</span><span class="info-value">${badgeStatus}</span></div>
|
||
|
||
<!-- Data Detail -->
|
||
<div style="margin:10px 0; padding:10px; background:rgba(0,0,0,0.2); border-radius:6px; font-size:10px;">
|
||
<div style="margin-bottom:4px; font-weight:bold; color:var(--text-secondary)">📝 Data Terlapor</div>
|
||
<div style="display:flex;justify-content:space-between"><span>Jumlah KK:</span> <span>${d.jumlah_kk || '-'} jiwa</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Tgl Lahir:</span> <span>${d.tanggal_lahir || '-'}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Pendidikan:</span> <span>${d.pendidikan || '-'}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Penyakit:</span> <span>${d.riwayat_penyakit || '-'}</span></div>
|
||
<div style="display:flex;justify-content:space-between;margin-top:4px;border-top:1px solid var(--border);padding-top:4px">
|
||
<span>Penghasilan (Skor):</span> <span style="color:var(--accent-yellow)">${d.skor_penghasilan || '-'}</span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between"><span>Rumah (Skor):</span> <span style="color:var(--accent-yellow)">${d.skor_rumah || '-'}</span></div>
|
||
<div style="display:flex;justify-content:space-between"><span>Pangan (Skor):</span> <span style="color:var(--accent-yellow)">${d.skor_makan || '-'}</span></div>
|
||
</div>
|
||
|
||
<div class="info-row"><span class="info-label">Alamat / Keterangan</span><span class="info-value" style="font-size:11px; font-style:italic;">"${d.deskripsi}"</span></div>
|
||
<div class="info-row"><span class="info-label">Tanggal</span><span class="info-value">${d.created_at}</span></div>
|
||
${btnAction}
|
||
</div>`;
|
||
}
|
||
|
||
function renderLaporan(d) {
|
||
if (laporanMarkers[d.id]) {
|
||
layerGroupLaporan.removeLayer(laporanMarkers[d.id]);
|
||
}
|
||
// Sembunyikan laporan Selesai dari map agar tidak menumpuk
|
||
if (d.status === 'Selesai') return;
|
||
|
||
const m = L.marker([d.latitude, d.longitude], { icon: ikonLaporMarker })
|
||
.bindPopup(popupInfoLaporan(d));
|
||
layerGroupLaporan.addLayer(m);
|
||
laporanMarkers[d.id] = m;
|
||
}
|
||
|
||
window.updateStatusLaporan = function (id, statusBaru) {
|
||
fetch(`${API_LAPOR}?id=${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status: statusBaru })
|
||
}).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
showToast(`✅ Status laporan diubah menjadi ${statusBaru}`);
|
||
loadDataLaporan();
|
||
});
|
||
};
|
||
|
||
// Verifikasi laporan → jadikan data kemiskinan resmi
|
||
window.verifikasiLaporan = function (id) {
|
||
if (!confirm('Verifikasi laporan ini dan masukkan ke data kemiskinan resmi?')) return;
|
||
fetch(`${API_LAPOR}?id=${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status: 'Diverifikasi', action: 'verify' })
|
||
}).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
showToast('✅ Laporan diverifikasi! Data telah masuk ke peta kemiskinan resmi.');
|
||
loadDataLaporan();
|
||
loadDataKemiskinan(); // Reload data kemiskinan
|
||
});
|
||
};
|
||
|
||
// Tolak laporan
|
||
window.tolakLaporan = function (id) {
|
||
const alasan = prompt('Alasan penolakan (opsional):');
|
||
if (alasan === null) return; // user cancel
|
||
fetch(`${API_LAPOR}?id=${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ status: 'Ditolak', alasan_tolak: alasan })
|
||
}).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
showToast('Laporan ditolak.');
|
||
loadDataLaporan();
|
||
});
|
||
};
|
||
|
||
window.laporanDataCache = []; // Global cache for laporan
|
||
|
||
function loadDataLaporan() {
|
||
fetch(API_LAPOR).then(r => r.json()).then(data => {
|
||
layerGroupLaporan.clearLayers();
|
||
for (let key in laporanMarkers) delete laporanMarkers[key];
|
||
|
||
window.laporanDataCache = data;
|
||
|
||
let pendingCount = 0;
|
||
if (data.length) {
|
||
data.forEach(d => {
|
||
renderLaporan(d);
|
||
if (d.status === 'Pending') pendingCount++;
|
||
});
|
||
}
|
||
|
||
// Update badge notif
|
||
const badge = document.getElementById('notif-badge');
|
||
if (badge) {
|
||
if (pendingCount > 0) {
|
||
badge.style.display = 'inline-block';
|
||
badge.textContent = pendingCount;
|
||
} else {
|
||
badge.style.display = 'none';
|
||
}
|
||
}
|
||
}).catch(() => console.error("Gagal memuat laporan"));
|
||
}
|
||
|
||
window.bukaRiwayatLaporan = function () {
|
||
document.getElementById('modal-riwayat-laporan').style.display = 'flex';
|
||
const listEl = document.getElementById('riwayat-laporan-list');
|
||
listEl.innerHTML = '';
|
||
|
||
if (!window.laporanDataCache || window.laporanDataCache.length === 0) {
|
||
listEl.innerHTML = '<div style="text-align:center;color:var(--text-muted);font-style:italic;">Belum ada laporan masuk.</div>';
|
||
return;
|
||
}
|
||
|
||
const statusColors = { 'Pending': '#eab308', 'Diproses': '#3b82f6', 'Diverifikasi': '#10b981', 'Ditolak': '#ef4444', 'Selesai': '#10b981' };
|
||
|
||
window.laporanDataCache.forEach(d => {
|
||
const sc = statusColors[d.status] || '#eab308';
|
||
const scText = d.status === 'Pending' ? '#000' : '#fff';
|
||
let badgeStatus = `<span style="background:${sc};color:${scText};padding:3px 8px;border-radius:12px;font-size:10px;font-weight:bold">${d.status}</span>`;
|
||
|
||
listEl.innerHTML += `
|
||
<div style="background:rgba(255,255,255,0.03); border:1px solid var(--border); border-radius:8px; padding:12px; margin-bottom:10px; display:flex; justify-content:space-between; align-items:flex-start;">
|
||
<div>
|
||
<div style="font-weight:bold; margin-bottom:4px; font-size:14px; color:var(--text-primary)">Laporan: ${d.nama_pelapor}</div>
|
||
<div style="font-size:11px; color:var(--text-muted); margin-bottom:6px;">Tanggal: ${d.created_at}</div>
|
||
<div style="font-size:12px; color:var(--text-secondary)">Keterangan: ${d.deskripsi}</div>
|
||
</div>
|
||
<div style="display:flex; flex-direction:column; align-items:flex-end; gap:8px;">
|
||
${badgeStatus}
|
||
<button onclick="document.getElementById('modal-riwayat-laporan').style.display='none'; map.setView([${d.latitude}, ${d.longitude}], 18); if(laporanMarkers[${d.id}]) laporanMarkers[${d.id}].openPopup();" style="background:var(--blue); color:white; border:none; border-radius:6px; padding:6px 12px; font-size:11px; cursor:pointer; font-weight:600;">📍 Lihat di Peta</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
};
|
||
|
||
|
||
// ============================================================
|
||
// API BANTUAN
|
||
// ============================================================
|
||
function showToast(msg, type = 'success') {
|
||
const t = document.getElementById('toast');
|
||
const el = document.createElement('div');
|
||
el.className = `toast-item toast-${type}`;
|
||
el.textContent = msg;
|
||
t.appendChild(el);
|
||
setTimeout(() => el.remove(), 3500);
|
||
}
|
||
|
||
function updateCounts() {
|
||
const listKemiskinan = Object.values(window.dataKemiskinanCache || {});
|
||
const kemiskinan = listKemiskinan.length;
|
||
const masjid = Math.floor(layerGroupMasjid.getLayers().length / 2);
|
||
|
||
let stats = {
|
||
'Sangat Miskin': { total: 0, sudah: 0, belum: 0 },
|
||
'Miskin': { total: 0, sudah: 0, belum: 0 },
|
||
'Hampir Miskin': { total: 0, sudah: 0, belum: 0 }
|
||
};
|
||
|
||
listKemiskinan.forEach(d => {
|
||
let kat = d.kategori || 'Miskin';
|
||
if (!stats[kat]) kat = 'Miskin';
|
||
stats[kat].total++;
|
||
if (d.status_bantuan === 'Sudah') stats[kat].sudah++;
|
||
else stats[kat].belum++;
|
||
});
|
||
|
||
if (document.getElementById('cnt-kemiskinan')) {
|
||
document.getElementById('cnt-kemiskinan').textContent = kemiskinan;
|
||
|
||
document.getElementById('cnt-sm').textContent = stats['Sangat Miskin'].total;
|
||
document.getElementById('stats-sm-detail').textContent = `Sudah: ${stats['Sangat Miskin'].sudah} | Belum: ${stats['Sangat Miskin'].belum}`;
|
||
|
||
document.getElementById('cnt-m').textContent = stats['Miskin'].total;
|
||
document.getElementById('stats-m-detail').textContent = `Sudah: ${stats['Miskin'].sudah} | Belum: ${stats['Miskin'].belum}`;
|
||
|
||
document.getElementById('cnt-hm').textContent = stats['Hampir Miskin'].total;
|
||
document.getElementById('stats-hm-detail').textContent = `Sudah: ${stats['Hampir Miskin'].sudah} | Belum: ${stats['Hampir Miskin'].belum}`;
|
||
}
|
||
if (document.getElementById('cnt-masjid')) document.getElementById('cnt-masjid').textContent = masjid;
|
||
|
||
document.getElementById('bb-kemiskinan').textContent = `Kemiskinan: ${kemiskinan}`;
|
||
document.getElementById('bb-masjid').textContent = `Rumah Ibadah: ${masjid}`;
|
||
}
|
||
|
||
// ============================================================
|
||
// IKON MARKER
|
||
// ============================================================
|
||
|
||
// ============================================================
|
||
// MODE TAMBAH (multi-mode dengan FAB)
|
||
// ============================================================
|
||
function resetAllModeBtns() {
|
||
document.querySelectorAll('.fab-item').forEach(el => el.style.border = '1px solid var(--border)');
|
||
}
|
||
|
||
window.aktifkanModeKemiskinan = function () {
|
||
modeCurrent = 'kemiskinan';
|
||
resetAllModeBtns();
|
||
document.getElementById('fab-kemiskinan').style.border = '2px solid #ea580c';
|
||
document.querySelector('.fab-menu').classList.remove('active');
|
||
showToast('📍 Klik lokasi di peta untuk menempatkan titik kemiskinan.', 'info');
|
||
};
|
||
|
||
window.aktifkanModeMasjid = function () {
|
||
modeCurrent = 'masjid';
|
||
resetAllModeBtns();
|
||
document.getElementById('fab-masjid').style.border = '2px solid #8b5cf6';
|
||
document.querySelector('.fab-menu').classList.remove('active');
|
||
showToast('📍 Klik lokasi di peta untuk menempatkan rumah ibadah baru.', 'info');
|
||
};
|
||
|
||
window.aktifkanModeLapor = function () {
|
||
modeCurrent = 'lapor';
|
||
resetAllModeBtns();
|
||
const fab = document.getElementById('fab-lapor');
|
||
if (fab) fab.style.border = '2px solid #ef4444';
|
||
document.querySelector('.fab-menu').classList.remove('active');
|
||
showToast('📍 Klik lokasi di peta untuk menempatkan titik laporan.', 'info');
|
||
};
|
||
|
||
map.on('click', function (e) {
|
||
if (!modeCurrent) return;
|
||
const mode = modeCurrent;
|
||
modeCurrent = null;
|
||
resetAllModeBtns();
|
||
if (mode === 'kemiskinan') tampilkanFormTambahKemiskinan(e.latlng.lat, e.latlng.lng);
|
||
if (mode === 'masjid') tampilkanFormTambahMasjid(e.latlng.lat, e.latlng.lng);
|
||
if (mode === 'lapor') tampilkanFormLapor(e.latlng.lat, e.latlng.lng);
|
||
});
|
||
|
||
// ============================================================
|
||
// FUNGSI LAPOR (MODAL DI PETA)
|
||
// ============================================================
|
||
function tampilkanFormLapor(lat, lng) {
|
||
const html = `<div class="gis-popup" style="max-height:450px;overflow-y:auto">
|
||
<div class="popup-title" style="color:var(--accent-red)">📢 Lapor Warga Miskin</div>
|
||
<div class="popup-form">
|
||
<div style="background:rgba(239,68,68,0.1); border:1px solid rgba(239,68,68,0.3); padding:8px; border-radius:6px; font-size:10px; color:#fca5a5; margin-bottom:12px;">
|
||
Mohon isi data selengkap mungkin agar mempermudah admin dalam menetapkan prioritas.
|
||
</div>
|
||
|
||
<label>Nama Kepala Keluarga (Wajib)</label>
|
||
<input type="text" id="add-lp-nama" placeholder="Cth: Pak Budi">
|
||
<label>NIK (16 Digit)</label>
|
||
<input type="text" id="add-lp-nik" placeholder="16 Digit NIK" maxlength="16">
|
||
<label>No. WhatsApp Pelapor (Opsional)</label>
|
||
<input type="text" id="add-lp-wa" placeholder="08xxxxxxxx">
|
||
|
||
<div style="display:flex; gap:10px; margin-top:8px">
|
||
<div style="flex:1">
|
||
<label>Tanggal Lahir</label>
|
||
<input type="date" id="add-lp-tgllahir">
|
||
</div>
|
||
<div style="flex:1">
|
||
<label>Pendidikan</label>
|
||
<select id="add-lp-pendidikan">
|
||
<option value="">- Pilih -</option>
|
||
<option value="Tidak Sekolah">Tidak Sekolah</option>
|
||
<option value="SD">SD</option>
|
||
<option value="SMP">SMP</option>
|
||
<option value="SMA">SMA</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<label>Riwayat Penyakit (Opsional)</label>
|
||
<input type="text" id="add-lp-penyakit" placeholder="Cth: Diabetes, Hipertensi">
|
||
|
||
<label style="color:var(--accent-yellow); margin-top:8px;">1. Penghasilan KK</label>
|
||
<select id="add-lp-skor_penghasilan">
|
||
<option value="3">< Rp 500rb / Tidak Tetap (Skor Tinggi)</option>
|
||
<option value="2">Rp 500rb - 1.5 Jt (Skor Sedang)</option>
|
||
<option value="1">> Rp 1.5 Jt (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label style="color:var(--accent-yellow)">2. Kondisi Rumah</label>
|
||
<select id="add-lp-skor_rumah">
|
||
<option value="3">Lantai Tanah / Dinding Bambu (Skor Tinggi)</option>
|
||
<option value="2">Semen Kasar / Papan (Skor Sedang)</option>
|
||
<option value="1">Keramik / Tembok Plester (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label style="color:var(--accent-yellow)">3. Kondisi Pangan</label>
|
||
<select id="add-lp-skor_makan">
|
||
<option value="3">Makan 1-2x Sehari (Skor Tinggi)</option>
|
||
<option value="2">Cukup 3x tapi gizi seadanya (Skor Sedang)</option>
|
||
<option value="1">Kebutuhan makan selalu terpenuhi (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label>Jumlah Anggota KK</label>
|
||
<input type="number" id="add-lp-jml" min="1" value="1">
|
||
<label>Alamat / Keterangan Kondisi (Wajib)</label>
|
||
<input type="text" id="add-lp-alamat" placeholder="Alamat & Kondisi secara ringkas">
|
||
<label>Koordinat</label>
|
||
<input type="text" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
|
||
</div>
|
||
<div class="popup-actions">
|
||
<button class="popup-btn popup-btn-save" onclick="simpanLaporan(${lat},${lng})" style="background:#ef4444">🚀 Kirim Laporan</button>
|
||
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
|
||
</div></div>`;
|
||
L.popup({ closeButton: true }).setLatLng([lat, lng]).setContent(html).openOn(map);
|
||
}
|
||
|
||
window.simpanLaporan = function (lat, lng) {
|
||
const nama = document.getElementById('add-lp-nama').value.trim();
|
||
const nik = document.getElementById('add-lp-nik').value.trim();
|
||
const wa = document.getElementById('add-lp-wa').value.trim();
|
||
const tglLahir = document.getElementById('add-lp-tgllahir').value;
|
||
const pendidikan = document.getElementById('add-lp-pendidikan').value;
|
||
const penyakit = document.getElementById('add-lp-penyakit').value.trim();
|
||
const jml = parseInt(document.getElementById('add-lp-jml').value) || 1;
|
||
const alamat = document.getElementById('add-lp-alamat').value.trim();
|
||
|
||
const sp = document.getElementById('add-lp-skor_penghasilan').value;
|
||
const sr = document.getElementById('add-lp-skor_rumah').value;
|
||
const sm = document.getElementById('add-lp-skor_makan').value;
|
||
|
||
if (!nama) { showToast('⚠️ Nama Kepala Keluarga wajib diisi!', 'error'); return; }
|
||
if (nik && nik.length !== 16) { showToast('⚠️ NIK harus 16 digit!', 'error'); return; }
|
||
if (!alamat) { showToast('⚠️ Alamat / Keterangan wajib diisi!', 'error'); return; }
|
||
|
||
// Tombol loading
|
||
const btn = document.querySelector('.popup-btn-save');
|
||
if (btn) btn.innerHTML = '⏳ Mengirim...';
|
||
|
||
fetch(API_LAPOR, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
nama_pelapor: nama,
|
||
nik: nik,
|
||
no_wa: wa,
|
||
deskripsi: alamat, // dikirim sebagai deskripsi
|
||
latitude: lat, longitude: lng,
|
||
|
||
skor_penghasilan: sp, skor_rumah: sr, skor_makan: sm,
|
||
tanggal_lahir: tglLahir, pendidikan: pendidikan, riwayat_penyakit: penyakit,
|
||
jumlah_kk: jml
|
||
})
|
||
})
|
||
.then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
map.closePopup();
|
||
showToast(`✅ Laporan berhasil dikirim!`, 'success');
|
||
loadDataLaporan(); // Render the new red pin
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
|
||
|
||
// ============================================================
|
||
// LOAD DATA DARI SERVER
|
||
// ============================================================
|
||
|
||
// ============================================================
|
||
// KEMISKINAN – IKON, RENDER, POPUP, CRUD, KARTU
|
||
// ============================================================
|
||
function ikonKemiskinan(statusBantuan, kategori) {
|
||
let color, border;
|
||
if (statusBantuan === 'Sudah') {
|
||
color = '#10b981'; border = '#065f46';
|
||
} else {
|
||
if (kategori === 'Sangat Miskin') {
|
||
color = '#ef4444'; border = '#7f1d1d';
|
||
} else if (kategori === 'Miskin') {
|
||
color = '#f97316'; border = '#c2410c';
|
||
} else {
|
||
color = '#eab308'; border = '#854d0e';
|
||
}
|
||
}
|
||
return L.divIcon({
|
||
className: '',
|
||
html: `<div style="width:32px;height:32px;border-radius:50% 50% 50% 0;background:${color};transform:rotate(-45deg);border:3px solid ${border};box-shadow:0 3px 10px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;"><span style="transform:rotate(45deg);font-size:15px;line-height:1">🏚️</span></div>`,
|
||
iconSize: [32, 32], iconAnchor: [16, 32], popupAnchor: [0, -34]
|
||
});
|
||
}
|
||
|
||
function popupInfoKemiskinan(d) {
|
||
let label = `<span class="badge ${d.status_bantuan === 'Sudah' ? 'badge-sudah' : 'badge-belum'}">${d.status_bantuan || 'Belum'}</span>`;
|
||
let txtPrioritas = '';
|
||
if (d.status_bantuan !== 'Sudah') {
|
||
if (d.kategori === 'Sangat Miskin') txtPrioritas = `<span style="color:#ef4444; font-weight:bold; font-size:11px; margin-left:8px;">🚨 Prioritas 1</span>`;
|
||
else if (d.kategori === 'Miskin') txtPrioritas = `<span style="color:#f97316; font-weight:bold; font-size:11px; margin-left:8px;">⚠️ Prioritas 2</span>`;
|
||
else txtPrioritas = `<span style="color:#eab308; font-weight:bold; font-size:11px; margin-left:8px;">💡 Prioritas 3</span>`;
|
||
}
|
||
|
||
let btnAction = '';
|
||
if (canEdit('kemiskinan')) {
|
||
btnAction = `
|
||
<div style="display:flex; gap:10px; margin-top:15px; border-top:1px solid var(--border); padding-top:15px;">
|
||
<button class="popup-btn popup-btn-edit" onclick="bukaFormEditKemiskinan(${d.id})">✏️ Edit</button>
|
||
<button class="popup-btn popup-btn-delete" onclick="hapusKemiskinan(${d.id})">🗑️ Hapus</button>
|
||
</div>
|
||
<button class="popup-btn" style="background:#4f7cff;color:white;width:100%;margin-top:8px" onclick="bukaRiwayatBantuan(${d.id}, '${d.nama_kk}')">🎁 Riwayat Bantuan</button>`;
|
||
} else {
|
||
btnAction = `
|
||
<div style="margin-top:15px; border-top:1px solid var(--border); padding-top:15px;">
|
||
<button class="popup-btn" style="background:#4f7cff;color:white;width:100%;" onclick="bukaRiwayatBantuan(${d.id}, '${d.nama_kk}')">🎁 Lihat Riwayat Bantuan</button>
|
||
</div>`;
|
||
}
|
||
|
||
return `
|
||
<div class="gis-popup">
|
||
<div class="popup-title" style="color:var(--kemiskinan)">🏚️ ${d.nama_kk} ${txtPrioritas}</div>
|
||
<div class="info-row"><span class="info-label">Spasial</span><span class="info-value" style="color:var(--accent-orange); font-weight:bold;">${d.is_blank_spot == 0 ? 'Terjangkau' : 'Blank Spot'}</span></div>
|
||
<div class="info-row"><span class="info-label">Bantuan</span><span class="info-value">${label}</span></div>
|
||
<div class="info-row"><span class="info-label">Kategori</span><span class="info-value">${d.kategori || '-'}</span></div>
|
||
<div class="info-row"><span class="info-label">NIK</span><span class="info-value">${d.nik || '-'}</span></div>
|
||
<div class="info-row"><span class="info-label">Tgl Lahir</span><span class="info-value">${d.tanggal_lahir || '-'}</span></div>
|
||
<div class="info-row"><span class="info-label">Pendidikan</span><span class="info-value">${d.pendidikan || '-'}</span></div>
|
||
<div class="info-row"><span class="info-label">Penyakit</span><span class="info-value" style="${d.riwayat_penyakit ? 'color:#ef4444;font-weight:600' : ''}">${d.riwayat_penyakit || '-'}</span></div>
|
||
<div class="info-row"><span class="info-label">Jumlah KK</span><span class="info-value">${d.jumlah_kk} jiwa</span></div>
|
||
<div class="info-row"><span class="info-label">Alamat</span><span class="info-value">${d.alamat}</span></div>
|
||
${btnAction}
|
||
</div>`;
|
||
}
|
||
|
||
function renderKemiskinan(d) {
|
||
if (kemiskinanMarkers[d.id]) layerGroupKemiskinan.removeLayer(kemiskinanMarkers[d.id]);
|
||
const marker = L.marker([d.latitude, d.longitude], { icon: ikonKemiskinan(d.status_bantuan, d.kategori), draggable: true });
|
||
marker.bindPopup(() => popupInfoKemiskinan(d));
|
||
marker.on('click', () => marker.openPopup());
|
||
marker.on('dragend', function () {
|
||
const p = marker.getLatLng();
|
||
fetch(`${API_KEMISKINAN}?id=${d.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ latitude: p.lat, longitude: p.lng }) })
|
||
.then(() => showToast('📍 Posisi diperbarui'));
|
||
});
|
||
layerGroupKemiskinan.addLayer(marker);
|
||
kemiskinanMarkers[d.id] = marker;
|
||
updateCounts();
|
||
return marker;
|
||
}
|
||
|
||
function hitungKategori(p, r, m) {
|
||
const total = parseInt(p) + parseInt(r) + parseInt(m);
|
||
if (total >= 7) return 'Sangat Miskin';
|
||
if (total >= 4) return 'Miskin';
|
||
return 'Hampir Miskin';
|
||
}
|
||
|
||
function tampilkanFormTambahKemiskinan(lat, lng) {
|
||
const html = `<div class="gis-popup" style="max-height:400px;overflow-y:auto">
|
||
<div class="popup-title">🏚️ Tambah Data Kemiskinan</div>
|
||
<div class="popup-form">
|
||
<label>Nama Kepala Keluarga</label>
|
||
<input type="text" id="add-km-nama" placeholder="Nama KK">
|
||
<label>NIK</label>
|
||
<input type="text" id="add-km-nik" placeholder="16 Digit NIK">
|
||
<div style="display:flex; gap:10px;">
|
||
<div style="flex:1">
|
||
<label>Tanggal Lahir</label>
|
||
<input type="date" id="add-km-tgllahir">
|
||
</div>
|
||
<div style="flex:1">
|
||
<label>Pendidikan</label>
|
||
<select id="add-km-pendidikan">
|
||
<option value="">- Pilih -</option>
|
||
<option value="Tidak Sekolah">Tidak Sekolah</option>
|
||
<option value="SD">SD</option>
|
||
<option value="SMP">SMP</option>
|
||
<option value="SMA">SMA</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<label>Riwayat Penyakit (Opsional)</label>
|
||
<input type="text" id="add-km-penyakit" placeholder="Cth: Diabetes, Hipertensi">
|
||
|
||
<label style="color:var(--accent-yellow); margin-top:8px;">1. Penghasilan KK</label>
|
||
<select id="add-km-skor_penghasilan">
|
||
<option value="3">< Rp 500rb / Tidak Tetap (Skor Tinggi)</option>
|
||
<option value="2">Rp 500rb - 1.5 Jt (Skor Sedang)</option>
|
||
<option value="1">> Rp 1.5 Jt (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label style="color:var(--accent-yellow)">2. Kondisi Rumah</label>
|
||
<select id="add-km-skor_rumah">
|
||
<option value="3">Lantai Tanah / Dinding Bambu (Skor Tinggi)</option>
|
||
<option value="2">Semen Kasar / Papan (Skor Sedang)</option>
|
||
<option value="1">Keramik / Tembok Plester (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label style="color:var(--accent-yellow)">3. Kondisi Pangan</label>
|
||
<select id="add-km-skor_makan">
|
||
<option value="3">Makan 1-2x Sehari (Skor Tinggi)</option>
|
||
<option value="2">Cukup 3x tapi gizi seadanya (Skor Sedang)</option>
|
||
<option value="1">Kebutuhan makan selalu terpenuhi (Skor Rendah)</option>
|
||
</select>
|
||
|
||
<label>Jumlah Anggota KK</label>
|
||
<input type="number" id="add-km-jml" min="1" value="1">
|
||
<label>Alamat</label>
|
||
<input type="text" id="add-km-alamat" placeholder="Alamat lengkap">
|
||
<label>Koordinat</label>
|
||
<input type="text" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
|
||
</div>
|
||
<div class="popup-actions">
|
||
<button class="popup-btn popup-btn-save" onclick="simpanKemiskinan(${lat},${lng})">💾 Simpan</button>
|
||
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
|
||
</div></div>`;
|
||
L.popup({ closeButton: true }).setLatLng([lat, lng]).setContent(html).openOn(map);
|
||
}
|
||
|
||
window.simpanKemiskinan = function (lat, lng) {
|
||
const nama = document.getElementById('add-km-nama').value.trim();
|
||
const nik = document.getElementById('add-km-nik').value.trim();
|
||
const tglLahir = document.getElementById('add-km-tgllahir').value;
|
||
const pendidikan = document.getElementById('add-km-pendidikan').value;
|
||
const penyakit = document.getElementById('add-km-penyakit').value.trim();
|
||
const jml = parseInt(document.getElementById('add-km-jml').value) || 1;
|
||
const alamat = document.getElementById('add-km-alamat').value.trim();
|
||
|
||
const sp = document.getElementById('add-km-skor_penghasilan').value;
|
||
const sr = document.getElementById('add-km-skor_rumah').value;
|
||
const sm = document.getElementById('add-km-skor_makan').value;
|
||
const kategori = hitungKategori(sp, sr, sm);
|
||
|
||
if (!nama) { showToast('⚠️ Nama KK wajib diisi!', 'error'); return; }
|
||
if (!nik) { showToast('⚠️ NIK wajib diisi!', 'error'); return; }
|
||
if (!alamat) { showToast('⚠️ Alamat wajib diisi!', 'error'); return; }
|
||
|
||
fetch(API_KEMISKINAN, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
nama_kk: nama, nik: nik, kategori: kategori,
|
||
skor_penghasilan: sp, skor_rumah: sr, skor_makan: sm,
|
||
tanggal_lahir: tglLahir, pendidikan: pendidikan, riwayat_penyakit: penyakit,
|
||
jumlah_kk: jml, alamat: alamat, latitude: lat, longitude: lng
|
||
})
|
||
})
|
||
.then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
map.closePopup();
|
||
const nd = { id: res.id, nama_kk: nama, nik, kategori, skor_penghasilan: sp, skor_rumah: sr, skor_makan: sm, tanggal_lahir: tglLahir, pendidikan: pendidikan, riwayat_penyakit: penyakit, jumlah_kk: jml, alamat, latitude: lat, longitude: lng, is_blank_spot: res.is_blank_spot, status_bantuan: 'Belum' };
|
||
window.dataKemiskinanCache[res.id] = nd;
|
||
renderKemiskinan(nd); tambahKartuKemiskinan(nd);
|
||
showToast(`✅ Kategori ditetapkan: ${kategori}`, 'success');
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.bukaFormEditKemiskinan = function (id) {
|
||
fetch(`${API_KEMISKINAN}?id=${id}`).then(r => r.json()).then(d => {
|
||
const html = `<div class="gis-popup" style="max-height:450px;overflow-y:auto">
|
||
<div class="popup-title">✏️ Edit Kemiskinan</div>
|
||
<div class="popup-form">
|
||
<label>Nama KK</label><input type="text" id="ekm-nama-${id}" value="${d.nama_kk}">
|
||
<label>NIK</label><input type="text" id="ekm-nik-${id}" value="${d.nik || ''}">
|
||
|
||
<div style="display:flex; gap:10px;">
|
||
<div style="flex:1">
|
||
<label>Tanggal Lahir</label>
|
||
<input type="date" id="ekm-tgl-${id}" value="${d.tanggal_lahir || ''}">
|
||
</div>
|
||
<div style="flex:1">
|
||
<label>Pendidikan</label>
|
||
<select id="ekm-pdd-${id}">
|
||
<option value="">- Pilih -</option>
|
||
<option value="Tidak Sekolah" ${d.pendidikan === 'Tidak Sekolah' ? 'selected' : ''}>Tidak Sekolah</option>
|
||
<option value="SD" ${d.pendidikan === 'SD' ? 'selected' : ''}>SD</option>
|
||
<option value="SMP" ${d.pendidikan === 'SMP' ? 'selected' : ''}>SMP</option>
|
||
<option value="SMA" ${d.pendidikan === 'SMA' ? 'selected' : ''}>SMA</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<label>Riwayat Penyakit</label>
|
||
<input type="text" id="ekm-penyakit-${id}" value="${d.riwayat_penyakit || ''}">
|
||
|
||
<label style="color:var(--accent-yellow); margin-top:8px;">1. Penghasilan KK</label>
|
||
<select id="ekm-sp-${id}">
|
||
<option value="3" ${d.skor_penghasilan == 3 ? 'selected' : ''}>< Rp 500rb / Tidak Tetap</option>
|
||
<option value="2" ${d.skor_penghasilan == 2 ? 'selected' : ''}>Rp 500rb - 1.5 Jt</option>
|
||
<option value="1" ${d.skor_penghasilan == 1 ? 'selected' : ''}>> Rp 1.5 Jt</option>
|
||
</select>
|
||
<label style="color:var(--accent-yellow)">2. Kondisi Rumah</label>
|
||
<select id="ekm-sr-${id}">
|
||
<option value="3" ${d.skor_rumah == 3 ? 'selected' : ''}>Lantai Tanah / Dinding Bambu</option>
|
||
<option value="2" ${d.skor_rumah == 2 ? 'selected' : ''}>Semen Kasar / Papan</option>
|
||
<option value="1" ${d.skor_rumah == 1 ? 'selected' : ''}>Keramik / Tembok</option>
|
||
</select>
|
||
<label style="color:var(--accent-yellow)">3. Kondisi Pangan</label>
|
||
<select id="ekm-sm-${id}">
|
||
<option value="3" ${d.skor_makan == 3 ? 'selected' : ''}>Makan 1-2x Sehari</option>
|
||
<option value="2" ${d.skor_makan == 2 ? 'selected' : ''}>Cukup 3x gizi seadanya</option>
|
||
<option value="1" ${d.skor_makan == 1 ? 'selected' : ''}>Makan terpenuhi</option>
|
||
</select>
|
||
|
||
<label>Jumlah KK</label><input type="number" id="ekm-jml-${id}" value="${d.jumlah_kk}" min="1">
|
||
<label>Alamat</label><input type="text" id="ekm-alamat-${id}" value="${d.alamat}">
|
||
</div>
|
||
<div class="popup-actions">
|
||
<button class="popup-btn popup-btn-save" onclick="updateKemiskinan(${id}, ${d.latitude}, ${d.longitude})">💾 Update</button>
|
||
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
|
||
</div></div>`;
|
||
kemiskinanMarkers[id].getPopup().setContent(html).update();
|
||
});
|
||
};
|
||
|
||
window.updateKemiskinan = function (id, lat, lng) {
|
||
const nama = document.getElementById(`ekm-nama-${id}`).value.trim();
|
||
const nik = document.getElementById(`ekm-nik-${id}`).value.trim();
|
||
const tgl = document.getElementById(`ekm-tgl-${id}`).value;
|
||
const pdd = document.getElementById(`ekm-pdd-${id}`).value;
|
||
const pny = document.getElementById(`ekm-penyakit-${id}`).value.trim();
|
||
const jml = parseInt(document.getElementById(`ekm-jml-${id}`).value) || 1;
|
||
const alamat = document.getElementById(`ekm-alamat-${id}`).value.trim();
|
||
|
||
const sp = document.getElementById(`ekm-sp-${id}`).value;
|
||
const sr = document.getElementById(`ekm-sr-${id}`).value;
|
||
const sm = document.getElementById(`ekm-sm-${id}`).value;
|
||
const kategori = hitungKategori(sp, sr, sm);
|
||
|
||
if (!nama || !alamat) { showToast('⚠️ Nama dan Alamat wajib diisi!', 'error'); return; }
|
||
fetch(`${API_KEMISKINAN}?id=${id}`, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
nama_kk: nama, nik: nik, jumlah_kk: jml, alamat: alamat,
|
||
latitude: lat, longitude: lng, kategori: kategori,
|
||
skor_penghasilan: sp, skor_rumah: sr, skor_makan: sm,
|
||
tanggal_lahir: tgl, pendidikan: pdd, riwayat_penyakit: pny
|
||
})
|
||
}).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
const cur = window.dataKemiskinanCache[id];
|
||
const statusBaru = cur ? cur.status_bantuan : 'Belum';
|
||
const nd = { id, nama_kk: nama, nik, kategori, skor_penghasilan: sp, skor_rumah: sr, skor_makan: sm, tanggal_lahir: tgl, pendidikan: pdd, riwayat_penyakit: pny, jumlah_kk: jml, alamat, latitude: lat, longitude: lng, is_blank_spot: cur.is_blank_spot, status_bantuan: statusBaru };
|
||
window.dataKemiskinanCache[id] = nd; // Update cache
|
||
kemiskinanMarkers[id].setIcon(ikonKemiskinan(nd.status_bantuan, nd.kategori));
|
||
kemiskinanMarkers[id].getPopup().setContent(popupInfoKemiskinan(nd)).update();
|
||
kemiskinanMarkers[id].closePopup();
|
||
updateKartuKemiskinan(nd);
|
||
updateCounts();
|
||
showToast('✅ Data diperbarui! Kategori: ' + kategori);
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.hapusKemiskinan = function (id) {
|
||
if (!confirm('Hapus data kemiskinan ini?')) return;
|
||
fetch(`${API_KEMISKINAN}?id=${id}`, { method: 'DELETE' }).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
layerGroupKemiskinan.removeLayer(kemiskinanMarkers[id]);
|
||
delete kemiskinanMarkers[id];
|
||
document.getElementById(`kartu-km-${id}`)?.remove();
|
||
updateCounts();
|
||
showToast('🗑️ Data kemiskinan dihapus');
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.zoomKeKemiskinan = function (id) {
|
||
if (kemiskinanMarkers[id]) map.setView(kemiskinanMarkers[id].getLatLng(), 17);
|
||
};
|
||
|
||
function tambahKartuKemiskinan(d) {
|
||
const list = document.getElementById('list-kemiskinan');
|
||
const loading = list.querySelector('.loading-text');
|
||
if (loading) loading.remove();
|
||
|
||
const editBtns = canEdit('kemiskinan')
|
||
? `<button class="btn-sm btn-edit" onclick="kemiskinanMarkers[${d.id}].openPopup();bukaFormEditKemiskinan(${d.id})">✏️ Edit</button>
|
||
<button class="btn-sm btn-delete" onclick="hapusKemiskinan(${d.id})">🗑️ Hapus</button>`
|
||
: '';
|
||
|
||
const statusBantuan = d.status_bantuan || 'Belum';
|
||
|
||
// Tentukan warna border kiri berdasarkan kategori
|
||
let katColor = '#f97316';
|
||
if (d.kategori === 'Sangat Miskin') katColor = '#ef4444';
|
||
if (d.kategori === 'Hampir Miskin') katColor = '#eab308';
|
||
if (statusBantuan === 'Sudah') katColor = '#10b981';
|
||
|
||
const statusColor = statusBantuan === 'Sudah' ? '#10b981' : '#f97316';
|
||
|
||
const div = document.createElement('div');
|
||
div.className = 'item-card'; div.id = `kartu-km-${d.id}`;
|
||
div.style.borderLeft = `4px solid ${katColor}`;
|
||
div.innerHTML = `<div class="item-card-header"><span class="item-card-name">🏚️ ${d.nama_kk}</span><span class="badge" style="background:${statusColor}22;color:${statusColor};border:1px solid ${statusColor}">${statusBantuan} Dibantu</span></div>
|
||
<div class="item-card-info">👥 ${d.jumlah_kk} jiwa • <strong style="color:${katColor}">${d.kategori || 'Belum diatur'}</strong> | 📍 ${d.alamat}</div>
|
||
<div class="item-card-actions">
|
||
<button class="btn-sm btn-zoom" onclick="zoomKeKemiskinan(${d.id})">🔍 Zoom</button>
|
||
${editBtns}
|
||
</div>`;
|
||
list.prepend(div);
|
||
}
|
||
|
||
function updateKartuKemiskinan(d) {
|
||
const div = document.getElementById(`kartu-km-${d.id}`);
|
||
if (!div) return;
|
||
|
||
const statusBantuan = d.status_bantuan || 'Belum';
|
||
|
||
// Tentukan warna border kiri berdasarkan kategori
|
||
let katColor = '#f97316';
|
||
if (d.kategori === 'Sangat Miskin') katColor = '#ef4444';
|
||
if (d.kategori === 'Hampir Miskin') katColor = '#eab308';
|
||
if (statusBantuan === 'Sudah') katColor = '#10b981';
|
||
|
||
const statusColor = statusBantuan === 'Sudah' ? '#10b981' : '#f97316';
|
||
|
||
div.style.borderLeft = `4px solid ${katColor}`;
|
||
div.innerHTML = `<div class="item-card-header"><span class="item-card-name">🏚️ ${d.nama_kk}</span><span class="badge" style="background:${statusColor}22;color:${statusColor};border:1px solid ${statusColor}">${statusBantuan} Dibantu</span></div>
|
||
<div class="item-card-info">👥 ${d.jumlah_kk} jiwa • <strong style="color:${katColor}">${d.kategori || 'Belum diatur'}</strong> | 📍 ${d.alamat}</div>
|
||
<div class="item-card-actions">
|
||
<button class="btn-sm btn-zoom" onclick="zoomKeKemiskinan(${d.id})">🔍 Zoom</button>
|
||
<button class="btn-sm btn-edit" onclick="kemiskinanMarkers[${d.id}].openPopup();bukaFormEditKemiskinan(${d.id})">✏️ Edit</button>
|
||
<button class="btn-sm btn-delete" onclick="hapusKemiskinan(${d.id})">🗑️ Hapus</button>
|
||
</div>`;
|
||
}
|
||
|
||
function loadDataKemiskinan() {
|
||
const list = document.getElementById('list-kemiskinan');
|
||
fetch(API_KEMISKINAN).then(r => r.json()).then(data => {
|
||
list.innerHTML = '';
|
||
if (!data.length) { list.innerHTML = '<div class="loading-text">Belum ada data kemiskinan.</div>'; return; }
|
||
|
||
// Simpan data di scope global agar mudah diambil
|
||
window.dataKemiskinanCache = {};
|
||
data.forEach(d => {
|
||
window.dataKemiskinanCache[d.id] = d;
|
||
renderKemiskinan(d);
|
||
tambahKartuKemiskinan(d);
|
||
});
|
||
}).catch(() => { list.innerHTML = '<div class="loading-text" style="color:#ef4444">⚠️ Gagal memuat data.</div>'; });
|
||
}
|
||
|
||
// ============================================================
|
||
// FUNGSI RIWAYAT BANTUAN
|
||
// ============================================================
|
||
window.bukaRiwayatBantuan = function (id_kemiskinan, nama_kk) {
|
||
document.getElementById('modal-bantuan-title').textContent = `Riwayat Bantuan: ${nama_kk}`;
|
||
document.getElementById('bantuan-id-kemiskinan').value = id_kemiskinan;
|
||
|
||
// Set tanggal hari ini sebagai default
|
||
document.getElementById('bantuan-tanggal').valueAsDate = new Date();
|
||
|
||
// Tampilkan modal
|
||
const modal = document.getElementById('modal-bantuan');
|
||
modal.style.display = 'flex';
|
||
|
||
// Sembunyikan form tambah jika bukan role yang berwenang (opsional: admin atau pengurus)
|
||
const formContainer = document.getElementById('form-bantuan-container');
|
||
if (userRole === 'pengurus' || userRole === 'admin') {
|
||
formContainer.style.display = 'block';
|
||
// Otomatis isi nama pemberi jika pengurus
|
||
if (userRole === 'pengurus' && currentUser.nama) {
|
||
document.getElementById('bantuan-pemberi').value = currentUser.nama;
|
||
}
|
||
} else {
|
||
formContainer.style.display = 'none';
|
||
}
|
||
|
||
muatRiwayatBantuan(id_kemiskinan);
|
||
};
|
||
|
||
function muatRiwayatBantuan(id_kemiskinan) {
|
||
const list = document.getElementById('modal-bantuan-list');
|
||
list.innerHTML = '<div class="loading-text">Memuat histori...</div>';
|
||
|
||
fetch(`${API_BANTUAN}?id_kemiskinan=${id_kemiskinan}`)
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
list.innerHTML = '';
|
||
if (data.error) {
|
||
list.innerHTML = `<div class="loading-text" style="color:red">${data.error}</div>`;
|
||
return;
|
||
}
|
||
if (data.length === 0) {
|
||
list.innerHTML = '<div class="loading-text" style="text-align:center;padding:20px;">Belum ada riwayat bantuan untuk keluarga ini.</div>';
|
||
return;
|
||
}
|
||
|
||
data.forEach(b => {
|
||
const btnHapus = (userRole === 'admin' || userRole === 'pengurus')
|
||
? `<button onclick="hapusRiwayatBantuan(${b.id}, ${id_kemiskinan})" style="background:none;border:none;color:var(--spbu-no24);cursor:pointer;font-size:12px;">Hapus</button>` : '';
|
||
|
||
list.innerHTML += `
|
||
<div style="background:var(--bg-dark); padding:12px; border-radius:8px; margin-bottom:10px; border:1px solid var(--border);">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:4px;">
|
||
<strong style="color:var(--text-primary);font-size:14px;">🎁 ${b.jenis_bantuan}</strong>
|
||
<span style="font-size:11px;color:var(--text-muted);">${b.tanggal}</span>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--text-secondary);margin-bottom:8px;">Oleh: ${b.pemberi}</div>
|
||
${b.keterangan ? `<div style="font-size:12px;background:var(--bg-card);padding:6px;border-radius:4px;color:var(--text-muted);margin-bottom:8px;">"${b.keterangan}"</div>` : ''}
|
||
<div style="text-align:right;">${btnHapus}</div>
|
||
</div>
|
||
`;
|
||
});
|
||
})
|
||
.catch(() => {
|
||
list.innerHTML = '<div class="loading-text" style="color:red">Gagal memuat histori.</div>';
|
||
});
|
||
}
|
||
|
||
window.simpanRiwayatBantuan = function () {
|
||
const id_kemiskinan = document.getElementById('bantuan-id-kemiskinan').value;
|
||
const pemberi = document.getElementById('bantuan-pemberi').value.trim();
|
||
const jenis = document.getElementById('bantuan-jenis').value.trim();
|
||
const tgl = document.getElementById('bantuan-tanggal').value;
|
||
const ket = document.getElementById('bantuan-keterangan').value.trim();
|
||
|
||
if (!pemberi || !jenis || !tgl) {
|
||
showToast('⚠️ Pemberi, Jenis, dan Tanggal wajib diisi!', 'error');
|
||
return;
|
||
}
|
||
|
||
fetch(API_BANTUAN, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
id_kemiskinan: id_kemiskinan,
|
||
pemberi: pemberi,
|
||
jenis_bantuan: jenis,
|
||
tanggal: tgl,
|
||
keterangan: ket
|
||
})
|
||
}).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
showToast('✅ Bantuan berhasil dicatat!');
|
||
|
||
// Clear form (kecuali pemberi dan tgl)
|
||
document.getElementById('bantuan-jenis').value = '';
|
||
document.getElementById('bantuan-keterangan').value = '';
|
||
|
||
// Refresh list di modal
|
||
muatRiwayatBantuan(id_kemiskinan);
|
||
|
||
// Perbarui status warna marker di peta
|
||
updateStatusBantuanMarker(id_kemiskinan, res.status_bantuan);
|
||
|
||
}).catch(() => showToast('❌ Terjadi kesalahan jaringan', 'error'));
|
||
};
|
||
|
||
window.hapusRiwayatBantuan = function (id_riwayat, id_kemiskinan) {
|
||
if (!confirm('Hapus riwayat bantuan ini?')) return;
|
||
fetch(`${API_BANTUAN}?id=${id_riwayat}&id_kemiskinan=${id_kemiskinan}`, { method: 'DELETE' })
|
||
.then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
showToast('🗑️ Riwayat dihapus');
|
||
muatRiwayatBantuan(id_kemiskinan);
|
||
updateStatusBantuanMarker(id_kemiskinan, res.status_bantuan);
|
||
});
|
||
};
|
||
|
||
function updateStatusBantuanMarker(id_kemiskinan, statusBaru) {
|
||
if (window.dataKemiskinanCache && window.dataKemiskinanCache[id_kemiskinan]) {
|
||
const data = window.dataKemiskinanCache[id_kemiskinan];
|
||
data.status_bantuan = statusBaru;
|
||
|
||
// Update Icon & Popup
|
||
if (kemiskinanMarkers[id_kemiskinan]) {
|
||
kemiskinanMarkers[id_kemiskinan].setIcon(ikonKemiskinan(statusBaru, data.kategori));
|
||
kemiskinanMarkers[id_kemiskinan].getPopup().setContent(popupInfoKemiskinan(data)).update();
|
||
}
|
||
// Update Kartu Sidebar
|
||
updateKartuKemiskinan(data);
|
||
}
|
||
}
|
||
|
||
|
||
// ============================================================
|
||
// MASJID – IKON, RENDER (+ CIRCLE), POPUP, CRUD, KARTU
|
||
// ============================================================
|
||
// emoji & warna per jenis rumah ibadah
|
||
function infoJenis(jenis) {
|
||
const map = {
|
||
'Masjid': { emoji: '🕌', color: '#8b5cf6' },
|
||
'Mushola': { emoji: '🕋', color: '#6366f1' },
|
||
'Gereja': { emoji: '⛪', color: '#3b82f6' },
|
||
'Pura': { emoji: '🛕', color: '#f59e0b' },
|
||
'Vihara': { emoji: '🏯', color: '#10b981' },
|
||
'Klenteng': { emoji: '🏮', color: '#ef4444' }
|
||
};
|
||
return map[jenis] || map['Masjid'];
|
||
}
|
||
|
||
function ikonMasjid(jenis) {
|
||
const info = infoJenis(jenis || 'Masjid');
|
||
return L.divIcon({
|
||
className: '',
|
||
html: `<div style="width:32px;height:32px;border-radius:50% 50% 50% 0;background:${info.color};transform:rotate(-45deg);border:3px solid rgba(0,0,0,.3);box-shadow:0 3px 10px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;"><span style="transform:rotate(45deg);font-size:15px;line-height:1">${info.emoji}</span></div>`,
|
||
iconSize: [32, 32], iconAnchor: [16, 32], popupAnchor: [0, -34]
|
||
});
|
||
}
|
||
|
||
function popupInfoMasjid(d) {
|
||
const info = infoJenis(d.jenis || 'Masjid');
|
||
const aksiHtml = canEdit('masjid')
|
||
? `<button class="popup-btn popup-btn-edit" onclick="bukaFormEditMasjid(${d.id})">✏️ Edit</button>
|
||
<button class="popup-btn popup-btn-delete" onclick="hapusMasjid(${d.id})">🗑️ Hapus</button>`
|
||
: `<span style="font-size:11px;color:var(--text-muted)">🔒 View Only</span>`;
|
||
return `<div class="gis-popup">
|
||
<div class="popup-title" style="border-bottom-color:${info.color}">${info.emoji} ${d.nama_masjid}</div>
|
||
<div class="info-row"><span class="info-label">Jenis</span><span class="info-value" style="color:${info.color}">${d.jenis || 'Masjid'}</span></div>
|
||
<div class="info-row"><span class="info-label">PIC</span><span class="info-value">${d.pic_masjid}</span></div>
|
||
<div class="info-row"><span class="info-label">Alamat</span><span class="info-value" style="font-size:10px">${d.alamat}</span></div>
|
||
<div class="info-row"><span class="info-label">Radius Buffer</span><span class="info-value" style="color:${info.color}">${d.radius_m} m</span></div>
|
||
<div class="info-row"><span class="info-label">Koordinat</span><span class="info-value" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span></div>
|
||
<div class="popup-actions">${aksiHtml}</div></div>`;
|
||
}
|
||
|
||
function renderMasjid(d) {
|
||
if (masjidMarkers[d.id]) {
|
||
layerGroupMasjid.removeLayer(masjidMarkers[d.id]);
|
||
layerGroupMasjid.removeLayer(masjidCircles[d.id]);
|
||
}
|
||
const circle = L.circle([d.latitude, d.longitude], {
|
||
radius: d.radius_m, color: infoJenis(d.jenis).color, fillColor: infoJenis(d.jenis).color,
|
||
fillOpacity: 0.12, weight: 2, dashArray: '5,5'
|
||
});
|
||
const marker = L.marker([d.latitude, d.longitude], { icon: ikonMasjid(d.jenis), draggable: true });
|
||
marker.bindPopup(() => popupInfoMasjid(d));
|
||
marker.on('click', () => marker.openPopup());
|
||
marker.on('dragend', function () {
|
||
const p = marker.getLatLng();
|
||
circle.setLatLng(p);
|
||
fetch(`${API_MASJID}?id=${d.id}`, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ latitude: p.lat, longitude: p.lng })
|
||
})
|
||
.then(() => showToast('📍 Posisi masjid diperbarui'));
|
||
});
|
||
layerGroupMasjid.addLayer(circle);
|
||
layerGroupMasjid.addLayer(marker);
|
||
masjidMarkers[d.id] = marker;
|
||
masjidCircles[d.id] = circle;
|
||
updateCounts();
|
||
return marker;
|
||
}
|
||
|
||
function sliderHtml(idPrefix, radiusVal) {
|
||
return `<div class="radius-wrap">
|
||
<div class="radius-header"><span>Radius Buffer</span><span class="radius-val" id="${idPrefix}-val">${radiusVal} m</span></div>
|
||
<input type="range" class="radius-slider" id="${idPrefix}-slider" min="100" max="5000" step="100" value="${radiusVal}"
|
||
oninput="document.getElementById('${idPrefix}-val').textContent=this.value+' m'">
|
||
<div class="radius-ticks"><span>100 m</span><span>2500 m</span><span>5000 m</span></div>
|
||
</div>`;
|
||
}
|
||
|
||
function jenisSelect(selected) {
|
||
const opts = ['Masjid', 'Mushola', 'Gereja', 'Pura', 'Vihara', 'Klenteng'];
|
||
return `<select id="msj-jenis">${opts.map(o => `<option value="${o}"${o === selected ? ' selected' : ''}>${infoJenis(o).emoji} ${o}</option>`).join('')}</select>`;
|
||
}
|
||
|
||
function tampilkanFormTambahMasjid(lat, lng) {
|
||
const html = `<div class="gis-popup">
|
||
<div class="popup-title">🕌 Tambah Rumah Ibadah</div>
|
||
<div class="popup-form">
|
||
<label>Jenis Tempat Ibadah</label>${jenisSelect('Masjid')}
|
||
<label>Nama Tempat Ibadah</label><input type="text" id="add-msj-nama" placeholder="Nama tempat ibadah">
|
||
<label>PIC / Penanggung Jawab</label><input type="text" id="add-msj-pic" placeholder="Nama PIC">
|
||
<label>Alamat</label><input type="text" id="add-msj-alamat" placeholder="Alamat lengkap">
|
||
${sliderHtml('add-msj', 500)}
|
||
<label>Koordinat</label>
|
||
<input type="text" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
|
||
</div>
|
||
<div class="popup-actions">
|
||
<button class="popup-btn popup-btn-save" onclick="simpanMasjid(${lat},${lng})">💾 Simpan</button>
|
||
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
|
||
</div></div>`;
|
||
L.popup({ closeButton: true, maxWidth: 320 }).setLatLng([lat, lng]).setContent(html).openOn(map);
|
||
}
|
||
|
||
window.simpanMasjid = function (lat, lng) {
|
||
const jenis = document.getElementById('msj-jenis').value;
|
||
const nama = document.getElementById('add-msj-nama').value.trim();
|
||
const pic = document.getElementById('add-msj-pic').value.trim();
|
||
const alamat = document.getElementById('add-msj-alamat').value.trim();
|
||
const radius = parseInt(document.getElementById('add-msj-slider').value) || 500;
|
||
if (!nama) { showToast('⚠️ Nama wajib diisi!', 'error'); return; }
|
||
if (!pic) { showToast('⚠️ PIC wajib diisi!', 'error'); return; }
|
||
if (!alamat) { showToast('⚠️ Alamat wajib diisi!', 'error'); return; }
|
||
fetch(API_MASJID, {
|
||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama_masjid: nama, jenis, pic_masjid: pic, alamat, radius_m: radius, latitude: lat, longitude: lng })
|
||
})
|
||
.then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
map.closePopup();
|
||
const nd = { id: res.id, nama_masjid: nama, jenis, pic_masjid: pic, alamat, radius_m: radius, latitude: lat, longitude: lng };
|
||
renderMasjid(nd); tambahKartuMasjid(nd);
|
||
showToast(`✅ ${jenis} "${nama}" berhasil disimpan!`);
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.bukaFormEditMasjid = function (id) {
|
||
fetch(`${API_MASJID}?id=${id}`).then(r => r.json()).then(d => {
|
||
const html = `<div class="gis-popup">
|
||
<div class="popup-title">✏️ Edit Rumah Ibadah</div>
|
||
<div class="popup-form">
|
||
<label>Jenis</label>${jenisSelect(d.jenis || 'Masjid').replace('id="msj-jenis"', `id="emsj-jenis-${id}"`)}
|
||
<label>Nama</label><input type="text" id="emsj-nama-${id}" value="${d.nama_masjid}">
|
||
<label>PIC</label><input type="text" id="emsj-pic-${id}" value="${d.pic_masjid}">
|
||
<label>Alamat</label><input type="text" id="emsj-alamat-${id}" value="${d.alamat}">
|
||
${sliderHtml('emsj-' + id, d.radius_m)}
|
||
</div>
|
||
<div class="popup-actions">
|
||
<button class="popup-btn popup-btn-save" onclick="eksekusiEditMasjid(${id})">💾 Simpan</button>
|
||
<button class="popup-btn popup-btn-cancel" onclick="masjidMarkers[${id}].openPopup()">✖ Batal</button>
|
||
</div></div>`;
|
||
masjidMarkers[id].getPopup().setContent(html).update();
|
||
setTimeout(() => {
|
||
const sl = document.getElementById(`emsj-${id}-slider`);
|
||
if (sl) sl.addEventListener('input', () => {
|
||
if (masjidCircles[id]) masjidCircles[id].setRadius(parseInt(sl.value));
|
||
});
|
||
}, 100);
|
||
});
|
||
};
|
||
|
||
window.eksekusiEditMasjid = function (id) {
|
||
const jenis = document.getElementById(`emsj-jenis-${id}`).value;
|
||
const nama = document.getElementById(`emsj-nama-${id}`).value.trim();
|
||
const pic = document.getElementById(`emsj-pic-${id}`).value.trim();
|
||
const alamat = document.getElementById(`emsj-alamat-${id}`).value.trim();
|
||
const radius = parseInt(document.getElementById(`emsj-${id}-slider`).value) || 500;
|
||
if (!nama || !pic || !alamat) { showToast('⚠️ Semua field wajib diisi!', 'error'); return; }
|
||
const p = masjidMarkers[id].getLatLng();
|
||
fetch(`${API_MASJID}?id=${id}`, {
|
||
method: 'PUT', headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama_masjid: nama, jenis, pic_masjid: pic, alamat, radius_m: radius, latitude: p.lat, longitude: p.lng })
|
||
})
|
||
.then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
if (masjidCircles[id]) {
|
||
masjidCircles[id].setRadius(radius);
|
||
masjidCircles[id].setStyle({ color: infoJenis(jenis).color, fillColor: infoJenis(jenis).color });
|
||
}
|
||
masjidMarkers[id].setIcon(ikonMasjid(jenis));
|
||
const nd = { id, nama_masjid: nama, jenis, pic_masjid: pic, alamat, radius_m: radius, latitude: p.lat, longitude: p.lng };
|
||
masjidMarkers[id].getPopup().setContent(popupInfoMasjid(nd)).update();
|
||
masjidMarkers[id].closePopup();
|
||
updateKartuMasjid(nd);
|
||
showToast('✅ Data rumah ibadah diperbarui!');
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.hapusMasjid = function (id) {
|
||
if (!confirm('Hapus data masjid ini?')) return;
|
||
fetch(`${API_MASJID}?id=${id}`, { method: 'DELETE' }).then(r => r.json()).then(res => {
|
||
if (res.error) { showToast('❌ ' + res.error, 'error'); return; }
|
||
layerGroupMasjid.removeLayer(masjidMarkers[id]);
|
||
layerGroupMasjid.removeLayer(masjidCircles[id]);
|
||
delete masjidMarkers[id]; delete masjidCircles[id];
|
||
document.getElementById(`kartu-msj-${id}`)?.remove();
|
||
updateCounts();
|
||
showToast('🗑️ Data masjid dihapus');
|
||
}).catch(() => showToast('❌ Gagal terhubung ke server', 'error'));
|
||
};
|
||
|
||
window.zoomKeMasjid = function (id) {
|
||
if (masjidMarkers[id]) map.setView(masjidMarkers[id].getLatLng(), 16);
|
||
};
|
||
|
||
function tambahKartuMasjid(d) {
|
||
const list = document.getElementById('list-masjid');
|
||
const loading = list.querySelector('.loading-text');
|
||
if (loading) loading.remove();
|
||
const info = infoJenis(d.jenis || 'Masjid');
|
||
const editBtns = canEdit('masjid')
|
||
? `<button class="btn-sm btn-edit" onclick="masjidMarkers[${d.id}].openPopup();bukaFormEditMasjid(${d.id})">✏️ Edit</button>
|
||
<button class="btn-sm btn-delete" onclick="hapusMasjid(${d.id})">🗑️ Hapus</button>`
|
||
: '';
|
||
const div = document.createElement('div');
|
||
div.className = 'item-card'; div.id = `kartu-msj-${d.id}`;
|
||
div.style.borderLeft = `3px solid ${info.color}`;
|
||
div.innerHTML = `<div class="item-card-header"><span class="item-card-name">${info.emoji} ${d.nama_masjid}</span><span class="badge" style="background:rgba(139,92,246,.15);color:#a78bfa;border:1px solid ${info.color}">${d.jenis || 'Masjid'} • ${d.radius_m}m</span></div>
|
||
<div class="item-card-info">👤 ${d.pic_masjid} | 📍 ${d.alamat}</div>
|
||
<div class="item-card-actions">
|
||
<button class="btn-sm btn-zoom" onclick="zoomKeMasjid(${d.id})">🔍 Zoom</button>
|
||
${editBtns}
|
||
</div>`;
|
||
list.prepend(div);
|
||
}
|
||
|
||
function updateKartuMasjid(d) {
|
||
const div = document.getElementById(`kartu-msj-${d.id}`);
|
||
if (!div) return;
|
||
const info = infoJenis(d.jenis || 'Masjid');
|
||
const editBtns = canEdit('masjid')
|
||
? `<button class="btn-sm btn-edit" onclick="masjidMarkers[${d.id}].openPopup();bukaFormEditMasjid(${d.id})">✏️ Edit</button>
|
||
<button class="btn-sm btn-delete" onclick="hapusMasjid(${d.id})">🗑️ Hapus</button>`
|
||
: '';
|
||
div.style.borderLeft = `3px solid ${info.color}`;
|
||
div.innerHTML = `<div class="item-card-header"><span class="item-card-name">${info.emoji} ${d.nama_masjid}</span><span class="badge" style="background:rgba(139,92,246,.15);color:#a78bfa;border:1px solid ${info.color}">${d.jenis || 'Masjid'} • ${d.radius_m}m</span></div>
|
||
<div class="item-card-info">👤 ${d.pic_masjid} | 📍 ${d.alamat}</div>
|
||
<div class="item-card-actions">
|
||
<button class="btn-sm btn-zoom" onclick="zoomKeMasjid(${d.id})">🔍 Zoom</button>
|
||
${editBtns}
|
||
</div>`;
|
||
}
|
||
|
||
function loadDataMasjid() {
|
||
const list = document.getElementById('list-masjid');
|
||
fetch(API_MASJID).then(r => r.json()).then(data => {
|
||
list.innerHTML = '';
|
||
if (!data.length) { list.innerHTML = '<div class="loading-text">Belum ada data masjid.</div>'; return; }
|
||
data.forEach(d => { renderMasjid(d); tambahKartuMasjid(d); });
|
||
}).catch(() => { list.innerHTML = '<div class="loading-text" style="color:#ef4444">⚠️ Gagal memuat data.</div>'; });
|
||
}
|
||
|
||
// ============================================================
|
||
// HEATMAP KEMISKINAN
|
||
// ============================================================
|
||
function buildHeatmap() {
|
||
if (heatLayer) map.removeLayer(heatLayer);
|
||
const points = Object.values(window.dataKemiskinanCache || {}).map(d => {
|
||
let intensity = 0.5;
|
||
if (d.kategori === 'Sangat Miskin') intensity = 1.0;
|
||
else if (d.kategori === 'Miskin') intensity = 0.7;
|
||
return [d.latitude, d.longitude, intensity];
|
||
});
|
||
if (points.length) {
|
||
heatLayer = L.heatLayer(points, {
|
||
radius: 30, blur: 20, maxZoom: 16,
|
||
gradient: { 0.2: '#2563eb', 0.4: '#f59e0b', 0.6: '#f97316', 0.8: '#ef4444', 1: '#dc2626' }
|
||
});
|
||
layerControl.addOverlay(heatLayer, "<span style='color:#ef4444;font-weight:700'>🔥 Heatmap Kemiskinan</span>");
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// EXPORT CSV
|
||
// ============================================================
|
||
function exportCSV() {
|
||
const data = Object.values(window.dataKemiskinanCache || {});
|
||
if (!data.length) { showToast('⚠️ Tidak ada data untuk di-export', 'error'); return; }
|
||
|
||
const headers = ['ID', 'Nama KK', 'NIK', 'Kategori', 'Status Bantuan', 'Jumlah KK', 'Alamat', 'Pendidikan', 'Riwayat Penyakit', 'Latitude', 'Longitude'];
|
||
const rows = data.map(d => [
|
||
d.id, d.nama_kk, d.nik || '', d.kategori || '', d.status_bantuan || 'Belum',
|
||
d.jumlah_kk, '"' + (d.alamat || '').replace(/"/g, '""') + '"',
|
||
d.pendidikan || '', d.riwayat_penyakit || '',
|
||
d.latitude, d.longitude
|
||
]);
|
||
|
||
let csv = '\uFEFF' + headers.join(',') + '\n';
|
||
rows.forEach(r => csv += r.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 = `data_kemiskinan_${new Date().toISOString().slice(0, 10)}.csv`;
|
||
a.click();
|
||
URL.revokeObjectURL(url);
|
||
showToast('📥 File CSV berhasil di-download!');
|
||
}
|
||
|
||
// ── Inisialisasi ──────────────────────────────────────────────
|
||
tampilkanUserInfo();
|
||
terapkanRoleUI();
|
||
loadDataKemiskinan();
|
||
loadDataMasjid();
|
||
loadDataLaporan();
|
||
|
||
// Build heatmap setelah data kemiskinan dimuat
|
||
setTimeout(buildHeatmap, 2000);
|
||
</script>
|
||
</body>
|
||
|
||
</html> |