767 lines
84 KiB
PHP
767 lines
84 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
<title>WebGIS Kemiskinan — Kota Pontianak</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--bg:#0a0d14;--bg2:#111520;--bg3:#181d2e;--bg4:#1e253a;--bg5:#252e45;
|
|
--b:rgba(255,255,255,.06);--b2:rgba(255,255,255,.11);--b3:rgba(255,255,255,.17);
|
|
--t:#dde2f0;--t2:#7e899e;--t3:#4a5368;
|
|
--blue:#4f8ef7;--bdim:rgba(79,142,247,.14);--bglow:rgba(79,142,247,.28);
|
|
--green:#34d399;--gdim:rgba(52,211,153,.13);
|
|
--red:#f87171;--rdim:rgba(248,113,113,.13);
|
|
--amber:#fbbf24;--adim:rgba(251,191,36,.13);
|
|
--purple:#a78bfa;--pdim:rgba(167,139,250,.13);
|
|
--rose:#fb7185;--rosedim:rgba(251,113,133,.13);
|
|
--r:8px;--rs:6px;--rl:12px;
|
|
--font:'Inter',sans-serif;--mono:'JetBrains Mono',monospace;
|
|
--shadow:0 4px 24px rgba(0,0,0,.45);
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0;-webkit-font-smoothing:antialiased}
|
|
html,body{height:100%;font-family:var(--font);background:var(--bg);color:var(--t);font-size:13px;overflow:hidden}
|
|
body{display:flex;flex-direction:column}
|
|
::-webkit-scrollbar{width:4px;height:4px}
|
|
::-webkit-scrollbar-thumb{background:var(--bg5);border-radius:4px}
|
|
|
|
/* LOGIN */
|
|
#login-screen{position:fixed;inset:0;background:var(--bg);z-index:99999;display:flex;align-items:center;justify-content:center;background-image:radial-gradient(ellipse at 20% 50%,rgba(79,142,247,.06) 0%,transparent 60%),radial-gradient(ellipse at 80% 20%,rgba(167,139,250,.05) 0%,transparent 50%)}
|
|
#login-screen.hidden{display:none}
|
|
.lcard{background:var(--bg2);border:1px solid var(--b2);border-radius:16px;padding:40px 36px;width:420px;max-width:96vw;box-shadow:var(--shadow)}
|
|
.lbrand{display:flex;align-items:center;gap:14px;margin-bottom:32px;padding-bottom:24px;border-bottom:1px solid var(--b)}
|
|
.lbrand-icon{width:44px;height:44px;background:linear-gradient(135deg,rgba(79,142,247,.9),rgba(167,139,250,.9));border-radius:11px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
.lbrand h1{font-size:15px;font-weight:700;letter-spacing:-.3px;line-height:1.2}
|
|
.lbrand p{font-size:11px;color:var(--t3);margin-top:3px;font-family:var(--mono)}
|
|
.lheading{font-size:22px;font-weight:700;letter-spacing:-.5px;margin-bottom:6px}
|
|
.lsub{font-size:12px;color:var(--t3);margin-bottom:24px;line-height:1.6}
|
|
.lerr{font-size:12px;color:var(--red);background:var(--rdim);border:1px solid rgba(248,113,113,.2);border-radius:var(--rs);padding:10px 13px;margin-bottom:16px;display:none;line-height:1.5}
|
|
.lf{margin-bottom:16px}
|
|
.lf label{display:flex;align-items:center;gap:6px;font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.7px;margin-bottom:7px}
|
|
.lf input{width:100%;padding:11px 14px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:13px;outline:none;transition:all .18s}
|
|
.lf input:focus{border-color:var(--blue);background:var(--bg4);box-shadow:0 0 0 3px var(--bdim)}
|
|
.lf input::placeholder{color:var(--t3)}
|
|
.lbtn{width:100%;height:46px;background:linear-gradient(135deg,var(--blue),#6366f1);border:none;border-radius:var(--rs);color:#fff;font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;transition:all .18s;margin-top:8px;letter-spacing:-.1px;box-shadow:0 2px 12px rgba(79,142,247,.35)}
|
|
.lbtn:hover{opacity:.92;transform:translateY(-1px)}
|
|
.lbtn:disabled{background:var(--bg4);box-shadow:none;cursor:not-allowed;transform:none;color:var(--t3)}
|
|
.laccs{margin-top:20px;padding:14px;background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs)}
|
|
.laccs-title{font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.7px;margin-bottom:10px}
|
|
.laccs table{width:100%;border-collapse:collapse;font-size:11px}
|
|
.laccs td{padding:4px 0}
|
|
.laccs td:first-child{color:var(--t2);font-weight:600;width:44%}
|
|
.laccs td:last-child{font-family:var(--mono);color:var(--t3);font-size:10px}
|
|
|
|
/* TOPBAR */
|
|
#topbar{height:52px;background:var(--bg2);border-bottom:1px solid var(--b);display:flex;align-items:center;padding:0 14px;gap:6px;flex-shrink:0;z-index:200}
|
|
.brand{display:flex;align-items:center;gap:10px;margin-right:auto}
|
|
.brand-icon{width:30px;height:30px;background:linear-gradient(135deg,var(--blue),var(--purple));border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
.brand h1{font-size:13px;font-weight:700;letter-spacing:-.3px}
|
|
.brand p{font-size:10px;color:var(--t3);font-family:var(--mono)}
|
|
.upill{display:flex;align-items:center;gap:7px;padding:5px 11px;background:var(--bg3);border:1px solid var(--b2);border-radius:20px}
|
|
.upill .uname{font-size:11px;font-weight:600}
|
|
.rbadge{font-size:9px;font-weight:700;padding:2px 8px;border-radius:20px;text-transform:uppercase;letter-spacing:.5px}
|
|
.r-superadmin{background:var(--pdim);color:var(--purple)}
|
|
.r-pimpinan{background:var(--bdim);color:var(--blue)}
|
|
.r-pengurus{background:var(--gdim);color:var(--green)}
|
|
.r-relawan{background:var(--adim);color:var(--amber)}
|
|
.r-masyarakat{background:var(--b);color:var(--t2)}
|
|
.nav-btn{height:32px;padding:0 12px;border-radius:var(--rs);border:1px solid transparent;background:transparent;color:var(--t2);font-family:var(--font);font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s;white-space:nowrap}
|
|
.nav-btn:hover{background:var(--bg3);color:var(--t)}
|
|
.nav-btn.active{background:var(--bdim);color:var(--blue);border-color:rgba(79,142,247,.3)}
|
|
.ndot{width:6px;height:6px;border-radius:50%;background:var(--red);flex-shrink:0;animation:pulse 1.8s infinite}
|
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.2}}
|
|
.tsep{width:1px;height:22px;background:var(--b2);flex-shrink:0;margin:0 2px}
|
|
|
|
/* BUTTONS */
|
|
.btn{height:32px;padding:0 13px;border-radius:var(--rs);border:1px solid var(--b2);background:var(--bg3);color:var(--t);font-family:var(--font);font-size:12px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:all .15s;white-space:nowrap}
|
|
.btn:hover{background:var(--bg4);border-color:var(--b3)}.btn:active{transform:scale(.97)}.btn:disabled{opacity:.3;cursor:not-allowed;pointer-events:none}
|
|
.btn-primary{background:var(--blue);border-color:var(--blue);color:#fff}.btn-primary:hover{background:#3b7cf0}
|
|
.btn-success{background:var(--green);border-color:var(--green);color:#0a0d14;font-weight:600}.btn-success:hover{background:#2ec48e}
|
|
.btn-danger{background:var(--red);border-color:var(--red);color:#fff}.btn-danger:hover{background:#f25c5c}
|
|
.btn-ghost{background:transparent;border-color:transparent;color:var(--t2)}.btn-ghost:hover{background:var(--bg3);color:var(--t);border-color:var(--b)}
|
|
.btn-sm{height:28px;padding:0 10px;font-size:11px;border-radius:5px}
|
|
.btn-xs{height:24px;padding:0 8px;font-size:10px;border-radius:4px}
|
|
|
|
/* LAYOUT */
|
|
#app{display:flex;flex:1;overflow:hidden}
|
|
#sidebar{width:284px;min-width:284px;background:var(--bg2);border-right:1px solid var(--b);display:flex;flex-direction:column;overflow:hidden;transition:width .25s,min-width .25s}
|
|
#sidebar.off{width:0;min-width:0}
|
|
.stabs{display:flex;border-bottom:1px solid var(--b);flex-shrink:0}
|
|
.stab{flex:1;padding:10px 4px;text-align:center;cursor:pointer;font-size:9px;font-weight:700;color:var(--t3);border-bottom:2px solid transparent;transition:all .15s;text-transform:uppercase;letter-spacing:.5px;white-space:nowrap}
|
|
.stab:hover{color:var(--t2)}.stab.active{color:var(--blue);border-bottom-color:var(--blue)}
|
|
.sbody{flex:1;overflow-y:auto;padding:14px;scrollbar-width:thin}
|
|
|
|
/* STAT CARDS */
|
|
.sgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px}
|
|
.scard{background:var(--bg3);border:1px solid var(--b);border-radius:var(--r);padding:12px 14px;cursor:pointer;transition:all .15s}
|
|
.scard:hover{border-color:var(--b2)}
|
|
.scard .num{font-size:24px;font-weight:700;font-family:var(--mono);line-height:1;margin-bottom:4px}
|
|
.scard .lbl{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.5px}
|
|
.cb{color:var(--blue)}.cr{color:var(--red)}.cg{color:var(--green)}.ca{color:var(--amber)}.cp{color:var(--purple)}.cro{color:var(--rose)}
|
|
.slbl{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.8px;margin:16px 0 9px;display:flex;align-items:center;justify-content:space-between}
|
|
|
|
/* ITEM CARDS */
|
|
.ilist{display:flex;flex-direction:column;gap:5px}
|
|
.icard{background:var(--bg3);border:1px solid var(--b);border-radius:var(--r);padding:10px 12px;cursor:pointer;transition:all .15s}
|
|
.icard:hover{border-color:var(--b2);background:var(--bg4)}
|
|
.ihead{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:3px}
|
|
.iname{font-size:12px;font-weight:600;color:var(--t);line-height:1.3}
|
|
.isub{font-size:11px;color:var(--t3);margin-bottom:3px;line-height:1.4}
|
|
.imeta{font-size:10px;color:var(--t3);font-family:var(--mono)}
|
|
.badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600;white-space:nowrap;flex-shrink:0}
|
|
.b-g{background:var(--gdim);color:var(--green)}.b-r{background:var(--rdim);color:var(--red)}
|
|
.b-b{background:var(--bdim);color:var(--blue)}.b-a{background:var(--adim);color:var(--amber)}
|
|
.b-p{background:var(--pdim);color:var(--purple)}.b-ro{background:var(--rosedim);color:var(--rose)}
|
|
.b-gray{background:var(--b2);color:var(--t2)}
|
|
.frow{display:flex;gap:6px;margin-bottom:10px}
|
|
.frow input,.frow select{flex:1;height:30px;padding:0 10px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:11px;outline:none;transition:all .15s}
|
|
.frow input:focus,.frow select:focus{border-color:var(--blue)}.frow input::placeholder{color:var(--t3)}
|
|
.frow select option{background:var(--bg3)}
|
|
.prog{height:3px;background:var(--bg5);border-radius:2px;overflow:hidden;margin-top:6px}
|
|
.pfill{height:100%;border-radius:2px;transition:width .4s}
|
|
|
|
/* MAP */
|
|
#map-container{flex:1;position:relative;display:flex;flex-direction:column;overflow:hidden}
|
|
#map{flex:1}
|
|
.leaflet-container{background:#0d1120;font-family:var(--font)}
|
|
.leaflet-control-attribution{background:rgba(10,13,20,.9)!important;color:var(--t3)!important;font-size:9px!important}
|
|
.leaflet-control-zoom{border:1px solid var(--b2)!important;border-radius:var(--rs)!important;overflow:hidden}
|
|
.leaflet-control-zoom a{background:var(--bg2)!important;border:none!important;border-bottom:1px solid var(--b)!important;color:var(--t2)!important;width:28px!important;height:28px!important;line-height:28px!important}
|
|
.leaflet-control-zoom a:last-child{border-bottom:none!important}
|
|
.leaflet-control-zoom a:hover{background:var(--bg4)!important;color:var(--t)!important}
|
|
.leaflet-popup-content-wrapper{background:var(--bg2)!important;border:1px solid var(--b2)!important;border-radius:var(--r)!important;box-shadow:var(--shadow)!important;color:var(--t)!important;font-family:var(--font)!important;padding:0!important}
|
|
.leaflet-popup-content{margin:0!important}
|
|
.leaflet-popup-tip{background:var(--bg2)!important}
|
|
.leaflet-popup-close-button{color:var(--t3)!important;top:10px!important;right:12px!important;font-size:16px!important}
|
|
.mpop{padding:14px 16px;min-width:200px;max-width:256px}
|
|
.mpop-name{font-size:13px;font-weight:700;margin-bottom:5px;padding-right:20px;letter-spacing:-.2px}
|
|
.mpop-info{font-size:11px;color:var(--t2);margin-bottom:10px;line-height:1.65}
|
|
.mpop-btn{width:100%;padding:7px;background:var(--bdim);border:1px solid rgba(79,142,247,.25);border-radius:var(--rs);color:var(--blue);font-family:var(--font);font-size:11px;font-weight:600;cursor:pointer;transition:all .15s}
|
|
.mpop-btn:hover{background:var(--blue);color:#fff}
|
|
|
|
/* MAP LAYER PANEL */
|
|
#mcp{position:absolute;top:12px;right:12px;z-index:500;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--r);padding:12px 14px;min-width:190px;box-shadow:0 4px 18px rgba(0,0,0,.4)}
|
|
.mcp-h{font-size:9px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.8px;margin-bottom:10px}
|
|
.lyr{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:7px;font-size:11px;color:var(--t2)}
|
|
.lyr:last-of-type{margin-bottom:0}
|
|
.lyr-l{display:flex;align-items:center;gap:7px}
|
|
.ldot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
|
|
.tog{width:30px;height:16px;background:var(--bg5);border-radius:8px;cursor:pointer;position:relative;transition:background .2s;flex-shrink:0;border:1px solid var(--b2)}
|
|
.tog.on{background:var(--blue);border-color:var(--blue)}
|
|
.tog::after{content:'';position:absolute;width:11px;height:11px;background:#fff;border-radius:50%;top:2px;left:2px;transition:left .2s}
|
|
.tog.on::after{left:15px}
|
|
.mcp-div{height:1px;background:var(--b);margin:10px 0}
|
|
.rad-lbl{display:flex;justify-content:space-between;font-size:10px;color:var(--t3);margin-bottom:5px}
|
|
.rad-lbl span:last-child{font-family:var(--mono);color:var(--blue);font-weight:600}
|
|
input[type=range]{width:100%;height:3px;-webkit-appearance:none;background:var(--bg5);border-radius:2px;outline:none;cursor:pointer}
|
|
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:13px;height:13px;border-radius:50%;background:var(--blue);border:2px solid var(--bg2);box-shadow:0 0 0 2px var(--bglow)}
|
|
|
|
/* MODALS */
|
|
.overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.7);z-index:9999;align-items:center;justify-content:center;backdrop-filter:blur(3px)}
|
|
.overlay.open{display:flex;animation:fi .18s ease}
|
|
@keyframes fi{from{opacity:0}to{opacity:1}}
|
|
.modal{background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rl);padding:24px;width:90%;max-width:520px;max-height:90vh;overflow-y:auto;scrollbar-width:thin;animation:su .2s ease;box-shadow:var(--shadow)}
|
|
.modal.wide{max-width:700px}
|
|
@keyframes su{from{transform:translateY(16px);opacity:0}to{transform:none;opacity:1}}
|
|
.mtitle{font-size:16px;font-weight:700;margin-bottom:20px;display:flex;align-items:center;gap:10px;letter-spacing:-.3px}
|
|
.mico{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;flex-shrink:0}
|
|
.mi-b{background:var(--bdim)}.mi-g{background:var(--gdim)}.mi-a{background:var(--adim)}.mi-r{background:var(--rdim)}
|
|
.fg{margin-bottom:14px}
|
|
.fg label{display:block;font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px}
|
|
.fg input,.fg select,.fg textarea{width:100%;padding:9px 12px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:12px;outline:none;transition:all .18s}
|
|
.fg input:focus,.fg select:focus,.fg textarea:focus{border-color:var(--blue);background:var(--bg4);box-shadow:0 0 0 3px var(--bdim)}
|
|
.fg input::placeholder{color:var(--t3)}.fg input[readonly]{background:var(--bg);color:var(--t2)}
|
|
.fg select option{background:var(--bg3)}.fg textarea{resize:vertical;min-height:72px;line-height:1.6}
|
|
.fr2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
|
.fr3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px}
|
|
.fhint{font-size:10px;color:var(--t3);margin-top:6px;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
|
|
.wbox{background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);border-radius:var(--rs);padding:9px 12px;font-size:11px;color:var(--amber);margin-top:8px;line-height:1.5;display:none}
|
|
.mfooter{display:flex;gap:8px;justify-content:flex-end;margin-top:20px;padding-top:16px;border-top:1px solid var(--b)}
|
|
.dtbl{width:100%;border-collapse:collapse;font-size:12px}
|
|
.dtbl td{padding:7px 0;vertical-align:top;border-bottom:1px solid var(--b);line-height:1.5}
|
|
.dtbl td:first-child{color:var(--t3);width:40%;padding-right:12px;font-size:11px}
|
|
.dtbl tr:last-child td{border-bottom:none}
|
|
.dklist{background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);overflow:hidden;max-height:200px;overflow-y:auto}
|
|
.dkrow{display:flex;justify-content:space-between;align-items:center;padding:7px 11px;border-bottom:1px solid var(--b);font-size:11px}
|
|
.dkrow:last-child{border-bottom:none}
|
|
|
|
/* CHAT */
|
|
.chat-msgs{overflow-y:auto;display:flex;flex-direction:column;gap:10px;min-height:300px;max-height:420px;padding:2px 0}
|
|
.cmsg{max-width:82%}
|
|
.cmsg.mine{align-self:flex-end}.cmsg.other{align-self:flex-start}
|
|
.cmsg-meta{font-size:9px;color:var(--t3);margin-bottom:3px;font-weight:700;text-transform:uppercase;letter-spacing:.3px}
|
|
.cmsg.mine .cmsg-meta{text-align:right}
|
|
.cmsg-bubble{padding:9px 13px;border-radius:10px;font-size:12px;line-height:1.6}
|
|
.cmsg.mine .cmsg-bubble{background:var(--blue);color:#fff;border-bottom-right-radius:3px}
|
|
.cmsg.other .cmsg-bubble{background:var(--bg4);border:1px solid var(--b2);border-bottom-left-radius:3px}
|
|
.cmsg-time{font-size:9px;opacity:.55;margin-top:3px;font-family:var(--mono)}
|
|
.cmsg.mine .cmsg-time{text-align:right}
|
|
.cinp-row{display:flex;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid var(--b)}
|
|
.cinp{flex:1;padding:9px 12px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-family:var(--font);font-size:12px;outline:none;resize:none;height:38px;transition:all .15s;line-height:1.4}
|
|
.cinp:focus{border-color:var(--blue);background:var(--bg4)}
|
|
|
|
/* DATA TABLE */
|
|
.dtable{width:100%;border-collapse:collapse;font-size:12px}
|
|
.dtable thead tr{background:var(--bg3)}
|
|
.dtable th{padding:9px 12px;text-align:left;font-weight:700;font-size:9px;text-transform:uppercase;letter-spacing:.5px;color:var(--t2);border-bottom:1px solid var(--b2);white-space:nowrap}
|
|
.dtable td{padding:9px 12px;border-bottom:1px solid var(--b);vertical-align:middle;line-height:1.4}
|
|
.dtable tr:hover td{background:rgba(255,255,255,.015)}
|
|
.dtable tr:last-child td{border-bottom:none}
|
|
|
|
/* PAGE */
|
|
.pwrap{flex:1;overflow-y:auto;background:var(--bg);padding:20px}
|
|
.pinner{max-width:960px;margin:0 auto}
|
|
.pheader{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;gap:12px;flex-wrap:wrap}
|
|
.ptitle{font-size:20px;font-weight:700;letter-spacing:-.5px}
|
|
.ptitle small{font-size:13px;font-weight:400;color:var(--t3);margin-left:8px}
|
|
.scard2{background:var(--bg2);border:1px solid var(--b);border-radius:var(--r);padding:16px;margin-bottom:14px}
|
|
.scard2 h3{font-size:13px;font-weight:600;margin-bottom:14px;letter-spacing:-.2px;display:flex;align-items:center;gap:8px;color:var(--t2)}
|
|
|
|
/* URGENCY */
|
|
.u-darurat{background:var(--rosedim);color:var(--rose)}
|
|
.u-tinggi{background:var(--rdim);color:var(--red)}
|
|
.u-sedang{background:var(--adim);color:var(--amber)}
|
|
.u-rendah{background:var(--gdim);color:var(--green)}
|
|
|
|
/* TOAST */
|
|
#toast{position:fixed;bottom:20px;right:20px;z-index:99999;display:flex;flex-direction:column;gap:8px;pointer-events:none;max-width:340px}
|
|
.ti{padding:11px 15px;border-radius:var(--rs);font-size:12px;font-weight:500;animation:tin .25s ease;pointer-events:auto;line-height:1.5;box-shadow:0 4px 16px rgba(0,0,0,.4)}
|
|
@keyframes tin{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
|
|
.t-ok{background:#15803d;color:#fff}.t-err{background:#b91c1c;color:#fff}
|
|
.t-info{background:var(--bg4);color:var(--t);border:1px solid var(--b2)}.t-warn{background:#92400e;color:#fef3c7}
|
|
|
|
/* LOADING */
|
|
#loading{position:fixed;inset:0;background:rgba(10,13,20,.8);z-index:99998;display:none;align-items:center;justify-content:center;flex-direction:column;gap:14px}
|
|
#loading.show{display:flex}
|
|
.spin{width:32px;height:32px;border:2px solid var(--b2);border-top-color:var(--blue);border-radius:50%;animation:sp .65s linear infinite}
|
|
@keyframes sp{to{transform:rotate(360deg)}}
|
|
.empty{text-align:center;padding:32px 16px;color:var(--t3)}
|
|
.empty svg{opacity:.2;margin:0 auto 10px;display:block}
|
|
.empty p{font-size:12px}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="loading"><div class="spin"></div><span style="font-size:12px;color:var(--t2)">Memuat sistem...</span></div>
|
|
<div id="toast"></div>
|
|
|
|
<!-- ===== LOGIN ===== -->
|
|
<div id="login-screen">
|
|
<div class="lcard">
|
|
<div class="lbrand">
|
|
<div class="lbrand-icon">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg>
|
|
</div>
|
|
<div><h1>WebGIS Pengentasan Kemiskinan</h1><p>Kota Pontianak · v3.0</p></div>
|
|
</div>
|
|
<div class="lheading">Masuk ke Sistem</div>
|
|
<p class="lsub">Masukkan username dan password yang telah diberikan oleh administrator sistem.</p>
|
|
<div class="lerr" id="lerr"></div>
|
|
<div class="lf">
|
|
<label>
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
Username
|
|
</label>
|
|
<input type="text" id="lu" placeholder="Masukkan username" autocomplete="username">
|
|
</div>
|
|
<div class="lf">
|
|
<label>
|
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
|
Password
|
|
</label>
|
|
<input type="password" id="lp" placeholder="Masukkan password" autocomplete="current-password">
|
|
</div>
|
|
<button class="lbtn" id="lbtn" onclick="doLogin()">Masuk ke Sistem</button>
|
|
<div class="laccs">
|
|
<div class="laccs-title">Akun Tersedia — semua password: <span style="font-family:var(--mono);color:var(--blue)">password</span></div>
|
|
<table>
|
|
<tr><td>Super Admin</td><td>superadmin</td></tr>
|
|
<tr><td>Pimpinan Daerah</td><td>pimpinan1</td></tr>
|
|
<tr><td>Pengurus Masjid</td><td>pengurus1</td></tr>
|
|
<tr><td>Pengurus Gereja</td><td>pengurus2</td></tr>
|
|
<tr><td>Pengurus Al-Falah</td><td>pengurus3</td></tr>
|
|
<tr><td>Relawan Lapangan</td><td>relawan1</td></tr>
|
|
<tr><td>Masyarakat Umum</td><td>masyarakat1</td></tr>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== TOPBAR ===== -->
|
|
<div id="topbar">
|
|
<button class="btn btn-ghost btn-sm" onclick="toggleSB()" style="padding:0 9px">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
|
|
</button>
|
|
<div class="brand">
|
|
<div class="brand-icon">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg>
|
|
</div>
|
|
<div><h1>WebGIS Kemiskinan — Kota Pontianak</h1><p>Sistem Pemetaan & Distribusi Bantuan Sosial Berbasis Komunitas</p></div>
|
|
</div>
|
|
<button class="nav-btn" id="nb-peta" onclick="goPage('peta')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/><line x1="9" y1="3" x2="9" y2="18"/><line x1="15" y1="6" x2="15" y2="21"/></svg>
|
|
Peta
|
|
</button>
|
|
<button class="nav-btn" id="nb-penduduk" onclick="goPage('penduduk')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
|
Penduduk
|
|
</button>
|
|
<button class="nav-btn" id="nb-bantuan" onclick="goPage('bantuan')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>
|
|
Bantuan
|
|
</button>
|
|
<button class="nav-btn" id="nb-laporan" onclick="goPage('laporan')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
|
Laporan
|
|
<span class="ndot" id="dlap" style="display:none"></span>
|
|
</button>
|
|
<button class="nav-btn" id="nb-chat" onclick="goPage('chat')">
|
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
|
|
Koordinasi
|
|
<span class="ndot" id="dchat" style="display:none"></span>
|
|
</button>
|
|
<div class="tsep"></div>
|
|
<div class="upill">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="color:var(--t3)"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
|
<span class="uname" id="uname">—</span>
|
|
<span class="rbadge" id="urole">—</span>
|
|
<button class="btn btn-ghost btn-xs" onclick="doLogout()" title="Keluar" style="padding:0 6px;margin-left:2px">
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MAIN APP ===== -->
|
|
<div id="app">
|
|
<div id="sidebar">
|
|
<div class="stabs" id="stabs"></div>
|
|
<div class="sbody" id="sbody"></div>
|
|
</div>
|
|
<div id="map-container">
|
|
<div id="map"></div>
|
|
<div id="mcp">
|
|
<div class="mcp-h">Layer Peta</div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#f87171"></div>Penduduk Miskin</div><div class="tog on" id="ly-miskin" onclick="togLyr(this,'miskin')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fbbf24"></div>Penduduk Rentan</div><div class="tog on" id="ly-rentan" onclick="togLyr(this,'rentan')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#34d399"></div>Penduduk Mampu</div><div class="tog" id="ly-mampu" onclick="togLyr(this,'mampu')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#4f8ef7"></div>Rumah Ibadah</div><div class="tog on" id="ly-ri" onclick="togLyr(this,'ri')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fb7185"></div>Laporan Warga</div><div class="tog on" id="ly-lap" onclick="togLyr(this,'laporan')"></div></div>
|
|
<div class="mcp-div"></div>
|
|
<div class="rad-lbl"><span>Radius Distribusi</span><span id="rv">300 m</span></div>
|
|
<input type="range" min="100" max="1000" step="50" value="300" id="rsl" oninput="setRad(this.value)">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MODAL KK ===== -->
|
|
<div class="overlay" id="modal-kk">
|
|
<div class="modal">
|
|
<div class="mtitle"><div class="mico mi-b"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--blue)" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></div><span id="mkk-t">Tambah Kartu Keluarga</span></div>
|
|
<div class="fr2"><div class="fg"><label>No. KK (16 digit)</label><input type="text" id="kk-nokk" maxlength="16" placeholder="617101XXXXXXXXXX"></div><div class="fg"><label>Nama Kepala Keluarga</label><input type="text" id="kk-nama" placeholder="Nama sesuai KK"></div></div>
|
|
<div class="fg"><label>Alamat Lengkap</label><input type="text" id="kk-alamat" placeholder="Jl. ..."></div>
|
|
<div class="fr3"><div class="fg"><label>RT</label><input type="text" id="kk-rt" maxlength="4" placeholder="03"></div><div class="fg"><label>RW</label><input type="text" id="kk-rw" maxlength="4" placeholder="05"></div><div class="fg"><label>Status Ekonomi</label><select id="kk-se"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div></div>
|
|
<div class="fr2"><div class="fg"><label>Kelurahan</label><input type="text" id="kk-kel" placeholder="Saigon"></div><div class="fg"><label>Kecamatan</label><input type="text" id="kk-kec" placeholder="Pontianak Timur"></div></div>
|
|
<div class="fr2"><div class="fg"><label>Latitude</label><input type="number" id="kk-lat" step="0.000001" placeholder="-0.021100"></div><div class="fg"><label>Longitude</label><input type="number" id="kk-lng" step="0.000001" placeholder="109.344500"></div></div>
|
|
<div class="fhint">
|
|
<button class="btn btn-sm" onclick="pickFor('kk')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg> Pilih di Peta</button>
|
|
<button class="btn btn-sm" onclick="gpsTo('kk')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg> GPS Saya</button>
|
|
</div>
|
|
<div class="fg" style="margin-top:12px"><label>Rumah Ibadah Terdekat</label><select id="kk-ri"><option value="">-- Pilih --</option></select></div>
|
|
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('kk')">Batal</button><button class="btn btn-primary" onclick="saveKK()">Simpan KK</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MODAL PENDUDUK ===== -->
|
|
<div class="overlay" id="modal-p">
|
|
<div class="modal wide">
|
|
<div class="mtitle"><div class="mico mi-g"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div><span id="mp-t">Tambah Data Penduduk</span></div>
|
|
<div class="fr2"><div class="fg"><label>NIK (16 digit)</label><input type="text" id="p-nik" maxlength="16" placeholder="6171XXXXXXXXXXXX"></div><div class="fg"><label>Nama Lengkap</label><input type="text" id="p-nama" placeholder="Nama sesuai KTP"></div></div>
|
|
<div class="fr3"><div class="fg"><label>Jenis Kelamin</label><select id="p-jk"><option value="L">Laki-laki</option><option value="P">Perempuan</option></select></div><div class="fg"><label>Tanggal Lahir</label><input type="date" id="p-tgl" oninput="cekPend()"></div><div class="fg"><label>Status dalam KK</label><select id="p-sk"><option value="kepala">Kepala KK</option><option value="istri">Istri</option><option value="anak">Anak</option><option value="orang_tua">Orang Tua</option><option value="lainnya">Lainnya</option></select></div></div>
|
|
<div class="fr3"><div class="fg"><label>Status Perkawinan</label><select id="p-sp"><option value="belum_kawin">Belum Kawin</option><option value="kawin">Kawin</option><option value="cerai_hidup">Cerai Hidup</option><option value="cerai_mati">Cerai Mati</option></select></div><div class="fg"><label>Status Hidup</label><select id="p-sh"><option value="hidup">Hidup</option><option value="meninggal">Meninggal</option></select></div><div class="fg"><label>Status Ekonomi</label><select id="p-se"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div></div>
|
|
<div class="fr3"><div class="fg"><label>Pendidikan Terakhir</label><select id="p-pend" onchange="cekPend()"><option value="tidak_sekolah">Tidak Sekolah</option><option value="sd">SD</option><option value="smp">SMP</option><option value="sma">SMA</option><option value="smk">SMK</option><option value="d3">D3</option><option value="s1">S1</option><option value="s2">S2</option><option value="s3">S3</option></select></div><div class="fg"><label>Status Pendidikan</label><select id="p-spend" onchange="cekPend()"><option value="tidak_sekolah">Sudah / Tidak Sekolah</option><option value="masih_sekolah">Masih Sekolah</option></select></div><div class="fg"><label>Kondisi Kesehatan</label><select id="p-kes"><option value="sehat">Sehat</option><option value="sakit_ringan">Sakit Ringan</option><option value="sakit_berat">Sakit Berat</option><option value="disabilitas">Disabilitas</option></select></div></div>
|
|
<div class="wbox" id="wp"></div>
|
|
<div class="fr2"><div class="fg"><label>Pekerjaan</label><input type="text" id="p-kerja" placeholder="Buruh Harian, Nelayan..."></div><div class="fg"><label>Penghasilan / Bulan (Rp)</label><input type="number" id="p-gaji" placeholder="0" min="0"></div></div>
|
|
<div class="fg"><label>Kartu Keluarga (KK)</label><select id="p-kk"><option value="">-- Pilih KK --</option></select></div>
|
|
<div class="fg"><label>Catatan</label><textarea id="p-cat" placeholder="Kondisi khusus atau keterangan tambahan..."></textarea></div>
|
|
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('p')">Batal</button><button class="btn btn-success" onclick="saveP()">Simpan Penduduk</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MODAL LAPORAN ===== -->
|
|
<div class="overlay" id="modal-lap">
|
|
<div class="modal">
|
|
<div class="mtitle"><div class="mico mi-r"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--red)" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></div>Buat Laporan Kondisi Warga</div>
|
|
<div class="fr2"><div class="fg"><label>Nama Pelapor <span style="opacity:.5;font-weight:400">(opsional)</span></label><input type="text" id="lap-nama" placeholder="Nama atau Anonim"></div><div class="fg"><label>Telepon <span style="opacity:.5;font-weight:400">(opsional)</span></label><input type="text" id="lap-telp" placeholder="08xx-xxxx-xxxx"></div></div>
|
|
<div class="fg"><label>Deskripsi Kondisi yang Dilaporkan</label><textarea id="lap-desc" placeholder="Jelaskan kondisi warga secara detail..."></textarea></div>
|
|
<div class="fg"><label>Alamat Lokasi</label><input type="text" id="lap-alamat" placeholder="Jl. ... RT/RW Kelurahan ..."></div>
|
|
<div class="fr2"><div class="fg"><label>Latitude</label><input type="number" id="lap-lat" step="0.000001"></div><div class="fg"><label>Longitude</label><input type="number" id="lap-lng" step="0.000001"></div></div>
|
|
<div class="fhint">
|
|
<button class="btn btn-sm" onclick="pickFor('lap')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="10" r="3"/><path d="M12 2a8 8 0 0 0-8 8c0 5.25 8 14 8 14s8-8.75 8-14a8 8 0 0 0-8-8z"/></svg> Pilih di Peta</button>
|
|
<button class="btn btn-sm" onclick="gpsTo('lap')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4m0 12v4M2 12h4m12 0h4"/></svg> GPS Saya</button>
|
|
</div>
|
|
<div class="fr2" style="margin-top:12px"><div class="fg"><label>Tingkat Urgensi</label><select id="lap-urg"><option value="rendah">Rendah</option><option value="sedang" selected>Sedang</option><option value="tinggi">Tinggi</option><option value="darurat">DARURAT</option></select></div><div class="fg"><label>Foto <span style="opacity:.5;font-weight:400">maks 5MB</span></label><input type="file" id="lap-foto" accept="image/*" style="padding:6px;font-size:11px"></div></div>
|
|
<div class="mfooter"><button class="btn btn-ghost" onclick="closeM('lap')">Batal</button><button class="btn btn-danger" onclick="saveLap()">Kirim Laporan</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== MODAL DETAIL ===== -->
|
|
<div class="overlay" id="modal-det">
|
|
<div class="modal wide">
|
|
<div class="mtitle" id="det-h"></div>
|
|
<div id="det-b"></div>
|
|
<div class="mfooter" id="det-f"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script>
|
|
const EP={auth:'api/auth.php',p:'api/penduduk.php',kk:'api/keluarga.php',ri:'api/rumah_ibadah.php',b:'api/bantuan.php',lap:'api/laporan.php',chat:'api/chat.php',stats:'api/stats.php'};
|
|
const PT=[-0.0263,109.3425];
|
|
const EC={miskin:'#f87171',rentan:'#fbbf24',mampu:'#34d399'};
|
|
const UC={darurat:'#fb7185',tinggi:'#f87171',sedang:'#fbbf24',rendah:'#34d399'};
|
|
let map,cu=null,riD=[],pD=[],lapD=[],bD=[],kkD=[];
|
|
let lyrs={miskin:true,rentan:true,mampu:false,ri:true,laporan:true};
|
|
let rad=300,pick=null,eKK=null,eP=null;
|
|
let mk={ri:[],p:[],lap:[],c:[]};
|
|
let cpoll=null,lastC='1970-01-01 00:00:00',curPg='peta';
|
|
|
|
const ld=v=>document.getElementById('loading').classList.toggle('show',v);
|
|
function toast(m,t='info'){const e=document.createElement('div');e.className='ti t-'+t;e.textContent=m;document.getElementById('toast').appendChild(e);setTimeout(()=>e.remove(),3800);}
|
|
async function rq(ep,method='GET',body=null,qs=''){
|
|
const r=await fetch(ep+(qs?'?'+qs:''),{method,headers:{'Content-Type':'application/json'},body:body?JSON.stringify(body):undefined});
|
|
const d=await r.json();if(!d.success)throw new Error(d.message||'Error');return d.data;
|
|
}
|
|
|
|
// SVG helpers
|
|
const riSVG=j=>{const m=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 20h20M4 20V10c0-1.1.4-2.1 1.2-2.8L12 3l6.8 4.2c.8.7 1.2 1.7 1.2 2.8v10"/><path d="M9 20v-5a3 3 0 0 1 6 0v5"/><path d="M12 3V1m-4 3L6 2m10 2 2-2"/></svg>`;const g=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M3 20h18M5 20V9.5L12 4l7 5.5V20"/><path d="M9 20v-5h6v5"/><line x1="12" y1="4" x2="12" y2="1"/><line x1="10" y1="2" x2="14" y2="2"/></svg>`;const v=`<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M2 20h20M4 20v-7l8-9 8 9v7"/><path d="M9 20v-4h6v4"/><path d="M1 13h3m16 0h3"/></svg>`;if(!j)return m;const k=j.toLowerCase();return k==='masjid'?m:k==='gereja'?g:v;};
|
|
const icoUser=`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
|
|
const icoHome=`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>`;
|
|
const icoWarn=`<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`;
|
|
const icoGift=`<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 12 20 22 4 22 4 12"/><rect x="2" y="7" width="20" height="5"/><line x1="12" y1="22" x2="12" y2="7"/><path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/><path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/></svg>`;
|
|
|
|
// LOGIN
|
|
async function doLogin(){
|
|
const u=document.getElementById('lu').value.trim(),p=document.getElementById('lp').value;
|
|
const err=document.getElementById('lerr'),btn=document.getElementById('lbtn');
|
|
err.style.display='none';
|
|
if(!u||!p){err.textContent='Username dan password wajib diisi.';err.style.display='block';return;}
|
|
btn.disabled=true;btn.textContent='Memverifikasi...';
|
|
try{
|
|
cu=await rq(EP.auth,'POST',{username:u,password:p},'action=login');
|
|
applyRole();document.getElementById('login-screen').classList.add('hidden');
|
|
toast('Selamat datang, '+cu.nama_lengkap,'ok');
|
|
initApp();
|
|
}catch(e){err.textContent=e.message;err.style.display='block';btn.disabled=false;btn.textContent='Masuk ke Sistem';}
|
|
}
|
|
async function doLogout(){
|
|
try{await rq(EP.auth,'POST',null,'action=logout');}catch(e){}
|
|
if(cpoll)clearInterval(cpoll);cu=null;if(map){map.remove();map=null;}
|
|
document.getElementById('login-screen').classList.remove('hidden');
|
|
document.getElementById('lp').value='';document.getElementById('lerr').style.display='none';
|
|
document.getElementById('lbtn').disabled=false;document.getElementById('lbtn').textContent='Masuk ke Sistem';
|
|
}
|
|
function applyRole(){
|
|
if(!cu)return;
|
|
document.getElementById('uname').textContent=cu.nama_lengkap;
|
|
const rb=document.getElementById('urole');rb.textContent=cu.role_nama;rb.className='rbadge r-'+cu.role;
|
|
const H=id=>{const e=document.getElementById(id);if(e)e.style.display='none';};
|
|
if(cu.role==='masyarakat'){H('nb-bantuan');H('nb-chat');}
|
|
if(cu.role==='pimpinan'){H('nb-bantuan');H('nb-chat');}
|
|
}
|
|
|
|
// APP INIT
|
|
function initApp(){
|
|
initMap();loadAll();
|
|
if(['superadmin','pengurus','relawan'].includes(cu.role)) cpoll=setInterval(pollChat,8000);
|
|
buildSB();goPage('peta');
|
|
}
|
|
|
|
// MAP
|
|
function initMap(){
|
|
map=L.map('map',{zoomControl:true}).setView(PT,13);
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© OSM © CARTO',maxZoom:19}).addTo(map);
|
|
map.on('click',onMapClick);
|
|
}
|
|
function onMapClick(e){
|
|
if(!pick)return;
|
|
const{lat,lng}=e.latlng;
|
|
if(pick==='kk'){document.getElementById('kk-lat').value=lat.toFixed(6);document.getElementById('kk-lng').value=lng.toFixed(6);map.closePopup();openM('kk');}
|
|
if(pick==='lap'){document.getElementById('lap-lat').value=lat.toFixed(6);document.getElementById('lap-lng').value=lng.toFixed(6);map.closePopup();openM('lap');}
|
|
pick=null;map.getContainer().style.cursor='';
|
|
}
|
|
function pickFor(t){pick=t;map.getContainer().style.cursor='crosshair';closeM(t==='kk'?'kk':'lap');L.popup({closeButton:false}).setLatLng(map.getCenter()).setContent('<div style="background:var(--bg3);border:1px solid var(--b2);border-radius:8px;padding:10px 14px;color:var(--t);font-size:12px">Klik lokasi di peta untuk memilih koordinat</div>').openOn(map);}
|
|
function gpsTo(t){if(!navigator.geolocation){toast('GPS tidak didukung','err');return;}navigator.geolocation.getCurrentPosition(pos=>{const pref=t==='kk'?'kk-':'lap-';document.getElementById(pref+'lat').value=pos.coords.latitude.toFixed(6);document.getElementById(pref+'lng').value=pos.coords.longitude.toFixed(6);},()=>toast('Gagal mengambil GPS','err'));}
|
|
function setRad(v){rad=parseInt(v);document.getElementById('rv').textContent=v+' m';mk.c.forEach(c=>c.setRadius(rad));}
|
|
function togLyr(el,t){el.classList.toggle('on');lyrs[t]=el.classList.contains('on');rerender();}
|
|
|
|
// LOAD
|
|
async function loadAll(){
|
|
ld(true);
|
|
try{
|
|
const[ri,p,lap,b,kk]=await Promise.all([rq(EP.ri),rq(EP.p),rq(EP.lap),rq(EP.b),rq(EP.kk)]);
|
|
riD=ri||[];pD=p||[];lapD=lap||[];bD=b||[];kkD=kk||[];
|
|
rerender();rSB();checkAlerts();toast('Data berhasil dimuat','ok');
|
|
}catch(e){toast('Gagal memuat data: '+e.message,'err');}
|
|
finally{ld(false);}
|
|
}
|
|
|
|
// MARKERS
|
|
function clrMk(){[...mk.ri,...mk.p,...mk.lap].forEach(m=>map&&map.removeLayer(m));mk.c.forEach(c=>map&&map.removeLayer(c));mk.ri=[];mk.p=[];mk.lap=[];mk.c=[];}
|
|
function rerender(){
|
|
clrMk();
|
|
if(lyrs.ri) riD.forEach(addRI);
|
|
pD.forEach(p=>{if(lyrs[p.status_ekonomi]) addP(p);});
|
|
if(lyrs.laporan) lapD.forEach(l=>{if(l.lat&&l.lng) addLap(l);});
|
|
}
|
|
function addRI(ri){
|
|
const m=L.marker([ri.lat,ri.lng],{icon:L.divIcon({html:`<div style="width:34px;height:34px;background:#111520;border:2px solid #4f8ef7;border-radius:50%;display:flex;align-items:center;justify-content:center;color:#4f8ef7;box-shadow:0 0 0 4px rgba(79,142,247,.18),0 3px 10px rgba(0,0,0,.6)">${riSVG(ri.jenis)}</div>`,iconSize:[34,34],iconAnchor:[17,17],className:''})}).addTo(map);
|
|
m.bindPopup(`<div class="mpop"><div class="mpop-name">${ri.nama}</div><div class="mpop-info">${ri.jenis} · ${ri.kelurahan||'—'}<br>${ri.kontak||'—'}<br><strong>${ri.kk_miskin||0}</strong> KK miskin dalam jangkauan</div><button class="mpop-btn" onclick="detRI(${ri.id});map.closePopup()">Lihat Detail</button></div>`,{maxWidth:240});
|
|
const c=L.circle([ri.lat,ri.lng],{radius:rad,color:'#4f8ef7',fillColor:'#4f8ef7',fillOpacity:.05,dashArray:'5,5',weight:1.5}).addTo(map);
|
|
mk.ri.push(m);mk.c.push(c);
|
|
}
|
|
function addP(p){
|
|
const col=EC[p.status_ekonomi]||'#888';const sk=p.kondisi_kesehatan&&p.kondisi_kesehatan!=='sehat';
|
|
const m=L.marker([p.lat||0,p.lng||0],{icon:L.divIcon({html:`<div style="width:12px;height:12px;background:${col};border-radius:50%;border:2px solid rgba(255,255,255,.8);box-shadow:0 0 0 3px ${sk?'rgba(251,113,133,.5)':'rgba(255,255,255,.1)'},0 2px 6px rgba(0,0,0,.5)"></div>`,iconSize:[12,12],iconAnchor:[6,6],className:''})}).addTo(map);
|
|
m.bindPopup(`<div class="mpop"><div class="mpop-name">${p.nama}</div><div class="mpop-info">NIK: ${p.nik||'—'}<br>${p.umur||'?'} thn · ${p.profil_umur||'—'}<br>Ekonomi: <strong style="color:${col}">${(p.status_ekonomi||'').toUpperCase()}</strong><br>Kesehatan: ${p.kondisi_kesehatan||'sehat'}<br>Bantuan: ${p.total_bantuan||0}x</div><button class="mpop-btn" onclick="detP(${p.id});map.closePopup()">Lihat Detail Lengkap</button></div>`,{maxWidth:240});
|
|
mk.p.push(m);
|
|
}
|
|
function addLap(l){
|
|
const col=UC[l.urgensi]||'#888';
|
|
const m=L.marker([l.lat,l.lng],{icon:L.divIcon({html:`<div style="width:17px;height:17px;background:${col};border-radius:50%;border:2.5px solid #fff;box-shadow:0 0 0 3px ${col}44,0 3px 8px rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center">${l.urgensi==='darurat'?'<span style="font-size:9px;color:#fff;font-weight:900;line-height:1">!</span>':''}</div>`,iconSize:[17,17],iconAnchor:[8,8],className:''})}).addTo(map);
|
|
m.bindPopup(`<div class="mpop"><div class="mpop-name">Laporan Warga</div><div class="mpop-info"><span class="badge u-${l.urgensi}" style="display:inline-flex;margin-bottom:5px">${(l.urgensi||'').toUpperCase()}</span><br>${(l.deskripsi||'').substring(0,80)}${(l.deskripsi||'').length>80?'…':''}</div><button class="mpop-btn" onclick="detLap(${l.id});map.closePopup()">Lihat Detail</button></div>`,{maxWidth:240});
|
|
mk.lap.push(m);
|
|
}
|
|
|
|
// SIDEBAR
|
|
function buildSB(){
|
|
document.getElementById('stabs').innerHTML=['Ringkasan','Ibadah','Keluarga','Prioritas'].map((t,i)=>`<div class="stab${i===0?' active':''}" onclick="showTab(${i})">${t}</div>`).join('');
|
|
showTab(0);
|
|
}
|
|
function showTab(i){
|
|
document.querySelectorAll('.stab').forEach((e,j)=>e.classList.toggle('active',i===j));
|
|
if(i===0)rDash();else if(i===1)rRITab();else if(i===2)rKKTab();else rPrioTab();
|
|
}
|
|
function rSB(){const i=[...document.querySelectorAll('.stab')].findIndex(e=>e.classList.contains('active'));showTab(i>=0?i:0);}
|
|
|
|
function rDash(){
|
|
const mis=pD.filter(p=>p.status_ekonomi==='miskin').length,ren=pD.filter(p=>p.status_ekonomi==='rentan').length;
|
|
const bel=pD.filter(p=>['miskin','rentan'].includes(p.status_ekonomi)&&!parseInt(p.total_bantuan)).length;
|
|
const lAkt=lapD.filter(l=>l.status!=='selesai').length,lDar=lapD.filter(l=>l.urgensi==='darurat'&&l.status!=='selesai').length;
|
|
document.getElementById('sbody').innerHTML=`
|
|
<div class="sgrid">
|
|
<div class="scard" onclick="goPage('penduduk')"><div class="num cb">${pD.length}</div><div class="lbl">Total Penduduk</div></div>
|
|
<div class="scard"><div class="num">${kkD.length}</div><div class="lbl">Total KK</div></div>
|
|
<div class="scard"><div class="num cr">${mis}</div><div class="lbl">Miskin</div></div>
|
|
<div class="scard"><div class="num ca">${ren}</div><div class="lbl">Rentan</div></div>
|
|
<div class="scard"><div class="num cro">${bel}</div><div class="lbl">Belum Dibantu</div></div>
|
|
<div class="scard" onclick="goPage('laporan')"><div class="num cp">${lAkt}${lDar?`<span style="font-size:11px;color:var(--rose)"> (${lDar}!)</span>`:''}</div><div class="lbl">Laporan Aktif</div></div>
|
|
</div>
|
|
<div class="slbl">Jangkauan per Rumah Ibadah</div>
|
|
<div style="display:flex;flex-direction:column;gap:6px">
|
|
${riD.map(ri=>{const k=kkD.filter(x=>x.rumah_ibadah_id==ri.id);const m=k.filter(x=>x.status_ekonomi==='miskin').length;const p=k.length?Math.round(m/k.length*100):0;return`<div style="background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);padding:9px 11px"><div style="display:flex;justify-content:space-between;align-items:center;font-size:11px;margin-bottom:4px"><span style="display:flex;align-items:center;gap:5px;color:var(--t2);font-weight:500">${riSVG(ri.jenis)} ${ri.nama.length>22?ri.nama.substring(0,22)+'…':ri.nama}</span><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${k.length} KK</span></div><div class="prog"><div class="pfill" style="width:${p}%;background:var(--red)"></div></div></div>`;}).join('')}
|
|
</div>
|
|
<div class="slbl" style="margin-top:18px">Legenda</div>
|
|
<div style="display:flex;flex-direction:column;gap:7px;font-size:11px;color:var(--t2)">
|
|
${[['#f87171','Penduduk Miskin'],['#fbbf24','Penduduk Rentan'],['#34d399','Penduduk Mampu'],['#4f8ef7','Rumah Ibadah'],['#fb7185','Laporan Darurat']].map(([c,l])=>`<div style="display:flex;align-items:center;gap:8px"><div style="width:9px;height:9px;background:${c};border-radius:50%;flex-shrink:0"></div>${l}</div>`).join('')}
|
|
</div>`;
|
|
}
|
|
|
|
function rRITab(){
|
|
const cA=cu?.role==='superadmin';
|
|
document.getElementById('sbody').innerHTML=`
|
|
${cA?`<button class="btn btn-primary btn-sm" style="width:100%;justify-content:center;margin-bottom:10px" onclick="toast('Fitur ini untuk Super Admin','info')"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah Rumah Ibadah</button>`:''}
|
|
<div class="frow"><input type="text" id="q-ri" placeholder="Cari nama..." oninput="fRI()"></div>
|
|
<div class="ilist" id="l-ri">${riCards(riD)}</div>`;
|
|
}
|
|
function riCards(d){if(!d.length)return`<div class="empty"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg><p>Tidak ada data</p></div>`;return d.map(r=>{const m=kkD.filter(k=>k.rumah_ibadah_id==r.id&&k.status_ekonomi==='miskin').length;return`<div class="icard" onclick="detRI(${r.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px;color:var(--blue)">${riSVG(r.jenis)} ${r.nama}</div><span class="badge b-b">${r.jenis}</span></div><div class="isub">${r.kelurahan||'—'}</div><div class="imeta">${r.jumlah_kk||0} KK  ·  ${m} miskin</div></div>`;}).join('');}
|
|
function fRI(){const q=(document.getElementById('q-ri')?.value||'').toLowerCase();const el=document.getElementById('l-ri');if(el)el.innerHTML=riCards(riD.filter(r=>r.nama.toLowerCase().includes(q)));}
|
|
|
|
function rKKTab(){
|
|
const cW=['superadmin','pengurus','relawan'].includes(cu?.role);
|
|
document.getElementById('sbody').innerHTML=`
|
|
${cW?`<div style="display:flex;gap:6px;margin-bottom:10px">
|
|
<button class="btn btn-primary btn-sm" style="flex:1;justify-content:center" onclick="openAddKK()"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> KK</button>
|
|
<button class="btn btn-success btn-sm" style="flex:1;justify-content:center" onclick="openAddP()"><svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Penduduk</button>
|
|
</div>`:''}
|
|
<div class="frow"><input type="text" id="q-kk" placeholder="Cari nama / No. KK..." oninput="fKK()"></div>
|
|
<div class="frow"><select id="fse-kk" onchange="fKK()"><option value="">Semua Status</option><option value="miskin">Miskin</option><option value="rentan">Rentan</option><option value="mampu">Mampu</option></select></div>
|
|
<div class="ilist" id="l-kk">${kkCards(kkD)}</div>`;
|
|
}
|
|
function kkCards(d){if(!d.length)return`<div class="empty"><svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg><p>Tidak ada data</p></div>`;return d.slice(0,60).map(k=>`<div class="icard" onclick="detKK(${k.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px">${icoHome} ${k.nama_kk}</div><span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}">${k.status_ekonomi}</span></div><div class="isub">${(k.alamat||'—').substring(0,38)}</div><div class="imeta">${k.jumlah_jiwa||0} jiwa  ·  Bantuan: ${k.total_bantuan_diterima||0}x</div></div>`).join('');}
|
|
function fKK(){const q=(document.getElementById('q-kk')?.value||'').toLowerCase();const fse=document.getElementById('fse-kk')?.value||'';const f=kkD.filter(k=>(!q||(k.nama_kk.toLowerCase().includes(q)||k.no_kk.includes(q)))&&(!fse||k.status_ekonomi===fse));const el=document.getElementById('l-kk');if(el)el.innerHTML=kkCards(f);}
|
|
|
|
async function rPrioTab(){
|
|
document.getElementById('sbody').innerHTML=`<div class="empty" style="margin-top:50px"><div class="spin" style="margin:0 auto 12px"></div><p>Menganalisis prioritas...</p></div>`;
|
|
try{
|
|
const d=await rq(EP.b,'GET',null,'action=prioritas');
|
|
document.getElementById('sbody').innerHTML=`
|
|
<div style="font-size:10px;color:var(--t3);margin-bottom:10px;padding:10px;background:var(--bg3);border:1px solid var(--b);border-radius:var(--rs);line-height:1.6">Warga miskin dengan penyakit berat atau lansia yang hidup sendiri mendapat prioritas tertinggi.</div>
|
|
<div class="ilist">${d.slice(0,30).map(p=>{const c=p.prioritas==='SANGAT TINGGI'?'b-ro':p.prioritas==='TINGGI'?'b-r':p.prioritas==='SEDANG'?'b-a':'b-g';return`<div class="icard" onclick="detP(${p.id})"><div class="ihead"><div class="iname" style="display:flex;align-items:center;gap:6px">${icoUser} ${p.nama}</div><span class="badge ${c}">${p.prioritas}</span></div><div class="isub">${p.umur||'?'} thn · ${p.kondisi_kesehatan||'—'}</div><div class="imeta">${p.kelurahan||'—'} · Bantuan: ${p.total_bantuan||0}x</div></div>`;}).join('')}</div>`;
|
|
}catch(e){toast(e.message,'err');}
|
|
}
|
|
|
|
// PAGES
|
|
function goPage(pg){
|
|
curPg=pg;
|
|
['peta','penduduk','bantuan','laporan','chat'].forEach(x=>document.getElementById('nb-'+x)?.classList.remove('active'));
|
|
document.getElementById('nb-'+pg)?.classList.add('active');
|
|
if(pg==='peta') rebuildPeta();
|
|
else if(pg==='penduduk') pgPenduduk();
|
|
else if(pg==='bantuan') pgBantuan();
|
|
else if(pg==='laporan') pgLaporan();
|
|
else if(pg==='chat') pgChat();
|
|
}
|
|
|
|
function rebuildPeta(){
|
|
document.getElementById('app').innerHTML=`
|
|
<div id="sidebar"><div class="stabs" id="stabs"></div><div class="sbody" id="sbody"></div></div>
|
|
<div id="map-container">
|
|
<div id="map"></div>
|
|
<div id="mcp">
|
|
<div class="mcp-h">Layer Peta</div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#f87171"></div>Penduduk Miskin</div><div class="tog ${lyrs.miskin?'on':''}" id="ly-miskin" onclick="togLyr(this,'miskin')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fbbf24"></div>Penduduk Rentan</div><div class="tog ${lyrs.rentan?'on':''}" id="ly-rentan" onclick="togLyr(this,'rentan')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#34d399"></div>Penduduk Mampu</div><div class="tog ${lyrs.mampu?'on':''}" id="ly-mampu" onclick="togLyr(this,'mampu')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#4f8ef7"></div>Rumah Ibadah</div><div class="tog ${lyrs.ri?'on':''}" id="ly-ri" onclick="togLyr(this,'ri')"></div></div>
|
|
<div class="lyr"><div class="lyr-l"><div class="ldot" style="background:#fb7185"></div>Laporan Warga</div><div class="tog ${lyrs.laporan?'on':''}" id="ly-lap" onclick="togLyr(this,'laporan')"></div></div>
|
|
<div class="mcp-div"></div>
|
|
<div class="rad-lbl"><span>Radius Distribusi</span><span id="rv">${rad} m</span></div>
|
|
<input type="range" min="100" max="1000" step="50" value="${rad}" id="rsl" oninput="setRad(this.value)">
|
|
</div>
|
|
</div>`;
|
|
if(map){map.remove();map=null;}
|
|
initMap();rerender();buildSB();
|
|
}
|
|
|
|
function pgPenduduk(){
|
|
const cW=['superadmin','pengurus','relawan'].includes(cu?.role);
|
|
document.getElementById('app').innerHTML=`
|
|
<div class="pwrap">
|
|
<div class="pinner">
|
|
<div class="pheader">
|
|
<div class="ptitle">Data Penduduk <small>${pD.length} terdaftar</small></div>
|
|
${cW?`<div style="display:flex;gap:8px">
|
|
<button class="btn btn-success" onclick="openAddP()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah Penduduk</button>
|
|
<button class="btn btn-primary" onclick="openAddKK()"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Tambah KK</button>
|
|
</div>`:''}
|
|
</div>
|
|
<div style="display:flex;gap:8px;margin-bottom:16px;flex-wrap:wrap">
|
|
<input type="text" id="qp" placeholder="Cari nama atau NIK..." style="flex:1;min-width:180px;height:34px;padding:0 12px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" oninput="fP()">
|
|
<select id="fsep" style="height:34px;padding:0 10px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" onchange="fP()"><option value="">Semua Ekonomi</option><option value="miskin">Miskin</option><option value="rentan">Rentan</option><option value="mampu">Mampu</option></select>
|
|
<select id="fkesp" style="height:34px;padding:0 10px;background:var(--bg2);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none" onchange="fP()"><option value="">Semua Kesehatan</option><option value="sehat">Sehat</option><option value="sakit_ringan">Sakit Ringan</option><option value="sakit_berat">Sakit Berat</option><option value="disabilitas">Disabilitas</option></select>
|
|
</div>
|
|
<div style="background:var(--bg2);border:1px solid var(--b);border-radius:var(--r);overflow:hidden">
|
|
<div id="ptbl" style="overflow-x:auto">${pTbl(pD)}</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
function pTbl(d){
|
|
if(!d.length)return`<div class="empty"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg><p>Tidak ada data</p></div>`;
|
|
return`<table class="dtable"><thead><tr><th>NIK</th><th>Nama</th><th>JK</th><th>Umur</th><th>Profil</th><th>Ekonomi</th><th>Kesehatan</th><th>Pekerjaan</th><th>Bantuan</th><th></th></tr></thead><tbody>${d.slice(0,100).map(p=>`<tr><td style="font-family:var(--mono);font-size:10px;color:var(--t3)">${p.nik||'—'}</td><td style="font-weight:600">${p.nama}</td><td>${p.jenis_kelamin||'—'}</td><td style="font-family:var(--mono)">${p.umur||'?'}</td><td><span class="badge b-b">${p.profil_umur||'—'}</span></td><td><span class="badge ${p.status_ekonomi==='miskin'?'b-r':p.status_ekonomi==='rentan'?'b-a':'b-g'}">${p.status_ekonomi}</span></td><td style="font-size:11px;color:${p.kondisi_kesehatan==='sehat'?'var(--green)':p.kondisi_kesehatan==='disabilitas'?'var(--rose)':'var(--amber)'}">${p.kondisi_kesehatan||'sehat'}</td><td style="font-size:11px;color:var(--t2)">${p.pekerjaan||'—'}</td><td style="text-align:center;font-family:var(--mono)">${p.total_bantuan||0}x</td><td><button class="btn btn-xs btn-ghost" onclick="detP(${p.id})">Detail</button></td></tr>`).join('')}${d.length>100?`<tr><td colspan="10" style="text-align:center;padding:12px;font-size:11px;color:var(--t3)">Menampilkan 100 dari ${d.length}. Gunakan filter untuk mempersempit pencarian.</td></tr>`:''}</tbody></table>`;
|
|
}
|
|
function fP(){const q=(document.getElementById('qp')?.value||'').toLowerCase();const fse=document.getElementById('fsep')?.value||'';const fkes=document.getElementById('fkesp')?.value||'';const f=pD.filter(p=>(!q||p.nama.toLowerCase().includes(q)||(p.nik||'').includes(q))&&(!fse||p.status_ekonomi===fse)&&(!fkes||p.kondisi_kesehatan===fkes));const el=document.getElementById('ptbl');if(el)el.innerHTML=pTbl(f);}
|
|
|
|
function pgBantuan(){
|
|
document.getElementById('app').innerHTML=`
|
|
<div class="pwrap">
|
|
<div class="pinner">
|
|
<div class="pheader"><div class="ptitle">Manajemen Bantuan Sosial</div></div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px">
|
|
<div class="scard2"><h3>${icoGift} Program Bantuan</h3><div style="display:flex;flex-direction:column;gap:6px">${bD.map(b=>`<div style="display:flex;justify-content:space-between;align-items:center;padding:6px 0;border-bottom:1px solid var(--b);font-size:12px"><span>${b.nama}</span><span class="badge b-b">${b.jenis}</span></div>`).join('')}</div></div>
|
|
<div class="scard2"><h3>${icoWarn} Belum Terima Bantuan</h3><div id="blist" style="display:flex;flex-direction:column;gap:5px;max-height:240px;overflow-y:auto;font-size:12px"><span style="color:var(--t3)">Memuat...</span></div></div>
|
|
</div>
|
|
<div class="scard2">
|
|
<h3>Catat Penyaluran Bantuan</h3>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr auto;gap:10px;align-items:end">
|
|
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Penduduk</div><select id="hb-pid" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:11px;outline:none"><option value="">-- Pilih --</option>${pD.map(p=>`<option value="${p.id}">${p.nama}</option>`).join('')}</select></div>
|
|
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Program Bantuan</div><select id="hb-bid" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:11px;outline:none"><option value="">-- Pilih --</option>${bD.map(b=>`<option value="${b.id}">${b.nama}</option>`).join('')}</select></div>
|
|
<div><div style="font-size:10px;color:var(--t3);font-weight:700;text-transform:uppercase;letter-spacing:.6px;margin-bottom:6px">Tanggal</div><input type="date" id="hb-tgl" value="${new Date().toISOString().split('T')[0]}" style="width:100%;height:32px;padding:0 9px;background:var(--bg3);border:1px solid var(--b2);border-radius:var(--rs);color:var(--t);font-size:12px;outline:none"></div>
|
|
<button class="btn btn-success" onclick="catatB()">Catat</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
rq(EP.b,'GET',null,'action=belum_terima').then(d=>{const el=document.getElementById('blist');if(!el)return;if(!d.length){el.innerHTML=`<div style="color:var(--green);font-size:12px">Semua warga sudah pernah menerima bantuan</div>`;return;}el.innerHTML=d.slice(0,20).map(p=>`<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid var(--b)"><span>${p.nama}</span><span class="badge ${p.status_ekonomi==='miskin'?'b-r':'b-a'}">${p.status_ekonomi}</span></div>`).join('');}).catch(()=>{});
|
|
}
|
|
async function catatB(){const pid=document.getElementById('hb-pid')?.value;const bid=document.getElementById('hb-bid')?.value;const tgl=document.getElementById('hb-tgl')?.value;if(!pid||!bid){toast('Pilih penduduk dan program bantuan','err');return;}try{await rq(EP.b,'POST',{penduduk_id:parseInt(pid),bantuan_id:parseInt(bid),tanggal:tgl,status:'disalurkan'},'action=histori');toast('Penyaluran bantuan berhasil dicatat','ok');}catch(e){toast(e.message,'err');}}
|
|
|
|
function pgLaporan(){
|
|
document.getElementById('app').innerHTML=`
|
|
<div class="pwrap">
|
|
<div class="pinner">
|
|
<div class="pheader">
|
|
<div class="ptitle">Laporan Masyarakat <small>${lapD.filter(l=>l.status!=='selesai').length} aktif</small></div>
|
|
<button class="btn btn-danger" onclick="openM('lap')"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> Buat Laporan</button>
|
|
</div>
|
|
<div style="display:flex;gap:7px;margin-bottom:16px;flex-wrap:wrap">${['','pending','diverifikasi','diproses','selesai'].map((s,i)=>`<button class="btn btn-sm ${i===0?'btn-primary':''}" id="fb-${s||'all'}" onclick="fLap('${s}',this)">${s||'Semua'}</button>`).join('')}</div>
|
|
<div id="lap-list" style="display:flex;flex-direction:column;gap:10px">${lapCards(lapD)}</div>
|
|
</div>
|
|
</div>`;
|
|
document.getElementById('dlap').style.display='none';
|
|
}
|
|
function lapCards(d){if(!d.length)return`<div class="empty"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg><p>Tidak ada laporan</p></div>`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);return d.map(l=>{const bd=l.urgensi==='darurat'?'rgba(251,113,133,.3)':l.urgensi==='tinggi'?'rgba(248,113,113,.2)':'var(--b)';return`<div style="background:var(--bg2);border:1px solid ${bd};border-radius:var(--r);padding:16px"><div style="display:flex;justify-content:space-between;gap:12px;margin-bottom:8px"><div style="font-size:13px;font-weight:600;line-height:1.5;letter-spacing:-.2px">${(l.deskripsi||'').substring(0,90)}${(l.deskripsi||'').length>90?'…':''}</div><div style="display:flex;gap:5px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end"><span class="badge u-${l.urgensi}">${(l.urgensi||'').toUpperCase()}</span><span class="badge b-gray">${l.status}</span></div></div><div style="font-size:11px;color:var(--t3);margin-bottom:10px">${l.alamat_laporan||'—'} · ${l.pelapor_nama||'Anonim'} · ${new Date(l.created_at).toLocaleDateString('id-ID')}</div><div style="display:flex;gap:7px;align-items:center;flex-wrap:wrap"><button class="btn btn-sm" onclick="detLap(${l.id})">Lihat Detail</button>${cW?`<select class="btn btn-sm" style="height:28px;font-size:11px;cursor:pointer" onchange="uLS(${l.id},this.value,this)"><option value="">Update Status</option>${['pending','diverifikasi','diproses','selesai'].map(s=>`<option value="${s}"${l.status===s?' selected':''}>${s}</option>`).join('')}</select>`:''}</div></div>`;}).join('');}
|
|
async function fLap(s,btn){document.querySelectorAll('[id^="fb-"]').forEach(b=>b.classList.remove('btn-primary'));btn?.classList.add('btn-primary');try{const d=await rq(EP.lap,'GET',null,s?'status='+s:'');const el=document.getElementById('lap-list');if(el)el.innerHTML=lapCards(d);}catch(e){toast(e.message,'err');}}
|
|
async function uLS(id,s,sel){if(!s)return;try{await rq(EP.lap,'PUT',{status:s},'id='+id);const i=lapD.findIndex(l=>l.id===id);if(i>-1)lapD[i].status=s;toast('Status diperbarui','ok');}catch(e){toast(e.message,'err');if(sel)sel.value='';}}
|
|
|
|
function pgChat(){
|
|
document.getElementById('app').innerHTML=`
|
|
<div class="pwrap">
|
|
<div class="pinner" style="max-width:680px">
|
|
<div class="pheader"><div class="ptitle">Koordinasi Pengurus</div><span style="font-size:11px;color:var(--t3)">Pesan dapat dilihat semua pengurus & relawan</span></div>
|
|
<div style="background:var(--bg2);border:1px solid var(--b2);border-radius:var(--r);padding:18px">
|
|
<div class="chat-msgs" id="cmsgs"></div>
|
|
<div class="cinp-row">
|
|
<textarea class="cinp" id="cinp" placeholder="Tulis pesan... (Enter untuk kirim)" onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendChat();}"></textarea>
|
|
<button class="btn btn-primary btn-sm" onclick="sendChat()" style="height:38px;padding:0 14px">Kirim</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
loadChat();document.getElementById('dchat').style.display='none';
|
|
}
|
|
async function loadChat(){try{const msgs=await rq(EP.chat,'GET',null,'since='+lastC);if(!msgs.length)return;lastC=msgs[msgs.length-1].created_at;const el=document.getElementById('cmsgs');if(!el)return;msgs.forEach(m=>{const mine=m.dari_id===cu.id;const d=document.createElement('div');d.className='cmsg '+(mine?'mine':'other');d.innerHTML=`<div class="cmsg-meta">${mine?'Anda':m.dari_nama}${m.dari_ri?' · '+m.dari_ri:''}</div><div class="cmsg-bubble">${m.pesan}</div><div class="cmsg-time">${new Date(m.created_at).toLocaleTimeString('id',{hour:'2-digit',minute:'2-digit'})}</div>`;el.appendChild(d);});el.scrollTop=el.scrollHeight;}catch(e){}}
|
|
async function pollChat(){if(curPg==='chat'){loadChat();return;}try{const msgs=await rq(EP.chat,'GET',null,'since='+lastC);if(msgs.length){lastC=msgs[msgs.length-1].created_at;document.getElementById('dchat').style.display='inline-block';}}catch(e){}}
|
|
async function sendChat(){const inp=document.getElementById('cinp');const p=inp?.value.trim();if(!p)return;try{await rq(EP.chat,'POST',{pesan:p});inp.value='';loadChat();}catch(e){toast(e.message,'err');}}
|
|
|
|
// DETAILS
|
|
async function detRI(id){ld(true);try{const ri=await rq(EP.ri,'GET',null,'id='+id);const kk2=kkD.filter(k=>k.rumah_ibadah_id==id);document.getElementById('det-h').innerHTML=`<div class="mico mi-b">${riSVG(ri.jenis)}</div>${ri.nama}`;document.getElementById('det-b').innerHTML=`<span class="badge b-b" style="margin-bottom:14px;display:inline-block">${ri.jenis}</span><table class="dtbl"><tr><td>Kontak</td><td>${ri.kontak||'—'}</td></tr><tr><td>Alamat</td><td>${ri.alamat||'—'}</td></tr><tr><td>Kelurahan</td><td>${ri.kelurahan||'—'}</td></tr><tr><td>Jumlah KK</td><td>${ri.jumlah_kk||0} KK (${ri.jumlah_jiwa||0} jiwa)</td></tr><tr><td>KK Miskin</td><td style="color:var(--red);font-weight:600">${ri.kk_miskin||0} KK</td></tr></table>${kk2.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">KK Terdampak (${kk2.length})</div><div class="dklist">${kk2.map(k=>`<div class="dkrow"><span>${k.nama_kk}</span><span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}">${k.status_ekonomi}</span></div>`).join('')}</div>`:''}`;document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
async function detP(id){ld(true);try{const p=await rq(EP.p,'GET',null,'id='+id);const col=EC[p.status_ekonomi]||'#888';document.getElementById('det-h').innerHTML=`<div class="mico mi-g"><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="var(--green)" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>${p.nama}`;document.getElementById('det-b').innerHTML=`<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px"><span class="badge ${p.status_ekonomi==='miskin'?'b-r':p.status_ekonomi==='rentan'?'b-a':'b-g'}">${(p.status_ekonomi||'').toUpperCase()}</span><span class="badge b-b">${p.profil_umur||'—'}</span><span class="badge ${p.kondisi_kesehatan==='sehat'?'b-g':'b-r'}">${p.kondisi_kesehatan||'sehat'}</span></div>${p.peringatan_pendidikan?`<div class="wbox" style="display:block;margin-bottom:14px">${p.peringatan_pendidikan}</div>`:''}<table class="dtbl"><tr><td>NIK</td><td style="font-family:var(--mono)">${p.nik||'—'}</td></tr><tr><td>Jenis Kelamin</td><td>${p.jenis_kelamin==='L'?'Laki-laki':'Perempuan'}</td></tr><tr><td>Tanggal Lahir</td><td>${p.tanggal_lahir||'—'} (${p.umur||'?'} tahun)</td></tr><tr><td>Status Keluarga</td><td>${(p.status_keluarga||'—').replace('_',' ')}</td></tr><tr><td>Status Perkawinan</td><td>${(p.status_perkawinan||'—').replace(/_/g,' ')}</td></tr><tr><td>Pekerjaan</td><td>${p.pekerjaan||'—'}</td></tr><tr><td>Penghasilan</td><td>Rp ${Number(p.penghasilan||0).toLocaleString('id-ID')}/bulan</td></tr><tr><td>Pendidikan</td><td>${(p.pendidikan_terakhir||'—').toUpperCase()} · ${(p.status_pendidikan||'—').replace('_',' ')}</td></tr><tr><td>No. KK</td><td style="font-family:var(--mono)">${p.no_kk||'—'}</td></tr><tr><td>Alamat</td><td>${p.alamat_kk||'—'}${p.rt?' RT '+p.rt+'/RW '+p.rw:''}</td></tr><tr><td>Kelurahan</td><td>${p.kelurahan||'—'}, ${p.kecamatan||'—'}</td></tr></table>${p.histori_bantuan?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Histori Bantuan (${p.histori_bantuan.length}x)</div><div class="dklist">${p.histori_bantuan.map(h=>`<div class="dkrow"><span>${h.bantuan_nama}</span><div style="display:flex;align-items:center;gap:8px"><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${h.tanggal}</span><span class="badge ${h.status==='disalurkan'?'b-g':'b-a'}">${h.status}</span></div></div>`).join('')}</div>`:`<div style="font-size:12px;padding:10px;background:var(--rdim);border-radius:var(--rs);margin-top:14px;color:var(--red)">Belum pernah menerima bantuan apapun</div>`}${p.riwayat_pelatihan?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Riwayat Pelatihan</div><div class="dklist">${p.riwayat_pelatihan.map(r=>`<div class="dkrow"><span>${r.nama_pelatihan}</span><span style="font-family:var(--mono);font-size:10px;color:var(--t3)">${r.tanggal_ikut}</span></div>`).join('')}</div>`:''}`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>${cW?`<button class="btn" onclick="closeM('det');openEditP(${p.id})">Edit Data</button>`:''}${cu?.role==='superadmin'?`<button class="btn btn-danger" onclick="delP(${p.id})">Hapus</button>`:''}`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
async function detKK(id){ld(true);try{const k=await rq(EP.kk,'GET',null,'id='+id);document.getElementById('det-h').innerHTML=`<div class="mico mi-b">${icoHome}</div>${k.nama_kk}`;document.getElementById('det-b').innerHTML=`<span class="badge ${k.status_ekonomi==='miskin'?'b-r':k.status_ekonomi==='rentan'?'b-a':'b-g'}" style="margin-bottom:14px;display:inline-block">${(k.status_ekonomi||'').toUpperCase()}</span><table class="dtbl"><tr><td>No. KK</td><td style="font-family:var(--mono)">${k.no_kk||'—'}</td></tr><tr><td>Alamat</td><td>${k.alamat||'—'} RT ${k.rt||'-'}/RW ${k.rw||'-'}</td></tr><tr><td>Kelurahan</td><td>${k.kelurahan||'—'}, ${k.kecamatan||'—'}</td></tr><tr><td>Rumah Ibadah</td><td>${k.ri_nama||'—'}</td></tr></table>${k.anggota?.length?`<div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin:16px 0 8px">Anggota (${k.anggota.length} jiwa)</div><div class="dklist">${k.anggota.map(a=>`<div class="dkrow"><span><strong>${a.nama}</strong> · ${(a.status_keluarga||'').replace('_',' ')} · ${a.umur}thn</span><span class="badge ${a.kondisi_kesehatan==='sehat'?'b-g':a.kondisi_kesehatan==='disabilitas'?'b-ro':'b-a'}">${a.kondisi_kesehatan||'sehat'}</span></div>`).join('')}</div>`:''}`;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>${cW?`<button class="btn btn-success" onclick="closeM('det');openAddP(${id})">+ Tambah Anggota</button>`:''}`;openM('det');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
async function detLap(id){const l=lapD.find(x=>x.id===id);if(!l)return;const cW=['superadmin','pengurus','relawan'].includes(cu?.role);document.getElementById('det-h').innerHTML=`<div class="mico mi-r">${icoWarn}</div>Laporan Masyarakat`;document.getElementById('det-b').innerHTML=`<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:14px"><span class="badge u-${l.urgensi}">${(l.urgensi||'').toUpperCase()}</span><span class="badge b-gray">${l.status}</span></div><table class="dtbl"><tr><td>Deskripsi</td><td style="line-height:1.7">${l.deskripsi||'—'}</td></tr><tr><td>Pelapor</td><td>${l.pelapor_nama||'Anonim'}${l.pelapor_telepon?' ('+l.pelapor_telepon+')':''}</td></tr><tr><td>Lokasi</td><td>${l.alamat_laporan||'—'}</td></tr><tr><td>Tanggal</td><td>${new Date(l.created_at).toLocaleDateString('id-ID',{weekday:'long',day:'numeric',month:'long',year:'numeric'})}</td></tr></table>${l.foto?`<img src="${l.foto}" style="width:100%;max-height:200px;object-fit:cover;border-radius:var(--rs);margin-top:14px">`:''}${cW?`<div style="margin-top:16px;padding-top:16px;border-top:1px solid var(--b)"><div style="font-size:10px;font-weight:700;color:var(--t3);text-transform:uppercase;letter-spacing:.6px;margin-bottom:8px">Update Status</div><div style="display:flex;gap:7px;flex-wrap:wrap">${['diverifikasi','diproses','selesai'].map(s=>`<button class="btn btn-sm ${l.status===s?'btn-primary':''}" onclick="uLS(${l.id},'${s}',null);closeM('det');toast('Status diperbarui','ok')">${s}</button>`).join('')}</div></div>`:''}`;document.getElementById('det-f').innerHTML=`<button class="btn btn-ghost" onclick="closeM('det')">Tutup</button>`;openM('det');}
|
|
|
|
// FORMS
|
|
function openAddKK(){eKK=null;document.getElementById('mkk-t').textContent='Tambah Kartu Keluarga';['kk-nokk','kk-nama','kk-alamat','kk-rt','kk-rw','kk-kel','kk-kec','kk-lat','kk-lng'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});document.getElementById('kk-se').value='rentan';const sel=document.getElementById('kk-ri');if(sel)sel.innerHTML='<option value="">-- Pilih --</option>'+riD.map(r=>`<option value="${r.id}">${r.nama}</option>`).join('');openM('kk');}
|
|
async function saveKK(){const no=document.getElementById('kk-nokk')?.value.trim(),nm=document.getElementById('kk-nama')?.value.trim();if(!no||!nm){toast('No KK dan nama wajib diisi','err');return;}if(no.length!==16){toast('No KK harus 16 digit','err');return;}const body={no_kk:no,nama_kk:nm,alamat:document.getElementById('kk-alamat')?.value||'',rt:document.getElementById('kk-rt')?.value||'',rw:document.getElementById('kk-rw')?.value||'',kelurahan:document.getElementById('kk-kel')?.value||'',kecamatan:document.getElementById('kk-kec')?.value||'',lat:parseFloat(document.getElementById('kk-lat')?.value)||null,lng:parseFloat(document.getElementById('kk-lng')?.value)||null,rumah_ibadah_id:document.getElementById('kk-ri')?.value||null,status_ekonomi:document.getElementById('kk-se')?.value||'rentan'};ld(true);try{if(eKK){await rq(EP.kk,'PUT',body,'id='+eKK);eKK=null;toast('KK diperbarui','ok');}else{await rq(EP.kk,'POST',body);toast('KK berhasil ditambahkan','ok');}closeM('kk');const kk=await rq(EP.kk);kkD=kk||[];fKK();rDash();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
function openAddP(kkId=null){eP=null;document.getElementById('mp-t').textContent='Tambah Data Penduduk';document.getElementById('p-nik').readOnly=false;['p-nik','p-nama','p-tgl','p-kerja','p-gaji','p-cat'].forEach(id=>{const el=document.getElementById(id);if(el)el.value='';});['p-jk','p-sk','p-sp','p-sh','p-se','p-pend','p-spend','p-kes'].forEach(id=>{const el=document.getElementById(id);if(!el)return;if(id==='p-sh')el.value='hidup';else if(id==='p-se')el.value='rentan';else if(id==='p-pend')el.value='tidak_sekolah';else if(id==='p-spend')el.value='tidak_sekolah';else if(id==='p-kes')el.value='sehat';else el.value=el.options[0]?.value;});document.getElementById('wp').style.display='none';const sel=document.getElementById('p-kk');if(sel)sel.innerHTML='<option value="">-- Pilih KK --</option>'+kkD.map(k=>`<option value="${k.id}"${k.id==kkId?' selected':''}>${k.nama_kk} (${k.no_kk})</option>`).join('');openM('p');}
|
|
|
|
async function openEditP(id){const p=pD.find(x=>x.id===id);if(!p){toast('Data tidak ditemukan','err');return;}eP=id;document.getElementById('mp-t').textContent='Edit Data Penduduk';document.getElementById('p-nik').value=p.nik||'';document.getElementById('p-nik').readOnly=true;document.getElementById('p-nama').value=p.nama||'';document.getElementById('p-tgl').value=p.tanggal_lahir||'';document.getElementById('p-jk').value=p.jenis_kelamin||'L';document.getElementById('p-sk').value=p.status_keluarga||'kepala';document.getElementById('p-sp').value=p.status_perkawinan||'belum_kawin';document.getElementById('p-sh').value=p.status_hidup||'hidup';document.getElementById('p-se').value=p.status_ekonomi||'rentan';document.getElementById('p-pend').value=p.pendidikan_terakhir||'tidak_sekolah';document.getElementById('p-spend').value=p.status_pendidikan||'tidak_sekolah';document.getElementById('p-kes').value=p.kondisi_kesehatan||'sehat';document.getElementById('p-kerja').value=p.pekerjaan||'';document.getElementById('p-gaji').value=p.penghasilan||0;document.getElementById('p-cat').value=p.catatan||'';const sel=document.getElementById('p-kk');if(sel)sel.innerHTML='<option value="">-- Pilih KK --</option>'+kkD.map(k=>`<option value="${k.id}"${k.id==p.keluarga_id?' selected':''}>${k.nama_kk} (${k.no_kk})</option>`).join('');cekPend();openM('p');}
|
|
|
|
async function saveP(){const nm=document.getElementById('p-nama')?.value.trim(),tgl=document.getElementById('p-tgl')?.value,kkid=document.getElementById('p-kk')?.value,nik=document.getElementById('p-nik')?.value.trim();if(!nm||!tgl||!kkid){toast('Nama, tanggal lahir, dan KK wajib diisi','err');return;}if(!eP&&(!nik||nik.length!==16)){toast('NIK harus 16 digit','err');return;}const body={nik,nama:nm,tanggal_lahir:tgl,jenis_kelamin:document.getElementById('p-jk')?.value||'L',status_keluarga:document.getElementById('p-sk')?.value||'kepala',status_perkawinan:document.getElementById('p-sp')?.value||'belum_kawin',status_hidup:document.getElementById('p-sh')?.value||'hidup',status_ekonomi:document.getElementById('p-se')?.value||'rentan',pendidikan_terakhir:document.getElementById('p-pend')?.value||'tidak_sekolah',status_pendidikan:document.getElementById('p-spend')?.value||'tidak_sekolah',kondisi_kesehatan:document.getElementById('p-kes')?.value||'sehat',pekerjaan:document.getElementById('p-kerja')?.value||'',penghasilan:parseFloat(document.getElementById('p-gaji')?.value||0),keluarga_id:parseInt(kkid),catatan:document.getElementById('p-cat')?.value||''};ld(true);try{let res;if(eP){res=await rq(EP.p,'PUT',body,'id='+eP);eP=null;toast('Data penduduk diperbarui','ok');}else{res=await rq(EP.p,'POST',body);toast('Penduduk berhasil ditambahkan','ok');}if(res?.peringatan)setTimeout(()=>toast('Perhatian: '+res.peringatan,'warn'),500);closeM('p');const pd=await rq(EP.p);pD=pd||[];rerender();rSB();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
async function delP(id){if(!confirm('Hapus data penduduk ini secara permanen?'))return;ld(true);try{await rq(EP.p,'DELETE',null,'id='+id);pD=pD.filter(p=>p.id!==id);closeM('det');rerender();rSB();toast('Data dihapus','ok');}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
function cekPend(){const tgl=document.getElementById('p-tgl')?.value;if(!tgl)return;const umur=Math.floor((new Date()-new Date(tgl))/31557600000);const pend=document.getElementById('p-pend')?.value;const spend=document.getElementById('p-spend')?.value;const el=document.getElementById('wp');if(!el)return;let w='';if(umur>=7&&umur<=15&&spend==='tidak_sekolah'&&['tidak_sekolah','sd'].includes(pend))w=`Usia ${umur} tahun seharusnya masih bersekolah SD/SMP.`;else if(umur>=16&&umur<=18&&spend==='tidak_sekolah')w=`Usia ${umur} tahun seharusnya masih bersekolah SMA/SMK.`;el.style.display=w?'block':'none';el.textContent=w?'Perhatian: '+w:'';}
|
|
|
|
async function saveLap(){const desc=document.getElementById('lap-desc')?.value.trim();if(!desc){toast('Deskripsi laporan wajib diisi','err');return;}const fd=new FormData();fd.append('deskripsi',desc);fd.append('pelapor_nama',document.getElementById('lap-nama')?.value||'');fd.append('pelapor_telepon',document.getElementById('lap-telp')?.value||'');fd.append('alamat_laporan',document.getElementById('lap-alamat')?.value||'');fd.append('urgensi',document.getElementById('lap-urg')?.value||'sedang');const lat=document.getElementById('lap-lat')?.value;const lng=document.getElementById('lap-lng')?.value;if(lat)fd.append('lat',lat);if(lng)fd.append('lng',lng);const foto=document.getElementById('lap-foto')?.files?.[0];if(foto)fd.append('foto',foto);ld(true);try{const r=await fetch(EP.lap,{method:'POST',body:fd});const d=await r.json();if(!d.success)throw new Error(d.message);toast('Laporan berhasil dikirim. Terima kasih!','ok');closeM('lap');const lp=await rq(EP.lap);lapD=lp||[];rerender();checkAlerts();}catch(e){toast(e.message,'err');}finally{ld(false);}}
|
|
|
|
function checkAlerts(){const dar=lapD.filter(l=>l.urgensi==='darurat'&&l.status!=='selesai');document.getElementById('dlap').style.display=dar.length?'inline-block':'none';}
|
|
|
|
// MODAL
|
|
function openM(n){document.getElementById('modal-'+n)?.classList.add('open');}
|
|
function closeM(n){document.getElementById('modal-'+n)?.classList.remove('open');}
|
|
document.querySelectorAll('.overlay').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)closeM(o.id.replace('modal-',''));}));
|
|
|
|
function toggleSB(){document.getElementById('sidebar')?.classList.toggle('off');}
|
|
|
|
document.getElementById('lp')?.addEventListener('keydown',e=>{if(e.key==='Enter')doLogin();});
|
|
document.getElementById('lu')?.addEventListener('keydown',e=>{if(e.key==='Enter')document.getElementById('lp')?.focus();});
|
|
</script>
|
|
</body>
|
|
</html>
|