Files
Athallah Ghathfan Aqila 6215b4367a 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>
2026-06-11 13:31:33 +07:00

878 lines
56 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>