Files
2026-06-10 21:46:56 +07:00

2661 lines
126 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS PovertyMap WebGIS Monitoring Bantuan Sosial v3.0</title>
<meta name="description" content="Sistem Informasi Geografis pendataan penduduk miskin dan monitoring bantuan sosial berbasis WebGIS">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/exifr/dist/lite.umd.js"></script>
<style>
/* ══════════════════════════════════════════════
DESIGN TOKENS LIGHT MODE
══════════════════════════════════════════════ */
:root {
--bg: #f0f4f9;
--surface: #ffffff;
--surface2: #f6f8fc;
--surface3: #eff2f8;
--border: #e0e4ef;
--border2: #b8c2d9;
--text: #0f172a;
--muted: #64748b;
--muted2: #94a3b8;
--accent: #2563eb;
--accent2: #10b981;
--danger: #ef4444;
--warn: #f59e0b;
--green: #10b981;
--red: #ef4444;
--yellow: #f59e0b;
--purple: #8b5cf6;
--radius: 10px;
--radius-lg: 16px;
--shadow: 0 10px 30px rgba(15,23,42,0.06);
--shadow-sm: 0 4px 12px rgba(15,23,42,0.04);
--font: 'Plus Jakarta Sans', sans-serif;
--mono: 'JetBrains Mono', monospace;
--header-h: 56px;
--stats-h: 48px;
--sidebar-w: 340px;
}
*{ box-sizing:border-box; margin:0; padding:0; }
html,body{ height:100%; overflow:hidden; }
body{ font-family:var(--font); background:var(--bg); color:var(--text); display:flex; flex-direction:column; }
::-webkit-scrollbar{ width:5px; height:5px; }
::-webkit-scrollbar-track{ background:var(--bg); }
::-webkit-scrollbar-thumb{ background:var(--border2); border-radius:3px; }
/* ══════════════════════════════════════
LOGIN SCREEN
══════════════════════════════════════ */
#login-screen{
position:fixed; inset:0; z-index:9999;
background:linear-gradient(135deg, #e8edf8 0%, #f1f4fb 50%, #e4ecfa 100%);
display:flex; align-items:center; justify-content:center;
}
.login-card{
background:var(--surface);
border:1px solid var(--border);
border-radius:20px; padding:40px 44px;
width:440px; max-width:94vw;
box-shadow:0 20px 60px rgba(30,50,100,.15);
animation:fadeSlideUp .4s ease;
}
@keyframes fadeSlideUp{ from{ opacity:0; transform:translateY(20px); } to{ opacity:1; transform:none; } }
.login-logo{ text-align:center; margin-bottom:28px; }
.login-logo .icon{ font-size:46px; display:block; margin-bottom:10px; }
.login-logo h1{ font-size:22px; font-weight:800; color:var(--text); letter-spacing:-.5px; }
.login-logo p{ font-size:12px; color:var(--muted); margin-top:4px; }
.login-role-label{ font-size:10px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.8px; margin-bottom:12px; display:block; }
.role-grid{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:10px; margin-bottom:20px; }
.role-card{
border:2px solid var(--border); border-radius:12px; padding:14px 8px;
cursor:pointer; text-align:center; background:var(--surface2); color:var(--text);
font-family:inherit; transition:all .2s;
}
.role-card:hover{ border-color:var(--accent); background:#eff4ff; }
.role-card.selected{ border-color:var(--accent); background:#eff4ff; box-shadow:0 0 0 3px rgba(59,124,244,.15); }
.role-card .role-icon{ font-size:26px; margin-bottom:6px; }
.role-card .role-name{ font-size:12px; font-weight:700; }
.role-card .role-desc{ font-size:9px; color:var(--muted); margin-top:3px; line-height:1.4; }
/* Pimpinan card - direct access */
.role-card.pimpinan-card{ border-color:#d1fae5; background:#f0fdf4; }
.role-card.pimpinan-card:hover, .role-card.pimpinan-card.selected{ border-color:var(--accent2); background:#dcfce7; box-shadow:0 0 0 3px rgba(5,150,105,.12); }
.role-direct-badge{ font-size:8px; color:var(--accent2); font-weight:700; background:#dcfce7; border-radius:4px; padding:1px 5px; display:inline-block; margin-top:3px; }
.login-divider{ border:none; border-top:1px solid var(--border); margin:16px 0; }
.login-field{ margin-bottom:13px; }
.login-field label{ display:block; font-size:10px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.7px; margin-bottom:6px; }
.login-field input{
width:100%; padding:10px 14px;
background:var(--surface2); border:1.5px solid var(--border);
border-radius:var(--radius); color:var(--text);
font-family:inherit; font-size:13px; outline:none; transition:border-color .2s;
}
.login-field input:focus{ border-color:var(--accent); background:var(--surface); }
.login-error{ font-size:11px; color:var(--danger); margin-bottom:10px; display:none; padding:8px 12px; background:#fef2f2; border-radius:6px; border:1px solid #fca5a5; }
.login-error.show{ display:block; }
.btn-login{
width:100%; padding:12px;
background:linear-gradient(135deg,#3b7cf4,#2563eb);
border:none; border-radius:var(--radius); color:#fff;
font-family:inherit; font-size:14px; font-weight:700;
cursor:pointer; transition:opacity .2s; margin-top:4px;
box-shadow:0 4px 12px rgba(59,124,244,.3);
}
.btn-login:hover{ opacity:.92; }
.login-hint{ font-size:10px; color:var(--muted); text-align:center; margin-top:14px; line-height:1.7; }
/* ══════════════════════════════════════
HEADER
══════════════════════════════════════ */
header{
background:var(--surface); border-bottom:1px solid var(--border);
padding:0 16px; height:var(--header-h); flex-shrink:0;
display:flex; align-items:center; gap:10px; z-index:1000;
box-shadow:0 1px 4px rgba(30,50,100,.07);
}
.logo{
font-size:14px; font-weight:800; color:var(--accent);
letter-spacing:-.4px; display:flex; align-items:center; gap:8px; white-space:nowrap;
}
.logo .v-badge{
background:linear-gradient(135deg,#3b7cf4,#059669);
-webkit-background-clip:text; -webkit-text-fill-color:transparent;
font-size:11px; font-weight:700; letter-spacing:.5px;
}
.header-sep{ width:1px; height:28px; background:var(--border); flex-shrink:0; }
.header-nav{ display:flex; gap:6px; }
.header-actions{ margin-left:auto; display:flex; align-items:center; gap:8px; }
.role-badge{
display:inline-flex; align-items:center; gap:5px;
padding:4px 12px; border-radius:20px; font-size:11px; font-weight:700;
}
.role-badge.admin { background:rgba(251,146,60,.12); color:#c2410c; border:1px solid rgba(251,146,60,.3); }
.role-badge.petugas { background:rgba(124,58,237,.1); color:#6d28d9; border:1px solid rgba(124,58,237,.25); }
.role-badge.pimpinan{ background:rgba(5,150,105,.1); color:#047857; border:1px solid rgba(5,150,105,.25); }
/* Notif */
.notif-btn{
position:relative; background:var(--surface2); border:1px solid var(--border);
border-radius:var(--radius); width:36px; height:36px;
display:flex; align-items:center; justify-content:center;
cursor:pointer; transition:background .15s; font-size:16px;
}
.notif-btn:hover{ background:var(--surface3); }
.notif-badge{
position:absolute; top:-5px; right:-5px;
background:var(--danger); color:#fff; font-size:9px; font-weight:700;
border-radius:10px; padding:1px 5px; min-width:16px; text-align:center;
display:none; border:2px solid var(--surface);
}
.notif-badge.show{ display:block; }
/* ══════════════════════════════════════
STATS BAR
══════════════════════════════════════ */
.stats-bar{
display:flex; gap:0; padding:0;
background:var(--surface); border-bottom:1px solid var(--border);
flex-shrink:0; overflow-x:auto; height:var(--stats-h);
}
.stat{
display:flex; align-items:center; gap:10px; padding:0 18px;
border-right:1px solid var(--border); min-width:fit-content; flex-shrink:0;
}
.stat-icon{ font-size:16px; }
.stat-info{ display:flex; flex-direction:column; gap:0; }
.stat-val{ font-size:16px; font-weight:800; color:var(--text); line-height:1; }
.stat-label{ font-size:9px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; }
.stat.green .stat-val{ color:var(--green); }
.stat.red .stat-val{ color:var(--red); }
.stat.warn .stat-val{ color:var(--warn); }
/* ══════════════════════════════════════
LAYOUT
══════════════════════════════════════ */
.app{ display:flex; flex:1; overflow:hidden; min-height:0; }
/* ══════════════════════════════════════
SIDEBAR
══════════════════════════════════════ */
#sidebar{
width:var(--sidebar-w);
background:var(--surface); border-right:1px solid var(--border);
display:flex; flex-direction:column; overflow:hidden;
transition:all .3s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink:0;
box-shadow:2px 0 12px rgba(15,23,42,.03);
}
#sidebar.collapsed{ width:0; border-right:none; box-shadow:none; }
.sidebar-search{ padding:10px 12px 6px; flex-shrink:0; }
.sidebar-search input{
width:100%; padding:9px 12px;
background:var(--surface2); border:1.5px solid var(--border);
border-radius:var(--radius); color:var(--text);
font-family:inherit; font-size:12.5px; outline:none; transition:all .2s;
}
.sidebar-search input:focus{ border-color:var(--accent); background:var(--surface); box-shadow:0 0 0 3px rgba(37,99,235,0.1); }
.tab-bar{ display:flex; border-bottom:1px solid var(--border); flex-shrink:0; }
.tab{
flex:1; padding:11px 4px; text-align:center; font-size:11.5px; font-weight:700;
cursor:pointer; color:var(--muted); border-bottom:2px solid transparent;
transition:all .2s; position:relative; white-space:nowrap;
background:var(--surface2);
}
.tab.active{ color:var(--accent); border-bottom-color:var(--accent); background:var(--surface); }
.tab:hover:not(.active){ color:var(--text); background:var(--surface3); }
.tab .tab-badge{
position:absolute; top:4px; right:6px;
background:var(--danger); color:#fff; font-size:8px; font-weight:700;
border-radius:10px; padding:1px 4px; min-width:14px; text-align:center;
}
.list-container{ flex:1; overflow-y:auto; padding:8px 10px; }
.list-item{
padding:12px 14px; border-radius:var(--radius); margin-bottom:8px;
cursor:pointer; border:1.5px solid transparent; transition:all .2s cubic-bezier(0.4, 0, 0.2, 1);
background:var(--surface2);
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
}
.list-item:hover{ background:var(--surface); border-color:var(--border); box-shadow:0 4px 12px rgba(15,23,42,0.05); transform: translateY(-1px); }
.list-item.selected{ background:#eff4ff; border-color:rgba(59,124,244,.3); box-shadow:0 4px 12px rgba(37,99,235,0.06); }
.list-item-header{ display:flex; align-items:center; gap:8px; margin-bottom:4px; }
.list-item-dot{ width:9px; height:9px; border-radius:50%; flex-shrink:0; }
.list-item-name{ font-size:13px; font-weight:700; flex:1; }
.list-item-nik{ font-size:10px; color:var(--muted); font-family:var(--mono); }
.list-item-meta{ font-size:11px; color:var(--muted2); margin-bottom:4px; }
.list-item-badges{ display:flex; gap:4px; flex-wrap:wrap; }
.list-item-actions{ display:flex; gap:4px; margin-top:6px; }
/* Badges */
.badge{
display:inline-flex; align-items:center; gap:3px;
padding:2px 8px; border-radius:20px; font-size:10px; font-weight:700; white-space:nowrap;
}
.badge-green { background:#dcfce7; color:#15803d; border:1px solid #bbf7d0; }
.badge-red { background:#fee2e2; color:#dc2626; border:1px solid #fca5a5; }
.badge-yellow { background:#fef3c7; color:#92400e; border:1px solid #fde68a; }
.badge-blue { background:#dbeafe; color:#1d4ed8; border:1px solid #bfdbfe; }
.badge-purple { background:#ede9fe; color:#5b21b6; border:1px solid #ddd6fe; }
.badge-gray { background:#f1f5f9; color:#475569; border:1px solid #e2e8f0; }
/* Filter Panel */
.filter-panel{ padding:12px 10px; flex:1; overflow-y:auto; }
.filter-group{ margin-bottom:14px; }
.filter-group label{ display:block; font-size:10px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.7px; margin-bottom:6px; }
.filter-group select, .filter-group input{
width:100%; padding:8px 10px;
background:var(--surface2); border:1.5px solid var(--border);
border-radius:var(--radius); color:var(--text);
font-family:inherit; font-size:12px; outline:none; transition:border-color .2s;
}
.filter-group select:focus, .filter-group input:focus{ border-color:var(--accent); }
.filter-footer{ display:flex; gap:8px; padding:10px; border-top:1px solid var(--border); flex-shrink:0; }
/* Notif Panel */
.notif-panel{ padding:8px 8px; flex:1; overflow-y:auto; }
.notif-item{
padding:10px 12px; border-radius:var(--radius); margin-bottom:4px;
border:1px solid var(--border); background:var(--surface2); transition:background .15s; cursor:pointer;
}
.notif-item:hover{ background:var(--surface3); }
.notif-item.unread{ border-left:3px solid var(--accent); background:#f5f8ff; }
.notif-item.jenis-belum_bantuan .notif-icon{ color:var(--danger); }
.notif-item.jenis-belum_verifikasi .notif-icon{ color:var(--warn); }
.notif-item.jenis-data_usang .notif-icon{ color:var(--purple); }
.notif-header{ display:flex; align-items:center; gap:8px; margin-bottom:4px; }
.notif-icon{ font-size:16px; flex-shrink:0; }
.notif-judul{ font-size:12px; font-weight:700; flex:1; }
.notif-time{ font-size:10px; color:var(--muted); white-space:nowrap; }
.notif-pesan{ font-size:11px; color:var(--muted2); line-height:1.5; }
.notif-actions{ padding:8px 10px; border-top:1px solid var(--border); flex-shrink:0; }
.notif-empty{ padding:40px 20px; text-align:center; color:var(--muted); font-size:12px; }
/* ══════════════════════════════════════
MAP
══════════════════════════════════════ */
#map{ flex:1; z-index:0; min-height:0; }
.map-container{ position:relative; flex:1; display:flex; }
/* Mode Bar */
#mode-bar{
position:absolute; top:14px; left:50%; transform:translateX(-50%);
background:var(--accent); color:#fff;
border-radius:20px; padding:8px 20px; font-size:12px; font-weight:700;
z-index:500; display:none; gap:10px; align-items:center; box-shadow:var(--shadow);
animation:pulse-bar 2s ease-in-out infinite; white-space:nowrap;
}
@keyframes pulse-bar{ 0%,100%{ box-shadow:var(--shadow); } 50%{ box-shadow:0 0 0 8px rgba(59,124,244,.2),var(--shadow); } }
#mode-bar.show{ display:flex; }
/* Legend */
.legend{
position:absolute; bottom:30px; left:12px; z-index:500;
background:rgba(255,255,255,.96); border:1px solid var(--border);
border-radius:10px; padding:10px 14px; font-size:11px;
box-shadow:var(--shadow-sm); backdrop-filter:blur(6px);
}
.legend-title{ font-weight:700; color:var(--muted); text-transform:uppercase; font-size:9px; letter-spacing:.7px; margin-bottom:8px; }
.legend-row{ display:flex; align-items:center; gap:8px; margin-bottom:5px; font-size:11px; font-weight:600; color:var(--text); }
.legend-dot{ width:10px; height:10px; border-radius:50%; flex-shrink:0; }
.legend-sq{ width:12px; height:12px; border-radius:3px; flex-shrink:0; }
/* FAB */
.map-fab{
position:absolute; bottom:80px; right:14px; z-index:500;
display:flex; flex-direction:column; gap:8px;
}
.fab-btn{
width:44px; height:44px; border-radius:50%;
background:var(--surface); border:1.5px solid var(--border);
color:var(--text); font-size:18px; cursor:pointer;
display:flex; align-items:center; justify-content:center;
box-shadow:var(--shadow-sm); transition:all .15s;
}
.fab-btn:hover{ background:var(--surface3); border-color:var(--border2); }
/* Radius Control */
.radius-control{
position:absolute; bottom:30px; left:50%; transform:translateX(-50%);
background:var(--surface); border:1.5px solid var(--border);
border-radius:10px; padding:10px 18px; z-index:500;
display:flex; align-items:center; gap:12px;
box-shadow:var(--shadow); font-size:12px; min-width:320px;
}
.radius-control label{ color:var(--muted); font-weight:700; white-space:nowrap; }
.radius-control input[type=range]{ flex:1; accent-color:var(--accent); cursor:pointer; }
.radius-value{ font-size:13px; font-weight:700; color:var(--accent); min-width:50px; text-align:right; }
/* ══════════════════════════════════════
BUTTONS
══════════════════════════════════════ */
.btn{
display:inline-flex; align-items:center; gap:6px;
padding:8px 16px; border-radius:var(--radius); border:none;
font-family:var(--font); font-size:12px; font-weight:700;
cursor:pointer; transition:all .15s; white-space:nowrap;
}
.btn-primary{ background:linear-gradient(135deg,#3b7cf4,#2563eb); color:#fff; box-shadow:0 2px 8px rgba(59,124,244,.25); }
.btn-primary:hover{ opacity:.92; }
.btn-success{ background:#dcfce7; color:#15803d; border:1px solid #bbf7d0; }
.btn-success:hover{ background:#bbf7d0; }
.btn-danger{ background:#fee2e2; color:var(--danger); border:1px solid #fca5a5; }
.btn-danger:hover{ background:#fca5a5; }
.btn-ghost{ background:var(--surface2); color:var(--text); border:1.5px solid var(--border); }
.btn-ghost:hover{ background:var(--surface3); }
.btn-warn{ background:#fef3c7; color:#92400e; border:1px solid #fde68a; }
.btn-warn:hover{ background:#fde68a; }
.btn-purple{ background:#ede9fe; color:#5b21b6; border:1px solid #ddd6fe; }
.btn-purple:hover{ background:#ddd6fe; }
.btn-sm{ padding:5px 10px; font-size:11px; }
.btn-logout{ background:transparent; border:1.5px solid var(--border); color:var(--muted); border-radius:var(--radius); padding:5px 12px; font-size:11px; font-weight:700; cursor:pointer; font-family:inherit; transition:all .15s; }
.btn-logout:hover{ border-color:var(--danger); color:var(--danger); }
/* Export dropdown */
.export-wrap{ position:relative; }
.export-menu{
position:absolute; top:100%; right:0; margin-top:6px;
background:var(--surface); border:1.5px solid var(--border);
border-radius:var(--radius); padding:6px; min-width:190px;
z-index:2000; display:none; box-shadow:var(--shadow);
}
.export-menu.open{ display:block; animation:fadeSlideUp .15s ease; }
.export-menu a, .export-menu button{
display:flex; align-items:center; gap:8px;
padding:8px 12px; border-radius:6px; font-size:12px; font-weight:600;
color:var(--text); text-decoration:none; width:100%;
background:transparent; border:none; font-family:inherit; cursor:pointer; transition:background .1s;
}
.export-menu a:hover, .export-menu button:hover{ background:var(--surface2); }
/* ══════════════════════════════════════
MODAL
══════════════════════════════════════ */
.modal-overlay{
position:fixed; inset:0; background:rgba(20,30,60,.35);
z-index:2000; display:flex; align-items:center; justify-content:center;
opacity:0; pointer-events:none; transition:opacity .2s;
backdrop-filter:blur(4px);
}
.modal-overlay.open{ opacity:1; pointer-events:all; }
.modal{
background:var(--surface); border:1.5px solid var(--border);
border-radius:var(--radius-lg); width:520px; max-width:96vw;
transform:translateY(12px); transition:transform .2s;
max-height:90vh; display:flex; flex-direction:column;
box-shadow:0 20px 60px rgba(20,40,100,.18);
}
.modal.modal-lg{ width:680px; }
.modal-overlay.open .modal{ transform:translateY(0); }
.modal-header{
display:flex; align-items:center; gap:10px; padding:20px 24px 16px;
border-bottom:1px solid var(--border); flex-shrink:0;
background:var(--surface2); border-radius:var(--radius-lg) var(--radius-lg) 0 0;
}
.modal-header h3{ font-size:16px; font-weight:800; flex:1; }
.modal-close{ background:transparent; border:none; color:var(--muted); font-size:20px; cursor:pointer; transition:color .15s; padding:0 4px; border-radius:6px; }
.modal-close:hover{ color:var(--text); background:var(--surface3); }
.modal-body{ overflow-y:auto; padding:20px 24px; flex:1; }
.modal-footer{ padding:16px 24px; border-top:1px solid var(--border); display:flex; gap:8px; justify-content:flex-end; flex-shrink:0; background:var(--surface2); border-radius:0 0 var(--radius-lg) var(--radius-lg); }
/* Form */
.form-grid{ display:grid; grid-template-columns:1fr 1fr; gap:14px; }
.form-grid.cols-3{ grid-template-columns:1fr 1fr 1fr; }
.form-full{ grid-column:1/-1; }
.form-group{ display:flex; flex-direction:column; gap:5px; }
.form-group label{ font-size:10px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.7px; }
.form-group input, .form-group select, .form-group textarea{
padding:9px 12px; background:var(--surface2); border:1.5px solid var(--border);
border-radius:var(--radius); color:var(--text);
font-family:inherit; font-size:13px; outline:none; transition:border-color .2s; width:100%;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus{ border-color:var(--accent); background:var(--surface); }
.form-group textarea{ resize:vertical; min-height:70px; }
.form-group .hint{ font-size:10px; color:var(--muted); line-height:1.5; }
.form-section{ margin:18px 0 12px; padding-bottom:8px; border-bottom:1.5px solid var(--border); }
.form-section h4{ font-size:11px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.8px; }
.geocode-status{ display:flex; align-items:center; gap:6px; font-size:11px; margin-top:5px; padding:6px 10px; border-radius:6px; transition:all .2s; }
.geocode-status.loading { background:#eff4ff; border:1px solid #bfdbfe; color:var(--accent); }
.geocode-status.success { background:#f0fdf4; border:1px solid #bbf7d0; color:var(--green); }
.geocode-status.error { background:#fef2f2; border:1px solid #fca5a5; color:var(--danger); }
.geocode-status.hidden { display:none; }
.coords-badge{ font-family:var(--mono); font-size:10px; color:var(--muted); background:var(--surface3); border:1px solid var(--border); border-radius:4px; padding:3px 8px; margin-top:4px; display:inline-block; }
.gps-btn{
display:flex; align-items:center; gap:6px; padding:9px 14px;
background:#f0fdf4; border:1.5px solid #bbf7d0;
border-radius:var(--radius); color:var(--accent2); font-size:12px; font-weight:700;
cursor:pointer; transition:all .2s; width:100%; justify-content:center; font-family:inherit;
}
.gps-btn:hover{ background:#dcfce7; }
.gps-btn.loading{ animation:pulse 1.5s ease infinite; }
@keyframes pulse{ 0%,100%{ opacity:1; } 50%{ opacity:.6; } }
/* Bantuan selector */
.bantuan-grid{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.bantuan-item{
border:2px solid var(--border); border-radius:var(--radius);
padding:10px 12px; cursor:pointer; transition:all .15s; background:var(--surface2);
}
.bantuan-item:hover{ border-color:var(--border2); background:var(--surface3); }
.bantuan-item.selected{ border-color:var(--accent); background:#eff4ff; }
.bantuan-item-header{ display:flex; align-items:center; gap:8px; }
.bantuan-kode{ font-size:13px; font-weight:800; }
.bantuan-nama{ font-size:11px; color:var(--muted2); margin-top:2px; }
.bantuan-check{ margin-left:auto; width:18px; height:18px; border-radius:4px; border:2px solid var(--border); background:var(--surface); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; }
.bantuan-item.selected .bantuan-check{ background:var(--accent); border-color:var(--accent); color:#fff; }
/* Photo gallery */
.foto-galeri{ display:grid; grid-template-columns:repeat(3,1fr); gap:8px; }
.foto-thumb-wrap{ position:relative; aspect-ratio:1; border-radius:var(--radius); overflow:hidden; border:1.5px solid var(--border); }
.foto-thumb-wrap img{ width:100%; height:100%; object-fit:cover; transition:transform .2s; cursor:zoom-in; }
.foto-thumb-wrap:hover img{ transform:scale(1.05); }
.foto-thumb-wrap .foto-primary-badge{ position:absolute; top:4px; left:4px; background:rgba(59,124,244,.9); color:#fff; font-size:9px; font-weight:700; padding:2px 6px; border-radius:4px; }
.foto-thumb-wrap .foto-delete{ position:absolute; top:4px; right:4px; background:rgba(220,38,38,.85); color:#fff; border:none; border-radius:4px; padding:3px 6px; font-size:11px; cursor:pointer; display:none; }
.foto-thumb-wrap:hover .foto-delete{ display:block; }
/* Upload zone */
.upload-zone{
border:2px dashed var(--border2); border-radius:var(--radius-lg);
padding:28px; text-align:center; cursor:pointer; transition:all .2s; background:var(--surface2);
}
.upload-zone:hover{ border-color:var(--accent); background:#eff4ff; }
.upload-zone.dragover{ border-color:var(--accent); background:#eff4ff; }
.upload-zone .upload-icon{ font-size:32px; display:block; margin-bottom:10px; }
.upload-zone p{ font-size:12px; color:var(--muted); }
/* Popup */
.popup-detail-section{ margin-bottom:12px; }
.popup-detail-section h5{ font-size:10px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.7px; margin-bottom:6px; padding-bottom:4px; border-bottom:1px solid var(--border); }
.popup-row{ display:flex; gap:6px; align-items:baseline; font-size:12px; margin-bottom:4px; }
.popup-label{ color:var(--muted); min-width:110px; flex-shrink:0; }
.popup-val{ font-weight:600; }
.popup-foto{ width:100%; height:140px; object-fit:cover; border-radius:var(--radius); margin-bottom:10px; border:1px solid var(--border); cursor:zoom-in; }
.popup-actions{ display:flex; gap:6px; margin-top:12px; flex-wrap:wrap; }
.popup-title{ font-size:15px; font-weight:800; margin-bottom:8px; display:flex; align-items:center; gap:8px; color:var(--text); }
/* Verifikasi section */
.verif-section{ background:#fffbeb; border:1.5px solid #fde68a; border-radius:var(--radius); padding:14px 16px; margin-top:8px; }
.verif-section h4{ font-size:12px; font-weight:700; color:#92400e; margin-bottom:10px; }
/* ══════════════════════════════════════
TOAST
══════════════════════════════════════ */
#toast{
position:fixed; bottom:24px; left:50%; transform:translateX(-50%);
background:var(--surface); border:1.5px solid var(--border);
border-radius:10px; padding:12px 22px;
font-size:13px; font-weight:600; color:var(--text);
z-index:9000; opacity:0; transition:opacity .3s;
pointer-events:none; max-width:400px; text-align:center; white-space:nowrap;
box-shadow:0 8px 24px rgba(30,50,100,.15);
}
#toast.show{ opacity:1; }
#toast.success{ border-color:#bbf7d0; color:var(--green); }
#toast.error { border-color:#fca5a5; color:var(--danger); }
#toast.info { border-color:#bfdbfe; color:var(--accent); }
/* Spinner */
.spinner{ display:inline-block; width:13px; height:13px; border:2px solid rgba(0,0,0,.1); border-top-color:currentColor; border-radius:50%; animation:spin .6s linear infinite; flex-shrink:0; }
@keyframes spin{ to{ transform:rotate(360deg); } }
/* Leaflet overrides for light mode */
.leaflet-control-zoom{ border:1.5px solid var(--border) !important; box-shadow:var(--shadow-sm) !important; }
.leaflet-control-zoom a{ background:var(--surface) !important; color:var(--text) !important; border-color:var(--border) !important; font-size:16px !important; }
.leaflet-control-zoom a:hover{ background:var(--surface2) !important; }
.leaflet-popup-content-wrapper{ background:var(--surface); color:var(--text); border:1.5px solid var(--border); border-radius:12px !important; box-shadow:var(--shadow); min-width:240px; }
.leaflet-popup-tip{ background:var(--surface); }
.leaflet-popup-content{ font-family:var(--font); font-size:12px; margin:14px 18px !important; }
.leaflet-popup-close-button{ color:var(--muted) !important; }
.leaflet-control-layers{ background:var(--surface) !important; border:1.5px solid var(--border) !important; border-radius:10px !important; color:var(--text) !important; font-family:var(--font) !important; font-size:12px !important; box-shadow:var(--shadow) !important; }
.leaflet-control-layers-list{ padding:6px 10px !important; }
.leaflet-control-layers label{ color:var(--text) !important; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:6px; margin:4px 0; }
.leaflet-control-layers input{ accent-color:var(--accent); }
.leaflet-control-layers-separator{ border-top:1px solid var(--border) !important; }
/* Role visibility */
body.role-pimpinan .admin-only,
body.role-pimpinan .petugas-only{ display:none !important; }
body.role-pimpinan .list-item-actions{ display:none !important; }
body.role-petugas .admin-only{ display:none !important; }
/* Responsive */
@media(max-width:640px){
#sidebar{ position:absolute; z-index:900; height:100%; box-shadow:var(--shadow); }
.form-grid{ grid-template-columns:1fr; }
.bantuan-grid{ grid-template-columns:1fr; }
.stats-bar{ display:none; }
.header-nav{ display:none; }
}
</style>
</head>
<body>
<!-- ═══════════════════════════════════════
LOGIN SCREEN
═══════════════════════════════════════ -->
<div id="login-screen">
<div class="login-card">
<div class="login-logo">
<span class="icon">🗺️</span>
<h1>SIG Penduduk Miskin</h1>
<p>WebGIS Monitoring Bantuan Sosial · Pontianak</p>
</div>
<span class="login-role-label">Masuk sebagai</span>
<div class="role-grid">
<button class="role-card selected" id="role-admin-btn" onclick="selectRole('admin')">
<div class="role-icon">🛠️</div>
<div class="role-name">Administrator</div>
<div class="role-desc">Kelola data &amp; verifikasi</div>
</button>
<button class="role-card" id="role-petugas-btn" onclick="selectRole('petugas')">
<div class="role-icon">📋</div>
<div class="role-name">Petugas Survei</div>
<div class="role-desc">Input &amp; upload foto</div>
</button>
<button class="role-card pimpinan-card" id="role-pimpinan-btn" onclick="selectRolePimpinan()">
<div class="role-icon">👁️</div>
<div class="role-name">Pimpinan</div>
<div class="role-desc">Monitor &amp; laporan</div>
<div class="role-direct-badge">Akses Langsung</div>
</button>
</div>
<div id="login-fields-wrap">
<hr class="login-divider">
<div class="login-field">
<label>Username</label>
<input type="text" id="login-username" placeholder="Masukkan username..." autocomplete="username">
</div>
<div class="login-field">
<label>Password</label>
<input type="password" id="login-password" placeholder="Masukkan password..." autocomplete="current-password"
onkeydown="if(event.key==='Enter')doLogin()">
</div>
<div class="login-error" id="login-error">⚠ Username atau password salah</div>
<button class="btn-login" id="btn-login-submit" onclick="doLogin()">Masuk →</button>
</div>
<div class="login-hint">
Demo · <b>admin</b> / <b>password</b> &nbsp;|&nbsp; <b>petugas</b> / <b>password</b>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
HEADER
═══════════════════════════════════════ -->
<header>
<div class="logo">
🗺 WebGIS PovertyMap
<span class="v-badge">v3.0</span>
</div>
<div class="header-sep"></div>
<div class="header-nav">
<button class="btn btn-ghost btn-sm" onclick="toggleSidebar()">☰ Panel</button>
<a href="dashboard.html" target="_blank" class="btn btn-ghost btn-sm">📊 Dashboard</a>
</div>
<div class="header-actions">
<button class="btn btn-success btn-sm admin-only" onclick="startAddIbadah()"> Ibadah</button>
<button class="btn btn-primary btn-sm" id="btn-tambah-penduduk" onclick="startAddPenduduk()" style="display:none;"> Penduduk</button>
<div class="export-wrap admin-only" id="export-wrap">
<button class="btn btn-ghost btn-sm" onclick="toggleExportMenu()" id="btn-export">📤 Export ▾</button>
<div class="export-menu" id="export-menu">
<a href="api/export_excel.php" target="_blank" id="link-export-excel">📊 Export Excel (.xlsx)</a>
<a href="api/export_pdf.php" target="_blank" id="link-export-pdf">📄 Export PDF</a>
<hr style="border:none;border-top:1px solid var(--border);margin:4px 0;">
<button onclick="openExportFilter()">🔽 Export dengan Filter…</button>
</div>
</div>
<div class="notif-btn admin-only" onclick="showNotifTab()" id="btn-notif" title="Notifikasi" style="display:none;">
🔔
<span class="notif-badge" id="notif-badge">0</span>
</div>
<span class="role-badge admin" id="header-role-badge">🛠 Administrator</span>
<button class="btn-logout" onclick="doLogout()" id="btn-logout" style="display:none;">Keluar</button>
</div>
</header>
<!-- ═══════════════════════════════════════
STATS BAR
═══════════════════════════════════════ -->
<div class="stats-bar">
<div class="stat">
<span class="stat-icon">🏠</span>
<div class="stat-info">
<div class="stat-val" id="s-kk"></div>
<div class="stat-label">KK Terdata</div>
</div>
</div>
<div class="stat">
<span class="stat-icon">👥</span>
<div class="stat-info">
<div class="stat-val" id="s-jiwa"></div>
<div class="stat-label">Total Jiwa</div>
</div>
</div>
<div class="stat green">
<span class="stat-icon"></span>
<div class="stat-info">
<div class="stat-val" id="s-bantuan"></div>
<div class="stat-label">Penerima Bantuan</div>
</div>
</div>
<div class="stat red">
<span class="stat-icon"></span>
<div class="stat-info">
<div class="stat-val" id="s-no-bantuan"></div>
<div class="stat-label">Belum Bantuan</div>
</div>
</div>
<div class="stat">
<span class="stat-icon">🕌</span>
<div class="stat-info">
<div class="stat-val" id="s-ibadah"></div>
<div class="stat-label">Rumah Ibadah</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
MAIN APP
═══════════════════════════════════════ -->
<div class="app">
<!-- SIDEBAR -->
<div id="sidebar">
<div class="sidebar-header" style="display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid var(--border);background:var(--surface2);">
<h3 style="font-size:13px;font-weight:800;color:var(--text);display:flex;align-items:center;gap:6px;">⚙️ Panel Kontrol</h3>
<button onclick="toggleSidebar()" style="background:none;border:none;cursor:pointer;font-size:14px;color:var(--muted);padding:4px;border-radius:6px;display:flex;align-items:center;justify-content:center;transition:all 0.15s;width:24px;height:24px;" onmouseover="this.style.background='var(--surface3)';this.style.color='var(--danger)'" onmouseout="this.style.background='none';this.style.color='var(--muted)'" title="Tutup panel"></button>
</div>
<div class="sidebar-search">
<input type="text" id="search-input" placeholder="🔍 Cari nama / NIK / alamat..." oninput="filterList()">
</div>
<div class="tab-bar">
<div class="tab active" id="tab-penduduk" onclick="switchTab('penduduk')">👥 Penduduk</div>
<div class="tab" id="tab-ibadah" onclick="switchTab('ibadah')">🕌 Ibadah</div>
<div class="tab" id="tab-filter" onclick="switchTab('filter')">🔽 Filter</div>
<div class="tab admin-only" id="tab-notif" onclick="switchTab('notif')" style="display:none;">🔔 Notif<span class="tab-badge" id="tab-notif-badge" style="display:none">0</span></div>
</div>
<!-- Penduduk List -->
<div class="list-container" id="list-penduduk" style="display:block;"></div>
<!-- Ibadah List -->
<div class="list-container" id="list-ibadah" style="display:none;"></div>
<!-- Filter Panel -->
<div id="panel-filter" style="display:none; flex-direction:column; flex:1; overflow:hidden;">
<div class="filter-panel">
<div class="filter-group">
<label>Kelurahan</label>
<select id="filter-kelurahan" onchange="applyFilter()">
<option value="">— Semua Kelurahan —</option>
</select>
</div>
<div class="filter-group">
<label>Kecamatan</label>
<select id="filter-kecamatan" onchange="applyFilter()">
<option value="">— Semua Kecamatan —</option>
</select>
</div>
<div class="filter-group">
<label>Kategori Kemiskinan</label>
<select id="filter-kategori" onchange="applyFilter()">
<option value="">— Semua Kategori —</option>
<option value="sangat_miskin">Sangat Miskin</option>
<option value="miskin">Miskin</option>
<option value="hampir_miskin">Nyaris Miskin</option>
<option value="mampu">Mampu</option>
</select>
</div>
<div class="filter-group">
<label>Status Bantuan</label>
<select id="filter-bantuan" onchange="applyFilter()">
<option value="">— Semua —</option>
<option value="ada">Sudah Menerima</option>
<option value="tidak">Belum Menerima</option>
</select>
</div>
<div class="filter-group">
<label>Status Pekerjaan</label>
<select id="filter-pekerjaan" onchange="applyFilter()">
<option value="">— Semua —</option>
<option value="tidak_bekerja">Tidak Bekerja</option>
<option value="buruh_harian">Buruh Harian</option>
<option value="petani">Petani</option>
<option value="nelayan">Nelayan</option>
<option value="pedagang">Pedagang</option>
<option value="pegawai_swasta">Pegawai Swasta</option>
<option value="pns">PNS / ASN</option>
<option value="wiraswasta">Wiraswasta</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
</div>
<div class="filter-footer">
<button class="btn btn-ghost btn-sm" style="flex:1" onclick="resetFilter()">↺ Reset</button>
<button class="btn btn-primary btn-sm" style="flex:2" onclick="applyFilter()">Terapkan Filter</button>
</div>
</div>
<!-- Notif Panel (admin only) -->
<div id="panel-notif" style="display:none; flex-direction:column; flex:1; overflow:hidden;">
<div class="notif-actions">
<button class="btn btn-ghost btn-sm" style="width:100%" onclick="markAllRead()">✓ Tandai Semua Dibaca</button>
</div>
<div class="notif-panel" id="list-notif"></div>
</div>
</div>
<!-- MAP -->
<div class="map-container">
<div id="map"></div>
<!-- Tombol buka sidebar kembali -->
<button id="sidebar-open-btn" onclick="toggleSidebar()" title="Buka Panel" style="display:none;position:absolute;top:60px;left:12px;z-index:600;background:var(--surface);border:1.5px solid var(--border);border-radius:8px;width:36px;height:36px;align-items:center;justify-content:center;cursor:pointer;font-size:16px;box-shadow:var(--shadow-sm);"></button>
<!-- Mode Bar -->
<div id="mode-bar">
<span id="mode-text">Klik peta untuk menentukan lokasi</span>
<button class="btn btn-ghost btn-sm" onclick="cancelMode()">✕ Batal</button>
</div>
<!-- Legend -->
<div class="legend" id="map-legend">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div class="legend-title" style="margin-bottom:0;">Legenda Peta</div>
<button onclick="toggleLegend()" style="background:none;border:none;cursor:pointer;font-size:12px;color:var(--muted);padding:0 0 0 8px;line-height:1;" title="Tutup legenda"></button>
</div>
<div id="legend-body">
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;border:2px solid #16a34a;box-shadow:0 0 0 3px rgba(34,197,94,.5);"></div> Di Dalam Radius</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;border:2px solid #dc2626;box-shadow:0 0 0 3px rgba(239,68,68,.45);"></div> Di Luar Radius</div>
<div style="border-top:1px solid var(--border);margin:6px 0;"></div>
<div class="legend-row"><div class="legend-sq" style="background:#3b7cf4;"></div> Masjid (Islam)</div>
<div class="legend-row"><div class="legend-sq" style="background:#8b5cf6;"></div> Gereja (Kristen)</div>
<div class="legend-row"><div class="legend-sq" style="background:#f97316;"></div> Pura (Hindu)</div>
<div class="legend-row"><div class="legend-sq" style="background:#ec4899;"></div> Vihara (Buddha)</div>
<div class="legend-row"><div class="legend-sq" style="background:#eab308;"></div> Klenteng (Konghucu)</div>
</div>
</div>
<!-- Tombol buka kembali legenda -->
<button id="legend-toggle-btn" onclick="toggleLegend()" style="display:none;position:absolute;bottom:30px;left:12px;z-index:500;background:rgba(255,255,255,.96);border:1px solid var(--border);border-radius:8px;padding:6px 12px;font-size:11px;font-weight:700;cursor:pointer;box-shadow:var(--shadow-sm);backdrop-filter:blur(6px);">🗺 Legenda</button>
<!-- Floating Action Buttons -->
<div class="map-fab">
<button class="fab-btn" onclick="fitAllMarkers()" title="Fit Semua Marker">🎯</button>
</div>
<!-- Radius Control -->
<div class="radius-control" id="radiusControl" style="display:none;">
<label>📐 Radius:</label>
<input type="range" id="radiusSlider" min="100" max="3000" step="50" value="500" oninput="onRadiusSlide(this.value)">
<span class="radius-value" id="radiusDisplay">500 m</span>
<button class="btn btn-primary btn-sm" onclick="saveRadius()">Simpan</button>
<button class="btn btn-ghost btn-sm" onclick="closeRadiusControl()"></button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
MODAL RUMAH IBADAH
═══════════════════════════════════════ -->
<div class="modal-overlay" id="modalIbadah">
<div class="modal">
<div class="modal-header">
<h3>🕌 <span id="modal-ibadah-title">Tambah Rumah Ibadah</span></h3>
<button class="modal-close" onclick="closeModal('modalIbadah')"></button>
</div>
<div class="modal-body">
<input type="hidden" id="ibadah-id">
<input type="hidden" id="ibadah-lat">
<input type="hidden" id="ibadah-lng">
<div class="form-grid">
<div class="form-group form-full">
<label>Nama Rumah Ibadah *</label>
<input type="text" id="ibadah-nama" placeholder="Masjid Al-Ikhlas...">
</div>
<div class="form-group">
<label>Jenis</label>
<select id="ibadah-jenis">
<option value="masjid">🕌 Masjid (Islam)</option>
<option value="musholla">🕌 Musholla (Islam)</option>
<option value="gereja">⛪ Gereja (Kristen Protestan)</option>
<option value="gereja_katolik">⛪ Gereja (Kristen Katolik)</option>
<option value="pura">🛕 Pura (Hindu)</option>
<option value="vihara">🏯 Vihara (Buddha)</option>
<option value="klenteng">🏮 Klenteng (Konghucu)</option>
</select>
</div>
<div class="form-group">
<label>Radius Default (m)</label>
<input type="number" id="ibadah-radius" value="500" min="100" max="3000" step="50">
</div>
<div class="form-group form-full">
<label>Alamat</label>
<textarea id="ibadah-alamat" rows="2" placeholder="Otomatis dari koordinat..."></textarea>
<div class="geocode-status hidden" id="geocode-status-i"></div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:4px;">
<div class="coords-badge" id="ibadah-coords-badge" style="margin-top:0;"></div>
<button type="button" class="btn btn-ghost btn-sm" style="padding:4px 10px;font-size:10px;" onclick="repositionIbadahMap()">🗺️ Ubah di Peta</button>
</div>
</div>
<div class="form-group">
<label>Penanggung Jawab (PIC)</label>
<input type="text" id="ibadah-pic" placeholder="Nama PIC...">
</div>
<div class="form-group">
<label>No. WhatsApp</label>
<input type="text" id="ibadah-wa" placeholder="08xxxxxxxxxx">
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalIbadah')">Batal</button>
<button class="btn btn-primary" onclick="saveIbadah()" id="btn-save-ibadah">💾 Simpan</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
MODAL PENDUDUK (Form Lengkap)
═══════════════════════════════════════ -->
<div class="modal-overlay" id="modalPenduduk">
<div class="modal modal-lg">
<div class="modal-header">
<h3>👥 <span id="modal-penduduk-title">Tambah Penduduk Miskin</span></h3>
<button class="modal-close" onclick="closeModal('modalPenduduk')"></button>
</div>
<div class="modal-body">
<input type="hidden" id="penduduk-id">
<input type="hidden" id="penduduk-lat">
<input type="hidden" id="penduduk-lng">
<!-- Identitas KK -->
<div class="form-section"><h4>🪪 Identitas Kepala Keluarga</h4></div>
<div class="form-grid">
<div class="form-group">
<label>NIK KK * (16 digit)</label>
<input type="text" id="p-nik" placeholder="6171XXXXXXXXXXXX" maxlength="16" pattern="\d{16}">
</div>
<div class="form-group">
<label>No. HP / WhatsApp</label>
<input type="text" id="p-hp" placeholder="08xxxxxxxxxx">
</div>
<div class="form-group form-full">
<label>Nama Kepala Keluarga *</label>
<input type="text" id="p-nama" placeholder="Nama lengkap KK...">
</div>
</div>
<!-- Lokasi -->
<div class="form-section"><h4>📍 Lokasi</h4></div>
<div class="form-grid">
<div class="form-group form-full">
<label>Alamat Lengkap</label>
<textarea id="p-alamat" rows="2" placeholder="Otomatis dari koordinat GPS..."></textarea>
<div class="geocode-status hidden" id="geocode-status-p"></div>
<div class="coords-badge" id="p-coords-badge"></div>
</div>
<div class="form-group">
<label>RT</label>
<input type="text" id="p-rt" placeholder="01" maxlength="5">
</div>
<div class="form-group">
<label>RW</label>
<input type="text" id="p-rw" placeholder="01" maxlength="5">
</div>
</div>
<!-- Data Keluarga -->
<div class="form-section"><h4>👨‍👩‍👧‍👦 Data Keluarga</h4></div>
<div class="form-grid cols-3">
<div class="form-group">
<label>Jumlah Anggota *</label>
<input type="number" id="p-jiwa" value="1" min="1" max="30">
</div>
<div class="form-group">
<label>Anak Sekolah</label>
<input type="number" id="p-anak" value="0" min="0" max="20">
</div>
<div class="form-group">
<label>Status BPJS</label>
<select id="p-bpjs">
<option value="tidak_punya">Tidak Punya</option>
<option value="PBI">BPJS PBI (Gratis)</option>
<option value="mandiri">BPJS Mandiri</option>
</select>
</div>
</div>
<!-- Ekonomi -->
<div class="form-section"><h4>💰 Data Ekonomi</h4></div>
<div class="form-grid">
<div class="form-group">
<label>Penghasilan Bulanan (Rp)</label>
<input type="number" id="p-penghasilan" placeholder="850000" min="0">
<span class="hint" id="p-kategori-hint">Kategori: —</span>
</div>
<div class="form-group">
<label>Sumber Penghasilan</label>
<input type="text" id="p-sumber" placeholder="Buruh bangunan, jualan...">
</div>
<div class="form-group form-full">
<label>Status Pekerjaan KK</label>
<select id="p-pekerjaan">
<option value="tidak_bekerja">Tidak Bekerja</option>
<option value="buruh_harian">Buruh Harian</option>
<option value="petani">Petani</option>
<option value="nelayan">Nelayan</option>
<option value="pedagang">Pedagang</option>
<option value="pegawai_swasta">Pegawai Swasta</option>
<option value="pns">PNS / ASN</option>
<option value="wiraswasta">Wiraswasta</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
</div>
<!-- Kondisi Rumah -->
<div class="form-section"><h4>🏠 Kondisi Rumah</h4></div>
<div class="form-grid">
<div class="form-group">
<label>Kondisi Rumah</label>
<select id="p-kondisi">
<option value="sangat_tidak_layak">Sangat Tidak Layak Huni</option>
<option value="tidak_layak" selected>Tidak Layak Huni</option>
<option value="layak">Layak Huni</option>
</select>
</div>
<div class="form-group">
<label>Kepemilikan Rumah</label>
<select id="p-kepemilikan">
<option value="milik_sendiri">Milik Sendiri</option>
<option value="kontrak">Kontrak/Sewa</option>
<option value="menumpang">Menumpang</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
</div>
<!-- Bantuan Sosial -->
<div class="form-section"><h4>💳 Bantuan Sosial yang Diterima</h4></div>
<div class="bantuan-grid" id="bantuan-selector"></div>
<!-- Foto Rumah (Geo-tagging) -->
<div class="form-section"><h4>📸 Foto Rumah (Geo-tagging)</h4></div>
<div class="form-grid">
<div class="form-group form-full">
<label>Pilih Foto Utama Rumah</label>
<input type="file" id="p-foto-input" accept="image/*" capture="environment" onchange="onPendudukFotoSelected(event)">
<div id="p-foto-preview-wrap" style="display:none;margin-top:10px;">
<img id="p-foto-preview" style="width:100%;max-height:180px;object-fit:cover;border-radius:var(--radius);border:1.5px solid var(--border);" alt="Preview">
<div id="p-foto-gps-info" style="font-size:11px;color:var(--muted);margin-top:5px;"></div>
</div>
</div>
</div>
<!-- hidden inputs retained for API compat -->
<input type="hidden" id="p-verifikasi" value="belum_diverifikasi">
<input type="hidden" id="p-catatan" value="">
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalPenduduk')">Batal</button>
<button class="btn btn-primary" onclick="savePenduduk()" id="btn-save-penduduk">💾 Simpan Data</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
MODAL UPLOAD FOTO
═══════════════════════════════════════ -->
<div class="modal-overlay" id="modalFoto">
<div class="modal">
<div class="modal-header">
<h3>📷 <span id="modal-foto-title">Foto Rumah</span></h3>
<button class="modal-close" onclick="closeModal('modalFoto')"></button>
</div>
<div class="modal-body">
<input type="hidden" id="foto-penduduk-id">
<div id="foto-galeri-wrap" style="margin-bottom:16px;">
<div class="form-section"><h4>📸 Foto Tersimpan</h4></div>
<div class="foto-galeri" id="foto-galeri">
<div style="color:var(--muted);font-size:12px;padding:20px;text-align:center;grid-column:1/-1;">Belum ada foto</div>
</div>
</div>
<div class="form-section"><h4>⬆ Upload Foto Baru</h4></div>
<div class="upload-zone" id="upload-zone" onclick="document.getElementById('foto-file-input').click()" ondragover="onDragOver(event)" ondrop="onDrop(event)">
<span class="upload-icon">📷</span>
<p><b>Klik</b> atau seret foto ke sini</p>
<p class="upload-hint" style="font-size:10px;color:var(--muted);margin-top:5px;">JPG, PNG, WebP · Maks 5 MB · Bisa ambil dari kamera</p>
</div>
<!-- capture="environment" untuk akses kamera langsung di smartphone -->
<input type="file" id="foto-file-input" accept="image/*" capture="environment" style="display:none" onchange="onFotoSelected(event)">
<div id="foto-preview-wrap" style="display:none;margin-top:12px;">
<div class="form-section"><h4>Preview Upload</h4></div>
<img id="foto-preview" style="width:100%;max-height:200px;object-fit:cover;border-radius:var(--radius);border:1.5px solid var(--border);" alt="Preview">
<div style="margin-top:10px;display:flex;gap:8px;align-items:center;">
<div style="flex:1;">
<label style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.7px;display:block;margin-bottom:4px;">Keterangan</label>
<input type="text" id="foto-keterangan" placeholder="Tampak depan, kondisi atap, dll..." style="width:100%;padding:8px 12px;background:var(--surface2);border:1.5px solid var(--border);border-radius:var(--radius);color:var(--text);font-family:inherit;font-size:12px;outline:none;">
</div>
<div>
<label style="font-size:10px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.7px;display:block;margin-bottom:4px;">Utama</label>
<select id="foto-is-primary" style="padding:8px;background:var(--surface2);border:1.5px solid var(--border);border-radius:var(--radius);color:var(--text);font-family:inherit;font-size:12px;outline:none;">
<option value="0">Tidak</option>
<option value="1">Ya (Thumbnail Peta)</option>
</select>
</div>
</div>
<div id="foto-gps-info" style="font-size:11px;color:var(--muted);margin-top:6px;"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalFoto')">Tutup</button>
<button class="btn btn-primary" onclick="uploadFoto()" id="btn-upload-foto" style="display:none;">⬆ Upload Foto</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════
MODAL EXPORT FILTER
═══════════════════════════════════════ -->
<div class="modal-overlay" id="modalExport">
<div class="modal">
<div class="modal-header">
<h3>📤 Export Laporan dengan Filter</h3>
<button class="modal-close" onclick="closeModal('modalExport')"></button>
</div>
<div class="modal-body">
<div class="form-group" style="margin-bottom:12px;">
<label>Kelurahan</label>
<select id="exp-kelurahan"><option value="">— Semua —</option></select>
</div>
<div class="form-group" style="margin-bottom:12px;">
<label>Kategori Kemiskinan</label>
<select id="exp-kategori">
<option value="">— Semua —</option>
<option value="sangat_miskin">Sangat Miskin</option>
<option value="miskin">Miskin</option>
<option value="hampir_miskin">Nyaris Miskin</option>
<option value="mampu">Mampu</option>
</select>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="closeModal('modalExport')">Batal</button>
<button class="btn btn-success" onclick="doExport('excel')">📊 Excel</button>
<button class="btn btn-primary" onclick="doExport('pdf')">📄 PDF</button>
</div>
</div>
</div>
<div id="toast"></div>
<!-- ═══════════════════════════════════════
SCRIPTS
═══════════════════════════════════════ -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ════════════════════════════════════
// CONSTANTS & STATE
// ════════════════════════════════════
const API = 'api';
const BATAS_SANGAT_MISKIN = 1500000; // < 1.5jt → sangat_miskin
const BATAS_MISKIN = 3000000; // < 3jt → miskin
const BATAS_HAMPIR_MISKIN = 5000000; // < 5jt → hampir_miskin
// >= 5jt → mampu
// Demo credentials (fallback jika backend tidak tersedia)
const USERS = {
admin: { password: 'password', role: 'admin', id: 1 },
petugas: { password: 'password', role: 'petugas', id: 2 },
};
let currentRole = null;
let currentUserId = null;
let selectedLoginRole = 'admin';
let map = null;
let ibadahData = [], pendudukData = [], jenisBantuanData = [], wilayahData = [];
let ibadahMarkers = {}, ibadahCircles = {}, pendudukMarkers = {};
let addMode = null;
let editingIbadah = null;
let currentTab = 'penduduk';
let filterActive = {};
let selectedFotoFile = null;
let lastGeocodeTime = 0;
// Warna per jenis - mengikuti legenda yang diperbaiki
const JENIS_COLOR = {
masjid: '#3b7cf4',
musholla: '#3b7cf4',
gereja: '#8b5cf6',
gereja_katolik: '#8b5cf6',
pura: '#f97316',
vihara: '#ec4899',
klenteng: '#eab308',
};
const JENIS_ICON = {
masjid: '🕌',
musholla: '🕌',
gereja: '⛪',
gereja_katolik: '⛪',
pura: '🛕',
vihara: '🏯',
klenteng: '🏮',
};
const JENIS_LABEL = {
masjid: 'Masjid (Islam)',
musholla: 'Musholla (Islam)',
gereja: 'Gereja (Kristen Protestan)',
gereja_katolik: 'Gereja (Kristen Katolik)',
pura: 'Pura (Hindu)',
vihara: 'Vihara (Buddha)',
klenteng: 'Klenteng (Konghucu)',
};
let layerIbadah, layerPenduduk, layerCircles;
// ════════════════════════════════════
// ROLE / AUTH
// ════════════════════════════════════
function selectRole(role) {
selectedLoginRole = role;
['admin','petugas'].forEach(r => {
const el = document.getElementById(`role-${r}-btn`);
if (el) el.classList.toggle('selected', r === role);
});
// Pimpinan card deselect
document.getElementById('role-pimpinan-btn')?.classList.remove('selected');
const uInput = document.getElementById('login-username');
if (!uInput.value || ['admin','petugas'].includes(uInput.value)) {
uInput.value = role;
}
// Show login fields
document.getElementById('login-fields-wrap').style.display = 'block';
}
// Pimpinan: akses langsung tanpa login
function selectRolePimpinan() {
document.getElementById('role-pimpinan-btn').classList.add('selected');
['admin','petugas'].forEach(r => {
document.getElementById(`role-${r}-btn`)?.classList.remove('selected');
});
// Langsung masuk sebagai pimpinan
setSession('pimpinan', null);
hideLogin();
}
async function doLogin() {
const username = document.getElementById('login-username').value.trim().toLowerCase();
const password = document.getElementById('login-password').value;
const errEl = document.getElementById('login-error');
const btn = document.getElementById('btn-login-submit');
if (!username || !password) {
errEl.classList.add('show');
errEl.textContent = '⚠ Username dan password wajib diisi';
return;
}
btn.textContent = '⏳ Masuk...'; btn.disabled = true;
try {
const r = await fetch(`${API}/auth.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
if (r.ok) {
const res = await r.json();
if (res.success) {
setSession(res.user.role, res.user.id);
errEl.classList.remove('show');
hideLogin();
return;
}
}
} catch(e) { /* Backend tidak tersedia, fallback demo */ }
// Fallback hardcoded
const u = USERS[username];
if (!u || u.password !== password) {
errEl.classList.add('show');
errEl.textContent = '⚠ Username atau password salah';
document.getElementById('login-password').value = '';
btn.textContent = 'Masuk →'; btn.disabled = false;
return;
}
errEl.classList.remove('show');
setSession(u.role, u.id);
hideLogin();
}
function setSession(role, userId) {
currentRole = role;
currentUserId = userId || 1;
document.body.classList.remove('role-admin','role-petugas','role-pimpinan');
document.body.classList.add(`role-${role}`);
const badge = document.getElementById('header-role-badge');
const roleInfo = {
admin: { icon:'🛠', label:'Administrator', cls:'admin' },
petugas: { icon:'📋', label:'Petugas Survei', cls:'petugas' },
pimpinan: { icon:'👁', label:'Pimpinan', cls:'pimpinan' },
};
const info = roleInfo[role] || roleInfo.pimpinan;
badge.textContent = `${info.icon} ${info.label}`;
badge.className = `role-badge ${info.cls}`;
// Tampilkan tombol tambah penduduk untuk admin & petugas
const btnTambah = document.getElementById('btn-tambah-penduduk');
if (role === 'admin' || role === 'petugas') {
btnTambah.style.display = 'inline-flex';
} else {
btnTambah.style.display = 'none';
}
// Tampilkan logout untuk semua role
document.getElementById('btn-logout').style.display = 'block';
// Tampilkan notif button untuk admin
if (role === 'admin') {
document.getElementById('btn-notif').style.display = 'flex';
const tabNotif = document.getElementById('tab-notif');
if (tabNotif) tabNotif.style.display = '';
document.querySelectorAll('.admin-only').forEach(el => el.style.display = '');
const verifWrap = document.getElementById('verif-section-wrap');
if (verifWrap) verifWrap.style.display = '';
}
const btn = document.getElementById('btn-login-submit');
if (btn) { btn.textContent = 'Masuk →'; btn.disabled = false; }
}
function hideLogin() {
document.getElementById('login-screen').style.display = 'none';
initMap();
setTimeout(() => loadAll(), 150);
}
function doLogout() {
currentRole = null;
currentUserId = null;
document.body.classList.remove('role-admin','role-petugas','role-pimpinan');
document.getElementById('login-screen').style.display = 'flex';
document.getElementById('login-username').value = '';
document.getElementById('login-password').value = '';
document.getElementById('login-error').classList.remove('show');
// Reset role cards
document.getElementById('role-admin-btn').classList.add('selected');
document.getElementById('role-petugas-btn').classList.remove('selected');
document.getElementById('role-pimpinan-btn').classList.remove('selected');
document.getElementById('login-fields-wrap').style.display = 'block';
selectedLoginRole = 'admin';
if (map) { map.remove(); map = null; }
ibadahData = []; pendudukData = [];
ibadahMarkers = {}; ibadahCircles = {}; pendudukMarkers = {};
}
// ════════════════════════════════════
// MAP INIT
// ════════════════════════════════════
function initMap() {
map = L.map('map', { zoomControl: true }).setView([-0.0263, 109.3425], 14);
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors', maxZoom: 19
});
const sat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles © Esri', maxZoom: 19
});
osm.addTo(map);
layerIbadah = L.layerGroup().addTo(map);
layerPenduduk = L.layerGroup().addTo(map);
layerCircles = L.layerGroup().addTo(map);
L.control.layers(
{ 'OpenStreetMap': osm, '🛰 Satelit (Esri)': sat },
{ '🕌 Rumah Ibadah': layerIbadah, '📐 Radius': layerCircles, '👥 Penduduk Miskin': layerPenduduk },
{ collapsed: true, position: 'topright' }
).addTo(map);
map.on('click', onMapClick);
}
function onMapClick(e) {
if (!addMode) return;
const { lat, lng } = e.latlng;
if (addMode === 'ibadah') {
document.getElementById('ibadah-lat').value = lat;
document.getElementById('ibadah-lng').value = lng;
document.getElementById('ibadah-alamat').value = '';
openModal('modalIbadah');
reverseGeocode(lat, lng, 'ibadah-alamat', 'geocode-status-i', 'ibadah-coords-badge');
} else if (addMode === 'penduduk') {
document.getElementById('penduduk-lat').value = lat;
document.getElementById('penduduk-lng').value = lng;
document.getElementById('p-alamat').value = '';
openModal('modalPenduduk');
reverseGeocode(lat, lng, 'p-alamat', 'geocode-status-p', 'p-coords-badge');
} else if (addMode === 'reposition_penduduk') {
document.getElementById('penduduk-lat').value = lat;
document.getElementById('penduduk-lng').value = lng;
openModal('modalPenduduk');
reverseGeocode(lat, lng, 'p-alamat', 'geocode-status-p', 'p-coords-badge');
} else if (addMode === 'reposition_ibadah') {
document.getElementById('ibadah-lat').value = lat;
document.getElementById('ibadah-lng').value = lng;
openModal('modalIbadah');
reverseGeocode(lat, lng, 'ibadah-alamat', 'geocode-status-i', 'ibadah-coords-badge');
}
cancelMode();
}
// ════════════════════════════════════
// LOAD DATA
// ════════════════════════════════════
async function loadAll() {
try {
const promises = [loadIbadah(), loadPenduduk(), loadWilayah(), loadJenisBantuan()];
if (currentRole === 'admin') promises.push(loadNotifikasi());
await Promise.all(promises);
} catch(e) {
console.error('loadAll error:', e);
toast('⚠ Gagal memuat data. Cek koneksi server.', 'error');
}
updateStats();
renderList();
}
async function loadIbadah() {
try {
const r = await fetch(`${API}/rumah_ibadah.php`);
const d = await r.json();
ibadahData = d.data || [];
} catch(e) { ibadahData = []; }
renderIbadahMarkers();
}
async function loadPenduduk() {
let url = `${API}/penduduk.php`;
const params = buildFilterParams();
if (params.toString()) url += '?' + params.toString();
try {
const r = await fetch(url);
const d = await r.json();
pendudukData = d.data || [];
} catch(e) { pendudukData = []; }
renderPendudukMarkers();
}
async function loadWilayah() {
try {
const r = await fetch(`${API}/wilayah.php`);
const d = await r.json();
wilayahData = d.data || [];
populateWilayahSelects();
} catch(e) { wilayahData = []; }
}
async function loadJenisBantuan() {
try {
const r = await fetch(`${API}/bantuan.php?action=jenis`);
if (!r.ok) throw new Error('API not available');
const d = await r.json();
jenisBantuanData = d.data || [];
if (jenisBantuanData.length === 0) throw new Error('Data kosong');
} catch(e) {
// Fallback demo data jika backend tidak tersedia
jenisBantuanData = [
{ id:1, kode:'PKH', nama:'Program Keluarga Harapan', warna_badge:'#059669' },
{ id:2, kode:'BPNT', nama:'Bantuan Pangan Non Tunai', warna_badge:'#2563eb' },
{ id:3, kode:'BLT', nama:'Bantuan Langsung Tunai', warna_badge:'#d97706' },
{ id:4, kode:'BST', nama:'Bantuan Sosial Tunai', warna_badge:'#7c3aed' },
{ id:5, kode:'BNBA', nama:'Bansos Beras/Sembako', warna_badge:'#dc2626' },
{ id:6, kode:'KIS', nama:'Kartu Indonesia Sehat', warna_badge:'#0891b2' },
{ id:7, kode:'KIP', nama:'Kartu Indonesia Pintar', warna_badge:'#16a34a' },
{ id:8, kode:'RUTILAHU', nama:'Rehabilitasi Rumah Tdk Layak', warna_badge:'#92400e' },
];
}
renderBantuanSelector([]);
}
async function loadNotifikasi() {
try {
const r = await fetch(`${API}/notifikasi.php?generate`);
const d = await r.json();
renderNotifikasi(d.data || []);
updateNotifBadge(d.unread_count || 0);
} catch(e) {}
}
function buildFilterParams() {
const p = new URLSearchParams();
if (filterActive.kelurahan) p.set('kelurahan', filterActive.kelurahan);
if (filterActive.kecamatan) p.set('kecamatan', filterActive.kecamatan);
if (filterActive.kategori) p.set('kategori', filterActive.kategori);
if (filterActive.bantuan) p.set('bantuan', filterActive.bantuan);
if (filterActive.verifikasi) p.set('verifikasi',filterActive.verifikasi);
return p;
}
function populateWilayahSelects() {
const kelurahans = [...new Set(wilayahData.map(w => w.kelurahan))].sort();
const kecamatans = [...new Set(wilayahData.map(w => w.kecamatan))].sort();
const pw = document.getElementById('p-wilayah');
if (pw) {
pw.innerHTML = '<option value="">— Pilih Kelurahan/Wilayah —</option>';
wilayahData.forEach(w => {
const opt = document.createElement('option');
opt.value = w.id;
opt.textContent = `${w.kelurahan} RT ${w.rt}/RW ${w.rw}`;
pw.appendChild(opt);
});
}
['filter-kelurahan','exp-kelurahan'].forEach(id => {
const sel = document.getElementById(id);
if (!sel) return;
const cur = sel.value;
sel.innerHTML = '<option value="">— Semua Kelurahan —</option>';
kelurahans.forEach(k => {
const opt = document.createElement('option');
opt.value = k; opt.textContent = k;
if (k === cur) opt.selected = true;
sel.appendChild(opt);
});
});
const fk = document.getElementById('filter-kecamatan');
if (fk) {
fk.innerHTML = '<option value="">— Semua Kecamatan —</option>';
kecamatans.forEach(k => {
const opt = document.createElement('option');
opt.value = k; opt.textContent = k; fk.appendChild(opt);
});
}
}
// ════════════════════════════════════
// BANTUAN SELECTOR
// ════════════════════════════════════
let selectedBantuanIds = new Set();
function renderBantuanSelector(activeBantuanIds) {
selectedBantuanIds = new Set(activeBantuanIds.map(Number));
const container = document.getElementById('bantuan-selector');
if (!container) return;
container.innerHTML = jenisBantuanData.map(jb => {
const sel = selectedBantuanIds.has(jb.id);
return `<div class="bantuan-item ${sel?'selected':''}" id="bi-${jb.id}" onclick="toggleBantuan(${jb.id})">
<div class="bantuan-item-header">
<span class="bantuan-kode" style="color:${jb.warna_badge}">${jb.kode}</span>
<div class="bantuan-check">${sel?'✓':''}</div>
</div>
<div class="bantuan-nama">${jb.nama}</div>
</div>`;
}).join('');
}
function toggleBantuan(id) {
const el = document.getElementById(`bi-${id}`);
if (selectedBantuanIds.has(id)) {
selectedBantuanIds.delete(id);
el.classList.remove('selected');
el.querySelector('.bantuan-check').textContent = '';
} else {
selectedBantuanIds.add(id);
el.classList.add('selected');
el.querySelector('.bantuan-check').textContent = '✓';
}
}
// ════════════════════════════════════
// MARKERS
// ════════════════════════════════════
function makePendudukColor(p) {
return p.nama_ibadah ? '#22c55e' : '#ef4444';
}
function makePendudukIcon(p) {
const color = makePendudukColor(p);
const borderMap = { '#22c55e': '#16a34a', '#ef4444': '#dc2626' };
const shadowMap = { '#22c55e': 'rgba(34,197,94,.4)', '#ef4444': 'rgba(239,68,68,.4)' };
const border = borderMap[color];
const shadow = shadowMap[color];
return L.divIcon({
className: '',
html: `<div style="width:16px;height:16px;border-radius:50%;background:${color};border:2.5px solid ${border};box-shadow:0 0 0 3px ${shadow},0 2px 6px rgba(0,0,0,.25);"></div>`,
iconSize: [16, 16], iconAnchor: [8, 8],
});
}
function makeIbadahIcon(jenis) {
const color = JENIS_COLOR[jenis] || '#6b7280';
const ic = JENIS_ICON[jenis] || '📍';
return L.divIcon({
className: '',
html: `<div style="width:36px;height:36px;border-radius:8px;background:${color};color:#fff;display:flex;align-items:center;justify-content:center;font-size:17px;box-shadow:0 3px 10px rgba(0,0,0,.2);border:2px solid rgba(255,255,255,.5);">${ic}</div>`,
iconSize: [36, 36], iconAnchor: [18, 36],
});
}
function renderIbadahMarkers() {
Object.values(ibadahMarkers).forEach(m => layerIbadah.removeLayer(m));
Object.values(ibadahCircles).forEach(c => layerCircles.removeLayer(c));
ibadahMarkers = {}; ibadahCircles = {};
ibadahData.forEach(ri => {
const color = JENIS_COLOR[ri.jenis] || '#6b7280';
const circle = L.circle([ri.lat, ri.lng], { radius: ri.radius, color, fillColor: color, fillOpacity: 0.06, weight: 1.5, dashArray: '5,4' }).addTo(layerCircles);
ibadahCircles[ri.id] = circle;
const marker = L.marker([ri.lat, ri.lng], { icon: makeIbadahIcon(ri.jenis) }).addTo(layerIbadah).bindPopup(buildIbadahPopup(ri));
ibadahMarkers[ri.id] = marker;
marker.on('click', () => selectListItem('ibadah', ri.id));
});
}
function renderPendudukMarkers() {
Object.values(pendudukMarkers).forEach(m => layerPenduduk.removeLayer(m));
pendudukMarkers = {};
const searchQ = document.getElementById('search-input')?.value?.toLowerCase() || '';
pendudukData.forEach(p => {
if (searchQ && !`${p.nama_kk} ${p.nik_kk} ${p.alamat||''}`.toLowerCase().includes(searchQ)) return;
const marker = L.marker([p.lat, p.lng], { icon: makePendudukIcon(p) })
.addTo(layerPenduduk)
.bindPopup(buildPendudukPopup(p), { maxWidth: 300 })
.bindTooltip(`<b>${p.nama_kk}</b><br>${p.kategori_kemiskinan?.replace(/_/g,' ')}`, { direction: 'top', offset: [0,-8], opacity: 0.95 });
pendudukMarkers[p.id] = marker;
});
}
// ════════════════════════════════════
// POPUP BUILDERS
// ════════════════════════════════════
function buildIbadahPopup(ri) {
const ic = JENIS_ICON[ri.jenis] || '📍';
const label = JENIS_LABEL[ri.jenis] || ri.jenis;
const isAdmin = currentRole === 'admin';
return `
<div class="popup-title">${ic} ${ri.nama}</div>
<div class="popup-detail-section">
<div class="popup-row"><span class="popup-label">Jenis</span><span class="popup-val">${label}</span></div>
<div class="popup-row"><span class="popup-label">Alamat</span><span>${ri.alamat||''}</span></div>
<div class="popup-row"><span class="popup-label">PIC</span><span>${ri.pic||''}</span></div>
<div class="popup-row"><span class="popup-label">Radius</span><span style="font-weight:700">${ri.radius} m</span></div>
<div class="popup-row"><span class="popup-label">Koordinat</span><span style="font-family:monospace;font-size:11px;color:var(--muted)">${parseFloat(ri.lat).toFixed(5)}, ${parseFloat(ri.lng).toFixed(5)}</span></div>
<div class="popup-row"><span class="popup-label">KK Tercakup</span><span><b>${ri.jumlah_kk??''}</b> KK / <b>${ri.total_jiwa??''}</b> jiwa</span></div>
</div>
${isAdmin ? `<div class="popup-actions">
<button class="btn btn-ghost btn-sm" onclick="editIbadah(${ri.id})">✏ Edit</button>
<button class="btn btn-ghost btn-sm" onclick="openRadiusControl(${ri.id})">📐 Radius</button>
<button class="btn btn-danger btn-sm" onclick="deleteIbadah(${ri.id})">🗑</button>
</div>` : ''}`;
}
function buildPendudukPopup(p) {
const isVerif = p.status_verifikasi === 'terverifikasi';
const hasBantuan = parseInt(p.jumlah_bantuan) > 0;
const bantuanLabel = hasBantuan ? `✅ Penerima Bantuan` : `❌ Belum Bantuan`;
const bantuanClr = hasBantuan ? 'var(--green)' : 'var(--red)';
const kategoriLabel = { sangat_miskin:'🔴 Sangat Miskin', miskin:'🟡 Miskin', hampir_miskin:'🟢 Nyaris Miskin', mampu:'🔵 Mampu' };
const radiusInfo = p.nama_ibadah
? `✅ Di Dalam Radius (${p.nama_ibadah} - ${Math.round(p.jarak_ke_ibadah)}m)`
: `❌ Di Luar Radius`;
const bantuanBadges = p.kode_bantuan ? p.kode_bantuan.split(',').map(k =>
`<span class="badge badge-blue" style="font-size:10px">${k.trim()}</span>`).join('') : '<span class="badge badge-red">Belum Ada</span>';
const foto = p.foto_utama ? `<img src="${p.foto_utama}" class="popup-foto" alt="Foto Rumah" onerror="this.style.display='none'">` : '';
const canEdit = ['admin','petugas'].includes(currentRole);
const isAdmin = currentRole === 'admin';
return `
${foto}
<div class="popup-title">👤 ${p.nama_kk}</div>
<div style="margin-bottom:6px;"><span style="font-size:12px;font-weight:700;color:${bantuanClr}">${bantuanLabel}</span></div>
<div class="popup-detail-section">
<div class="popup-row"><span class="popup-label">NIK</span><span style="font-family:monospace;font-size:11px">${p.nik_kk||''}</span></div>
<div class="popup-row"><span class="popup-label">Alamat</span><span>${p.alamat||''}</span></div>
<div class="popup-row"><span class="popup-label">Anggota</span><span>${p.jumlah_anggota} jiwa (${p.jumlah_anak_sekolah} anak sekolah)</span></div>
<div class="popup-row"><span class="popup-label">Penghasilan</span><span>${p.penghasilan_bulanan ? 'Rp '+Number(p.penghasilan_bulanan).toLocaleString('id') : ''}/bln</span></div>
<div class="popup-row"><span class="popup-label">Kategori</span><span>${kategoriLabel[p.kategori_kemiskinan]||p.kategori_kemiskinan}</span></div>
<div class="popup-row"><span class="popup-label">Pekerjaan</span><span>${(p.status_pekerjaan||'').replace(/_/g,' ')}</span></div>
<div class="popup-row"><span class="popup-label">Kondisi Rmh</span><span>${(p.kondisi_rumah||'').replace(/_/g,' ')}</span></div>
<div class="popup-row"><span class="popup-label">Bantuan</span><span>${bantuanBadges}</span></div>
<div class="popup-row"><span class="popup-label">Radius Ibadah</span><span style="font-weight:600">${radiusInfo}</span></div>
</div>
${canEdit ? `<div class="popup-actions">
<button class="btn btn-ghost btn-sm" onclick="editPenduduk(${p.id})">✏ Edit</button>
<button class="btn btn-purple btn-sm" onclick="openFotoModal(${p.id},'${p.nama_kk.replace(/'/g,"\\'")}')">📷 Foto</button>
${isAdmin ? `<button class="btn btn-danger btn-sm" onclick="deletePenduduk(${p.id})">🗑</button>` : ''}
</div>` : ''}`;
}
// ════════════════════════════════════
// STATS
// ════════════════════════════════════
function updateStats() {
const total = pendudukData.length;
const jiwa = pendudukData.reduce((s,p) => s + (p.jumlah_anggota||0), 0);
const bantuan = pendudukData.filter(p => parseInt(p.jumlah_bantuan) > 0).length;
const noBantuan = total - bantuan;
document.getElementById('s-kk').textContent = total;
document.getElementById('s-jiwa').textContent = jiwa;
document.getElementById('s-bantuan').textContent = bantuan;
document.getElementById('s-no-bantuan').textContent = noBantuan;
document.getElementById('s-ibadah').textContent = ibadahData.length;
}
// ════════════════════════════════════
// SIDEBAR TABS
// ════════════════════════════════════
function switchTab(tab) {
currentTab = tab;
['penduduk','ibadah','filter','notif'].forEach(t => {
const tabEl = document.getElementById(`tab-${t}`);
if (tabEl) tabEl.classList.toggle('active', t === tab);
});
document.getElementById('list-penduduk').style.display = tab === 'penduduk' ? 'block' : 'none';
document.getElementById('list-ibadah').style.display = tab === 'ibadah' ? 'block' : 'none';
document.getElementById('panel-filter').style.display = tab === 'filter' ? 'flex' : 'none';
document.getElementById('panel-notif').style.display = tab === 'notif' ? 'flex' : 'none';
renderList();
}
function showNotifTab() { switchTab('notif'); }
function renderList() {
if (currentTab === 'penduduk') renderPendudukList();
else if (currentTab === 'ibadah') renderIbadahList();
}
function filterList() { renderPendudukMarkers(); renderPendudukList(); }
function renderPendudukList() {
const c = document.getElementById('list-penduduk');
const searchQ = (document.getElementById('search-input')?.value || '').toLowerCase();
let data = pendudukData;
if (searchQ) data = data.filter(p => `${p.nama_kk} ${p.nik_kk} ${p.alamat||''}`.toLowerCase().includes(searchQ));
if (data.length === 0) {
c.innerHTML = `<div style="padding:30px;text-align:center;color:var(--muted);font-size:12px">
${searchQ ? '🔍 Tidak ditemukan.' : '📭 Belum ada data.'}<br><small>${searchQ ? 'Coba kata kunci lain.' : 'Klik "+ Penduduk" untuk menambah data.'}</small></div>`;
return;
}
const canEdit = ['admin','petugas'].includes(currentRole);
c.innerHTML = data.map(p => {
const hasBantuan = parseInt(p.jumlah_bantuan) > 0;
const color = makePendudukColor(p);
const bantuanBadge = hasBantuan
? `<span class="badge badge-green">✅ ${p.kode_bantuan||'Ada'}</span>`
: `<span class="badge badge-red">❌ Belum Bantuan</span>`;
const radiusBadge = p.nama_ibadah
? `<span class="badge badge-green" title="${p.nama_ibadah} (${Math.round(p.jarak_ke_ibadah)}m)" style="background:#dcfce7;color:#15803d;border-color:#bbf7d0;">🟢 Dalam Radius</span>`
: `<span class="badge badge-red" style="background:#fee2e2;color:#dc2626;border-color:#fca5a5;">🔴 Luar Radius</span>`;
const katLabel = { sangat_miskin:'Sangat Miskin', miskin:'Miskin', hampir_miskin:'Nyaris Miskin', mampu:'Mampu' };
return `<div class="list-item" onclick="flyToPenduduk(${p.id})" id="li-p-${p.id}">
<div class="list-item-header">
<div class="list-item-dot" style="background:${color};"></div>
<div class="list-item-name">${p.nama_kk}</div>
</div>
<div class="list-item-nik">NIK: ${p.nik_kk||''}</div>
<div class="list-item-meta">${p.alamat||p.kelurahan||''}</div>
<div class="list-item-badges">${bantuanBadge}${radiusBadge}<span class="badge badge-blue">👪 ${p.jumlah_anggota} jiwa</span><span class="badge badge-gray">${katLabel[p.kategori_kemiskinan]||''}</span></div>
${canEdit ? `<div class="list-item-actions">
<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();editPenduduk(${p.id})">✏ Edit</button>
<button class="btn btn-purple btn-sm" onclick="event.stopPropagation();openFotoModal(${p.id},'${p.nama_kk.replace(/'/g,"\\'")}')">📷</button>
${currentRole==='admin'?`<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deletePenduduk(${p.id})">🗑</button>`:''}
</div>` : ''}
</div>`;
}).join('');
}
function renderIbadahList() {
const c = document.getElementById('list-ibadah');
if (ibadahData.length === 0) {
c.innerHTML = `<div style="padding:30px;text-align:center;color:var(--muted);font-size:12px">📭 Belum ada data.</div>`;
return;
}
const isAdmin = currentRole === 'admin';
c.innerHTML = ibadahData.map(ri => {
const color = JENIS_COLOR[ri.jenis] || '#6b7280';
const ic = JENIS_ICON[ri.jenis] || '📍';
const label = JENIS_LABEL[ri.jenis] || ri.jenis;
return `<div class="list-item" onclick="flyToIbadah(${ri.id})" id="li-i-${ri.id}">
<div class="list-item-header">
<span style="background:${color};width:22px;height:22px;border-radius:5px;display:inline-flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0;">${ic}</span>
<div class="list-item-name">${ri.nama}</div>
</div>
<div class="list-item-meta" style="font-size:10px;color:var(--muted);">${label}</div>
<div class="list-item-meta">${ri.alamat||''} · radius ${ri.radius}m</div>
<div class="list-item-badges">
<span class="badge badge-blue">👥 ${ri.jumlah_kk??0} KK</span>
<span class="badge badge-green">👤 ${ri.total_jiwa??0} jiwa</span>
</div>
${isAdmin ? `<div class="list-item-actions">
<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();editIbadah(${ri.id})">✏ Edit</button>
<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();openRadiusControl(${ri.id})">📐</button>
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deleteIbadah(${ri.id})">🗑</button>
</div>` : ''}
</div>`;
}).join('');
}
// ════════════════════════════════════
// NOTIFIKASI
// ════════════════════════════════════
const NOTIF_ICON = { belum_bantuan:'🚨', belum_verifikasi:'⏳', data_usang:'📅', info:'️' };
function renderNotifikasi(data) {
const c = document.getElementById('list-notif');
if (!data.length) { c.innerHTML = `<div class="notif-empty">🎉 Tidak ada notifikasi</div>`; return; }
c.innerHTML = data.map(n => `
<div class="notif-item jenis-${n.jenis} ${!n.sudah_dibaca?'unread':''}" onclick="clickNotif(${n.id}, ${n.penduduk_id||0})">
<div class="notif-header">
<span class="notif-icon">${NOTIF_ICON[n.jenis]||'️'}</span>
<span class="notif-judul">${n.judul}</span>
<span class="notif-time">${timeAgo(n.created_at)}</span>
</div>
<div class="notif-pesan">${n.pesan}${n.nama_kk?` — <b>${n.nama_kk}</b>`:''}</div>
</div>`).join('');
}
function updateNotifBadge(count) {
const badge = document.getElementById('notif-badge');
const tabBadge = document.getElementById('tab-notif-badge');
if (count > 0) {
badge.textContent = count; badge.classList.add('show');
if (tabBadge) { tabBadge.textContent = count; tabBadge.style.display = 'block'; }
} else {
badge.classList.remove('show');
if (tabBadge) tabBadge.style.display = 'none';
}
}
async function clickNotif(id, pendudukId) {
await fetch(`${API}/notifikasi.php`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id}) });
if (pendudukId) flyToPenduduk(pendudukId);
await loadNotifikasi();
}
async function markAllRead() {
await fetch(`${API}/notifikasi.php`, { method:'PUT', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id:'all', user_id: currentUserId}) });
await loadNotifikasi();
toast('✓ Semua notifikasi ditandai dibaca', 'success');
}
function timeAgo(dateStr) {
const d = new Date(dateStr);
const diff = Math.floor((Date.now() - d) / 1000);
if (diff < 60) return `${diff}d lalu`;
if (diff < 3600) return `${Math.floor(diff/60)}m lalu`;
if (diff < 86400) return `${Math.floor(diff/3600)}j lalu`;
return `${Math.floor(diff/86400)} hari lalu`;
}
// ════════════════════════════════════
// NAVIGATION
// ════════════════════════════════════
function flyToIbadah(id) {
const ri = ibadahData.find(r => r.id == id);
if (!ri) return;
switchTab('ibadah');
map.flyTo([ri.lat, ri.lng], 16, { duration: .8 });
setTimeout(() => ibadahMarkers[id]?.openPopup(), 900);
selectListItem('ibadah', id);
}
function flyToPenduduk(id) {
const p = pendudukData.find(x => x.id == id);
if (!p) return;
switchTab('penduduk');
map.flyTo([p.lat, p.lng], 17, { duration: .8 });
setTimeout(() => pendudukMarkers[id]?.openPopup(), 900);
selectListItem('penduduk', id);
}
function selectListItem(type, id) {
document.querySelectorAll('.list-item').forEach(el => el.classList.remove('selected'));
document.getElementById(`li-${type === 'penduduk'?'p':'i'}-${id}`)?.classList.add('selected');
}
function fitAllMarkers() {
const bounds = [];
Object.values(ibadahMarkers).concat(Object.values(pendudukMarkers)).forEach(m => {
if (m.getLatLng) bounds.push(m.getLatLng());
});
if (bounds.length) map.fitBounds(bounds, { padding: [40, 40] });
}
// ════════════════════════════════════
// FILTER
// ════════════════════════════════════
function applyFilter() {
filterActive = {
kelurahan: document.getElementById('filter-kelurahan')?.value || '',
kecamatan: document.getElementById('filter-kecamatan')?.value || '',
kategori: document.getElementById('filter-kategori')?.value || '',
bantuan: document.getElementById('filter-bantuan')?.value || '',
pekerjaan: document.getElementById('filter-pekerjaan')?.value || '',
};
Object.keys(filterActive).forEach(k => { if (!filterActive[k]) delete filterActive[k]; });
loadPenduduk().then(() => { updateStats(); renderList(); });
}
function resetFilter() {
['filter-kelurahan','filter-kecamatan','filter-kategori','filter-bantuan','filter-pekerjaan'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
filterActive = {};
loadPenduduk().then(() => { updateStats(); renderList(); });
}
// ════════════════════════════════════
// REVERSE GEOCODING
// ════════════════════════════════════
async function reverseGeocode(lat, lng, fieldId, statusId, badgeId) {
const field = document.getElementById(fieldId);
const status = document.getElementById(statusId);
const badge = document.getElementById(badgeId);
const now = Date.now();
const wait = Math.max(0, 1100 - (now - lastGeocodeTime));
if (wait > 0) await new Promise(r => setTimeout(r, wait));
lastGeocodeTime = Date.now();
const latStr = parseFloat(lat).toFixed(6);
const lngStr = parseFloat(lng).toFixed(6);
if (badge) badge.textContent = `📍 ${latStr}, ${lngStr}`;
setStatus(status, 'loading', '⏳ Mencari alamat...');
try {
const url = new URL('https://nominatim.openstreetmap.org/reverse');
url.searchParams.set('format','jsonv2'); url.searchParams.set('lat', lat);
url.searchParams.set('lon', lng); url.searchParams.set('zoom','18');
url.searchParams.set('addressdetails','1'); url.searchParams.set('accept-language','id');
const res = await fetch(url.toString(), { headers: { 'Accept':'application/json','User-Agent':'SIG-Kemiskinan/3.0' } });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.error) throw new Error(data.error);
const alamat = buildAlamatIndonesia(data.address || {});
if (field && field.value === '') {
field.value = alamat;
field.style.transition = 'border-color .3s';
field.style.borderColor = 'var(--accent2)';
setTimeout(() => { field.style.borderColor = ''; }, 1500);
}
setStatus(status, 'success', `✅ Alamat ditemukan`);
setTimeout(() => setStatus(status, 'hidden', ''), 4000);
} catch(err) {
if (field && field.value === '') field.value = `${latStr}, ${lngStr}`;
setStatus(status, 'error', '⚠ Alamat tidak ditemukan koordinat disimpan');
setTimeout(() => setStatus(status, 'hidden', ''), 5000);
}
}
function buildAlamatIndonesia(addr) {
const parts = [];
const jalan = addr.road||addr.pedestrian||addr.path||addr.residential||null;
const nomer = addr.house_number ? ` No. ${addr.house_number}` : '';
if (jalan) parts.push(jalan + nomer);
const rtrw = addr.hamlet||addr.allotments||null;
if (rtrw) parts.push(rtrw);
const kel = addr.neighbourhood||addr.suburb||addr.quarter||addr.village||addr.town||null;
if (kel) parts.push('Kel. '+kel);
const kec = addr.municipality||addr.district||null;
if (kec && kec !== kel) parts.push('Kec. '+kec);
const kota = addr.city||addr.county||null;
if (kota && kota !== kec) parts.push(kota);
const prov = addr.state||null; if (prov) parts.push(prov);
if (parts.length === 0 && addr.display_name) return addr.display_name.split(',').slice(0,4).join(',').trim();
return parts.join(', ');
}
function setStatus(el, type, msg) {
if (!el) return;
el.className = `geocode-status ${type}`;
if (type === 'loading') el.innerHTML = `<span class="spinner"></span> ${msg}`;
else el.textContent = msg;
}
// ════════════════════════════════════
// GPS AUTO DETECT
// ════════════════════════════════════
async function getGPSPenduduk() {
const btn = document.getElementById('btn-gps-penduduk');
if (!navigator.geolocation) return toast('GPS tidak didukung browser ini', 'error');
btn.textContent = '⏳ Mengambil koordinat...'; btn.classList.add('loading');
navigator.geolocation.getCurrentPosition(
pos => {
const lat = pos.coords.latitude, lng = pos.coords.longitude;
document.getElementById('penduduk-lat').value = lat;
document.getElementById('penduduk-lng').value = lng;
reverseGeocode(lat, lng, 'p-alamat', 'geocode-status-p', 'p-coords-badge');
btn.textContent = `✅ GPS: ${lat.toFixed(5)}, ${lng.toFixed(5)}${Math.round(pos.coords.accuracy)}m)`;
btn.classList.remove('loading');
toast(`📍 Koordinat berhasil diambil (±${Math.round(pos.coords.accuracy)}m)`, 'success');
},
err => {
btn.textContent = '📡 Ambil Koordinat GPS Otomatis';
btn.classList.remove('loading');
toast('GPS tidak tersedia: '+err.message, 'error');
},
{ enableHighAccuracy: true, timeout: 10000 }
);
}
// ════════════════════════════════════
// KATEGORI HINT
// ════════════════════════════════════
document.addEventListener('DOMContentLoaded', () => {
const pengInput = document.getElementById('p-penghasilan');
if (pengInput) {
pengInput.addEventListener('input', () => {
const v = parseFloat(pengInput.value);
let kat = '—', color = 'var(--muted)';
if (!isNaN(v) && v >= 0) {
if (v < BATAS_SANGAT_MISKIN) { kat = '🔴 Sangat Miskin'; color='var(--red)'; }
else if (v < BATAS_MISKIN) { kat = '🟡 Miskin'; color='var(--warn)'; }
else if (v < BATAS_HAMPIR_MISKIN) { kat = '🟢 Nyaris Miskin'; color='var(--green)'; }
else { kat = '🔵 Mampu'; color='var(--accent)'; }
}
const hint = document.getElementById('p-kategori-hint');
if (hint) { hint.textContent = `Kategori: ${kat}`; hint.style.color = color; }
});
}
});
// ════════════════════════════════════
// ADD MODE
// ════════════════════════════════════
function startAddIbadah() {
if (!['admin'].includes(currentRole)) return toast('Hanya admin yang dapat menambah ibadah', 'error');
addMode = 'ibadah';
document.getElementById('ibadah-id').value = '';
['ibadah-nama','ibadah-alamat','ibadah-pic','ibadah-wa'].forEach(id => document.getElementById(id).value = '');
document.getElementById('ibadah-radius').value = 500;
document.getElementById('ibadah-coords-badge').textContent = '';
setStatus(document.getElementById('geocode-status-i'), 'hidden', '');
document.getElementById('modal-ibadah-title').textContent = 'Tambah Rumah Ibadah';
document.getElementById('mode-text').textContent = '🕌 Klik peta untuk lokasi Rumah Ibadah';
document.getElementById('mode-bar').classList.add('show');
map.getContainer().style.cursor = 'crosshair';
}
function startAddPenduduk() {
if (!['admin','petugas'].includes(currentRole)) return toast('Anda tidak memiliki akses untuk menambah data', 'error');
addMode = 'penduduk';
document.getElementById('penduduk-id').value = '';
['p-nik','p-nama','p-hp','p-alamat','p-sumber','p-catatan'].forEach(id => document.getElementById(id).value = '');
['penduduk-lat','penduduk-lng'].forEach(id => document.getElementById(id).value = '');
document.getElementById('p-jiwa').value = 1;
document.getElementById('p-anak').value = 0;
document.getElementById('p-bpjs').value = 'tidak_punya';
document.getElementById('p-pekerjaan').value = 'tidak_bekerja';
document.getElementById('p-kondisi').value = 'tidak_layak';
document.getElementById('p-kepemilikan').value = 'milik_sendiri';
document.getElementById('p-verifikasi').value = 'belum_diverifikasi';
document.getElementById('p-coords-badge').textContent = '';
setStatus(document.getElementById('geocode-status-p'), 'hidden', '');
document.getElementById('p-kategori-hint').textContent = 'Kategori: —';
renderBantuanSelector([]);
// Reset foto utama input & preview
document.getElementById('p-foto-input').value = '';
document.getElementById('p-foto-preview').src = '';
document.getElementById('p-foto-preview-wrap').style.display = 'none';
document.getElementById('p-foto-gps-info').textContent = '';
delete document.getElementById('p-foto-gps-info').dataset.lat;
delete document.getElementById('p-foto-gps-info').dataset.lng;
document.getElementById('modal-penduduk-title').textContent = 'Tambah Penduduk Miskin';
document.getElementById('mode-text').textContent = '👥 Klik peta untuk lokasi Rumah Penduduk';
document.getElementById('mode-bar').classList.add('show');
map.getContainer().style.cursor = 'crosshair';
}
function cancelMode() {
addMode = null;
document.getElementById('mode-bar').classList.remove('show');
if (map) map.getContainer().style.cursor = '';
}
// ════════════════════════════════════
// CRUD RUMAH IBADAH
// ════════════════════════════════════
async function saveIbadah() {
const id = document.getElementById('ibadah-id').value;
const data = {
nama: document.getElementById('ibadah-nama').value.trim(),
jenis: document.getElementById('ibadah-jenis').value,
alamat: document.getElementById('ibadah-alamat').value.trim(),
pic: document.getElementById('ibadah-pic').value.trim(),
no_wa: document.getElementById('ibadah-wa').value.trim(),
lat: document.getElementById('ibadah-lat').value,
lng: document.getElementById('ibadah-lng').value,
radius: document.getElementById('ibadah-radius').value,
};
if (!data.nama) return toast('Nama rumah ibadah wajib diisi!', 'error');
if (!data.lat || !data.lng) return toast('Lokasi wajib diisi! Klik peta terlebih dahulu.', 'error');
const btn = document.getElementById('btn-save-ibadah');
btn.innerHTML = '<span class="spinner"></span> Menyimpan...'; btn.disabled = true;
try {
const method = id ? 'PUT' : 'POST';
if (id) data.id = id;
const r = await fetch(`${API}/rumah_ibadah.php`, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
const text = await r.text();
let res;
try { res = JSON.parse(text); }
catch(e) {
console.error('Server response (non-JSON):', text);
throw new Error('Server error: ' + (text.substring(0,120) || `HTTP ${r.status}`));
}
if (!res.success) throw new Error(res.error || res.message || 'Gagal menyimpan');
toast(res.message || '✅ Berhasil disimpan', 'success');
closeModal('modalIbadah');
await loadAll();
} catch(e) {
console.error('saveIbadah error:', e);
toast('❌ ' + e.message, 'error');
}
finally { btn.innerHTML = '💾 Simpan'; btn.disabled = false; }
}
async function editIbadah(id) {
const ri = ibadahData.find(r => r.id == id);
if (!ri) return;
document.getElementById('ibadah-id').value = ri.id;
document.getElementById('ibadah-lat').value = ri.lat;
document.getElementById('ibadah-lng').value = ri.lng;
document.getElementById('ibadah-nama').value = ri.nama;
document.getElementById('ibadah-jenis').value = ri.jenis;
document.getElementById('ibadah-alamat').value = ri.alamat||'';
document.getElementById('ibadah-pic').value = ri.pic||'';
document.getElementById('ibadah-wa').value = ri.no_wa||'';
document.getElementById('ibadah-radius').value = ri.radius;
document.getElementById('ibadah-coords-badge').textContent = `📍 ${parseFloat(ri.lat).toFixed(6)}, ${parseFloat(ri.lng).toFixed(6)}`;
setStatus(document.getElementById('geocode-status-i'), 'hidden', '');
document.getElementById('modal-ibadah-title').textContent = 'Edit Rumah Ibadah';
map.closePopup();
openModal('modalIbadah');
}
async function deleteIbadah(id) {
const ri = ibadahData.find(r => r.id == id);
if (!confirm(`Hapus "${ri?.nama}"?`)) return;
try {
const r = await fetch(`${API}/rumah_ibadah.php?id=${id}`, { method:'DELETE' });
const text = await r.text();
let res;
try { res = JSON.parse(text); } catch(e) { throw new Error('Server error: ' + text.substring(0,100)); }
if (res.success) { toast('✅ Berhasil dihapus', 'success'); await loadAll(); }
else toast('❌ ' + (res.error||'Gagal menghapus'), 'error');
} catch(e) {
console.error('deleteIbadah error:', e);
toast('❌ ' + e.message, 'error');
}
map.closePopup();
}
// ════════════════════════════════════
// CRUD PENDUDUK
// ════════════════════════════════════
async function savePenduduk() {
const id = document.getElementById('penduduk-id').value;
const lat = document.getElementById('penduduk-lat').value;
const lng = document.getElementById('penduduk-lng').value;
const nik = document.getElementById('p-nik').value.trim();
const nama= document.getElementById('p-nama').value.trim();
if (!nik || nik.length !== 16) return toast('NIK harus 16 digit!', 'error');
if (!nama) return toast('Nama Kepala Keluarga wajib diisi!', 'error');
if (!lat || !lng) return toast('Lokasi wajib diisi! Klik peta atau gunakan GPS.', 'error');
const data = {
nik_kk: nik, nama_kk: nama,
no_hp: document.getElementById('p-hp').value.trim(),
alamat: document.getElementById('p-alamat').value.trim(),
wilayah_id: null,
rt: document.getElementById('p-rt').value.trim(),
rw: document.getElementById('p-rw').value.trim(),
lat: parseFloat(lat), lng: parseFloat(lng),
jumlah_anggota: parseInt(document.getElementById('p-jiwa').value) || 1,
jumlah_anak_sekolah: parseInt(document.getElementById('p-anak').value) || 0,
penghasilan_bulanan: document.getElementById('p-penghasilan').value || null,
sumber_penghasilan: document.getElementById('p-sumber').value.trim(),
status_pekerjaan: document.getElementById('p-pekerjaan').value,
kondisi_rumah: document.getElementById('p-kondisi').value,
kepemilikan_rumah: document.getElementById('p-kepemilikan').value,
punya_bpjs: document.getElementById('p-bpjs').value !== 'tidak_punya' ? 1 : 0,
jenis_bpjs: document.getElementById('p-bpjs').value,
surveyor_id: currentUserId,
bantuan_ids: [...selectedBantuanIds],
};
// Verifikasi hanya untuk admin
if (currentRole === 'admin') {
data.status_verifikasi = document.getElementById('p-verifikasi').value;
data.catatan_verifikasi = document.getElementById('p-catatan').value.trim();
} else {
if (!id) data.status_verifikasi = 'belum_diverifikasi';
}
if (id) { data.id = id; data.updated_by = currentUserId; }
const btn = document.getElementById('btn-save-penduduk');
btn.innerHTML = '<span class="spinner"></span> Menyimpan...'; btn.disabled = true;
try {
const method = id ? 'PUT' : 'POST';
const r = await fetch(`${API}/penduduk.php`, { method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
const text = await r.text();
let res;
try {
res = JSON.parse(text);
} catch(err) {
console.error("Non-JSON Server Response:", text);
throw new Error(`Respon tidak valid (HTTP ${r.status}): ${text.substring(0, 150)}`);
}
if (!r.ok || !res.success) {
throw new Error(res.error || res.message || `Gagal menyimpan (Error ${r.status})`);
}
const newId = res.data?.id || res.id || id;
// Save bantuan (fallback jika backend tidak handle bantuan_ids di penduduk.php)
if (newId) {
try {
const bantuans = [...selectedBantuanIds].map(bid => ({ bantuan_id: bid, diinput_oleh: currentUserId }));
await fetch(`${API}/bantuan.php`, {
method: 'POST', headers: {'Content-Type':'application/json'},
body: JSON.stringify({ penduduk_id: newId, bantuans })
});
} catch(bantuanErr) { console.warn('Bantuan save warning:', bantuanErr); }
}
// Upload foto utama jika dipilih di form penduduk
const pFotoInput = document.getElementById('p-foto-input');
if (pFotoInput && pFotoInput.files && pFotoInput.files[0]) {
const pFotoFile = pFotoInput.files[0];
const gpsEl = document.getElementById('p-foto-gps-info');
const fLat = gpsEl.dataset.lat || '';
const fLng = gpsEl.dataset.lng || '';
const formData = new FormData();
formData.append('foto', pFotoFile);
formData.append('penduduk_id', newId);
formData.append('keterangan', 'Foto Utama dari Form Penduduk');
formData.append('is_primary', '1');
formData.append('lat_foto', fLat);
formData.append('lng_foto', fLng);
formData.append('diambil_oleh', currentUserId || 1);
try {
const uploadRes = await fetch(`${API}/foto.php`, { method: 'POST', body: formData });
const uploadResJson = await uploadRes.json();
if (!uploadResJson.success) throw new Error(uploadResJson.error || 'Gagal mengupload foto utama');
} catch (photoErr) {
console.warn('Foto upload warning:', photoErr);
toast('⚠️ Data disimpan, tapi gagal upload foto: ' + photoErr.message, 'warn');
}
}
// Reset/clear foto input
if (pFotoInput) pFotoInput.value = '';
const pPreviewWrap = document.getElementById('p-foto-preview-wrap');
if (pPreviewWrap) pPreviewWrap.style.display = 'none';
toast(res.message||'Data berhasil disimpan! ✅', 'success');
closeModal('modalPenduduk');
await loadAll();
} catch(e) {
console.error('savePenduduk error:', e);
toast('❌ ' + e.message, 'error');
}
finally { btn.innerHTML = '💾 Simpan Data'; btn.disabled = false; }
}
async function editPenduduk(id) {
// Petugas hanya bisa edit data yang dia input (berdasarkan surveyor_id)
// Admin bisa edit semua
let p = pendudukData.find(x => x.id == id);
if (!p) {
try {
const r = await fetch(`${API}/penduduk.php?id=${id}`);
const d = await r.json();
p = d.data;
} catch(e) { return toast('Gagal memuat data', 'error'); }
}
if (!p) return;
// Cek akses petugas
if (currentRole === 'petugas' && p.surveyor_id && p.surveyor_id != currentUserId) {
return toast('Anda hanya dapat mengedit data yang Anda input', 'error');
}
document.getElementById('penduduk-id').value = p.id;
document.getElementById('penduduk-lat').value = p.lat;
document.getElementById('penduduk-lng').value = p.lng;
document.getElementById('p-nik').value = p.nik_kk||'';
document.getElementById('p-nama').value = p.nama_kk||'';
document.getElementById('p-hp').value = p.no_hp||'';
document.getElementById('p-alamat').value= p.alamat||'';
const pWilayahEl = document.getElementById('p-wilayah');
if (pWilayahEl) pWilayahEl.value = p.wilayah_id||'';
document.getElementById('p-rt').value = p.rt||'';
document.getElementById('p-rw').value = p.rw||'';
document.getElementById('p-jiwa').value = p.jumlah_anggota||1;
document.getElementById('p-anak').value = p.jumlah_anak_sekolah||0;
document.getElementById('p-penghasilan').value = p.penghasilan_bulanan||'';
document.getElementById('p-sumber').value = p.sumber_penghasilan||'';
document.getElementById('p-pekerjaan').value = p.status_pekerjaan||'tidak_bekerja';
document.getElementById('p-kondisi').value = p.kondisi_rumah||'tidak_layak';
document.getElementById('p-kepemilikan').value = p.kepemilikan_rumah||'milik_sendiri';
document.getElementById('p-bpjs').value = p.jenis_bpjs||'tidak_punya';
if (currentRole === 'admin') {
document.getElementById('p-verifikasi').value = p.status_verifikasi||'belum_diverifikasi';
document.getElementById('p-catatan').value = p.catatan_verifikasi||'';
}
document.getElementById('p-coords-badge').textContent = `📍 ${parseFloat(p.lat).toFixed(6)}, ${parseFloat(p.lng).toFixed(6)}`;
setStatus(document.getElementById('geocode-status-p'), 'hidden', '');
document.getElementById('modal-penduduk-title').textContent = 'Edit Data Penduduk';
if (p.penghasilan_bulanan) document.getElementById('p-penghasilan').dispatchEvent(new Event('input'));
// Load current photo preview in form if exists
const pPreviewImg = document.getElementById('p-foto-preview');
const pPreviewWrap = document.getElementById('p-foto-preview-wrap');
const pGpsInfo = document.getElementById('p-foto-gps-info');
document.getElementById('p-foto-input').value = '';
if (p.foto_utama) {
pPreviewImg.src = p.foto_utama;
pPreviewWrap.style.display = 'block';
pGpsInfo.textContent = 'Foto Utama saat ini (Unggah file baru untuk menggantinya)';
delete pGpsInfo.dataset.lat;
delete pGpsInfo.dataset.lng;
} else {
pPreviewImg.src = '';
pPreviewWrap.style.display = 'none';
pGpsInfo.textContent = '';
delete pGpsInfo.dataset.lat;
delete pGpsInfo.dataset.lng;
}
try {
const r = await fetch(`${API}/bantuan.php?penduduk_id=${id}`);
const d = await r.json();
const activeBantuanIds = (d.data||[]).filter(b => b.status==='aktif').map(b => b.bantuan_id);
renderBantuanSelector(activeBantuanIds);
} catch(e) { renderBantuanSelector([]); }
map.closePopup();
openModal('modalPenduduk');
}
async function deletePenduduk(id) {
const p = pendudukData.find(x => x.id == id);
if (!confirm(`Hapus data KK "${p?.nama_kk}"? Semua foto akan dihapus.`)) return;
try {
const r = await fetch(`${API}/penduduk.php?id=${id}`, { method:'DELETE' });
const text = await r.text();
let res;
try { res = JSON.parse(text); } catch(e) { throw new Error('Server error: ' + text.substring(0,100)); }
if (res.success) { toast('✅ Data berhasil dihapus', 'success'); await loadAll(); }
else throw new Error(res.error || 'Gagal menghapus');
} catch(e) {
console.error('deletePenduduk error:', e);
toast('❌ ' + e.message, 'error');
}
map.closePopup();
}
// ════════════════════════════════════
// FOTO MODAL
// ════════════════════════════════════
async function openFotoModal(pendudukId, namakk) {
document.getElementById('foto-penduduk-id').value = pendudukId;
document.getElementById('modal-foto-title').textContent = `Foto Rumah ${namakk}`;
document.getElementById('foto-preview-wrap').style.display = 'none';
document.getElementById('btn-upload-foto').style.display = 'none';
document.getElementById('foto-file-input').value = '';
selectedFotoFile = null;
try {
const r = await fetch(`${API}/foto.php?penduduk_id=${pendudukId}`);
const d = await r.json();
renderFotoGaleri(d.data || []);
} catch(e) { renderFotoGaleri([]); }
openModal('modalFoto');
}
function renderFotoGaleri(fotos) {
const c = document.getElementById('foto-galeri');
if (!fotos.length) {
c.innerHTML = `<div style="color:var(--muted);font-size:12px;padding:20px;text-align:center;grid-column:1/-1;">Belum ada foto tersimpan</div>`;
return;
}
c.innerHTML = fotos.map(f => `
<div class="foto-thumb-wrap">
<img src="${f.file_path}" alt="Foto rumah" onclick="window.open('${f.file_path}','_blank')">
${f.is_primary?'<span class="foto-primary-badge">🏠 Utama</span>':''}
<button class="foto-delete" onclick="deleteFoto(${f.id})">✕</button>
</div>`).join('');
}
function onFotoSelected(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) return toast('Ukuran foto maks 5 MB', 'error');
selectedFotoFile = file;
const reader = new FileReader();
reader.onload = e => {
document.getElementById('foto-preview').src = e.target.result;
document.getElementById('foto-preview-wrap').style.display = 'block';
document.getElementById('btn-upload-foto').style.display = 'inline-flex';
};
reader.readAsDataURL(file);
// GPS tag otomatis
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const gpsEl = document.getElementById('foto-gps-info');
gpsEl.textContent = `📍 GPS: ${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}${Math.round(pos.coords.accuracy)}m)`;
gpsEl.dataset.lat = pos.coords.latitude;
gpsEl.dataset.lng = pos.coords.longitude;
}, () => {});
}
}
function onDragOver(e) { e.preventDefault(); document.getElementById('upload-zone').classList.add('dragover'); }
function onDrop(e) {
e.preventDefault(); document.getElementById('upload-zone').classList.remove('dragover');
const file = e.dataTransfer.files[0];
if (file) { document.getElementById('foto-file-input').files = e.dataTransfer.files; onFotoSelected({target:{files:[file]}}); }
}
async function uploadFoto() {
if (!selectedFotoFile) return;
const pendudukId = document.getElementById('foto-penduduk-id').value;
if (!pendudukId) return toast('ID penduduk tidak ditemukan', 'error');
const keterangan = document.getElementById('foto-keterangan').value;
const isPrimary = document.getElementById('foto-is-primary').value;
const gpsEl = document.getElementById('foto-gps-info');
const lat = gpsEl.dataset.lat || '';
const lng = gpsEl.dataset.lng || '';
const formData = new FormData();
formData.append('foto', selectedFotoFile);
formData.append('penduduk_id', pendudukId);
formData.append('keterangan', keterangan);
formData.append('is_primary', isPrimary);
formData.append('lat_foto', lat);
formData.append('lng_foto', lng);
formData.append('diambil_oleh', currentUserId || 1);
const btn = document.getElementById('btn-upload-foto');
btn.innerHTML = '<span class="spinner"></span> Mengupload...'; btn.disabled = true;
try {
const r = await fetch(`${API}/foto.php`, { method:'POST', body: formData });
const text = await r.text();
let res;
try { res = JSON.parse(text); } catch(e) { throw new Error('Server error: ' + text.substring(0,120)); }
if (!res.success) throw new Error(res.error || 'Gagal upload');
toast('📷 Foto berhasil diupload!', 'success');
selectedFotoFile = null;
document.getElementById('foto-preview-wrap').style.display = 'none';
document.getElementById('foto-file-input').value = '';
document.getElementById('foto-keterangan').value = '';
document.getElementById('foto-is-primary').value = '0';
document.getElementById('btn-upload-foto').style.display = 'none';
// Refresh galeri
const r2 = await fetch(`${API}/foto.php?penduduk_id=${pendudukId}`);
const d2 = await r2.json();
renderFotoGaleri(d2.data || []);
if (isPrimary === '1') await loadPenduduk().then(() => renderList());
} catch(e) {
console.error('uploadFoto error:', e);
toast('❌ ' + e.message, 'error');
}
finally { btn.innerHTML = '⬆ Upload Foto'; btn.disabled = false; }
}
async function deleteFoto(id) {
if (!confirm('Hapus foto ini?')) return;
try {
const r = await fetch(`${API}/foto.php?id=${id}`, { method:'DELETE' });
const text = await r.text();
let res;
try { res = JSON.parse(text); } catch(e) { throw new Error('Server error: ' + text.substring(0,100)); }
if (!res.success) throw new Error(res.error || 'Gagal menghapus foto');
toast('✅ Foto dihapus', 'success');
const pendudukId = document.getElementById('foto-penduduk-id').value;
const r2 = await fetch(`${API}/foto.php?penduduk_id=${pendudukId}`);
const d2 = await r2.json();
renderFotoGaleri(d2.data || []);
} catch(e) {
console.error('deleteFoto error:', e);
toast('❌ ' + e.message, 'error');
}
}
// ════════════════════════════════════
// RADIUS CONTROL
// ════════════════════════════════════
function openRadiusControl(id) {
editingIbadah = id;
const ri = ibadahData.find(r => r.id == id);
if (!ri) return;
document.getElementById('radiusSlider').value = ri.radius;
document.getElementById('radiusDisplay').textContent = ri.radius + ' m';
document.getElementById('radiusControl').style.display = 'flex';
map.closePopup();
flyToIbadah(id);
}
function onRadiusSlide(val) {
document.getElementById('radiusDisplay').textContent = val + ' m';
if (editingIbadah && ibadahCircles[editingIbadah]) ibadahCircles[editingIbadah].setRadius(parseInt(val));
}
function closeRadiusControl() { document.getElementById('radiusControl').style.display = 'none'; editingIbadah = null; }
async function saveRadius() {
if (!editingIbadah) return;
const radius = parseInt(document.getElementById('radiusSlider').value);
try {
const r = await fetch(`${API}/update_radius.php`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({id:editingIbadah, radius}) });
const text = await r.text();
let res;
try { res = JSON.parse(text); } catch(e) { throw new Error('Server error: ' + text.substring(0,100)); }
if (res.success) { toast(res.message || '✅ Radius disimpan', 'success'); closeRadiusControl(); await loadAll(); }
else throw new Error(res.error || 'Gagal menyimpan radius');
} catch(e) {
console.error('saveRadius error:', e);
toast('❌ ' + e.message, 'error');
}
}
// ════════════════════════════════════
// EXPORT
// ════════════════════════════════════
function toggleExportMenu() { document.getElementById('export-menu').classList.toggle('open'); }
function openExportFilter() { document.getElementById('export-menu').classList.remove('open'); openModal('modalExport'); }
function doExport(type) {
const kel = document.getElementById('exp-kelurahan').value;
const kat = document.getElementById('exp-kategori').value;
const ver = document.getElementById('exp-verifikasi').value;
const params = new URLSearchParams();
if (kel) params.set('kelurahan', kel);
if (kat) params.set('kategori', kat);
if (ver) params.set('verifikasi', ver);
const url = type === 'excel' ? `api/export_excel.php` : `api/export_pdf.php`;
window.open(url + (params.toString() ? '?'+params.toString() : ''), '_blank');
closeModal('modalExport');
}
document.addEventListener('click', e => {
const wrap = document.getElementById('export-wrap');
if (wrap && !wrap.contains(e.target)) document.getElementById('export-menu')?.classList.remove('open');
});
// ════════════════════════════════════
// MODAL HELPERS
// ════════════════════════════════════
function openModal(id) { document.getElementById(id).classList.add('open'); }
function closeModal(id) { document.getElementById(id).classList.remove('open'); cancelMode(); }
document.querySelectorAll('.modal-overlay').forEach(overlay => {
overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(overlay.id); });
});
// ════════════════════════════════════
// LEGEND TOGGLE
// ════════════════════════════════════
function toggleLegend() {
const legend = document.getElementById('map-legend');
const btn = document.getElementById('legend-toggle-btn');
if (legend.style.display === 'none') {
legend.style.display = '';
btn.style.display = 'none';
} else {
legend.style.display = 'none';
btn.style.display = '';
}
}
// ════════════════════════════════════
// SIDEBAR TOGGLE
// ════════════════════════════════════
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const openBtn = document.getElementById('sidebar-open-btn');
sidebar.classList.toggle('collapsed');
if (openBtn) openBtn.style.display = sidebar.classList.contains('collapsed') ? 'flex' : 'none';
setTimeout(() => map?.invalidateSize(), 300);
}
// ════════════════════════════════════
// TOAST
// ════════════════════════════════════
let toastTimer;
function toast(msg, type='success') {
const el = document.getElementById('toast');
el.textContent = msg; el.className = `show ${type}`;
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.className = '', 3500);
}
// ════════════════════════════════════
// NEW HELPER FUNCTIONS (FOTO & REPOSITION)
// ════════════════════════════════════
function onPendudukFotoSelected(event) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 5 * 1024 * 1024) return toast('Ukuran foto maks 5 MB', 'error');
const reader = new FileReader();
reader.onload = e => {
document.getElementById('p-foto-preview').src = e.target.result;
document.getElementById('p-foto-preview-wrap').style.display = 'block';
};
reader.readAsDataURL(file);
// Baca GPS EXIF dari foto
const gpsEl = document.getElementById('p-foto-gps-info');
gpsEl.textContent = '📍 Membaca data lokasi dari foto...';
delete gpsEl.dataset.lat;
delete gpsEl.dataset.lng;
// Coba baca EXIF GPS menggunakan library exifr
if (typeof exifr !== 'undefined') {
exifr.gps(file).then(gps => {
if (gps && gps.latitude && gps.longitude) {
const lat = gps.latitude.toFixed(6);
const lng = gps.longitude.toFixed(6);
gpsEl.textContent = `📍 GPS dari EXIF: ${lat}, ${lng}`;
gpsEl.style.color = 'var(--green)';
gpsEl.dataset.lat = gps.latitude;
gpsEl.dataset.lng = gps.longitude;
} else {
// Fallback ke GPS perangkat jika EXIF tidak ada
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude.toFixed(5);
const lng = pos.coords.longitude.toFixed(5);
const acc = Math.round(pos.coords.accuracy);
gpsEl.textContent = `📍 GPS Perangkat: ${lat}, ${lng}${acc}m)`;
gpsEl.style.color = 'var(--warn)';
gpsEl.dataset.lat = pos.coords.latitude;
gpsEl.dataset.lng = pos.coords.longitude;
}, () => {
gpsEl.textContent = '⚠️ Tidak ada data GPS pada foto ini';
gpsEl.style.color = 'var(--muted)';
});
} else {
gpsEl.textContent = '⚠️ Tidak ada data GPS pada foto ini';
gpsEl.style.color = 'var(--muted)';
}
}
}).catch(() => {
// Fallback ke GPS perangkat jika EXIF gagal dibaca
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
gpsEl.textContent = `📍 GPS Perangkat: ${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}`;
gpsEl.style.color = 'var(--warn)';
gpsEl.dataset.lat = pos.coords.latitude;
gpsEl.dataset.lng = pos.coords.longitude;
}, () => {
gpsEl.textContent = '⚠️ Data GPS tidak tersedia';
gpsEl.style.color = 'var(--muted)';
});
} else {
gpsEl.textContent = '⚠️ Data GPS tidak tersedia';
gpsEl.style.color = 'var(--muted)';
}
});
} else {
// Jika exifr tidak tersedia, fallback GPS perangkat
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(pos => {
gpsEl.textContent = `📍 GPS Perangkat: ${pos.coords.latitude.toFixed(5)}, ${pos.coords.longitude.toFixed(5)}${Math.round(pos.coords.accuracy)}m)`;
gpsEl.style.color = 'var(--warn)';
gpsEl.dataset.lat = pos.coords.latitude;
gpsEl.dataset.lng = pos.coords.longitude;
}, () => { gpsEl.textContent = '⚠️ GPS tidak tersedia'; gpsEl.style.color = 'var(--muted)'; });
} else {
gpsEl.textContent = '⚠️ GPS tidak tersedia';
gpsEl.style.color = 'var(--muted)';
}
}
}
function repositionPendudukMap() {
document.getElementById('modalPenduduk').classList.remove('open');
addMode = 'reposition_penduduk';
document.getElementById('mode-text').textContent = '📍 Klik peta untuk menentukan lokasi baru Penduduk';
document.getElementById('mode-bar').classList.add('show');
map.getContainer().style.cursor = 'crosshair';
}
function repositionIbadahMap() {
document.getElementById('modalIbadah').classList.remove('open');
addMode = 'reposition_ibadah';
document.getElementById('mode-text').textContent = '📍 Klik peta untuk menentukan lokasi baru Rumah Ibadah';
document.getElementById('mode-bar').classList.add('show');
map.getContainer().style.cursor = 'crosshair';
}
</script>
</body>
</html>