1759 lines
88 KiB
PHP
1759 lines
88 KiB
PHP
<?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... 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 ·
|
||
🕌 ${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,"'").replace(/"/g,'"').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,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||
|
||
// ══════════════════════════════════════════
|
||
// INIT
|
||
// ══════════════════════════════════════════
|
||
loadDashboard();
|
||
loadOverview();
|
||
loadIbadah();
|
||
loadPenduduk();
|
||
loadNotif();
|
||
loadChatUnread();
|
||
setInterval(loadNotif, 30000);
|
||
setInterval(loadChatUnread, 30000);
|
||
</script>
|
||
</body>
|
||
</html>
|