2661 lines
126 KiB
HTML
2661 lines
126 KiB
HTML
<!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 & 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 & 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 & 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> | <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> |