Files
2026-06-09 19:02:22 +07:00

2398 lines
108 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;">&times;</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;">&times;</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 &copy; 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 - Opsional)</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 &bull; <strong style="color:${katColor}">${d.kategori || 'Belum diatur'}</strong> &nbsp;|&nbsp; 📍 ${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 &bull; <strong style="color:${katColor}">${d.kategori || 'Belum diatur'}</strong> &nbsp;|&nbsp; 📍 ${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'} &bull; ${d.radius_m}m</span></div>
<div class="item-card-info">👤 ${d.pic_masjid} &nbsp;|&nbsp; 📍 ${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'} &bull; ${d.radius_m}m</span></div>
<div class="item-card-info">👤 ${d.pic_masjid} &nbsp;|&nbsp; 📍 ${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>