2612 lines
120 KiB
PHP
2612 lines
120 KiB
PHP
<?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 🟠 Miskin 🟡 Rentan</div>
|
||
<div>🕌 Masjid ⛪ Gereja 🏯 Vihara 🛕 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, ''');
|
||
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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||
|
||
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>
|