6215b4367a
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>
878 lines
56 KiB
PHP
878 lines
56 KiB
PHP
<?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=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[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,'"');
|
||
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,'"');
|
||
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>
|