Files
luthfihadinugroho79 e950468760 add files
2026-06-10 19:42:59 +07:00

2612 lines
120 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once '../includes/config.php';
requireLogin(['pengurus_ibadah','admin']);
$user = currentUser();
$ibadahId = (int)$user['ibadah'];
$ibadahInfo = $ibadahId ? $conn->query("SELECT nama,jenis FROM rumah_ibadah WHERE id=$ibadahId")->fetch_assoc() : null;
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Pengurus — WebGIS Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root {
--bg: #080c14;
--surf: #0d1424;
--card: #111827;
--card2: #161f30;
--border: rgba(148,163,184,0.08);
--border2: rgba(148,163,184,0.14);
--accent: #6366f1;
--accent2: #818cf8;
--teal: #14b8a6;
--green: #22c55e;
--red: #f43f5e;
--amber: #f59e0b;
--purple: #a855f7;
--pink: #ec4899;
--sky: #38bdf8;
--text: #e2e8f0;
--text2: #64748b;
--text3: #94a3b8;
--glow-a: rgba(99,102,241,0.2);
--glow-t: rgba(20,184,166,0.2);
--topbar-h: 60px;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
background: var(--bg);
color: var(--text);
font-family: 'Plus Jakarta Sans', sans-serif;
overflow: hidden;
}
/* ─── TOPBAR ─────────────────────────────── */
.topbar {
height: var(--topbar-h);
background: var(--surf);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 18px;
gap: 12px;
position: sticky;
top: 0;
z-index: 200;
box-shadow: 0 1px 30px rgba(0,0,0,0.4);
}
.brand {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.brand-logo {
width: 36px;
height: 36px;
background: linear-gradient(135deg, var(--accent), var(--teal));
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
box-shadow: 0 4px 12px var(--glow-a);
}
.brand-text { line-height: 1.2; }
.brand-name { font-size: 13px; font-weight: 800; color: var(--text); }
.brand-sub { font-size: 10px; color: var(--accent2); font-weight: 500; }
.nav-sep { width: 1px; height: 24px; background: var(--border2); flex-shrink: 0; }
.nav-tabs {
display: flex;
gap: 2px;
flex: 1;
overflow-x: auto;
scrollbar-width: none;
}
.nav-tabs::-webkit-scrollbar { display: none; }
.nav-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 14px;
border-radius: 9px;
border: none;
background: transparent;
color: var(--text2);
font-family: inherit;
font-size: 12px;
font-weight: 700;
cursor: pointer;
white-space: nowrap;
transition: all 0.2s;
}
.nav-tab:hover { background: rgba(255,255,255,0.04); color: var(--text3); }
.nav-tab.active { background: rgba(99,102,241,0.12); color: var(--accent2); }
.nav-tab .tab-icon { font-size: 14px; }
.nav-tab .tab-badge {
background: var(--red);
color: #fff;
font-size: 9px;
font-weight: 800;
padding: 1px 5px;
border-radius: 6px;
min-width: 16px;
text-align: center;
display: none;
}
.nav-tab .tab-badge.show { display: inline-block; }
.topbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; flex-shrink: 0; }
.notif-btn {
position: relative;
width: 36px;
height: 36px;
border-radius: 9px;
border: 1px solid var(--border2);
background: rgba(255,255,255,0.03);
color: var(--text3);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
transition: all 0.2s;
}
.notif-btn:hover { background: rgba(99,102,241,0.1); color: var(--accent2); border-color: var(--accent); }
.notif-dot {
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
background: var(--red);
border-radius: 50%;
border: 2px solid var(--surf);
display: none;
}
.notif-dot.show { display: block; }
.user-pill {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border2);
border-radius: 20px;
}
.user-av {
width: 24px;
height: 24px;
background: linear-gradient(135deg, var(--accent), var(--teal));
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.user-info { line-height: 1.3; }
.user-name { font-size: 11px; font-weight: 700; }
.user-role { font-size: 9px; color: var(--teal); font-weight: 600; }
.btn-logout {
padding: 7px 13px;
border-radius: 9px;
border: 1px solid rgba(244,63,94,0.25);
background: rgba(244,63,94,0.08);
color: var(--red);
font-family: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
}
.btn-logout:hover { background: rgba(244,63,94,0.18); }
/* ─── NOTIF PANEL ─────────────────────────── */
.notif-wrap { position: relative; }
.notif-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 320px;
background: var(--card);
border: 1px solid var(--border2);
border-radius: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,0.5);
overflow: hidden;
z-index: 400;
display: none;
animation: slideDown 0.2s cubic-bezier(0.23,1,0.32,1);
}
.notif-panel.open { display: block; }
@keyframes slideDown { from { opacity:0; transform: translateY(-8px); } }
.notif-hd {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
}
.notif-hd-title { font-size: 12px; font-weight: 800; }
.notif-clear { font-size: 10px; color: var(--accent2); background: none; border: none; cursor: pointer; font-family: inherit; font-weight: 600; }
.notif-item {
padding: 10px 16px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
}
.notif-item:hover { background: rgba(99,102,241,0.06); }
.notif-item.unread { background: rgba(99,102,241,0.04); }
.notif-item-title { font-size: 11px; font-weight: 700; margin-bottom: 2px; }
.notif-item-body { font-size: 10px; color: var(--text2); margin-bottom: 3px; }
.notif-item-time { font-size: 9px; color: var(--text2); }
/* ─── PAGES ─────────────────────────────── */
.page-wrap {
height: calc(100vh - var(--topbar-h));
display: none;
flex-direction: column;
}
.page-wrap.active { display: flex; }
/* ─── PAGE: PETA ─────────────────────────── */
.peta-layout { display: flex; flex: 1; overflow: hidden; }
.peta-sidebar {
width: 340px;
flex-shrink: 0;
background: var(--surf);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
}
.peta-map { flex: 1; position: relative; }
#peta-map { position: absolute; inset: 0; }
/* Sidebar Tabs */
.stabs {
display: flex;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.stab {
flex: 1;
padding: 11px 4px;
background: none;
border: none;
color: var(--text2);
font-family: inherit;
font-size: 10px;
font-weight: 800;
letter-spacing: 0.3px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.stab.active { color: var(--accent2); border-bottom-color: var(--accent); background: rgba(99,102,241,0.04); }
.spanel { display: none; flex-direction: column; flex: 1; overflow: hidden; }
.spanel.active { display: flex; }
.spanel-body { flex: 1; overflow-y: auto; }
/* Stats cards */
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 12px; }
.stat-card {
background: var(--card2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 12px;
}
.stat-val { font-size: 26px; font-weight: 800; line-height: 1; margin-bottom: 3px; font-family: 'JetBrains Mono', monospace; }
.stat-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text2); }
.sv-blue .stat-val { color: var(--sky); }
.sv-amber .stat-val { color: var(--amber); }
.sv-green .stat-val { color: var(--green); }
.sv-purple .stat-val { color: var(--purple); }
.sdiv {
padding: 8px 14px 5px;
font-size: 9px;
font-weight: 800;
color: var(--text2);
letter-spacing: 0.8px;
text-transform: uppercase;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.map-controls { padding: 10px 12px; display: flex; flex-direction: column; gap: 7px; }
/* Item list */
.item-list { display: flex; flex-direction: column; gap: 5px; padding: 8px 10px; }
.icard {
background: var(--card2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 12px;
cursor: pointer;
transition: all 0.2s;
}
.icard:hover { border-color: var(--accent); background: rgba(99,102,241,0.04); }
.icard-hd { display: flex; justify-content: space-between; align-items: center; margin-bottom: 5px; }
.icard-name { font-size: 12px; font-weight: 700; }
.icard-meta { font-size: 10px; color: var(--text2); display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
/* Radius slider in icard */
.icard-radius { margin-top: 8px; padding-top: 7px; border-top: 1px solid rgba(99,102,241,0.12); }
.icard-radius-label { font-size: 9px; color: var(--text2); font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; }
.icard-radius-val { color: #a5b4fc; font-size: 10px; font-weight: 800; }
.radius-slider { width: 100%; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 2px; background: linear-gradient(to right, #6366f1 0%, #6366f1 var(--slider-pct, 15%), rgba(255,255,255,0.1) var(--slider-pct, 15%), rgba(255,255,255,0.1) 100%); outline: none; cursor: pointer; }
.radius-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #6366f1; box-shadow: 0 2px 8px rgba(99,102,241,0.5); cursor: pointer; }
.radius-slider::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: #6366f1; border: none; cursor: pointer; }
/* ─── FULL PAGES ─────────────────────────── */
.full-page { flex: 1; overflow-y: auto; padding: 20px 24px; }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
gap: 16px;
}
.page-title { font-size: 20px; font-weight: 800; }
.page-desc { font-size: 12px; color: var(--text2); margin-top: 3px; }
.page-actions { display: flex; gap: 8px; flex-shrink: 0; }
/* ─── FILTER BAR ─────────────────────────── */
.filter-bar {
display: flex;
gap: 8px;
margin-bottom: 14px;
flex-wrap: wrap;
}
/* ─── TABLES ─────────────────────────────── */
.table-wrap {
background: var(--surf);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
}
table { width: 100%; border-collapse: collapse; }
thead tr { background: rgba(255,255,255,0.02); border-bottom: 1px solid var(--border2); }
th {
padding: 11px 14px;
text-align: left;
font-size: 10px;
font-weight: 800;
color: var(--text2);
letter-spacing: 0.6px;
text-transform: uppercase;
}
td { padding: 11px 14px; font-size: 12px; border-bottom: 1px solid var(--border); }
tbody tr:last-child td { border-bottom: none; }
tbody tr { transition: background 0.15s; }
tbody tr:hover { background: rgba(99,102,241,0.03); }
/* ─── CARD GRID ─────────────────────────── */
.card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 12px; }
.rcard {
background: var(--card2);
border: 1px solid var(--border);
border-radius: 14px;
padding: 16px;
transition: all 0.2s;
}
.rcard:hover { border-color: var(--border2); }
.rcard.pending { border-color: rgba(245,158,11,0.3); }
.rcard.disetujui { border-color: rgba(34,197,94,0.2); }
.rcard.ditolak { border-color: rgba(244,63,94,0.2); }
.rcard-hd { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }
.rcard-title { font-size: 13px; font-weight: 800; }
.rcard-meta { font-size: 11px; color: var(--text2); line-height: 1.8; margin-bottom: 10px; }
.rcard-body {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 9px;
padding: 10px;
font-size: 11px;
color: var(--text2);
line-height: 1.6;
margin-bottom: 10px;
}
/* ─── BANTUAN CARDS ─────────────────────── */
.bcard {
background: rgba(20,184,166,0.04);
border: 1px solid rgba(20,184,166,0.12);
border-radius: 12px;
padding: 14px;
}
.bcard-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px; }
.bcard-jenis { font-size: 13px; font-weight: 800; color: var(--teal); }
.bcard-tgl { font-size: 10px; color: var(--text2); }
.bcard-kk { font-size: 12px; font-weight: 700; margin-bottom: 5px; }
.bcard-detail { font-size: 11px; color: var(--text2); line-height: 1.7; }
.bcard-footer {
margin-top: 8px;
padding: 5px 10px;
background: rgba(99,102,241,0.06);
border: 1px solid var(--border);
border-radius: 8px;
font-size: 10px;
color: var(--text2);
}
/* ─── PESAN PAGE ─────────────────────────── */
.pesan-layout { display: grid; grid-template-columns: 300px 1fr; gap: 14px; flex: 1; overflow: hidden; padding: 20px 24px; }
.pesan-inbox {
background: var(--surf);
border: 1px solid var(--border);
border-radius: 14px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pesan-detail {
background: var(--surf);
border: 1px solid var(--border);
border-radius: 14px;
overflow-y: auto;
padding: 20px;
}
.pesan-inbox-hd {
padding: 13px 14px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.inbox-list { flex: 1; overflow-y: auto; }
.msg-item {
padding: 11px 14px;
border-bottom: 1px solid var(--border);
cursor: pointer;
transition: background 0.15s;
}
.msg-item:hover { background: rgba(99,102,241,0.06); }
.msg-item.unread { background: rgba(99,102,241,0.04); }
.msg-from { font-size: 11px; font-weight: 700; }
.msg-subject { font-size: 11px; color: var(--text); margin: 2px 0; }
.msg-time { font-size: 9px; color: var(--text2); }
/* ─── STAT ROW ───────────────────────────── */
.stat-row { display: grid; grid-template-columns: repeat(4,1fr); gap: 10px; margin-bottom: 20px; }
.mini-card {
background: var(--card2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 14px 16px;
}
.mini-val { font-size: 24px; font-weight: 800; margin-bottom: 3px; font-family: 'JetBrains Mono', monospace; }
.mini-lbl { font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.6px; color: var(--text2); }
.mc-green .mini-val { color: var(--green); }
.mc-sky .mini-val { color: var(--sky); }
.mc-amber .mini-val { color: var(--amber); }
.mc-purple .mini-val { color: var(--purple); }
/* ─── FORM ───────────────────────────────── */
.fl { margin-bottom: 12px; }
.fl label { display: block; font-size: 10px; font-weight: 800; color: var(--text2); letter-spacing: 0.4px; margin-bottom: 5px; text-transform: uppercase; }
.finput {
width: 100%;
background: rgba(255,255,255,0.03);
border: 1px solid var(--border2);
color: var(--text);
padding: 9px 11px;
border-radius: 9px;
font-family: inherit;
font-size: 12px;
outline: none;
transition: all 0.2s;
}
.finput:focus { border-color: var(--accent); background: rgba(99,102,241,0.05); box-shadow: 0 0 0 3px var(--glow-a); }
select.finput option { background: var(--card); }
textarea.finput { resize: vertical; min-height: 72px; }
.finput[type=file] { cursor: pointer; }
/* ─── BUTTONS ────────────────────────────── */
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 8px 14px;
border-radius: 9px;
border: none;
font-family: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary { background: linear-gradient(135deg, var(--accent), #818cf8); color: #fff; box-shadow: 0 4px 14px var(--glow-a); }
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 8px 20px var(--glow-a); }
.btn-teal { background: linear-gradient(135deg, var(--teal), #0d9488); color: #fff; box-shadow: 0 4px 14px var(--glow-t); }
.btn-teal:hover { transform: translateY(-1px); }
.btn-success { background: rgba(34,197,94,0.1); border: 1px solid rgba(34,197,94,0.25); color: var(--green); }
.btn-success:hover { background: rgba(34,197,94,0.2); }
.btn-danger { background: rgba(244,63,94,0.1); border: 1px solid rgba(244,63,94,0.25); color: var(--red); }
.btn-danger:hover { background: rgba(244,63,94,0.2); }
.btn-ghost { background: rgba(255,255,255,0.04); border: 1px solid var(--border2); color: var(--text2); }
.btn-ghost:hover { background: rgba(255,255,255,0.07); color: var(--text3); }
.btn-full { width: 100%; justify-content: center; }
.select-input {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border2);
color: var(--text);
padding: 8px 12px;
border-radius: 9px;
font-family: inherit;
font-size: 11px;
outline: none;
cursor: pointer;
}
.select-input option { background: var(--card); }
.search-input {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border2);
color: var(--text);
padding: 8px 12px;
border-radius: 9px;
font-family: inherit;
font-size: 12px;
outline: none;
flex: 1;
min-width: 200px;
}
.search-input:focus { border-color: var(--accent); }
.search-input::placeholder { color: var(--text2); }
/* ─── BADGES ─────────────────────────────── */
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 6px;
font-size: 9px;
font-weight: 800;
letter-spacing: 0.3px;
}
.badge-blue { background: rgba(56,189,248,0.12); color: #7dd3fc; }
.badge-green { background: rgba(34,197,94,0.12); color: #86efac; }
.badge-amber { background: rgba(245,158,11,0.12); color: #fcd34d; }
.badge-red { background: rgba(244,63,94,0.12); color: #fda4af; }
.badge-purple { background: rgba(168,85,247,0.12); color: #d8b4fe; }
.badge-teal { background: rgba(20,184,166,0.12); color: #5eead4; }
.badge-gray { background: rgba(255,255,255,0.06); color: var(--text2); }
/* ─── MODAL ──────────────────────────────── */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.7);
z-index: 500;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
animation: fadeIn 0.2s;
}
.modal-overlay.hidden { display: none; }
@keyframes fadeIn { from { opacity: 0; } }
.modal {
background: var(--card);
border: 1px solid var(--border2);
border-radius: 18px;
width: 90%;
max-width: 560px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
animation: popIn 0.3s cubic-bezier(0.23,1,0.32,1);
}
.modal.wide { max-width: 720px; }
@keyframes popIn { from { opacity: 0; transform: scale(0.95); } }
.modal-hd {
padding: 18px 22px;
border-bottom: 1px solid var(--border);
display: flex;
justify-content: space-between;
align-items: center;
position: sticky;
top: 0;
background: var(--card);
z-index: 2;
}
.modal-title { font-size: 15px; font-weight: 800; }
.modal-close {
width: 28px;
height: 28px;
background: rgba(255,255,255,0.04);
border: none;
border-radius: 7px;
color: var(--text2);
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.modal-close:hover { background: rgba(244,63,94,0.2); color: var(--red); }
.modal-body { padding: 20px 22px; }
.modal-ft { padding: 14px 22px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }
/* Form grid */
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.span2 { grid-column: span 2; }
/* ─── ANGGOTA ────────────────────────────── */
.anggota-card {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
}
.anggota-card-hd { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.anggota-card-title { font-size: 11px; font-weight: 800; color: var(--text3); }
/* ─── DETAIL KK ──────────────────────────── */
.member-item {
background: rgba(255,255,255,0.02);
border: 1px solid var(--border);
border-radius: 10px;
padding: 11px;
margin-bottom: 6px;
display: flex;
gap: 10px;
align-items: flex-start;
}
.member-av {
width: 36px;
height: 36px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 17px;
flex-shrink: 0;
}
.member-av.male { background: rgba(56,189,248,0.12); }
.member-av.female { background: rgba(236,72,153,0.12); }
.member-info { flex: 1; min-width: 0; }
.member-name { font-size: 12px; font-weight: 700; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.member-meta { font-size: 10px; color: var(--text2); margin-top: 2px; line-height: 1.7; }
.member-actions { display: flex; gap: 5px; margin-top: 7px; flex-wrap: wrap; }
.mbtn { padding: 4px 9px; border-radius: 7px; border: none; font-family: inherit; font-size: 9px; font-weight: 800; cursor: pointer; transition: all 0.15s; letter-spacing: 0.2px; }
.mbtn-edit { background: rgba(99,102,241,0.15); color: #a5b4fc; border: 1px solid rgba(99,102,241,0.25); }
.mbtn-edit:hover { background: rgba(99,102,241,0.3); }
.mbtn-del { background: rgba(239,68,68,0.1); color: #fca5a5; border: 1px solid rgba(239,68,68,0.2); }
.mbtn-del:hover { background: rgba(239,68,68,0.25); }
.mbtn-status { background: rgba(245,158,11,0.1); color: #fcd34d; border: 1px solid rgba(245,158,11,0.2); }
.mbtn-status:hover { background: rgba(245,158,11,0.25); }
/* Status hidup badge */
.sh-hidup { background: rgba(16,185,129,0.15); color: #6ee7b7; border: 1px solid rgba(16,185,129,0.25); padding: 1px 7px; border-radius: 5px; font-size: 9px; font-weight: 800; }
.sh-meninggal{ background: rgba(239,68,68,0.12); color: #fca5a5; border: 1px solid rgba(239,68,68,0.2); padding: 1px 7px; border-radius: 5px; font-size: 9px; font-weight: 800; }
.sh-pindah { background: rgba(245,158,11,0.12); color: #fcd34d; border: 1px solid rgba(245,158,11,0.2); padding: 1px 7px; border-radius: 5px; font-size: 9px; font-weight: 800; }
/* Pendidikan rows in modal */
.pend-row { display: grid; grid-template-columns: 1fr 1fr 80px 80px 1fr 28px; gap: 5px; align-items: center; margin-bottom: 6px; }
.pend-row input, .pend-row select { background: rgba(255,255,255,0.04); border: 1px solid var(--border); color: var(--text); padding: 6px 8px; border-radius: 8px; font-family: inherit; font-size: 11px; outline: none; }
.pend-row input:focus, .pend-row select:focus { border-color: var(--accent); }
.pend-row .del-pend { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); color: #fca5a5; border-radius: 7px; width: 26px; height: 26px; cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center; }
.pend-header { display: grid; grid-template-columns: 1fr 1fr 80px 80px 1fr 28px; gap: 5px; font-size: 9px; font-weight: 800; color: var(--text2); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 4px; }
/* ─── EMPTY ──────────────────────────────── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 36px 16px;
color: var(--text2);
text-align: center;
}
.empty-icon { font-size: 32px; margin-bottom: 8px; }
.empty-text { font-size: 12px; }
/* ─── TOAST ──────────────────────────────── */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
padding: 11px 16px;
border-radius: 11px;
font-size: 12px;
font-weight: 700;
max-width: 280px;
display: none;
animation: toastIn 0.3s cubic-bezier(0.23,1,0.32,1);
font-family: inherit;
}
@keyframes toastIn { from { opacity:0; transform: translateY(10px); } }
.toast-success { background: #052e16; border: 1px solid var(--green); color: #86efac; }
.toast-error { background: #4c0519; border: 1px solid var(--red); color: #fda4af; }
.toast-info { background: #1e1b4b; border: 1px solid var(--accent2); color: #c7d2fe; }
/* ─── SECTION ────────────────────────────── */
.section-hd {
font-size: 10px;
font-weight: 800;
color: var(--text2);
letter-spacing: 0.6px;
text-transform: uppercase;
padding: 10px 0 7px;
border-top: 1px solid var(--border);
margin-top: 10px;
}
.section-hd:first-child { border-top: none; margin-top: 0; padding-top: 0; }
/* ─── SCOPE TOGGLE ───────────────────────── */
.scope-btn {
padding: 7px 14px;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text2);
font-family: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.scope-btn.active {
background: linear-gradient(135deg, var(--accent), #818cf8);
color: #fff;
box-shadow: 0 2px 10px var(--glow-a);
}
.scope-btn:not(.active):hover { color: var(--text3); background: rgba(255,255,255,0.04); }
/* Row: bukan binaan */
tbody tr.non-binaan td { opacity: 0.72; }
tbody tr.non-binaan:hover td { opacity: 1; }
tbody tr.non-binaan td:first-child::before {
content: '';
display: inline-block;
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--text2);
margin-right: 6px;
vertical-align: middle;
}
/* ─── SCROLL ─────────────────────────────── */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.18); }
/* ─── OVERVIEW ────────────────────────────── */
.overview { padding: 20px; }
.kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 14px; margin-bottom: 20px; }
.kpi {
background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px;
text-align: center; border-top: 3px solid var(--kpi-color, var(--accent));
transition: all 0.3s;
}
.kpi:hover { transform: translateY(-2px); border-color: var(--border2); }
.kpi-icon { font-size: 24px; margin-bottom: 8px; }
.kpi-num { font-size: 22px; font-weight: 700; color: var(--accent2); margin-bottom: 6px; }
.kpi-label { font-size: 11px; color: var(--text2); text-transform: uppercase; letter-spacing: 0.5px; }
.scope-toggle { display: flex; gap: 8px; margin-bottom: 20px; padding: 0 20px; }
.scope-btn {
flex: 1; padding: 10px 14px; border: 1px solid var(--border); background: var(--card);
color: var(--text3); border-radius: 8px; cursor: pointer; font-size: 11px; font-weight: 600;
transition: all 0.3s;
}
.scope-btn.active { background: var(--accent); color: var(--text); border-color: var(--accent); }
.scope-btn:hover:not(.active) { border-color: var(--border2); color: var(--text2); }
.charts-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
.chart-card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 16px; }
.cc-title { font-size: 12px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
.cc-sub { font-size: 10px; color: var(--text2); margin-bottom: 12px; }
.chart-wrap { position: relative; height: 200px; }
.kec-table { width: 100%; font-size: 10px; border-collapse: collapse; margin-top: 12px; }
.kec-table th { background: rgba(99,102,241,0.1); color: var(--accent2); padding: 8px; text-align: left; font-weight: 600; }
.kec-table td { padding: 8px; border-top: 1px solid var(--border); }
.kec-table tr:hover { background: rgba(99,102,241,0.05); }
.stream-item {
display: flex; gap: 10px; padding: 12px; border-bottom: 1px solid var(--border);
align-items: flex-start;
}
.stream-item:last-child { border-bottom: none; }
.stream-icon { font-size: 14px; flex-shrink: 0; margin-top: 2px; }
.stream-content { flex: 1; min-width: 0; }
.stream-header { font-size: 11px; font-weight: 600; color: var(--accent2); margin-bottom: 2px; }
.stream-desc { font-size: 10px; color: var(--text2); line-height: 1.4; }
.stream-time { font-size: 9px; color: var(--text3); margin-top: 4px; }
/* ─── RESPONSIVE ─────────────────────────── */
@media (max-width: 900px) {
.nav-tabs .nav-tab span:not(.tab-icon):not(.tab-badge) { display: none; }
.stat-row { grid-template-columns: 1fr 1fr; }
.pesan-layout { grid-template-columns: 1fr; }
.peta-sidebar { width: 280px; }
}
@media (max-width: 640px) {
.user-info { display: none; }
.peta-sidebar { width: 100%; position: absolute; bottom: 0; z-index: 50; height: 45%; border-right: none; border-top: 1px solid var(--border); }
.peta-map { height: 100%; }
}
/* Leaflet popup tweak */
.leaflet-popup-content-wrapper {
background: var(--card);
color: var(--text);
border: 1px solid var(--border2);
border-radius: 12px;
box-shadow: 0 12px 40px rgba(0,0,0,0.5);
font-family: 'Plus Jakarta Sans', sans-serif;
}
.leaflet-popup-tip { background: var(--card); }
.leaflet-popup-content { margin: 10px 14px; font-size: 12px; }
/* ─── CHAT WARGA ─────────────────────────────── */
.chat-layout { display: grid; grid-template-columns: 280px 1fr; gap: 14px; flex: 1; overflow: hidden; padding: 20px 24px; }
.chat-list-panel { background: var(--card); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; display: flex; flex-direction: column; }
.chat-list-hd { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 800; flex-shrink: 0; }
.chat-list-body { overflow-y: auto; flex: 1; }
.chat-conv-item { padding: 12px 14px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.15s; }
.chat-conv-item:hover { background: rgba(99,102,241,0.06); }
.chat-conv-item.active { background: rgba(99,102,241,0.1); border-left: 3px solid var(--accent); }
.chat-conv-item.unread { background: rgba(99,102,241,0.04); }
.chat-conv-name { font-size: 12px; font-weight: 700; margin-bottom: 3px; display: flex; justify-content: space-between; align-items: center; }
.chat-conv-last { font-size: 10px; color: var(--text2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.chat-conv-badge { background: var(--accent); color: #fff; font-size: 9px; font-weight: 800; padding: 1px 6px; border-radius: 8px; min-width: 16px; text-align: center; }
.chat-msg-panel { background: var(--card); border: 1px solid var(--border); border-radius: 14px; display: flex; flex-direction: column; overflow: hidden; }
.chat-msg-hd { padding: 14px 16px; border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 800; flex-shrink: 0; }
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 10px; }
.chat-bubble-wrap { display: flex; flex-direction: column; }
.chat-bubble-wrap.me { align-items: flex-end; }
.chat-bubble-wrap.them { align-items: flex-start; }
.chat-bubble { max-width: 70%; padding: 10px 14px; border-radius: 14px; font-size: 12px; line-height: 1.6; word-break: break-word; }
.chat-bubble.me { background: linear-gradient(135deg, var(--accent), #818cf8); color: #fff; border-bottom-right-radius: 4px; }
.chat-bubble.them { background: var(--card2); border: 1px solid var(--border); color: var(--text); border-bottom-left-radius: 4px; }
.chat-bubble-meta { font-size: 9px; color: var(--text2); margin-top: 3px; }
.chat-input-row { padding: 12px 16px; border-top: 1px solid var(--border); display: flex; gap: 8px; flex-shrink: 0; }
.chat-input { flex: 1; background: rgba(255,255,255,0.05); border: 1px solid var(--border2); color: var(--text); padding: 10px 14px; border-radius: 10px; font-family: inherit; font-size: 12px; outline: none; resize: none; }
.chat-input:focus { border-color: var(--accent); }
.chat-send-btn { padding: 10px 16px; background: var(--accent); color: #fff; border: none; border-radius: 10px; cursor: pointer; font-weight: 700; font-size: 12px; font-family: inherit; transition: all 0.2s; flex-shrink: 0; }
.chat-send-btn:hover { background: var(--accent2); }
.chat-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: var(--text2); gap: 10px; }
</style>
<body>
<!-- ═══════════════ TOPBAR ═══════════════ -->
<div class="topbar">
<div class="brand">
<div class="brand-logo">🕌</div>
<div class="brand-text">
<div class="brand-name">Pengurus Ibadah</div>
<div class="brand-sub"><?= htmlspecialchars($ibadahInfo['nama'] ?? 'Semua Wilayah') ?></div>
</div>
</div>
<div class="nav-sep"></div>
<div class="nav-tabs">
<button class="nav-tab" onclick="goPage('overview',this)">
<span class="tab-icon">📊</span><span>Overview</span>
</button>
<button class="nav-tab active" onclick="goPage('peta',this)">
<span class="tab-icon">🗺️</span><span>Peta</span>
</button>
<button class="nav-tab" onclick="goPage('laporan',this)">
<span class="tab-icon">📢</span><span>Laporan</span>
<span class="tab-badge" id="badge-laporan"></span>
</button>
<button class="nav-tab" onclick="goPage('bantuan',this)">
<span class="tab-icon">🤝</span><span>Bantuan</span>
</button>
<button class="nav-tab" onclick="goPage('pesan',this)">
<span class="tab-icon">💬</span><span>Pesan</span>
<span class="tab-badge" id="badge-pesan"></span>
</button>
<button class="nav-tab" onclick="goPage('chat',this)">
<span class="tab-icon">🗨️</span><span>Chat Warga</span>
<span class="tab-badge" id="badge-chat"></span>
</button>
<button class="nav-tab" onclick="goPage('penduduk',this)">
<span class="tab-icon">👥</span><span>Penduduk</span>
</button>
</div>
<div class="topbar-right">
<div class="notif-wrap">
<button class="notif-btn" id="notif-btn" onclick="toggleNotif()">
🔔
<span class="notif-dot" id="notif-dot"></span>
</button>
<div class="notif-panel" id="notif-panel">
<div class="notif-hd">
<span class="notif-hd-title">Notifikasi</span>
<button class="notif-clear" onclick="markAllRead()">Tandai semua dibaca</button>
</div>
<div id="notif-list"><div class="empty-state" style="padding:20px"><div class="empty-icon">🔔</div><div class="empty-text">Belum ada notifikasi</div></div></div>
</div>
</div>
<div class="user-pill">
<div class="user-av">🕌</div>
<div class="user-info">
<div class="user-name"><?= htmlspecialchars($user['nama']) ?></div>
<div class="user-role"><?= htmlspecialchars($ibadahInfo['jenis'] ?? 'Pengurus') ?></div>
</div>
</div>
<button class="btn-logout" onclick="location.href='../api/auth.php?action=logout'">Keluar</button>
</div>
</div>
<!-- ═══════════════ PAGE: OVERVIEW ═══════════════ -->
<div class="page-wrap active" id="page-overview">
<div class="overview">
<div class="scope-toggle">
<button class="scope-btn active" onclick="setOverviewScope('binaan',this)">🕌 Wilayah Binaan</button>
<button class="scope-btn" onclick="setOverviewScope('semua',this)">🌍 Semua Penduduk Miskin</button>
</div>
<div class="kpi-row">
<div class="kpi" style="--kpi-color:var(--accent)">
<div class="kpi-icon">🏘️</div>
<div class="kpi-num" id="pg-kk">—</div>
<div class="kpi-label">Total KK</div>
</div>
<div class="kpi" style="--kpi-color:var(--accent2)">
<div class="kpi-icon">👥</div>
<div class="kpi-num" id="pg-jiwa">—</div>
<div class="kpi-label">Total Jiwa</div>
</div>
<div class="kpi" style="--kpi-color:var(--red)">
<div class="kpi-icon">⚠️</div>
<div class="kpi-num" id="pg-sm">—</div>
<div class="kpi-label">Sangat Miskin</div>
</div>
<div class="kpi" style="--kpi-color:var(--amber)">
<div class="kpi-icon">🟠</div>
<div class="kpi-num" id="pg-miskin">—</div>
<div class="kpi-label">Miskin</div>
</div>
<div class="kpi" style="--kpi-color:var(--green)">
<div class="kpi-icon">📢</div>
<div class="kpi-num" id="pg-laporan">—</div>
<div class="kpi-label">Laporan Pending</div>
</div>
<div class="kpi" style="--kpi-color:var(--purple)">
<div class="kpi-icon">🤝</div>
<div class="kpi-num" id="pg-bantuan">—</div>
<div class="kpi-label">Bantuan Dicatat</div>
</div>
</div>
<div class="charts-row">
<div class="chart-card">
<div class="cc-title">📊 Distribusi Status Ekonomi</div>
<div class="cc-sub">KK berdasarkan tingkat kemiskinan</div>
<div class="chart-wrap"><canvas id="pg-chart-donut" height="180"></canvas></div>
</div>
<div class="chart-card">
<div class="cc-title">📢 Status Laporan</div>
<div class="cc-sub">Jumlah laporan per status</div>
<div class="chart-wrap"><canvas id="pg-chart-laporan" height="180"></canvas></div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: PETA ═══════════════ -->
<div class="page-wrap" id="page-peta">
<div class="peta-layout">
<div class="peta-sidebar">
<div class="stabs">
<button class="stab active" onclick="switchStab('stat',this)">📊 Statistik</button>
<button class="stab" onclick="switchStab('ibadah',this)">🕌 Ibadah</button>
<button class="stab" onclick="switchStab('warga',this)">👥 Warga</button>
</div>
<!-- Panel: Stat -->
<div class="spanel active" id="sp-stat">
<div class="spanel-body">
<div class="sdiv">🗺️ Kontrol Peta</div>
<div class="map-controls">
<select class="select-input" id="f-peta-ekonomi" onchange="loadPendudukPeta()">
<option value="all">Semua Status Ekonomi</option>
<option value="sangat_miskin">⚠️ Sangat Miskin</option>
<option value="miskin">🟠 Miskin</option>
<option value="rentan">🟡 Rentan</option>
</select>
<div style="display:flex;gap:6px">
<button class="btn btn-ghost" style="flex:1;font-size:10px" onclick="toggleLayer('ibadah')">🕌 Ibadah</button>
<button class="btn btn-ghost" style="flex:1;font-size:10px" onclick="toggleLayer('radius')">⭕ Radius</button>
<button class="btn btn-ghost" style="flex:1;font-size:10px" onclick="petaHome()">🏠 Pusat</button>
</div>
</div>
<div class="sdiv">📍 Legenda</div>
<div style="padding:8px 14px;font-size:10px;color:var(--text2);line-height:2.2">
<div>⚠️ Sangat Miskin &nbsp;🟠 Miskin &nbsp;🟡 Rentan</div>
<div>🕌 Masjid &nbsp;⛪ Gereja &nbsp;🏯 Vihara &nbsp;🛕 Pura</div>
<div style="margin-top:5px;padding:7px 9px;background:rgba(99,102,241,0.06);border:1px solid rgba(99,102,241,0.15);border-radius:8px">
<div>🔵 <b style="color:var(--sky)">Besar</b> = Warga binaan Anda</div>
<div style="margin-top:2px">⚫ <b>Redup</b> = Luar wilayah</div>
</div>
</div>
</div>
</div>
<!-- Panel: Ibadah (hanya tampil milik sendiri, tanpa tombol tambah) -->
<div class="spanel" id="sp-ibadah">
<div class="sdiv">🕌 Rumah Ibadah Saya</div>
<div class="spanel-body">
<div class="item-list" id="list-ibadah"><div class="empty-state"><div class="empty-icon">🕌</div><div class="empty-text">Memuat...</div></div></div>
</div>
</div>
<!-- Panel: Warga -->
<div class="spanel" id="sp-warga">
<div class="sdiv" style="display:flex;justify-content:space-between;align-items:center;padding-right:10px">
<span>👥 Warga Binaan</span>
<button class="btn btn-primary" style="font-size:9px;padding:4px 9px" onclick="openM('m-kk')"> Tambah KK</button>
</div>
<div class="spanel-body">
<div class="item-list" id="list-warga"><div class="empty-state"><div class="empty-icon">👥</div><div class="empty-text">Memuat...</div></div></div>
</div>
</div>
</div>
<div class="peta-map">
<div id="peta-map"></div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: LAPORAN ═══════════════ -->
<div class="page-wrap" id="page-laporan">
<div class="full-page">
<div class="page-header">
<div>
<div class="page-title">📢 Laporan Masyarakat</div>
<div class="page-desc">Verifikasi laporan kelahiran, kematian, pindah, dan lainnya dari warga binaan</div>
</div>
</div>
<div class="filter-bar">
<select class="select-input" id="f-laporan-status" onchange="loadLaporan()">
<option value="">Semua Status</option>
<option value="pending">⏳ Pending</option>
<option value="diproses">🔄 Diproses</option>
<option value="disetujui">✅ Disetujui</option>
<option value="ditolak">❌ Ditolak</option>
</select>
<select class="select-input" id="f-laporan-jenis" onchange="loadLaporan()">
<option value="">Semua Jenis</option>
<option value="kelahiran">🍼 Kelahiran</option>
<option value="kematian">⚰️ Kematian</option>
<option value="pindah_masuk">📦 Pindah Masuk</option>
<option value="pindah_keluar">🚚 Pindah Keluar</option>
<option value="lainnya">📌 Lainnya</option>
</select>
</div>
<div class="card-grid" id="grid-laporan">
<div class="empty-state" style="grid-column:span 3"><div class="empty-icon">⏳</div><div class="empty-text">Memuat laporan...</div></div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: BANTUAN ═══════════════ -->
<div class="page-wrap" id="page-bantuan">
<div class="full-page">
<div class="page-header">
<div>
<div class="page-title">🤝 Manajemen Bantuan</div>
<div class="page-desc">Catat dan pantau penyaluran bantuan kepada warga binaan</div>
</div>
<div class="page-actions">
<button class="btn btn-primary" onclick="openM('m-bantuan')"> Catat Bantuan</button>
</div>
</div>
<div class="stat-row">
<div class="mini-card mc-green"><div class="mini-val" id="bt-total">—</div><div class="mini-lbl">Total Bantuan</div></div>
<div class="mini-card mc-sky"><div class="mini-val" id="bt-nominal">—</div><div class="mini-lbl">Total Nominal (Rp)</div></div>
<div class="mini-card mc-amber"><div class="mini-val" id="bt-ibadah">—</div><div class="mini-lbl">Dari Rumah Ibadah</div></div>
<div class="mini-card mc-purple"><div class="mini-val" id="bt-pemerin">—</div><div class="mini-lbl">Dari Pemerintah</div></div>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px">
<div style="font-size:13px;font-weight:800">📋 Riwayat Bantuan</div>
<select class="select-input" id="f-bantuan-ibadah" onchange="loadBantuan()">
<option value="">Semua Rumah Ibadah</option>
</select>
</div>
<div class="card-grid" id="grid-bantuan">
<div class="empty-state" style="grid-column:span 3"><div class="empty-icon">🤝</div><div class="empty-text">Belum ada bantuan tercatat</div></div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: PESAN ═══════════════ -->
<div class="page-wrap" id="page-pesan">
<div class="pesan-layout">
<div class="pesan-inbox">
<div class="pesan-inbox-hd">
<div style="font-size:13px;font-weight:800">💬 Kotak Pesan</div>
<button class="btn btn-primary" style="font-size:10px" onclick="openM('m-kirim-pesan')">✉️ Kirim</button>
</div>
<div class="stabs" style="border-top:none">
<button class="stab active" id="tab-masuk" onclick="loadInbox();switchPesanTab('masuk')">Masuk</button>
<button class="stab" id="tab-terkirim" onclick="loadOutbox();switchPesanTab('terkirim')">Terkirim</button>
</div>
<div class="inbox-list" id="inbox-list">
<div class="empty-state"><div class="empty-icon">💬</div><div class="empty-text">Tidak ada pesan</div></div>
</div>
</div>
<div class="pesan-detail" id="pesan-detail">
<div class="empty-state" style="height:100%;justify-content:center">
<div class="empty-icon">💬</div>
<div class="empty-text">Pilih pesan untuk membaca</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: CHAT WARGA ═══════════════ -->
<div class="page-wrap" id="page-chat">
<div class="chat-layout">
<div class="chat-list-panel">
<div class="chat-list-hd">🗨️ Chat Warga <span id="chat-unread-count"></span></div>
<div class="chat-list-body" id="chat-conv-list">
<div class="chat-empty"><div style="font-size:32px">🗨️</div><div>Belum ada percakapan</div></div>
</div>
</div>
<div class="chat-msg-panel" id="chat-msg-panel">
<div class="chat-empty">
<div style="font-size:40px">🗨️</div>
<div>Pilih percakapan untuk membuka chat</div>
</div>
</div>
</div>
</div>
<!-- ═══════════════ PAGE: PENDUDUK ═══════════════ -->
<div class="page-wrap" id="page-penduduk">
<div class="full-page">
<div class="page-header">
<div>
<div class="page-title">👥 Data Penduduk</div>
<div class="page-desc">Lihat seluruh penduduk miskin atau fokus pada wilayah binaan Anda</div>
</div>
<div class="page-actions">
<button class="btn btn-primary" onclick="openM('m-kk')"> Tambah KK</button>
</div>
</div>
<!-- Scope toggle -->
<div style="display:flex;align-items:center;gap:10px;margin-bottom:14px;flex-wrap:wrap">
<div style="display:flex;background:rgba(255,255,255,0.04);border:1px solid var(--border2);border-radius:10px;padding:3px;gap:2px">
<button class="scope-btn active" id="scope-binaan" onclick="setScope('binaan')">
🕌 Wilayah Binaan
</button>
<button class="scope-btn" id="scope-semua" onclick="setScope('semua')">
🌍 Semua Penduduk Miskin
</button>
</div>
<div id="scope-info" style="font-size:11px;color:var(--text2)">Menampilkan warga binaan rumah ibadah Anda</div>
</div>
<div class="filter-bar">
<input class="search-input" type="text" id="search-penduduk" placeholder="🔍 Cari nama atau alamat..." oninput="filterPenduduk()">
<select class="select-input" id="f-p-ekonomi" onchange="loadPendudukTable()">
<option value="">Semua Status Ekonomi</option>
<option value="sangat_miskin">⚠️ Sangat Miskin</option>
<option value="miskin">🟠 Miskin</option>
<option value="rentan">🟡 Rentan</option>
</select>
<select class="select-input" id="f-p-kecamatan" onchange="loadPendudukTable()">
<option value="">Semua Kecamatan</option>
</select>
</div>
<!-- Summary bar -->
<div id="penduduk-summary" style="display:flex;gap:8px;margin-bottom:12px;flex-wrap:wrap"></div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Kepala Keluarga</th>
<th>Alamat & Wilayah</th>
<th id="th-ibadah">Rumah Ibadah</th>
<th style="text-align:center">Anggota</th>
<th>Status Ekonomi</th>
<th style="text-align:center">Bantuan</th>
<th style="text-align:center">Aksi</th>
</tr>
</thead>
<tbody id="tbody-penduduk">
<tr><td colspan="7" style="text-align:center;padding:30px;color:var(--text2)">Memuat data...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ═══════════════ MODALS ═══════════════ -->
<!-- Modal: Tambah KK -->
<div class="modal-overlay hidden" id="m-kk">
<div class="modal wide">
<div class="modal-hd">
<div class="modal-title">👥 Tambah Kepala Keluarga</div>
<button class="modal-close" onclick="closeM('m-kk')">✕</button>
</div>
<div class="modal-body">
<div class="form-grid">
<div class="fl span2"><label>Nama Kepala Keluarga *</label><input class="finput" type="text" id="kk-nama" placeholder="Ahmad Yusuf..."></div>
<div class="fl"><label>No. KK</label><input class="finput" type="text" id="kk-nokk" placeholder="6171..."></div>
<div class="fl"><label>NIK</label><input class="finput" type="text" id="kk-nik" placeholder="6171..."></div>
<div class="fl"><label>Status Ekonomi</label>
<select class="finput" id="kk-ekonomi">
<option value="sangat_miskin">⚠️ Sangat Miskin</option>
<option value="miskin" selected>🟠 Miskin</option>
<option value="rentan">🟡 Rentan</option>
</select>
</div>
<div class="fl"><label>Status Hunian</label>
<select class="finput" id="kk-hunian">
<option value="milik_sendiri">Milik Sendiri</option>
<option value="sewa">Sewa</option>
<option value="menumpang">Menumpang</option>
</select>
</div>
<div class="fl"><label>RT</label><input class="finput" type="text" id="kk-rt" placeholder="001"></div>
<div class="fl"><label>RW</label><input class="finput" type="text" id="kk-rw" placeholder="002"></div>
<div class="fl span2"><label>Alamat Lengkap</label><input class="finput" type="text" id="kk-alamat" placeholder="Jl. ..."></div>
<div class="fl"><label>Kelurahan</label><input class="finput" type="text" id="kk-kel" placeholder="Bansir Laut"></div>
<div class="fl"><label>Kecamatan</label><input class="finput" type="text" id="kk-kec" placeholder="Pontianak Selatan"></div>
<div class="fl"><label>Latitude *</label><input class="finput" type="number" id="kk-lat" step="any" placeholder="-0.0263"></div>
<div class="fl"><label>Longitude *</label><input class="finput" type="number" id="kk-lng" step="any" placeholder="109.3425"></div>
</div>
<div class="section-hd">👥 Anggota Keluarga</div>
<div id="anggota-container" style="margin-top:8px"></div>
<button class="btn btn-ghost" style="margin-top:6px;font-size:11px" onclick="tambahAnggota()"> Tambah Anggota</button>
</div>
<div class="modal-ft">
<button class="btn btn-ghost" onclick="closeM('m-kk')">Batal</button>
<button class="btn btn-primary" onclick="saveKK()">💾 Simpan KK</button>
</div>
</div>
</div>
<!-- Modal: Catat Bantuan -->
<div class="modal-overlay hidden" id="m-bantuan">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">🤝 Catat Bantuan Baru</div>
<button class="modal-close" onclick="closeM('m-bantuan')">✕</button>
</div>
<div class="modal-body">
<div class="fl"><label>Keluarga Penerima *</label><select class="finput" id="b-kk"><option value="">-- Pilih Kepala Keluarga --</option></select></div>
<div class="fl"><label>Kategori Bantuan *</label><select class="finput" id="b-kategori"><option value="">-- Pilih Kategori --</option></select></div>
<div class="fl"><label>Tanggal Diberikan *</label><input class="finput" type="date" id="b-tgl" value="<?= date('Y-m-d') ?>"></div>
<div class="fl"><label>Bentuk Bantuan (Detail) *</label><input class="finput" type="text" id="b-bentuk" placeholder="Paket sembako 10kg beras + 1kg gula..."></div>
<div class="fl"><label>Nominal (Rp, kosongkan jika bukan uang)</label><input class="finput" type="number" id="b-nominal" placeholder="500000"></div>
<div class="fl"><label>Kondisi Keluarga Saat Diterima</label><textarea class="finput" id="b-kondisi" placeholder="Ceritakan kondisi keluarga..."></textarea></div>
<div class="fl"><label>Rumah Ibadah Mitra (Kolaborasi)</label><select class="finput" id="b-mitra"><option value="">-- Tidak Ada Mitra --</option></select></div>
<div class="fl"><label>Catatan Tambahan</label><textarea class="finput" id="b-catatan" placeholder="Catatan lain..."></textarea></div>
<div class="fl"><label>Foto Dokumentasi</label><input class="finput" type="file" id="b-foto" accept="image/*"></div>
</div>
<div class="modal-ft">
<button class="btn btn-ghost" onclick="closeM('m-bantuan')">Batal</button>
<button class="btn btn-teal" onclick="saveBantuan()">💾 Simpan Bantuan</button>
</div>
</div>
</div>
<!-- Modal: Verifikasi Laporan -->
<div class="modal-overlay hidden" id="m-verif">
<div class="modal">
<div class="modal-hd">
<div class="modal-title" id="verif-title">Verifikasi Laporan</div>
<button class="modal-close" onclick="closeM('m-verif')">✕</button>
</div>
<div class="modal-body" id="verif-body"></div>
<div class="modal-ft">
<button class="btn btn-ghost" onclick="closeM('m-verif')">Tutup</button>
<button class="btn btn-danger" onclick="doVerif('ditolak')">❌ Tolak</button>
<button class="btn btn-success" onclick="doVerif('disetujui')">✅ Setujui</button>
</div>
</div>
</div>
<!-- Modal: Kirim Pesan -->
<div class="modal-overlay hidden" id="m-kirim-pesan">
<div class="modal">
<div class="modal-hd">
<div class="modal-title">✉️ Kirim Pesan ke Rumah Ibadah</div>
<button class="modal-close" onclick="closeM('m-kirim-pesan')">✕</button>
</div>
<div class="modal-body">
<div class="fl"><label>Tujuan Rumah Ibadah *</label><select class="finput" id="p-tujuan"><option value="">-- Pilih Tujuan --</option></select></div>
<div class="fl"><label>Terkait Keluarga (Opsional)</label><select class="finput" id="p-kk"><option value="">-- Tidak terkait KK tertentu --</option></select></div>
<div class="fl"><label>Perihal *</label><input class="finput" type="text" id="p-perihal" placeholder="Permohonan bantuan keluarga..."></div>
<div class="fl"><label>Isi Pesan *</label><textarea class="finput" id="p-isi" rows="5" placeholder="Sampaikan informasi secara lengkap..."></textarea></div>
</div>
<div class="modal-ft">
<button class="btn btn-ghost" onclick="closeM('m-kirim-pesan')">Batal</button>
<button class="btn btn-primary" onclick="kirimPesan()">📤 Kirim Pesan</button>
</div>
</div>
</div>
<!-- Modal: Detail KK -->
<div class="modal-overlay hidden" id="m-detail-kk">
<div class="modal wide">
<div class="modal-hd">
<div class="modal-title" id="detail-kk-title">Detail Keluarga</div>
<button class="modal-close" onclick="closeM('m-detail-kk')">✕</button>
</div>
<div class="modal-body" id="detail-kk-body">
<div class="empty-state"><div class="empty-icon">⏳</div><div class="empty-text">Memuat...</div></div>
</div>
</div>
</div>
<!-- Modal: Tambah / Edit Anggota Keluarga -->
<div class="modal-overlay hidden" id="m-anggota">
<div class="modal wide">
<div class="modal-hd">
<div class="modal-title" id="anggota-modal-title">👤 Tambah Anggota</div>
<button class="modal-close" onclick="closeM('m-anggota')">✕</button>
</div>
<div class="modal-body">
<input type="hidden" id="angg-id">
<input type="hidden" id="angg-kk-id">
<div class="form-grid">
<div class="fl span2"><label>Nama Lengkap *</label><input class="finput" type="text" id="angg-nama" placeholder="Nama sesuai KTP/KK"></div>
<div class="fl"><label>NIK</label><input class="finput" type="text" id="angg-nik" placeholder="16 digit" maxlength="16" inputmode="numeric"></div>
<div class="fl">
<label>Hubungan dalam KK *</label>
<select class="finput" id="angg-hub">
<option>Kepala Keluarga</option><option>Istri</option><option>Anak</option>
<option>Orang Tua</option><option>Mertua</option><option>Saudara</option><option>Lainnya</option>
</select>
</div>
<div class="fl">
<label>Jenis Kelamin *</label>
<select class="finput" id="angg-jk">
<option value="L">Laki-laki</option><option value="P">Perempuan</option>
</select>
</div>
<div class="fl"><label>Tempat Lahir</label><input class="finput" type="text" id="angg-tmplahir" placeholder="Pontianak..."></div>
<div class="fl"><label>Tanggal Lahir *</label><input class="finput" type="date" id="angg-tgllahir"></div>
<div class="fl">
<label>Agama</label>
<select class="finput" id="angg-agama">
<option>Islam</option><option>Kristen Protestan</option><option>Katolik</option>
<option>Hindu</option><option>Buddha</option><option>Konghucu</option><option>Lainnya</option>
</select>
</div>
<div class="fl"><label>Pekerjaan</label><input class="finput" type="text" id="angg-pkj" placeholder="Pelajar / Wiraswasta..."></div>
<div class="fl"><label>Nomor HP</label><input class="finput" type="tel" id="angg-hp" placeholder="08xx-xxxx-xxxx"></div>
<div class="fl">
<label>Status Hidup</label>
<select class="finput" id="angg-status-hidup">
<option value="hidup">Hidup</option>
<option value="meninggal">Meninggal</option>
<option value="pindah">Pindah</option>
</select>
</div>
</div>
<!-- Riwayat Pendidikan -->
<div class="section-hd">📚 Riwayat Pendidikan <button type="button" onclick="addPendRow()" style="margin-left:8px;padding:2px 8px;background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;border-radius:6px;font-size:10px;font-weight:700;cursor:pointer;font-family:inherit"> Tambah</button></div>
<div id="pend-rows"></div>
</div>
<div class="modal-ft">
<button class="btn btn-ghost" onclick="closeM('m-anggota');openDetailKK(_currentKKId)">← Kembali</button>
<button class="btn btn-primary" id="angg-save-btn" onclick="saveAnggota()">💾 Simpan</button>
</div>
</div>
</div>
<!-- TOAST -->
<div class="toast" id="toast"></div>
<!-- SCRIPTS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ═══════════════ CONSTANTS ═══════════════
const IBADAH_ID = <?= $ibadahId ?: 0 ?>;
const PONTIANAK = [-0.0263, 109.3425];
// ═══════════════ MAP ═══════════════
let map, mapReady = false;
const LG = {};
const LV = { ibadah: true, radius: true };
const RADIUS_CIRCLES = {}; // id_ibadah → L.circle
function initMap() {
if (mapReady) return;
mapReady = true;
map = L.map('peta-map', { center: PONTIANAK, zoom: 13, zoomControl: false });
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap', maxZoom: 19
}).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
LG.ibadah = L.layerGroup().addTo(map);
LG.penduduk = L.layerGroup().addTo(map);
LG.radius = L.layerGroup().addTo(map);
[50, 200, 500, 1000].forEach(t => setTimeout(() => map.invalidateSize(), t));
loadIbadahPeta();
loadPendudukPeta();
}
function toggleLayer(k) {
if (!map) return;
if (LV[k]) { map.removeLayer(LG[k]); LV[k] = false; }
else { map.addLayer(LG[k]); LV[k] = true; }
}
function petaHome() { if (map) map.setView(PONTIANAK, 13); }
function mkIcon(color, emoji, size = 28, opacity = 1) {
return L.divIcon({
className: '',
html: `<div style="width:${size}px;height:${size}px;background:${color};border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:${Math.round(size * 0.46)}px;box-shadow:0 3px 10px rgba(0,0,0,.5);border:2px solid rgba(255,255,255,${opacity > 0.8 ? 0.35 : 0.12});opacity:${opacity}">${emoji}</div>`,
iconSize: [size, size], iconAnchor: [size / 2, size / 2], popupAnchor: [0, -size / 2]
});
}
const ITYPE_ICON = {
masjid: { c: '#6366f1', e: '🕌' }, gereja: { c: '#a855f7', e: '⛪' },
vihara: { c: '#f59e0b', e: '🏯' }, pura: { c: '#14b8a6', e: '🛕' },
klenteng: { c: '#38bdf8', e: '⛩️' }, lainnya: { c: '#64748b', e: '🏢' }
};
const SE_ICON = {
sangat_miskin: { c: '#f43f5e', e: '⚠️' }, miskin: { c: '#f97316', e: '🔸' },
rentan: { c: '#eab308', e: '🔶' }, mampu: { c: '#22c55e', e: '🔷' }
};
async function loadIbadahPeta() {
const gj = await api('rumah_ibadah.php?action=geojson').catch(() => ({ features: [] }));
LG.ibadah.clearLayers(); LG.radius.clearLayers();
const el = document.getElementById('list-ibadah'); el.innerHTML = '';
const rows = [];
(gj.features || []).forEach(f => {
const p = f.properties, [lng, lat] = f.geometry.coordinates;
const t = ITYPE_ICON[(p.jenis || '').toLowerCase()] || ITYPE_ICON.lainnya;
const rc = L.circle([lat, lng], { radius: p.radius || 300, color: '#6366f1', fillColor: '#6366f1', fillOpacity: 0.04, weight: 1, dashArray: '4,4' }).addTo(LG.radius);
RADIUS_CIRCLES[p.id] = rc;
const mk = L.marker([lat, lng], { icon: mkIcon(t.c, t.e) }).addTo(LG.ibadah);
mk.bindPopup(`<b>${p.nama}</b><br><small>${p.jenis} · ${p.jumlah_kk || 0} KK binaan</small>`);
// Hanya tampilkan card ibadah milik sendiri di sidebar
if (p.id == IBADAH_ID) {
const radVal = p.radius || 300;
const radPct = (((radVal - 50) / (2000 - 50)) * 100).toFixed(1);
rows.push(`<div class="icard" onclick="map&&map.flyTo([${lat},${lng}],16)" id="icard-${p.id}">
<div class="icard-hd"><div class="icard-name">${p.nama}</div><span class="badge badge-blue">${p.jenis}</span></div>
<div class="icard-meta"><span>👥 ${p.jumlah_kk || 0} KK</span><span>🤝 ${p.jumlah_bantuan || 0} bantuan</span>${(p.laporan_pending || 0) > 0 ? `<span class="badge badge-red">⏳ ${p.laporan_pending} pending</span>` : ''}</div>
<div class="icard-radius" onclick="event.stopPropagation()">
<div class="icard-radius-label">
<span>⭕ Radius Binaan</span>
<span class="icard-radius-val" id="rv-${p.id}">${radVal} m</span>
</div>
<input type="range" class="radius-slider" id="rs-${p.id}"
min="50" max="2000" value="${radVal}"
style="--slider-pct:${radPct}%"
oninput="onRadiusSlide(${p.id},${lat},${lng},this.value)" onchange="saveRadius(${p.id},this.value)">
</div>
</div>`);
}
});
el.innerHTML = rows.join('') || '<div class="empty-state"><div class="empty-icon">🕌</div><div class="empty-text">Data rumah ibadah tidak ditemukan</div></div>';
}
async function loadPendudukPeta() {
const se = document.getElementById('f-peta-ekonomi').value;
const url = `penduduk.php?action=geojson${se !== 'all' && se ? `&status_ekonomi=${se}` : ''}`;
const gj = await api(url).catch(() => ({ features: [] }));
LG.penduduk.clearLayers();
(gj.features || []).forEach(f => {
const p = f.properties, [lng, lat] = f.geometry.coordinates;
const isBinaan = (p.id_rumah_ibadah == IBADAH_ID);
const se_info = SE_ICON[p.status_ekonomi] || { c: '#64748b', e: '📍' };
const ico = mkIcon(se_info.c, se_info.e, isBinaan ? 30 : 22, isBinaan ? 1 : 0.5);
const mk = L.marker([lat, lng], { icon: ico, opacity: isBinaan ? 1 : 0.8 }).addTo(LG.penduduk);
const kkNm = (p.nama_kepala_keluarga || '').replace(/'/g, '&#39;');
mk.bindPopup(`<div style="min-width:200px">
<b style="font-size:13px">${p.nama_kepala_keluarga}</b>${isBinaan ? '<span style="margin-left:6px;padding:2px 6px;background:rgba(99,102,241,0.15);color:#c7d2fe;font-size:9px;font-weight:800;border-radius:4px">BINAAN</span>' : ''}
<div style="margin:5px 0">${seBadge(p.status_ekonomi)}</div>
<div style="font-size:11px;color:#94a3b8;line-height:1.7">
📍 ${p.alamat || '-'}<br>
👥 ${p.jumlah_anggota} jiwa · 🕌 ${p.nama_ibadah || '-'}<br>
🤝 ${p.jumlah_bantuan || 0}x bantuan
</div>
<div style="margin-top:8px;display:flex;gap:6px">
<button onclick="openDetailKK(${p.id})" style="padding:5px 10px;background:#6366f1;color:#fff;border:none;border-radius:7px;cursor:pointer;font-size:10px;font-weight:700;font-family:inherit">Detail</button>
<button onclick="map&&map.closePopup();openM('m-bantuan');preKK(${p.id},'${kkNm}')" style="padding:5px 10px;background:#14b8a6;color:#fff;border:none;border-radius:7px;cursor:pointer;font-size:10px;font-weight:700;font-family:inherit">+ Bantuan</button>
</div>
</div>`);
});
// Sidebar warga binaan
const el = document.getElementById('list-warga'); el.innerHTML = '';
const binaan = (gj.features || []).filter(f => f.properties.id_rumah_ibadah == IBADAH_ID);
if (!binaan.length) { el.innerHTML = '<div class="empty-state"><div class="empty-icon">👥</div><div class="empty-text">Belum ada warga binaan</div></div>'; return; }
el.innerHTML = binaan.map(f => {
const p = f.properties, [lng, lat] = f.geometry.coordinates;
return `<div class="icard" onclick="map&&map.flyTo([${lat},${lng}],17)">
<div class="icard-hd"><div class="icard-name">${p.nama_kepala_keluarga}</div>${seBadge(p.status_ekonomi)}</div>
<div class="icard-meta"><span>📍 ${p.kelurahan || '-'}</span><span>👥 ${p.jumlah_anggota} jiwa</span><span>🤝 ${p.jumlah_bantuan || 0}x</span></div>
</div>`;
}).join('');
}
// ═══════════════ PAGES ═══════════════
function goPage(name, el) {
document.querySelectorAll('.page-wrap').forEach(p => p.classList.remove('active'));
document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
document.getElementById('page-' + name).classList.add('active');
if (el) el.classList.add('active');
if (name === 'peta') {
initMap();
if (map) [50, 200, 500].forEach(t => setTimeout(() => map.invalidateSize(), t));
loadIbadahPeta(); loadPendudukPeta();
}
if (name === 'laporan') loadLaporan();
if (name === 'bantuan') { loadBantuanStats(); loadBantuan(); }
if (name === 'pesan') { loadInbox(); loadIbadahOpts(); }
if (name === 'chat') { loadChatConversations(); }
if (name === 'penduduk') { setScope(currentScope); }
if (name === 'overview') { loadPengurusOverview(); }
}
function switchStab(name, el) {
document.querySelectorAll('.stab').forEach(t => {
if (t.closest('.stabs') === el.closest('.stabs')) t.classList.remove('active');
});
document.querySelectorAll('.spanel').forEach(p => p.classList.remove('active'));
el.classList.add('active');
document.getElementById('sp-' + name).classList.add('active');
}
function switchPesanTab(name) {
['masuk','terkirim'].forEach(t => document.getElementById('tab-' + t).classList.remove('active'));
document.getElementById('tab-' + name).classList.add('active');
}
// ═══════════════ DASHBOARD ═══════════════
async function loadDashboard() {
const d = await api('penduduk.php?action=dashboard').catch(() => ({}));
setText('d-kk', d.total_kk || 0);
setText('d-laporan', d.pending_laporan || 0);
setText('d-bantuan', d.total_bantuan || 0);
// badge laporan
const n = d.pending_laporan || 0;
const badge = document.getElementById('badge-laporan');
if (badge) { badge.textContent = n; badge.className = 'tab-badge' + (n > 0 ? ' show' : ''); }
// pesan unread
api('pesan.php?action=inbox').then(msgs => {
const un = (msgs || []).filter(m => m.status_pesan === 'terkirim').length;
setText('d-pesan', un);
const bp = document.getElementById('badge-pesan');
if (bp) { bp.textContent = un; bp.className = 'tab-badge' + (un > 0 ? ' show' : ''); }
}).catch(() => {});
}
// ═══════════════ LAPORAN ═══════════════
let currentLaporan = null;
async function loadLaporan() {
const status = val('f-laporan-status'), jenis = val('f-laporan-jenis');
const data = await api(`laporan.php?action=list${status ? `&status=${status}` : ''}${jenis ? `&jenis=${jenis}` : ''}`).catch(() => []);
const grid = document.getElementById('grid-laporan');
if (!data.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="empty-icon">📢</div><div class="empty-text">Tidak ada laporan ditemukan</div></div>';
return;
}
grid.innerHTML = data.map(l => `
<div class="rcard ${l.status_verifikasi}">
<div class="rcard-hd">
<div class="rcard-title">${jenisLabel(l.jenis_laporan)}</div>
${svBadge(l.status_verifikasi)}
</div>
<div class="rcard-meta">
👤 <b style="color:var(--text)">${l.nama_pelapor || 'Anonim'}</b> · ${l.hp_pelapor || '-'}<br>
📅 ${fDate(l.tanggal_kejadian)} · 🕌 ${l.nama_ibadah || '-'}<br>
${l.nama_kepala_keluarga ? `🏘️ <b style="color:var(--text)">${l.nama_kepala_keluarga}</b><br>` : ''}
${l.nama_terkait ? `Terkait: <b style="color:var(--text)">${l.nama_terkait}</b>` : ''}
</div>
<div class="rcard-body">${(l.deskripsi || '').substring(0, 130)}${(l.deskripsi || '').length > 130 ? '...' : ''}</div>
${l.catatan_pengurus ? `<div style="font-size:10px;color:var(--green);margin-bottom:8px">✅ ${l.catatan_pengurus}</div>` : ''}
${l.status_verifikasi === 'pending' || l.status_verifikasi === 'diproses'
? `<button class="btn btn-primary btn-full" style="font-size:11px" onclick='openVerif(${JSON.stringify(l)})'>🔍 Verifikasi Laporan</button>` : ''}
</div>`).join('');
}
function openVerif(l) {
currentLaporan = l;
document.getElementById('verif-title').textContent = 'Verifikasi: ' + jenisLabel(l.jenis_laporan);
document.getElementById('verif-body').innerHTML = `
<div style="margin-bottom:14px">
<div style="font-size:13px;font-weight:800;margin-bottom:8px">${jenisLabel(l.jenis_laporan)} ${svBadge(l.status_verifikasi)}</div>
<div style="font-size:11px;color:var(--text2);line-height:2">
👤 Pelapor: <b style="color:var(--text)">${l.nama_pelapor || 'Anonim'}</b> (${l.hp_pelapor || '-'})<br>
📅 Tanggal: <b style="color:var(--text)">${fDate(l.tanggal_kejadian)}</b><br>
${l.nama_terkait ? `👥 Terkait: <b style="color:var(--text)">${l.nama_terkait}</b><br>` : ''}
${l.nama_kepala_keluarga ? `🏘️ Keluarga: <b style="color:var(--text)">${l.nama_kepala_keluarga}</b><br>` : ''}
📍 Wilayah: <b style="color:var(--text)">${l.kelurahan || '-'}, ${l.kecamatan || '-'}</b>
</div>
</div>
<div style="background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:10px;padding:12px;font-size:12px;color:var(--text2);line-height:1.7;margin-bottom:14px">${l.deskripsi}</div>
<div class="fl"><label>Catatan Pengurus</label><textarea class="finput" id="verif-catatan" rows="3" placeholder="Tambahkan keterangan atau tindak lanjut..."></textarea></div>`;
openM('m-verif');
}
async function doVerif(status) {
const catatan = val('verif-catatan');
const d = await fetch('../api/laporan.php?action=verifikasi', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: currentLaporan.id, status_verifikasi: status, catatan_pengurus: catatan })
}).then(r => r.json());
if (d.success) {
toast(`Laporan ${status === 'disetujui' ? 'disetujui ✅' : 'ditolak ❌'}`, 'success');
closeM('m-verif'); loadLaporan(); loadDashboard();
} else toast(d.error || 'Gagal', 'error');
}
// ═══════════════ BANTUAN ═══════════════
async function loadBantuanStats() {
const d = await api('bantuan.php?action=statistik').catch(() => ({}));
setText('bt-total', d.total_bantuan || 0);
const nom = d.total_nominal || 0;
setText('bt-nominal', nom > 1e6 ? `${(nom / 1e6).toFixed(1)}jt` : nom.toLocaleString('id'));
const ib = (d.per_jenis || []).filter(j => j.sumber === 'rumah_ibadah').reduce((s, j) => s + parseInt(j.jumlah || 0), 0);
const pem = (d.per_jenis || []).filter(j => j.sumber === 'pemerintah').reduce((s, j) => s + parseInt(j.jumlah || 0), 0);
setText('bt-ibadah', ib);
setText('bt-pemerin', pem);
}
async function loadBantuan() {
const ibadah = val('f-bantuan-ibadah');
const data = await api(`bantuan.php?action=list${ibadah ? `&id_ibadah=${ibadah}` : ''}`).catch(() => []);
const grid = document.getElementById('grid-bantuan');
if (!data.length) {
grid.innerHTML = '<div class="empty-state" style="grid-column:span 3"><div class="empty-icon">🤝</div><div class="empty-text">Belum ada bantuan tercatat</div></div>';
return;
}
grid.innerHTML = data.map(b => `
<div class="bcard">
<div class="bcard-top">
<div class="bcard-jenis">${b.jenis_bantuan}</div>
<div class="bcard-tgl">${fDate(b.tanggal_bantuan)}</div>
</div>
<div class="bcard-kk">${b.nama_kepala_keluarga}</div>
<div class="bcard-detail">
📦 ${b.bentuk_bantuan}
${b.jumlah_nominal ? `<br>💰 Rp${Number(b.jumlah_nominal).toLocaleString('id')}` : ''}
${b.kondisi_saat_diberikan ? `<br>📋 ${(b.kondisi_saat_diberikan || '').substring(0, 80)}` : ''}
</div>
<div class="bcard-footer">🕌 ${b.nama_ibadah || b.sumber_bantuan || '-'}${b.nama_mitra ? ` + mitra: ${b.nama_mitra}` : ''} · Oleh: ${b.dicatat_oleh || '-'}</div>
</div>`).join('');
}
async function saveBantuan() {
const fd = new FormData();
fd.append('id_penduduk', val('b-kk'));
fd.append('id_kategori_bantuan', val('b-kategori'));
fd.append('tanggal_bantuan', val('b-tgl'));
fd.append('bentuk_bantuan', val('b-bentuk'));
fd.append('jumlah_nominal', val('b-nominal'));
fd.append('kondisi_saat_diberikan', val('b-kondisi'));
fd.append('id_rumah_ibadah_mitra', val('b-mitra'));
fd.append('catatan_tambahan', val('b-catatan'));
const foto = document.getElementById('b-foto').files[0];
if (foto) fd.append('foto_dokumentasi', foto);
const d = await fetch('../api/bantuan.php', { method: 'POST', body: fd }).then(r => r.json());
if (d.success) { toast('Bantuan berhasil dicatat ✅', 'success'); closeM('m-bantuan'); loadBantuan(); loadBantuanStats(); loadDashboard(); }
else toast(d.error || 'Gagal menyimpan', 'error');
}
// ═══════════════ PESAN ═══════════════
async function loadInbox() {
const data = await api('pesan.php?action=inbox').catch(() => []);
renderMsgList(data, 'inbox');
}
async function loadOutbox() {
const data = await api('pesan.php?action=outbox').catch(() => []);
renderMsgList(data, 'outbox');
}
function renderMsgList(data, type) {
const el = document.getElementById('inbox-list');
if (!data.length) { el.innerHTML = '<div class="empty-state"><div class="empty-icon">💬</div><div class="empty-text">Tidak ada pesan</div></div>'; return; }
el.innerHTML = data.map(m => `
<div class="msg-item ${m.status_pesan === 'terkirim' ? 'unread' : ''}" onclick='showPesan(${JSON.stringify(m)},"${type}")'>
<div style="display:flex;justify-content:space-between;align-items:flex-start">
<div class="msg-from">${type === 'inbox' ? (m.dari_ibadah || '-') : (m.kepada_ibadah || '-')}</div>
<div class="msg-time">${fTime(m.created_at)}</div>
</div>
<div class="msg-subject">${m.perihal}</div>
${m.nama_kepala_keluarga ? `<div class="msg-time">👥 ${m.nama_kepala_keluarga}</div>` : ''}
<div style="margin-top:4px">
${m.status_pesan === 'terkirim' ? '<span class="badge badge-amber">Belum dibaca</span>' :
m.status_pesan === 'dibalas' ? '<span class="badge badge-green">Dibalas</span>' :
`<span class="badge badge-gray">${m.status_pesan}</span>`}
</div>
</div>`).join('');
}
function showPesan(m, type) {
const el = document.getElementById('pesan-detail');
el.innerHTML = `
<div style="margin-bottom:16px">
<div style="font-size:16px;font-weight:800;margin-bottom:8px">${m.perihal}</div>
<div style="font-size:11px;color:var(--text2);line-height:2">
📤 Dari: <b style="color:var(--text)">${m.dari_ibadah || '-'}</b>${m.nama_pengirim ? ` · ${m.nama_pengirim}` : ''}<br>
📥 Ke: <b style="color:var(--text)">${m.kepada_ibadah || '-'}</b><br>
📅 ${fTime(m.created_at)}<br>
${m.nama_kepala_keluarga ? `👥 Terkait: <b style="color:var(--text)">${m.nama_kepala_keluarga}</b>` : ''}
</div>
</div>
<div style="background:rgba(255,255,255,.02);border:1px solid var(--border);border-radius:10px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:14px">${m.isi_pesan}</div>
${m.pesan_balasan ? `<div style="background:rgba(34,197,94,.06);border:1px solid rgba(34,197,94,.2);border-radius:10px;padding:12px;font-size:12px;color:var(--green)">✅ Balasan: ${m.pesan_balasan}</div>` : ''}
${type === 'inbox' && m.status_pesan !== 'ditindaklanjuti' ? `
<div style="margin-top:16px">
<div class="fl"><label>Balas Pesan</label><textarea class="finput" id="reply-isi" rows="4" placeholder="Tulis balasan Anda..."></textarea></div>
<div style="display:flex;gap:8px">
<button class="btn btn-ghost" onclick="updatePesan(${m.id},'dibaca','')">Tandai Dibaca</button>
<button class="btn btn-success btn-full" onclick="updatePesan(${m.id},'ditindaklanjuti',document.getElementById('reply-isi').value)">✅ Balas & Tindaklanjut</button>
</div>
</div>` : ''}`;
}
async function updatePesan(id, status, balasan) {
const d = await fetch('../api/pesan.php?action=inbox', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, status_pesan: status, pesan_balasan: balasan })
}).then(r => r.json());
if (d.success) { toast('Pesan diperbarui ✅', 'success'); loadInbox(); }
else toast(d.error || 'Gagal', 'error');
}
async function kirimPesan() {
const d = await fetch('../api/pesan.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id_penerima_ibadah: val('p-tujuan'), id_penduduk: val('p-kk'),
perihal: val('p-perihal'), isi_pesan: val('p-isi')
})
}).then(r => r.json());
if (d.success) { toast('Pesan berhasil dikirim ✅', 'success'); closeM('m-kirim-pesan'); loadOutbox(); switchPesanTab('terkirim'); }
else toast(d.error || 'Gagal', 'error');
}
// ═══════════════ PENDUDUK TABLE ═══════════════
let pendudukData = [];
let currentScope = 'binaan'; // 'binaan' | 'semua'
function setScope(scope) {
currentScope = scope;
document.getElementById('scope-binaan').classList.toggle('active', scope === 'binaan');
document.getElementById('scope-semua').classList.toggle('active', scope === 'semua');
const info = document.getElementById('scope-info');
if (scope === 'binaan') {
info.textContent = 'Menampilkan warga binaan rumah ibadah Anda';
info.style.color = 'var(--text2)';
} else {
info.innerHTML = '⚠️ Mode lihat semua — data dari seluruh kota, tidak terbatas wilayah binaan';
info.style.color = 'var(--amber)';
}
// Filter kecamatan hanya muncul di mode semua
document.getElementById('f-p-kecamatan').style.display = scope === 'semua' ? '' : 'none';
document.getElementById('th-ibadah').style.display = scope === 'semua' ? '' : 'none';
loadPendudukTable();
}
async function loadPendudukTable() {
const se = val('f-p-ekonomi');
const kec = val('f-p-kecamatan');
let url = 'penduduk.php?action=geojson';
// Scope binaan: filter per ibadah; scope semua: semua data miskin
if (currentScope === 'binaan' && IBADAH_ID) url += `&id_ibadah=${IBADAH_ID}`;
if (se) url += `&status_ekonomi=${se}`;
if (kec && currentScope === 'semua') url += `&kecamatan=${encodeURIComponent(kec)}`;
const gj = await api(url).catch(() => ({ features: [] }));
pendudukData = gj.features || [];
// Populate kecamatan filter (mode semua, sekali)
if (currentScope === 'semua') {
const selKec = document.getElementById('f-p-kecamatan');
const existing = new Set(Array.from(selKec.options).map(o => o.value).filter(Boolean));
pendudukData.forEach(f => {
const k = f.properties.kecamatan;
if (k && !existing.has(k)) { selKec.add(new Option(k, k)); existing.add(k); }
});
}
renderSummary(pendudukData);
renderPendudukTable(pendudukData);
}
function renderSummary(features) {
const el = document.getElementById('penduduk-summary');
const total = features.length;
const sm = features.filter(f => f.properties.status_ekonomi === 'sangat_miskin').length;
const ms = features.filter(f => f.properties.status_ekonomi === 'miskin').length;
const rn = features.filter(f => f.properties.status_ekonomi === 'rentan').length;
const binaan = features.filter(f => f.properties.id_rumah_ibadah == IBADAH_ID).length;
let html = `<span class="badge badge-gray">Total: <b>${total} KK</b></span>`;
if (sm) html += `<span class="badge badge-red">⚠️ Sangat Miskin: ${sm}</span>`;
if (ms) html += `<span class="badge badge-amber">🟠 Miskin: ${ms}</span>`;
if (rn) html += `<span class="badge badge-purple">🟡 Rentan: ${rn}</span>`;
if (currentScope === 'semua') html += `<span class="badge badge-blue">🕌 Binaan Anda: ${binaan}</span>`;
el.innerHTML = html;
}
function filterPenduduk() {
const q = document.getElementById('search-penduduk').value.toLowerCase();
renderPendudukTable(pendudukData.filter(f =>
(f.properties.nama_kepala_keluarga || '').toLowerCase().includes(q) ||
(f.properties.alamat || '').toLowerCase().includes(q) ||
(f.properties.kelurahan || '').toLowerCase().includes(q)
));
}
function renderPendudukTable(features) {
const tbody = document.getElementById('tbody-penduduk');
const showIbadah = currentScope === 'semua';
const colspan = showIbadah ? 7 : 6;
if (!features.length) {
tbody.innerHTML = `<tr><td colspan="${colspan}" style="text-align:center;padding:30px;color:var(--text2)">Tidak ada data penduduk</td></tr>`;
return;
}
tbody.innerHTML = features.map(f => {
const p = f.properties;
const nm = (p.nama_kepala_keluarga || '').replace(/'/g, "\\'");
const isBinaan = (p.id_rumah_ibadah == IBADAH_ID);
const rowClass = (!isBinaan && currentScope === 'semua') ? 'non-binaan' : '';
const ibadahCell = showIbadah
? `<td>
<div style="font-size:11px;font-weight:${isBinaan ? '700' : '400'};color:${isBinaan ? 'var(--accent2)' : 'var(--text2)'}">
${isBinaan ? '🔵 ' : ''}${p.nama_ibadah || '-'}
</div>
${isBinaan ? '<div style="font-size:9px;color:var(--teal);font-weight:700">BINAAN ANDA</div>' : ''}
</td>`
: '';
return `<tr class="${rowClass}">
<td>
<div style="font-weight:700">${p.nama_kepala_keluarga}</div>
<div style="font-size:10px;color:var(--text2)">${p.no_kk || 'KK belum tersedia'}</div>
</td>
<td>
<div>${p.alamat || '-'}</div>
<div style="font-size:10px;color:var(--text2)">${[p.kelurahan, p.kecamatan].filter(Boolean).join(', ')}</div>
</td>
${ibadahCell}
<td style="text-align:center">${p.jumlah_anggota || 0} jiwa</td>
<td>${seBadge(p.status_ekonomi)}</td>
<td style="text-align:center">${p.jumlah_bantuan || 0}×</td>
<td style="text-align:center">
<button class="btn btn-ghost" style="font-size:10px;padding:5px 10px" onclick="openDetailKK(${p.id})">Detail</button>
<button class="btn btn-teal" style="font-size:10px;padding:5px 10px;margin-left:4px" onclick="openM('m-bantuan');preKK(${p.id},'${nm}')">+ Bantuan</button>
</td>
</tr>`;
}).join('');
}
// ═══════════════ TAMBAH KK ═══════════════
let anggotaIdx = 0;
function tambahAnggota() {
const i = anggotaIdx++;
const div = document.createElement('div');
div.className = 'anggota-card';
div.dataset.ai = i;
div.innerHTML = `
<div class="anggota-card-hd">
<span class="anggota-card-title">Anggota ${i + 1}</span>
<button type="button" class="btn btn-danger" style="font-size:10px;padding:3px 8px" onclick="this.closest('.anggota-card').remove()">✕ Hapus</button>
</div>
<div class="form-grid">
<div class="fl"><label>Nama</label><input class="finput" type="text" name="a[${i}][nama]" placeholder="Nama anggota..."></div>
<div class="fl"><label>Hubungan</label>
<select class="finput" name="a[${i}][hubungan]">
<option>Kepala Keluarga</option><option>Istri/Suami</option><option>Anak</option><option>Orang Tua</option><option>Lainnya</option>
</select>
</div>
<div class="fl"><label>Jenis Kelamin</label>
<select class="finput" name="a[${i}][jenis_kelamin]"><option value="L">Laki-laki</option><option value="P">Perempuan</option></select>
</div>
<div class="fl"><label>Tanggal Lahir</label><input class="finput" type="date" name="a[${i}][tanggal_lahir]"></div>
<div class="fl"><label>Agama</label>
<select class="finput" name="a[${i}][agama]">
<option>Islam</option><option>Kristen</option><option>Katolik</option><option>Hindu</option><option>Buddha</option><option>Konghucu</option>
</select>
</div>
<div class="fl"><label>Pekerjaan</label><input class="finput" type="text" name="a[${i}][pekerjaan]" placeholder="Buruh, Petani, dll..."></div>
<div class="fl span2"><label>Pendidikan Terakhir</label>
<select class="finput" name="a[${i}][pendidikan]">
<option value="">-- Pilih --</option><option>Tidak Sekolah</option><option>SD</option><option>SMP</option>
<option>SMA/SMK</option><option>D3</option><option>S1</option><option>S2</option>
</select>
</div>
</div>`;
document.getElementById('anggota-container').appendChild(div);
}
async function saveKK() {
const fd = new FormData();
const fields = ['nama','nokk','nik','ekonomi','hunian','rt','rw','alamat','kel','kec','lat','lng'];
fields.forEach(f => fd.append(f === 'nama' ? 'nama_kepala_keluarga' : f === 'nokk' ? 'no_kk' : f === 'nik' ? 'nik_kepala' : f === 'ekonomi' ? 'status_ekonomi' : f === 'hunian' ? 'status_hunian' : f === 'kel' ? 'kelurahan' : f === 'kec' ? 'kecamatan' : f === 'lat' ? 'latitude' : f === 'lng' ? 'longitude' : f, val('kk-' + f)));
const anggota = [];
document.querySelectorAll('#anggota-container .anggota-card').forEach(card => {
const a = {};
card.querySelectorAll('input, select').forEach(inp => {
const m = inp.name.match(/a\[(\d+)\]\[(\w+)\]/);
if (m) a[m[2]] = inp.value;
});
if (a.nama) {
anggota.push({ ...a, pendidikan: a.pendidikan ? [{ jenjang: a.pendidikan, status_pendidikan: 'lulus' }] : [] });
}
});
fd.append('anggota', JSON.stringify(anggota));
fd.append('jumlah_anggota', anggota.length || 1);
const d = await fetch('../api/penduduk.php', { method: 'POST', body: fd }).then(r => r.json());
if (d.success) { toast('Data KK berhasil disimpan ✅', 'success'); closeM('m-kk'); loadPendudukPeta(); loadPendudukTable(); loadDashboard(); anggotaIdx = 0; document.getElementById('anggota-container').innerHTML = ''; }
else toast(d.error || 'Gagal menyimpan', 'error');
}
// ═══════════════ DETAIL KK + MANAJEMEN ANGGOTA ═══════════════
let _currentKKId = null;
async function openDetailKK(id) {
_currentKKId = id;
openM('m-detail-kk');
document.getElementById('detail-kk-title').textContent = 'Detail Keluarga';
document.getElementById('detail-kk-body').innerHTML = '<div class="empty-state"><div class="empty-icon">⏳</div><div class="empty-text">Memuat data...</div></div>';
await _renderDetailKK(id);
}
async function _renderDetailKK(id) {
const f = await api(`penduduk.php?action=get&id=${id}`).catch(() => null);
if (!f || !f.properties) {
document.getElementById('detail-kk-body').innerHTML = '<p style="text-align:center;color:var(--red);padding:20px">Gagal memuat data</p>';
return;
}
const p = f.properties;
const anggota = p.anggota || [];
const bantuan = p.histori_bantuan || [];
document.getElementById('detail-kk-title').textContent = p.nama_kepala_keluarga;
const shBadge = sh => {
if (!sh || sh === 'hidup') return '<span class="sh-hidup">✅ Hidup</span>';
if (sh === 'meninggal') return '<span class="sh-meninggal">💀 Meninggal</span>';
if (sh === 'pindah') return '<span class="sh-pindah">📦 Pindah</span>';
return '';
};
const anggHTML = anggota.length ? anggota.map(a => {
const pend = a.pendidikan_terakhir;
return `<div class="member-item" id="member-${a.id}">
<div class="member-av ${a.jenis_kelamin === 'L' ? 'male' : 'female'}">${a.jenis_kelamin === 'L' ? '👦' : '👧'}</div>
<div class="member-info">
<div class="member-name">
${a.nama}
${shBadge(a.status_hidup)}
</div>
<div class="member-meta">
${a.hubungan} · ${a.umur || 0} tahun · ${a.agama || '-'}
${a.pekerjaan ? `<br>💼 ${a.pekerjaan}` : ''}
${pend ? `<br>📚 ${pend.jenjang}${pend.nama_institusi ? ' — ' + pend.nama_institusi : ''}` : ''}
${a.no_hp ? `<br>📱 ${a.no_hp}` : ''}
</div>
<div class="member-actions">
<button class="mbtn mbtn-edit" onclick="openEditAnggota(${a.id},${id})">✏️ Edit</button>
<button class="mbtn mbtn-status" onclick="openStatusAnggota(${a.id},'${a.status_hidup||'hidup'}','${a.nama.replace(/'/g,"\\'")}',${id})">🔄 Status</button>
<button class="mbtn mbtn-del" onclick="konfirmasiHapusAnggota(${a.id},${id},'${a.nama.replace(/'/g,"\\'")}')">🗑️ Hapus</button>
</div>
</div>
</div>`;
}).join('') : '<div class="empty-state" style="padding:16px"><div class="empty-icon">👨‍👩‍👧</div><div class="empty-text">Belum ada anggota tercatat</div></div>';
document.getElementById('detail-kk-body').innerHTML = `
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px">
${seBadge(p.status_ekonomi)}
${p.nama_ibadah ? `<span class="badge badge-blue">🕌 ${p.nama_ibadah}</span>` : ''}
${p.status_hunian ? `<span class="badge badge-gray">🏠 ${p.status_hunian.replace('_',' ')}</span>` : ''}
</div>
<div style="font-size:11px;color:var(--text2);line-height:1.9;margin-bottom:12px">
📍 ${p.alamat || '-'}${p.rt ? ` · RT${p.rt}/RW${p.rw}` : ''}<br>
${p.kelurahan ? `🏘️ Kel. ${p.kelurahan}, Kec. ${p.kecamatan || '-'}` : ''}
${p.jarak_ke_ibadah ? `<br>📏 Jarak ke rumah ibadah: ±${p.jarak_ke_ibadah}m` : ''}
</div>
<div class="section-hd" style="display:flex;justify-content:space-between;align-items:center">
<span>👥 Anggota Keluarga (${anggota.length})</span>
<button onclick="openTambahAnggota(${id})" style="padding:4px 10px;background:rgba(99,102,241,0.15);border:1px solid rgba(99,102,241,0.3);color:#a5b4fc;border-radius:7px;font-size:9px;font-weight:800;cursor:pointer;font-family:inherit"> Tambah</button>
</div>
<div id="anggota-list-${id}">${anggHTML}</div>
<div class="section-hd">🤝 Riwayat Bantuan (${bantuan.length}×)</div>
${bantuan.slice(0, 5).map(b => `
<div class="bcard" style="margin-bottom:7px">
<div class="bcard-top"><div class="bcard-jenis">${b.jenis_bantuan}</div><div class="bcard-tgl">${fDate(b.tanggal_bantuan)}</div></div>
<div class="bcard-detail">📦 ${b.bentuk_bantuan}${b.jumlah_nominal ? `<br>💰 Rp${Number(b.jumlah_nominal).toLocaleString('id')}` : ''}</div>
<div class="bcard-footer">🕌 ${b.nama_ibadah || b.sumber_bantuan || '-'}${b.nama_mitra ? ` + mitra: ${b.nama_mitra}` : ''}</div>
</div>`).join('') || '<div class="empty-state" style="padding:12px"><div class="empty-text">Belum ada bantuan tercatat</div></div>'}
<div style="display:flex;gap:8px;margin-top:14px">
<button class="btn btn-teal btn-full" onclick="closeM('m-detail-kk');openM('m-bantuan');preKK(${p.id},'${(p.nama_kepala_keluarga || '').replace(/'/g, "\\'")}')">🤝 Catat Bantuan</button>
<button class="btn btn-ghost btn-full" onclick="closeM('m-detail-kk')">Tutup</button>
</div>`;
}
// ── Buka form tambah anggota baru ─────────────────────────
function openTambahAnggota(kkId) {
_currentKKId = kkId;
document.getElementById('anggota-modal-title').textContent = '👤 Tambah Anggota Keluarga';
document.getElementById('angg-id').value = '';
document.getElementById('angg-kk-id').value = kkId;
// Reset fields
['angg-nama','angg-nik','angg-tmplahir','angg-tgllahir','angg-pkj','angg-hp'].forEach(i => {
const el = document.getElementById(i); if (el) el.value = '';
});
document.getElementById('angg-hub').value = 'Anak';
document.getElementById('angg-jk').value = 'L';
document.getElementById('angg-agama').value = 'Islam';
document.getElementById('angg-status-hidup').value = 'hidup';
document.getElementById('pend-rows').innerHTML = '';
closeM('m-detail-kk');
openM('m-anggota');
}
// ── Buka form edit anggota ─────────────────────────────────
async function openEditAnggota(anggId, kkId) {
_currentKKId = kkId;
document.getElementById('anggota-modal-title').textContent = '✏️ Edit Anggota Keluarga';
document.getElementById('angg-id').value = anggId;
document.getElementById('angg-kk-id').value = kkId;
const a = await api(`anggota.php?action=get&id=${anggId}`).catch(() => null);
if (!a || a.error) { toast('Gagal memuat data anggota', 'error'); return; }
document.getElementById('angg-nama').value = a.nama || '';
document.getElementById('angg-nik').value = a.nik || '';
document.getElementById('angg-hub').value = a.hubungan || 'Anak';
document.getElementById('angg-jk').value = a.jenis_kelamin || 'L';
document.getElementById('angg-tmplahir').value = a.tempat_lahir || '';
document.getElementById('angg-tgllahir').value = a.tanggal_lahir ? a.tanggal_lahir.split('T')[0] : '';
document.getElementById('angg-agama').value = a.agama || 'Islam';
document.getElementById('angg-pkj').value = a.pekerjaan || '';
document.getElementById('angg-hp').value = a.no_hp || '';
document.getElementById('angg-status-hidup').value = a.status_hidup || 'hidup';
// Riwayat pendidikan
const pendRows = document.getElementById('pend-rows');
pendRows.innerHTML = '';
(a.riwayat_pendidikan || []).forEach(pd => addPendRow(pd));
closeM('m-detail-kk');
openM('m-anggota');
}
// ── Quick-change status hidup lewat confirm dialog ─────────
async function openStatusAnggota(anggId, statusSaat, namaAngg, kkId) {
_currentKKId = kkId;
const opts = ['hidup','meninggal','pindah'];
const labels = { hidup:'✅ Hidup', meninggal:'💀 Meninggal', pindah:'📦 Pindah' };
// Buat dialog inline kecil dalam area member
const el = document.getElementById('member-' + anggId);
if (!el) return;
// Cek apakah sudah ada popup status
const old = document.getElementById('status-popup-' + anggId);
if (old) { old.remove(); return; }
const popup = document.createElement('div');
popup.id = 'status-popup-' + anggId;
popup.style.cssText = 'margin-top:7px;padding:8px 10px;background:rgba(15,20,36,0.95);border:1px solid rgba(99,102,241,0.2);border-radius:9px;display:flex;gap:6px;flex-wrap:wrap;align-items:center';
popup.innerHTML = `<span style="font-size:9px;font-weight:800;color:var(--text2);text-transform:uppercase;letter-spacing:0.4px">Ubah Status:</span>` +
opts.map(s => `<button onclick="doSetStatus(${anggId},${kkId},'${s}',this)" style="padding:4px 9px;border-radius:7px;border:1px solid ${s===statusSaat?'rgba(99,102,241,0.5)':'rgba(255,255,255,0.1)'};background:${s===statusSaat?'rgba(99,102,241,0.2)':'rgba(255,255,255,0.04)'};color:${s===statusSaat?'#a5b4fc':'var(--text2)'};font-family:inherit;font-size:9px;font-weight:800;cursor:pointer">${labels[s]}</button>`).join('') +
`<button onclick="document.getElementById('status-popup-${anggId}').remove()" style="padding:4px 7px;border-radius:7px;border:1px solid rgba(255,255,255,0.08);background:none;color:var(--text2);font-family:inherit;font-size:9px;cursor:pointer">✕</button>`;
el.querySelector('.member-info').appendChild(popup);
}
async function doSetStatus(anggId, kkId, newStatus, btnEl) {
const d = await fetch('../api/anggota.php', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: anggId, action: 'status', status_hidup: newStatus })
}).then(r => r.json()).catch(() => ({ success: false }));
if (d.success) {
toast('Status anggota diperbarui ✅', 'success');
document.getElementById('status-popup-' + anggId)?.remove();
// Refresh anggota list saja tanpa reload penuh
_refreshAnggotaList(kkId);
} else {
toast(d.error || 'Gagal ubah status', 'error');
}
}
async function _refreshAnggotaList(kkId) {
const f = await api(`penduduk.php?action=get&id=${kkId}`).catch(() => null);
if (!f || !f.properties) return;
const p = f.properties;
const anggota = p.anggota || [];
const shBadge = sh => {
if (!sh || sh === 'hidup') return '<span class="sh-hidup">✅ Hidup</span>';
if (sh === 'meninggal') return '<span class="sh-meninggal">💀 Meninggal</span>';
if (sh === 'pindah') return '<span class="sh-pindah">📦 Pindah</span>';
return '';
};
const container = document.getElementById('anggota-list-' + kkId);
if (!container) return;
container.innerHTML = anggota.length ? anggota.map(a => {
const pend = a.pendidikan_terakhir;
return `<div class="member-item" id="member-${a.id}">
<div class="member-av ${a.jenis_kelamin === 'L' ? 'male' : 'female'}">${a.jenis_kelamin === 'L' ? '👦' : '👧'}</div>
<div class="member-info">
<div class="member-name">${a.nama} ${shBadge(a.status_hidup)}</div>
<div class="member-meta">
${a.hubungan} · ${a.umur || 0} tahun · ${a.agama || '-'}
${a.pekerjaan ? `<br>💼 ${a.pekerjaan}` : ''}
${pend ? `<br>📚 ${pend.jenjang}${pend.nama_institusi ? ' — ' + pend.nama_institusi : ''}` : ''}
${a.no_hp ? `<br>📱 ${a.no_hp}` : ''}
</div>
<div class="member-actions">
<button class="mbtn mbtn-edit" onclick="openEditAnggota(${a.id},${kkId})">✏️ Edit</button>
<button class="mbtn mbtn-status" onclick="openStatusAnggota(${a.id},'${a.status_hidup||'hidup'}','${a.nama.replace(/'/g,"\\'")}',${kkId})">🔄 Status</button>
<button class="mbtn mbtn-del" onclick="konfirmasiHapusAnggota(${a.id},${kkId},'${a.nama.replace(/'/g,"\\'")}')">🗑️ Hapus</button>
</div>
</div>
</div>`;
}).join('') : '<div class="empty-state" style="padding:16px"><div class="empty-icon">👨‍👩‍👧</div><div class="empty-text">Belum ada anggota tercatat</div></div>';
}
// ── Konfirmasi hapus anggota ───────────────────────────────
function konfirmasiHapusAnggota(anggId, kkId, nama) {
// Mini confirm popup di dalam member-item
const el = document.getElementById('member-' + anggId);
if (!el) return;
const old = document.getElementById('del-confirm-' + anggId);
if (old) { old.remove(); return; }
const popup = document.createElement('div');
popup.id = 'del-confirm-' + anggId;
popup.style.cssText = 'margin-top:7px;padding:8px 10px;background:rgba(239,68,68,0.07);border:1px solid rgba(239,68,68,0.2);border-radius:9px;font-size:10px;color:#fca5a5;display:flex;align-items:center;gap:8px;flex-wrap:wrap';
popup.innerHTML = `<span>Hapus <b>${nama}</b> dari KK?</span>
<button onclick="doHapusAnggota(${anggId},${kkId})" style="padding:4px 10px;background:rgba(239,68,68,0.2);border:1px solid rgba(239,68,68,0.3);color:#fca5a5;border-radius:7px;font-family:inherit;font-size:9px;font-weight:800;cursor:pointer">Ya, Hapus</button>
<button onclick="document.getElementById('del-confirm-${anggId}').remove()" style="padding:4px 8px;background:none;border:1px solid rgba(255,255,255,0.1);color:var(--text2);border-radius:7px;font-family:inherit;font-size:9px;cursor:pointer">Batal</button>`;
el.querySelector('.member-info').appendChild(popup);
}
async function doHapusAnggota(anggId, kkId) {
const d = await fetch(`../api/anggota.php?id=${anggId}`, {
method: 'DELETE'
}).then(r => r.json()).catch(() => ({ success: false }));
if (d.success) {
toast('Anggota berhasil dihapus ✅', 'success');
_refreshAnggotaList(kkId);
document.getElementById('member-' + anggId)?.remove();
} else {
toast(d.error || 'Gagal menghapus', 'error');
}
}
// ── Simpan anggota (tambah/edit) ───────────────────────────
async function saveAnggota() {
const id = document.getElementById('angg-id').value;
const kkId = parseInt(document.getElementById('angg-kk-id').value);
const nama = document.getElementById('angg-nama').value.trim();
if (!nama) { toast('Nama wajib diisi', 'error'); return; }
const btn = document.getElementById('angg-save-btn');
btn.disabled = true; btn.textContent = '⏳ Menyimpan...';
// Kumpulkan riwayat pendidikan
const pendRows = document.querySelectorAll('.pend-row-item');
const riwayat = [];
pendRows.forEach(row => {
const jj = row.querySelector('.pr-jenjang').value;
if (!jj) return;
riwayat.push({
jenjang: jj,
nama_institusi: row.querySelector('.pr-institusi').value,
tahun_masuk: row.querySelector('.pr-masuk').value || null,
tahun_lulus: row.querySelector('.pr-lulus').value || null,
status_pendidikan:row.querySelector('.pr-status').value
});
});
const payload = {
id_penduduk: kkId,
nama,
nik: document.getElementById('angg-nik').value.trim(),
hubungan: document.getElementById('angg-hub').value,
jenis_kelamin: document.getElementById('angg-jk').value,
tempat_lahir: document.getElementById('angg-tmplahir').value.trim(),
tanggal_lahir: document.getElementById('angg-tgllahir').value,
agama: document.getElementById('angg-agama').value,
pekerjaan: document.getElementById('angg-pkj').value.trim(),
no_hp: document.getElementById('angg-hp').value.trim(),
status_hidup: document.getElementById('angg-status-hidup').value,
riwayat_pendidikan: riwayat
};
let res;
if (id) {
// Edit
payload.id = parseInt(id);
res = await fetch('../api/anggota.php', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(r => r.json()).catch(() => ({ success: false, error: 'Koneksi gagal' }));
} else {
// Tambah baru
res = await fetch('../api/anggota.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(r => r.json()).catch(() => ({ success: false, error: 'Koneksi gagal' }));
}
btn.disabled = false; btn.textContent = '💾 Simpan';
if (res.success) {
toast((id ? 'Data anggota diperbarui' : 'Anggota berhasil ditambahkan') + ' ✅', 'success');
closeM('m-anggota');
openDetailKK(kkId);
} else {
toast(res.error || 'Gagal menyimpan', 'error');
}
}
// ── Baris riwayat pendidikan dinamis ──────────────────────
let _pendIdx = 0;
function addPendRow(data = {}) {
const idx = _pendIdx++;
const container = document.getElementById('pend-rows');
if (!container.querySelector('.pend-header')) {
const hdr = document.createElement('div');
hdr.className = 'pend-header';
hdr.innerHTML = '<span>Jenjang</span><span>Institusi</span><span>Masuk</span><span>Lulus</span><span>Status</span><span></span>';
container.prepend(hdr);
}
const row = document.createElement('div');
row.className = 'pend-row pend-row-item';
row.innerHTML = `
<select class="pr-jenjang">
${['SD','SMP','SMA/SMK','D3','S1','S2','S3','Pesantren','Non-formal'].map(j =>
`<option${data.jenjang===j?' selected':''}>${j}</option>`).join('')}
</select>
<input class="pr-institusi" type="text" placeholder="Nama sekolah/kampus" value="${data.nama_institusi||''}">
<input class="pr-masuk" type="number" placeholder="2010" min="1950" max="2030" value="${data.tahun_masuk||''}">
<input class="pr-lulus" type="number" placeholder="2013" min="1950" max="2035" value="${data.tahun_lulus||''}">
<select class="pr-status">
${['lulus','sedang_berjalan','putus_sekolah'].map(s =>
`<option value="${s}"${data.status_pendidikan===s?' selected':''}>${s==='sedang_berjalan'?'Sedang Berjalan':s==='putus_sekolah'?'Putus Sekolah':'Lulus'}</option>`).join('')}
</select>
<button type="button" class="del-pend" onclick="this.closest('.pend-row-item').remove()">✕</button>`;
container.appendChild(row);
}
// ═══════════════ RADIUS SLIDER ═══════════════
// Dipanggil saat slider digeser — update peta & label secara live
function onRadiusSlide(ibadahId, lat, lng, val) {
val = parseInt(val);
// Update label
const rv = document.getElementById('rv-' + ibadahId);
if (rv) rv.textContent = val + ' m';
// Update gradient slider
const sl = document.getElementById('rs-' + ibadahId);
if (sl) {
const pct = (((val - 50) / (2000 - 50)) * 100).toFixed(1);
sl.style.setProperty('--slider-pct', pct + '%');
}
// Update lingkaran di peta
if (RADIUS_CIRCLES[ibadahId]) {
RADIUS_CIRCLES[ibadahId].setRadius(val);
}
}
// Dipanggil saat slider dilepas — simpan ke server
let _radiusSaveTimer = {};
async function saveRadius(ibadahId, val) {
clearTimeout(_radiusSaveTimer[ibadahId]);
_radiusSaveTimer[ibadahId] = setTimeout(async () => {
// Ambil data ibadah terbaru
const ri = await api(`rumah_ibadah.php?action=get&id=${ibadahId}`).catch(() => null);
if (!ri || ri.error) { toast('Gagal ambil data ibadah', 'error'); return; }
const payload = {
id: ibadahId, nama: ri.nama, jenis: ri.jenis, kontak: ri.kontak || '',
email: ri.email || '', latitude: ri.latitude, longitude: ri.longitude,
alamat: ri.alamat || '', kelurahan: ri.kelurahan || '', kecamatan: ri.kecamatan || '',
radius: parseInt(val)
};
const d = await fetch('../api/rumah_ibadah.php', {
method: 'PUT', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(r => r.json()).catch(() => ({ success: false, error: 'Koneksi gagal' }));
if (d.success) toast(`Radius ${ri.nama} diperbarui → ${val} m ✅`, 'success');
else toast(d.error || 'Gagal simpan radius', 'error');
}, 500);
}
// ═══════════════ CHAT WARGA ═══════════════
let activeChatUserId = null;
let chatPollTimer = null;
async function loadChatConversations() {
const data = await api('chat.php?action=conversations').catch(() => []);
const el = document.getElementById('chat-conv-list');
const badge = document.getElementById('badge-chat');
const totalUnread = (data || []).reduce((s, c) => s + (parseInt(c.unread) || 0), 0);
if (badge) {
badge.textContent = totalUnread > 0 ? totalUnread : '';
badge.className = 'tab-badge' + (totalUnread > 0 ? ' show' : '');
}
if (!data || !data.length) {
el.innerHTML = '<div class="chat-empty" style="padding:30px"><div style="font-size:28px">🗨️</div><div style="font-size:11px">Belum ada percakapan</div></div>';
return;
}
el.innerHTML = data.map(c => `
<div class="chat-conv-item ${activeChatUserId == c.id_user_masyarakat ? 'active' : ''} ${parseInt(c.unread) > 0 ? 'unread' : ''}"
onclick="openChatWith(${c.id_user_masyarakat},'${escHtml(c.nama_masyarakat)}',this)">
<div class="chat-conv-name">
<span>👤 ${escHtml(c.nama_masyarakat)}</span>
${parseInt(c.unread) > 0 ? `<span class="chat-conv-badge">${c.unread}</span>` : ''}
</div>
<div class="chat-conv-last">💬 ${c.total_pesan} pesan · ${fTime(c.last_time)}</div>
</div>`).join('');
}
function escHtml(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function openChatWith(userId, namaUser, el) {
activeChatUserId = userId;
document.querySelectorAll('.chat-conv-item').forEach(i => i.classList.remove('active'));
if (el) el.classList.add('active');
const panel = document.getElementById('chat-msg-panel');
panel.innerHTML = `
<div class="chat-msg-hd">🗨️ Chat dengan ${escHtml(namaUser)}</div>
<div class="chat-messages" id="chat-messages-body">
<div class="chat-empty"><div style="font-size:24px">⏳</div><div>Memuat pesan...</div></div>
</div>
<div class="chat-input-row">
<textarea class="chat-input" id="chat-input-text" rows="2" placeholder="Tulis pesan..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChatMsg();}"></textarea>
<button class="chat-send-btn" onclick="sendChatMsg()">📤 Kirim</button>
</div>`;
loadChatMessages(userId);
clearInterval(chatPollTimer);
chatPollTimer = setInterval(() => loadChatMessages(userId, true), 5000);
}
async function loadChatMessages(userId, silent = false) {
const data = await api(`chat.php?action=messages&id_user_masyarakat=${userId}`).catch(() => []);
const el = document.getElementById('chat-messages-body');
if (!el) return;
if (!data || !data.length) {
if (!silent) el.innerHTML = '<div class="chat-empty"><div style="font-size:24px">💬</div><div>Belum ada pesan dalam percakapan ini</div></div>';
return;
}
const wasAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60;
el.innerHTML = data.map(m => {
const isMe = (m.pengirim_role === 'pengurus_ibadah');
return `<div class="chat-bubble-wrap ${isMe ? 'me' : 'them'}">
<div class="chat-bubble ${isMe ? 'me' : 'them'}">${escHtml(m.isi_pesan)}</div>
<div class="chat-bubble-meta">${isMe ? '🕌 Anda' : '👤 Warga'} · ${fTime(m.created_at)}</div>
</div>`;
}).join('');
if (wasAtBottom || !silent) el.scrollTop = el.scrollHeight;
if (!silent) loadChatConversations();
}
async function sendChatMsg() {
if (!activeChatUserId) return;
const inputEl = document.getElementById('chat-input-text');
const isi = (inputEl?.value || '').trim();
if (!isi) return;
inputEl.value = '';
inputEl.disabled = true;
const d = await fetch('../api/chat.php', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id_user_masyarakat: activeChatUserId, isi })
}).then(r => r.json()).catch(() => ({ success: false }));
inputEl.disabled = false;
inputEl.focus();
if (d.success) { loadChatMessages(activeChatUserId); }
else toast(d.error || 'Gagal mengirim pesan', 'error');
}
async function loadChatBadge() {
const d = await api('chat.php?action=unread').catch(() => ({ unread: 0 }));
const badge = document.getElementById('badge-chat');
if (badge) {
badge.textContent = d.unread > 0 ? d.unread : '';
badge.className = 'tab-badge' + (d.unread > 0 ? ' show' : '');
}
}
// ═══════════════ NOTIFIKASI ═══════════════
let notifOpen = false;
async function loadNotif() {
const d = await api('notifikasi.php?unread=1').catch(() => ({ notifikasi: [], unread: 0 }));
const dot = document.getElementById('notif-dot');
if (dot) dot.className = 'notif-dot' + (d.unread > 0 ? ' show' : '');
const el = document.getElementById('notif-list');
if (!d.notifikasi || !d.notifikasi.length) {
el.innerHTML = '<div class="empty-state" style="padding:20px"><div class="empty-icon">🔔</div><div class="empty-text">Tidak ada notifikasi baru</div></div>';
return;
}
el.innerHTML = d.notifikasi.map(n => `
<div class="notif-item ${n.is_read ? '' : 'unread'}" onclick="markRead(${n.id})">
<div class="notif-item-title">${n.judul}</div>
<div class="notif-item-body">${(n.isi || '').substring(0, 70)}</div>
<div class="notif-item-time">${fTime(n.created_at)}</div>
</div>`).join('');
}
function toggleNotif() {
notifOpen = !notifOpen;
document.getElementById('notif-panel').classList.toggle('open', notifOpen);
if (notifOpen) loadNotif();
}
async function markRead(id) { await fetch('../api/notifikasi.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }) }); loadNotif(); }
async function markAllRead() { await fetch('../api/notifikasi.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 0 }) }); loadNotif(); }
// ═══════════════ DROPDOWNS ═══════════════
async function loadDropdowns() {
const gj = await api('penduduk.php?action=geojson').catch(() => ({ features: [] }));
const kks = (gj.features || []).map(f => f.properties);
['b-kk', 'p-kk'].forEach(id => {
const sel = document.getElementById(id); if (!sel) return;
kks.forEach(p => sel.add(new Option(p.nama_kepala_keluarga, p.id)));
});
const kat = await api('bantuan.php?action=kategori').catch(() => []);
const selK = document.getElementById('b-kategori');
(kat || []).forEach(k => selK.add(new Option(`${k.nama} (${k.sumber})`, k.id)));
}
async function loadIbadahOpts() {
const data = await api('rumah_ibadah.php?action=list').catch(() => []);
['p-tujuan', 'b-mitra', 'f-bantuan-ibadah'].forEach(id => {
const sel = document.getElementById(id); if (!sel) return;
// Hapus opsi lama kecuali pertama
while (sel.options.length > 1) sel.remove(1);
(data || []).forEach(ri => {
const ico = ri.jenis === 'Masjid' ? '🕌' : ri.jenis === 'Gereja' ? '⛪' : ri.jenis === 'Vihara' ? '🏯' : '🛕';
sel.add(new Option(`${ico} ${ri.nama}`, ri.id));
});
});
}
function preKK(id, nama) {
const sel = document.getElementById('b-kk');
let opt = Array.from(sel.options).find(o => o.value == id);
if (!opt) { opt = new Option(nama, id); sel.add(opt); }
sel.value = id;
}
// ═══════════════ HELPERS ═══════════════
function api(path) { return fetch('../api/' + path).then(r => r.json()); }
function val(id) { const el = document.getElementById(id); return el ? el.value : ''; }
function setText(id, v) { const el = document.getElementById(id); if (el) el.textContent = v; }
function openM(id) { document.getElementById(id).classList.remove('hidden'); }
function closeM(id) { document.getElementById(id).classList.add('hidden'); }
function seBadge(se) {
const map = {
sangat_miskin: ['badge-red', '⚠️ Sangat Miskin'],
miskin: ['badge-amber', '🟠 Miskin'],
rentan: ['badge-purple', '🟡 Rentan'],
mampu: ['badge-green', '✅ Mampu']
};
const [cls, lbl] = map[se] || ['badge-gray', se || '-'];
return `<span class="badge ${cls}">${lbl}</span>`;
}
function svBadge(s) {
const map = { pending: 'badge-amber', diproses: 'badge-blue', disetujui: 'badge-green', ditolak: 'badge-red' };
return `<span class="badge ${map[s] || 'badge-gray'}">${s}</span>`;
}
function jenisLabel(j) {
const m = { kelahiran: '🍼 Kelahiran', kematian: '⚰️ Kematian', pindah_masuk: '📦 Pindah Masuk', pindah_keluar: '🚚 Pindah Keluar', pernikahan: '💒 Pernikahan', perubahan_data: '📝 Perubahan Data', lainnya: '📌 Lainnya' };
return m[j] || j;
}
function fDate(d) { if (!d) return '-'; return new Date(d).toLocaleDateString('id-ID', { day: 'numeric', month: 'short', year: 'numeric' }); }
function fTime(d) { if (!d) return '-'; return new Date(d).toLocaleString('id-ID', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }); }
function toast(msg, type = 'info') {
const el = document.getElementById('toast');
el.className = `toast toast-${type === 'success' ? 'success' : type === 'error' ? 'error' : 'info'}`;
el.textContent = msg;
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 3500);
}
// ═══════════════ INIT ═══════════════
initMap();
loadDashboard();
loadPengurusOverview();
loadDropdowns();
loadIbadahOpts();
loadNotif();
loadChatBadge();
// Sembunyikan filter kecamatan di awal (hanya muncul mode semua)
document.getElementById('f-p-kecamatan').style.display = 'none';
document.getElementById('th-ibadah').style.display = 'none';
setInterval(() => { loadNotif(); loadDashboard(); loadPengurusOverview(); loadChatBadge(); }, 30000);
// Tutup notif saat klik di luar
document.addEventListener('click', e => {
if (!e.target.closest('.notif-wrap')) {
notifOpen = false;
document.getElementById('notif-panel').classList.remove('open');
}
});
// ═══════════════ OVERVIEW ═══════════════
let pgOverviewScope = 'binaan';
async function loadPengurusOverview() {
try {
const scope = pgOverviewScope;
const endpoint = scope === 'semua' ? '../api/penduduk.php?action=dashboard' : '../api/penduduk.php?action=dashboard-ibadah';
const d = await fetch(endpoint).then(r => r.json());
document.getElementById('pg-kk').textContent = d.total_kk || 0;
document.getElementById('pg-jiwa').textContent = d.total_jiwa || 0;
document.getElementById('pg-sm').textContent = d.sangat_miskin || 0;
document.getElementById('pg-miskin').textContent = d.miskin || 0;
document.getElementById('pg-laporan').textContent = d.laporan_pending || 0;
document.getElementById('pg-bantuan').textContent = d.total_bantuan || 0;
if (d.status_counts) renderPGCharts(d.status_counts, d.laporan_status);
} catch(e) { console.error('Load pengurus overview error:', e); }
}
function renderPGCharts(statusCounts, laporanStatus) {
if (!statusCounts) return;
const labels = Object.keys(statusCounts);
const data = Object.values(statusCounts);
const colors = ['#ef4444','#f97316','#eab308'];
const bgColors = ['rgba(239,68,68,0.1)','rgba(249,115,22,0.1)','rgba(234,179,8,0.1)'];
if (window.pgChartDonut) window.pgChartDonut.destroy();
const ctx = document.getElementById('pg-chart-donut')?.getContext('2d');
if (ctx) {
window.pgChartDonut = new Chart(ctx, {
type: 'doughnut',
data: { labels, datasets: [{ data, backgroundColor: bgColors, borderColor: colors, borderWidth: 2 }] },
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: '#e2e8f0', font: { size: 11 } } } } }
});
}
if (laporanStatus) {
const lLabels = Object.keys(laporanStatus);
const lData = Object.values(laporanStatus);
const lColors = ['#3b82f6','#f59e0b','#ef4444','#10b981'];
if (window.pgChartLaporan) window.pgChartLaporan.destroy();
const lCtx = document.getElementById('pg-chart-laporan')?.getContext('2d');
if (lCtx) {
window.pgChartLaporan = new Chart(lCtx, {
type: 'bar',
data: {
labels: lLabels,
datasets: [{ label: 'Status Laporan', data: lData, backgroundColor: lColors, borderRadius: 6, borderSkipped: false }]
},
options: {
indexAxis: 'y', responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { x: { grid: { color: 'rgba(148,163,184,0.08)' } }, y: { grid: { display: false } } }
}
});
}
}
}
function setOverviewScope(scope, el) {
pgOverviewScope = scope;
document.querySelectorAll('.scope-btn').forEach(b => b.classList.remove('active'));
if (el) el.classList.add('active');
loadPengurusOverview();
}
</script>
</body>
</html>