Initial commit — WebGIS UAS GeoMiskin

Sistem WebGIS analisis sebaran penduduk miskin dan jangkauan rumah ibadah.
Fitur: peta interaktif Leaflet.js, CRUD rumah ibadah & penduduk miskin,
auto-kategorisasi haversine, heatmap, export PDF/CSV, dashboard analitik,
manajemen pengguna (superadmin/admin), login sistem.

Teknologi: PHP, MySQL, Leaflet.js, OpenStreetMap, Nominatim Geocoding, mPDF.

Catatan: jalankan `composer install` di folder uas/ untuk install dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Athallah Ghathfan Aqila
2026-06-11 13:31:33 +07:00
commit 6215b4367a
59 changed files with 6154 additions and 0 deletions
+877
View File
@@ -0,0 +1,877 @@
<?php
include 'auth.php';
requireLogin();
$user = currentUser();
$isAdmin = isSuperAdmin();
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoMiskin — WebGIS Sebaran Penduduk Miskin</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" rel="stylesheet">
<link href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" rel="stylesheet">
<style>
:root{--sb:320px;--bg:#0f172a;--bg2:#1e293b;--bg3:#334155;--txt:#e2e8f0;--muted:#64748b;--blue:#3b82f6;--green:#22c55e;--red:#ef4444;--yellow:#f59e0b;--indigo:#6366f1;--radius:12px;--trans:.2s ease}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',sans-serif;height:100vh;overflow:hidden;display:flex}
/* SIDEBAR */
#sidebar{width:var(--sb);min-width:var(--sb);height:100vh;background:var(--bg);color:var(--txt);display:flex;flex-direction:column;overflow-y:auto;overflow-x:hidden;transition:transform var(--trans);z-index:1000;scrollbar-width:thin;scrollbar-color:var(--bg3) transparent}
#sidebar::-webkit-scrollbar{width:4px}
#sidebar::-webkit-scrollbar-thumb{background:var(--bg3);border-radius:4px}
.sb-brand{padding:20px 20px 16px;display:flex;align-items:center;gap:12px;border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0}
.sb-logo{width:40px;height:40px;background:linear-gradient(135deg,var(--blue),var(--indigo));border-radius:10px;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(59,130,246,.4);flex-shrink:0}
.sb-logo i{color:#fff;font-size:18px}
.sb-title{font-weight:700;font-size:16px;line-height:1.2}
.sb-sub{font-size:10px;color:var(--muted);font-weight:400;letter-spacing:.5px;text-transform:uppercase;margin-top:2px}
.sb-user{padding:14px 20px;display:flex;align-items:center;gap:10px;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.06);flex-shrink:0}
.sb-avatar{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--blue),var(--indigo));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:700;font-size:13px;flex-shrink:0}
.sb-uname{font-size:13px;font-weight:600;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sb-role{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}
.btn-logout{background:none;border:none;color:var(--muted);cursor:pointer;padding:6px;border-radius:8px;transition:var(--trans);flex-shrink:0}
.btn-logout:hover{color:var(--red);background:rgba(239,68,68,.1)}
.sb-section{padding:14px 20px 8px;flex-shrink:0}
.sb-section-title{font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase;font-weight:600;margin-bottom:10px}
/* STATS */
.stats-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 20px 14px}
.stat-card{background:var(--bg2);border-radius:var(--radius);padding:12px;border:1px solid rgba(255,255,255,.06);transition:var(--trans)}
.stat-card:hover{border-color:rgba(255,255,255,.12);transform:translateY(-1px)}
.stat-icon{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;margin-bottom:8px}
.stat-icon i{font-size:14px}
.stat-num{font-size:22px;font-weight:700;line-height:1;margin-bottom:3px}
.stat-lbl{font-size:10px;color:var(--muted);font-weight:500;line-height:1.3}
.ic-blue{background:rgba(59,130,246,.2)}.ic-blue i{color:var(--blue)}
.ic-red{background:rgba(239,68,68,.2)}.ic-red i{color:var(--red)}
.ic-green{background:rgba(34,197,94,.2)}.ic-green i{color:var(--green)}
.ic-yellow{background:rgba(245,158,11,.2)}.ic-yellow i{color:var(--yellow)}
/* SEARCH */
.sb-search{padding:0 20px 12px;flex-shrink:0}
.search-wrap{position:relative}
.search-wrap i{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:13px}
.search-wrap input{width:100%;background:var(--bg2);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:10px 10px 10px 36px;color:var(--txt);font-size:13px;font-family:inherit;outline:none;transition:var(--trans)}
.search-wrap input:focus{border-color:var(--blue);background:rgba(59,130,246,.08)}
.search-wrap input::placeholder{color:var(--muted)}
/* FILTER */
.filter-row{display:flex;flex-direction:column;gap:8px;padding:0 20px 12px}
.filter-row select{background:var(--bg2);border:1px solid rgba(255,255,255,.08);border-radius:10px;padding:9px 12px;color:var(--txt);font-size:12px;font-family:inherit;outline:none;cursor:pointer;transition:var(--trans)}
.filter-row select:focus{border-color:var(--blue)}
.filter-row select option{background:var(--bg2);color:var(--txt)}
/* LAYERS */
.layer-list{padding:0 20px 12px;display:flex;flex-direction:column;gap:6px}
.layer-item{display:flex;align-items:center;justify-content:space-between;background:var(--bg2);border-radius:10px;padding:10px 12px;border:1px solid rgba(255,255,255,.06)}
.layer-left{display:flex;align-items:center;gap:8px}
.layer-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
.layer-name{font-size:12px;font-weight:500}
.toggle-switch{position:relative;width:36px;height:20px;flex-shrink:0}
.toggle-switch input{opacity:0;width:0;height:0}
.slider{position:absolute;inset:0;border-radius:20px;background:var(--bg3);cursor:pointer;transition:var(--trans)}
.slider:before{content:'';position:absolute;width:14px;height:14px;border-radius:50%;left:3px;top:3px;background:#fff;transition:var(--trans)}
.toggle-switch input:checked+.slider{background:var(--blue)}
.toggle-switch input:checked+.slider:before{transform:translateX(16px)}
/* ACTIONS */
.action-list{padding:0 20px 12px;display:flex;flex-direction:column;gap:6px}
.action-btn{display:flex;align-items:center;gap:10px;background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:10px;padding:10px 14px;color:var(--txt);font-size:12px;font-weight:500;cursor:pointer;transition:var(--trans);font-family:inherit;width:100%;text-align:left}
.action-btn:hover{border-color:var(--blue);background:rgba(59,130,246,.1);color:var(--blue)}
.action-btn i{width:16px;text-align:center;font-size:13px}
.action-btn.green:hover{border-color:var(--green);background:rgba(34,197,94,.1);color:var(--green)}
.action-btn.yellow:hover{border-color:var(--yellow);background:rgba(245,158,11,.1);color:var(--yellow)}
/* LEGEND */
.legend{padding:0 20px 20px}
.legend-title{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;font-weight:600;margin-bottom:10px}
.legend-items{display:flex;flex-direction:column;gap:6px}
.legend-item{display:flex;align-items:center;gap:8px;font-size:11px;color:var(--muted)}
.legend-dot{width:10px;height:10px;border-radius:50%;border:2px solid rgba(255,255,255,.3);flex-shrink:0}
/* MAP */
#map-container{flex:1;position:relative;min-width:0}
#map{width:100%;height:100%}
#sidebar-toggle{position:absolute;top:16px;left:16px;z-index:500;background:#fff;border:none;border-radius:10px;width:40px;height:40px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.15);transition:var(--trans)}
#sidebar-toggle:hover{background:#f8fafc}
#sidebar-toggle i{font-size:16px;color:#334155}
#pick-hint{position:absolute;top:70px;left:50%;transform:translateX(-50%);background:rgba(15,23,42,.9);color:#fff;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:500;z-index:1000;display:none;pointer-events:none;border:1px solid rgba(255,255,255,.15);backdrop-filter:blur(8px)}
#pick-hint i{color:var(--yellow);margin-right:6px}
.user-dot{width:18px;height:18px;border-radius:50%;background:#3b82f6;border:3px solid #fff;box-shadow:0 0 0 4px rgba(59,130,246,.3);animation:pulse 2s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 4px rgba(59,130,246,.3)}50%{box-shadow:0 0 0 8px rgba(59,130,246,.08)}}
/* MODAL */
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:2000;display:none;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(4px)}
.modal-overlay.open{display:flex}
.modal-box{background:#fff;border-radius:20px;width:100%;max-width:560px;max-height:90vh;overflow-y:auto;box-shadow:0 25px 60px rgba(0,0,0,.4);animation:modalIn .2s ease}
@keyframes modalIn{from{opacity:0;transform:scale(.96) translateY(10px)}to{opacity:1;transform:scale(1) translateY(0)}}
.modal-box.picking{opacity:.15;pointer-events:none}
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid #f1f5f9;position:sticky;top:0;background:#fff;z-index:1}
.modal-head h3{font-size:16px;font-weight:700;color:#0f172a;display:flex;align-items:center;gap:8px}
.modal-head h3 i{color:var(--blue)}
.btn-close-modal{background:none;border:none;cursor:pointer;width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;color:#94a3b8;transition:var(--trans)}
.btn-close-modal:hover{background:#f1f5f9;color:#334155}
.modal-body{padding:20px 24px}
.modal-foot{padding:16px 24px;border-top:1px solid #f1f5f9;display:flex;gap:10px;justify-content:flex-end;position:sticky;bottom:0;background:#fff}
/* FORM */
.fg{margin-bottom:16px}
.fg label{display:block;font-size:12px;font-weight:600;color:#374151;margin-bottom:6px}
.fg label .req{color:var(--red);margin-left:2px}
.fg input,.fg select,.fg textarea{width:100%;border:1.5px solid #e2e8f0;border-radius:10px;padding:10px 14px;font-size:14px;font-family:inherit;color:#0f172a;outline:none;transition:var(--trans);background:#fff}
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--blue);box-shadow:0 0 0 3px rgba(59,130,246,.1)}
.fg .hint{font-size:11px;color:#94a3b8;margin-top:4px}
.row2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.row3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
/* COORD */
.coord-group{background:#f8fafc;border-radius:10px;padding:14px;border:1px solid #e2e8f0;margin-bottom:16px}
.coord-group .coord-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px}
.coord-group label{font-size:11px;font-weight:600;color:#374151;display:block;margin-bottom:5px}
.coord-group input{border:1.5px solid #e2e8f0;border-radius:8px;padding:8px 12px;font-size:13px;font-family:inherit;color:#0f172a;outline:none;transition:var(--trans);width:100%;background:#fff}
.coord-group input:focus{border-color:var(--blue)}
.btn-pick{display:inline-flex;align-items:center;gap:6px;background:var(--blue);color:#fff;border:none;border-radius:8px;padding:8px 14px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
.btn-pick:hover{background:#2563eb;transform:translateY(-1px)}
/* ANGGOTA */
.anggota-wrap{background:#f8fafc;border-radius:10px;padding:14px;border:1px solid #e2e8f0;margin-bottom:16px}
.anggota-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.anggota-head h4{font-size:13px;font-weight:700;color:#0f172a}
.anggota-row{background:#fff;border-radius:8px;border:1px solid #e2e8f0;padding:12px;margin-bottom:8px;position:relative}
.btn-remove-anggota{position:absolute;top:10px;right:10px;background:none;border:none;color:#dc2626;cursor:pointer;padding:4px;border-radius:6px;transition:var(--trans)}
.btn-remove-anggota:hover{background:#fef2f2}
.btn-add-anggota{display:flex;align-items:center;justify-content:center;gap:6px;width:100%;padding:9px;border:1.5px dashed #cbd5e1;border-radius:8px;background:none;color:#64748b;font-size:12px;font-weight:500;cursor:pointer;transition:var(--trans);font-family:inherit;margin-top:8px}
.btn-add-anggota:hover{border-color:var(--blue);color:var(--blue);background:rgba(59,130,246,.05)}
/* STATUS BADGE */
.status-badge{display:inline-flex;align-items:center;gap:5px;padding:4px 10px;border-radius:20px;font-size:11px;font-weight:600}
.sb-belum{background:#fef2f2;color:#dc2626}
.sb-sudah{background:#f0fdf4;color:#16a34a}
.sb-menunggu{background:#fffbeb;color:#d97706}
/* UPLOAD */
.upload-area{border:2px dashed #cbd5e1;border-radius:10px;padding:20px;text-align:center;cursor:pointer;transition:var(--trans);background:#f8fafc}
.upload-area:hover{border-color:var(--blue);background:#eff6ff}
.upload-area i{font-size:24px;color:#94a3b8;margin-bottom:6px;display:block}
.upload-area p{font-size:12px;color:#64748b}
.upload-area input{display:none}
.upload-preview{font-size:12px;color:#374151;margin-top:8px;padding:6px 10px;background:#f1f5f9;border-radius:6px}
/* DIVIDER */
.divider{display:flex;align-items:center;gap:10px;margin:16px 0;font-size:11px;color:#94a3b8;font-weight:500}
.divider::before,.divider::after{content:'';flex:1;height:1px;background:#e2e8f0}
/* BUTTONS */
.btn-primary{background:var(--blue);color:#fff;border:none;border-radius:10px;padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
.btn-primary:hover{background:#2563eb;transform:translateY(-1px)}
.btn-secondary{background:#f1f5f9;color:#374151;border:none;border-radius:10px;padding:10px 20px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
.btn-secondary:hover{background:#e2e8f0}
.btn-danger{background:#fef2f2;color:#dc2626;border:none;border-radius:10px;padding:10px 16px;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;transition:var(--trans)}
.btn-danger:hover{background:#fee2e2}
#bantuan-detail{border-top:1px solid #f1f5f9;padding-top:14px;margin-top:4px;display:none}
/* TOAST */
#toast-wrap{position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px}
.toast{background:#0f172a;color:#fff;padding:12px 18px;border-radius:12px;font-size:13px;font-weight:500;box-shadow:0 8px 24px rgba(0,0,0,.3);display:flex;align-items:center;gap:10px;min-width:240px;max-width:320px;animation:slideUp .25s ease;border-left:4px solid var(--blue)}
.toast.success{border-left-color:var(--green)}
.toast.error{border-left-color:var(--red)}
.toast.warning{border-left-color:var(--yellow)}
@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}
.toast i{font-size:15px;flex-shrink:0}
.toast.success i{color:var(--green)}.toast.error i{color:var(--red)}.toast.warning i{color:var(--yellow)}
@media(max-width:768px){
#sidebar{position:fixed;top:0;left:0;z-index:1000;transform:translateX(-100%)}
#sidebar.open{transform:translateX(0)}
#map-container{width:100%}
}
</style>
</head>
<body>
<!-- ===== SIDEBAR ===== -->
<div id="sidebar">
<div class="sb-brand">
<div class="sb-logo"><i class="fas fa-map-marked-alt"></i></div>
<div><div class="sb-title">GeoMiskin</div><div class="sb-sub">Sebaran Penduduk Miskin</div></div>
</div>
<div class="sb-user">
<div class="sb-avatar"><?= strtoupper(substr($user['nama'],0,1)) ?></div>
<div style="flex:1;min-width:0"><div class="sb-uname"><?= htmlspecialchars($user['nama']) ?></div><div class="sb-role"><?= $user['role'] ?></div></div>
<a href="logout.php" class="btn-logout" title="Logout"><i class="fas fa-sign-out-alt"></i></a>
</div>
<div class="sb-section"><div class="sb-section-title">Ringkasan</div></div>
<div class="stats-grid">
<div class="stat-card"><div class="stat-icon ic-blue"><i class="fas fa-place-of-worship"></i></div><div class="stat-num" id="stat-ibadah">0</div><div class="stat-lbl">Rumah Ibadah</div></div>
<div class="stat-card"><div class="stat-icon ic-red"><i class="fas fa-users"></i></div><div class="stat-num" id="stat-total">0</div><div class="stat-lbl">Total KK Miskin</div></div>
<div class="stat-card"><div class="stat-icon ic-green"><i class="fas fa-check-circle"></i></div><div class="stat-num" id="stat-sudah">0</div><div class="stat-lbl">Sudah Dibantu</div></div>
<div class="stat-card"><div class="stat-icon ic-yellow"><i class="fas fa-clock"></i></div><div class="stat-num" id="stat-belum">0</div><div class="stat-lbl">Belum Dibantu</div></div>
</div>
<div style="padding:0 20px 14px">
<div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted);margin-bottom:6px"><span>Sudah Dibantu</span><span id="progress-pct">0%</span></div>
<div style="background:var(--bg3);border-radius:6px;height:6px;overflow:hidden"><div id="progress-bar" style="height:100%;background:var(--green);border-radius:6px;width:0;transition:width .5s ease"></div></div>
</div>
<div class="sb-search">
<div class="search-wrap"><i class="fas fa-search"></i><input type="text" id="search-box" placeholder="Cari warga atau rumah ibadah..."></div>
</div>
<div class="sb-section"><div class="sb-section-title">Filter</div></div>
<div class="filter-row">
<select id="filter-jenis">
<option value="">Semua Jenis Rumah Ibadah</option>
<option>Masjid</option><option>Musholla</option><option>Gereja Protestan</option>
<option>Gereja Katolik</option><option>Pura</option><option>Vihara</option>
<option>Klenteng</option><option>Lainnya</option>
</select>
<select id="filter-status">
<option value="">Semua Status Bantuan</option>
<option value="belum_dibantu">Belum Dibantu</option>
<option value="sudah_dibantu">Sudah Dibantu</option>
<option value="menunggu_verifikasi">Menunggu Verifikasi</option>
</select>
<select id="filter-kecamatan">
<option value="">Semua Kecamatan</option>
</select>
</div>
<div class="sb-section"><div class="sb-section-title">Layer Peta</div></div>
<div class="layer-list">
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:#3b82f6"></div><span class="layer-name">Rumah Ibadah + Radius</span></div><label class="toggle-switch"><input type="checkbox" id="layer-ibadah" checked><span class="slider"></span></label></div>
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:#ef4444"></div><span class="layer-name">Penduduk Miskin</span></div><label class="toggle-switch"><input type="checkbox" id="layer-penduduk" checked><span class="slider"></span></label></div>
<div class="layer-item"><div class="layer-left"><div class="layer-dot" style="background:linear-gradient(to right,#fbbf24,#ef4444)"></div><span class="layer-name">Heatmap Kemiskinan</span></div><label class="toggle-switch"><input type="checkbox" id="layer-heat"><span class="slider"></span></label></div>
</div>
<div class="sb-section"><div class="sb-section-title">Aksi</div></div>
<div class="action-list">
<button class="action-btn" onclick="openModalIbadah()"><i class="fas fa-plus-circle"></i> Tambah Rumah Ibadah</button>
<button class="action-btn green" onclick="openModalPenduduk()"><i class="fas fa-user-plus"></i> Tambah Penduduk Miskin</button>
<button class="action-btn yellow" onclick="getUserLocation()"><i class="fas fa-location-crosshairs"></i> Lokasi Saya Saat Ini</button>
<button class="action-btn" onclick="exportCSV()"><i class="fas fa-file-csv"></i> Export Data CSV</button>
<button class="action-btn" onclick="window.open('api/export_pdf.php','_blank')"><i class="fas fa-file-pdf"></i> Export PDF</button>
</div>
<div class="sb-section"><div class="sb-section-title">Akun</div></div>
<div class="action-list">
<button class="action-btn" onclick="window.location='dashboard.php'"><i class="fas fa-chart-bar"></i> Dashboard</button>
<button class="action-btn" onclick="openModal('password')"><i class="fas fa-key"></i> Ubah Password</button>
<?php if ($isAdmin): ?>
<button class="action-btn" onclick="window.location='users.php'"><i class="fas fa-users-cog"></i> Manajemen Pengguna</button>
<?php endif; ?>
<button class="action-btn" onclick="window.location='../index.php'"><i class="fas fa-home"></i> Kembali ke Portal</button>
</div>
<div class="legend">
<div class="legend-title">Legenda Penduduk</div>
<div class="legend-items">
<div class="legend-item"><div class="legend-dot" style="background:#ef4444"></div> Belum Dibantu</div>
<div class="legend-item"><div class="legend-dot" style="background:#22c55e"></div> Sudah Dibantu</div>
<div class="legend-item"><div class="legend-dot" style="background:#f59e0b"></div> Menunggu Verifikasi</div>
</div>
</div>
</div>
<!-- ===== MAP ===== -->
<div id="map-container">
<button id="sidebar-toggle" title="Toggle sidebar"><i class="fas fa-bars"></i></button>
<div id="pick-hint"><i class="fas fa-mouse-pointer"></i>Klik di peta untuk memilih lokasi — tekan Esc untuk batal</div>
<div id="map"></div>
</div>
<div id="toast-wrap"></div>
<!-- ================================================================ MODAL: Rumah Ibadah ================================================================ -->
<div class="modal-overlay" id="modal-ibadah">
<div class="modal-box" id="modal-ibadah-box">
<div class="modal-head">
<h3><i class="fas fa-place-of-worship"></i> <span id="ibadah-modal-title">Tambah Rumah Ibadah</span></h3>
<button class="btn-close-modal" onclick="closeModal('ibadah')"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<input type="hidden" id="ibadah-id">
<div class="fg"><label>Nama Rumah Ibadah <span class="req">*</span></label><input type="text" id="ibadah-nama" placeholder="cth. Masjid Al-Falah"></div>
<div class="row2">
<div class="fg"><label>Jenis <span class="req">*</span></label><select id="ibadah-jenis"><option>Masjid</option><option>Musholla</option><option>Gereja Protestan</option><option>Gereja Katolik</option><option>Pura</option><option>Vihara</option><option>Klenteng</option><option>Lainnya</option></select></div>
<div class="fg"><label>Kontak / Telepon</label><input type="text" id="ibadah-kontak" placeholder="0561-XXXXXX"></div>
</div>
<div class="coord-group">
<div class="coord-row">
<div><label>Latitude <span class="req">*</span></label><input type="number" step="any" id="ibadah-lat" placeholder="-0.0262"></div>
<div><label>Longitude <span class="req">*</span></label><input type="number" step="any" id="ibadah-lng" placeholder="109.3429"></div>
</div>
<button type="button" class="btn-pick" onclick="startPick('ibadah')"><i class="fas fa-map-pin"></i> Pilih Lokasi dari Peta</button>
</div>
<div class="fg"><label>Alamat <span class="req">*</span></label><input type="text" id="ibadah-alamat" placeholder="Akan terisi otomatis dari koordinat..."><div class="hint">Otomatis terisi saat memilih lokasi dari peta (Nominatim)</div></div>
<div class="fg"><label>Radius Layanan (meter)</label><input type="number" id="ibadah-radius" value="300" min="50" max="5000" step="50"><div class="hint">Default: 300 m. Perubahan langsung terlihat di peta.</div></div>
</div>
<div class="modal-foot">
<button class="btn-secondary" onclick="closeModal('ibadah')">Batal</button>
<button class="btn-primary" onclick="saveIbadah()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
</div>
<!-- ================================================================ MODAL: Penduduk Miskin ================================================================ -->
<div class="modal-overlay" id="modal-penduduk">
<div class="modal-box" id="modal-penduduk-box">
<div class="modal-head">
<h3><i class="fas fa-user-plus"></i> <span id="penduduk-modal-title">Tambah Penduduk Miskin</span></h3>
<button class="btn-close-modal" onclick="closeModal('penduduk')"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<input type="hidden" id="penduduk-id">
<div class="divider">Lokasi</div>
<div class="coord-group">
<div class="coord-row">
<div><label>Latitude <span class="req">*</span></label><input type="number" step="any" id="penduduk-lat" placeholder="-0.0262"></div>
<div><label>Longitude <span class="req">*</span></label><input type="number" step="any" id="penduduk-lng" placeholder="109.3429"></div>
</div>
<button type="button" class="btn-pick" onclick="startPick('penduduk')"><i class="fas fa-map-pin"></i> Pilih Lokasi dari Peta</button>
</div>
<div class="fg"><label>Alamat Lengkap <span class="req">*</span></label><input type="text" id="penduduk-alamat" placeholder="Akan terisi otomatis dari koordinat..."></div>
<div class="row2">
<div class="fg"><label>Kecamatan</label><input type="text" id="penduduk-kecamatan" placeholder="Terisi otomatis..."></div>
<div class="fg"><label>Kelurahan / Desa</label><input type="text" id="penduduk-kelurahan" placeholder="Terisi otomatis..."></div>
</div>
<div class="divider">Data Kepala Keluarga</div>
<div class="fg"><label>Nama Kepala Keluarga <span class="req">*</span></label><input type="text" id="penduduk-kk-nama" placeholder="cth. Ahmad Subandi"></div>
<div class="row2">
<div class="fg"><label>NIK (Opsional)</label><input type="text" id="penduduk-nik" placeholder="16 digit NIK" maxlength="16"></div>
<div class="fg"><label>Jumlah Anggota <span class="req">*</span></label><input type="number" id="penduduk-jumlah" min="1" value="1"></div>
</div>
<div class="divider">Anggota Keluarga</div>
<div class="anggota-wrap">
<div class="anggota-head"><h4><i class="fas fa-users" style="color:#64748b;margin-right:6px"></i>Daftar Anggota</h4></div>
<div id="anggota-list"></div>
<button type="button" class="btn-add-anggota" onclick="addAnggotaRow()"><i class="fas fa-plus"></i> Tambah Anggota</button>
</div>
<div class="divider">Status Bantuan</div>
<div class="fg"><label>Status Bantuan <span class="req">*</span></label>
<select id="penduduk-status" onchange="toggleBantuanDetail(this.value)">
<option value="belum_dibantu">Belum Dibantu</option>
<option value="menunggu_verifikasi">Menunggu Verifikasi</option>
<option value="sudah_dibantu">Sudah Dibantu</option>
</select>
</div>
<div id="bantuan-detail">
<div class="row2">
<div class="fg"><label>Jenis Bantuan</label><input type="text" id="penduduk-jenis-bantuan" placeholder="PKH, BPNT, BLT, dll"></div>
<div class="fg"><label>Tanggal Bantuan</label><input type="date" id="penduduk-tanggal-bantuan"></div>
</div>
</div>
<div class="divider">Bukti Pendukung (Opsional)</div>
<div class="upload-area" onclick="document.getElementById('penduduk-bukti').click()">
<i class="fas fa-cloud-upload-alt"></i>
<p>Klik untuk upload foto / PDF / dokumen</p>
<p style="color:#94a3b8;font-size:10px;margin-top:4px">Maks. 5 MB</p>
<input type="file" id="penduduk-bukti" accept="image/*,application/pdf,.doc,.docx" onchange="previewFile(this)">
</div>
<div id="upload-preview" class="upload-preview" style="display:none"></div>
</div>
<div class="modal-foot">
<button class="btn-secondary" onclick="closeModal('penduduk')">Batal</button>
<button class="btn-primary" onclick="savePenduduk()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
</div>
<!-- ================================================================ MODAL: Detail Ibadah ================================================================ -->
<div class="modal-overlay" id="modal-detail-ibadah">
<div class="modal-box" style="max-width:480px">
<div class="modal-head"><h3><i class="fas fa-info-circle"></i> Detail Rumah Ibadah</h3><button class="btn-close-modal" onclick="closeModal('detail-ibadah')"><i class="fas fa-times"></i></button></div>
<div class="modal-body" id="detail-ibadah-body"></div>
<div class="modal-foot" id="detail-ibadah-foot"></div>
</div>
</div>
<!-- ================================================================ MODAL: Detail Penduduk ================================================================ -->
<div class="modal-overlay" id="modal-detail-penduduk">
<div class="modal-box" style="max-width:520px">
<div class="modal-head"><h3><i class="fas fa-user"></i> Detail Penduduk Miskin</h3><button class="btn-close-modal" onclick="closeModal('detail-penduduk')"><i class="fas fa-times"></i></button></div>
<div class="modal-body" id="detail-penduduk-body"></div>
<div class="modal-foot" id="detail-penduduk-foot"></div>
</div>
</div>
<!-- MODAL: Ubah Password -->
<div class="modal-overlay" id="modal-password">
<div class="modal-box" style="max-width:420px">
<div class="modal-head">
<h3><i class="fas fa-key"></i> Ubah Password</h3>
<button class="btn-close-modal" onclick="closeModal('password')"><i class="fas fa-times"></i></button>
</div>
<div class="modal-body">
<div class="fg"><label>Password Lama <span class="req">*</span></label><input type="password" id="pw-old" placeholder="Masukkan password saat ini"></div>
<div class="fg"><label>Password Baru <span class="req">*</span></label><input type="password" id="pw-new" placeholder="Minimal 6 karakter"></div>
<div class="fg"><label>Konfirmasi Password Baru <span class="req">*</span></label><input type="password" id="pw-conf" placeholder="Ulangi password baru"></div>
</div>
<div class="modal-foot">
<button class="btn-secondary" onclick="closeModal('password')">Batal</button>
<button class="btn-primary" onclick="savePassword()"><i class="fas fa-save"></i> Simpan</button>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://leaflet.github.io/Leaflet.heat/dist/leaflet-heat.js"></script>
<script>
const IS_ADMIN = <?= $isAdmin ? 'true' : 'false' ?>;
// ================================================================ STATE
let map, ibadahLayer, pendudukLayer, heatLayer, userMarker;
let allIbadah = [], allPenduduk = [];
let anggotaCount = 0, pickingFor = null;
let editingIbadahId = null, editingPendudukId = null;
let tempIM = null, tempIC = null, tempPM = null;
const SC = {belum_dibantu:'#ef4444',sudah_dibantu:'#22c55e',menunggu_verifikasi:'#f59e0b'};
const IC = {Masjid:'#10b981',Musholla:'#34d399','Gereja Protestan':'#3b82f6','Gereja Katolik':'#6366f1',Pura:'#f59e0b',Vihara:'#ef4444',Klenteng:'#ec4899',Lainnya:'#94a3b8'};
// ================================================================ MAP
function initMap() {
map = L.map('map', {center:[-0.0263,109.3425], zoom:13, zoomControl:false});
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {attribution:'© <a href="https://osm.org">OpenStreetMap</a>', maxZoom:19}).addTo(map);
L.control.zoom({position:'topright'}).addTo(map);
ibadahLayer = L.layerGroup().addTo(map);
pendudukLayer = L.layerGroup().addTo(map);
map.on('click', onMapClick);
document.addEventListener('keydown', e => { if (e.key==='Escape' && pickingFor) stopPick(); });
}
// ================================================================ DATA
async function loadData() {
try {
const r = await fetch('api/get_data.php');
const d = await r.json();
allIbadah = d.ibadah || [];
allPenduduk = d.penduduk || [];
// Populate kecamatan filter from data
const kecList = [...new Set(allPenduduk.map(p=>p.kecamatan).filter(Boolean))].sort();
const selK = document.getElementById('filter-kecamatan');
const prevK = selK.value;
selK.innerHTML = '<option value="">Semua Kecamatan</option>' +
kecList.map(k=>`<option value="${k}"${k===prevK?' selected':''}>${k}</option>`).join('');
renderAll(); updateStats();
} catch(e) { toast('Gagal memuat data: '+e.message,'error'); }
}
// ================================================================ RENDER
function renderAll() {
ibadahLayer.clearLayers(); pendudukLayer.clearLayers();
const q = document.getElementById('search-box').value.toLowerCase();
const fJ = document.getElementById('filter-jenis').value;
const fS = document.getElementById('filter-status').value;
const fK = document.getElementById('filter-kecamatan').value;
allIbadah.filter(i => (!fJ||i.jenis===fJ)&&(!q||i.nama.toLowerCase().includes(q))).forEach(renderIbadah);
allPenduduk.filter(p =>
(!fS||p.status_bantuan===fS) &&
(!fK||p.kecamatan===fK) &&
(!q||p.kk_nama.toLowerCase().includes(q)||
(p.kecamatan||'').toLowerCase().includes(q)||
(p.kelurahan||'').toLowerCase().includes(q))
).forEach(renderPenduduk);
updateHeat();
}
function ibadahIcon(jenis) {
const c = IC[jenis]||'#94a3b8';
return L.divIcon({className:'',html:`<div style="background:${c};width:30px;height:30px;border-radius:50%;border:3px solid #fff;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 10px rgba(0,0,0,.3);font-size:13px;color:#fff">🛕</div>`,iconSize:[30,30],iconAnchor:[15,15]});
}
function renderIbadah(d) {
const lat=parseFloat(d.lat), lng=parseFloat(d.lng), c=IC[d.jenis]||'#94a3b8';
ibadahLayer.addLayer(L.circle([lat,lng],{radius:parseInt(d.radius)||300,color:c,fillColor:c,fillOpacity:.08,weight:1.5,dashArray:'6,4'}));
const m = L.marker([lat,lng],{icon:ibadahIcon(d.jenis)});
m.on('click',()=>showDetailIbadah(d));
ibadahLayer.addLayer(m);
}
function pendudukIcon(status) {
const c = SC[status]||'#ef4444';
return L.divIcon({className:'',html:`<div style="background:${c};width:14px;height:14px;border-radius:50%;border:2.5px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.35)"></div>`,iconSize:[14,14],iconAnchor:[7,7]});
}
function renderPenduduk(d) {
const m = L.marker([parseFloat(d.lat),parseFloat(d.lng)],{icon:pendudukIcon(d.status_bantuan)});
m.on('click',()=>showDetailPenduduk(d));
pendudukLayer.addLayer(m);
}
// ================================================================ STATS
function updateStats() {
document.getElementById('stat-ibadah').textContent = allIbadah.length;
document.getElementById('stat-total').textContent = allPenduduk.length;
const sudah = allPenduduk.filter(p=>p.status_bantuan==='sudah_dibantu').length;
document.getElementById('stat-sudah').textContent = sudah;
document.getElementById('stat-belum').textContent = allPenduduk.filter(p=>p.status_bantuan==='belum_dibantu').length;
const pct = allPenduduk.length > 0 ? Math.round(sudah / allPenduduk.length * 100) : 0;
document.getElementById('progress-bar').style.width = pct + '%';
document.getElementById('progress-pct').textContent = pct + '%';
}
// ================================================================ HEATMAP
function updateHeat() {
if (heatLayer) { try{map.removeLayer(heatLayer);}catch(e){} }
if (!document.getElementById('layer-heat').checked) return;
const pts = allPenduduk.map(p=>[parseFloat(p.lat),parseFloat(p.lng),0.6]);
if (!pts.length) return;
heatLayer = L.heatLayer(pts,{radius:30,blur:20,maxZoom:17,gradient:{0.4:'#fbbf24',0.7:'#f97316',1:'#ef4444'}});
map.addLayer(heatLayer);
}
// ================================================================ LAYER TOGGLES
document.getElementById('layer-ibadah').addEventListener('change', function(){if(this.checked)map.addLayer(ibadahLayer);else map.removeLayer(ibadahLayer)});
document.getElementById('layer-penduduk').addEventListener('change', function(){if(this.checked)map.addLayer(pendudukLayer);else map.removeLayer(pendudukLayer)});
document.getElementById('layer-heat').addEventListener('change', updateHeat);
// ================================================================ SEARCH / FILTER
document.getElementById('search-box').addEventListener('input', renderAll);
document.getElementById('filter-jenis').addEventListener('change', renderAll);
document.getElementById('filter-status').addEventListener('change', renderAll);
document.getElementById('filter-kecamatan').addEventListener('change', renderAll);
// ================================================================ PICK LOCATION
function startPick(target) {
pickingFor = target;
document.getElementById(`modal-${target}`).classList.remove('open');
document.getElementById('pick-hint').style.display = 'block';
map.getContainer().style.cursor = 'crosshair';
}
function stopPick(reopen = true) {
if (!pickingFor) return;
const target = pickingFor;
pickingFor = null;
document.getElementById('pick-hint').style.display = 'none';
map.getContainer().style.cursor = '';
if (reopen) document.getElementById(`modal-${target}`).classList.add('open');
}
async function onMapClick(e) {
if (!pickingFor) return;
const {lat,lng} = e.latlng;
const fL=lat.toFixed(7), fG=lng.toFixed(7);
if (pickingFor==='ibadah') {
document.getElementById('ibadah-lat').value=fL;
document.getElementById('ibadah-lng').value=fG;
previewIbadahOnMap(lat,lng);
reverseGeocode(lat,lng).then(a=>document.getElementById('ibadah-alamat').value=a.display_name);
} else if (pickingFor==='penduduk') {
document.getElementById('penduduk-lat').value=fL;
document.getElementById('penduduk-lng').value=fG;
if(tempPM)pendudukLayer.removeLayer(tempPM);
tempPM=L.marker([lat,lng],{icon:pendudukIcon(document.getElementById('penduduk-status').value)});
pendudukLayer.addLayer(tempPM);
map.flyTo([lat,lng],16,{duration:.5});
reverseGeocode(lat,lng).then(a=>{
document.getElementById('penduduk-alamat').value=a.display_name;
document.getElementById('penduduk-kecamatan').value=a.kecamatan;
document.getElementById('penduduk-kelurahan').value=a.kelurahan;
});
}
stopPick();
}
function previewIbadahOnMap(lat,lng) {
if(tempIM){ibadahLayer.removeLayer(tempIM);ibadahLayer.removeLayer(tempIC);}
const r=parseInt(document.getElementById('ibadah-radius').value)||300;
const c=IC[document.getElementById('ibadah-jenis').value]||'#3b82f6';
tempIC=L.circle([lat,lng],{radius:r,color:c,fillColor:c,fillOpacity:.08,weight:1.5,dashArray:'6,4'});
tempIM=L.marker([lat,lng],{icon:ibadahIcon(document.getElementById('ibadah-jenis').value)});
ibadahLayer.addLayer(tempIC); ibadahLayer.addLayer(tempIM);
map.flyTo([lat,lng],15,{duration:.5});
}
document.addEventListener('input', e=>{
if(e.target.id==='ibadah-radius'&&tempIC) tempIC.setRadius(parseInt(e.target.value)||300);
});
// ================================================================ REVERSE GEOCODING
async function reverseGeocode(lat,lng) {
try {
const r=await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&accept-language=id`);
const d=await r.json();
const a=d.address||{};
return {
display_name: d.display_name||'',
kecamatan: a.city_district||a.county||a.district||'',
kelurahan: a.suburb||a.village||a.neighbourhood||a.quarter||''
};
} catch { return {display_name:'',kecamatan:'',kelurahan:''}; }
}
// ================================================================ GEOLOCATION
function getUserLocation() {
if(!navigator.geolocation){toast('Browser tidak mendukung Geolocation','error');return;}
toast('Mendapatkan lokasi...','warning');
navigator.geolocation.getCurrentPosition(
pos=>{
const {latitude:lat,longitude:lng}=pos.coords;
if(userMarker)map.removeLayer(userMarker);
userMarker=L.marker([lat,lng],{icon:L.divIcon({className:'',html:'<div class="user-dot"></div>',iconSize:[18,18],iconAnchor:[9,9]})}).addTo(map).bindPopup('<b>📍 Lokasi Anda</b>').openPopup();
map.flyTo([lat,lng],16,{duration:1});
toast('Lokasi ditemukan!','success');
},
err=>toast('Gagal: '+err.message,'error')
);
}
// ================================================================ MODAL
function openModal(id) { document.getElementById(`modal-${id}`).classList.add('open'); }
function closeModal(id) {
document.getElementById(`modal-${id}`).classList.remove('open');
if (pickingFor === id) stopPick(false);
else stopPick();
if(id==='ibadah'){if(tempIM){ibadahLayer.removeLayer(tempIM);ibadahLayer.removeLayer(tempIC);tempIM=null;tempIC=null;}editingIbadahId=null;}
if(id==='penduduk'){if(tempPM){pendudukLayer.removeLayer(tempPM);tempPM=null;}editingPendudukId=null;}
}
document.querySelectorAll('.modal-overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)closeModal(o.id.replace('modal-',''))}));
function openModalIbadah(d=null) {
editingIbadahId=d?d.id:null;
document.getElementById('ibadah-modal-title').textContent=d?'Edit Rumah Ibadah':'Tambah Rumah Ibadah';
['id','nama','kontak','lat','lng','alamat'].forEach(f=>document.getElementById(`ibadah-${f}`).value=d?.[f]||'');
document.getElementById('ibadah-jenis').value=d?.jenis||'Masjid';
document.getElementById('ibadah-radius').value=d?.radius||300;
openModal('ibadah');
}
function openModalPenduduk(d=null) {
editingPendudukId=d?d.id:null;
document.getElementById('penduduk-modal-title').textContent=d?'Edit Penduduk Miskin':'Tambah Penduduk Miskin';
document.getElementById('penduduk-id').value=d?.id||'';
document.getElementById('penduduk-lat').value=d?.lat||'';
document.getElementById('penduduk-lng').value=d?.lng||'';
document.getElementById('penduduk-alamat').value=d?.alamat||'';
document.getElementById('penduduk-kecamatan').value=d?.kecamatan||'';
document.getElementById('penduduk-kelurahan').value=d?.kelurahan||'';
document.getElementById('penduduk-kk-nama').value=d?.kk_nama||'';
document.getElementById('penduduk-nik').value=d?.nik||'';
document.getElementById('penduduk-jumlah').value=d?.jumlah_anggota||1;
document.getElementById('penduduk-status').value=d?.status_bantuan||'belum_dibantu';
document.getElementById('penduduk-jenis-bantuan').value=d?.jenis_bantuan||'';
document.getElementById('penduduk-tanggal-bantuan').value=d?.tanggal_bantuan||'';
toggleBantuanDetail(document.getElementById('penduduk-status').value);
document.getElementById('anggota-list').innerHTML=''; anggotaCount=0;
if(d?.anggota?.length) d.anggota.forEach(a=>addAnggotaRow(a)); else addAnggotaRow();
document.getElementById('upload-preview').style.display='none';
openModal('penduduk');
}
// ================================================================ ANGGOTA
function addAnggotaRow(d=null) {
const idx=anggotaCount++, row=document.createElement('div');
row.className='anggota-row'; row.id=`ar-${idx}`;
row.innerHTML=`<button type="button" class="btn-remove-anggota" onclick="removeAnggota(${idx})"><i class="fas fa-times"></i></button>
<div class="row2" style="margin-bottom:8px">
<div class="fg" style="margin:0"><label>Nama</label><input type="text" name="an[]" placeholder="Nama lengkap" value="${d?.nama||''}"></div>
<div class="fg" style="margin:0"><label>Hubungan</label><select name="ah[]">${['Kepala Keluarga','Istri','Anak','Orang Tua','Saudara','Lainnya'].map(h=>`<option ${d?.hubungan===h?'selected':''}>${h}</option>`).join('')}</select></div>
</div>
<div class="row3">
<div class="fg" style="margin:0"><label>Umur</label><input type="number" name="au[]" placeholder="Thn" min="0" max="120" value="${d?.umur||''}"></div>
<div class="fg" style="margin:0"><label>Pekerjaan</label><input type="text" name="ap[]" placeholder="Pekerjaan" value="${d?.pekerjaan||''}"></div>
<div class="fg" style="margin:0"><label>Keterangan</label><input type="text" name="ak[]" placeholder="Opsional" value="${d?.keterangan||''}"></div>
</div>`;
document.getElementById('anggota-list').appendChild(row);
}
function removeAnggota(idx) { const el=document.getElementById(`ar-${idx}`); if(el)el.remove(); }
function collectAnggota() {
return Array.from(document.querySelectorAll('#anggota-list .anggota-row')).map(row=>({
nama: row.querySelector('[name="an[]"]')?.value||'',
hubungan: row.querySelector('[name="ah[]"]')?.value||'',
umur: row.querySelector('[name="au[]"]')?.value||'',
pekerjaan:row.querySelector('[name="ap[]"]')?.value||'',
keterangan:row.querySelector('[name="ak[]"]')?.value||''
}));
}
function toggleBantuanDetail(status) {
document.getElementById('bantuan-detail').style.display=status==='sudah_dibantu'?'block':'none';
}
// ================================================================ SAVE
async function saveIbadah() {
const nama=document.getElementById('ibadah-nama').value.trim();
const lat=document.getElementById('ibadah-lat').value;
const lng=document.getElementById('ibadah-lng').value;
const alamat=document.getElementById('ibadah-alamat').value.trim();
if(!nama||!lat||!lng||!alamat){toast('Lengkapi semua field wajib','error');return;}
const fd=new FormData();
fd.append('id',editingIbadahId||''); fd.append('nama',nama);
fd.append('jenis',document.getElementById('ibadah-jenis').value);
fd.append('kontak',document.getElementById('ibadah-kontak').value);
fd.append('alamat',alamat); fd.append('lat',lat); fd.append('lng',lng);
fd.append('radius',document.getElementById('ibadah-radius').value||300);
const url=editingIbadahId?'api/update_ibadah.php':'api/save_ibadah.php';
const r=await fetch(url,{method:'POST',body:fd});
const result=await r.json();
if(result.status==='success'){closeModal('ibadah');toast(editingIbadahId?'Diperbarui!':'Berhasil ditambahkan!','success');loadData();}
else toast('Error: '+result.message,'error');
}
async function savePenduduk() {
const lat=document.getElementById('penduduk-lat').value;
const lng=document.getElementById('penduduk-lng').value;
const kk=document.getElementById('penduduk-kk-nama').value.trim();
const alamat=document.getElementById('penduduk-alamat').value.trim();
if(!lat||!lng||!kk||!alamat){toast('Lengkapi semua field wajib','error');return;}
const fd=new FormData();
fd.append('id',editingPendudukId||''); fd.append('lat',lat); fd.append('lng',lng);
fd.append('alamat',alamat);
fd.append('kecamatan',document.getElementById('penduduk-kecamatan').value);
fd.append('kelurahan',document.getElementById('penduduk-kelurahan').value);
fd.append('kk_nama',kk);
fd.append('nik',document.getElementById('penduduk-nik').value);
fd.append('jumlah_anggota',document.getElementById('penduduk-jumlah').value||1);
fd.append('status_bantuan',document.getElementById('penduduk-status').value);
fd.append('jenis_bantuan',document.getElementById('penduduk-jenis-bantuan').value);
fd.append('tanggal_bantuan',document.getElementById('penduduk-tanggal-bantuan').value);
fd.append('anggota',JSON.stringify(collectAnggota()));
const fi=document.getElementById('penduduk-bukti');
if(fi.files[0]) fd.append('bukti_file',fi.files[0]);
const url=editingPendudukId?'api/update_penduduk.php':'api/save_penduduk.php';
const r=await fetch(url,{method:'POST',body:fd});
const result=await r.json();
if(result.status==='success'){closeModal('penduduk');toast(editingPendudukId?'Diperbarui!':'Data penduduk ditambahkan!','success');loadData();}
else toast('Error: '+result.message,'error');
}
// ================================================================ DELETE
async function deleteRecord(type,id,name) {
if(!confirm(`Hapus "${name}"? Tindakan ini tidak dapat dibatalkan.`)) return;
const fd=new FormData(); fd.append('type',type); fd.append('id',id);
const r=await fetch('api/delete.php',{method:'POST',body:fd});
const result=await r.json();
if(result.status==='success'){
closeModal('detail-ibadah'); closeModal('detail-penduduk');
toast('Data berhasil dihapus.','success'); loadData();
} else toast('Gagal hapus: '+result.message,'error');
}
// ================================================================ DETAIL
function esc(s){return String(s||'').replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]))}
const SL={belum_dibantu:'Belum Dibantu',sudah_dibantu:'Sudah Dibantu',menunggu_verifikasi:'Menunggu Verifikasi'};
const SK={belum_dibantu:'sb-belum',sudah_dibantu:'sb-sudah',menunggu_verifikasi:'sb-menunggu'};
function showDetailIbadah(d) {
const binaan=allPenduduk.filter(p=>p.rumah_ibadah_id==d.id).length;
const dJSON=JSON.stringify(d).replace(/"/g,'&quot;');
document.getElementById('detail-ibadah-body').innerHTML=`
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<div style="width:48px;height:48px;border-radius:14px;background:${IC[d.jenis]||'#94a3b8'};display:flex;align-items:center;justify-content:center;font-size:24px;flex-shrink:0">🛕</div>
<div><div style="font-weight:700;font-size:17px;color:#0f172a">${esc(d.nama)}</div><span style="background:#eff6ff;color:#2563eb;padding:2px 10px;border-radius:20px;font-size:11px;font-weight:600">${d.jenis}</span></div>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr><td style="padding:7px 0;color:#64748b;font-weight:500;width:120px"><i class="fas fa-map-marker-alt" style="color:#3b82f6;margin-right:6px"></i>Alamat</td><td style="padding:7px 0;color:#374151">${esc(d.alamat)||'-'}</td></tr>
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-phone" style="color:#3b82f6;margin-right:6px"></i>Kontak</td><td style="padding:7px 0;color:#374151">${esc(d.kontak)||'-'}</td></tr>
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-circle" style="color:#3b82f6;margin-right:6px"></i>Radius</td><td style="padding:7px 0;color:#374151">${d.radius} meter</td></tr>
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-users" style="color:#22c55e;margin-right:6px"></i>KK Binaan</td><td style="padding:7px 0;font-weight:700;font-size:17px;color:#0f172a">${binaan} KK</td></tr>
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-map-pin" style="color:#3b82f6;margin-right:6px"></i>Koordinat</td><td style="padding:7px 0;color:#374151;font-family:monospace;font-size:11px">${parseFloat(d.lat).toFixed(6)}, ${parseFloat(d.lng).toFixed(6)}</td></tr>
</table>`;
document.getElementById('detail-ibadah-foot').innerHTML=`
<button class="btn-secondary" onclick="closeModal('detail-ibadah')">Tutup</button>
<button class="btn-primary" onclick="closeModal('detail-ibadah');openModalIbadah(${dJSON})"><i class="fas fa-edit"></i> Edit</button>
${IS_ADMIN ? `<button class="btn-danger" onclick="deleteRecord('ibadah',${d.id},'${esc(d.nama)}')"><i class="fas fa-trash"></i> Hapus</button>` : ''}`;
openModal('detail-ibadah');
map.flyTo([parseFloat(d.lat),parseFloat(d.lng)],15,{duration:.5});
}
function showDetailPenduduk(d) {
const ibadah=allIbadah.find(i=>i.id==d.rumah_ibadah_id);
const dJSON=JSON.stringify(d).replace(/"/g,'&quot;');
let anggotaHtml='';
if(d.anggota?.length){
anggotaHtml=`<div style="margin-top:14px;border-top:1px solid #f1f5f9;padding-top:14px">
<div style="font-size:11px;font-weight:600;color:#64748b;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Anggota Keluarga</div>
<table style="width:100%;font-size:12px;border-collapse:collapse">
<tr style="background:#f8fafc"><th style="padding:6px 8px;text-align:left;color:#374151">Nama</th><th style="padding:6px 8px;text-align:left;color:#374151">Hubungan</th><th style="padding:6px 8px;text-align:left;color:#374151">Umur</th><th style="padding:6px 8px;text-align:left;color:#374151">Pekerjaan</th></tr>
${d.anggota.map(a=>`<tr style="border-top:1px solid #f1f5f9"><td style="padding:6px 8px;color:#374151">${esc(a.nama)}</td><td style="padding:6px 8px;color:#64748b">${esc(a.hubungan)||'-'}</td><td style="padding:6px 8px;color:#64748b">${esc(String(a.umur||''))||'-'}</td><td style="padding:6px 8px;color:#64748b">${esc(a.pekerjaan)||'-'}</td></tr>`).join('')}
</table></div>`;
}
document.getElementById('detail-penduduk-body').innerHTML=`
<div style="display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:14px">
<div><div style="font-weight:700;font-size:17px;color:#0f172a">${esc(d.kk_nama)}</div>${d.nik?`<div style="font-size:11px;color:#94a3b8;font-family:monospace">NIK: ${d.nik}</div>`:''}</div>
<span class="status-badge ${SK[d.status_bantuan]||'sb-belum'}">${SL[d.status_bantuan]||d.status_bantuan}</span>
</div>
<table style="width:100%;border-collapse:collapse;font-size:13px">
<tr><td style="padding:7px 0;color:#64748b;font-weight:500;width:140px"><i class="fas fa-map-marker-alt" style="color:#ef4444;margin-right:6px"></i>Alamat</td><td style="padding:7px 0;color:#374151">${esc(d.alamat)||'-'}</td></tr>
${(d.kecamatan||d.kelurahan)?`<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-map" style="color:#8b5cf6;margin-right:6px"></i>Wilayah</td><td style="padding:7px 0;color:#374151">${[d.kelurahan,d.kecamatan].filter(Boolean).map(esc).join(', ')}</td></tr>`:''}
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-users" style="color:#3b82f6;margin-right:6px"></i>Jumlah Anggota</td><td style="padding:7px 0;color:#374151">${d.jumlah_anggota||'-'} jiwa</td></tr>
<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-place-of-worship" style="color:#10b981;margin-right:6px"></i>Rumah Ibadah</td><td style="padding:7px 0;color:#374151">${ibadah?esc(ibadah.nama):'Belum teridentifikasi'} ${d.jarak_meter?`<span style="color:#94a3b8;font-size:11px">(${parseFloat(d.jarak_meter).toFixed(0)} m)</span>`:''}</td></tr>
${d.jenis_bantuan?`<tr><td style="padding:7px 0;color:#64748b;font-weight:500"><i class="fas fa-hand-holding-heart" style="color:#22c55e;margin-right:6px"></i>Bantuan</td><td style="padding:7px 0;color:#374151">${esc(d.jenis_bantuan)} ${d.tanggal_bantuan?`<span style="color:#94a3b8;font-size:11px">(${d.tanggal_bantuan})</span>`:''}</td></tr>`:''}
${d.bukti_file?`<tr><td colspan="2" style="padding:7px 0"><a href="uploads/bukti/${d.bukti_file}" target="_blank" style="color:#3b82f6;font-size:12px"><i class="fas fa-paperclip"></i> Lihat Bukti</a></td></tr>`:''}
</table>${anggotaHtml}`;
document.getElementById('detail-penduduk-foot').innerHTML=`
<button class="btn-secondary" onclick="closeModal('detail-penduduk')">Tutup</button>
<button class="btn-primary" onclick="closeModal('detail-penduduk');openModalPenduduk(${dJSON})"><i class="fas fa-edit"></i> Edit</button>
${IS_ADMIN ? `<button class="btn-danger" onclick="deleteRecord('penduduk',${d.id},'${esc(d.kk_nama)}')"><i class="fas fa-trash"></i> Hapus</button>` : ''}`;
openModal('detail-penduduk');
map.flyTo([parseFloat(d.lat),parseFloat(d.lng)],16,{duration:.5});
}
// ================================================================ EXPORT
function exportCSV() {
let csv='No,Nama KK,NIK,Alamat,Kecamatan,Kelurahan,Jumlah Anggota,Rumah Ibadah Terdekat,Jarak (m),Status Bantuan,Jenis Bantuan,Tanggal Bantuan,Lat,Lng\n';
allPenduduk.forEach((p,i)=>{
const ib=allIbadah.find(ib=>ib.id==p.rumah_ibadah_id);
csv+=`${i+1},"${p.kk_nama||''}",${p.nik||''},"${p.alamat||''}","${p.kecamatan||''}","${p.kelurahan||''}",${p.jumlah_anggota||''},"${ib?.nama||''}",${p.jarak_meter||''},${p.status_bantuan||''},"${p.jenis_bantuan||''}",${p.tanggal_bantuan||''},${p.lat},${p.lng}\n`;
});
const a=document.createElement('a');
a.href=URL.createObjectURL(new Blob([''+csv],{type:'text/csv;charset=utf-8'}));
a.download=`penduduk_miskin_${new Date().toISOString().slice(0,10)}.csv`;
a.click(); toast('CSV berhasil diunduh','success');
}
function previewFile(input) {
if(!input.files[0]) return;
const f=input.files[0];
document.getElementById('upload-preview').textContent=`📎 ${f.name} (${(f.size/1024).toFixed(1)} KB)`;
document.getElementById('upload-preview').style.display='block';
}
// ================================================================ SIDEBAR TOGGLE
document.getElementById('sidebar-toggle').addEventListener('click',()=>document.getElementById('sidebar').classList.toggle('open'));
// ================================================================ TOAST
function toast(msg,type='info') {
const icons={success:'fa-check-circle',error:'fa-exclamation-circle',warning:'fa-exclamation-triangle',info:'fa-info-circle'};
const t=document.createElement('div');
t.className=`toast ${type}`;
t.innerHTML=`<i class="fas ${icons[type]}"></i><span>${msg}</span>`;
document.getElementById('toast-wrap').appendChild(t);
setTimeout(()=>{t.style.cssText='opacity:0;transform:translateY(8px);transition:.3s';setTimeout(()=>t.remove(),300)},3000);
}
// ================================================================ CHANGE PASSWORD
async function savePassword() {
const old = document.getElementById('pw-old').value;
const nw = document.getElementById('pw-new').value;
const cf = document.getElementById('pw-conf').value;
if (!old || !nw || !cf) { toast('Semua field wajib diisi','error'); return; }
if (nw.length < 6) { toast('Password baru minimal 6 karakter','error'); return; }
if (nw !== cf) { toast('Konfirmasi password tidak cocok','error'); return; }
const fd = new FormData();
fd.append('old_password', old);
fd.append('new_password', nw);
const r = await fetch('api/change_password.php', {method:'POST', body:fd});
const result = await r.json();
if (result.status === 'success') {
closeModal('password');
['pw-old','pw-new','pw-conf'].forEach(id=>document.getElementById(id).value='');
toast('Password berhasil diubah!', 'success');
} else {
toast('Error: ' + result.message, 'error');
}
}
// ================================================================ INIT
initMap(); loadData();
</script>
</body>
</html>