917 lines
36 KiB
HTML
917 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="id">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<title>SIG Pendataan Warga Kurang Mampu - Pontianak</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font-family: Arial; }
|
||
#map { height: 100vh; }
|
||
|
||
/* ══════════════════════════════════════
|
||
TOPBAR
|
||
══════════════════════════════════════ */
|
||
#topbar {
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 46px;
|
||
background: linear-gradient(90deg, #1a3a5c, #2e6da4);
|
||
z-index: 2000;
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 14px;
|
||
gap: 10px;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
|
||
}
|
||
|
||
#topbar .tb-logo { font-size: 20px; }
|
||
|
||
#topbar .tb-title {
|
||
color: white;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
flex: 1;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
#topbar .tb-badge {
|
||
background: rgba(255,255,255,0.18);
|
||
color: white;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 3px 9px;
|
||
border-radius: 20px;
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#topbar .tb-badge.admin-badge { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.5); }
|
||
#topbar .tb-badge.user-badge { background: rgba(255,255,255,0.15); }
|
||
|
||
#topbar .tb-user {
|
||
color: rgba(255,255,255,0.85);
|
||
font-size: 12px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#topbar .tb-logout {
|
||
background: rgba(255,255,255,0.15);
|
||
border: 1px solid rgba(255,255,255,0.3);
|
||
color: white;
|
||
padding: 5px 12px;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
transition: background 0.2s;
|
||
white-space: nowrap;
|
||
}
|
||
#topbar .tb-logout:hover { background: rgba(255,255,255,0.28); }
|
||
|
||
/* ══════════════════════════════════════
|
||
INFO BANNER (khusus pengguna biasa)
|
||
══════════════════════════════════════ */
|
||
#infoBanner {
|
||
position: absolute;
|
||
top: 46px; left: 0; right: 0;
|
||
background: linear-gradient(90deg, #e8f4fd, #d0eaf8);
|
||
border-bottom: 1px solid #aed6f1;
|
||
padding: 7px 14px;
|
||
font-size: 12px;
|
||
color: #1a5276;
|
||
z-index: 1500;
|
||
display: none;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* ══════════════════════════════════════
|
||
PANELS — offset turun untuk topbar
|
||
══════════════════════════════════════ */
|
||
.panel-offset { top: 56px; } /* 46px topbar + 10px gap */
|
||
|
||
.control-box {
|
||
position: absolute;
|
||
top: 56px;
|
||
left: 50px;
|
||
background: white;
|
||
padding: 12px;
|
||
z-index: 1000;
|
||
border-radius: 8px;
|
||
width: 240px;
|
||
box-shadow: 0 0 5px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
.control-box label {
|
||
font-size: 14px;
|
||
display: block;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
/* ── readonly-mode: sembunyikan control box ── */
|
||
.control-box.readonly { display: none; }
|
||
|
||
/* ══════════════════════════════════════
|
||
TOMBOL TOGGLE — posisi tetap di kiri,
|
||
tidak ikut panel buka/tutup
|
||
══════════════════════════════════════ */
|
||
.toggle-btn {
|
||
position: absolute;
|
||
left: 8px;
|
||
width: 34px;
|
||
height: 34px;
|
||
background: white;
|
||
border: none;
|
||
border-radius: 6px;
|
||
box-shadow: 0 0 5px rgba(0,0,0,0.3);
|
||
cursor: pointer;
|
||
font-size: 17px;
|
||
z-index: 1100;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.toggle-btn:hover { background: #f0f4f8; }
|
||
|
||
/* Posisi vertikal tiap tombol — Admin (ada control-box) */
|
||
#toggleStatsBtn { top: 172px; }
|
||
#toggleLayerBtn { top: 214px; }
|
||
#toggleHeatmapBtn { top: 256px; }
|
||
|
||
/* Posisi vertikal — Pengguna (tidak ada control-box) */
|
||
body.readonly-mode #toggleStatsBtn { top: 66px; }
|
||
body.readonly-mode #toggleLayerBtn { top: 108px; }
|
||
body.readonly-mode #toggleHeatmapBtn { top: 150px; }
|
||
|
||
/* ══════════════════════════════════════
|
||
PANEL — muncul di samping kanan tombol
|
||
══════════════════════════════════════ */
|
||
.panel-box {
|
||
position: absolute;
|
||
left: 50px;
|
||
background: white;
|
||
padding: 12px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 0 5px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
transition: opacity 0.2s ease, visibility 0.2s ease;
|
||
}
|
||
.panel-box.hidden {
|
||
visibility: hidden;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Ukuran & posisi per panel — Admin */
|
||
.stats-box { top: 166px; width: 240px; font-size: 14px; max-height: 300px; overflow-y: auto; }
|
||
.layer-box { top: 208px; width: 220px; }
|
||
.heatmap-box { top: 250px; width: 220px; }
|
||
|
||
/* Posisi panel — Pengguna */
|
||
body.readonly-mode .stats-box { top: 60px; }
|
||
body.readonly-mode .layer-box { top: 102px; }
|
||
body.readonly-mode .heatmap-box { top: 144px; }
|
||
|
||
/* Tooltip */
|
||
.toggle-btn .tooltip {
|
||
position: absolute;
|
||
left: 40px; top: 50%;
|
||
transform: translateY(-50%);
|
||
background: rgba(0,0,0,0.75);
|
||
color: white;
|
||
padding: 3px 8px;
|
||
border-radius: 4px;
|
||
font-size: 11px;
|
||
white-space: nowrap;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.toggle-btn:hover .tooltip { opacity: 1; }
|
||
|
||
.popup-btn {
|
||
padding: 4px 10px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
margin-top: 4px;
|
||
}
|
||
.popup-btn.edit { background: #eaf2fb; color: #1a3a5c; }
|
||
.popup-btn.hapus { background: #fdecea; color: #c0392b; }
|
||
.popup-btn:hover { opacity: 0.82; }
|
||
|
||
/* ── Readonly notice di popup ── */
|
||
.popup-readonly-note {
|
||
font-size: 11px;
|
||
color: #888;
|
||
font-style: italic;
|
||
margin-top: 6px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.stats-box, .layer-box { left: 10px; width: 220px; }
|
||
#topbar .tb-title { font-size: 12px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
TOPBAR
|
||
══════════════════════════════════════ -->
|
||
<div id="topbar">
|
||
<span class="tb-logo">🗺️</span>
|
||
<span class="tb-title">SIG Pendataan Warga Kurang Mampu – Pontianak</span>
|
||
<span class="tb-badge" id="roleBadge">⬤ –</span>
|
||
<span class="tb-user" id="usernameLabel"></span>
|
||
<button class="tb-logout" onclick="doLogout()">🚪 Keluar</button>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
INFO BANNER (Pengguna biasa)
|
||
══════════════════════════════════════ -->
|
||
<div id="infoBanner">
|
||
ℹ️ Anda masuk sebagai <strong>Pengguna</strong>. Anda hanya dapat melihat data. Hubungi Admin untuk mengubah data.
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
PANEL MODE INPUT (Admin only)
|
||
══════════════════════════════════════ -->
|
||
<div class="control-box" id="controlBox">
|
||
<b>Mode Input:</b>
|
||
<label><input type="radio" name="mode" value="none" checked> Tidak ada</label>
|
||
<label><input type="radio" name="mode" value="ibadah"> Rumah Ibadah</label>
|
||
<label><input type="radio" name="mode" value="warga"> Rumah Warga</label>
|
||
<hr>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
TOMBOL TOGGLE — berdiri sendiri, posisi tetap
|
||
══════════════════════════════════════ -->
|
||
<button id="toggleStatsBtn" class="toggle-btn" onclick="toggleStatistik()" >📊<span class="tooltip">Statistik Analitis</span></button>
|
||
<button id="toggleLayerBtn" class="toggle-btn" onclick="toggleLayerBox()" >🗂️<span class="tooltip">Layer Control</span></button>
|
||
<button id="toggleHeatmapBtn" class="toggle-btn" onclick="toggleHeatmapBox()" >🔥<span class="tooltip">Heatmap</span></button>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
PANEL STATISTIK
|
||
══════════════════════════════════════ -->
|
||
<div class="stats-box panel-box hidden" id="statsBox">
|
||
<b>STATISTIK ANALITIS</b>
|
||
<hr>
|
||
<div id="statistikContent">Memuat statistik...</div>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
PANEL LAYER CONTROL
|
||
══════════════════════════════════════ -->
|
||
<div class="layer-box panel-box hidden" id="layerBox">
|
||
<b>LAYER CONTROL</b>
|
||
<hr>
|
||
<label>
|
||
<input type="checkbox" checked id="checkAllLayer" onchange="toggleSemuaLayer(this)">
|
||
Semua Layer
|
||
</label>
|
||
<hr>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerMasjid)"> Masjid</label><br>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerGereja)"> Gereja</label><br>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerVihara)"> Vihara</label><br>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerPura)"> Pura</label>
|
||
<hr>
|
||
<b>REKOMENDASI</b><br><br>
|
||
<label><input type="checkbox" checked onchange="toggleRekomendasi(this)"> Lokasi Baru</label>
|
||
<hr>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerWargaCover)"> Warga Tercover</label><br>
|
||
<label><input type="checkbox" checked onchange="toggleLayer(this, layerWargaTidakCover)"> Warga Belum Tercover</label>
|
||
</div>
|
||
|
||
<!-- ══════════════════════════════════════
|
||
PANEL HEATMAP
|
||
══════════════════════════════════════ -->
|
||
<div class="heatmap-box panel-box hidden" id="heatmapBox">
|
||
<b>HEATMAP</b>
|
||
<hr>
|
||
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('semua')"> Semua Warga</label><br>
|
||
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('cover')"> Warga Tercover</label><br>
|
||
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('tidakcover')"> Warga Belum Tercover</label><br><br>
|
||
<label><input type="radio" name="heatmapOption" onchange="pilihHeatmap('none')" checked> Nonaktifkan Heatmap</label>
|
||
</div>
|
||
|
||
<div id="map"></div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<script src="https://unpkg.com/leaflet.heat/dist/leaflet-heat.js"></script>
|
||
|
||
<script>
|
||
|
||
// ══════════════════════════════════════
|
||
// AUTENTIKASI — cek sesi
|
||
// ══════════════════════════════════════
|
||
var ROLE = sessionStorage.getItem('sig_role');
|
||
var USERNAME = sessionStorage.getItem('sig_username');
|
||
var LOGGEDIN = sessionStorage.getItem('sig_loggedin');
|
||
var IS_ADMIN = (ROLE === 'admin');
|
||
|
||
if (LOGGEDIN !== '1') {
|
||
// Belum login → redirect ke halaman login
|
||
window.location.href = 'login.html';
|
||
}
|
||
|
||
function doLogout() {
|
||
sessionStorage.clear();
|
||
window.location.href = 'login.html';
|
||
}
|
||
|
||
// ── Tampilkan info user di topbar ──────────────────────────────
|
||
document.getElementById('usernameLabel').textContent = USERNAME || '';
|
||
|
||
var badge = document.getElementById('roleBadge');
|
||
if (IS_ADMIN) {
|
||
badge.innerHTML = '🛡️ Admin';
|
||
badge.classList.add('admin-badge');
|
||
} else {
|
||
badge.innerHTML = '👤 Pengguna';
|
||
badge.classList.add('user-badge');
|
||
}
|
||
|
||
// ── Tampilkan info banner untuk pengguna biasa ─────────────────
|
||
if (!IS_ADMIN) {
|
||
document.getElementById('infoBanner').style.display = 'flex';
|
||
}
|
||
|
||
// ── Atur posisi panel (ada/tidaknya control-box) ───────────────
|
||
if (!IS_ADMIN) {
|
||
// Sembunyikan panel Mode Input
|
||
document.getElementById('controlBox').classList.add('readonly');
|
||
document.body.classList.add('readonly-mode');
|
||
// Geser panel lain ke atas
|
||
document.getElementById('statsBox').classList.add('readonly-pos');
|
||
document.getElementById('layerBox').classList.add('readonly-pos');
|
||
document.getElementById('heatmapBox').classList.add('readonly-pos');
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// INISIALISASI PETA
|
||
// ══════════════════════════════════════
|
||
var map = L.map('map').setView([-0.0263, 109.3425], 13);
|
||
|
||
var ibadahLayers = [];
|
||
var dataIbadah = [];
|
||
var dataWarga = [];
|
||
var heatmapLayer;
|
||
var wargaMarkers = {};
|
||
|
||
var layerRekomendasi = L.layerGroup().addTo(map);
|
||
var layerMasjid = L.layerGroup().addTo(map);
|
||
var layerGereja = L.layerGroup().addTo(map);
|
||
var layerVihara = L.layerGroup().addTo(map);
|
||
var layerPura = L.layerGroup().addTo(map);
|
||
var layerWargaCover = L.layerGroup().addTo(map);
|
||
var layerWargaTidakCover = L.layerGroup().addTo(map);
|
||
var heatmapSemua, heatmapCover, heatmapTidakCover;
|
||
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap'
|
||
}).addTo(map);
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// HELPER FUNGSI IKON
|
||
// ══════════════════════════════════════
|
||
function getMode() {
|
||
return document.querySelector('input[name="mode"]:checked').value;
|
||
}
|
||
|
||
function getColorByJenis(jenis) {
|
||
if (jenis === 'Masjid') return 'green';
|
||
if (jenis === 'Gereja') return 'blue';
|
||
if (jenis === 'Vihara') return 'orange';
|
||
if (jenis === 'Pura') return 'yellow';
|
||
return 'gray';
|
||
}
|
||
|
||
function getIconByJenis(jenis) {
|
||
return new L.Icon({
|
||
iconUrl: `https://maps.google.com/mapfiles/ms/icons/${getColorByJenis(jenis)}-dot.png`,
|
||
iconSize: [32, 32], iconAnchor: [16, 32]
|
||
});
|
||
}
|
||
|
||
function getWargaIcon(tercover = true) {
|
||
return new L.Icon({
|
||
iconUrl: `https://maps.google.com/mapfiles/ms/icons/${tercover ? 'red' : 'purple'}-dot.png`,
|
||
iconSize: [32, 32], iconAnchor: [16, 32]
|
||
});
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// POPUP BUILDER — konten beda per role
|
||
// ══════════════════════════════════════
|
||
function popupIbadah(nama, jenis, leafletId, radius, dbId) {
|
||
var sliderHtml = IS_ADMIN
|
||
? `<input type="range" min="100" max="2000" value="${radius}"
|
||
oninput="updateRadius(this.value, ${leafletId}, ${dbId})"><br><br>
|
||
<button class="popup-btn edit" onclick="editIbadah(${dbId}, ${JSON.stringify(nama)}, ${JSON.stringify(jenis)}, ${radius})">✏️ Edit</button>
|
||
<button class="popup-btn hapus" onclick="hapusIbadah(${dbId})">🗑 Hapus</button>`
|
||
: `<em class="popup-readonly-note">🔒 Hanya Admin yang dapat mengubah data</em>`;
|
||
|
||
return `<b>${nama}</b><br>
|
||
Jenis: ${jenis}<br>
|
||
Radius: <span id="rVal-${leafletId}">${radius}</span> m<br>
|
||
${sliderHtml}`;
|
||
}
|
||
|
||
function popupWarga(nama, jumlah, pengurus, tercover, dbId) {
|
||
var aksiHtml = IS_ADMIN
|
||
? `<button class="popup-btn edit" onclick="editWarga(${dbId}, '${nama.replace(/'/g,"\\'")}', ${jumlah})">✏️ Edit</button>
|
||
<button class="popup-btn hapus" onclick="hapusWarga(${dbId})">🗑 Hapus</button>`
|
||
: `<em class="popup-readonly-note">🔒 Hanya Admin yang dapat mengubah data</em>`;
|
||
|
||
return `<b>Warga Kurang Mampu</b><br>
|
||
Nama KK: ${nama}<br>
|
||
Jumlah KK: ${jumlah}<br>
|
||
Pengurus: ${pengurus}
|
||
${!tercover ? "<br><b style='color:red'>⚠ Belum Tercover</b>" : ''}<br><br>
|
||
${aksiHtml}`;
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// KLIK PETA — hanya Admin
|
||
// ══════════════════════════════════════
|
||
map.on('click', function(e) {
|
||
|
||
if (!IS_ADMIN) return; // Pengguna biasa tidak bisa klik input
|
||
|
||
var mode = getMode();
|
||
if (mode === 'none') { alert('Pilih mode input terlebih dahulu!'); return; }
|
||
|
||
if (mode === 'ibadah') {
|
||
L.popup().setLatLng(e.latlng).setContent(`
|
||
<b>Input Rumah Ibadah</b><br>
|
||
Nama:<br><input type="text" id="namaIbadah"><br><br>
|
||
Jenis:<br>
|
||
<select id="popupJenis">
|
||
<option value="Masjid">Masjid</option>
|
||
<option value="Gereja">Gereja</option>
|
||
<option value="Vihara">Vihara</option>
|
||
<option value="Pura">Pura</option>
|
||
</select><br><br>
|
||
<button onclick="simpanIbadah(${e.latlng.lat}, ${e.latlng.lng})">Simpan</button>
|
||
`).openOn(map);
|
||
}
|
||
|
||
if (mode === 'warga') {
|
||
L.popup().setLatLng(e.latlng).setContent(`
|
||
<b>Input Data Warga</b><br>
|
||
Nama Kepala KK:<br><input type="text" id="namaKK"><br><br>
|
||
Jumlah KK:<br><input type="number" id="jumlahKK"><br><br>
|
||
<button onclick="simpanWarga(${e.latlng.lat}, ${e.latlng.lng})">Simpan</button>
|
||
`).openOn(map);
|
||
}
|
||
});
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// SIMPAN IBADAH
|
||
// ══════════════════════════════════════
|
||
function simpanIbadah(lat, lng) {
|
||
var nama = document.getElementById('namaIbadah').value;
|
||
var jenis = document.getElementById('popupJenis').value;
|
||
|
||
if (!nama.trim()) { alert('Nama wajib diisi'); return; }
|
||
|
||
var color = getColorByJenis(jenis);
|
||
var icon = getIconByJenis(jenis);
|
||
var radius = 500;
|
||
var latlng = [lat, lng];
|
||
|
||
var targetLayer = { Masjid: layerMasjid, Gereja: layerGereja, Vihara: layerVihara, Pura: layerPura }[jenis] || layerMasjid;
|
||
|
||
var circle = L.circle(latlng, { radius, color, fillColor: color, fillOpacity: 0.3 }).addTo(targetLayer);
|
||
var marker = L.marker(latlng, { icon }).addTo(targetLayer);
|
||
var lid = circle._leaflet_id;
|
||
ibadahLayers[lid] = circle;
|
||
|
||
fetch('api/simpan_ibadah.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama, jenis, lat, lng, radius })
|
||
})
|
||
.then(r => r.json())
|
||
.then(result => {
|
||
marker.bindPopup(popupIbadah(nama, jenis, lid, radius, result.id)).openPopup();
|
||
});
|
||
|
||
dataIbadah.push({ nama, jenis, lat, lng, circle });
|
||
updateStatistik();
|
||
map.closePopup();
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// SIMPAN WARGA
|
||
// ══════════════════════════════════════
|
||
function simpanWarga(lat, lng) {
|
||
var nama = document.getElementById('namaKK').value;
|
||
var jumlah = document.getElementById('jumlahKK').value;
|
||
|
||
if (!nama.trim()) { alert('Nama wajib diisi'); return; }
|
||
if (!jumlah || jumlah <= 0) { alert('Jumlah KK tidak valid'); return; }
|
||
|
||
var pengurus = cariPengurus(lat, lng);
|
||
var infoPengurus = pengurus ? `${pengurus.nama} (${pengurus.jenis})` : 'Tidak tercover';
|
||
var tercover = cekTercover(lat, lng);
|
||
|
||
fetch('api/simpan_warga.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama, jumlah, lat, lng, pengurus: infoPengurus })
|
||
})
|
||
.then(r => r.json())
|
||
.then(result => {
|
||
var dbId = result.id;
|
||
|
||
dataWarga.push({ id: dbId, nama, jumlah, lat, lng });
|
||
updateStatistik();
|
||
buatRekomendasiLokasi();
|
||
buatHeatmap();
|
||
|
||
var targetLayer = tercover ? layerWargaCover : layerWargaTidakCover;
|
||
var marker = L.marker([lat, lng], { icon: getWargaIcon(tercover) }).addTo(targetLayer);
|
||
wargaMarkers[dbId] = marker;
|
||
marker.bindPopup(popupWarga(nama, jumlah, infoPengurus, tercover, dbId));
|
||
});
|
||
|
||
map.closePopup();
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// EDIT & HAPUS — hanya dipanggil admin
|
||
// ══════════════════════════════════════
|
||
function editWarga(id, namaLama, jumlahLama) {
|
||
if (!IS_ADMIN) return;
|
||
var namaBaru = prompt('Nama Kepala KK:', namaLama); if (!namaBaru) return;
|
||
var jumlahBaru = prompt('Jumlah KK:', jumlahLama); if (!jumlahBaru) return;
|
||
|
||
fetch('api/update_warga.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, nama: namaBaru, jumlah: jumlahBaru })
|
||
}).then(() => { alert('Data warga berhasil diupdate'); location.reload(); });
|
||
}
|
||
|
||
function editIbadah(id, namaLama, jenisLama, radiusLama) {
|
||
if (!IS_ADMIN) return;
|
||
var namaBaru = prompt('Nama Rumah Ibadah:', namaLama); if (!namaBaru) return;
|
||
var jenisBaru = prompt('Jenis (Masjid/Gereja/Vihara/Pura):', jenisLama); if (!jenisBaru) return;
|
||
var radiusBaru = prompt('Radius:', radiusLama); if (!radiusBaru) return;
|
||
|
||
fetch('api/update_ibadah.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id, nama: namaBaru, jenis: jenisBaru, radius: radiusBaru })
|
||
}).then(() => { alert('Data rumah ibadah berhasil diupdate'); location.reload(); });
|
||
}
|
||
|
||
function hapusIbadah(id) {
|
||
if (!IS_ADMIN) return;
|
||
if (!confirm('Hapus data ini?')) return;
|
||
fetch('api/hapus_ibadah.php?id=' + id).then(() => location.reload());
|
||
}
|
||
|
||
function hapusWarga(id) {
|
||
if (!IS_ADMIN) return;
|
||
if (!confirm('Hapus data ini?')) return;
|
||
fetch('api/hapus_warga.php?id=' + id).then(() => location.reload());
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// UPDATE RADIUS — hanya admin
|
||
// ══════════════════════════════════════
|
||
function updateRadius(val, leafletId, dbId) {
|
||
if (!IS_ADMIN) return;
|
||
|
||
var circle = ibadahLayers[leafletId];
|
||
if (circle) circle.setRadius(Number(val));
|
||
|
||
var text = document.getElementById('rVal-' + leafletId);
|
||
if (text) text.innerHTML = val;
|
||
|
||
fetch('api/update_radius.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: dbId, radius: Number(val) })
|
||
});
|
||
|
||
updatePengurusWarga();
|
||
updateStatistik();
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// LOAD DATA (baca saja — semua role)
|
||
// ══════════════════════════════════════
|
||
window.onload = function() { loadData(); };
|
||
|
||
function loadData() {
|
||
fetch('api/get_data.php')
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
|
||
L.geoJSON(data, {
|
||
pointToLayer: function(feature, latlng) {
|
||
|
||
if (feature.properties.tipe === 'ibadah') {
|
||
var jenis = feature.properties.jenis;
|
||
var color = getColorByJenis(jenis);
|
||
var targetLayer = { Masjid: layerMasjid, Gereja: layerGereja, Vihara: layerVihara, Pura: layerPura }[jenis] || layerMasjid;
|
||
|
||
var marker = L.marker(latlng, { icon: getIconByJenis(jenis) }).addTo(targetLayer);
|
||
var circle = L.circle(latlng, {
|
||
radius: feature.properties.radius,
|
||
color, fillColor: color, fillOpacity: 0.3
|
||
}).addTo(targetLayer);
|
||
|
||
var lid = circle._leaflet_id;
|
||
ibadahLayers[lid] = circle;
|
||
|
||
marker.bindPopup(
|
||
popupIbadah(
|
||
feature.properties.nama,
|
||
jenis,
|
||
lid,
|
||
feature.properties.radius,
|
||
feature.properties.id
|
||
)
|
||
);
|
||
|
||
dataIbadah.push({
|
||
nama: feature.properties.nama,
|
||
jenis,
|
||
lat: latlng.lat,
|
||
lng: latlng.lng,
|
||
circle
|
||
});
|
||
|
||
return marker;
|
||
}
|
||
|
||
if (feature.properties.tipe === 'warga') {
|
||
var tercover = feature.properties.pengurus !== 'Tidak tercover';
|
||
var targetLayer = tercover ? layerWargaCover : layerWargaTidakCover;
|
||
var marker = L.marker(latlng, { icon: getWargaIcon(tercover) }).addTo(targetLayer);
|
||
|
||
wargaMarkers[feature.properties.id] = marker;
|
||
|
||
marker.bindPopup(
|
||
popupWarga(
|
||
feature.properties.nama,
|
||
feature.properties.jumlah_kk,
|
||
feature.properties.pengurus,
|
||
tercover,
|
||
feature.properties.id
|
||
)
|
||
);
|
||
|
||
dataWarga.push({
|
||
id: feature.properties.id,
|
||
nama: feature.properties.nama,
|
||
jumlah: feature.properties.jumlah_kk,
|
||
lat: latlng.lat,
|
||
lng: latlng.lng
|
||
});
|
||
|
||
return marker;
|
||
}
|
||
}
|
||
}).addTo(map);
|
||
|
||
updateStatistik();
|
||
buatRekomendasiLokasi();
|
||
});
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// ANALISIS SPASIAL
|
||
// ══════════════════════════════════════
|
||
function cariPengurus(lat, lng) {
|
||
var terdekat = null, jarakMin = Infinity;
|
||
dataIbadah.forEach(item => {
|
||
if (!item.circle) return;
|
||
var jarak = map.distance([lat, lng], item.circle.getLatLng());
|
||
if (jarak <= item.circle.getRadius() && jarak < jarakMin) {
|
||
jarakMin = jarak; terdekat = item;
|
||
}
|
||
});
|
||
return terdekat;
|
||
}
|
||
|
||
function cekTercover(lat, lng) {
|
||
return dataIbadah.some(ibadah => {
|
||
if (!ibadah.circle) return false;
|
||
return map.distance([lat, lng], ibadah.circle.getLatLng()) <= ibadah.circle.getRadius();
|
||
});
|
||
}
|
||
|
||
function updatePengurusWarga() {
|
||
dataWarga.forEach(warga => {
|
||
var pengurusBaru = 'Tidak tercover';
|
||
dataIbadah.forEach(ibadah => {
|
||
if (!ibadah.circle) return;
|
||
var jarak = map.distance([warga.lat, warga.lng], ibadah.circle.getLatLng());
|
||
if (jarak <= ibadah.circle.getRadius()) {
|
||
pengurusBaru = `${ibadah.nama} (${ibadah.jenis})`;
|
||
}
|
||
});
|
||
|
||
fetch('api/update_pengurus.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ id: warga.id, pengurus: pengurusBaru })
|
||
});
|
||
|
||
var marker = wargaMarkers[warga.id];
|
||
if (!marker) return;
|
||
|
||
var tercover = pengurusBaru !== 'Tidak tercover';
|
||
marker.setIcon(getWargaIcon(tercover));
|
||
layerWargaCover.removeLayer(marker);
|
||
layerWargaTidakCover.removeLayer(marker);
|
||
(tercover ? layerWargaCover : layerWargaTidakCover).addLayer(marker);
|
||
|
||
marker.setPopupContent(
|
||
popupWarga(warga.nama, warga.jumlah, pengurusBaru, tercover, warga.id)
|
||
);
|
||
});
|
||
|
||
buatRekomendasiLokasi();
|
||
buatHeatmap();
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// STATISTIK
|
||
// ══════════════════════════════════════
|
||
function updateStatistik() {
|
||
var totalWarga = dataWarga.length;
|
||
var totalKK = dataWarga.reduce((s, w) => s + Number(w.jumlah || 0), 0);
|
||
|
||
var counts = { Masjid: 0, Gereja: 0, Vihara: 0, Pura: 0 };
|
||
dataIbadah.forEach(i => { if (counts[i.jenis] !== undefined) counts[i.jenis]++; });
|
||
|
||
var ranking = dataIbadah
|
||
.filter(i => i.circle)
|
||
.map(i => ({
|
||
nama: i.nama,
|
||
jenis: i.jenis,
|
||
total: dataWarga.filter(w => map.distance([w.lat, w.lng], i.circle.getLatLng()) <= i.circle.getRadius()).length
|
||
}))
|
||
.sort((a, b) => b.total - a.total);
|
||
|
||
var belum = dataWarga
|
||
.filter(w => !cekTercover(w.lat, w.lng))
|
||
.map(w => w.nama);
|
||
|
||
var html = `
|
||
<b>Total Warga Kurang Mampu:</b><br>${totalWarga}<br><br>
|
||
<b>Total KK:</b><br>${totalKK}<br><br>
|
||
<b>Total Rumah Ibadah:</b><br>
|
||
Masjid: ${counts.Masjid}<br>
|
||
Gereja: ${counts.Gereja}<br>
|
||
Vihara: ${counts.Vihara}<br>
|
||
Pura: ${counts.Pura}<br><br>
|
||
<b>Cakupan Warga Terbanyak:</b><br>
|
||
`;
|
||
ranking.forEach((item, i) => { html += `${i + 1}. ${item.nama} (${item.total} warga)<br>`; });
|
||
html += `<br><b>Warga Belum Tercover:</b><br>`;
|
||
html += belum.length === 0
|
||
? 'Semua warga sudah tercover'
|
||
: belum.map(n => `- ${n}`).join('<br>');
|
||
|
||
document.getElementById('statistikContent').innerHTML = html;
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// HEATMAP
|
||
// ══════════════════════════════════════
|
||
function buatHeatmap() {
|
||
var dataSemua = [], dataCover = [], dataTidakCover = [];
|
||
|
||
dataWarga.forEach(w => {
|
||
var pt = [w.lat, w.lng, (w.jumlah || 1) * 2];
|
||
dataSemua.push(pt);
|
||
(cekTercover(w.lat, w.lng) ? dataCover : dataTidakCover).push(pt);
|
||
});
|
||
|
||
var mk = (data, grad, r = 45, b = 18, mo = 0.5) =>
|
||
L.heatLayer(data, { radius: r, blur: b, maxZoom: 18, minOpacity: mo, gradient: grad });
|
||
|
||
[heatmapSemua, heatmapCover, heatmapTidakCover].forEach(h => { if (h) map.removeLayer(h); });
|
||
|
||
heatmapSemua = mk(dataSemua, { 0.2:'#d6ecff', 0.4:'#a8d8ff', 0.6:'#73c2fb', 0.8:'#3399ff', 1:'#0066cc' });
|
||
heatmapCover = mk(dataCover, { 0.2:'#b3ffb3', 0.4:'#66ff66', 0.6:'#00cc00', 0.8:'#009900', 1:'#006600' });
|
||
heatmapTidakCover = mk(dataTidakCover, { 0.2:'#ffd6d6', 0.4:'#ff8080', 0.6:'#ff3333', 0.8:'#e60000', 1:'#800000' }, 50, 20, 0.6);
|
||
}
|
||
|
||
function pilihHeatmap(jenis) {
|
||
buatHeatmap();
|
||
[heatmapSemua, heatmapCover, heatmapTidakCover].forEach(h => { if (h) map.removeLayer(h); });
|
||
var target = { semua: heatmapSemua, cover: heatmapCover, tidakcover: heatmapTidakCover }[jenis];
|
||
if (target) map.addLayer(target);
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// REKOMENDASI LOKASI
|
||
// ══════════════════════════════════════
|
||
function buatRekomendasiLokasi() {
|
||
layerRekomendasi.clearLayers();
|
||
|
||
var belum = dataWarga.filter(w => !cekTercover(w.lat, w.lng));
|
||
if (belum.length === 0) return;
|
||
|
||
var sumLat = belum.reduce((s, w) => s + w.lat, 0);
|
||
var sumLng = belum.reduce((s, w) => s + w.lng, 0);
|
||
var totalKK = belum.reduce((s, w) => s + (Number(w.jumlah) || 1), 0);
|
||
var centerLat = sumLat / belum.length;
|
||
var centerLng = sumLng / belum.length;
|
||
|
||
var rekIcon = new L.Icon({
|
||
iconUrl: 'https://maps.google.com/mapfiles/kml/paddle/wht-circle.png',
|
||
iconSize: [32, 32], iconAnchor: [16, 32]
|
||
});
|
||
|
||
L.marker([centerLat, centerLng], { icon: rekIcon })
|
||
.bindPopup(`<b>Rekomendasi Lokasi Baru</b><hr>
|
||
Jumlah Warga Belum Tercover: <b>${totalKK}</b><br><br>
|
||
Titik rekomendasi ini merupakan pusat area warga yang belum tercover rumah ibadah.`)
|
||
.addTo(layerRekomendasi);
|
||
}
|
||
|
||
|
||
// ══════════════════════════════════════
|
||
// TOGGLE PANEL
|
||
// ══════════════════════════════════════
|
||
function toggleStatistik() {
|
||
var s = document.getElementById('statsBox');
|
||
var l = document.getElementById('layerBox');
|
||
var h = document.getElementById('heatmapBox');
|
||
var isOpen = !s.classList.contains('hidden');
|
||
s.classList.toggle('hidden');
|
||
if (!isOpen) { l.classList.add('hidden'); h.classList.add('hidden'); }
|
||
}
|
||
|
||
function toggleLayerBox() {
|
||
var l = document.getElementById('layerBox');
|
||
var s = document.getElementById('statsBox');
|
||
var h = document.getElementById('heatmapBox');
|
||
var isOpen = !l.classList.contains('hidden');
|
||
l.classList.toggle('hidden');
|
||
if (!isOpen) { s.classList.add('hidden'); h.classList.add('hidden'); }
|
||
}
|
||
|
||
function toggleHeatmapBox() {
|
||
var h = document.getElementById('heatmapBox');
|
||
var s = document.getElementById('statsBox');
|
||
var l = document.getElementById('layerBox');
|
||
var isOpen = !h.classList.contains('hidden');
|
||
h.classList.toggle('hidden');
|
||
if (!isOpen) { s.classList.add('hidden'); l.classList.add('hidden'); }
|
||
}
|
||
|
||
function toggleLayer(checkbox, layer) {
|
||
checkbox.checked ? map.addLayer(layer) : map.removeLayer(layer);
|
||
}
|
||
|
||
function toggleSemuaLayer(checkbox) {
|
||
document.querySelectorAll('#layerBox input[type="checkbox"]').forEach(cb => cb.checked = checkbox.checked);
|
||
[layerMasjid, layerGereja, layerVihara, layerPura,
|
||
layerWargaCover, layerWargaTidakCover, layerRekomendasi,
|
||
heatmapSemua, heatmapCover, heatmapTidakCover]
|
||
.forEach(layer => { if (!layer) return; checkbox.checked ? map.addLayer(layer) : map.removeLayer(layer); });
|
||
}
|
||
|
||
function toggleRekomendasi(checkbox) {
|
||
checkbox.checked ? map.addLayer(layerRekomendasi) : map.removeLayer(layerRekomendasi);
|
||
}
|
||
|
||
</script>
|
||
</body>
|
||
</html>
|