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

1759 lines
88 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
require_once '../includes/config.php';
requireLogin(['masyarakat']);
$user = currentUser();
$uid = (int)$user['id'];
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Portal Masyarakat — WebGIS Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@300;400;500;600;700;800&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;--surface:#0f1522;--card:#141b2d;--card2:#1a2235;
--border:rgba(99,179,237,0.1);--accent:#3b82f6;--accent2:#06b6d4;
--green:#10b981;--red:#ef4444;--amber:#f59e0b;--purple:#8b5cf6;
--text:#e2e8f0;--text2:#7c93b5;--glow:rgba(59,130,246,0.2);
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
body{font-family:'Sora',sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column}
/* ── TOPBAR ── */
.topbar{
background:var(--surface);border-bottom:1px solid var(--border);
padding:0 18px;height:56px;display:flex;align-items:center;
justify-content:space-between;z-index:200;flex-shrink:0;
box-shadow:0 2px 20px rgba(0,0,0,0.4);
}
.tb-left{display:flex;align-items:center;gap:10px}
.logo{width:34px;height:34px;background:linear-gradient(135deg,var(--accent),var(--accent2));border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:16px}
.tb-title{font-size:14px;font-weight:800}
.tb-sub{font-size:10px;color:var(--text2)}
.tb-right{display:flex;align-items:center;gap:8px}
.notif-wrap{position:relative}
.notif-btn{width:34px;height:34px;border-radius:9px;background:rgba(255,255,255,0.05);border:1px solid var(--border);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;color:var(--text);transition:all 0.2s}
.notif-btn:hover{background:rgba(59,130,246,0.1);border-color:var(--accent)}
.nb-badge{position:absolute;top:-4px;right:-4px;background:var(--red);color:#fff;font-size:9px;font-weight:800;min-width:15px;height:15px;border-radius:8px;display:none;align-items:center;justify-content:center}
.user-chip{display:flex;align-items:center;gap:7px;background:rgba(255,255,255,0.04);border:1px solid var(--border);padding:5px 11px;border-radius:20px;font-size:11px;font-weight:600}
.user-av{width:22px;height:22px;background:linear-gradient(135deg,var(--green),var(--accent));border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px}
.logout{background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.2);color:var(--red);padding:5px 10px;border-radius:8px;font-family:'Sora',sans-serif;font-size:11px;font-weight:700;cursor:pointer;transition:all 0.2s}
.logout:hover{background:rgba(239,68,68,0.2)}
/* ── MAIN LAYOUT ── */
.main{display:flex;flex:1;overflow:hidden;height:calc(100vh - 56px);flex-direction:row}
/* ── SIDEBAR ── */
.sidebar{
width:340px;min-width:340px;background:var(--surface);
border-right:1px solid var(--border);
display:flex;flex-direction:column;overflow:hidden;
}
/* ── PRIMARY NAV (OVERVIEW / PETA) ── */
.primary-nav{
display:flex;gap:6px;padding:10px 12px 0;
background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;
}
.pnav-btn{
flex:1;padding:9px 8px;background:rgba(255,255,255,0.04);border:1px solid var(--border);
color:var(--text2);font-family:'Sora',sans-serif;font-size:11px;font-weight:800;
cursor:pointer;border-radius:10px 10px 0 0;transition:all 0.2s;letter-spacing:0.3px;
display:flex;align-items:center;justify-content:center;gap:6px;
}
.pnav-btn.active{
background:rgba(59,130,246,0.12);border-color:var(--accent);color:var(--accent);
border-bottom-color:var(--surface);box-shadow:0 -2px 12px rgba(59,130,246,0.15);
}
.pnav-btn:hover:not(.active){color:var(--text);background:rgba(255,255,255,0.07)}
/* ── SECONDARY NAV (per-panel sub-tabs) ── */
.sec-nav{
display:flex;gap:0;border-bottom:1px solid var(--border);flex-shrink:0;
background:rgba(255,255,255,0.015);
}
.snav-btn{
flex:1;padding:8px 6px;background:none;border:none;border-bottom:2px solid transparent;
color:var(--text2);font-family:'Sora',sans-serif;font-size:10px;font-weight:800;
cursor:pointer;transition:all 0.18s;letter-spacing:0.4px;
display:flex;align-items:center;justify-content:center;gap:4px;
}
.snav-btn.active{color:var(--accent);border-bottom-color:var(--accent)}
.snav-btn:hover:not(.active){color:var(--text);background:rgba(255,255,255,0.02)}
/* ── PANELS ── */
.s-panel{display:none;flex:1;overflow-y:auto;flex-direction:column}
.s-panel.active{display:flex}
/* sub-panels inside a primary panel */
.sub-panel{display:none;flex:1;flex-direction:column;overflow-y:auto}
.sub-panel.active{display:flex}
/* ── PANEL HEADER ── */
.ph{padding:13px 15px 10px;border-bottom:1px solid var(--border);flex-shrink:0}
.ph-title{font-size:13px;font-weight:800}
.ph-sub{font-size:10px;color:var(--text2);margin-top:2px}
/* ── STAT GRID ── */
.sg{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:10px 12px}
.sc{background:var(--card2);border:1px solid var(--border);border-radius:11px;padding:11px 12px}
.sc-n{font-size:22px;font-weight:800;line-height:1;margin-bottom:3px}
.sc-l{font-size:9px;color:var(--text2);font-weight:700;text-transform:uppercase;letter-spacing:0.5px}
.c-b .sc-n{color:var(--accent)}.c-g .sc-n{color:var(--green)}.c-a .sc-n{color:var(--amber)}.c-p .sc-n{color:var(--purple)}
/* ── FILTER ── */
.filter-box{padding:10px 13px;border-bottom:1px solid var(--border);flex-shrink:0}
.filter-box label{font-size:10px;font-weight:800;color:var(--text2);display:block;margin-bottom:5px;letter-spacing:0.4px;text-transform:uppercase}
.filter-box select{
width:100%;background:rgba(255,255,255,0.04);border:1px solid var(--border);
color:var(--text);padding:7px 10px;border-radius:9px;
font-family:'Sora',sans-serif;font-size:11px;outline:none;margin-bottom:8px;
}
.filter-box select:focus{border-color:var(--accent)}
.fb-row{display:flex;gap:6px}
.fb{
flex:1;padding:6px 4px;background:rgba(255,255,255,0.04);border:1px solid var(--border);
color:var(--text2);border-radius:8px;cursor:pointer;
font-family:'Sora',sans-serif;font-size:10px;font-weight:700;transition:all 0.2s;
}
.fb:hover,.fb.on{background:rgba(59,130,246,0.1);border-color:var(--accent);color:var(--accent)}
/* ── LEGENDA ── */
.legenda-box{padding:10px 13px;border-top:1px solid var(--border);flex-shrink:0}
.leg-t{font-size:9px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;margin-bottom:6px}
.leg-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px}
.leg-item{font-size:10px;color:var(--text2);display:flex;align-items:center;gap:4px}
.leg-note{margin-top:6px;padding:6px 8px;background:rgba(59,130,246,0.06);border:1px solid rgba(59,130,246,0.15);border-radius:7px;font-size:10px;color:rgba(147,197,253,0.8);line-height:1.5}
/* ── KK LIST ── */
.kk-list{display:flex;flex-direction:column;gap:6px;padding:8px 10px}
.kk-item{
background:var(--card2);border:1px solid var(--border);
border-radius:10px;padding:10px 11px;cursor:pointer;transition:all 0.2s;
}
.kk-item:hover{border-color:var(--accent);background:rgba(59,130,246,0.05)}
.kk-top{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:4px}
.kk-nama{font-size:12px;font-weight:700;flex:1}
.kk-meta{font-size:10px;color:var(--text2);display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
.kk-hint{font-size:10px;color:var(--accent);margin-top:4px}
/* ── BADGES ── */
.badge{padding:2px 7px;border-radius:5px;font-size:9px;font-weight:800;white-space:nowrap}
.bb{background:rgba(59,130,246,.15);color:#93c5fd}
.bg{background:rgba(16,185,129,.15);color:#6ee7b7}
.ba{background:rgba(245,158,11,.15);color:#fcd34d}
.br{background:rgba(239,68,68,.15);color:#fca5a5}
.bp{background:rgba(139,92,246,.15);color:#c4b5fd}
.bgr{background:rgba(255,255,255,.07);color:var(--text2)}
/* ── ANGGOTA ── */
.ak-card{background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:9px;padding:10px;margin-bottom:7px}
.ak-row{display:flex;gap:9px;align-items:flex-start}
.ak-av{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0}
.ak-nama{font-size:12px;font-weight:700}
.ak-meta{font-size:10px;color:var(--text2);margin-top:2px}
.ak-pend{font-size:10px;color:var(--accent);margin-top:3px}
/* ── BANTUAN CARD ── */
.bc{background:rgba(16,185,129,.05);border:1px solid rgba(16,185,129,.15);border-radius:9px;padding:10px;margin-bottom:7px}
.bc-top{display:flex;justify-content:space-between;margin-bottom:4px}
.bc-jenis{font-size:12px;font-weight:700;color:var(--green)}
.bc-tgl{font-size:10px;color:var(--text2)}
.bc-detail{font-size:11px;color:var(--text2);line-height:1.5}
.bc-src{margin-top:5px;padding:4px 8px;background:rgba(59,130,246,.06);border:1px solid var(--border);border-radius:6px;font-size:10px;color:var(--text2)}
/* ── MAP ── */
.map-wrap{flex:1;position:relative;min-width:0}
#map{position:absolute;top:0;left:0;width:100%;height:100%}
/* ── MAP FAB ── */
.fab-wrap{position:absolute;bottom:24px;right:14px;z-index:400;display:flex;flex-direction:column;gap:8px}
.fab{
width:44px;height:44px;border-radius:13px;border:none;cursor:pointer;
display:flex;align-items:center;justify-content:center;font-size:20px;
box-shadow:0 4px 16px rgba(0,0,0,0.4);transition:all 0.2s;
}
.fab:hover{transform:scale(1.1)}
.fab-loc{background:linear-gradient(135deg,var(--accent),var(--accent2))}
.fab-reset{background:rgba(255,255,255,0.1);backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,0.15)}
/* ── NOTIF PANEL ── */
.np{
position:absolute;top:44px;right:0;width:290px;
background:var(--card);border:1px solid var(--border);
border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.5);
z-index:200;overflow:hidden;animation:dropIn .2s;
}
@keyframes dropIn{from{opacity:0;transform:translateY(-8px)}}
.np-hd{padding:10px 13px;font-size:12px;font-weight:800;border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center}
.ni{padding:9px 13px;border-bottom:1px solid var(--border);cursor:pointer;transition:background .15s}
.ni:hover{background:rgba(59,130,246,0.06)}
.ni.unread{background:rgba(59,130,246,0.04)}
.ni-t{font-size:11px;font-weight:700;margin-bottom:2px}
.ni-i{font-size:10px;color:var(--text2);line-height:1.4}
.ni-d{font-size:9px;color:var(--text2);margin-top:3px}
/* ── MODAL ── */
.mo{position:fixed;inset:0;background:rgba(0,0,0,.78);z-index:600;display:flex;align-items:flex-end;justify-content:center;backdrop-filter:blur(4px);animation:fadeIn .2s}
@keyframes fadeIn{from{opacity:0}}
.mo.hidden{display:none}
.md{background:var(--card);border:1px solid var(--border);border-radius:20px 20px 0 0;width:100%;max-width:600px;max-height:92vh;overflow-y:auto;box-shadow:0 -8px 40px rgba(0,0,0,.6);animation:slideUp .3s cubic-bezier(.23,1,.32,1)}
@keyframes slideUp{from{transform:translateY(30px);opacity:0}}
.mh{padding:16px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;background:var(--card);z-index:2}
.mh-drag{width:40px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;margin:0 auto 14px}
.mt-m{font-size:15px;font-weight:800}
.mc{background:rgba(255,255,255,.05);border:none;color:var(--text2);width:28px;height:28px;border-radius:8px;cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:all .2s}
.mc:hover{background:rgba(239,68,68,.2);color:var(--red)}
.mb-m{padding:16px 20px}
/* ── LAPORAN FORM ── */
.lap-section{margin-bottom:14px}
.fl{margin-bottom:11px}
.fl label{display:block;font-size:10px;font-weight:800;color:var(--text2);margin-bottom:5px;letter-spacing:0.4px;text-transform:uppercase}
.fl select,.fl input,.fl textarea{
width:100%;background:rgba(255,255,255,.05);border:1px solid var(--border);
color:var(--text);padding:9px 11px;border-radius:9px;
font-family:'Sora',sans-serif;font-size:12px;transition:all .2s;outline:none;
}
.fl select:focus,.fl input:focus,.fl textarea:focus{
border-color:var(--accent);background:rgba(59,130,246,.05);box-shadow:0 0 0 3px var(--glow);
}
.fl select option{background:var(--card)}
.fl textarea{resize:vertical;min-height:80px}
.kk-preview{
background:rgba(59,130,246,.07);border:1px solid rgba(59,130,246,.2);
border-radius:10px;padding:11px 13px;margin-bottom:14px;
}
.kk-preview-title{font-size:10px;font-weight:800;color:var(--text2);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:5px}
.kk-preview-nama{font-size:13px;font-weight:800;margin-bottom:3px}
.kk-preview-meta{font-size:11px;color:var(--text2)}
.info-tip{
padding:9px 12px;background:rgba(245,158,11,.07);border:1px solid rgba(245,158,11,.2);
border-radius:9px;font-size:10px;color:rgba(253,211,77,.85);line-height:1.5;margin-bottom:14px;
}
/* ── BUTTONS ── */
.btn{padding:9px 14px;border-radius:9px;border:none;font-family:'Sora',sans-serif;font-size:12px;font-weight:700;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:5px}
.btn-p{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 4px 12px var(--glow)}
.btn-p:hover{transform:translateY(-1px);box-shadow:0 8px 20px var(--glow)}
.btn-p:disabled{opacity:.6;cursor:not-allowed;transform:none}
.btn-g{background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text2)}
.btn-full{width:100%;justify-content:center}
/* ── SPINNER ── */
.spinner{width:14px;height:14px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
/* ── RIWAYAT LAPORAN ── */
.lap-item{background:var(--card2);border:1px solid var(--border);border-radius:10px;padding:10px 12px;margin-bottom:7px}
.lap-top{display:flex;align-items:center;justify-content:space-between;margin-bottom:5px}
.lap-nama{font-size:12px;font-weight:700}
.lap-meta{font-size:10px;color:var(--text2)}
.lap-desk{font-size:11px;color:var(--text2);margin-top:4px;line-height:1.4}
.lap-catatan{margin-top:6px;padding:5px 8px;background:rgba(16,185,129,.08);border:1px solid rgba(16,185,129,.2);border-radius:7px;font-size:10px;color:var(--green)}
/* ── TOAST ── */
.toast{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);z-index:9999;padding:11px 18px;border-radius:11px;font-size:12px;font-weight:700;pointer-events:none;animation:ti .3s cubic-bezier(.23,1,.32,1);white-space:nowrap}
@keyframes ti{from{opacity:0;transform:translateX(-50%) translateY(12px)}}
.ts{background:#065f46;border:1px solid var(--green);color:#6ee7b7}
.te{background:#7f1d1d;border:1px solid var(--red);color:#fca5a5}
.ti-c{background:#1e3a5f;border:1px solid var(--accent);color:#93c5fd}
/* ── EMPTY ── */
.empty{text-align:center;padding:28px 16px;color:var(--text2)}
.ei{font-size:32px;margin-bottom:8px}
.ep{font-size:12px}
/* ── SECTION DIV ── */
.sdiv{font-size:9px;font-weight:800;color:var(--text2);letter-spacing:1px;text-transform:uppercase;padding:8px 13px 5px;border-bottom:1px solid var(--border);flex-shrink:0}
/* ── OVERVIEW PAGE (redesigned) ── */
.overview{width:100%;overflow-y:auto;padding:22px 24px 32px;display:flex;flex-direction:column;gap:22px}
/* greeting banner */
.ov-greeting{background:linear-gradient(135deg,rgba(59,130,246,0.12) 0%,rgba(6,182,212,0.08) 50%,rgba(139,92,246,0.06) 100%);border:1px solid rgba(99,179,237,0.14);border-radius:18px;padding:20px 24px;display:flex;align-items:center;gap:18px;position:relative;overflow:hidden}
.ov-greeting::before{content:'';position:absolute;top:-40px;right:-40px;width:180px;height:180px;background:radial-gradient(circle,rgba(59,130,246,0.1) 0%,transparent 70%);pointer-events:none}
.ov-greeting-icon{font-size:40px;filter:drop-shadow(0 0 12px rgba(59,130,246,0.5));flex-shrink:0}
.ov-greeting-text h2{font-size:17px;font-weight:800;line-height:1.2;margin-bottom:4px}
.ov-greeting-text p{font-size:11px;color:var(--text2);line-height:1.6}
.ov-greeting-time{margin-left:auto;text-align:right;flex-shrink:0}
.ov-greeting-time .time-big{font-size:28px;font-weight:800;letter-spacing:-1px;background:linear-gradient(135deg,var(--accent),var(--accent2));-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
.ov-greeting-time .time-date{font-size:10px;color:var(--text2);margin-top:1px}
/* KPI grid */
.kpi-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
.kpi2{background:var(--card2);border:1px solid var(--border);border-radius:16px;padding:18px 16px 14px;position:relative;overflow:hidden;cursor:default;transition:transform 0.18s,border-color 0.18s,box-shadow 0.18s}
.kpi2:hover{transform:translateY(-3px);border-color:var(--kpi-c,var(--accent));box-shadow:0 8px 28px rgba(0,0,0,0.25)}
.kpi2-glow{position:absolute;inset:0;border-radius:16px;opacity:0;transition:opacity 0.18s;background:radial-gradient(ellipse at 20% 30%,var(--kpi-c,var(--accent)) 0%,transparent 65%);opacity:0.06;pointer-events:none}
.kpi2-stripe{position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--kpi-c,var(--accent)),transparent);border-radius:16px 16px 0 0}
.kpi2-head{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:10px}
.kpi2-icon{width:36px;height:36px;border-radius:10px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;font-size:18px}
.kpi2-trend{font-size:9px;font-weight:800;padding:3px 7px;border-radius:20px}
.kpi2-trend.up{background:rgba(16,185,129,0.12);color:#34d399}
.kpi2-trend.down{background:rgba(239,68,68,0.12);color:#f87171}
.kpi2-trend.neutral{background:rgba(124,147,181,0.12);color:var(--text2)}
.kpi2-num{font-size:30px;font-weight:900;line-height:1;letter-spacing:-1px;color:var(--kpi-c,var(--text));margin-bottom:5px}
.kpi2-label{font-size:10px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:0.7px;margin-bottom:3px}
.kpi2-bar{height:3px;border-radius:2px;background:rgba(255,255,255,0.05);margin-top:10px;overflow:hidden}
.kpi2-bar-fill{height:100%;border-radius:2px;background:linear-gradient(90deg,var(--kpi-c,var(--accent)),transparent);transition:width 1s cubic-bezier(.4,0,.2,1)}
/* section header */
.ov-sec-hd{display:flex;align-items:center;justify-content:space-between;margin-bottom:2px}
.ov-sec-title{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:0.8px;color:var(--text2);display:flex;align-items:center;gap:7px}
.ov-sec-title::before{content:'';width:3px;height:14px;background:var(--accent);border-radius:2px;display:inline-block}
.ov-sec-pill{font-size:9px;padding:3px 9px;background:rgba(99,179,237,0.08);border:1px solid rgba(99,179,237,0.15);border-radius:20px;color:var(--text2)}
/* chart cards */
.ov-charts{display:grid;grid-template-columns:1.6fr 1fr;gap:14px}
.ov-ccard{background:var(--card2);border:1px solid var(--border);border-radius:16px;padding:18px 20px;overflow:hidden}
.ov-ccard-title{font-size:13px;font-weight:800;margin-bottom:2px}
.ov-ccard-sub{font-size:10px;color:var(--text2);margin-bottom:14px}
/* status breakdown custom */
.status-list{display:flex;flex-direction:column;gap:10px}
.sl-row{display:flex;align-items:center;gap:10px}
.sl-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}
.sl-name{font-size:11px;font-weight:700;flex:1}
.sl-bar-track{flex:2;height:6px;background:rgba(255,255,255,0.05);border-radius:3px;overflow:hidden}
.sl-bar-fill{height:100%;border-radius:3px;transition:width 1s cubic-bezier(.4,0,.2,1)}
.sl-val{font-size:11px;font-weight:800;min-width:32px;text-align:right}
.sl-pct{font-size:9px;color:var(--text2);min-width:30px;text-align:right}
/* activity feed */
.ov-activity{background:var(--card2);border:1px solid var(--border);border-radius:16px;padding:18px 20px}
.act-list{display:flex;flex-direction:column;gap:0}
.act-item{display:flex;align-items:flex-start;gap:12px;padding:11px 0;border-bottom:1px solid rgba(255,255,255,0.04)}
.act-item:last-child{border-bottom:none;padding-bottom:0}
.act-icon-wrap{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
.act-body{flex:1;min-width:0}
.act-title{font-size:11px;font-weight:700;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.act-sub{font-size:10px;color:var(--text2)}
.act-time{font-size:9px;color:var(--text2);flex-shrink:0;margin-top:2px}
/* ibadah mini-cards */
.ov-ibadah-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px}
.ov-icard{background:rgba(59,130,246,0.04);border:1px solid rgba(59,130,246,0.1);border-radius:12px;padding:12px 14px;display:flex;align-items:center;gap:10px;transition:background 0.15s,border-color 0.15s;cursor:default}
.ov-icard:hover{background:rgba(59,130,246,0.08);border-color:rgba(59,130,246,0.2)}
.ov-icard-icon{font-size:22px;flex-shrink:0}
.ov-icard-body{flex:1;min-width:0}
.ov-icard-name{font-size:11px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-bottom:1px}
.ov-icard-kk{font-size:10px;color:var(--text2)}
/* bottom row: 3 cols */
.ov-bottom{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media(max-width:768px){
.sidebar{width:100%;min-width:100%;position:absolute;bottom:0;z-index:50;height:55vh;border-right:none;border-top:1px solid var(--border)}
.map-wrap{height:45vh}
.md{border-radius:16px 16px 0 0}
}
/* ── CHAT ── */
.chat-panel-wrap{display:flex;flex-direction:column;flex:1;overflow:hidden}
.chat-ibadah-sel{padding:10px 12px;border-bottom:1px solid var(--border);flex-shrink:0}
.chat-ibadah-sel select{width:100%;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;outline:none;margin-bottom:6px}
.chat-ibadah-sel select:focus{border-color:var(--accent)}
.chat-msgs{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px}
.ch-bw{display:flex;flex-direction:column}
.ch-bw.me{align-items:flex-end}
.ch-bw.them{align-items:flex-start}
.ch-b{max-width:78%;padding:9px 13px;border-radius:12px;font-size:11px;line-height:1.6;word-break:break-word}
.ch-b.me{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;border-bottom-right-radius:3px}
.ch-b.them{background:var(--card2);border:1px solid var(--border);border-bottom-left-radius:3px}
.ch-meta{font-size:9px;color:var(--text2);margin-top:2px}
.chat-input-row{padding:10px 12px;border-top:1px solid var(--border);display:flex;gap:7px;flex-shrink:0}
.chat-inp{flex:1;background:rgba(255,255,255,0.05);border:1px solid var(--border);color:var(--text);padding:9px 12px;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;outline:none;resize:none}
.chat-inp:focus{border-color:var(--accent)}
.chat-sbtn{padding:9px 14px;background:var(--accent);color:#fff;border:none;border-radius:9px;cursor:pointer;font-weight:700;font-size:11px;font-family:'Sora',sans-serif;transition:all 0.2s;flex-shrink:0}
.chat-sbtn:hover{background:var(--accent2)}
.chat-badge{display:inline-flex;align-items:center;justify-content:center;background:var(--red);color:#fff;font-size:9px;font-weight:800;padding:1px 5px;border-radius:7px;min-width:15px;margin-left:4px}
/* ── CHAT MODAL ── */
.chat-modal-overlay{
position:fixed;inset:0;background:rgba(0,0,0,0.72);z-index:700;
display:flex;align-items:flex-end;justify-content:center;
backdrop-filter:blur(4px);
transition:opacity 0.2s, visibility 0.2s;
}
.chat-modal-overlay.hidden{
opacity:0;visibility:hidden;pointer-events:none;
}
.chat-modal{
background:var(--card);border:1px solid var(--border);
border-radius:20px 20px 0 0;width:100%;max-width:520px;
height:72vh;display:flex;flex-direction:column;
box-shadow:0 -8px 40px rgba(0,0,0,.6);
transform:translateY(0);transition:transform 0.3s cubic-bezier(.23,1,.32,1);
}
.chat-modal-overlay.hidden .chat-modal{
transform:translateY(100%);
}
.chat-modal-hd{
padding:14px 18px 12px;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:10px;flex-shrink:0;
}
.chat-modal-drag{width:36px;height:4px;background:rgba(255,255,255,0.15);border-radius:2px;position:absolute;top:10px;left:50%;transform:translateX(-50%)}
.chat-modal-icon{font-size:24px;flex-shrink:0}
.chat-modal-info{flex:1;min-width:0}
.chat-modal-name{font-size:13px;font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.chat-modal-sub{font-size:10px;color:var(--text2);margin-top:1px}
.chat-modal-close{
width:30px;height:30px;background:rgba(255,255,255,0.05);border:1px solid var(--border);
color:var(--text2);border-radius:8px;cursor:pointer;font-size:14px;
display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;
}
.chat-modal-close:hover{background:rgba(239,68,68,.2);color:var(--red);border-color:rgba(239,68,68,.3)}
.chat-modal-msgs{
flex:1;overflow-y:auto;padding:12px 16px;display:flex;flex-direction:column;gap:8px;
}
.chat-modal-input{
padding:10px 14px;border-top:1px solid var(--border);display:flex;gap:8px;flex-shrink:0;
}
.chat-modal-inp{
flex:1;background:rgba(255,255,255,0.05);border:1px solid var(--border);
color:var(--text);padding:9px 12px;border-radius:10px;
font-family:'Sora',sans-serif;font-size:11px;outline:none;resize:none;
}
.chat-modal-inp:focus{border-color:var(--accent)}
.chat-modal-send{
padding:9px 14px;background:var(--accent);color:#fff;border:none;
border-radius:10px;cursor:pointer;font-size:14px;transition:all 0.2s;flex-shrink:0;
}
.chat-modal-send:hover{background:var(--accent2)}
/* badge on ibadah card */
.ov-icard-chat{
width:26px;height:26px;background:rgba(59,130,246,0.12);border:1px solid rgba(59,130,246,0.2);
border-radius:8px;display:flex;align-items:center;justify-content:center;
font-size:13px;flex-shrink:0;cursor:pointer;transition:all 0.15s;
}
.ov-icard-chat:hover{background:rgba(59,130,246,0.25);border-color:var(--accent)}
.ov-icard{cursor:pointer}
</style>
<!-- ── TOPBAR ── -->
<div class="topbar">
<div class="tb-left">
<div class="logo">🗺️</div>
<div>
<div class="tb-title">Portal Masyarakat</div>
<div class="tb-sub">WebGIS Kota Pontianak</div>
</div>
</div>
<div class="tb-right">
<div class="notif-wrap">
<button class="notif-btn" onclick="toggleNotif(this)" id="nb-btn">🔔
<div class="nb-badge" id="nb-badge"></div>
</button>
<div id="np" class="np" style="display:none">
<div class="np-hd">🔔 Notifikasi
<button onclick="bacaSemua()" style="background:none;border:none;color:var(--accent);font-size:10px;cursor:pointer;font-family:inherit;font-weight:700">Baca semua</button>
</div>
<div id="np-list"></div>
</div>
</div>
<div class="user-chip">
<div class="user-av">👤</div>
<span><?= htmlspecialchars($user['nama']) ?></span>
</div>
<button class="logout" onclick="location.href='../api/auth.php?action=logout'">Keluar</button>
</div>
</div>
<!-- ── MAIN ── -->
<div class="main">
<!-- ── SIDEBAR ── -->
<div class="sidebar">
<!-- ══ PRIMARY NAVBAR ══ -->
<div class="primary-nav">
<button class="pnav-btn active" id="pnav-overview" onclick="switchPrimary('overview')">
📊 OVERVIEW
</button>
<button class="pnav-btn" id="pnav-peta" onclick="switchPrimary('peta')">
🗺️ PETA
</button>
<button class="pnav-btn" id="pnav-bantuan" onclick="switchPrimary('bantuan')">
🤝 BANTUAN
</button>
<button class="pnav-btn" id="pnav-riwayat" onclick="switchPrimary('riwayat')">
📋 LAPORAN
</button>
</div>
<!-- ══ PANEL: OVERVIEW ══ -->
<div class="s-panel active" id="tab-overview" style="overflow:auto">
<!-- Secondary navbar for overview -->
<div class="sec-nav">
<button class="snav-btn active" id="ovnav-stat" onclick="switchOverviewSub('stat')">📈 Statistik</button>
<button class="snav-btn" id="ovnav-ibadah" onclick="switchOverviewSub('ibadah')">🕌 Ibadah</button>
<button class="snav-btn" id="ovnav-aktivitas" onclick="switchOverviewSub('aktivitas')">📋 Aktivitas</button>
</div>
<!-- Sub-panel: Statistik -->
<div class="sub-panel active" id="ovpanel-stat">
<div class="overview">
<!-- Greeting Banner -->
<div class="ov-greeting">
<div class="ov-greeting-icon" id="ov-greeting-icon">🌤️</div>
<div class="ov-greeting-text">
<h2 id="ov-greeting-title">Selamat datang</h2>
<p id="ov-greeting-sub">Portal Masyarakat · WebGIS Kota Pontianak</p>
</div>
<div class="ov-greeting-time">
<div class="time-big" id="ov-clock">--:--</div>
<div class="time-date" id="ov-date">--</div>
</div>
</div>
<!-- KPI Grid -->
<div>
<div class="ov-sec-hd" style="margin-bottom:12px">
<div class="ov-sec-title">Statistik Utama</div>
<div class="ov-sec-pill" id="ov-last-update">Memuat...</div>
</div>
<div class="kpi-grid">
<div class="kpi2" style="--kpi-c:var(--accent)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">🏘️</div><div class="kpi2-trend neutral" id="kpi-trend-kk">KK</div></div>
<div class="kpi2-num" id="ov-kk">—</div>
<div class="kpi2-label">Kepala Keluarga</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" id="bar-kk" style="width:0%"></div></div>
</div>
<div class="kpi2" style="--kpi-c:var(--accent2)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">👥</div><div class="kpi2-trend neutral" id="kpi-trend-jiwa">Jiwa</div></div>
<div class="kpi2-num" id="ov-jiwa">—</div>
<div class="kpi2-label">Total Jiwa</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" style="width:80%;background:linear-gradient(90deg,var(--accent2),transparent)"></div></div>
</div>
<div class="kpi2" style="--kpi-c:var(--green)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">🤝</div><div class="kpi2-trend up">Aktif</div></div>
<div class="kpi2-num" id="ov-bantuan">—</div>
<div class="kpi2-label">Bantuan Disalurkan</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" style="width:55%;background:linear-gradient(90deg,var(--green),transparent)"></div></div>
</div>
<div class="kpi2" style="--kpi-c:var(--red)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">⚠️</div><div class="kpi2-trend down">Prioritas</div></div>
<div class="kpi2-num" id="ov-sm">—</div>
<div class="kpi2-label">Sangat Miskin</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" id="bar-sm" style="width:0%;background:linear-gradient(90deg,var(--red),transparent)"></div></div>
</div>
<div class="kpi2" style="--kpi-c:var(--amber)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">🟠</div><div class="kpi2-trend neutral">Pantau</div></div>
<div class="kpi2-num" id="ov-miskin">—</div>
<div class="kpi2-label">Kategori Miskin</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" id="bar-mis" style="width:0%;background:linear-gradient(90deg,var(--amber),transparent)"></div></div>
</div>
<div class="kpi2" style="--kpi-c:var(--purple)">
<div class="kpi2-glow"></div><div class="kpi2-stripe"></div>
<div class="kpi2-head"><div class="kpi2-icon">🕌</div><div class="kpi2-trend up">Aktif</div></div>
<div class="kpi2-num" id="ov-ibadah">—</div>
<div class="kpi2-label">Rumah Ibadah</div>
<div class="kpi2-bar"><div class="kpi2-bar-fill" style="width:70%;background:linear-gradient(90deg,var(--purple),transparent)"></div></div>
</div>
</div>
</div>
<!-- Charts -->
<div>
<div class="ov-sec-hd" style="margin-bottom:12px">
<div class="ov-sec-title">Distribusi Status Ekonomi</div>
</div>
<div class="ov-charts">
<div class="ov-ccard">
<div class="ov-ccard-title">📊 KK per Kecamatan</div>
<div class="ov-ccard-sub">Jumlah kepala keluarga berdasarkan wilayah</div>
<div style="position:relative;height:168px"><canvas id="ov-chart-kecamatan"></canvas></div>
</div>
<div class="ov-ccard">
<div class="ov-ccard-title">📉 Tingkat Kemiskinan</div>
<div class="ov-ccard-sub">Komposisi status ekonomi warga</div>
<div class="status-list" id="ov-status-list" style="margin-top:4px">
<div class="sl-row">
<div class="sl-dot" style="background:var(--red)"></div>
<div class="sl-name">Sangat Miskin</div>
<div class="sl-bar-track"><div class="sl-bar-fill" id="slb-sm" style="width:0%;background:var(--red)"></div></div>
<div class="sl-val" id="slv-sm">—</div>
<div class="sl-pct" id="slp-sm">—</div>
</div>
<div class="sl-row">
<div class="sl-dot" style="background:var(--amber)"></div>
<div class="sl-name">Miskin</div>
<div class="sl-bar-track"><div class="sl-bar-fill" id="slb-mis" style="width:0%;background:var(--amber)"></div></div>
<div class="sl-val" id="slv-mis">—</div>
<div class="sl-pct" id="slp-mis">—</div>
</div>
<div class="sl-row">
<div class="sl-dot" style="background:var(--accent)"></div>
<div class="sl-name">Hampir Miskin</div>
<div class="sl-bar-track"><div class="sl-bar-fill" id="slb-hm" style="width:0%;background:var(--accent)"></div></div>
<div class="sl-val" id="slv-hm">—</div>
<div class="sl-pct" id="slp-hm">—</div>
</div>
<div class="sl-row">
<div class="sl-dot" style="background:var(--green)"></div>
<div class="sl-name">Tidak Miskin</div>
<div class="sl-bar-track"><div class="sl-bar-fill" id="slb-tm" style="width:0%;background:var(--green)"></div></div>
<div class="sl-val" id="slv-tm">—</div>
<div class="sl-pct" id="slp-tm">—</div>
</div>
</div>
<div style="margin-top:18px;position:relative;height:110px"><canvas id="ov-chart-donut"></canvas></div>
</div>
</div>
</div>
</div>
</div><!-- /ovpanel-stat -->
<!-- Sub-panel: Rumah Ibadah -->
<div class="sub-panel" id="ovpanel-ibadah">
<div class="overview">
<div class="ov-sec-hd" style="margin-bottom:12px">
<div class="ov-sec-title">Rumah Ibadah Terdekat</div>
</div>
<div class="ov-ibadah-grid" id="ov-ibadah-list">
<div style="grid-column:span 2;text-align:center;padding:20px;color:var(--text2);font-size:11px">Memuat data...</div>
</div>
</div>
</div><!-- /ovpanel-ibadah -->
<!-- Sub-panel: Aktivitas -->
<div class="sub-panel" id="ovpanel-aktivitas">
<div class="overview">
<div class="ov-sec-hd" style="margin-bottom:12px">
<div class="ov-sec-title">Aktivitas Terkini</div>
</div>
<div id="ov-activity-list">
<div style="text-align:center;padding:20px;color:var(--text2);font-size:11px">Memuat...</div>
</div>
</div>
</div><!-- /ovpanel-aktivitas -->
</div><!-- /tab-overview -->
<!-- ══ PANEL: PETA ══ -->
<div class="s-panel" id="tab-peta">
<!-- Secondary navbar for peta -->
<div class="sec-nav">
<button class="snav-btn active" id="pnav2-filter" onclick="switchPetaSub('filter')">🔍 Filter</button>
<button class="snav-btn" id="pnav2-legenda" onclick="switchPetaSub('legenda')">🗺️ Legenda</button>
<button class="snav-btn" id="pnav2-aksi" onclick="switchPetaSub('aksi')">⚡ Aksi</button>
</div>
<!-- Sub-panel: Filter -->
<div class="sub-panel active" id="petapanel-filter">
<div style="padding:12px 13px;display:flex;flex-direction:column;gap:10px">
<div>
<label style="font-size:10px;font-weight:800;color:var(--text2);display:block;margin-bottom:5px;letter-spacing:0.4px;text-transform:uppercase">Filter Status Ekonomi</label>
<select id="filter-ekonomi" onchange="loadPenduduk()" style="width:100%;background:rgba(255,255,255,0.04);border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:9px;font-family:'Sora',sans-serif;font-size:11px;outline:none">
<option value="all">Semua Status</option>
<option value="sangat_miskin">🔴 Sangat Miskin</option>
<option value="miskin">🟠 Miskin</option>
<option value="rentan">🟡 Rentan</option>
</select>
</div>
<div>
<label style="font-size:10px;font-weight:800;color:var(--text2);display:block;margin-bottom:5px;letter-spacing:0.4px;text-transform:uppercase">Tampilkan Layer</label>
<div style="display:flex;gap:6px">
<button class="fb on" id="fb-ibadah" onclick="toggleLayer('ibadah',this)" style="flex:1">🕌 Ibadah</button>
<button class="fb on" id="fb-radius" onclick="toggleLayer('radius',this)" style="flex:1">⭕ Radius</button>
</div>
</div>
</div>
</div>
<!-- Sub-panel: Legenda -->
<div class="sub-panel" id="petapanel-legenda">
<div style="padding:12px 13px">
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;margin-bottom:8px">Simbol Peta</div>
<div class="leg-grid" style="margin-bottom:10px">
<div class="leg-item">⚠️ Sangat Miskin</div>
<div class="leg-item">🟠 Miskin</div>
<div class="leg-item">🟡 Rentan</div>
<div class="leg-item">📍 Lokasi Saya</div>
<div class="leg-item">🕌 Masjid</div>
<div class="leg-item">⛪ Gereja</div>
<div class="leg-item">🏯 Vihara</div>
<div class="leg-item">⭕ Radius</div>
</div>
<div class="leg-note">
💡 <b>Cara kirim laporan:</b><br>
Klik marker penduduk di peta → pilih "Laporan Kejadian" → isi formulir
</div>
</div>
</div>
<!-- Sub-panel: Aksi -->
<div class="sub-panel" id="petapanel-aksi">
<div style="padding:12px 13px;display:flex;flex-direction:column;gap:8px">
<button class="fb" onclick="goMyLocation()" style="padding:11px;font-size:11px">📍 Tampilkan Lokasi Saya</button>
<button class="fb" onclick="resetMap()" style="padding:11px;font-size:11px">🔄 Reset Peta</button>
<div style="padding:9px 11px;background:rgba(59,130,246,0.06);border:1px solid rgba(59,130,246,0.15);border-radius:9px;font-size:10px;color:rgba(147,197,253,0.85);line-height:1.6;margin-top:4px">
️ Klik marker pada peta untuk melihat detail penduduk dan mengirim laporan kejadian.
</div>
</div>
</div>
</div><!-- /tab-peta -->
<!-- ── TAB: BANTUAN ── -->
<div class="s-panel" id="tab-bantuan">
<div class="ph"><div class="ph-title">🤝 Riwayat Bantuan</div><div class="ph-sub">Bantuan yang diterima keluarga</div></div>
<div id="bantuan-list" style="padding:12px">
<div class="empty"><div class="ei">⏳</div><p>Memuat riwayat bantuan...</p></div>
</div>
</div>
<!-- ── TAB: RIWAYAT LAPORAN ── -->
<div class="s-panel" id="tab-riwayat">
<div class="ph"><div class="ph-title">📋 Riwayat Laporan</div><div class="ph-sub">Laporan yang pernah Anda kirim</div></div>
<div id="riwayat-list" style="padding:12px">
<div class="empty"><div class="ei">⏳</div><p>Memuat...</p></div>
</div>
</div>
<!-- ── TAB: CHAT ── -->
</div>
<!-- ── CHAT MODAL ── -->
<div class="chat-modal-overlay hidden" id="chat-modal-overlay" onclick="closeChatModal(event)">
<div class="chat-modal">
<div style="position:relative">
<div class="chat-modal-drag"></div>
</div>
<div class="chat-modal-hd">
<div class="chat-modal-icon" id="cmo-icon">🕌</div>
<div class="chat-modal-info">
<div class="chat-modal-name" id="cmo-name">Rumah Ibadah</div>
<div class="chat-modal-sub" id="cmo-sub">Chat langsung dengan pengurus</div>
</div>
<button class="chat-modal-close" onclick="closeChatModal()">✕</button>
</div>
<div class="chat-modal-msgs" id="chat-msgs-body">
<div class="empty" style="height:100%;justify-content:center">
<div class="ei">💬</div>
<p>Memuat pesan...</p>
</div>
</div>
<div class="chat-modal-input">
<textarea class="chat-modal-inp" id="chat-mas-input" rows="1" placeholder="Tulis pesan..."
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChatMasyarakat();}"></textarea>
<button class="chat-modal-send" onclick="sendChatMasyarakat()">📤</button>
</div>
</div>
</div>
<!-- ── MAP ── -->
<div class="map-wrap">
<div id="map"></div>
<div class="fab-wrap">
<button class="fab fab-loc" onclick="goMyLocation()" title="Lokasi Saya">📍</button>
<button class="fab fab-reset" onclick="resetMap()" title="Reset Peta">🔄</button>
</div>
</div>
</div>
<!-- ══════════════ MODAL: DETAIL KK + LAPORAN ══════════════ -->
<div class="mo hidden" id="modal-kk">
<div class="md">
<div class="mh">
<div class="mt-m" id="modal-kk-title">Detail Penduduk</div>
<button class="mc" onclick="closeModal()">✕</button>
</div>
<div class="mb-m" id="modal-kk-body"></div>
</div>
</div>
<!-- ══════════════ MODAL: FORM LAPORAN ══════════════ -->
<div class="mo hidden" id="modal-laporan">
<div class="md">
<div class="mh">
<div class="mt-m">📢 Kirim Laporan Kejadian</div>
<button class="mc" onclick="closeLaporan()">✕</button>
</div>
<div class="mb-m">
<!-- Preview KK yang dilaporkan -->
<div class="kk-preview" id="lap-kk-preview">
<div class="kk-preview-title">📍 Terkait Penduduk</div>
<div class="kk-preview-nama" id="lap-kk-nama">—</div>
<div class="kk-preview-meta" id="lap-kk-meta">—</div>
</div>
<div class="info-tip">
⚡ Laporan akan langsung diteruskan ke pengurus rumah ibadah di wilayah penduduk ini.
</div>
<div class="lap-section">
<div class="fl">
<label>Jenis Kejadian *</label>
<select id="lap-jenis">
<option value="kelahiran">🍼 Kelahiran — Ada bayi baru lahir</option>
<option value="kematian">⚰️ Kematian — Ada yang meninggal dunia</option>
<option value="pindah_masuk">📦 Pindah Masuk — Ada penduduk pindah ke sini</option>
<option value="pindah_keluar">🚚 Pindah Keluar — Penduduk pindah keluar</option>
<option value="pernikahan">💒 Pernikahan — Ada pernikahan baru</option>
<option value="perubahan_data">📝 Perubahan Data — Koreksi data penduduk</option>
<option value="lainnya">📌 Lainnya</option>
</select>
</div>
<div class="fl">
<label>Nama yang Terlibat</label>
<input type="text" id="lap-nama" placeholder="Nama orang yang terkait kejadian...">
</div>
<div class="fl">
<label>Tanggal Kejadian *</label>
<input type="date" id="lap-tgl">
</div>
<div class="fl">
<label>Deskripsi Lengkap *</label>
<textarea id="lap-desk" rows="4" placeholder="Ceritakan apa yang terjadi secara singkat dan jelas...&#10;&#10;Contoh: Bapak Sukirman (50 th) warga RT03/RW02 telah meninggal dunia pada hari ini pukul 08.00 WIB karena sakit."></textarea>
</div>
<div class="fl">
<label>Foto Pendukung (opsional)</label>
<input type="file" id="lap-foto" accept="image/*" style="font-size:11px">
</div>
<div style="display:flex;gap:8px">
<button class="btn btn-g" onclick="closeLaporan()" style="flex:1;justify-content:center">Batal</button>
<button class="btn btn-p" id="btn-lap-kirim" onclick="kirimLaporan()" style="flex:2;justify-content:center">📤 Kirim Laporan</button>
</div>
</div>
</div>
</div>
</div>
<!-- NOTIF PANEL backdrop -->
<div id="notif-back" onclick="tutupNotif()" style="display:none;position:fixed;inset:0;z-index:150"></div>
<!-- TOAST -->
<div id="toast" style="display:none" class="toast"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// ══════════════════════════════════════════
// MAP SETUP
// ══════════════════════════════════════════
let map = L.map('map', {
center: [-0.0263, 109.3425],
zoom: 13,
zoomControl: false
});
window.map = map; // buat accessible dari Leaflet popup onclick
// Paksa Leaflet hitung ulang ukuran kontainer (retry beberapa kali agar pasti)
[100,300,600,1000].forEach(ms => setTimeout(() => map.invalidateSize(), ms));
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 19
}).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
// Layer groups
const LG = {
ibadah: L.layerGroup().addTo(map),
penduduk: L.layerGroup().addTo(map),
radius: L.layerGroup().addTo(map),
myLoc: L.layerGroup().addTo(map),
};
const LV = { ibadah: true, radius: true };
// ── ICONS ──
function mkIcon(color, emoji, sz = 30, opacity = 1) {
return L.divIcon({
className: '',
html: `<div style="
width:${sz}px;height:${sz}px;background:${color};border-radius:50%;
display:flex;align-items:center;justify-content:center;font-size:${Math.round(sz*0.47)}px;
box-shadow:0 3px 12px rgba(0,0,0,0.5);
border:2px solid rgba(255,255,255,0.3);
opacity:${opacity};cursor:pointer;
transition:transform 0.15s;
">${emoji}</div>`,
iconSize: [sz, sz],
iconAnchor: [sz/2, sz/2],
popupAnchor: [0, -sz/2 - 4]
});
}
const IMAP = {
masjid: mkIcon('#3b82f6','🕌',32),
gereja: mkIcon('#8b5cf6','⛪',32),
vihara: mkIcon('#f59e0b','🏯',32),
pura: mkIcon('#10b981','🛕',32),
klenteng: mkIcon('#06b6d4','⛩️',32),
lainnya: mkIcon('#64748b','🏢',32),
sangat_miskin: mkIcon('#ef4444','⚠️',34),
miskin: mkIcon('#f97316','🔸',30),
rentan: mkIcon('#eab308','🔶',28),
mampu: mkIcon('#22c55e','🔷',26),
me: mkIcon('#06b6d4','📍',36),
};
function iIcon(j) { return IMAP[(j||'').toLowerCase()] || IMAP.lainnya; }
function pIcon(se){ return IMAP[se] || IMAP.miskin; }
function toggleLayer(key, btn) {
if (LV[key]) {
map.removeLayer(LG[key]); LV[key] = false;
btn && btn.classList.remove('on');
} else {
map.addLayer(LG[key]); LV[key] = true;
btn && btn.classList.add('on');
}
}
function resetMap() { map.setView([-0.0263, 109.3425], 13); }
// ══════════════════════════════════════════
// LOAD DATA
// ══════════════════════════════════════════
async function loadDashboard() {
try {
const d = await fetch('../api/penduduk.php?action=dashboard').then(r => r.json());
document.getElementById('s-kk').textContent = d.total_kk || 0;
document.getElementById('s-jiwa').textContent = d.total_jiwa || 0;
document.getElementById('s-ibadah').textContent = d.total_ibadah|| 0;
document.getElementById('s-bantuan').textContent = d.total_bantuan||0;
} catch(e) {}
}
async function loadIbadah() {
try {
const gj = await fetch('../api/rumah_ibadah.php?action=geojson').then(r => r.json());
LG.ibadah.clearLayers();
LG.radius.clearLayers();
(gj.features || []).forEach(f => {
const p = f.properties, [lng, lat] = f.geometry.coordinates;
L.circle([lat, lng], {
radius: p.radius, color: '#3b82f6', fillColor: '#3b82f6',
fillOpacity: 0.05, weight: 1, dashArray: '5,4'
}).addTo(LG.radius);
const ico = p.jenis==='Masjid'?'🕌':p.jenis==='Gereja'?'⛪':p.jenis==='Vihara'?'🏯':'🛕';
const m = L.marker([lat, lng], { icon: iIcon(p.jenis) }).addTo(LG.ibadah);
m.bindPopup(`
<div style="font-family:'Sora',sans-serif;padding:4px 2px;min-width:200px">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
<span style="font-size:22px">${ico}</span>
<div>
<div style="font-size:13px;font-weight:800;line-height:1.2">${p.nama||'-'}</div>
<div style="font-size:10px;color:#7c93b5;margin-top:1px">${p.jenis||''}</div>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:10px">
<div style="background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.15);border-radius:8px;padding:6px 8px;text-align:center">
<div style="font-size:16px;font-weight:800;color:#3b82f6">${p.jumlah_kk||0}</div>
<div style="font-size:9px;color:#7c93b5;font-weight:700">KK Binaan</div>
</div>
<div style="background:rgba(16,185,129,0.08);border:1px solid rgba(16,185,129,0.15);border-radius:8px;padding:6px 8px;text-align:center">
<div style="font-size:16px;font-weight:800;color:#10b981">${p.jumlah_bantuan||0}</div>
<div style="font-size:9px;color:#7c93b5;font-weight:700">Bantuan</div>
</div>
</div>
${p.kecamatan ? `<div style="font-size:10px;color:#7c93b5;margin-bottom:8px">📍 ${p.kecamatan}</div>` : ''}
<button class="popup-chat-btn"
style="width:100%;padding:8px;background:linear-gradient(135deg,#3b82f6,#06b6d4);
color:#fff;border:none;border-radius:9px;font-family:'Sora',sans-serif;
font-size:11px;font-weight:700;cursor:pointer;display:flex;align-items:center;
justify-content:center;gap:6px">
💬 Chat dengan Pengurus
</button>
</div>`, { maxWidth: 240 });
// Bind klik tombol chat via event listener (aman dari quote-escape)
m.on('popupopen', () => {
const btn = m.getPopup().getElement().querySelector('.popup-chat-btn');
if (btn) btn.onclick = () => openChatModal(p.id, ico, p.nama||'');
});
});
} catch(e) {}
}
// Data KK cache untuk sidebar
let kkCache = [];
async function loadPenduduk() {
try {
const se = document.getElementById('filter-ekonomi').value;
const url = `../api/penduduk.php?action=geojson${se !== 'all' ? '&status_ekonomi=' + se : ''}`;
const gj = await fetch(url).then(r => r.json());
LG.penduduk.clearLayers();
kkCache = gj.features || [];
kkCache.forEach(f => {
const p = f.properties, [lng, lat] = f.geometry.coordinates;
const m = L.marker([lat, lng], { icon: pIcon(p.status_ekonomi) }).addTo(LG.penduduk);
// Buat popup dengan tombol laporan
m.bindPopup(buildKKPopup(p), { maxWidth: 260 });
// Saat popup dibuka, set penduduk aktif
m.on('popupopen', () => { activeKK = p; });
});
// Update sidebar KK list
renderKKList(kkCache);
} catch(e) {}
}
// Penduduk aktif (dipilih di peta)
let activeKK = null;
function buildKKPopup(p) {
return `<div style="font-family:'Sora',sans-serif;padding:4px;min-width:220px">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px">
<b style="font-size:13px;flex:1">${p.nama_kepala_keluarga}</b>
${seBadge(p.status_ekonomi)}
</div>
<div style="font-size:11px;color:#64748b;line-height:1.8;margin-bottom:8px">
📍 ${p.alamat||'-'}${p.rt?`, RT${p.rt}/RW${p.rw}`:''}<br>
👥 ${p.jumlah_anggota} jiwa &nbsp;·&nbsp;
🕌 ${p.nama_ibadah||'-'}<br>
🤝 ${p.jumlah_bantuan||0}x bantuan diterima
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:6px">
<button onclick="openDetailKK(${p.id})"
style="padding:6px;background:#1e3a5f;border:1px solid #3b82f6;color:#93c5fd;border-radius:8px;cursor:pointer;font-size:10px;font-weight:700;font-family:'Sora',sans-serif">
🔍 Detail
</button>
<button onclick="bukaFormLaporan(${p.id},'${escQ(p.nama_kepala_keluarga)}','${escQ(p.alamat||'')}','${escQ(p.kelurahan||'')}',${p.id_rumah_ibadah||0})"
style="padding:6px;background:linear-gradient(135deg,#3b82f6,#06b6d4);border:none;color:#fff;border-radius:8px;cursor:pointer;font-size:10px;font-weight:700;font-family:'Sora',sans-serif">
📢 Laporan
</button>
</div>
</div>`;
}
function escQ(s) { return (s||'').replace(/'/g,"&#39;").replace(/"/g,'&quot;').replace(/\n/g,' '); }
function seBadge(se) {
const m = {
sangat_miskin: ['#fca5a5','rgba(239,68,68,0.15)','Sangat Miskin'],
miskin: ['#fcd34d','rgba(245,158,11,0.15)','Miskin'],
rentan: ['#c4b5fd','rgba(139,92,246,0.15)','Rentan'],
mampu: ['#6ee7b7','rgba(16,185,129,0.15)','Mampu'],
};
const v = m[se] || ['#94a3b8','rgba(255,255,255,0.08)', se||'-'];
return `<span style="padding:2px 8px;border-radius:5px;font-size:9px;font-weight:800;color:${v[0]};background:${v[1]}">${v[2]}</span>`;
}
function renderKKList(features) {
// Tidak ada sidebar KK list di tab peta — panduan cukup di legenda
}
// ══════════════════════════════════════════
// DETAIL KK MODAL
// ══════════════════════════════════════════
async function openDetailKK(id) {
map.closePopup();
document.getElementById('modal-kk').classList.remove('hidden');
document.getElementById('modal-kk-body').innerHTML =
'<div style="text-align:center;padding:30px;color:var(--text2)">⏳ Memuat data...</div>';
try {
const f = await fetch(`../api/penduduk.php?action=get&id=${id}`).then(r => r.json());
const p = f.properties;
const anggota = p.anggota || [];
const bantuan = (p.histori_bantuan || []).slice(0, 5);
document.getElementById('modal-kk-title').textContent = p.nama_kepala_keluarga;
document.getElementById('modal-kk-body').innerHTML = `
<div style="margin-bottom:14px">
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
${seBadge(p.status_ekonomi)}
${p.nama_ibadah ? `<span style="padding:2px 8px;border-radius:5px;font-size:9px;font-weight:800;color:#93c5fd;background:rgba(59,130,246,0.15)">🕌 ${p.nama_ibadah}</span>` : ''}
</div>
<div style="font-size:11px;color:var(--text2);line-height:1.8">
📍 ${p.alamat||'-'}${p.rt?`, RT${p.rt}/RW${p.rw}`:''}
${p.kelurahan?`<br>🏘️ Kel. ${p.kelurahan}, Kec. ${p.kecamatan||'-'}`:''}
${p.jarak_ke_ibadah?`<br>📏 Jarak ke ibadah: ${p.jarak_ke_ibadah}m`:''}
</div>
</div>
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;padding-bottom:8px;border-bottom:1px solid var(--border);margin-bottom:10px">
👥 Anggota Keluarga (${anggota.length})
</div>
${anggota.length ? anggota.map(a => `
<div class="ak-card">
<div class="ak-row">
<div class="ak-av" style="background:${a.jenis_kelamin==='L'?'rgba(59,130,246,0.15)':'rgba(236,72,153,0.15)'}">
${a.jenis_kelamin==='L'?'👦':'👧'}
</div>
<div style="flex:1">
<div class="ak-nama">${a.nama} ${a.status_hidup!=='hidup'?`<span class="badge br" style="font-size:9px">${a.status_hidup}</span>`:''}</div>
<div class="ak-meta">${a.hubungan} · ${a.umur||0} tahun · ${a.agama||'-'}</div>
${a.pendidikan_terakhir ? `<div class="ak-pend">📚 ${a.pendidikan_terakhir.jenjang}${a.pendidikan_terakhir.nama_institusi ? ' — '+a.pendidikan_terakhir.nama_institusi : ''}${a.pendidikan_terakhir.status_pendidikan==='sedang_berjalan'?' (aktif)':''}</div>` : ''}
${a.pekerjaan ? `<div style="font-size:10px;color:var(--text2);margin-top:2px">💼 ${a.pekerjaan}</div>` : ''}
</div>
</div>
</div>`).join('') : '<div class="empty" style="padding:14px"><p>Belum ada data anggota</p></div>'}
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;padding:10px 0 8px;border-top:1px solid var(--border);margin-top:4px">
🤝 Riwayat Bantuan (${(p.histori_bantuan||[]).length}x)
</div>
${bantuan.length ? bantuan.map(b => `
<div class="bc">
<div class="bc-top">
<div class="bc-jenis">${b.jenis_bantuan}</div>
<div class="bc-tgl">${fDate(b.tanggal_bantuan)}</div>
</div>
<div class="bc-detail">
📦 ${b.bentuk_bantuan}
${b.jumlah_nominal?`<br>💰 Rp${Number(b.jumlah_nominal).toLocaleString('id')}`:''}
</div>
<div class="bc-src">🕌 ${b.nama_ibadah||b.sumber_bantuan||'-'}${b.nama_mitra?` + mitra: ${b.nama_mitra}`:''}</div>
</div>`).join('') : '<div class="empty" style="padding:12px"><p>Belum ada bantuan tercatat</p></div>'}
<div style="margin-top:14px">
<button class="btn btn-p btn-full"
onclick="closeModal();bukaFormLaporan(${p.id},'${escQ(p.nama_kepala_keluarga)}','${escQ(p.alamat||'')}','${escQ(p.kelurahan||'')}',${p.id_rumah_ibadah||0})">
📢 Laporkan Kejadian pada Keluarga Ini
</button>
</div>`;
} catch(e) {
document.getElementById('modal-kk-body').innerHTML =
'<p style="text-align:center;color:var(--red);padding:20px">Gagal memuat data</p>';
}
}
function closeModal() { document.getElementById('modal-kk').classList.add('hidden'); }
// ══════════════════════════════════════════
// FORM LAPORAN
// ══════════════════════════════════════════
let lapKKId = null;
function bukaFormLaporan(kkId, nama, alamat, kelurahan, ibadahId) {
// Tutup popup & modal lain
map.closePopup();
document.getElementById('modal-kk').classList.add('hidden');
lapKKId = kkId;
// Set preview KK
document.getElementById('lap-kk-nama').textContent = nama;
document.getElementById('lap-kk-meta').textContent =
`📍 ${alamat}${kelurahan ? ', Kel. '+kelurahan : ''}`;
// Reset form
document.getElementById('lap-jenis').value = 'kelahiran';
document.getElementById('lap-nama').value = '';
document.getElementById('lap-tgl').value = today();
document.getElementById('lap-desk').value = '';
document.getElementById('lap-foto').value = '';
const btn = document.getElementById('btn-lap-kirim');
btn.disabled = false;
btn.innerHTML = '📤 Kirim Laporan';
document.getElementById('modal-laporan').classList.remove('hidden');
}
function closeLaporan() {
document.getElementById('modal-laporan').classList.add('hidden');
lapKKId = null;
}
async function kirimLaporan() {
const desk = document.getElementById('lap-desk').value.trim();
const tgl = document.getElementById('lap-tgl').value;
if (!lapKKId) { showToast('Pilih penduduk di peta terlebih dahulu','error'); return; }
if (!desk) { showToast('Deskripsi wajib diisi','error'); return; }
if (!tgl) { showToast('Tanggal kejadian wajib diisi','error'); return; }
const btn = document.getElementById('btn-lap-kirim');
btn.disabled = true;
btn.innerHTML = '<div class="spinner"></div> Mengirim...';
const fd = new FormData();
fd.append('id_penduduk', lapKKId);
fd.append('jenis_laporan', document.getElementById('lap-jenis').value);
fd.append('nama_terkait', document.getElementById('lap-nama').value.trim());
fd.append('tanggal_kejadian', tgl);
fd.append('deskripsi', desk);
const foto = document.getElementById('lap-foto').files[0];
if (foto) fd.append('lampiran', foto);
try {
const d = await fetch('../api/laporan.php?action=kirim', { method:'POST', body: fd }).then(r => r.json());
if (d.success) {
showToast('✅ Laporan berhasil dikirim! Terima kasih.', 'success');
closeLaporan();
// Refresh riwayat jika tab aktif
if (document.getElementById('tab-riwayat').classList.contains('active')) loadRiwayat();
} else {
showToast(d.error || 'Gagal mengirim laporan', 'error');
btn.disabled = false;
btn.innerHTML = '📤 Kirim Laporan';
}
} catch(e) {
showToast('Kesalahan koneksi', 'error');
btn.disabled = false;
btn.innerHTML = '📤 Kirim Laporan';
}
}
// ══════════════════════════════════════════
// GEOLOCATION
// ══════════════════════════════════════════
function goMyLocation() {
if (!navigator.geolocation) { showToast('Geolokasi tidak didukung browser ini', 'error'); return; }
showToast('🔍 Mencari lokasi Anda...', 'info');
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lng = pos.coords.longitude;
LG.myLoc.clearLayers();
L.marker([lat, lng], { icon: IMAP.me })
.addTo(LG.myLoc)
.bindPopup(`<div style="font-family:'Sora',sans-serif">
<b>📍 Lokasi Saya</b><br>
<span style="font-size:11px;color:#64748b">
${lat.toFixed(5)}, ${lng.toFixed(5)}<br>
Akurasi ±${Math.round(pos.coords.accuracy)}m
</span>
</div>`).openPopup();
L.circle([lat, lng], {
radius: pos.coords.accuracy,
color: '#06b6d4', fillColor: '#06b6d4', fillOpacity: 0.08, weight: 1
}).addTo(LG.myLoc);
map.flyTo([lat, lng], 16, { animate: true, duration: 1 });
showToast('✅ Lokasi ditemukan!', 'success');
}, err => {
const msgs = {1:'Izin ditolak. Aktifkan lokasi di browser.',2:'Sinyal GPS lemah.',3:'Timeout. Coba lagi.'};
showToast(msgs[err.code]||'Gagal mendapat lokasi', 'error');
}, { enableHighAccuracy: true, timeout: 10000 });
}
// ══════════════════════════════════════════
// OVERVIEW
// ══════════════════════════════════════════
// ── Clock & greeting ──────────────────────────────────────
(function tickClock() {
const pad = n => String(n).padStart(2,'0');
function update() {
const now = new Date();
const h = now.getHours(), m = now.getMinutes();
const el = document.getElementById('ov-clock');
const de = document.getElementById('ov-date');
const gi = document.getElementById('ov-greeting-icon');
const gt = document.getElementById('ov-greeting-title');
if (el) el.textContent = `${pad(h)}:${pad(m)}`;
const days = ['Minggu','Senin','Selasa','Rabu','Kamis','Jumat','Sabtu'];
const months = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Ags','Sep','Okt','Nov','Des'];
if (de) de.textContent = `${days[now.getDay()]}, ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`;
if (gi && gt) {
if (h < 6) { gi.textContent='🌙'; gt.textContent='Selamat malam di Portal Masyarakat'; }
else if (h < 11) { gi.textContent='🌅'; gt.textContent='Selamat pagi di Portal Masyarakat'; }
else if (h < 15) { gi.textContent='☀️'; gt.textContent='Selamat siang di Portal Masyarakat'; }
else if (h < 19) { gi.textContent='🌤️'; gt.textContent='Selamat sore di Portal Masyarakat'; }
else { gi.textContent='🌙'; gt.textContent='Selamat malam di Portal Masyarakat'; }
}
}
update();
setInterval(update, 30000);
})();
async function loadOverview() {
try {
const d = await fetch('../api/penduduk.php?action=dashboard').then(r => r.json());
// Animate numbers counting up
const animNum = (id, target) => {
const el = document.getElementById(id); if (!el) return;
const dur = 900, start = performance.now();
const step = ts => {
const p = Math.min((ts - start) / dur, 1);
const ease = 1 - Math.pow(1 - p, 3);
el.textContent = Math.round(ease * target).toLocaleString('id-ID');
if (p < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
};
animNum('ov-kk', d.total_kk || 0);
animNum('ov-jiwa', d.total_jiwa || 0);
animNum('ov-sm', d.sangat_miskin|| 0);
animNum('ov-miskin', d.miskin || 0);
animNum('ov-bantuan', d.total_bantuan|| 0);
animNum('ov-ibadah', d.total_ibadah|| 0);
// Progress bars on KPI cards
const kk = d.total_kk || 1;
setTimeout(() => {
const setBar = (id, pct) => {
const el = document.getElementById(id); if (el) el.style.width = Math.min(pct,100) + '%';
};
setBar('bar-kk', 85);
setBar('bar-sm', ((d.sangat_miskin || 0) / kk) * 100 * 3);
setBar('bar-mis', ((d.miskin || 0) / kk) * 100 * 3);
}, 100);
// Update last-update badge
const upd = document.getElementById('ov-last-update');
if (upd) { const now = new Date(); upd.textContent = `Diperbarui ${now.getHours()}:${String(now.getMinutes()).padStart(2,'0')}`; }
// Status breakdown bars
const total = (d.sangat_miskin||0) + (d.miskin||0) + (d.hampir_miskin||0) + (d.tidak_miskin||0) || kk;
const setPct = (barId, valId, pctId, val) => {
setTimeout(() => {
const b = document.getElementById(barId); if (b) b.style.width = ((val/total)*100).toFixed(1) + '%';
}, 200);
const v = document.getElementById(valId); if (v) v.textContent = (val||0).toLocaleString('id-ID');
const p = document.getElementById(pctId); if (p) p.textContent = total > 0 ? ((val/total)*100).toFixed(1)+'%' : '0%';
};
setPct('slb-sm', 'slv-sm', 'slp-sm', d.sangat_miskin || 0);
setPct('slb-mis', 'slv-mis', 'slp-mis', d.miskin || 0);
setPct('slb-hm', 'slv-hm', 'slp-hm', d.hampir_miskin || 0);
setPct('slb-tm', 'slv-tm', 'slp-tm', d.tidak_miskin || 0);
if (d.kecamatan_data) renderOVCharts(d.kecamatan_data, d.status_counts);
renderActivityFeed(d);
renderIbadahCards(d.ibadah_list);
} catch(e) { console.error('Load overview error:', e); }
}
function renderOVCharts(kecData, statusCounts) {
const fontCfg = { family: "'Sora', sans-serif", size: 10 };
const gridColor = 'rgba(99,179,237,0.08)';
const textColor = '#7c93b5';
// Donut (mini, horizontal legend)
if (window.ovChartDonut) window.ovChartDonut.destroy();
const ctx = document.getElementById('ov-chart-donut')?.getContext('2d');
if (ctx && statusCounts) {
const labels = Object.keys(statusCounts);
const data = Object.values(statusCounts);
const palette = { 'Sangat Miskin':'#ef4444','Miskin':'#f59e0b','Hampir Miskin':'#3b82f6','Tidak Miskin':'#10b981' };
const colors = labels.map(l => palette[l] || '#6366f1');
window.ovChartDonut = new Chart(ctx, {
type: 'doughnut',
data: { labels, datasets: [{ data, backgroundColor: colors.map(c=>c+'22'), borderColor: colors, borderWidth: 2, hoverBorderWidth: 3 }] },
options: {
responsive: true, maintainAspectRatio: false, cutout: '68%',
plugins: {
legend: { position: 'bottom', labels: { color: textColor, font: fontCfg, padding: 10, boxWidth: 10, boxHeight: 10 } },
tooltip: { callbacks: { label: ctx => ` ${ctx.label}: ${ctx.parsed.toLocaleString('id-ID')} KK` } }
}
}
});
}
// Bar chart per kecamatan
if (kecData && Object.keys(kecData).length > 0) {
if (window.ovChartKecamatan) window.ovChartKecamatan.destroy();
const kctx = document.getElementById('ov-chart-kecamatan')?.getContext('2d');
if (kctx) {
const kecLabels = Object.keys(kecData);
const kecValues = Object.values(kecData);
const max = Math.max(...kecValues);
const bgColors = kecValues.map(v => {
const pct = v / max;
return `rgba(${Math.round(59 + (6-59)*pct)},${Math.round(130 + (182-130)*pct)},${Math.round(246 + (212-246)*pct)},0.7)`;
});
window.ovChartKecamatan = new Chart(kctx, {
type: 'bar',
data: {
labels: kecLabels,
datasets: [{ label: 'Jumlah KK', data: kecValues, backgroundColor: bgColors,
borderColor: 'rgba(99,179,237,0.4)', borderWidth: 1, borderRadius: 7, borderSkipped: false }]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false },
tooltip: { callbacks: { label: ctx => ` ${ctx.parsed.y.toLocaleString('id-ID')} KK` } } },
scales: {
y: { grid: { color: gridColor }, ticks: { color: textColor, font: fontCfg },
border: { color: 'transparent' } },
x: { grid: { display: false }, ticks: { color: textColor, font: fontCfg, maxRotation: 35 },
border: { color: 'transparent' } }
}
}
});
}
}
}
function renderActivityFeed(d) {
const el = document.getElementById('ov-activity-list');
if (!el) return;
const items = [];
if (d.total_bantuan > 0)
items.push({ icon:'🤝', bg:'rgba(16,185,129,0.1)', title:`${(d.total_bantuan||0).toLocaleString('id-ID')} Bantuan Disalurkan`, sub:'Total program bantuan aktif', time:'Terkini' });
if (d.sangat_miskin > 0)
items.push({ icon:'⚠️', bg:'rgba(239,68,68,0.1)', title:`${(d.sangat_miskin||0).toLocaleString('id-ID')} KK Prioritas Penanganan`, sub:'Kategori sangat miskin butuh perhatian', time:'Data terbaru' });
if (d.total_ibadah > 0)
items.push({ icon:'🕌', bg:'rgba(139,92,246,0.1)', title:`${d.total_ibadah} Rumah Ibadah Aktif`, sub:'Melayani wilayah Kota Pontianak', time:'Terverifikasi' });
items.push({ icon:'📊', bg:'rgba(59,130,246,0.1)', title:`${(d.total_jiwa||0).toLocaleString('id-ID')} Jiwa Terdaftar`, sub:'Data kependudukan terverifikasi', time:'Real-time' });
if (!items.length) { el.innerHTML='<div style="text-align:center;padding:20px;color:var(--text2);font-size:11px">Belum ada aktivitas</div>'; return; }
el.innerHTML = items.map(it => `
<div class="act-item">
<div class="act-icon-wrap" style="background:${it.bg}">${it.icon}</div>
<div class="act-body">
<div class="act-title">${it.title}</div>
<div class="act-sub">${it.sub}</div>
</div>
<div class="act-time">${it.time}</div>
</div>`).join('');
}
function renderIbadahCards(ibadahList) {
const el = document.getElementById('ov-ibadah-list');
if (!el) return;
const icons = { masjid:'🕌', gereja:'⛪', vihara:'🏯', pura:'🛕', klenteng:'🔴' };
const makeCard = (p) => {
const ico = icons[(p.jenis||'').toLowerCase()] || '🕍';
const id = p.id || p.id_rumah_ibadah || 0;
const nm = (p.nama||'-').replace(/'/g,"\\'");
return `<div class="ov-icard" onclick="openChatModal(${id},'${ico}','${nm}')">
<div class="ov-icard-icon">${ico}</div>
<div class="ov-icard-body">
<div class="ov-icard-name">${p.nama||'-'}</div>
<div class="ov-icard-kk">👥 ${p.jumlah_kk||0} KK binaan</div>
</div>
<div class="ov-icard-chat" title="Chat">🗨️</div>
</div>`;
};
if (!ibadahList || !ibadahList.length) {
fetch('../api/rumah_ibadah.php?action=geojson').then(r=>r.json()).then(gj => {
const list = (gj.features||[]).slice(0,6).map(f=>({...f.properties, id:f.properties.id||f.id}));
if (!list.length) { el.innerHTML='<div style="grid-column:span 2;text-align:center;padding:16px;color:var(--text2);font-size:11px">Belum ada data rumah ibadah</div>'; return; }
el.innerHTML = list.map(makeCard).join('');
}).catch(()=>{ el.innerHTML='<div style="grid-column:span 2;text-align:center;padding:16px;color:var(--text2);font-size:11px">Data tidak tersedia</div>'; });
return;
}
el.innerHTML = ibadahList.slice(0,6).map(makeCard).join('');
}
// ══════════════════════════════════════════
// KELUARGA & BANTUAN
// ══════════════════════════════════════════
async function loadKeluarga() {
const el = document.getElementById('keluarga-content');
try {
const gj = await fetch('../api/penduduk.php?action=geojson').then(r => r.json());
const features = gj.features || [];
if (!features.length) {
el.innerHTML = `<div class="empty">
<div class="ei">👨‍👩‍👧</div>
<p>Data keluarga belum terdaftar.<br>Hubungi pengurus rumah ibadah setempat.</p>
</div>`; return;
}
const f = features[0], p = f.properties, anggota = p.anggota || [];
el.innerHTML = `
<div style="background:var(--card2);border:1px solid var(--border);border-radius:12px;padding:13px;margin-bottom:12px">
<div style="font-size:14px;font-weight:800;margin-bottom:6px">🏘️ ${p.nama_kepala_keluarga}</div>
<div style="font-size:11px;color:var(--text2);line-height:1.8">
📍 ${p.alamat||'-'}${p.rt?`, RT${p.rt}/RW${p.rw}`:''}
${p.kelurahan?`<br>🏘️ Kel. ${p.kelurahan}, Kec. ${p.kecamatan||'-'}`:''}
${p.nama_ibadah?`<br>🕌 ${p.nama_ibadah}`:''}
</div>
<div style="margin-top:8px">${seBadge(p.status_ekonomi)}</div>
</div>
<div style="font-size:10px;font-weight:800;color:var(--text2);letter-spacing:0.5px;text-transform:uppercase;margin-bottom:8px">
👥 ANGGOTA (${anggota.length})
</div>
${anggota.map(a => `
<div class="ak-card">
<div class="ak-row">
<div class="ak-av" style="background:${a.jenis_kelamin==='L'?'rgba(59,130,246,0.15)':'rgba(236,72,153,0.15)'}">
${a.jenis_kelamin==='L'?'👦':'👧'}
</div>
<div style="flex:1">
<div class="ak-nama">${a.nama} ${a.status_hidup!=='hidup'?`<span class="badge br">${a.status_hidup}</span>`:''}</div>
<div class="ak-meta">${a.hubungan} · ${a.umur||0} tahun · ${a.agama||'-'}</div>
${a.pendidikan_terakhir?`<div class="ak-pend">📚 ${a.pendidikan_terakhir.jenjang}${a.pendidikan_terakhir.status_pendidikan==='sedang_berjalan'?' (aktif)':''}</div>`:'<div style="font-size:10px;color:var(--text2);margin-top:2px">📚 Belum ada data pendidikan</div>'}
${a.pekerjaan?`<div style="font-size:10px;color:var(--text2);margin-top:2px">💼 ${a.pekerjaan}</div>`:''}
</div>
</div>
</div>`).join('') || '<div class="empty" style="padding:16px"><p>Belum ada anggota terdaftar</p></div>'}
<button class="btn btn-p btn-full" style="margin-top:8px" onclick="switchTab('peta',null);goMyLocation()">
📍 Tampilkan Lokasi Saya di Peta
</button>`;
} catch(e) {
el.innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>Gagal memuat data</p></div>';
}
}
async function loadBantuan() {
const el = document.getElementById('bantuan-list');
try {
const data = await fetch('../api/bantuan.php?action=list').then(r => r.json());
if (!data.length) {
el.innerHTML = '<div class="empty"><div class="ei">🤝</div><p>Belum ada bantuan tercatat</p></div>';
return;
}
el.innerHTML = data.slice(0, 20).map(b => `
<div class="bc">
<div class="bc-top">
<div class="bc-jenis">${b.jenis_bantuan}</div>
<div class="bc-tgl">${fDate(b.tanggal_bantuan)}</div>
</div>
<div style="font-size:12px;font-weight:700;color:var(--text);margin-bottom:4px">${b.nama_kepala_keluarga}</div>
<div class="bc-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,60)}`:''}
</div>
<div class="bc-src">
🕌 ${b.nama_ibadah||b.sumber_bantuan||'-'}
${b.nama_mitra?` + mitra: ${b.nama_mitra}`:''}
</div>
</div>`).join('');
} catch(e) {
el.innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>Gagal memuat</p></div>';
}
}
async function loadRiwayat() {
const el = document.getElementById('riwayat-list');
try {
const data = await fetch('../api/laporan.php?action=list').then(r => r.json());
if (!data.length) {
el.innerHTML = `<div class="empty">
<div class="ei">📋</div>
<p>Belum ada laporan yang dikirim.<br>Klik marker penduduk di peta untuk melaporkan.</p>
</div>`; return;
}
el.innerHTML = data.slice(0, 20).map(l => `
<div class="lap-item">
<div class="lap-top">
<div class="lap-nama">${jenisL(l.jenis_laporan)}</div>
${svBadge(l.status_verifikasi)}
</div>
${l.nama_kepala_keluarga?`<div style="font-size:11px;font-weight:600;color:var(--text2)">👥 ${l.nama_kepala_keluarga}</div>`:''}
<div class="lap-meta">📅 ${fDate(l.tanggal_kejadian)}${l.nama_terkait?' · 👤 '+l.nama_terkait:''}</div>
<div class="lap-desk">${(l.deskripsi||'').substring(0,100)}${(l.deskripsi||'').length>100?'...':''}</div>
${l.catatan_pengurus?`<div class="lap-catatan">✅ ${l.catatan_pengurus}</div>`:''}
</div>`).join('');
} catch(e) {
el.innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>Gagal memuat</p></div>';
}
}
// ══════════════════════════════════════════
// NOTIFIKASI
// ══════════════════════════════════════════
let notifOpen = false;
async function loadNotif() {
try {
const d = await fetch('../api/notifikasi.php?unread=1').then(r => r.json());
const badge = document.getElementById('nb-badge');
badge.style.display = d.unread > 0 ? 'flex' : 'none';
if (d.unread > 0) badge.textContent = d.unread > 9 ? '9+' : d.unread;
const el = document.getElementById('np-list');
if (!d.notifikasi || !d.notifikasi.length) {
el.innerHTML = '<div class="empty" style="padding:18px"><div class="ei">🔔</div><p>Tidak ada notifikasi baru</p></div>';
return;
}
el.innerHTML = d.notifikasi.map(n => `
<div class="ni ${n.is_read?'':'unread'}" onclick="bacaN(${n.id})">
<div class="ni-t">${n.judul}</div>
<div class="ni-i">${n.isi}</div>
<div class="ni-d">${fTime(n.created_at)}</div>
</div>`).join('');
} catch(e) {}
}
function toggleNotif() {
const p = document.getElementById('np'), back = document.getElementById('notif-back');
notifOpen = !notifOpen;
p.style.display = notifOpen ? 'block' : 'none';
back.style.display = notifOpen ? 'block' : 'none';
if (notifOpen) loadNotif();
}
function tutupNotif() {
document.getElementById('np').style.display = 'none';
document.getElementById('notif-back').style.display = 'none';
notifOpen = false;
}
async function bacaN(id) {
await fetch('../api/notifikasi.php', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id}) });
loadNotif();
}
async function bacaSemua() {
await fetch('../api/notifikasi.php', { method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify({id:0}) });
loadNotif();
}
// ══════════════════════════════════════════
// NAVIGATION
// ══════════════════════════════════════════
function switchPrimary(tab) {
// primary nav buttons
document.querySelectorAll('.pnav-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('pnav-' + tab);
if (btn) btn.classList.add('active');
// panels
document.querySelectorAll('.s-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById('tab-' + tab);
if (panel) panel.classList.add('active');
// trigger loads
if (tab === 'bantuan') loadBantuan();
if (tab === 'riwayat') loadRiwayat();
if (tab === 'chat') initChatTab();
}
function switchOverviewSub(sub) {
document.querySelectorAll('#tab-overview .snav-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('ovnav-' + sub);
if (btn) btn.classList.add('active');
document.querySelectorAll('#tab-overview .sub-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById('ovpanel-' + sub);
if (panel) panel.classList.add('active');
}
function switchPetaSub(sub) {
document.querySelectorAll('#tab-peta .snav-btn').forEach(b => b.classList.remove('active'));
const btn = document.getElementById('pnav2-' + sub);
if (btn) btn.classList.add('active');
document.querySelectorAll('#tab-peta .sub-panel').forEach(p => p.classList.remove('active'));
const panel = document.getElementById('petapanel-' + sub);
if (panel) panel.classList.add('active');
}
// legacy alias (used by btn-full "Lihat di Peta" etc.)
function switchTab(tab, el) { switchPrimary(tab); }
// ══════════════════════════════════════════
// HELPERS
// ══════════════════════════════════════════
function jenisL(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 svBadge(s) {
const m = {pending:'ba',diproses:'bb',disetujui:'bg',ditolak:'br'};
return `<span class="badge ${m[s]||'bgr'}">${s}</span>`;
}
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 today() { return new Date().toISOString().split('T')[0]; }
function showToast(msg, type = 'info') {
const t = document.getElementById('toast');
t.className = 'toast ' + (type==='success'?'ts':type==='error'?'te':'ti-c');
t.textContent = msg;
t.style.display = 'block';
setTimeout(() => t.style.display = 'none', 3500);
}
// Tutup modal klik overlay
document.querySelectorAll('.mo').forEach(mo => {
mo.addEventListener('click', e => { if (e.target === mo) mo.classList.add('hidden'); });
});
// ══════════════════════════════════════════
// CHAT
// ══════════════════════════════════════════
let masChatIbadahId = null;
let masChatPoll = null;
let chatIbadahLoaded = false;
// ══════════════════════════════════════════
// CHAT MODAL
// ══════════════════════════════════════════
async function openChatModal(ibadahId, icon, nama) {
if (!ibadahId) return;
if (window.map) window.map.closePopup();
masChatIbadahId = parseInt(ibadahId);
document.getElementById('cmo-icon').textContent = icon || '🕌';
document.getElementById('cmo-name').textContent = nama || 'Rumah Ibadah';
document.getElementById('cmo-sub').textContent = 'Chat dengan pengurus';
const overlay = document.getElementById('chat-modal-overlay');
overlay.classList.remove('hidden');
document.getElementById('chat-msgs-body').innerHTML =
'<div class="empty" style="height:100%;justify-content:center"><div class="ei">⏳</div><p>Memuat pesan...</p></div>';
clearInterval(masChatPoll);
await loadChatMsgs();
masChatPoll = setInterval(() => loadChatMsgs(true), 5000);
setTimeout(() => document.getElementById('chat-mas-input')?.focus(), 200);
}
window.openChatModal = openChatModal;
function closeChatModal(e) {
if (e && e.target !== document.getElementById('chat-modal-overlay')) return;
document.getElementById('chat-modal-overlay').classList.add('hidden');
clearInterval(masChatPoll);
masChatIbadahId = null;
}
window.closeChatModal = closeChatModal;
// alias kept for any legacy call
async function initChatTab() {}
function onSelectIbadahChat() {}
async function loadChatMsgs(silent=false) {
if (!masChatIbadahId) return;
const data = await fetch(`../api/chat.php?action=messages&id_rumah_ibadah=${masChatIbadahId}`)
.then(r=>r.json()).catch(()=>[]);
const el = document.getElementById('chat-msgs-body');
if (!el) return;
if (!data||!data.length) {
if (!silent) el.innerHTML='<div class="empty" style="height:100%;justify-content:center"><div class="ei">💬</div><p>Belum ada pesan. Mulai percakapan!</p></div>';
return;
}
const wasBottom = el.scrollHeight-el.scrollTop-el.clientHeight < 60;
el.innerHTML = data.map(m => {
const isMe = (m.pengirim_role==='masyarakat');
return `<div class="ch-bw ${isMe?'me':'them'}">
<div class="ch-b ${isMe?'me':'them'}">${escH(m.isi_pesan)}</div>
<div class="ch-meta">${isMe?'👤 Anda':'🕌 '+escH(m.nama_ibadah||'')} · ${fTime(m.created_at)}</div>
</div>`;
}).join('');
if (wasBottom||!silent) el.scrollTop = el.scrollHeight;
if (!silent) loadChatUnread();
}
async function sendChatMasyarakat() {
if (!masChatIbadahId) return;
const inp = document.getElementById('chat-mas-input');
const isi = (inp?.value||'').trim();
if (!isi) return;
inp.value = ''; inp.disabled = true;
const d = await fetch('../api/chat.php', {
method:'POST', headers:{'Content-Type':'application/json'},
body: JSON.stringify({ id_rumah_ibadah: masChatIbadahId, isi })
}).then(r=>r.json()).catch(()=>({success:false}));
inp.disabled = false; inp.focus();
if (d.success) loadChatMsgs();
else showToast(d.error||'Gagal mengirim', 'error');
}
async function loadChatUnread() {
const d = await fetch('../api/chat.php?action=unread').then(r=>r.json()).catch(()=>({unread:0}));
const badge = document.getElementById('chat-badge');
if (badge) {
badge.textContent = d.unread||'';
badge.style.display = d.unread>0 ? 'inline-flex' : 'none';
}
}
function escH(s) { return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
// ══════════════════════════════════════════
// INIT
// ══════════════════════════════════════════
loadDashboard();
loadOverview();
loadIbadah();
loadPenduduk();
loadNotif();
loadChatUnread();
setInterval(loadNotif, 30000);
setInterval(loadChatUnread, 30000);
</script>
</body>
</html>