1100 lines
56 KiB
PHP
1100 lines
56 KiB
PHP
<!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> |