Files
2026-06-03 05:19:00 +00:00

1100 lines
56 KiB
PHP
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">
<title>WebGIS Poverty Mapping Sistem Geospasial Pengentasan Kemiskinan</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* ══════════════════════════════════════
BASE & RESET
══════════════════════════════════════ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--blue: #2563eb;
--blue-lt: #dbeafe;
--blue-dk: #1d4ed8;
--green: #16a34a;
--green-lt: #dcfce7;
--red: #dc2626;
--red-lt: #fee2e2;
--orange: #ea580c;
--orange-lt:#ffedd5;
--gray: #64748b;
--gray-lt: #f1f5f9;
--dark: #0f172a;
--white: #ffffff;
--shadow: 0 4px 24px rgba(0,0,0,.12);
--shadow-sm:0 2px 8px rgba(0,0,0,.08);
--radius: 12px;
--radius-sm:8px;
}
body { font-family: 'Inter', sans-serif; overflow: hidden; background: #0f172a; color: var(--dark); }
/* ══════════════════════════════════════
MAP
══════════════════════════════════════ */
#map { width: 100vw; height: 100vh; z-index: 1; }
/* ══════════════════════════════════════
TOP BAR
══════════════════════════════════════ */
#topbar {
position: absolute; top: 0; left: 0; right: 0; z-index: 900;
display: flex; align-items: center; gap: 12px;
padding: 12px 16px;
background: linear-gradient(180deg, rgba(15,23,42,.85) 0%, transparent 100%);
pointer-events: none;
}
#topbar > * { pointer-events: all; }
.brand {
display: flex; align-items: center; gap: 10px;
background: var(--white); border-radius: var(--radius);
padding: 8px 16px; box-shadow: var(--shadow-sm);
}
.brand-icon { font-size: 22px; }
.brand-text { line-height: 1.2; }
.brand-text strong { font-size: 13px; color: var(--dark); font-weight: 700; display: block; }
.brand-text span { font-size: 10px; color: var(--gray); font-weight: 400; }
.spacer { flex: 1; }
/* Role badge */
.role-badge {
display: flex; align-items: center; gap: 8px;
background: var(--white); border-radius: var(--radius);
padding: 8px 14px; box-shadow: var(--shadow-sm); font-size: 13px; font-weight: 600;
}
.role-dot { width: 8px; height: 8px; border-radius: 50%; }
.role-dot.relawan { background: var(--orange); }
.role-dot.pengurus { background: var(--blue); }
.role-dot.pemerintah { background: var(--green); }
/* Mode selector */
.mode-pills {
display: flex; background: var(--white); border-radius: var(--radius);
box-shadow: var(--shadow-sm); overflow: hidden;
}
.mode-pill {
padding: 9px 14px; font-size: 12px; font-weight: 600; cursor: pointer;
border: none; background: transparent; color: var(--gray); transition: .2s;
display: flex; align-items: center; gap: 6px;
}
.mode-pill.active { background: var(--blue); color: white; }
.mode-pill.active.green{ background: var(--green); }
.mode-pill:hover:not(.active) { background: var(--gray-lt); }
/* Topbar buttons */
.tb-btn {
background: var(--white); border: none; border-radius: var(--radius);
padding: 9px 16px; font-size: 13px; font-weight: 600; cursor: pointer;
box-shadow: var(--shadow-sm); color: var(--dark); transition: .2s;
display: flex; align-items: center; gap: 7px;
}
.tb-btn:hover { background: var(--gray-lt); transform: translateY(-1px); }
.tb-btn.primary { background: var(--blue); color: white; }
.tb-btn.primary:hover { background: var(--blue-dk); }
/* ══════════════════════════════════════
SIDEBAR
══════════════════════════════════════ */
#sidebar {
position: absolute; top: 0; left: -420px; width: 380px; height: 100vh;
background: var(--white); z-index: 2000;
box-shadow: 6px 0 30px rgba(0,0,0,.15);
transition: left .35s cubic-bezier(.4,0,.2,1);
display: flex; flex-direction: column;
}
#sidebar.open { left: 0; }
.sb-head {
background: linear-gradient(135deg, var(--blue) 0%, var(--blue-dk) 100%);
color: white; padding: 20px;
display: flex; justify-content: space-between; align-items: center;
}
.sb-head h2 { font-size: 16px; font-weight: 700; }
.sb-head span { font-size: 11px; opacity: .8; margin-top: 2px; display: block; }
.sb-close {
background: rgba(255,255,255,.2); border: none; color: white;
width: 32px; height: 32px; border-radius: 8px; cursor: pointer; font-size: 18px;
display: flex; align-items: center; justify-content: center; transition: .2s;
}
.sb-close:hover { background: rgba(255,255,255,.35); }
.sb-tabs {
display: flex; border-bottom: 2px solid #e2e8f0;
}
.sb-tab {
flex: 1; padding: 12px; font-size: 12px; font-weight: 600; cursor: pointer;
border: none; background: transparent; color: var(--gray); transition: .2s;
border-bottom: 2px solid transparent; margin-bottom: -2px;
}
.sb-tab.active { color: var(--blue); border-bottom-color: var(--blue); }
.sb-body { flex: 1; overflow-y: auto; padding: 16px; }
/* Stat cards */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
.stat-card {
background: var(--gray-lt); border-radius: var(--radius-sm); padding: 14px;
text-align: center;
}
.stat-card .num { font-size: 28px; font-weight: 800; line-height: 1; margin-bottom: 4px; }
.stat-card .lbl { font-size: 11px; color: var(--gray); font-weight: 500; }
.stat-card.blue-card { background: var(--blue-lt); }
.stat-card.blue-card .num { color: var(--blue); }
.stat-card.green-card { background: var(--green-lt); }
.stat-card.green-card .num { color: var(--green); }
.stat-card.red-card { background: var(--red-lt); }
.stat-card.red-card .num { color: var(--red); }
.stat-card.orange-card{ background: var(--orange-lt); }
.stat-card.orange-card .num { color: var(--orange); }
/* Coverage bar */
.coverage-wrap { margin-bottom: 16px; }
.coverage-label { font-size: 12px; font-weight: 600; margin-bottom: 6px; color: var(--dark); display: flex; justify-content: space-between; }
.coverage-bar { height: 10px; background: var(--red-lt); border-radius: 99px; overflow: hidden; }
.coverage-fill { height: 100%; background: linear-gradient(90deg, var(--green), #22c55e); border-radius: 99px; transition: width .6s; }
/* Ibadah card in sidebar */
.ibadah-item {
border: 1px solid #e2e8f0; border-radius: var(--radius-sm); padding: 12px;
margin-bottom: 10px; transition: .2s;
}
.ibadah-item:hover { border-color: var(--blue); box-shadow: 0 0 0 2px var(--blue-lt); }
.ibadah-item h4 { font-size: 13px; font-weight: 700; color: var(--dark); margin-bottom: 4px; }
.ibadah-item .meta { font-size: 11px; color: var(--gray); margin-bottom: 8px; }
.ibadah-item .pills { display: flex; gap: 6px; flex-wrap: wrap; }
.pill {
font-size: 10px; font-weight: 600; padding: 3px 8px; border-radius: 99px;
display: inline-flex; align-items: center; gap: 4px;
}
.pill.blue { background: var(--blue-lt); color: var(--blue); }
.pill.green { background: var(--green-lt); color: var(--green); }
.pill.red { background: var(--red-lt); color: var(--red); }
/* Warga list */
.warga-list { margin-top: 8px; border-top: 1px dashed #e2e8f0; padding-top: 8px; }
.warga-list ul { padding-left: 16px; }
.warga-list li { font-size: 11px; color: #475569; line-height: 1.8; }
.warga-empty { font-size: 11px; color: #94a3b8; font-style: italic; }
/* ══════════════════════════════════════
POPUP / FORM
══════════════════════════════════════ */
.leaflet-popup-content-wrapper {
border-radius: var(--radius) !important;
box-shadow: var(--shadow) !important;
padding: 0 !important;
overflow: hidden;
}
.leaflet-popup-content { margin: 0 !important; }
.popup-wrap { width: 280px; }
.popup-head {
padding: 14px 16px; font-weight: 700; font-size: 14px;
display: flex; align-items: center; gap: 8px; color: white;
}
.popup-head.blue { background: linear-gradient(135deg, var(--blue), var(--blue-dk)); }
.popup-head.orange { background: linear-gradient(135deg, #f97316, #ea580c); }
.popup-head.green { background: linear-gradient(135deg, var(--green), #15803d); }
.popup-head.red { background: linear-gradient(135deg, var(--red), #b91c1c); }
.popup-body { padding: 14px 16px; }
.form-row { margin-bottom: 12px; }
.form-row label {
display: block; font-size: 10px; font-weight: 700; color: var(--gray);
text-transform: uppercase; letter-spacing: .5px; margin-bottom: 5px;
}
.form-row input,
.form-row select,
.form-row textarea {
width: 100%; padding: 9px 11px; border: 1.5px solid #e2e8f0; border-radius: var(--radius-sm);
font-family: 'Inter'; font-size: 13px; color: var(--dark); transition: .2s;
background: var(--white);
}
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
outline: none; border-color: var(--blue); box-shadow: 0 0 0 3px var(--blue-lt);
}
.form-row input.ro { background: var(--gray-lt); color: var(--gray); font-size: 11px; cursor: default; }
.form-row textarea { resize: vertical; min-height: 60px; }
.form-2col { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
/* Detail row in popup */
.detail-row { display: flex; justify-content: space-between; align-items: center; padding: 5px 0; border-bottom: 1px solid #f1f5f9; font-size: 12px; }
.detail-row:last-child { border-bottom: none; }
.detail-row .dk { color: var(--gray); font-weight: 500; }
.detail-row .dv { font-weight: 600; color: var(--dark); text-align: right; max-width: 55%; }
/* Buttons */
.btn-row { display: flex; gap: 7px; margin-top: 12px; }
.btn {
flex: 1; padding: 9px 12px; font-family: 'Inter'; font-size: 12px; font-weight: 700;
border: none; border-radius: var(--radius-sm); cursor: pointer; transition: .2s;
display: flex; align-items: center; justify-content: center; gap: 5px;
}
.btn:hover { transform: translateY(-1px); }
.btn-primary { background: var(--blue); color: white; }
.btn-primary:hover { background: var(--blue-dk); }
.btn-success { background: var(--green); color: white; }
.btn-danger { background: var(--red); color: white; }
.btn-warning { background: var(--orange); color: white; }
.btn-ghost { background: var(--gray-lt); color: var(--dark); }
.btn-ghost:hover { background: #e2e8f0; }
/* Slider */
.slider-wrap { margin-top: 10px; }
.slider-wrap label { font-size: 11px; font-weight: 600; color: var(--gray); display: flex; justify-content: space-between; margin-bottom: 5px; }
.radius-num { color: var(--blue); font-weight: 700; }
input[type=range] {
width: 100%; accent-color: var(--blue); cursor: pointer;
}
/* Badge status */
.badge {
display: inline-flex; align-items: center; gap: 4px;
font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 99px;
}
.badge.terjangkau { background: var(--green-lt); color: var(--green); }
.badge.belum { background: var(--red-lt); color: var(--red); }
/* ══════════════════════════════════════
FLOATING LEGEND
══════════════════════════════════════ */
#legend {
position: absolute; bottom: 30px; right: 16px; z-index: 900;
background: white; border-radius: var(--radius); padding: 14px 16px;
box-shadow: var(--shadow); font-size: 12px; min-width: 180px;
}
#legend h4 { font-size: 11px; font-weight: 700; color: var(--gray); text-transform: uppercase; letter-spacing: .5px; margin-bottom: 10px; }
.leg-item { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; font-size: 12px; font-weight: 500; }
.leg-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.leg-rect { width: 20px; height: 10px; border-radius: 3px; border: 2px solid; flex-shrink: 0; }
/* ══════════════════════════════════════
MODE HINT
══════════════════════════════════════ */
#mode-hint {
position: absolute; bottom: 30px; left: 50%; transform: translateX(-50%);
z-index: 900; background: rgba(15,23,42,.85); color: white;
padding: 10px 20px; border-radius: 99px; font-size: 12px; font-weight: 500;
backdrop-filter: blur(4px); pointer-events: none; transition: opacity .3s;
}
#mode-hint.hidden { opacity: 0; }
/* ══════════════════════════════════════
TOAST
══════════════════════════════════════ */
#toast-wrap { position: absolute; top: 80px; right: 16px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
.toast {
background: white; border-radius: var(--radius-sm); padding: 12px 16px;
box-shadow: var(--shadow); font-size: 13px; font-weight: 600;
display: flex; align-items: center; gap: 8px; min-width: 220px;
animation: slideIn .3s ease; border-left: 4px solid var(--blue);
}
.toast.success { border-left-color: var(--green); }
.toast.error { border-left-color: var(--red); }
@keyframes slideIn { from { transform: translateX(60px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* ══════════════════════════════════════
ROLE MODAL
══════════════════════════════════════ */
#role-modal {
position: fixed; inset: 0; z-index: 99999;
background: rgba(15,23,42,.8); backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
}
.role-modal-box {
background: white; border-radius: 20px; padding: 40px;
max-width: 480px; width: 90%; box-shadow: 0 20px 60px rgba(0,0,0,.3); text-align: center;
}
.role-modal-box h1 { font-size: 22px; font-weight: 800; color: var(--dark); margin-bottom: 6px; }
.role-modal-box p { font-size: 13px; color: var(--gray); margin-bottom: 28px; }
.role-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.role-card {
padding: 20px 12px; border-radius: var(--radius); border: 2px solid #e2e8f0;
cursor: pointer; transition: .2s; text-align: center;
}
.role-card:hover { border-color: var(--blue); background: var(--blue-lt); transform: translateY(-3px); }
.role-card .ic { font-size: 32px; margin-bottom: 8px; }
.role-card .rc-name { font-size: 13px; font-weight: 700; color: var(--dark); margin-bottom: 4px; }
.role-card .rc-desc { font-size: 10px; color: var(--gray); line-height: 1.5; }
/* Scrollbar */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 99px; }
</style>
</head>
<body>
<!-- ══════════════════════════════════════
ROLE SELECTION MODAL
══════════════════════════════════════ -->
<div id="role-modal">
<div class="role-modal-box">
<div style="font-size:40px;margin-bottom:12px;">🗺️</div>
<h1>WebGIS Poverty Mapping</h1>
<p>Sistem Geospasial Pengentasan Kemiskinan Berbasis Partisipasi Masyarakat<br>Pilih peran Anda untuk melanjutkan</p>
<div class="role-cards">
<div class="role-card" onclick="pilihRole('relawan')">
<div class="ic">🙋</div>
<div class="rc-name">Relawan</div>
<div class="rc-desc">Surveyor lapangan mendata rumah warga miskin</div>
</div>
<div class="role-card" onclick="pilihRole('pengurus')">
<div class="ic">🕌</div>
<div class="rc-name">Pengurus</div>
<div class="rc-desc">Operator rumah ibadah kelola titik & radius bantuan</div>
</div>
<div class="role-card" onclick="pilihRole('pemerintah')">
<div class="ic">🏛️</div>
<div class="rc-name">Pemerintah</div>
<div class="rc-desc">Dinsos / eksekutif pantau & analisis data</div>
</div>
</div>
</div>
</div>
<!-- ══════════════════════════════════════
TOP BAR
══════════════════════════════════════ -->
<div id="topbar">
<div class="brand">
<span class="brand-icon">📍</span>
<div class="brand-text">
<strong>WebGIS Poverty Mapping</strong>
<span>Universitas Tanjungpura</span>
</div>
</div>
<div class="mode-pills" id="mode-pills" style="display:none">
<!-- diisi JS sesuai role -->
</div>
<div class="spacer"></div>
<div class="role-badge" id="role-badge" style="display:none">
<div class="role-dot" id="role-dot"></div>
<span id="role-label">-</span>
</div>
<button class="tb-btn" onclick="toggleSidebar()">
<i class="fa fa-chart-bar"></i> Statistik & Data
</button>
<button class="tb-btn" onclick="pilihRole(null)" title="Ganti Peran">
<i class="fa fa-user-circle"></i>
</button>
</div>
<!-- ══════════════════════════════════════
SIDEBAR
══════════════════════════════════════ -->
<div id="sidebar">
<div class="sb-head">
<div>
<h2>📊 Data & Statistik</h2>
<span>WebGIS Poverty Mapping</span>
</div>
<button class="sb-close" onclick="toggleSidebar()"></button>
</div>
<div class="sb-tabs">
<button class="sb-tab active" onclick="switchTab('ringkasan')">Ringkasan</button>
<button class="sb-tab" onclick="switchTab('ibadah')">Rumah Ibadah</button>
<button class="sb-tab" onclick="switchTab('miskin')">Data KPM</button>
</div>
<div class="sb-body" id="sb-body"></div>
</div>
<!-- ══════════════════════════════════════
MAP
══════════════════════════════════════ -->
<div id="map"></div>
<!-- ══════════════════════════════════════
LEGEND
══════════════════════════════════════ -->
<div id="legend">
<h4>Legenda</h4>
<div class="leg-item"><div class="leg-dot" style="background:#2563eb"></div> Rumah Ibadah</div>
<div class="leg-item"><div class="leg-rect" style="background:rgba(37,99,235,.15);border-color:#2563eb"></div> Radius Bantuan</div>
<div class="leg-item"><div class="leg-dot" style="background:#16a34a"></div> KPM Terjangkau</div>
<div class="leg-item"><div class="leg-dot" style="background:#dc2626"></div> KPM Blank Spot</div>
</div>
<!-- Mode hint -->
<div id="mode-hint" class="hidden">Klik peta untuk menambah data</div>
<!-- Toast container -->
<div id="toast-wrap"></div>
<!-- ══════════════════════════════════════
LEAFLET + SCRIPTS
══════════════════════════════════════ -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ════════════════════════════════════════
// STATE
// ════════════════════════════════════════
var currentRole = null;
var currentMode = null; // 'ibadah' | 'miskin' | 'view'
var currentTab = 'ringkasan';
var dataIbadah = {};
var dataMiskin = {};
var sidebarOpen = false;
// ════════════════════════════════════════
// MAP SETUP
// ════════════════════════════════════════
var streetLayer = L.tileLayer(
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',
{ attribution: '© OpenStreetMap contributors © CARTO', subdomains: 'abcd', maxZoom: 20 }
);
var satelliteLayer = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{ attribution: '© Esri', maxZoom: 20 }
);
var topoLayer = L.tileLayer(
'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
{ attribution: '© OpenTopoMap', maxZoom: 17 }
);
var map = L.map('map', {
center: [-0.0263, 109.3425],
zoom: 14,
layers: [streetLayer],
zoomControl: false
});
L.control.zoom({ position: 'bottomleft' }).addTo(map);
L.control.layers({
"🗺️ Peta Jalan": streetLayer,
"🌍 Peta Satelit": satelliteLayer,
"🏔️ Peta Topografi": topoLayer
}, {}, { position: 'topright' }).addTo(map);
// ════════════════════════════════════════
// ICONS
// ════════════════════════════════════════
function makeIcon(color) {
return new L.Icon({
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${color}.png`,
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
}
var iconBlue = makeIcon('blue');
var iconGreen = makeIcon('green');
var iconRed = makeIcon('red');
var iconOrange = makeIcon('orange');
// ════════════════════════════════════════
// ROLE MANAGEMENT
// ════════════════════════════════════════
var ROLES = {
relawan: {
label: 'Relawan Lapangan', dot: 'relawan', emoji: '🙋',
modes: [{ id: 'miskin', label: '🏠 Tambah KPM', cls: '' }, { id: 'view', label: '👁️ Lihat Saja', cls: '' }]
},
pengurus: {
label: 'Pengurus Rumah Ibadah', dot: 'pengurus', emoji: '🕌',
modes: [{ id: 'ibadah', label: '🕌 Tambah Ibadah', cls: '' }, { id: 'view', label: '👁️ Lihat Saja', cls: '' }]
},
pemerintah: {
label: 'Pemerintah / Dinsos', dot: 'pemerintah', emoji: '🏛️',
modes: [{ id: 'view', label: '👁️ Mode Pantau', cls: 'green' }]
}
};
function pilihRole(role) {
if (!role) {
document.getElementById('role-modal').style.display = 'flex';
document.getElementById('role-badge').style.display = 'none';
document.getElementById('mode-pills').style.display = 'none';
document.getElementById('mode-hint').classList.add('hidden');
currentRole = null; currentMode = null;
return;
}
currentRole = role;
var cfg = ROLES[role];
// Badge
var badge = document.getElementById('role-badge');
badge.style.display = 'flex';
document.getElementById('role-dot').className = 'role-dot ' + cfg.dot;
document.getElementById('role-label').textContent = cfg.emoji + ' ' + cfg.label;
// Mode pills
var pills = document.getElementById('mode-pills');
pills.innerHTML = '';
cfg.modes.forEach(m => {
var btn = document.createElement('button');
btn.className = 'mode-pill' + (m.cls ? ' ' + m.cls : '');
btn.textContent = m.label;
btn.dataset.mode = m.id;
btn.onclick = () => setMode(m.id);
pills.appendChild(btn);
});
pills.style.display = 'flex';
// Default mode
setMode(cfg.modes[0].id);
document.getElementById('role-modal').style.display = 'none';
}
function setMode(mode) {
currentMode = mode;
document.querySelectorAll('.mode-pill').forEach(p => {
p.classList.toggle('active', p.dataset.mode === mode);
});
var hint = document.getElementById('mode-hint');
if (mode === 'view') {
hint.classList.add('hidden');
map.getContainer().style.cursor = '';
} else {
var labels = { ibadah: '🕌 Klik peta untuk menambah Rumah Ibadah', miskin: '🏠 Klik peta untuk menambah data KPM' };
hint.textContent = labels[mode] || '';
hint.classList.remove('hidden');
map.getContainer().style.cursor = 'crosshair';
}
}
// ════════════════════════════════════════
// MAP CLICK
// ════════════════════════════════════════
map.on('click', function(e) {
if (!currentMode || currentMode === 'view') return;
var lat = e.latlng.lat.toFixed(6);
var lng = e.latlng.lng.toFixed(6);
if (currentMode === 'ibadah') bukaFormIbadah(lat, lng, e.latlng);
if (currentMode === 'miskin') bukaFormMiskin(lat, lng, e.latlng);
});
// ════════════════════════════════════════
// FORM — RUMAH IBADAH (TAMBAH)
// ════════════════════════════════════════
function bukaFormIbadah(lat, lng, latlng) {
var html = `
<div class="popup-wrap">
<div class="popup-head blue"><i class="fa fa-mosque"></i> Tambah Rumah Ibadah</div>
<div class="popup-body">
<div class="form-row"><label>Nama Lembaga / Rumah Ibadah *</label>
<input type="text" id="fi_nama" placeholder="cth: Masjid Al-Ikhlas..."></div>
<div class="form-row"><label>Kategori *</label>
<select id="fi_kategori">
<option>Masjid</option><option>Musholla</option><option>Gereja</option>
<option>Pura</option><option>Vihara</option><option>Klenteng</option><option>Lainnya</option>
</select></div>
<div class="form-row"><label>Alamat Lengkap</label>
<input type="text" id="fi_alamat" placeholder="Jl. ..."></div>
<div class="form-2col">
<div class="form-row"><label>Nama Pengurus</label>
<input type="text" id="fi_pengurus" placeholder="Nama ketua"></div>
<div class="form-row"><label>No. Telp</label>
<input type="text" id="fi_telp" placeholder="08xx"></div>
</div>
<div class="form-2col">
<div class="form-row"><label>Kuota Bantuan (KK)</label>
<input type="number" id="fi_kuota" value="20" min="1" max="999"></div>
<div class="form-row"><label>Radius Awal (m)</label>
<input type="number" id="fi_radius" value="300" min="50" max="2000"></div>
</div>
<div class="form-row"><label>Koordinat</label>
<input class="ro" readonly value="${lat}, ${lng}"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="simpanIbadah('${lat}','${lng}')"><i class="fa fa-save"></i> Simpan</button>
</div>
</div>
</div>`;
L.popup({ maxWidth: 320 }).setLatLng(latlng).setContent(html).openOn(map);
}
function simpanIbadah(lat, lng) {
var nama = document.getElementById('fi_nama').value.trim();
if (!nama) { toast('Nama wajib diisi!', 'error'); return; }
var fd = new FormData();
fd.append('aksi', 'simpan_ibadah');
fd.append('nama_ibadah', nama);
fd.append('kategori', document.getElementById('fi_kategori').value);
fd.append('alamat', document.getElementById('fi_alamat').value);
fd.append('nama_pengurus', document.getElementById('fi_pengurus').value);
fd.append('no_telp', document.getElementById('fi_telp').value);
fd.append('kuota_bantuan', document.getElementById('fi_kuota').value);
fd.append('radius_m', document.getElementById('fi_radius').value);
fd.append('lat', lat); fd.append('lng', lng);
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
if (d.status === 'ok') { map.closePopup(); toast('Rumah ibadah berhasil disimpan ✅', 'success'); loadData(); }
else toast('Error: ' + d.message, 'error');
});
}
// ════════════════════════════════════════
// FORM — RUMAH MISKIN / KPM (TAMBAH)
// ════════════════════════════════════════
function bukaFormMiskin(lat, lng, latlng) {
var html = `
<div class="popup-wrap">
<div class="popup-head orange"><i class="fa fa-house"></i> Tambah Data KPM</div>
<div class="popup-body">
<div class="form-row"><label>Nama Kepala Keluarga *</label>
<input type="text" id="fm_nama" placeholder="Nama lengkap..."></div>
<div class="form-2col">
<div class="form-row"><label>NIK (16 digit)</label>
<input type="text" id="fm_nik" maxlength="16" placeholder="NIK"></div>
<div class="form-row"><label>Jml Tanggungan</label>
<input type="number" id="fm_tanggungan" value="1" min="0"></div>
</div>
<div class="form-row"><label>Pendapatan Bulanan (Rp)</label>
<input type="number" id="fm_pendapatan" value="0" min="0" placeholder="0"></div>
<div class="form-row"><label>Kondisi Rumah *</label>
<select id="fm_kondisi">
<option>Tidak Layak</option><option>Sangat Tidak Layak</option><option>Layak</option>
</select></div>
<div class="form-row"><label>No. Telp / WA</label>
<input type="text" id="fm_telp" placeholder="08xx"></div>
<div class="form-row"><label>Keterangan Tambahan</label>
<textarea id="fm_ket" placeholder="Kondisi ekonomi, catatan surveyor..."></textarea></div>
<div class="form-row"><label>Koordinat</label>
<input class="ro" readonly value="${lat}, ${lng}"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="map.closePopup()">Batal</button>
<button class="btn btn-warning" onclick="simpanMiskin('${lat}','${lng}')"><i class="fa fa-save"></i> Simpan</button>
</div>
</div>
</div>`;
L.popup({ maxWidth: 320 }).setLatLng(latlng).setContent(html).openOn(map);
}
function simpanMiskin(lat, lng) {
var nama = document.getElementById('fm_nama').value.trim();
if (!nama) { toast('Nama wajib diisi!', 'error'); return; }
var fd = new FormData();
fd.append('aksi', 'simpan_miskin');
fd.append('nama_kk', nama);
fd.append('nik', document.getElementById('fm_nik').value);
fd.append('jumlah_tanggungan', document.getElementById('fm_tanggungan').value);
fd.append('pendapatan_bulanan',document.getElementById('fm_pendapatan').value);
fd.append('kondisi_rumah', document.getElementById('fm_kondisi').value);
fd.append('no_telp', document.getElementById('fm_telp').value);
fd.append('keterangan', document.getElementById('fm_ket').value);
fd.append('lat', lat); fd.append('lng', lng);
fetch('proses.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(d => {
if (d.status === 'ok') { map.closePopup(); toast('Data KPM berhasil disimpan ✅', 'success'); loadData(); }
else toast('Error: ' + d.message, 'error');
});
}
// ════════════════════════════════════════
// POPUP DETAIL — RUMAH IBADAH
// ════════════════════════════════════════
function popupIbadah(row) {
return `
<div class="popup-wrap">
<div class="popup-head blue"><i class="fa fa-mosque"></i> ${row.kategori} — ${row.nama_ibadah}</div>
<div class="popup-body">
<div class="detail-row"><span class="dk">Pengurus</span><span class="dv">${row.nama_pengurus || '-'}</span></div>
<div class="detail-row"><span class="dk">Alamat</span><span class="dv">${row.alamat || '-'}</span></div>
<div class="detail-row"><span class="dk">No. Telp</span><span class="dv">${row.no_telp || '-'}</span></div>
<div class="detail-row"><span class="dk">Kuota Bantuan</span><span class="dv">${row.kuota_bantuan} KK</span></div>
<div class="slider-wrap">
<label>Radius Jangkauan <span class="radius-num" id="rval_${row.id}">${row.radius_m}</span> m</label>
<input type="range" min="50" max="2000" step="10" value="${row.radius_m}"
oninput="geserRadius(${row.id}, this.value)"
onchange="simpanRadius(${row.id}, this.value)">
</div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="formEditIbadah(${row.id})"><i class="fa fa-edit"></i> Edit</button>
<button class="btn btn-danger" onclick="hapus('ibadah',${row.id})"><i class="fa fa-trash"></i> Hapus</button>
</div>
</div>
</div>`;
}
// ════════════════════════════════════════
// POPUP DETAIL — KPM
// ════════════════════════════════════════
function popupMiskin(row) {
var statusBadge = row.status_bantuan === 'Terjangkau'
? '<span class="badge terjangkau">● Terjangkau</span>'
: '<span class="badge belum">● Blank Spot</span>';
var pendapatanFmt = parseInt(row.pendapatan_bulanan).toLocaleString('id-ID');
return `
<div class="popup-wrap">
<div class="popup-head ${row.status_bantuan === 'Terjangkau' ? 'green' : 'red'}">
<i class="fa fa-house"></i> KPM — ${row.nama_kepala_keluarga}
</div>
<div class="popup-body">
<div style="margin-bottom:8px">${statusBadge}</div>
<div class="detail-row"><span class="dk">NIK</span><span class="dv">${row.nik || '-'}</span></div>
<div class="detail-row"><span class="dk">Tanggungan</span><span class="dv">${row.jumlah_tanggungan} jiwa</span></div>
<div class="detail-row"><span class="dk">Pendapatan</span><span class="dv">Rp ${pendapatanFmt}/bln</span></div>
<div class="detail-row"><span class="dk">Kondisi Rumah</span><span class="dv">${row.kondisi_rumah}</span></div>
<div class="detail-row"><span class="dk">No. Telp</span><span class="dv">${row.no_telp || '-'}</span></div>
${row.keterangan ? `<div class="detail-row"><span class="dk">Catatan</span><span class="dv" style="font-size:11px">${row.keterangan}</span></div>` : ''}
<div class="btn-row">
<button class="btn btn-ghost" onclick="formEditMiskin(${row.id})"><i class="fa fa-edit"></i> Edit</button>
<button class="btn btn-danger" onclick="hapus('miskin',${row.id})"><i class="fa fa-trash"></i> Hapus</button>
</div>
</div>
</div>`;
}
// ════════════════════════════════════════
// FORM EDIT — RUMAH IBADAH
// ════════════════════════════════════════
window.formEditIbadah = function(id) {
var d = dataIbadah[id].data;
var html = `
<div class="popup-wrap">
<div class="popup-head blue"><i class="fa fa-edit"></i> Edit Rumah Ibadah</div>
<div class="popup-body">
<div class="form-row"><label>Nama *</label><input type="text" id="ei_nama" value="${d.nama_ibadah}"></div>
<div class="form-row"><label>Kategori</label>
<select id="ei_kat">
${['Masjid','Musholla','Gereja','Pura','Vihara','Klenteng','Lainnya']
.map(k=>`<option ${k===d.kategori?'selected':''}>${k}</option>`).join('')}
</select></div>
<div class="form-row"><label>Alamat</label><input type="text" id="ei_alamat" value="${d.alamat||''}"></div>
<div class="form-2col">
<div class="form-row"><label>Pengurus</label><input type="text" id="ei_pengurus" value="${d.nama_pengurus||''}"></div>
<div class="form-row"><label>No. Telp</label><input type="text" id="ei_telp" value="${d.no_telp||''}"></div>
</div>
<div class="form-row"><label>Kuota (KK)</label><input type="number" id="ei_kuota" value="${d.kuota_bantuan}"></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="map.closePopup()">Batal</button>
<button class="btn btn-primary" onclick="updateIbadah(${id})"><i class="fa fa-save"></i> Simpan</button>
</div>
</div>
</div>`;
dataIbadah[id].marker.getPopup().setContent(html);
};
window.updateIbadah = function(id) {
var fd = new FormData();
fd.append('aksi','edit_ibadah'); fd.append('id',id);
fd.append('nama_ibadah', document.getElementById('ei_nama').value);
fd.append('kategori', document.getElementById('ei_kat').value);
fd.append('alamat', document.getElementById('ei_alamat').value);
fd.append('nama_pengurus', document.getElementById('ei_pengurus').value);
fd.append('no_telp', document.getElementById('ei_telp').value);
fd.append('kuota_bantuan', document.getElementById('ei_kuota').value);
fetch('proses.php',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{
if(d.status==='ok'){map.closePopup();toast('Data diperbarui ✅','success');loadData();}
});
};
// ════════════════════════════════════════
// FORM EDIT — KPM
// ════════════════════════════════════════
window.formEditMiskin = function(id) {
var d = dataMiskin[id].data;
var html = `
<div class="popup-wrap">
<div class="popup-head orange"><i class="fa fa-edit"></i> Edit Data KPM</div>
<div class="popup-body">
<div class="form-row"><label>Nama KK *</label><input type="text" id="em_nama" value="${d.nama_kepala_keluarga}"></div>
<div class="form-2col">
<div class="form-row"><label>NIK</label><input type="text" id="em_nik" value="${d.nik||''}"></div>
<div class="form-row"><label>Tanggungan</label><input type="number" id="em_tanggungan" value="${d.jumlah_tanggungan}"></div>
</div>
<div class="form-row"><label>Pendapatan (Rp)</label><input type="number" id="em_pendapatan" value="${d.pendapatan_bulanan}"></div>
<div class="form-row"><label>Kondisi Rumah</label>
<select id="em_kondisi">
${['Tidak Layak','Sangat Tidak Layak','Layak']
.map(k=>`<option ${k===d.kondisi_rumah?'selected':''}>${k}</option>`).join('')}
</select></div>
<div class="form-row"><label>No. Telp</label><input type="text" id="em_telp" value="${d.no_telp||''}"></div>
<div class="form-row"><label>Keterangan</label><textarea id="em_ket">${d.keterangan||''}</textarea></div>
<div class="btn-row">
<button class="btn btn-ghost" onclick="map.closePopup()">Batal</button>
<button class="btn btn-warning" onclick="updateMiskin(${id})"><i class="fa fa-save"></i> Simpan</button>
</div>
</div>
</div>`;
dataMiskin[id].marker.getPopup().setContent(html);
};
window.updateMiskin = function(id) {
var fd = new FormData();
fd.append('aksi','edit_miskin'); fd.append('id',id);
fd.append('nama_kk', document.getElementById('em_nama').value);
fd.append('nik', document.getElementById('em_nik').value);
fd.append('jumlah_tanggungan', document.getElementById('em_tanggungan').value);
fd.append('pendapatan_bulanan', document.getElementById('em_pendapatan').value);
fd.append('kondisi_rumah', document.getElementById('em_kondisi').value);
fd.append('no_telp', document.getElementById('em_telp').value);
fd.append('keterangan', document.getElementById('em_ket').value);
fetch('proses.php',{method:'POST',body:fd}).then(r=>r.json()).then(d=>{
if(d.status==='ok'){map.closePopup();toast('Data diperbarui ✅','success');loadData();}
});
};
// ════════════════════════════════════════
// HAPUS
// ════════════════════════════════════════
window.hapus = function(jenis, id) {
if (!confirm('Yakin ingin menghapus data ini? Tindakan ini tidak bisa dibatalkan.')) return;
var fd = new FormData();
fd.append('aksi','hapus'); fd.append('jenis',jenis); fd.append('id',id);
fetch('proses.php',{method:'POST',body:fd}).then(()=>{
map.closePopup();
toast('Data berhasil dihapus 🗑️','success');
loadData();
});
};
// ════════════════════════════════════════
// RADIUS INTERAKTIF
// ════════════════════════════════════════
window.geserRadius = function(id, r) {
var el = document.getElementById('rval_' + id);
if (el) el.innerText = r;
if (dataIbadah[id]) dataIbadah[id].circle.setRadius(parseInt(r));
kalkulasi();
};
window.simpanRadius = function(id, r) {
var fd = new FormData();
fd.append('aksi','update_radius'); fd.append('id',id); fd.append('radius',r);
fetch('proses.php',{method:'POST',body:fd});
};
// ════════════════════════════════════════
// LOAD DATA
// ════════════════════════════════════════
function loadData() {
// Bersihkan layer lama
Object.values(dataIbadah).forEach(d => { map.removeLayer(d.marker); map.removeLayer(d.circle); });
Object.values(dataMiskin).forEach(d => { map.removeLayer(d.marker); });
dataIbadah = {}; dataMiskin = {};
fetch('proses.php?aksi=tampil_semua')
.then(r => r.json())
.then(data => {
// ── Rumah Ibadah ──
data.ibadah.forEach(row => {
var ll = [row.lat, row.lng];
var circle = L.circle(ll, {
color: '#2563eb', fillColor: '#3b82f6',
fillOpacity: 0.1, weight: 2, radius: row.radius_m
}).addTo(map);
var marker = L.marker(ll, { icon: iconBlue, draggable: true }).addTo(map);
marker.bindPopup(popupIbadah(row), { maxWidth: 320 });
marker.on('drag', e => { circle.setLatLng(e.target.getLatLng()); kalkulasi(); });
marker.on('dragend', e => {
var p = e.target.getLatLng();
var fd = new FormData();
fd.append('aksi','update_coords'); fd.append('jenis','ibadah');
fd.append('id',row.id); fd.append('lat',p.lat); fd.append('lng',p.lng);
fetch('proses.php',{method:'POST',body:fd});
});
dataIbadah[row.id] = { marker, circle, data: row };
});
// ── Rumah Miskin ──
data.miskin.forEach(row => {
var ll = [row.lat, row.lng];
var marker = L.marker(ll, { icon: iconRed, draggable: true }).addTo(map);
marker.bindPopup(popupMiskin(row), { maxWidth: 320 });
marker.on('drag', () => kalkulasi());
marker.on('dragend', e => {
var p = e.target.getLatLng();
var fd = new FormData();
fd.append('aksi','update_coords'); fd.append('jenis','miskin');
fd.append('id',row.id); fd.append('lat',p.lat); fd.append('lng',p.lng);
fetch('proses.php',{method:'POST',body:fd});
});
dataMiskin[row.id] = { marker, data: row };
});
kalkulasi();
});
}
// ════════════════════════════════════════
// KALKULASI RADIUS
// ════════════════════════════════════════
function kalkulasi() {
var updates = [];
var statsMap = {};
Object.values(dataIbadah).forEach(ib => {
statsMap[ib.data.id] = { nama: ib.data.nama_ibadah, warga: [], radius: ib.circle.getRadius() };
});
Object.values(dataMiskin).forEach(mis => {
var ll = mis.marker.getLatLng();
var masuk = false;
var namaWarga = mis.data.nama_kepala_keluarga;
Object.values(dataIbadah).forEach(ib => {
if (!masuk && ll.distanceTo(ib.marker.getLatLng()) <= ib.circle.getRadius()) {
masuk = true;
statsMap[ib.data.id].warga.push(namaWarga);
}
});
var statusBaru = masuk ? 'Terjangkau' : 'Belum Terbantu';
mis.marker.setIcon(masuk ? iconGreen : iconRed);
mis.data.status_bantuan = statusBaru;
// Perbarui popup
mis.marker.setPopupContent(popupMiskin(mis.data));
updates.push({ id: mis.data.id, status: statusBaru });
});
// Simpan status ke DB
if (updates.length > 0) {
fetch('proses.php?aksi=update_status_bantuan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
});
}
renderSidebar(statsMap);
}
// ════════════════════════════════════════
// SIDEBAR
// ════════════════════════════════════════
function toggleSidebar() {
sidebarOpen = !sidebarOpen;
document.getElementById('sidebar').classList.toggle('open', sidebarOpen);
renderSidebar();
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.sb-tab').forEach((t,i) => {
t.classList.toggle('active', ['ringkasan','ibadah','miskin'][i] === tab);
});
renderSidebar();
}
function renderSidebar(statsMap) {
if (!sidebarOpen) return;
statsMap = statsMap || {};
var totalIbadah = Object.keys(dataIbadah).length;
var totalMiskin = Object.keys(dataMiskin).length;
var totalMasuk = Object.values(dataMiskin).filter(d => d.data.status_bantuan === 'Terjangkau').length;
var totalBlank = totalMiskin - totalMasuk;
var pct = totalMiskin > 0 ? Math.round(totalMasuk / totalMiskin * 100) : 0;
var body = document.getElementById('sb-body');
if (currentTab === 'ringkasan') {
body.innerHTML = `
<div class="stat-grid">
<div class="stat-card blue-card"><div class="num">${totalIbadah}</div><div class="lbl">🕌 Rumah Ibadah</div></div>
<div class="stat-card orange-card"><div class="num">${totalMiskin}</div><div class="lbl">🏠 Total KPM</div></div>
<div class="stat-card green-card"><div class="num">${totalMasuk}</div><div class="lbl">✅ Terjangkau</div></div>
<div class="stat-card red-card"><div class="num">${totalBlank}</div><div class="lbl">🔴 Blank Spot</div></div>
</div>
<div class="coverage-wrap">
<div class="coverage-label"><span>Cakupan Bantuan</span><span style="color:var(--green)">${pct}%</span></div>
<div class="coverage-bar"><div class="coverage-fill" style="width:${pct}%"></div></div>
</div>
<h4 style="font-size:12px;font-weight:700;color:var(--gray);margin-bottom:10px;text-transform:uppercase;letter-spacing:.5px">Rincian Per Ibadah</h4>
${Object.values(statsMap).length === 0
? Object.values(dataIbadah).map(ib => renderIbadahItem(ib.data.id, ib.data.nama_ibadah, ib.circle.getRadius(), [])).join('')
: Object.entries(statsMap).map(([id, st]) => renderIbadahItem(id, st.nama, st.radius, st.warga)).join('')
}
${totalIbadah === 0 ? '<p style="color:var(--gray);font-size:12px;text-align:center;margin-top:20px">Belum ada data. Mulai dengan menambah rumah ibadah.</p>' : ''}
`;
}
else if (currentTab === 'ibadah') {
var list = Object.values(dataIbadah);
if (!list.length) { body.innerHTML = '<p style="color:var(--gray);font-size:12px;text-align:center;margin-top:30px">Belum ada data rumah ibadah.</p>'; return; }
body.innerHTML = list.map(d => {
var row = d.data;
return `<div class="ibadah-item">
<h4>${row.nama_ibadah}</h4>
<div class="meta">${row.kategori} · ${row.alamat || 'Alamat belum diisi'}</div>
<div class="pills">
<span class="pill blue"><i class="fa fa-user"></i> ${row.nama_pengurus || '-'}</span>
<span class="pill blue"><i class="fa fa-users"></i> Kuota ${row.kuota_bantuan} KK</span>
<span class="pill blue"><i class="fa fa-circle"></i> R=${row.radius_m}m</span>
</div>
</div>`;
}).join('');
}
else if (currentTab === 'miskin') {
var list = Object.values(dataMiskin);
if (!list.length) { body.innerHTML = '<p style="color:var(--gray);font-size:12px;text-align:center;margin-top:30px">Belum ada data KPM.</p>'; return; }
body.innerHTML = list.map(d => {
var row = d.data;
var terjangkau = row.status_bantuan === 'Terjangkau';
var pend = parseInt(row.pendapatan_bulanan).toLocaleString('id-ID');
return `<div class="ibadah-item" onclick="map.setView([${row.lat},${row.lng}],17)" style="cursor:pointer">
<h4 style="display:flex;justify-content:space-between;align-items:center">
${row.nama_kepala_keluarga}
<span class="badge ${terjangkau ? 'terjangkau' : 'belum'}">${terjangkau ? '●' : '●'} ${terjangkau ? 'Terjangkau' : 'Blank Spot'}</span>
</h4>
<div class="meta">NIK: ${row.nik || '-'} · ${row.kondisi_rumah}</div>
<div class="pills">
<span class="pill ${terjangkau ? 'green' : 'red'}"><i class="fa fa-users"></i> ${row.jumlah_tanggungan} jiwa</span>
<span class="pill blue">Rp ${pend}/bln</span>
</div>
</div>`;
}).join('');
}
}
function renderIbadahItem(id, nama, radius, warga) {
var wargaHtml = warga.length > 0
? `<div class="warga-list"><ul>${warga.map(n=>`<li>${n}</li>`).join('')}</ul></div>`
: `<div class="warga-list"><span class="warga-empty">Belum ada KPM dalam radius</span></div>`;
return `<div class="ibadah-item">
<h4>🕌 ${nama}</h4>
<div class="pills" style="margin-bottom:6px">
<span class="pill blue">R = ${radius} m</span>
<span class="pill green">${warga.length} KK terjangkau</span>
</div>
${wargaHtml}
</div>`;
}
// ════════════════════════════════════════
// TOAST NOTIFICATION
// ════════════════════════════════════════
function toast(msg, type) {
var icons = { success: '✅', error: '❌', info: '️' };
var el = document.createElement('div');
el.className = 'toast ' + (type || '');
el.innerHTML = `<span>${icons[type] || ''}</span> ${msg}`;
document.getElementById('toast-wrap').appendChild(el);
setTimeout(() => el.remove(), 3500);
}
// ════════════════════════════════════════
// INIT
// ════════════════════════════════════════
loadData();
</script>
</body>
</html>