5261 lines
178 KiB
HTML
5261 lines
178 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>GeoSosial — Sistem Informasi Sosial Kota Pontianak</title>
|
||
<meta name="description"
|
||
content="Sistem Informasi Geografis Kemiskinan dan Bantuan Sosial Kota Pontianak — Dinas Sosial">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=Syne:wght@700;800&display=swap"
|
||
rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--ink: #0b1120;
|
||
--ink2: #3d4a60;
|
||
--ink3: #8a97ab;
|
||
--paper: #f0f2f8;
|
||
--w: #fff;
|
||
--blue: #1a5fff;
|
||
--bdim: #e8efff;
|
||
--teal: #00c9aa;
|
||
--tdim: #dcfaf5;
|
||
--red: #f03e5a;
|
||
--rdim: #fde8ec;
|
||
--orange: #f97316;
|
||
--odim: #fff3e8;
|
||
--purple: #7c3aed;
|
||
--pdim: #f0ebff;
|
||
--green: #10b981;
|
||
--gdim: #e5f8f1;
|
||
--yellow: #f59e0b;
|
||
--ydim: #fef3c7;
|
||
--bdr: rgba(11, 17, 32, .08);
|
||
--sh: 0 4px 24px rgba(11, 17, 32, .08);
|
||
--shm: 0 8px 40px rgba(11, 17, 32, .14);
|
||
--r: 14px;
|
||
--rs: 9px;
|
||
--glass: rgba(255, 255, 255, .88)
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
margin: 0;
|
||
padding: 0
|
||
}
|
||
|
||
html,
|
||
body {
|
||
height: 100%;
|
||
font-family: 'Plus Jakarta Sans', sans-serif;
|
||
background: var(--paper)
|
||
}
|
||
|
||
.page {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 10;
|
||
display: none
|
||
}
|
||
|
||
.page.active {
|
||
display: flex
|
||
}
|
||
|
||
/* LOGIN */
|
||
#pageLogin {
|
||
background: linear-gradient(145deg, #0b1a3e, #0d2560 40%, #091830);
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
flex-direction: column
|
||
}
|
||
|
||
.lbg {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
overflow: hidden
|
||
}
|
||
|
||
.lblob {
|
||
position: absolute;
|
||
border-radius: 50%;
|
||
filter: blur(60px);
|
||
opacity: .14;
|
||
will-change: transform;
|
||
contain: strict
|
||
}
|
||
|
||
.lblob.b1 {
|
||
width: 500px;
|
||
height: 500px;
|
||
background: #1a5fff;
|
||
top: -120px;
|
||
right: -100px
|
||
}
|
||
|
||
.lblob.b2 {
|
||
width: 400px;
|
||
height: 400px;
|
||
background: #00c9aa;
|
||
bottom: -80px;
|
||
left: -80px
|
||
}
|
||
|
||
.lblob.b3 {
|
||
width: 300px;
|
||
height: 300px;
|
||
background: #7c3aed;
|
||
top: 40%;
|
||
left: 30%
|
||
}
|
||
|
||
.lgrid {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image: linear-gradient(rgba(255, 255, 255, .04)1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, .04)1px, transparent 1px);
|
||
background-size: 48px 48px
|
||
}
|
||
|
||
.lcard {
|
||
position: relative;
|
||
z-index: 2;
|
||
background: rgba(15, 30, 70, 0.82);
|
||
border: 1px solid rgba(255, 255, 255, .12);
|
||
border-radius: 24px;
|
||
padding: 44px 40px;
|
||
width: 440px;
|
||
max-width: calc(100vw - 32px);
|
||
box-shadow: 0 24px 80px rgba(0, 0, 0, .4)
|
||
}
|
||
|
||
.llogo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 32px
|
||
}
|
||
|
||
.llogo-ic {
|
||
width: 44px;
|
||
height: 44px;
|
||
background: linear-gradient(135deg, #1a5fff, #00c9aa);
|
||
border-radius: 13px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 4px 16px rgba(26, 95, 255, .4)
|
||
}
|
||
|
||
.llogo-tx {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 22px;
|
||
color: #fff;
|
||
letter-spacing: -.5px
|
||
}
|
||
|
||
.llogo-sb {
|
||
font-size: 10px;
|
||
color: rgba(255, 255, 255, .4);
|
||
margin-top: 1px;
|
||
letter-spacing: .06em
|
||
}
|
||
|
||
.lh1 {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 26px;
|
||
color: #fff;
|
||
margin-bottom: 6px;
|
||
letter-spacing: -.5px
|
||
}
|
||
|
||
.ldesc {
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, .5);
|
||
margin-bottom: 28px;
|
||
line-height: 1.6
|
||
}
|
||
|
||
.llbl {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, .5);
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.linp {
|
||
width: 100%;
|
||
padding: 13px 16px;
|
||
background: rgba(255, 255, 255, .08);
|
||
border: 1px solid rgba(255, 255, 255, .14);
|
||
border-radius: 10px;
|
||
font-size: 14px;
|
||
color: #fff;
|
||
outline: none;
|
||
font-family: inherit;
|
||
transition: all .2s;
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.linp::placeholder {
|
||
color: rgba(255, 255, 255, .28)
|
||
}
|
||
|
||
.linp:focus {
|
||
background: rgba(255, 255, 255, .13);
|
||
border-color: rgba(26, 95, 255, .8);
|
||
box-shadow: 0 0 0 3px rgba(26, 95, 255, .18)
|
||
}
|
||
|
||
.btn-login {
|
||
width: 100%;
|
||
padding: 14px;
|
||
margin-top: 20px;
|
||
background: linear-gradient(90deg, #1a5fff, #00c9aa);
|
||
border: none;
|
||
border-radius: 11px;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s;
|
||
box-shadow: 0 6px 24px rgba(26, 95, 255, .35)
|
||
}
|
||
|
||
.btn-login:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 10px 32px rgba(26, 95, 255, .45)
|
||
}
|
||
|
||
.lerr {
|
||
display: none;
|
||
background: rgba(240, 62, 90, .15);
|
||
border: 1px solid rgba(240, 62, 90, .3);
|
||
border-radius: 8px;
|
||
padding: 10px 14px;
|
||
font-size: 12px;
|
||
color: #f9b4bf;
|
||
margin-top: 14px;
|
||
text-align: center
|
||
}
|
||
|
||
.lerr.show {
|
||
display: block
|
||
}
|
||
|
||
.btn-lapor {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 8px;
|
||
width: 100%;
|
||
padding: 11px;
|
||
margin-top: 10px;
|
||
background: rgba(255, 255, 255, .08);
|
||
border: 1px solid rgba(255, 255, 255, .2);
|
||
border-radius: 11px;
|
||
color: rgba(255, 255, 255, .8);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.btn-lapor:hover {
|
||
background: rgba(255, 255, 255, .15);
|
||
border-color: rgba(255, 255, 255, .35)
|
||
}
|
||
|
||
.ldemo {
|
||
margin-top: 18px;
|
||
padding: 14px;
|
||
background: rgba(255, 255, 255, .05);
|
||
border: 1px solid rgba(255, 255, 255, .08);
|
||
border-radius: 10px;
|
||
font-size: 11px;
|
||
color: rgba(255, 255, 255, .35);
|
||
text-align: center;
|
||
line-height: 1.9
|
||
}
|
||
|
||
.ldemo b {
|
||
color: rgba(255, 255, 255, .55)
|
||
}
|
||
|
||
/* PUBLIC REPORT */
|
||
#pageRep {
|
||
background: linear-gradient(160deg, #0b1a3e, #0d2560 50%, #0a2040);
|
||
flex-direction: column;
|
||
overflow: hidden
|
||
}
|
||
|
||
.rh {
|
||
padding: 14px 24px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, .1);
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.btn-back {
|
||
padding: 7px 14px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
background: rgba(255, 255, 255, .1);
|
||
border: 1px solid rgba(255, 255, 255, .15);
|
||
color: rgba(255, 255, 255, .7);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.btn-back:hover {
|
||
background: rgba(255, 255, 255, .18);
|
||
color: #fff
|
||
}
|
||
|
||
.rb-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-left: 4px
|
||
}
|
||
|
||
.rb-lic {
|
||
width: 34px;
|
||
height: 34px;
|
||
background: linear-gradient(135deg, #1a5fff, #00c9aa);
|
||
border-radius: 10px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px
|
||
}
|
||
|
||
.rb-ltx {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 16px;
|
||
color: #fff
|
||
}
|
||
|
||
.rb-lsb {
|
||
font-size: 10px;
|
||
color: rgba(255, 255, 255, .4);
|
||
margin-top: 1px
|
||
}
|
||
|
||
.rb-badge {
|
||
margin-left: auto;
|
||
background: rgba(0, 201, 170, .2);
|
||
border: 1px solid rgba(0, 201, 170, .3);
|
||
padding: 4px 12px;
|
||
border-radius: 999px;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: #00c9aa;
|
||
letter-spacing: .05em
|
||
}
|
||
|
||
.rb-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 24px
|
||
}
|
||
|
||
.rb-card {
|
||
background: rgba(10, 28, 70, 0.82);
|
||
border: 1px solid rgba(255, 255, 255, .1);
|
||
border-radius: 20px;
|
||
padding: 32px;
|
||
width: 100%;
|
||
max-width: 640px;
|
||
height: fit-content;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, .3)
|
||
}
|
||
|
||
.rb-h1 {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 20px;
|
||
color: #fff;
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.rb-desc {
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, .5);
|
||
margin-bottom: 28px;
|
||
line-height: 1.6
|
||
}
|
||
|
||
.rs-lbl {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: rgba(255, 255, 255, .45);
|
||
letter-spacing: .1em;
|
||
text-transform: uppercase;
|
||
margin: 20px 0 10px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, .08)
|
||
}
|
||
|
||
.rf-lbl {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: rgba(255, 255, 255, .5);
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.rf-inp,
|
||
.rf-sel,
|
||
.rf-ta {
|
||
width: 100%;
|
||
padding: 12px 14px;
|
||
background: rgba(255, 255, 255, .08);
|
||
border: 1px solid rgba(255, 255, 255, .14);
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
color: #fff;
|
||
outline: none;
|
||
font-family: inherit;
|
||
transition: all .2s;
|
||
margin-bottom: 14px
|
||
}
|
||
|
||
.rf-inp::placeholder,
|
||
.rf-ta::placeholder {
|
||
color: rgba(255, 255, 255, .28)
|
||
}
|
||
|
||
.rf-inp:focus,
|
||
.rf-sel:focus,
|
||
.rf-ta:focus {
|
||
background: rgba(255, 255, 255, .13);
|
||
border-color: rgba(26, 95, 255, .8);
|
||
box-shadow: 0 0 0 3px rgba(26, 95, 255, .18)
|
||
}
|
||
|
||
.rf-sel option {
|
||
background: #0d2560;
|
||
color: #fff
|
||
}
|
||
|
||
.rf-ta {
|
||
resize: vertical;
|
||
min-height: 80px
|
||
}
|
||
|
||
.rf-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px
|
||
}
|
||
|
||
.rf-map {
|
||
width: 100%;
|
||
height: 180px;
|
||
border-radius: 12px;
|
||
border: 1px solid rgba(255, 255, 255, .15);
|
||
overflow: hidden;
|
||
cursor: crosshair;
|
||
position: relative;
|
||
margin-bottom: 8px
|
||
}
|
||
|
||
.rf-maplbl {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(11, 17, 32, .7);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 4px 12px;
|
||
border-radius: 999px;
|
||
pointer-events: none;
|
||
z-index: 10;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.rf-coords {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 14px
|
||
}
|
||
|
||
.rf-cbox {
|
||
flex: 1;
|
||
padding: 8px 11px;
|
||
background: rgba(255, 255, 255, .06);
|
||
border: 1px solid rgba(255, 255, 255, .1);
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
color: rgba(255, 255, 255, .6);
|
||
font-family: inherit
|
||
}
|
||
|
||
.btn-srep {
|
||
width: 100%;
|
||
padding: 14px;
|
||
border-radius: 11px;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
background: linear-gradient(90deg, #00c9aa, #1a5fff);
|
||
border: none;
|
||
color: #fff;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s;
|
||
margin-top: 8px;
|
||
box-shadow: 0 6px 24px rgba(0, 201, 170, .3)
|
||
}
|
||
|
||
.btn-srep:hover {
|
||
transform: translateY(-1px);
|
||
opacity: .92
|
||
}
|
||
|
||
.rep-ok {
|
||
display: none;
|
||
text-align: center;
|
||
padding: 32px 0
|
||
}
|
||
|
||
.rep-ok.show {
|
||
display: block
|
||
}
|
||
|
||
.rep-ok-ic {
|
||
font-size: 56px;
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.rep-ok-h1 {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 22px;
|
||
color: #00c9aa;
|
||
margin-bottom: 8px
|
||
}
|
||
|
||
.rep-ok-desc {
|
||
font-size: 13px;
|
||
color: rgba(255, 255, 255, .5);
|
||
line-height: 1.6;
|
||
margin-bottom: 20px
|
||
}
|
||
|
||
.rep-ok-no {
|
||
background: rgba(0, 201, 170, .15);
|
||
border: 1px solid rgba(0, 201, 170, .3);
|
||
padding: 12px 20px;
|
||
border-radius: 12px;
|
||
display: inline-block;
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 18px;
|
||
color: #00c9aa
|
||
}
|
||
|
||
/* APP */
|
||
#pageApp {
|
||
flex-direction: column;
|
||
background: var(--paper);
|
||
display: none;
|
||
overflow: hidden
|
||
}
|
||
|
||
#pageApp.active {
|
||
display: flex
|
||
}
|
||
|
||
#topbar {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 56px;
|
||
z-index: 900;
|
||
display: flex;
|
||
align-items: center;
|
||
background: rgba(255, 255, 255, .98);
|
||
border-bottom: 1px solid var(--bdr);
|
||
padding: 0 20px;
|
||
gap: 6px;
|
||
box-shadow: 0 2px 12px rgba(11, 17, 32, .06)
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
margin-right: 10px;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.brand-ic {
|
||
width: 30px;
|
||
height: 30px;
|
||
background: linear-gradient(135deg, #1a5fff, #00c9aa);
|
||
border-radius: 9px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px
|
||
}
|
||
|
||
.brand-tx {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 14px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.brand-sb {
|
||
font-size: 10px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.ndiv {
|
||
width: 1px;
|
||
height: 20px;
|
||
background: var(--bdr);
|
||
margin: 0 4px;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.np {
|
||
padding: 6px 13px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--ink2);
|
||
cursor: pointer;
|
||
border: none;
|
||
background: transparent;
|
||
transition: all .18s;
|
||
font-family: inherit;
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 5px
|
||
}
|
||
|
||
.np:hover {
|
||
background: rgba(26, 95, 255, .07);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.np.active {
|
||
background: var(--bdim);
|
||
color: var(--blue);
|
||
font-weight: 700
|
||
}
|
||
|
||
.np.hidden {
|
||
display: none !important
|
||
}
|
||
|
||
.np .nbadge {
|
||
background: var(--red);
|
||
color: #fff;
|
||
font-size: 9px;
|
||
font-weight: 800;
|
||
min-width: 16px;
|
||
height: 16px;
|
||
border-radius: 99px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 4px
|
||
}
|
||
|
||
.tr {
|
||
margin-left: auto;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.role-badge {
|
||
background: linear-gradient(90deg, #1a5fff, #00c9aa);
|
||
color: #fff;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 4px 11px;
|
||
border-radius: 999px;
|
||
letter-spacing: .05em
|
||
}
|
||
|
||
.top-user {
|
||
font-size: 11px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.top-user-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
padding: 4px 10px 4px 4px;
|
||
border-radius: 99px;
|
||
border: 1px solid var(--bdr);
|
||
background: var(--paper);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
transition: all .18s;
|
||
}
|
||
|
||
.top-user-btn:hover {
|
||
border-color: var(--blue);
|
||
background: var(--bdim)
|
||
}
|
||
|
||
.top-user-av {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
color: #fff;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.top-user-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
line-height: 1.25
|
||
}
|
||
|
||
.top-user-name {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.top-user-role {
|
||
font-size: 9px;
|
||
color: var(--ink3);
|
||
font-weight: 500
|
||
}
|
||
|
||
.btn-out {
|
||
font-size: 12px;
|
||
color: var(--ink3);
|
||
background: none;
|
||
border: 1px solid var(--bdr);
|
||
cursor: pointer;
|
||
padding: 5px 11px;
|
||
border-radius: 7px;
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.btn-out:hover {
|
||
color: var(--red);
|
||
border-color: var(--rdim);
|
||
background: var(--rdim)
|
||
}
|
||
|
||
/* MAP */
|
||
#map {
|
||
position: fixed;
|
||
top: 56px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 0
|
||
}
|
||
|
||
.panel {
|
||
position: fixed;
|
||
z-index: 800;
|
||
background: rgba(248, 249, 252, 0.97);
|
||
border: 1px solid rgba(220, 226, 240, .9);
|
||
border-radius: var(--r);
|
||
box-shadow: var(--sh)
|
||
}
|
||
|
||
#panelLeft {
|
||
top: 70px;
|
||
left: 14px;
|
||
width: 252px;
|
||
max-height: calc(100vh - 100px);
|
||
padding: 16px 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgba(26,95,255,.18) transparent
|
||
}
|
||
#panelLeft::-webkit-scrollbar { width: 4px }
|
||
#panelLeft::-webkit-scrollbar-track { background: transparent }
|
||
#panelLeft::-webkit-scrollbar-thumb { background: rgba(26,95,255,.2); border-radius: 99px }
|
||
#panelLeft::-webkit-scrollbar-thumb:hover { background: rgba(26,95,255,.4) }
|
||
|
||
.ph {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 13px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.ps {
|
||
font-size: 11px;
|
||
color: var(--ink3);
|
||
margin-top: 1px
|
||
}
|
||
|
||
.fl {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
color: var(--ink3);
|
||
margin-bottom: 5px
|
||
}
|
||
|
||
select.fs {
|
||
width: 100%;
|
||
background: rgba(255, 255, 255, .7);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: var(--rs);
|
||
padding: 8px 10px;
|
||
font-size: 12px;
|
||
color: var(--ink);
|
||
outline: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: border-color .15s
|
||
}
|
||
|
||
select.fs:focus {
|
||
border-color: var(--blue);
|
||
background: #fff
|
||
}
|
||
|
||
.pdiv {
|
||
height: 1px;
|
||
background: var(--bdr)
|
||
}
|
||
|
||
.act-btn {
|
||
width: 100%;
|
||
padding: 9px 13px;
|
||
border-radius: var(--rs);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
transition: all .18s;
|
||
font-family: inherit
|
||
}
|
||
|
||
.btn-ri {
|
||
background: var(--tdim);
|
||
color: #007e6a
|
||
}
|
||
|
||
.btn-ri:hover {
|
||
background: #c2f5ed
|
||
}
|
||
|
||
.btn-km {
|
||
background: var(--bdim);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.btn-km:hover {
|
||
background: #cfdeff
|
||
}
|
||
|
||
.btn-blank {
|
||
background: var(--rdim);
|
||
color: var(--red)
|
||
}
|
||
|
||
.btn-blank:hover {
|
||
background: #fac8d2
|
||
}
|
||
|
||
.btn-blank.on {
|
||
background: var(--red);
|
||
color: #fff
|
||
}
|
||
|
||
.btn-radar {
|
||
background: var(--pdim);
|
||
color: var(--purple)
|
||
}
|
||
|
||
.btn-radar:hover {
|
||
background: #ddd1fc
|
||
}
|
||
|
||
.btn-radar.on {
|
||
background: var(--purple);
|
||
color: #fff
|
||
}
|
||
|
||
.rw {
|
||
overflow: hidden;
|
||
transition: max-height .3s;
|
||
max-height: 0
|
||
}
|
||
|
||
.rw.open {
|
||
max-height: 60px
|
||
}
|
||
|
||
.rr {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px
|
||
}
|
||
|
||
input[type=range] {
|
||
flex: 1;
|
||
accent-color: var(--purple);
|
||
cursor: pointer
|
||
}
|
||
|
||
.rl {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--purple);
|
||
min-width: 38px;
|
||
text-align: right
|
||
}
|
||
|
||
#panelStats {
|
||
top: 70px;
|
||
right: 14px;
|
||
width: 216px;
|
||
padding: 14px 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px
|
||
}
|
||
|
||
#panelLayers {
|
||
top: auto;
|
||
bottom: 170px;
|
||
right: 14px;
|
||
width: 216px;
|
||
padding: 12px 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px
|
||
}
|
||
|
||
.si {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 9px;
|
||
padding: 9px 11px;
|
||
border-radius: var(--rs);
|
||
background: rgba(255, 255, 255, .6);
|
||
border: 1px solid var(--bdr)
|
||
}
|
||
|
||
.sic {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.sic.r {
|
||
background: var(--rdim)
|
||
}
|
||
|
||
.sic.g {
|
||
background: var(--gdim)
|
||
}
|
||
|
||
.sic.p {
|
||
background: var(--pdim)
|
||
}
|
||
|
||
.sic.b {
|
||
background: var(--bdim)
|
||
}
|
||
|
||
.sn {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 17px;
|
||
color: var(--ink);
|
||
line-height: 1
|
||
}
|
||
|
||
.sl {
|
||
font-size: 10px;
|
||
color: var(--ink3);
|
||
margin-top: 1px
|
||
}
|
||
|
||
#panelLeg {
|
||
bottom: 24px;
|
||
left: 14px;
|
||
padding: 13px 15px;
|
||
min-width: 175px
|
||
}
|
||
|
||
.lt {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--ink2);
|
||
margin-bottom: 9px;
|
||
letter-spacing: .06em;
|
||
text-transform: uppercase
|
||
}
|
||
|
||
.li {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--ink2);
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.ld {
|
||
width: 9px;
|
||
height: 9px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.lp {
|
||
opacity: .85
|
||
}
|
||
|
||
#panelList {
|
||
bottom: 24px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: 13px 15px;
|
||
width: 510px;
|
||
max-width: calc(100vw - 280px)
|
||
}
|
||
|
||
.ltt {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--ink2);
|
||
margin-bottom: 9px;
|
||
letter-spacing: .06em;
|
||
text-transform: uppercase
|
||
}
|
||
|
||
.rsc {
|
||
display: flex;
|
||
gap: 7px;
|
||
overflow-x: auto;
|
||
padding-bottom: 2px
|
||
}
|
||
|
||
.rsc::-webkit-scrollbar {
|
||
height: 3px
|
||
}
|
||
|
||
.rsc::-webkit-scrollbar-thumb {
|
||
background: var(--bdr);
|
||
border-radius: 99px
|
||
}
|
||
|
||
.rc {
|
||
flex-shrink: 0;
|
||
background: rgba(255, 255, 255, .7);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: var(--rs);
|
||
padding: 9px 11px;
|
||
cursor: pointer;
|
||
transition: all .18s;
|
||
min-width: 135px
|
||
}
|
||
|
||
.rc:hover {
|
||
background: #fff;
|
||
box-shadow: 0 4px 16px rgba(26, 95, 255, .1);
|
||
border-color: var(--blue);
|
||
transform: translateY(-1px)
|
||
}
|
||
|
||
.rn {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--ink);
|
||
margin-bottom: 2px
|
||
}
|
||
|
||
.rs {
|
||
font-size: 10px;
|
||
color: var(--ink3);
|
||
margin-bottom: 5px
|
||
}
|
||
|
||
.rb2 {
|
||
height: 3px;
|
||
background: rgba(0, 0, 0, .07);
|
||
border-radius: 99px;
|
||
overflow: hidden
|
||
}
|
||
|
||
.rf2 {
|
||
height: 100%;
|
||
border-radius: 99px;
|
||
transition: width .4s
|
||
}
|
||
|
||
.rp {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
margin-top: 3px
|
||
}
|
||
|
||
/* LAYER TOGGLE: OPSIONAL */
|
||
.opt-section {
|
||
margin-top: 2px
|
||
}
|
||
.opt-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 6px 2px;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
border-radius: 6px;
|
||
transition: background .15s
|
||
}
|
||
.opt-head:hover { background: rgba(26, 95, 255, .05) }
|
||
.opt-title {
|
||
font-size: 10px;
|
||
font-weight: 800;
|
||
letter-spacing: .09em;
|
||
text-transform: uppercase;
|
||
color: var(--ink3)
|
||
}
|
||
.opt-arrow {
|
||
font-size: 11px;
|
||
color: var(--ink3);
|
||
transition: transform .2s
|
||
}
|
||
.opt-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 2px;
|
||
padding: 2px 0 4px
|
||
}
|
||
.opt-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 6px;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background .15s;
|
||
position: relative
|
||
}
|
||
.opt-item:hover { background: rgba(26, 95, 255, .06) }
|
||
.opt-item input[type=checkbox] {
|
||
width: 14px;
|
||
height: 14px;
|
||
accent-color: var(--blue);
|
||
cursor: pointer;
|
||
flex-shrink: 0
|
||
}
|
||
.opt-dot {
|
||
width: 10px;
|
||
height: 10px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0
|
||
}
|
||
.opt-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px
|
||
}
|
||
.opt-name {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--ink)
|
||
}
|
||
.opt-sub {
|
||
font-size: 10px;
|
||
color: var(--ink3)
|
||
}
|
||
.opt-soon {
|
||
color: var(--orange);
|
||
font-weight: 600
|
||
}
|
||
.opt-disabled {
|
||
opacity: .5;
|
||
cursor: not-allowed
|
||
}
|
||
.opt-disabled:hover { background: transparent }
|
||
.opt-disabled input { cursor: not-allowed }
|
||
|
||
/* POPUP */
|
||
.leaflet-popup-content-wrapper {
|
||
background: #fff !important;
|
||
border: 1px solid rgba(220, 226, 240, .9) !important;
|
||
border-radius: 16px !important;
|
||
box-shadow: var(--shm) !important;
|
||
padding: 0 !important
|
||
}
|
||
|
||
.leaflet-popup-tip-container {
|
||
display: none !important
|
||
}
|
||
|
||
.leaflet-popup-content {
|
||
margin: 0 !important
|
||
}
|
||
|
||
.leaflet-popup-close-button {
|
||
display: none !important
|
||
}
|
||
|
||
.pw {
|
||
padding: 15px 17px;
|
||
min-width: 240px
|
||
}
|
||
|
||
.pt {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
margin-bottom: 10px
|
||
}
|
||
|
||
.pn {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 13px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.pn2 {
|
||
font-size: 10px;
|
||
color: var(--ink3);
|
||
margin-top: 2px
|
||
}
|
||
|
||
.kb {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 3px 9px;
|
||
border-radius: 999px;
|
||
letter-spacing: .04em;
|
||
white-space: nowrap;
|
||
margin-left: 8px;
|
||
flex-shrink: 0
|
||
}
|
||
|
||
.ksm {
|
||
background: var(--rdim);
|
||
color: var(--red)
|
||
}
|
||
|
||
.km {
|
||
background: var(--odim);
|
||
color: var(--orange)
|
||
}
|
||
|
||
.khm {
|
||
background: var(--bdim);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.prows {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-bottom: 10px
|
||
}
|
||
|
||
.pr {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px
|
||
}
|
||
|
||
.pk {
|
||
font-size: 11px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.pv {
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
color: var(--ink);
|
||
text-align: right;
|
||
max-width: 140px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.pv.teal {
|
||
color: var(--teal) !important
|
||
}
|
||
|
||
.pcta-row {
|
||
display: flex;
|
||
gap: 6px
|
||
}
|
||
|
||
.pcta {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border-radius: 9px;
|
||
background: linear-gradient(90deg, #1a5fff, #00c9aa);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
border: none;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 5px;
|
||
transition: opacity .15s;
|
||
font-family: inherit
|
||
}
|
||
|
||
.pcta:hover {
|
||
opacity: .88
|
||
}
|
||
|
||
.pcta.sec {
|
||
background: var(--paper);
|
||
color: var(--ink2);
|
||
border: 1px solid var(--bdr);
|
||
font-weight: 600
|
||
}
|
||
|
||
.pcta.sec:hover {
|
||
background: #e4e8f2
|
||
}
|
||
|
||
.pdel {
|
||
margin-top: 7px;
|
||
width: 100%;
|
||
padding: 8px;
|
||
border-radius: 9px;
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
border: none;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.pdel:hover {
|
||
background: var(--red);
|
||
color: #fff
|
||
}
|
||
|
||
/* TAB CONTENT */
|
||
.tc {
|
||
position: fixed;
|
||
top: 56px;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
overflow-y: auto;
|
||
z-index: 5;
|
||
background: var(--paper);
|
||
display: none;
|
||
padding: 28px 28px 40px
|
||
}
|
||
|
||
.tc.active {
|
||
display: block
|
||
}
|
||
|
||
#tabMap {
|
||
padding: 0;
|
||
overflow: hidden;
|
||
background: transparent;
|
||
z-index: 0
|
||
}
|
||
|
||
#tabMap.active {
|
||
display: block
|
||
}
|
||
|
||
/* DASHBOARD */
|
||
.dash-h {
|
||
margin-bottom: 24px
|
||
}
|
||
|
||
.dash-t {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 22px;
|
||
color: var(--ink);
|
||
margin-bottom: 4px
|
||
}
|
||
|
||
.dash-s {
|
||
font-size: 13px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 14px;
|
||
margin-bottom: 24px
|
||
}
|
||
|
||
.kpi {
|
||
background: #fff;
|
||
border-radius: var(--r);
|
||
padding: 20px;
|
||
border: 1px solid var(--bdr);
|
||
box-shadow: var(--sh);
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: transform .2s, box-shadow .2s
|
||
}
|
||
|
||
.kpi:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shm)
|
||
}
|
||
|
||
.kpi::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
height: 3px
|
||
}
|
||
|
||
.kpi.cr::before {
|
||
background: var(--red)
|
||
}
|
||
|
||
.kpi.cb::before {
|
||
background: var(--blue)
|
||
}
|
||
|
||
.kpi.ct::before {
|
||
background: var(--teal)
|
||
}
|
||
|
||
.kpi.cp::before {
|
||
background: var(--purple)
|
||
}
|
||
|
||
.kpi-ic {
|
||
font-size: 28px;
|
||
margin-bottom: 10px
|
||
}
|
||
|
||
.kpi-v {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 28px;
|
||
color: var(--ink);
|
||
line-height: 1;
|
||
margin-bottom: 5px
|
||
}
|
||
|
||
.kpi-l {
|
||
font-size: 12px;
|
||
color: var(--ink3);
|
||
font-weight: 500
|
||
}
|
||
|
||
.kpi-d {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
margin-top: 6px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.charts-grid {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 16px;
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.charts-grid2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.ccard {
|
||
background: #fff;
|
||
border-radius: var(--r);
|
||
padding: 22px;
|
||
border: 1px solid var(--bdr);
|
||
box-shadow: var(--sh)
|
||
}
|
||
|
||
.ct {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 13px;
|
||
color: var(--ink);
|
||
margin-bottom: 4px
|
||
}
|
||
|
||
.cs {
|
||
font-size: 11px;
|
||
color: var(--ink3);
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
/* DATA TABLE */
|
||
.dh {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 20px
|
||
}
|
||
|
||
.dt {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 20px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.dtbar {
|
||
background: #fff;
|
||
border-radius: var(--r);
|
||
padding: 14px 16px;
|
||
border: 1px solid var(--bdr);
|
||
box-shadow: var(--sh);
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap
|
||
}
|
||
|
||
.dts {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 9px 14px;
|
||
background: var(--paper);
|
||
border: 1.5px solid var(--bdr);
|
||
border-radius: var(--rs);
|
||
font-size: 13px;
|
||
color: var(--ink);
|
||
outline: none;
|
||
font-family: inherit;
|
||
transition: border-color .2s
|
||
}
|
||
|
||
.dts:focus {
|
||
border-color: var(--blue)
|
||
}
|
||
|
||
.dtsel {
|
||
padding: 9px 12px;
|
||
background: var(--paper);
|
||
border: 1.5px solid var(--bdr);
|
||
border-radius: var(--rs);
|
||
font-size: 12px;
|
||
color: var(--ink2);
|
||
outline: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: border-color .2s
|
||
}
|
||
|
||
.dtsel:focus {
|
||
border-color: var(--blue)
|
||
}
|
||
|
||
.dtc {
|
||
font-size: 12px;
|
||
color: var(--ink3);
|
||
white-space: nowrap
|
||
}
|
||
|
||
.dtexp {
|
||
padding: 9px 16px;
|
||
background: var(--tdim);
|
||
color: #007e6a;
|
||
border: none;
|
||
border-radius: var(--rs);
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.dtexp:hover {
|
||
background: #c2f5ed
|
||
}
|
||
|
||
.tw {
|
||
background: #fff;
|
||
border-radius: var(--r);
|
||
border: 1px solid var(--bdr);
|
||
box-shadow: var(--sh);
|
||
overflow: hidden
|
||
}
|
||
|
||
table.mt {
|
||
width: 100%;
|
||
border-collapse: collapse
|
||
}
|
||
|
||
table.mt th {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--ink2);
|
||
text-transform: uppercase;
|
||
letter-spacing: .08em;
|
||
padding: 14px 18px;
|
||
text-align: left;
|
||
background: #fafbfe;
|
||
border-bottom: 1.5px solid var(--bdr);
|
||
}
|
||
|
||
table.mt td {
|
||
font-size: 13px;
|
||
color: var(--ink);
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid rgba(11, 17, 32, 0.05);
|
||
vertical-align: middle;
|
||
}
|
||
|
||
table.mt tr:last-child td {
|
||
border-bottom: none
|
||
}
|
||
|
||
table.mt tr:hover td {
|
||
background: #fcfdfe;
|
||
}
|
||
|
||
.td-name {
|
||
font-weight: 600
|
||
}
|
||
|
||
.td-nik {
|
||
font-family: monospace;
|
||
font-size: 11px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.td-act {
|
||
display: flex;
|
||
gap: 5px
|
||
}
|
||
|
||
.asm {
|
||
padding: 5px 10px;
|
||
border-radius: 6px;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.asm.b {
|
||
background: var(--bdim);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.asm.b:hover {
|
||
background: var(--blue);
|
||
color: #fff
|
||
}
|
||
|
||
.asm.t {
|
||
background: var(--tdim);
|
||
color: #007e6a
|
||
}
|
||
|
||
.asm.t:hover {
|
||
background: var(--teal);
|
||
color: #fff
|
||
}
|
||
|
||
.asm.r {
|
||
background: var(--rdim);
|
||
color: var(--red)
|
||
}
|
||
|
||
.asm.r:hover {
|
||
background: var(--red);
|
||
color: #fff
|
||
}
|
||
|
||
.kb2 {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 3px 9px;
|
||
border-radius: 999px
|
||
}
|
||
|
||
.kb2.sm {
|
||
background: var(--rdim);
|
||
color: var(--red)
|
||
}
|
||
|
||
.kb2.m {
|
||
background: var(--odim);
|
||
color: var(--orange)
|
||
}
|
||
|
||
.kb2.hm {
|
||
background: var(--bdim);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.pgn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 14px 16px;
|
||
justify-content: flex-end;
|
||
border-top: 1px solid var(--bdr)
|
||
}
|
||
|
||
.pgb {
|
||
width: 32px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
border: 1px solid var(--bdr);
|
||
background: #fff;
|
||
cursor: pointer;
|
||
color: var(--ink2);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all .15s
|
||
}
|
||
|
||
.pgb:hover {
|
||
background: var(--bdim);
|
||
border-color: var(--blue);
|
||
color: var(--blue)
|
||
}
|
||
|
||
.pgb.active {
|
||
background: var(--blue);
|
||
color: #fff;
|
||
border-color: var(--blue)
|
||
}
|
||
|
||
.pgb:disabled {
|
||
opacity: .4;
|
||
cursor: not-allowed
|
||
}
|
||
|
||
/* LAPORAN */
|
||
.ls-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 20px
|
||
}
|
||
|
||
.ls {
|
||
background: #fff;
|
||
border-radius: var(--rs);
|
||
padding: 16px;
|
||
border: 1px solid var(--bdr);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px
|
||
}
|
||
|
||
.ls-ic {
|
||
font-size: 22px
|
||
}
|
||
|
||
.ls-v {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 20px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.ls-l {
|
||
font-size: 11px;
|
||
color: var(--ink3);
|
||
margin-top: 1px
|
||
}
|
||
|
||
.sb {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 3px 10px;
|
||
border-radius: 99px;
|
||
letter-spacing: .04em;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.sb.menunggu {
|
||
background: var(--ydim);
|
||
color: #b45309
|
||
}
|
||
|
||
.sb.disetujui {
|
||
background: var(--gdim);
|
||
color: var(--green)
|
||
}
|
||
|
||
.sb.ditolak {
|
||
background: var(--rdim);
|
||
color: var(--red)
|
||
}
|
||
|
||
/* MODAL */
|
||
.mo {
|
||
position: fixed;
|
||
inset: 0;
|
||
z-index: 2000;
|
||
background: rgba(11, 17, 32, .45);
|
||
backdrop-filter: blur(6px);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity .25s
|
||
}
|
||
|
||
.mo.open {
|
||
opacity: 1;
|
||
pointer-events: all
|
||
}
|
||
|
||
.modal {
|
||
background: #fff;
|
||
border-radius: 20px;
|
||
box-shadow: var(--shm);
|
||
width: 520px;
|
||
max-width: calc(100vw - 32px);
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
transform: translateY(24px) scale(.97);
|
||
transition: transform .28s cubic-bezier(.34, 1.2, .64, 1)
|
||
}
|
||
|
||
.mo.open .modal {
|
||
transform: translateY(0) scale(1)
|
||
}
|
||
|
||
.modal.lg {
|
||
width: 680px
|
||
}
|
||
|
||
.mh {
|
||
padding: 22px 24px 16px;
|
||
border-bottom: 1px solid var(--bdr);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
position: sticky;
|
||
top: 0;
|
||
background: #fff;
|
||
z-index: 2;
|
||
border-radius: 20px 20px 0 0
|
||
}
|
||
|
||
.mh-t {
|
||
font-family: 'Syne', sans-serif;
|
||
font-size: 16px;
|
||
color: var(--ink)
|
||
}
|
||
|
||
.mh-s {
|
||
font-size: 11px;
|
||
color: var(--ink3);
|
||
margin-top: 2px
|
||
}
|
||
|
||
.mc {
|
||
width: 30px;
|
||
height: 30px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--bdr);
|
||
background: none;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 15px;
|
||
color: var(--ink2);
|
||
transition: all .15s
|
||
}
|
||
|
||
.mc:hover {
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
border-color: transparent
|
||
}
|
||
|
||
.mb {
|
||
padding: 20px 24px
|
||
}
|
||
|
||
.mf {
|
||
padding: 14px 24px 20px;
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
border-top: 1px solid var(--bdr)
|
||
}
|
||
|
||
.fg {
|
||
margin-bottom: 16px
|
||
}
|
||
|
||
.flb {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--ink3);
|
||
letter-spacing: .07em;
|
||
text-transform: uppercase;
|
||
margin-bottom: 6px
|
||
}
|
||
|
||
.fi,
|
||
.fsl,
|
||
.fta {
|
||
width: 100%;
|
||
padding: 11px 13px;
|
||
background: var(--paper);
|
||
border: 1.5px solid var(--bdr);
|
||
border-radius: var(--rs);
|
||
font-size: 13px;
|
||
color: var(--ink);
|
||
outline: none;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.fi:focus,
|
||
.fsl:focus,
|
||
.fta:focus {
|
||
border-color: var(--blue);
|
||
background: #fff;
|
||
box-shadow: 0 0 0 3px rgba(26, 95, 255, .1)
|
||
}
|
||
|
||
.fta {
|
||
resize: vertical;
|
||
min-height: 72px
|
||
}
|
||
|
||
.fr {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px
|
||
}
|
||
|
||
.fmpp {
|
||
width: 100%;
|
||
height: 180px;
|
||
border-radius: 10px;
|
||
border: 1.5px solid var(--bdr);
|
||
overflow: hidden;
|
||
cursor: crosshair;
|
||
position: relative
|
||
}
|
||
|
||
.fmpl {
|
||
position: absolute;
|
||
top: 8px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: rgba(11, 17, 32, .7);
|
||
color: #fff;
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
padding: 4px 12px;
|
||
border-radius: 999px;
|
||
pointer-events: none;
|
||
z-index: 10;
|
||
white-space: nowrap
|
||
}
|
||
|
||
.fmpc {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 8px
|
||
}
|
||
|
||
.fmcb {
|
||
flex: 1;
|
||
padding: 8px 11px;
|
||
background: var(--paper);
|
||
border: 1.5px solid var(--bdr);
|
||
border-radius: 8px;
|
||
font-size: 12px;
|
||
color: var(--ink2);
|
||
font-family: inherit
|
||
}
|
||
|
||
.penyakit-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 6px;
|
||
margin-bottom: 4px
|
||
}
|
||
|
||
.pcb {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 12px;
|
||
color: var(--ink2);
|
||
cursor: pointer;
|
||
padding: 7px 10px;
|
||
border-radius: 8px;
|
||
border: 1.5px solid var(--bdr);
|
||
background: var(--paper);
|
||
transition: all .15s;
|
||
user-select: none
|
||
}
|
||
|
||
.pcb:has(input:checked) {
|
||
border-color: var(--red);
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
font-weight: 600
|
||
}
|
||
|
||
.pcb input {
|
||
display: none
|
||
}
|
||
|
||
.at {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 8px
|
||
}
|
||
|
||
.at th {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--ink3);
|
||
text-transform: uppercase;
|
||
letter-spacing: .06em;
|
||
padding: 6px 8px;
|
||
text-align: left;
|
||
background: var(--paper);
|
||
border-bottom: 1px solid var(--bdr)
|
||
}
|
||
|
||
.at td {
|
||
padding: 5px 3px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, .04)
|
||
}
|
||
|
||
.ai {
|
||
width: 100%;
|
||
padding: 6px 8px;
|
||
background: #fff;
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
color: var(--ink);
|
||
outline: none;
|
||
font-family: inherit
|
||
}
|
||
|
||
.ai:focus {
|
||
border-color: var(--blue)
|
||
}
|
||
|
||
.asel {
|
||
font-size: 11px;
|
||
padding: 5px 8px;
|
||
background: #fff;
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 6px;
|
||
font-family: inherit;
|
||
color: var(--ink);
|
||
outline: none;
|
||
width: 100%
|
||
}
|
||
|
||
.apy-wrap {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 4px;
|
||
padding: 5px 4px;
|
||
background: var(--paper);
|
||
border: 1px solid var(--bdr);
|
||
border-radius: 6px;
|
||
min-height: 38px;
|
||
align-content: flex-start
|
||
}
|
||
|
||
.apy-wrap label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
font-size: 10px;
|
||
color: var(--ink2);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
user-select: none;
|
||
padding: 3px 7px;
|
||
border-radius: 20px;
|
||
border: 1px solid var(--bdr);
|
||
background: #fff;
|
||
transition: all .15s;
|
||
font-family: inherit
|
||
}
|
||
|
||
.apy-wrap label:has(input:checked) {
|
||
border-color: var(--red);
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
font-weight: 600
|
||
}
|
||
|
||
.apy-wrap input[type=checkbox] {
|
||
display: none
|
||
}
|
||
|
||
.btn-add {
|
||
padding: 8px 16px;
|
||
border-radius: 8px;
|
||
border: none;
|
||
background: linear-gradient(135deg, var(--blue) 0%, var(--teal) 100%);
|
||
color: #fff;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
box-shadow: 0 4px 12px rgba(26, 95, 255, .2);
|
||
transition: all .2s ease;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.btn-add:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 6px 16px rgba(26, 95, 255, .3);
|
||
opacity: 0.95;
|
||
}
|
||
|
||
.btn-usr-prof {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 6px 14px;
|
||
border-radius: 8px;
|
||
background: linear-gradient(135deg, var(--blue) 0%, var(--teal) 100%);
|
||
color: #fff;
|
||
font-size: 11.5px;
|
||
font-weight: 700;
|
||
border: none;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
box-shadow: 0 2px 8px rgba(26, 95, 255, .15);
|
||
transition: all .2s;
|
||
}
|
||
.btn-usr-prof:hover {
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 12px rgba(26, 95, 255, .25);
|
||
opacity: 0.95;
|
||
}
|
||
|
||
.btn-add-a {
|
||
width: 100%;
|
||
padding: 8px;
|
||
border-radius: 8px;
|
||
border: 1.5px dashed var(--bdr);
|
||
background: none;
|
||
color: var(--ink3);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.btn-add-a:hover {
|
||
border-color: var(--blue);
|
||
color: var(--blue);
|
||
background: var(--bdim)
|
||
}
|
||
|
||
.btn-del-a {
|
||
padding: 4px 8px;
|
||
border-radius: 5px;
|
||
border: none;
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
transition: all .15s
|
||
}
|
||
|
||
.btn-del-a:hover {
|
||
background: var(--red);
|
||
color: #fff
|
||
}
|
||
|
||
.bt {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-bottom: 10px
|
||
}
|
||
|
||
.bt th {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
color: var(--ink3);
|
||
text-transform: uppercase;
|
||
letter-spacing: .06em;
|
||
padding: 8px 10px;
|
||
text-align: left;
|
||
background: var(--paper);
|
||
border-bottom: 1px solid var(--bdr)
|
||
}
|
||
|
||
.bt td {
|
||
font-size: 12px;
|
||
color: var(--ink);
|
||
padding: 9px 10px;
|
||
border-bottom: 1px solid rgba(0, 0, 0, .04)
|
||
}
|
||
|
||
.bt tr:hover td {
|
||
background: var(--paper)
|
||
}
|
||
|
||
.btot {
|
||
text-align: right;
|
||
font-size: 12px;
|
||
color: var(--ink3);
|
||
margin-bottom: 12px;
|
||
padding: 0 2px
|
||
}
|
||
|
||
.btot b {
|
||
color: var(--ink)
|
||
}
|
||
|
||
.bform {
|
||
background: var(--paper);
|
||
border-radius: var(--rs);
|
||
padding: 16px;
|
||
border: 1px solid var(--bdr)
|
||
}
|
||
|
||
.bform-t {
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: var(--ink);
|
||
margin-bottom: 12px
|
||
}
|
||
|
||
.jb {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 2px 8px;
|
||
border-radius: 99px
|
||
}
|
||
|
||
.jb.pkh {
|
||
background: #fef3c7;
|
||
color: #b45309
|
||
}
|
||
|
||
.jb.bpnt {
|
||
background: #dbeafe;
|
||
color: #1d4ed8
|
||
}
|
||
|
||
.jb.blt {
|
||
background: #fce7f3;
|
||
color: #9d174d
|
||
}
|
||
|
||
.jb.bansos {
|
||
background: #d1fae5;
|
||
color: #065f46
|
||
}
|
||
|
||
.jb.kip {
|
||
background: #ede9fe;
|
||
color: #6d28d9
|
||
}
|
||
|
||
.jb.kis {
|
||
background: #fee2e2;
|
||
color: #991b1b
|
||
}
|
||
|
||
.jb.lain {
|
||
background: var(--paper);
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.lpd-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid var(--bdr)
|
||
}
|
||
|
||
.lpd-row:last-child {
|
||
border-bottom: none
|
||
}
|
||
|
||
.lpd-k {
|
||
font-size: 12px;
|
||
color: var(--ink3)
|
||
}
|
||
|
||
.lpd-v {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--ink);
|
||
text-align: right;
|
||
max-width: 270px
|
||
}
|
||
|
||
.btn-cancel {
|
||
padding: 10px 20px;
|
||
border-radius: var(--rs);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
border: 1.5px solid var(--bdr);
|
||
background: none;
|
||
cursor: pointer;
|
||
color: var(--ink2);
|
||
font-family: inherit;
|
||
transition: all .15s
|
||
}
|
||
|
||
.btn-cancel:hover {
|
||
background: var(--paper)
|
||
}
|
||
|
||
.btn-sub {
|
||
padding: 10px 24px;
|
||
border-radius: var(--rs);
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: #fff;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.btn-sub.teal {
|
||
background: linear-gradient(90deg, #00c9aa, #009b85);
|
||
box-shadow: 0 4px 16px rgba(0, 201, 170, .3)
|
||
}
|
||
|
||
.btn-sub.blue {
|
||
background: linear-gradient(90deg, #1a5fff, #0040d0);
|
||
box-shadow: 0 4px 16px rgba(26, 95, 255, .3)
|
||
}
|
||
|
||
.btn-sub.green {
|
||
background: linear-gradient(90deg, #10b981, #059669);
|
||
box-shadow: 0 4px 16px rgba(16, 185, 129, .3)
|
||
}
|
||
|
||
.btn-sub:hover {
|
||
transform: translateY(-1px);
|
||
opacity: .92
|
||
}
|
||
|
||
.btn-approve {
|
||
flex: 1;
|
||
padding: 11px;
|
||
border-radius: var(--rs);
|
||
background: var(--green);
|
||
color: #fff;
|
||
border: none;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.btn-approve:hover {
|
||
background: #0ea371
|
||
}
|
||
|
||
.btn-reject {
|
||
flex: 1;
|
||
padding: 11px;
|
||
border-radius: var(--rs);
|
||
background: var(--rdim);
|
||
color: var(--red);
|
||
border: 1px solid transparent;
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all .2s
|
||
}
|
||
|
||
.btn-reject:hover {
|
||
background: var(--red);
|
||
color: #fff
|
||
}
|
||
|
||
.pick-pin {
|
||
background: var(--blue);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
border-radius: 50%;
|
||
width: 26px;
|
||
height: 26px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border: 2px solid #fff;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, .3)
|
||
}
|
||
|
||
#toast {
|
||
position: fixed;
|
||
bottom: 28px;
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(20px);
|
||
background: var(--ink);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 10px 20px;
|
||
border-radius: 999px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
z-index: 9999;
|
||
opacity: 0;
|
||
transition: all .3s cubic-bezier(.34, 1.56, .64, 1);
|
||
pointer-events: none
|
||
}
|
||
|
||
#toast.show {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0)
|
||
}
|
||
|
||
.leaflet-control-zoom {
|
||
border: 1px solid rgba(255, 255, 255, .65) !important;
|
||
border-radius: 11px !important;
|
||
overflow: hidden;
|
||
backdrop-filter: blur(16px);
|
||
box-shadow: var(--sh) !important
|
||
}
|
||
|
||
.leaflet-control-zoom a {
|
||
background: var(--glass) !important;
|
||
color: var(--ink) !important;
|
||
border-bottom: 1px solid var(--bdr) !important;
|
||
font-size: 14px !important;
|
||
line-height: 28px !important
|
||
}
|
||
|
||
.leaflet-control-zoom a:hover {
|
||
background: #fff !important;
|
||
color: var(--blue) !important
|
||
}
|
||
|
||
.leaflet-control-attribution {
|
||
background: rgba(255, 255, 255, .7) !important;
|
||
color: var(--ink3) !important;
|
||
font-size: 10px !important;
|
||
backdrop-filter: blur(8px);
|
||
border-radius: 8px 0 0 0 !important;
|
||
padding: 3px 8px !important
|
||
}
|
||
|
||
.sec-sep {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
color: var(--ink3);
|
||
letter-spacing: .08em;
|
||
text-transform: uppercase;
|
||
margin: 8px 0 12px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 1px solid var(--bdr)
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<!-- LOADING OVERLAY (tampil default, hilang setelah checkSession) -->
|
||
<div id="pageLoading" style="position:fixed;inset:0;z-index:9999;background:linear-gradient(135deg,#0b1120 0%,#1a2540 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:20px;">
|
||
<div style="display:flex;align-items:center;gap:12px">
|
||
<div style="width:40px;height:40px;border-radius:12px;background:linear-gradient(135deg,#1a5fff,#00c9aa);display:flex;align-items:center;justify-content:center;font-size:22px">📍</div>
|
||
<div>
|
||
<div style="font-size:20px;font-weight:700;color:#fff;letter-spacing:-.3px">GeoSosial</div>
|
||
<div style="font-size:11px;color:#8a97ab;margin-top:1px">Kota Pontianak</div>
|
||
</div>
|
||
</div>
|
||
<div style="width:36px;height:36px;border-radius:50%;border:3px solid rgba(255,255,255,.15);border-top-color:#1a5fff;animation:ldSpin .7s linear infinite"></div>
|
||
<style>@keyframes ldSpin{to{transform:rotate(360deg)}}</style>
|
||
</div>
|
||
|
||
<!-- LOGIN PAGE (tidak aktif — redirect ke index.php jika tidak ada sesi) -->
|
||
<div id="pageLogin" class="page">
|
||
<div class="lbg">
|
||
<div class="lblob b1"></div>
|
||
<div class="lblob b2"></div>
|
||
<div class="lblob b3"></div>
|
||
<div class="lgrid"></div>
|
||
</div>
|
||
<div class="lcard">
|
||
<div class="llogo">
|
||
<div class="llogo-ic">📍</div>
|
||
<div>
|
||
<div class="llogo-tx">GeoSosial</div>
|
||
<div class="llogo-sb">SISTEM INFORMASI SOSIAL — KOTA PONTIANAK</div>
|
||
</div>
|
||
</div>
|
||
<div class="lh1">Selamat Datang</div>
|
||
<div class="ldesc">Masuk untuk mengakses sistem informasi geografis kemiskinan dan bantuan sosial Kota Pontianak.
|
||
</div>
|
||
<div class="llbl">Username</div>
|
||
<input class="linp" id="liUser" type="text" placeholder="Masukkan username" autocomplete="username">
|
||
<div class="llbl">Password</div>
|
||
<input class="linp" id="liPass" type="password" placeholder="Masukkan password" autocomplete="current-password">
|
||
<button class="btn-login" onclick="doLogin()">Masuk ke Sistem</button>
|
||
<div class="lerr" id="loginErr">⚠ Username atau password salah.</div>
|
||
<button class="btn-lapor" onclick="showPublicReportPage()">📢 Laporkan Kondisi Kemiskinan (Publik)</button>
|
||
<div class="ldemo"><b>Demo Akun:</b><br>Admin: <b>admin</b>/<b>admin123</b> | Petugas:
|
||
<b>petugas</b>/<b>petugas123</b><br>Walikota: <b>walikota</b>/<b>wali2024</b> | Viewer:
|
||
<b>viewer</b>/<b>viewer123</b></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PUBLIC REPORT PAGE -->
|
||
<div id="pageRep" class="page">
|
||
<div class="rh">
|
||
<button class="btn-back" onclick="showPage('pageLogin')">← Kembali</button>
|
||
<div class="rb-logo">
|
||
<div class="rb-lic">📍</div>
|
||
<div>
|
||
<div class="rb-ltx">GeoSosial</div>
|
||
<div class="rb-lsb">KOTA PONTIANAK</div>
|
||
</div>
|
||
</div>
|
||
<div class="rb-badge">LAPORAN PUBLIK</div>
|
||
</div>
|
||
<div class="rb-body">
|
||
<div class="rb-card">
|
||
<div id="repFormWrap">
|
||
<div class="rb-h1">📢 Laporkan Kondisi Kemiskinan</div>
|
||
<div class="rb-desc">Bantu kami mengidentifikasi warga yang membutuhkan bantuan. Data akan diverifikasi
|
||
petugas Dinas Sosial. Laporan boleh anonim.</div>
|
||
<div class="rs-lbl">Data Pelapor (Opsional)</div>
|
||
<div class="rf-row">
|
||
<div>
|
||
<div class="rf-lbl">Nama Pelapor</div><input class="rf-inp" id="rep_pelapor"
|
||
placeholder="Nama Anda (boleh kosong)">
|
||
</div>
|
||
<div>
|
||
<div class="rf-lbl">No. HP / WhatsApp</div><input class="rf-inp" id="rep_hp" placeholder="0812xxxxx"
|
||
type="tel">
|
||
</div>
|
||
</div>
|
||
<div class="rs-lbl">Data Warga yang Dilaporkan</div>
|
||
<div class="rf-row">
|
||
<div>
|
||
<div class="rf-lbl">Nama Kepala Keluarga *</div><input class="rf-inp" id="rep_nama"
|
||
placeholder="Nama lengkap">
|
||
</div>
|
||
<div>
|
||
<div class="rf-lbl">NIK (jika diketahui)</div><input class="rf-inp" id="rep_nik"
|
||
placeholder="16 digit NIK" maxlength="16">
|
||
</div>
|
||
</div>
|
||
<div class="rf-row">
|
||
<div>
|
||
<div class="rf-lbl">Kelurahan *</div><select class="rf-sel" id="rep_kel"></select>
|
||
</div>
|
||
<div>
|
||
<div class="rf-lbl">Kategori Kondisi *</div>
|
||
<select class="rf-sel" id="rep_kat">
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="rf-row">
|
||
<div>
|
||
<div class="rf-lbl">Jumlah Anggota KK</div><input class="rf-inp" id="rep_jml" placeholder="cth. 4"
|
||
type="number" min="1">
|
||
</div>
|
||
<div>
|
||
<div class="rf-lbl">Perkiraan Penghasilan/Bln</div><input class="rf-inp" id="rep_ph" placeholder="Rp ..."
|
||
type="number" min="0">
|
||
</div>
|
||
</div>
|
||
<div class="rf-lbl">Keterangan Kondisi *</div>
|
||
<textarea class="rf-ta" id="rep_ket"
|
||
placeholder="Ceritakan kondisi warga: rumah, pekerjaan, kesehatan, dll..."></textarea>
|
||
<div class="rf-lbl">Lokasi Rumah (Klik peta)</div>
|
||
<div class="rf-map" id="repMap">
|
||
<div class="rf-maplbl" id="repMapLbl">🖱 Klik untuk memilih lokasi</div>
|
||
</div>
|
||
<div class="rf-coords"><input class="rf-cbox" id="rep_lat" placeholder="Latitude" readonly><input
|
||
class="rf-cbox" id="rep_lng" placeholder="Longitude" readonly></div>
|
||
<button class="btn-srep" onclick="submitPublicReport()">📨 Kirim Laporan</button>
|
||
</div>
|
||
<div class="rep-ok" id="repOk">
|
||
<div class="rep-ok-ic">✅</div>
|
||
<div class="rep-ok-h1">Laporan Berhasil Dikirim!</div>
|
||
<div class="rep-ok-desc">Laporan Anda diterima dan akan diverifikasi petugas Dinas Sosial Kota
|
||
Pontianak.<br>Simpan nomor laporan berikut:</div>
|
||
<div class="rep-ok-no" id="repOkNo">RPT-2026-XXX</div>
|
||
<button class="btn-lapor" style="margin-top:20px;max-width:300px;" onclick="resetRep()">📢 Buat Laporan
|
||
Baru</button>
|
||
<button class="btn-lapor" style="margin-top:8px;max-width:300px;" onclick="showPage('pageLogin')">← Kembali ke
|
||
Halaman Utama</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- APP PAGE -->
|
||
<div id="pageApp" class="page">
|
||
<nav id="topbar">
|
||
<div class="brand">
|
||
<div class="brand-ic">📍</div>
|
||
<div>
|
||
<div class="brand-tx">GeoSosial</div>
|
||
<div class="brand-sb">Kota Pontianak</div>
|
||
</div>
|
||
</div>
|
||
<div class="ndiv"></div>
|
||
<button class="np active" id="nav-map" onclick="switchTab('map')">🗺️ Peta Spasial</button>
|
||
<button class="np" id="nav-dashboard" onclick="switchTab('dashboard')">📊 Dashboard</button>
|
||
<button class="np" id="nav-data" onclick="switchTab('data')">📋 Data Warga</button>
|
||
<button class="np hidden" id="nav-laporan" onclick="switchTab('laporan')">📩 Laporan <span class="nbadge"
|
||
id="lbadge" style="display:none">0</span></button>
|
||
<button class="np hidden" id="nav-pengguna" onclick="switchTab('pengguna')">👤 Pengguna</button>
|
||
<div class="tr">
|
||
<a class="top-user-btn" id="topUserBtn" href="profile.html" title="Lihat Profil Saya">
|
||
<div class="top-user-av" id="topUserAv">A</div>
|
||
<div class="top-user-info">
|
||
<span class="top-user-name" id="topUser"></span>
|
||
<span class="top-user-role" id="topRole">ADMIN</span>
|
||
</div>
|
||
</a>
|
||
<button class="btn-out" onclick="doLogout()">Keluar →</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- MAP TAB -->
|
||
<div id="tabMap" class="tc active">
|
||
<div id="map"></div>
|
||
<div class="panel" id="panelLeft">
|
||
<div>
|
||
<div class="ph">Filter Peta</div>
|
||
<div class="ps">Pemetaan spasial kemiskinan Pontianak</div>
|
||
</div>
|
||
<div>
|
||
<div class="fl">Kategori</div><select class="fs" id="fKat" onchange="applyFilter()">
|
||
<option value="semua">Semua Kategori</option>
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<div class="fl">Kecamatan</div><select class="fs" id="fKec" onchange="applyFilter()">
|
||
<option value="semua">Semua Kecamatan</option>
|
||
<option>Pontianak Kota</option>
|
||
<option>Pontianak Selatan</option>
|
||
<option>Pontianak Barat</option>
|
||
<option>Pontianak Utara</option>
|
||
<option>Pontianak Timur</option>
|
||
<option>Pontianak Tenggara</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<div class="fl">Kelurahan</div><select class="fs" id="fKel" onchange="applyFilter()">
|
||
<option value="semua">Semua Kelurahan</option>
|
||
</select>
|
||
</div>
|
||
<div class="pdiv"></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px" id="adminActions">
|
||
<button class="act-btn btn-ri" onclick="openModalRI()">+ Tambah Rumah Ibadah</button>
|
||
<button class="act-btn btn-km" onclick="openModalKM()">+ Tambah Data Warga</button>
|
||
</div>
|
||
<div class="pdiv"></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<button class="act-btn btn-blank" id="btnBlank" onclick="toggleBlank()">◉ Deteksi Blank Spot</button>
|
||
<button class="act-btn btn-radar" id="btnRadar" onclick="toggleRadar()">⚡ Radar Rumah Ibadah</button>
|
||
<div class="rw" id="radiusWrap">
|
||
<div class="rr"><input type="range" id="radSlider" min="100" max="1500" value="500" step="50"
|
||
oninput="updateRadius(this.value)"><span class="rl" id="radLbl">500m</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel" id="panelStats">
|
||
<div class="si">
|
||
<div class="sic r">👥</div>
|
||
<div>
|
||
<div class="sn" id="sKK">0</div>
|
||
<div class="sl">Total KK Miskin</div>
|
||
</div>
|
||
</div>
|
||
<div class="si">
|
||
<div class="sic g">🕌</div>
|
||
<div>
|
||
<div class="sn" id="sRI">0</div>
|
||
<div class="sl">Rumah Ibadah</div>
|
||
</div>
|
||
</div>
|
||
<div class="si">
|
||
<div class="sic p">◎</div>
|
||
<div>
|
||
<div class="sn" id="sBS">—</div>
|
||
<div class="sl">Blank Spot</div>
|
||
</div>
|
||
</div>
|
||
<div class="si">
|
||
<div class="sic b">📡</div>
|
||
<div>
|
||
<div class="sn" id="sCov">—</div>
|
||
<div class="sl">Cakupan Radar</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- PANEL LAYER TOGGLE (kanan bawah) -->
|
||
<div class="panel" id="panelLayers">
|
||
<div class="opt-section">
|
||
<div class="opt-head" onclick="toggleOptSection('optMain')">
|
||
<span class="opt-title">Tematik Utama</span>
|
||
<span class="opt-arrow" id="arrowOptMain">▾</span>
|
||
</div>
|
||
<div class="opt-body" id="optMain">
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layerWarga" checked onchange="toggleLayerWarga(this.checked)">
|
||
<div class="opt-dot" style="background:#f03e5a"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">Kemiskinan</span>
|
||
<span class="opt-sub">Data KK miskin</span>
|
||
</div>
|
||
</label>
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layerRI" checked onchange="toggleLayerRI(this.checked)">
|
||
<div class="opt-dot" style="background:#10b981"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">Rumah Ibadah</span>
|
||
<span class="opt-sub">Fasilitas ibadah</span>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div class="opt-section">
|
||
<div class="opt-head" onclick="toggleOptSection('optOps')">
|
||
<span class="opt-title">Opsional</span>
|
||
<span class="opt-arrow" id="arrowOptOps">▸</span>
|
||
</div>
|
||
<div class="opt-body" id="optOps" style="display:none">
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layer_kesehatan" onchange="toggleLayerFasilitas('kesehatan', this.checked)">
|
||
<div class="opt-dot" style="background:#f59e0b"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">🏥 Fasilitas Kesehatan</span>
|
||
<span class="opt-sub">Puskesmas & RS</span>
|
||
</div>
|
||
</label>
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layer_pendidikan" onchange="toggleLayerFasilitas('pendidikan', this.checked)">
|
||
<div class="opt-dot" style="background:#1a5fff"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">🏫 Fasilitas Pendidikan</span>
|
||
<span class="opt-sub">Sekolah & Kampus</span>
|
||
</div>
|
||
</label>
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layer_pasar" onchange="toggleLayerFasilitas('pasar', this.checked)">
|
||
<div class="opt-dot" style="background:#7c3aed"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">🛒 Pasar Tradisional</span>
|
||
<span class="opt-sub">Pasar Rakyat</span>
|
||
</div>
|
||
</label>
|
||
<label class="opt-item">
|
||
<input type="checkbox" id="layer_kelurahan" onchange="toggleLayerFasilitas('kelurahan', this.checked)">
|
||
<div class="opt-dot" style="background:#00c9aa"></div>
|
||
<div class="opt-info">
|
||
<span class="opt-name">🏛️ Kantor Kelurahan</span>
|
||
<span class="opt-sub">Kantor Lurah</span>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="panel" id="panelLeg">
|
||
<div class="lt">Legenda</div>
|
||
<div class="li">
|
||
<div class="ld" style="background:#f03e5a"></div>Sangat Miskin
|
||
</div>
|
||
<div class="li">
|
||
<div class="ld" style="background:#f97316"></div>Miskin
|
||
</div>
|
||
<div class="li">
|
||
<div class="ld" style="background:#1a5fff"></div>Hampir Miskin
|
||
</div>
|
||
<div class="li">
|
||
<div class="ld" style="background:#10b981;border:2px solid #059669"></div>Rumah Ibadah
|
||
</div>
|
||
<div class="li">
|
||
<div class="ld lp" style="background:#7c3aed"></div>Blank Spot
|
||
</div>
|
||
</div>
|
||
<div class="panel" id="panelList">
|
||
<div class="ltt">Rumah Ibadah — Kapasitas Terpakai</div>
|
||
<div class="rsc" id="riList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DASHBOARD TAB -->
|
||
<div id="tabDashboard" class="tc">
|
||
<div class="dash-h">
|
||
<div class="dash-t">📊 Dashboard Eksekutif</div>
|
||
<div class="dash-s">Ringkasan Data Kemiskinan & Bantuan Sosial — Kota Pontianak</div>
|
||
<div style="font-size:12px;color:var(--ink3);margin-top:2px" id="dashDate"></div>
|
||
</div>
|
||
<div class="kpi-grid">
|
||
<div class="kpi cr">
|
||
<div class="kpi-ic">👥</div>
|
||
<div class="kpi-v" id="kpiKK">0</div>
|
||
<div class="kpi-l">Total KK Terdaftar</div>
|
||
<div class="kpi-d" id="kpiLap"></div>
|
||
</div>
|
||
<div class="kpi cb">
|
||
<div class="kpi-ic">🏠</div>
|
||
<div class="kpi-v" id="kpiSM">0</div>
|
||
<div class="kpi-l">Keluarga Sangat Miskin</div>
|
||
</div>
|
||
<div class="kpi ct">
|
||
<div class="kpi-ic">💰</div>
|
||
<div class="kpi-v" id="kpiBn">0</div>
|
||
<div class="kpi-l">Total Bantuan Disalurkan</div>
|
||
<div class="kpi-d" id="kpiBnNom"></div>
|
||
</div>
|
||
<div class="kpi cp">
|
||
<div class="kpi-ic">🏥</div>
|
||
<div class="kpi-v" id="kpiPy">—</div>
|
||
<div class="kpi-l">Penyakit Terbanyak</div>
|
||
</div>
|
||
</div>
|
||
<div class="charts-grid">
|
||
<div class="ccard">
|
||
<div class="ct">KK Miskin per Kelurahan</div>
|
||
<div class="cs">10 kelurahan tertinggi</div>
|
||
<div style="height:260px;position:relative"><canvas id="chartKel"></canvas></div>
|
||
</div>
|
||
<div class="ccard">
|
||
<div class="ct">Distribusi Kategori</div>
|
||
<div class="cs">Proporsi tingkat kemiskinan</div>
|
||
<div style="height:260px;position:relative"><canvas id="chartKat"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div class="charts-grid2">
|
||
<div class="ccard">
|
||
<div class="ct">Program Bantuan Disalurkan</div>
|
||
<div class="cs">Jumlah penyaluran per program</div>
|
||
<div style="height:200px;position:relative"><canvas id="chartBn"></canvas></div>
|
||
</div>
|
||
<div class="ccard">
|
||
<div class="ct">Prevalensi Penyakit</div>
|
||
<div class="cs">Distribusi riwayat penyakit seluruh anggota</div>
|
||
<div style="height:200px;position:relative"><canvas id="chartPy"></canvas></div>
|
||
</div>
|
||
</div>
|
||
<div class="tw">
|
||
<div style="padding:20px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||
<div class="ct">🏆 Ranking Kelurahan — Kemiskinan Tertinggi</div>
|
||
<div id="rankInfo" style="font-size:12px;color:var(--ink3)"></div>
|
||
</div>
|
||
<div class="cs" style="margin-bottom:16px">Data terkini seluruh kelurahan di Kota Pontianak</div>
|
||
<table class="mt">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Kelurahan</th>
|
||
<th>Kecamatan</th>
|
||
<th>Sangat Miskin</th>
|
||
<th>Miskin</th>
|
||
<th>Hampir Miskin</th>
|
||
<th>Total</th>
|
||
<th>Total Bantuan</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dashTb"></tbody>
|
||
</table>
|
||
<div id="rankPgn" style="display:flex;align-items:center;justify-content:center;gap:6px;margin-top:16px;flex-wrap:wrap"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DATA TAB -->
|
||
<div id="tabData" class="tc">
|
||
<div class="dh">
|
||
<div>
|
||
<div class="dt">📋 Manajemen Data Warga</div>
|
||
</div><button class="act-btn btn-km" style="width:auto;padding:10px 18px" id="addWBtn" onclick="openModalKM()">+
|
||
Tambah Data Warga</button>
|
||
</div>
|
||
<div class="dtbar">
|
||
<input class="dts" id="dtSearch" type="search" placeholder="🔍 Cari nama, NIK, kelurahan..."
|
||
oninput="renderDT()">
|
||
<select class="dtsel" id="dtKat" onchange="renderDT()">
|
||
<option value="">Semua Kategori</option>
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
<select class="dtsel" id="dtKel" onchange="renderDT()">
|
||
<option value="">Semua Kelurahan</option>
|
||
</select>
|
||
<select class="dtsel" id="dtPy" onchange="renderDT()">
|
||
<option value="">Semua Kondisi</option>
|
||
<option value="hipertensi">Hipertensi</option>
|
||
<option value="diabetes">Diabetes</option>
|
||
<option value="tbc">TBC</option>
|
||
<option value="stunting">Stunting</option>
|
||
<option value="odgj">ODGJ</option>
|
||
</select>
|
||
<div class="dtc" id="dtCount">0 data</div>
|
||
<button class="dtexp" onclick="exportCSV()">⬇ Export CSV</button>
|
||
</div>
|
||
<div class="tw">
|
||
<table class="mt">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Nama KK</th>
|
||
<th>Kelurahan</th>
|
||
<th>Kategori</th>
|
||
<th>Tgl Lahir</th>
|
||
<th>Pendidikan</th>
|
||
<th>Penghasilan</th>
|
||
<th>Penyakit</th>
|
||
<th>Bantuan</th>
|
||
<th>Aksi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="dtTb"></tbody>
|
||
</table>
|
||
<div class="pgn" id="pgn"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- LAPORAN TAB -->
|
||
<div id="tabLaporan" class="tc">
|
||
<div class="dh">
|
||
<div>
|
||
<div class="dt">📩 Laporan Masuk dari Masyarakat</div>
|
||
</div>
|
||
<button class="act-btn btn-km" id="addLapBtn" style="width:auto;padding:10px 18px" onclick="openModalLaporanAdmin()">+ Buat Laporan</button>
|
||
</div>
|
||
<div class="ls-row">
|
||
<div class="ls">
|
||
<div class="ls-ic">⏳</div>
|
||
<div>
|
||
<div class="ls-v" id="lsM">0</div>
|
||
<div class="ls-l">Menunggu Verifikasi</div>
|
||
</div>
|
||
</div>
|
||
<div class="ls">
|
||
<div class="ls-ic">✅</div>
|
||
<div>
|
||
<div class="ls-v" id="lsD">0</div>
|
||
<div class="ls-l">Disetujui</div>
|
||
</div>
|
||
</div>
|
||
<div class="ls">
|
||
<div class="ls-ic">❌</div>
|
||
<div>
|
||
<div class="ls-v" id="lsT">0</div>
|
||
<div class="ls-l">Ditolak</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="tw">
|
||
<table class="mt">
|
||
<thead>
|
||
<tr>
|
||
<th>No. Laporan</th>
|
||
<th>Tanggal</th>
|
||
<th>Pelapor</th>
|
||
<th>Nama Dilaporkan</th>
|
||
<th>Kelurahan</th>
|
||
<th>Kategori</th>
|
||
<th>Status</th>
|
||
<th>Aksi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="lapTb"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PENGGUNA TAB -->
|
||
<div id="tabPengguna" class="tc">
|
||
<div class="dh">
|
||
<div class="dt">👤 Manajemen Pengguna</div>
|
||
<button class="btn-add" id="addUsrBtn" onclick="openAddUser()" style="display:none">+ Tambah Pengguna</button>
|
||
</div>
|
||
<div class="tw">
|
||
<table class="mt">
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>Username</th>
|
||
<th>Jabatan</th>
|
||
<th>Role</th>
|
||
<th>Hak Akses</th>
|
||
<th>Status</th>
|
||
<th>Aksi</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="usrTb"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="toast">✓ <span id="toastMsg"></span></div>
|
||
</div>
|
||
|
||
<!-- MODAL PROFIL PENGGUNA -->
|
||
<div class="mo" id="modalProfile">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t" id="profModalTitle">👤 Profil Pengguna</div>
|
||
<div class="mh-s" id="profModalSub">Detail akun</div>
|
||
</div><button class="mc" onclick="closeModal('modalProfile')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<input type="hidden" id="prof_id">
|
||
<!-- Avatar -->
|
||
<div style="display:flex;align-items:center;gap:16px;padding:12px 0 18px">
|
||
<div id="profAvatar" style="width:56px;height:56px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:22px;font-weight:800;color:#fff;background:var(--blue);flex-shrink:0">A</div>
|
||
<div>
|
||
<div id="profUsername" style="font-size:18px;font-weight:800;font-family:monospace;color:var(--ink)">—</div>
|
||
<div id="profCreated" style="font-size:11px;color:var(--ink3);margin-top:3px"></div>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Jabatan</div>
|
||
<input class="fi" id="prof_jabatan" placeholder="cth. Kadis Sosial">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Role</div>
|
||
<select class="fsl" id="prof_role">
|
||
<option value="admin">Admin</option>
|
||
<option value="petugas">Petugas</option>
|
||
<option value="walikota">Walikota</option>
|
||
<option value="viewer">Viewer</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Hak Akses</div>
|
||
<select class="fsl" id="prof_can_edit">
|
||
<option value="1">✏️ Baca & Tulis</option>
|
||
<option value="0">👁️ Baca Saja</option>
|
||
</select>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Status</div>
|
||
<select class="fsl" id="prof_aktif">
|
||
<option value="1">● Aktif</option>
|
||
<option value="0">○ Nonaktif</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fg" id="prof_pw_wrap">
|
||
<div class="flb">Ganti Password <span style="font-size:10px;color:var(--ink3)">(kosongkan jika tidak diganti)</span></div>
|
||
<input class="fi" id="prof_password" type="password" placeholder="Min. 6 karakter" autocomplete="new-password">
|
||
</div>
|
||
</div>
|
||
<div class="mf" id="profFooter">
|
||
<button class="btn-cancel" onclick="closeModal('modalProfile')">Tutup</button>
|
||
<button class="btn-sub blue" id="profSaveBtn" onclick="saveUserProfile()">Simpan Perubahan</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL TAMBAH PENGGUNA -->
|
||
<div class="mo" id="modalAddUser">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">➕ Tambah Pengguna Baru</div>
|
||
<div class="mh-s">Isi data akun pengguna baru</div>
|
||
</div><button class="mc" onclick="closeModal('modalAddUser')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Username *</div>
|
||
<input class="fi" id="nu_username" placeholder="cth. budi_dinsos">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Password *</div>
|
||
<input class="fi" id="nu_password" type="password" placeholder="Min. 6 karakter">
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Jabatan</div>
|
||
<input class="fi" id="nu_jabatan" placeholder="cth. Staf Verifikasi">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Role</div>
|
||
<select class="fsl" id="nu_role">
|
||
<option value="admin">Admin</option>
|
||
<option value="petugas">Petugas</option>
|
||
<option value="walikota">Walikota</option>
|
||
<option value="viewer">Viewer</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mf">
|
||
<button class="btn-cancel" onclick="closeModal('modalAddUser')">Batal</button>
|
||
<button class="btn-sub blue" onclick="submitAddUser()">Tambahkan</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL RUMAH IBADAH -->
|
||
<div class="mo" id="modalRI">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t" id="riModalTitle">🕌 Tambah Rumah Ibadah</div>
|
||
<div class="mh-s" id="riModalSub">Klik mini-peta untuk menentukan lokasi</div>
|
||
</div><button class="mc" onclick="closeModal('modalRI')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<input type="hidden" id="ri_edit_id" value="">
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Nama *</div><input class="fi" id="ri_nama" placeholder="cth. Masjid Al-Falah">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Tipe *</div><select class="fsl" id="ri_tipe">
|
||
<option value="masjid">🕌 Masjid</option>
|
||
<option value="gereja">⛪ Gereja</option>
|
||
<option value="pura">🛕 Pura</option>
|
||
<option value="vihara">🏛️ Vihara</option>
|
||
<option value="klenteng">🏯 Klenteng</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Kapasitas</div><input class="fi" id="ri_kap" placeholder="cth. 200" type="number" min="1">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Kelurahan</div><select class="fsl" id="ri_kel"></select>
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Lokasi — Klik peta *</div>
|
||
<div class="fmpp" id="riMapPick">
|
||
<div class="fmpl" id="riMapLbl">🖱 Klik untuk memilih lokasi</div>
|
||
</div>
|
||
<div class="fmpc"><input class="fmcb" id="ri_lat" placeholder="Latitude" readonly><input class="fmcb"
|
||
id="ri_lng" placeholder="Longitude" readonly></div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Alamat</div><input class="fi" id="ri_alamat" placeholder="Jl. ..., Pontianak">
|
||
</div>
|
||
</div>
|
||
<div class="mf"><button class="btn-cancel" onclick="closeModal('modalRI')">Batal</button><button
|
||
class="btn-sub teal" id="riSubmitBtn" onclick="submitRI()">Simpan</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL DATA WARGA -->
|
||
<div class="mo" id="modalKM">
|
||
<div class="modal lg">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">👤 Tambah Data Warga</div>
|
||
<div class="mh-s">Lengkapi data kepala keluarga dan anggota</div>
|
||
</div><button class="mc" onclick="closeModal('modalKM')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div class="sec-sep">Data Kepala Keluarga</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Nama KK *</div><input class="fi" id="km_nama" placeholder="Nama lengkap">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">NIK</div><input class="fi" id="km_nik" placeholder="16 digit NIK" maxlength="16">
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Tanggal Lahir</div><input class="fi" id="km_tgl" type="date">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Pendidikan Terakhir</div><select class="fsl" id="km_pend">
|
||
<option value="">— Pilih —</option>
|
||
<option value="tidak_sekolah">Tidak Sekolah</option>
|
||
<option value="sd">SD</option>
|
||
<option value="smp">SMP</option>
|
||
<option value="sma">SMA/SMK</option>
|
||
<option value="d3">D3/Diploma</option>
|
||
<option value="s1">S1/Sarjana</option>
|
||
<option value="s2">S2/Pasca</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Kategori *</div><select class="fsl" id="km_kat">
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Kelurahan *</div><select class="fsl" id="km_kel"></select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Jumlah Anggota KK</div><input class="fi" id="km_jml" placeholder="cth. 4" type="number"
|
||
min="1" max="20">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Penghasilan/Bulan (Rp)</div><input class="fi" id="km_ph" placeholder="cth. 500000"
|
||
type="number" min="0">
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Riwayat Penyakit KK</div>
|
||
<div class="penyakit-grid">
|
||
<label class="pcb"><input type="checkbox" value="hipertensi" class="pykk"> 🩸 Hipertensi</label>
|
||
<label class="pcb"><input type="checkbox" value="diabetes" class="pykk"> 🍬 Diabetes</label>
|
||
<label class="pcb"><input type="checkbox" value="tbc" class="pykk"> 🫁 TBC</label>
|
||
<label class="pcb"><input type="checkbox" value="asma" class="pykk"> 💨 Asma</label>
|
||
<label class="pcb"><input type="checkbox" value="stroke" class="pykk"> 🧠 Stroke</label>
|
||
<label class="pcb"><input type="checkbox" value="jantung" class="pykk"> ❤️ Jantung</label>
|
||
<label class="pcb"><input type="checkbox" value="odgj" class="pykk"> 🧩 ODGJ</label>
|
||
<label class="pcb"><input type="checkbox" value="stunting" class="pykk"> 📏 Stunting</label>
|
||
<label class="pcb"><input type="checkbox" value="kanker" class="pykk"> 🔬 Kanker</label>
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Lokasi Rumah — Klik peta *</div>
|
||
<div class="fmpp" id="kmMapPick">
|
||
<div class="fmpl" id="kmMapLbl">🖱 Klik untuk memilih lokasi</div>
|
||
</div>
|
||
<div class="fmpc"><input class="fmcb" id="km_lat" placeholder="Latitude" readonly><input class="fmcb"
|
||
id="km_lng" placeholder="Longitude" readonly></div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Keterangan</div><textarea class="fta" id="km_ket"
|
||
placeholder="Kondisi rumah, pekerjaan, dll..."></textarea>
|
||
</div>
|
||
<div class="sec-sep">Anggota Keluarga & Riwayat Penyakit</div>
|
||
<table class="at" id="anggotaTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:32%">Nama</th>
|
||
<th style="width:12%">Usia</th>
|
||
<th style="width:20%">Hubungan</th>
|
||
<th style="width:29%">Penyakit</th>
|
||
<th style="width:7%"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="anggotaBody"></tbody>
|
||
</table>
|
||
<button class="btn-add-a" onclick="addAnggotaRow()">+ Tambah Anggota</button>
|
||
</div>
|
||
<div class="mf"><button class="btn-cancel" onclick="closeModal('modalKM')">Batal</button><button
|
||
class="btn-sub blue" onclick="submitKM()">Simpan Data</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL EDIT WARGA -->
|
||
<div class="mo" id="modalEditWarga">
|
||
<div class="modal lg">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">✏️ Edit Data Warga</div>
|
||
<div class="mh-s" id="ewSubtitle">Perbarui data kepala keluarga dan anggota</div>
|
||
</div><button class="mc" onclick="closeModal('modalEditWarga')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div class="sec-sep">Data Kepala Keluarga</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Nama KK *</div><input class="fi" id="ew_nama" placeholder="Nama lengkap">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">NIK</div><input class="fi" id="ew_nik" placeholder="16 digit NIK" maxlength="16">
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Tanggal Lahir</div><input class="fi" id="ew_tgl" type="date">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Pendidikan Terakhir</div><select class="fsl" id="ew_pend">
|
||
<option value="">— Pilih —</option>
|
||
<option value="tidak_sekolah">Tidak Sekolah</option>
|
||
<option value="sd">SD</option>
|
||
<option value="smp">SMP</option>
|
||
<option value="sma">SMA/SMK</option>
|
||
<option value="d3">D3/Diploma</option>
|
||
<option value="s1">S1/Sarjana</option>
|
||
<option value="s2">S2/Pasca</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Kategori *</div><select class="fsl" id="ew_kat">
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Kelurahan *</div><select class="fsl" id="ew_kel"></select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Jumlah Anggota KK</div><input class="fi" id="ew_jml" placeholder="cth. 4" type="number" min="1" max="20">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Penghasilan/Bulan (Rp)</div><input class="fi" id="ew_ph" placeholder="cth. 500000" type="number" min="0">
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Riwayat Penyakit KK</div>
|
||
<div class="penyakit-grid">
|
||
<label class="pcb"><input type="checkbox" value="hipertensi" class="ewpykk"> 🩸 Hipertensi</label>
|
||
<label class="pcb"><input type="checkbox" value="diabetes" class="ewpykk"> 🍬 Diabetes</label>
|
||
<label class="pcb"><input type="checkbox" value="tbc" class="ewpykk"> 🫁 TBC</label>
|
||
<label class="pcb"><input type="checkbox" value="asma" class="ewpykk"> 💨 Asma</label>
|
||
<label class="pcb"><input type="checkbox" value="stroke" class="ewpykk"> 🧠 Stroke</label>
|
||
<label class="pcb"><input type="checkbox" value="jantung" class="ewpykk"> ❤️ Jantung</label>
|
||
<label class="pcb"><input type="checkbox" value="odgj" class="ewpykk"> 🧩 ODGJ</label>
|
||
<label class="pcb"><input type="checkbox" value="stunting" class="ewpykk"> 📏 Stunting</label>
|
||
<label class="pcb"><input type="checkbox" value="kanker" class="ewpykk"> 🔬 Kanker</label>
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Lokasi Rumah — Klik peta untuk memperbarui</div>
|
||
<div class="fmpp" id="ewMapPick">
|
||
<div class="fmpl" id="ewMapLbl" style="display:none">🖱 Klik untuk memperbarui lokasi</div>
|
||
</div>
|
||
<div class="fmpc"><input class="fmcb" id="ew_lat" placeholder="Latitude" readonly><input class="fmcb" id="ew_lng" placeholder="Longitude" readonly></div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Keterangan</div><textarea class="fta" id="ew_ket" placeholder="Kondisi rumah, pekerjaan, dll..."></textarea>
|
||
</div>
|
||
<div class="sec-sep">Anggota Keluarga & Riwayat Penyakit</div>
|
||
<table class="at" id="ewAnggotaTable">
|
||
<thead>
|
||
<tr>
|
||
<th style="width:32%">Nama</th>
|
||
<th style="width:12%">Usia</th>
|
||
<th style="width:20%">Hubungan</th>
|
||
<th style="width:29%">Penyakit</th>
|
||
<th style="width:7%"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="ewAnggotaBody"></tbody>
|
||
</table>
|
||
<button class="btn-add-a" onclick="addEwAnggotaRow()">+ Tambah Anggota</button>
|
||
</div>
|
||
<div class="mf">
|
||
<button class="btn-cancel" onclick="closeModal('modalEditWarga')">Batal</button>
|
||
<button class="btn-sub blue" onclick="submitEditWarga()">💾 Simpan Perubahan</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL HISTORI BANTUAN -->
|
||
<div class="mo" id="modalBantuan">
|
||
<div class="modal lg">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">💰 Histori Bantuan Sosial</div>
|
||
<div class="mh-s" id="bnWargaNama">—</div>
|
||
</div><button class="mc" onclick="closeModal('modalBantuan')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<table class="bt">
|
||
<thead>
|
||
<tr>
|
||
<th>Tanggal</th>
|
||
<th>Program</th>
|
||
<th>Nominal</th>
|
||
<th>Sumber</th>
|
||
<th>Keterangan</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="bnTb"></tbody>
|
||
</table>
|
||
<div class="btot" id="bnTot"></div>
|
||
<div id="bnEmpty" style="display:none;text-align:center;padding:20px;color:var(--ink3);font-size:13px">💸 Belum
|
||
ada riwayat bantuan</div>
|
||
<div class="bform" id="bnForm">
|
||
<div class="bform-t">+ Tambah Penyaluran Bantuan</div>
|
||
<div class="fr">
|
||
<div class="fg" style="margin-bottom:0">
|
||
<div class="flb">Tanggal *</div><input class="fi" id="bh_tgl" type="date">
|
||
</div>
|
||
<div class="fg" style="margin-bottom:0">
|
||
<div class="flb">Program Bantuan *</div><select class="fsl" id="bh_jenis">
|
||
<option value="PKH">PKH (Program Keluarga Harapan)</option>
|
||
<option value="BPNT">BPNT (Bantuan Pangan Non Tunai)</option>
|
||
<option value="BLT">BLT (Bantuan Langsung Tunai)</option>
|
||
<option value="Bansos Pemkot">Bansos Pemkot Pontianak</option>
|
||
<option value="KIP">KIP (Kartu Indonesia Pintar)</option>
|
||
<option value="KIS">KIS (Kartu Indonesia Sehat)</option>
|
||
<option value="Sembako">Bantuan Sembako</option>
|
||
<option value="Lainnya">Lainnya</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr" style="margin-top:10px">
|
||
<div class="fg" style="margin-bottom:0">
|
||
<div class="flb">Nominal (Rp)</div><input class="fi" id="bh_nom" type="number" placeholder="cth. 750000"
|
||
min="0">
|
||
</div>
|
||
<div class="fg" style="margin-bottom:0">
|
||
<div class="flb">Sumber Dana</div><select class="fsl" id="bh_src">
|
||
<option value="Kemensos RI">Kemensos RI</option>
|
||
<option value="APBD Kota Pontianak">APBD Kota Pontianak</option>
|
||
<option value="APBN">APBN</option>
|
||
<option value="Dana Desa">Dana Desa</option>
|
||
<option value="CSR / Donasi">CSR / Donasi</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fg" style="margin-top:10px;margin-bottom:0">
|
||
<div class="flb">Keterangan</div><input class="fi" id="bh_ket" placeholder="cth. Triwulan I 2026...">
|
||
</div>
|
||
<button class="btn-sub blue" style="margin-top:12px;padding:10px 20px" onclick="submitBantuan()">Simpan
|
||
Bantuan</button>
|
||
</div>
|
||
</div>
|
||
<div class="mf"><button class="btn-cancel" onclick="closeModal('modalBantuan')">Tutup</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL ANGGOTA -->
|
||
<div class="mo" id="modalAnggota">
|
||
<div class="modal">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">👨👩👧 Data Anggota Keluarga</div>
|
||
<div class="mh-s" id="agWargaNama">—</div>
|
||
</div><button class="mc" onclick="closeModal('modalAnggota')">✕</button>
|
||
</div>
|
||
<div class="mb" id="agBody"></div>
|
||
<div class="mf"><button class="btn-cancel" onclick="closeModal('modalAnggota')">Tutup</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL LAPORAN ADMIN -->
|
||
<div class="mo" id="modalLaporanAdmin">
|
||
<div class="modal lg">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">📢 Buat Laporan Baru</div>
|
||
<div class="mh-s">Buat laporan kondisi kemiskinan warga</div>
|
||
</div><button class="mc" onclick="closeModal('modalLaporanAdmin')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div class="sec-sep">Data Pelapor (Opsional)</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Nama Pelapor</div><input class="fi" id="lap_adm_pelapor" placeholder="Nama pelapor (boleh kosong)">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">No. HP / WhatsApp</div><input class="fi" id="lap_adm_hp" placeholder="0812xxxxx" type="tel">
|
||
</div>
|
||
</div>
|
||
<div class="sec-sep">Data Warga yang Dilaporkan</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Nama Kepala Keluarga *</div><input class="fi" id="lap_adm_nama" placeholder="Nama lengkap">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">NIK (jika diketahui)</div><input class="fi" id="lap_adm_nik" placeholder="16 digit NIK" maxlength="16">
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Kelurahan *</div><select class="fsl" id="lap_adm_kel"></select>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Kategori Kondisi *</div>
|
||
<select class="fsl" id="lap_adm_kat">
|
||
<option value="sangat_miskin">Sangat Miskin</option>
|
||
<option value="miskin">Miskin</option>
|
||
<option value="hampir_miskin">Hampir Miskin</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="fr">
|
||
<div class="fg">
|
||
<div class="flb">Jumlah Anggota KK</div><input class="fi" id="lap_adm_jml" placeholder="cth. 4" type="number" min="1">
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Perkiraan Penghasilan/Bln</div><input class="fi" id="lap_adm_ph" placeholder="Rp ..." type="number" min="0">
|
||
</div>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Keterangan Kondisi *</div>
|
||
<textarea class="fta" id="lap_adm_ket" placeholder="Ceritakan kondisi warga: rumah, pekerjaan, kesehatan, dll..."></textarea>
|
||
</div>
|
||
<div class="fg">
|
||
<div class="flb">Lokasi Rumah — Klik peta</div>
|
||
<div class="fmpp" id="lapAdmMapPick">
|
||
<div class="fmpl" id="lapAdmMapLbl">🖱 Klik untuk memilih lokasi</div>
|
||
</div>
|
||
<div class="fmpc"><input class="fmcb" id="lap_adm_lat" placeholder="Latitude" readonly><input class="fmcb" id="lap_adm_lng" placeholder="Longitude" readonly></div>
|
||
</div>
|
||
</div>
|
||
<div class="mf"><button class="btn-cancel" onclick="closeModal('modalLaporanAdmin')">Batal</button><button class="btn-sub teal" onclick="submitLaporanAdmin()">Kirim Laporan</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MODAL LAPORAN DETAIL -->
|
||
<div class="mo" id="modalLaporan">
|
||
<div class="modal lg">
|
||
<div class="mh">
|
||
<div>
|
||
<div class="mh-t">📩 Detail Laporan Masyarakat</div>
|
||
<div class="mh-s" id="lapNo">—</div>
|
||
</div><button class="mc" onclick="closeModal('modalLaporan')">✕</button>
|
||
</div>
|
||
<div class="mb">
|
||
<div id="lapDetailBody"></div>
|
||
<div id="lapMap"
|
||
style="width:100%;height:150px;border-radius:10px;overflow:hidden;border:1px solid var(--bdr);margin:12px 0;display:none">
|
||
</div>
|
||
</div>
|
||
<div class="mf" id="lapFoot"><button class="btn-cancel" onclick="closeModal('modalLaporan')">Tutup</button></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ══ STATE & CONSTANTS ══ */
|
||
let currentUser = null;
|
||
let kelurahanList = [];
|
||
let KEC = {};
|
||
let KEL = [];
|
||
|
||
const PL = { hipertensi: 'Hipertensi', diabetes: 'Diabetes', tbc: 'TBC', asma: 'Asma', stroke: 'Stroke', jantung: 'Jantung', odgj: 'ODGJ', stunting: 'Stunting', kanker: 'Kanker' };
|
||
const PLIK = { hipertensi: '🩸', diabetes: '🍬', tbc: '🫁', asma: '💨', stroke: '🧠', jantung: '❤️', odgj: '🧩', stunting: '📏', kanker: '🔬' };
|
||
const EDL = { '': '—', tidak_sekolah: 'Tdk Sekolah', sd: 'SD', smp: 'SMP', sma: 'SMA/SMK', d3: 'D3', s1: 'S1', s2: 'S2' };
|
||
const KL = { sangat_miskin: { label: 'Sangat Miskin', cls: 'ksm', col: '#f03e5a' }, miskin: { label: 'Miskin', cls: 'km', col: '#f97316' }, hampir_miskin: { label: 'Hampir Miskin', cls: 'khm', col: '#1a5fff' } };
|
||
const JL = { PKH: 'pkh', BPNT: 'bpnt', BLT: 'blt', 'Bansos Pemkot': 'bansos', KIP: 'kip', KIS: 'kis' };
|
||
|
||
let rumahIbadah = [];
|
||
let warga = [];
|
||
let laporan = [];
|
||
|
||
let curBantuanId = null, curLapId = null;
|
||
|
||
/* ══ PERFORMANCE CACHE ══ */
|
||
let _nearestCache = new Map(); // warga.id → { ri, jarak }
|
||
let _pairDist = new Map(); // ri.id → Map<wargaId, jarak>
|
||
let dataDirty = true; // flag: data perlu di-reload?
|
||
let sharedRenderer = null; // Leaflet shared canvas renderer
|
||
|
||
/* ══ MAP CONFIG ══ */
|
||
const CENTER = [-0.023, 109.342];
|
||
let map = null, mapReady = false;
|
||
let radius = 500, modeBlank = false, modeRadar = false;
|
||
let wMarkers = [], riMarkers = [], radarL = [], blankL = [];
|
||
let fasMarkers = { kesehatan: [], pendidikan: [], pasar: [], kelurahan: [] };
|
||
let fasData = { kesehatan: null, pendidikan: null, pasar: null, kelurahan: null };
|
||
let charts = {};
|
||
let riPickMap = null, riPickMk = null, kmPickMap = null, kmPickMk = null, repPickMap2 = null, repPickMk2 = null, lapAdmPickMap = null, lapAdmPickMk = null, ewPickMap = null, ewPickMk = null;
|
||
|
||
/* ══ AUTHENTICATION ══ */
|
||
async function doLogin() {
|
||
const u = document.getElementById('liUser').value.trim();
|
||
const p = document.getElementById('liPass').value;
|
||
const err = document.getElementById('loginErr');
|
||
if (!u || !p) { err.textContent = '⚠ Username dan password wajib diisi.'; err.classList.add('show'); return; }
|
||
try {
|
||
const res = await fetch('api/auth.php?action=login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username: u, password: p })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
currentUser = json.data;
|
||
err.classList.remove('show');
|
||
showPage('pageApp');
|
||
setTopbarUser(currentUser);
|
||
setupNav();
|
||
initApp();
|
||
} else {
|
||
err.textContent = '⚠ ' + json.message;
|
||
err.classList.add('show');
|
||
document.getElementById('liPass').value = '';
|
||
document.getElementById('liPass').focus();
|
||
}
|
||
} catch (e) {
|
||
err.textContent = '⚠ Koneksi ke server gagal.';
|
||
err.classList.add('show');
|
||
}
|
||
}
|
||
|
||
async function doLogout() {
|
||
try {
|
||
await fetch('api/auth.php?action=logout', { method: 'POST' });
|
||
} catch (e) { }
|
||
currentUser = null;
|
||
location.href = 'index.php';
|
||
}
|
||
|
||
async function checkSession() {
|
||
try {
|
||
const res = await fetch('api/auth.php?action=check');
|
||
const json = await res.json();
|
||
// Sembunyikan loading overlay
|
||
const ld = document.getElementById('pageLoading');
|
||
if (ld) ld.style.display = 'none';
|
||
if (json.success) {
|
||
currentUser = json.data;
|
||
showPage('pageApp');
|
||
setTopbarUser(currentUser);
|
||
setupNav();
|
||
initApp();
|
||
} else {
|
||
location.href = 'index.php';
|
||
}
|
||
} catch (e) {
|
||
location.href = 'index.php';
|
||
}
|
||
}
|
||
|
||
function setTopbarUser(u) {
|
||
const ROLE_LABELS = { admin: 'Super Admin', petugas: 'Petugas', walikota: 'Walikota', viewer: 'Viewer' };
|
||
const AV_COLORS = ['#10b981','#1a5fff','#f97316','#7c3aed','#f03e5a','#00c9aa'];
|
||
const initials = (u.username || 'U').substring(0, 2).toUpperCase();
|
||
const color = AV_COLORS[(u.id || 0) % AV_COLORS.length];
|
||
const av = document.getElementById('topUserAv');
|
||
if (av) { av.textContent = initials; av.style.background = color; }
|
||
const tu = document.getElementById('topUser');
|
||
if (tu) tu.textContent = u.jabatan || u.username;
|
||
const tr = document.getElementById('topRole');
|
||
if (tr) tr.textContent = ROLE_LABELS[u.role] || u.role.toUpperCase();
|
||
}
|
||
|
||
function setupNav() {
|
||
const lapNav = document.getElementById('nav-laporan');
|
||
const pgnNav = document.getElementById('nav-pengguna');
|
||
if (currentUser.is_admin) {
|
||
lapNav?.classList.remove('hidden');
|
||
pgnNav?.classList.remove('hidden');
|
||
} else if (currentUser.role === 'petugas' || currentUser.role === 'admin') {
|
||
lapNav?.classList.remove('hidden');
|
||
}
|
||
if (!currentUser.can_edit) {
|
||
const aa = document.getElementById('adminActions'); if (aa) aa.style.display = 'none';
|
||
const aw = document.getElementById('addWBtn'); if (aw) aw.style.display = 'none';
|
||
const al = document.getElementById('addLapBtn'); if (al) al.style.display = 'none';
|
||
} else {
|
||
const al = document.getElementById('addLapBtn'); if (al) al.style.display = '';
|
||
}
|
||
updateLapBadge();
|
||
}
|
||
|
||
function showPage(id) { document.querySelectorAll('.page').forEach(p => p.classList.remove('active')); document.getElementById(id).classList.add('active'); }
|
||
|
||
document.getElementById('liPass').addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
||
document.getElementById('liUser').addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('liPass').focus(); });
|
||
|
||
/* ══ TABS ══ */
|
||
async function switchTab(tab) {
|
||
document.querySelectorAll('.np').forEach(p => p.classList.remove('active'));
|
||
document.getElementById('nav-' + tab)?.classList.add('active');
|
||
document.querySelectorAll('.tc').forEach(t => t.classList.remove('active'));
|
||
document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active');
|
||
const mapPanels = ['panelLeft', 'panelStats', 'panelLeg', 'panelList', 'panelLayers'];
|
||
mapPanels.forEach(id => { const el = document.getElementById(id); if (el) el.style.display = tab === 'map' ? '' : 'none'; });
|
||
|
||
// Hanya loadData jika data ditandai dirty (ada perubahan sejak terakhir load)
|
||
if (currentUser && dataDirty) await loadData();
|
||
|
||
if (tab === 'map' && map) {
|
||
setTimeout(() => map.invalidateSize(), 100);
|
||
// Render ulang hanya jika data baru dimuat atau belum pernah render
|
||
if (!wMarkers.length || dataDirty) render();
|
||
else _updateOverlays();
|
||
}
|
||
else if (tab === 'dashboard') renderDashboard();
|
||
else if (tab === 'data') { populateDtKel(); renderDT(); }
|
||
else if (tab === 'laporan') renderLap();
|
||
else if (tab === 'pengguna') renderUsers();
|
||
}
|
||
|
||
/* ══ MASTER DATA & CORE DATA ══ */
|
||
async function fetchKelurahan() {
|
||
try {
|
||
const res = await fetch('api/kelurahan.php');
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
kelurahanList = json.data;
|
||
KEL = kelurahanList.map(k => k.nama);
|
||
// Build KEC mapping dynamically
|
||
KEC = {};
|
||
kelurahanList.forEach(k => {
|
||
if (!KEC[k.kecamatan]) KEC[k.kecamatan] = [];
|
||
if (!KEC[k.kecamatan].includes(k.nama)) KEC[k.kecamatan].push(k.nama);
|
||
});
|
||
populateSelects();
|
||
}
|
||
} catch (e) {
|
||
console.error('Error fetching kelurahan:', e);
|
||
}
|
||
}
|
||
|
||
function getKec(kel) { for (const [k, v] of Object.entries(KEC)) if (v.includes(kel)) return k; return '—'; }
|
||
|
||
async function loadData(force = false) {
|
||
// Hanya fetch ulang jika data ditandai dirty atau dipaksa
|
||
if (!force && !dataDirty) return;
|
||
try {
|
||
const [resW, resR] = await Promise.all([
|
||
fetch('api/warga.php'),
|
||
fetch('api/rumah_ibadah.php')
|
||
]);
|
||
const jsonW = await resW.json();
|
||
const jsonR = await resR.json();
|
||
|
||
if (jsonW.success) {
|
||
warga = jsonW.data.map(w => ({
|
||
id: w.id, nama: w.nama, nik: w.nik || '',
|
||
kel: w.kelurahan || '',
|
||
lat: parseFloat(w.lat), lng: parseFloat(w.lng),
|
||
kat: w.kategori, jml: parseInt(w.jumlah_anggota) || 1,
|
||
ph: parseInt(w.penghasilan) || 0, tgl: w.tgl_lahir || '',
|
||
pend: w.pendidikan || '', rp: w.riwayat_penyakit || [],
|
||
anggota: (w.anggota || []).map(a => ({
|
||
id: a.id, nama: a.nama, usia: parseInt(a.usia) || 0,
|
||
hub: a.hubungan || 'Anak', py: a.penyakit || []
|
||
})),
|
||
bantuan: (w.histori_bantuan || []).map(b => ({
|
||
id: b.id, tgl: b.tgl || b.tanggal, jenis: b.jenis,
|
||
nom: parseInt(b.nominal) || 0, src: b.sumber || b.sumber_dana || '',
|
||
ket: b.keterangan || ''
|
||
})),
|
||
status: w.status, ket: w.keterangan || '',
|
||
pelapor: w.pelapor, no_laporan: w.no_laporan
|
||
}));
|
||
}
|
||
if (jsonR.success) {
|
||
rumahIbadah = jsonR.data.map(ri => ({
|
||
id: ri.id, nama: ri.nama, tipe: ri.tipe, ikon: ri.ikon,
|
||
lat: parseFloat(ri.lat), lng: parseFloat(ri.lng),
|
||
kap: parseInt(ri.kapasitas) || 0, kel: ri.kelurahan || '', alamat: ri.alamat || ''
|
||
}));
|
||
}
|
||
if (currentUser && (currentUser.is_admin || currentUser.role === 'admin' || currentUser.role === 'petugas')) {
|
||
const resL = await fetch('api/laporan.php');
|
||
const jsonL = await resL.json();
|
||
if (jsonL.success) laporan = jsonL.data;
|
||
}
|
||
// Precompute nearest cache setelah data fresh
|
||
_buildNearestCache();
|
||
dataDirty = false;
|
||
} catch (e) {
|
||
console.error('Error loading data:', e);
|
||
}
|
||
}
|
||
|
||
/* Precompute nearest RI dan semua jarak pasangan RI×warga — O(n×m) sekali saja */
|
||
function _buildNearestCache() {
|
||
_nearestCache.clear();
|
||
_pairDist.clear();
|
||
// 1. Precompute semua jarak RI×warga
|
||
rumahIbadah.forEach(ri => {
|
||
const wDists = new Map();
|
||
warga.forEach(w => { wDists.set(w.id, Math.round(dist(w, ri))); });
|
||
_pairDist.set(ri.id, wDists);
|
||
});
|
||
// 2. Nearest per warga pakai hasil precompute
|
||
warga.forEach(w => {
|
||
let best = null, bd = Infinity;
|
||
rumahIbadah.forEach(ri => {
|
||
const d = _pairDist.get(ri.id)?.get(w.id) ?? Infinity;
|
||
if (d < bd) { bd = d; best = ri; }
|
||
});
|
||
_nearestCache.set(w.id, { ri: best, jarak: Math.round(bd) });
|
||
});
|
||
}
|
||
|
||
function populateSelects() {
|
||
['ri_kel', 'km_kel', 'rep_kel', 'lap_adm_kel', 'ew_kel'].forEach(id => {
|
||
const el = document.getElementById(id); if (!el) return;
|
||
el.innerHTML = '<option value="">— Pilih —</option>';
|
||
kelurahanList.forEach(k => { const o = document.createElement('option'); o.value = k.id; o.textContent = k.nama; el.appendChild(o); });
|
||
});
|
||
const fk = document.getElementById('fKel');
|
||
if (fk) {
|
||
fk.innerHTML = '<option value="semua">Semua Kelurahan</option>';
|
||
kelurahanList.forEach(k => { const o = document.createElement('option'); o.value = k.nama; o.textContent = k.nama; fk.appendChild(o); });
|
||
}
|
||
}
|
||
|
||
function populateDtKel() {
|
||
const el = document.getElementById('dtKel'); if (!el || el.options.length > 1) return;
|
||
kelurahanList.forEach(k => { const o = document.createElement('option'); o.value = k.nama; o.textContent = k.nama; el.appendChild(o); });
|
||
}
|
||
|
||
function dist(a, b) { const R = 6371000, dLa = (b.lat - a.lat) * Math.PI / 180, dLo = (b.lng - a.lng) * Math.PI / 180; const x = Math.sin(dLa / 2) ** 2 + Math.cos(a.lat * Math.PI / 180) * Math.cos(b.lat * Math.PI / 180) * Math.sin(dLo / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1 - x)); }
|
||
/* nearest() — gunakan cache jika tersedia, hitung langsung jika belum */
|
||
function nearest(w) { return _nearestCache.get(w.id) || (function(){ let best = null, bd = Infinity; rumahIbadah.forEach(r => { const d = dist(w, r); if (d < bd) { bd = d; best = r; } }); return { ri: best, jarak: Math.round(bd) }; })(); }
|
||
function riColor(t) { return { masjid: '#10b981', gereja: '#1a5fff', pura: '#f59e0b', vihara: '#7c3aed', klenteng: '#e11d48' }[t] || '#10b981'; }
|
||
function riIkon(t) { return { masjid: '🕌', gereja: '⛪', pura: '🛕', vihara: '🏛️', klenteng: '🏯' }[t] || '🛕'; }
|
||
function fmtRp(n) { if (!n) return '—'; return 'Rp ' + parseInt(n).toLocaleString('id-ID'); }
|
||
function fmtD(d) { if (!d) return '—'; return new Date(d).toLocaleDateString('id-ID', { day: '2-digit', month: 'short', year: 'numeric' }); }
|
||
function calcAge(d) { if (!d) return null; const t = new Date(), b = new Date(d); let a = t.getFullYear() - b.getFullYear(); if (t.getMonth() < b.getMonth() || (t.getMonth() === b.getMonth() && t.getDate() < b.getDate())) a--; return a; }
|
||
|
||
function clearAll() {
|
||
[...wMarkers, ...riMarkers, ...radarL, ...blankL].forEach(l => map.removeLayer(l));
|
||
wMarkers = []; riMarkers = []; radarL = []; blankL = [];
|
||
['kesehatan', 'pendidikan', 'pasar', 'kelurahan'].forEach(kat => {
|
||
if (fasMarkers[kat]) {
|
||
fasMarkers[kat].forEach(m => { if (map.hasLayer(m)) map.removeLayer(m); });
|
||
}
|
||
});
|
||
}
|
||
|
||
function getFiltered() {
|
||
const fk = document.getElementById('fKat')?.value || 'semua';
|
||
const fl = document.getElementById('fKel')?.value || 'semua';
|
||
const fc = document.getElementById('fKec')?.value || 'semua';
|
||
return warga.filter(w => w.status === 'aktif' && (fk === 'semua' || w.kat === fk) && (fl === 'semua' || w.kel === fl) && (fc === 'semua' || (KEC[fc] || []).includes(w.kel)));
|
||
}
|
||
|
||
/* ══ MAP RENDER ══ */
|
||
function render() {
|
||
if (!mapReady) return;
|
||
clearAll();
|
||
document.getElementById('sBS').textContent = '—';
|
||
document.getElementById('sCov').textContent = '—';
|
||
const vis = getFiltered();
|
||
// Gunakan shared canvas renderer — BUKAN L.canvas() per marker!
|
||
const renderer = sharedRenderer;
|
||
vis.forEach(w => {
|
||
const k = KL[w.kat]; const n = nearest(w); const age = calcAge(w.tgl);
|
||
const py = w.rp?.length ? w.rp.map(p => PL[p] || p).join(', ') : 'Tidak Ada';
|
||
const m = L.circleMarker([w.lat, w.lng], { radius: 9, color: '#fff', weight: 2, fillColor: k.col, fillOpacity: .9, renderer }).addTo(map);
|
||
const del = currentUser?.can_edit ? `<button class="pdel" onclick="deleteW('${w.id}')">🗑 Hapus Data Ini</button>` : '';
|
||
m.bindPopup(`<div class="pw">
|
||
<div class="pt"><div><div class="pn">${w.nama}</div><div class="pn2">NIK: ${w.nik || '—'}</div></div><span class="kb ${k.cls}">${k.label}</span></div>
|
||
<div class="prows">
|
||
<div class="pr"><span class="pk">Kelurahan</span><span class="pv">${w.kel}</span></div>
|
||
${age !== null ? `<div class="pr"><span class="pk">Usia</span><span class="pv">${age} thn (${fmtD(w.tgl)})</span></div>` : ''}
|
||
<div class="pr"><span class="pk">Pendidikan</span><span class="pv">${EDL[w.pend || '']}</span></div>
|
||
<div class="pr"><span class="pk">Anggota KK</span><span class="pv">${w.jml || '—'} orang</span></div>
|
||
<div class="pr"><span class="pk">Penghasilan</span><span class="pv">${fmtRp(w.ph)}</span></div>
|
||
<div class="pr"><span class="pk">Penyakit</span><span class="pv" style="color:var(--red)">${py}</span></div>
|
||
<div class="pr"><span class="pk">RI Terdekat</span><span class="pv">${n.ri ? n.ri.nama : '—'}</span></div>
|
||
<div class="pr"><span class="pk">Jarak</span><span class="pv teal">${n.jarak}m</span></div>
|
||
<div class="pr"><span class="pk">Riwayat Bantuan</span><span class="pv">${w.bantuan?.length || 0} kali</span></div>
|
||
</div>
|
||
<div class="pcta-row">
|
||
<button class="pcta sec" onclick="openAnggota('${w.id}')">👨👩👧 Anggota</button>
|
||
<button class="pcta" onclick="openBantuan('${w.id}')">💰 Bantuan</button>
|
||
</div>
|
||
${del}</div>`, { maxWidth: 290, closeButton: false });
|
||
wMarkers.push(m);
|
||
});
|
||
rumahIbadah.forEach(ri => {
|
||
const col = riColor(ri.tipe);
|
||
const html = `<div style="width:34px;height:34px;background:${col};border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:17px;border:3px solid #fff;box-shadow:0 2px 10px rgba(0,0,0,.22)">${ri.ikon || riIkon(ri.tipe)}</div>`;
|
||
const icon = L.divIcon({ html, iconSize: [34, 34], iconAnchor: [17, 17], className: '' });
|
||
const m = L.marker([ri.lat, ri.lng], { icon }).addTo(map);
|
||
const actRI = currentUser?.can_edit ? `<div class="pctas" style="margin-top:8px;display:flex;gap:8px"><button class="pcta" onclick="openEditRI(${ri.id})">✏️ Edit</button><button class="pcta" style="background:var(--rdim);color:var(--red);border-color:var(--red)" onclick="deleteRI(${ri.id})">🗑 Hapus</button></div>` : '';
|
||
m.bindPopup(`<div class="pw"><div class="pt"><div class="pn">${ri.nama}</div><span class="kb khm">${ri.tipe}</span></div><div class="prows"><div class="pr"><span class="pk">Kelurahan</span><span class="pv">${ri.kel || '—'}</span></div><div class="pr"><span class="pk">Kapasitas</span><span class="pv">${ri.kap} jemaah</span></div><div class="pr"><span class="pk">Alamat</span><span class="pv">${ri.alamat || '—'}</span></div></div>${actRI}</div>`, { maxWidth: 260, closeButton: false });
|
||
m.bindTooltip(`<b>${ri.nama}</b>`, { direction: 'top' });
|
||
riMarkers.push(m);
|
||
});
|
||
if (modeRadar) renderRadar(vis);
|
||
if (modeBlank) renderBlank(vis);
|
||
updateStats(vis); buildList(vis);
|
||
// Terapkan kembali state toggle layer setelah rebuild marker
|
||
if (!layerWargaVisible) toggleLayerWarga(false);
|
||
if (!layerRIVisible) toggleLayerRI(false);
|
||
['kesehatan', 'pendidikan', 'pasar', 'kelurahan'].forEach(kat => {
|
||
const cb = document.getElementById('layer_' + kat);
|
||
if (cb && cb.checked) {
|
||
if (fasMarkers[kat]) {
|
||
fasMarkers[kat].forEach(m => { if (!map.hasLayer(m)) m.addTo(map); });
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/* Hanya update overlay (radar/blank) tanpa rebuild semua marker warga/RI */
|
||
function _updateOverlays() {
|
||
if (!mapReady) return;
|
||
[...radarL, ...blankL].forEach(l => map.removeLayer(l));
|
||
radarL = []; blankL = [];
|
||
const vis = getFiltered();
|
||
if (modeRadar) renderRadar(vis);
|
||
if (modeBlank) renderBlank(vis);
|
||
updateStats(vis);
|
||
buildList(vis);
|
||
}
|
||
|
||
function renderRadar(vis) {
|
||
// Pakai _pairDist precompute — zero distance calculation!
|
||
const visSet = new Set(vis.map(w => w.id));
|
||
rumahIbadah.forEach(ri => {
|
||
const wDists = _pairDist.get(ri.id) || new Map();
|
||
let wIn = 0;
|
||
wDists.forEach((d, wId) => { if (d <= radius && visSet.has(wId)) wIn++; });
|
||
const col = wIn > 12 ? '#f03e5a' : wIn > 5 ? '#f97316' : '#7c3aed';
|
||
const c = L.circle([ri.lat, ri.lng], { radius, color: col, fillColor: col, fillOpacity: .07, weight: 1.5, dashArray: '5 4', opacity: .55 }).addTo(map);
|
||
radarL.push(c);
|
||
});
|
||
}
|
||
function renderBlank(vis) {
|
||
let bs = 0;
|
||
vis.forEach(w => {
|
||
const n = nearest(w);
|
||
if (n.jarak > radius) {
|
||
bs++;
|
||
const c = L.circleMarker([w.lat, w.lng], { radius: 16, color: '#7c3aed', fillColor: '#7c3aed', fillOpacity: .15, weight: 2, opacity: .7, dashArray: '4 3' }).addTo(map);
|
||
c.bindTooltip(`⚠ Blank Spot: ${w.nama}<br>${n.jarak}m`, { direction: 'top' });
|
||
blankL.push(c);
|
||
}
|
||
});
|
||
document.getElementById('sBS').textContent = bs;
|
||
}
|
||
function updateStats(vis) {
|
||
document.getElementById('sKK').textContent = vis.length;
|
||
document.getElementById('sRI').textContent = rumahIbadah.length;
|
||
if (!modeBlank) document.getElementById('sBS').textContent = '—';
|
||
if (!modeRadar) { document.getElementById('sCov').textContent = '—'; return; }
|
||
// Pakai _pairDist untuk coverage — akurat per RI bukan nearest saja
|
||
const visSet = new Set(vis.map(w => w.id));
|
||
const cov = new Set();
|
||
rumahIbadah.forEach(ri => {
|
||
const wDists = _pairDist.get(ri.id) || new Map();
|
||
wDists.forEach((d, wId) => { if (d <= radius && visSet.has(wId)) cov.add(wId); });
|
||
});
|
||
document.getElementById('sCov').textContent = vis.length ? Math.round(cov.size / vis.length * 100) + '%' : '—';
|
||
}
|
||
function buildList(vis) {
|
||
const el = document.getElementById('riList'); el.innerHTML = '';
|
||
const visSet = new Set(vis.map(w => w.id));
|
||
rumahIbadah.forEach(ri => {
|
||
// Pakai _pairDist: hitung warga yang benar-benar dalam radius RI ini
|
||
const wDists = _pairDist.get(ri.id) || new Map();
|
||
let wIn = 0;
|
||
wDists.forEach((d, wId) => { if (d <= radius && visSet.has(wId)) wIn++; });
|
||
const pct = Math.min(100, Math.round(wIn / ri.kap * 100));
|
||
const col = pct > 50 ? '#f03e5a' : pct > 25 ? '#f97316' : '#10b981';
|
||
const d2 = document.createElement('div'); d2.className = 'rc';
|
||
d2.innerHTML = `<div class="rn">${ri.ikon || riIkon(ri.tipe)} ${ri.nama}</div><div class="rs">${wIn} warga dalam radius</div><div class="rb2"><div class="rf2" style="width:${pct}%;background:${col}"></div></div><div class="rp" style="color:${col}">${pct}%</div>`;
|
||
d2.onclick = () => map.setView([ri.lat, ri.lng], 16, { animate: true }); el.appendChild(d2);
|
||
});
|
||
}
|
||
function toggleBlank() { modeBlank = !modeBlank; document.getElementById('btnBlank').classList.toggle('on', modeBlank); _updateOverlays(); }
|
||
function toggleRadar() { modeRadar = !modeRadar; document.getElementById('btnRadar').classList.toggle('on', modeRadar); document.getElementById('radiusWrap').classList.toggle('open', modeRadar); _updateOverlays(); }
|
||
function updateRadius(v) { radius = parseInt(v); document.getElementById('radLbl').textContent = v + 'm'; _updateOverlays(); }
|
||
function applyFilter() { render(); }
|
||
|
||
/* ══ LAYER TOGGLE OPSIONAL ══ */
|
||
let layerWargaVisible = true;
|
||
let layerRIVisible = true;
|
||
|
||
function toggleOptSection(id) {
|
||
const body = document.getElementById(id);
|
||
const isOpen = body.style.display !== 'none';
|
||
body.style.display = isOpen ? 'none' : '';
|
||
const sectionId = id === 'optMain' ? 'arrowOptMain' : 'arrowOptOps';
|
||
document.getElementById(sectionId).textContent = isOpen ? '▸' : '▾';
|
||
}
|
||
|
||
function toggleLayerWarga(visible) {
|
||
layerWargaVisible = visible;
|
||
wMarkers.forEach(m => {
|
||
if (visible) { if (!map.hasLayer(m)) m.addTo(map); }
|
||
else { if (map.hasLayer(m)) map.removeLayer(m); }
|
||
});
|
||
}
|
||
|
||
function toggleLayerRI(visible) {
|
||
layerRIVisible = visible;
|
||
riMarkers.forEach(m => {
|
||
if (visible) { if (!map.hasLayer(m)) m.addTo(map); }
|
||
else { if (map.hasLayer(m)) map.removeLayer(m); }
|
||
});
|
||
}
|
||
|
||
async function toggleLayerFasilitas(kategori, visible) {
|
||
if (visible) {
|
||
if (!fasData[kategori]) {
|
||
try {
|
||
const res = await fetch(`api/fasilitas_umum.php?kategori=${kategori}`);
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
fasData[kategori] = json.data;
|
||
} else {
|
||
toast('Gagal memuat data: ' + json.message);
|
||
const cb = document.getElementById('layer_' + kategori);
|
||
if (cb) cb.checked = false;
|
||
return;
|
||
}
|
||
} catch (e) {
|
||
toast('Koneksi server gagal.');
|
||
const cb = document.getElementById('layer_' + kategori);
|
||
if (cb) cb.checked = false;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (!fasMarkers[kategori] || fasMarkers[kategori].length === 0) {
|
||
fasMarkers[kategori] = [];
|
||
const colors = { kesehatan: '#f59e0b', pendidikan: '#1a5fff', pasar: '#7c3aed', kelurahan: '#00c9aa' };
|
||
const icons = { kesehatan: '🏥', pendidikan: '🏫', pasar: '🛒', kelurahan: '🏛️' };
|
||
const col = colors[kategori] || '#999';
|
||
const ik = icons[kategori] || '📍';
|
||
|
||
fasData[kategori].forEach(f => {
|
||
const html = `<div style="width:30px;height:30px;background:${col};border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;border:2px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.2)">${ik}</div>`;
|
||
const icon = L.divIcon({ html, iconSize: [30, 30], iconAnchor: [15, 15], className: '' });
|
||
const m = L.marker([parseFloat(f.lat), parseFloat(f.lng)], { icon });
|
||
|
||
m.bindPopup(`<div class="pw">
|
||
<div class="pt"><div class="pn">${f.nama}</div><span class="kb" style="background:${col};color:#fff">${kategori.toUpperCase()}</span></div>
|
||
<div class="prows">
|
||
<div class="pr"><span class="pk">Alamat</span><span class="pv">${f.alamat || '—'}</span></div>
|
||
<div class="pr"><span class="pk">Keterangan</span><span class="pv">${f.keterangan || '—'}</span></div>
|
||
</div>
|
||
</div>`, { maxWidth: 260, closeButton: false });
|
||
m.bindTooltip(`<b>${f.nama}</b>`, { direction: 'top' });
|
||
fasMarkers[kategori].push(m);
|
||
});
|
||
}
|
||
|
||
fasMarkers[kategori].forEach(m => {
|
||
if (!map.hasLayer(m)) m.addTo(map);
|
||
});
|
||
} else {
|
||
if (fasMarkers[kategori]) {
|
||
fasMarkers[kategori].forEach(m => {
|
||
if (map.hasLayer(m)) map.removeLayer(m);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
async function deleteW(id) {
|
||
map.closePopup();
|
||
if (!confirm('Hapus data warga ini?')) return;
|
||
await processDeleteW(id);
|
||
}
|
||
async function delWData(id) {
|
||
if (!confirm('Hapus data warga ini?')) return;
|
||
await processDeleteW(id);
|
||
}
|
||
async function processDeleteW(id) {
|
||
try {
|
||
const res = await fetch(`api/warga.php?id=${id}`, { method: 'DELETE' });
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
dataDirty = true;
|
||
await loadData();
|
||
render();
|
||
if (document.getElementById('tabData').classList.contains('active')) renderDT();
|
||
toast('Data warga berhasil dihapus');
|
||
} else {
|
||
alert('Gagal menghapus: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
async function deleteRI(id) {
|
||
if (!confirm('Hapus rumah ibadah ini?')) return;
|
||
map.closePopup();
|
||
try {
|
||
const res = await fetch(`api/rumah_ibadah.php?id=${id}`, { method: 'DELETE' });
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
dataDirty = true;
|
||
await loadData();
|
||
render();
|
||
toast('Rumah ibadah berhasil dihapus');
|
||
} else {
|
||
alert('Gagal menghapus: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* ══ DASHBOARD ══ */
|
||
async function renderDashboard() {
|
||
try {
|
||
const res = await fetch('api/dashboard.php');
|
||
const json = await res.json();
|
||
if (!json.success) {
|
||
console.error('Gagal mengambil data dashboard:', json.message);
|
||
return;
|
||
}
|
||
const d = json.data;
|
||
|
||
// KPIs
|
||
const lapM = laporan.filter(l => l.status === 'menunggu').length;
|
||
document.getElementById('kpiKK').textContent = d.kpis.total_kk;
|
||
document.getElementById('kpiSM').textContent = d.kpis.sangat_miskin;
|
||
document.getElementById('kpiBn').textContent = d.kpis.total_penyaluran;
|
||
document.getElementById('kpiLap').textContent = lapM + ' laporan menunggu verifikasi';
|
||
document.getElementById('kpiBnNom').textContent = 'Total: ' + fmtRp(d.kpis.total_nominal);
|
||
document.getElementById('kpiPy').textContent = PL[d.kpis.penyakit_top] || d.kpis.penyakit_top || '—';
|
||
document.getElementById('dashDate').textContent = 'Per ' + new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
||
|
||
// Chart 1: Kelurahan
|
||
if (charts.kel) charts.kel.destroy();
|
||
charts.kel = new Chart(document.getElementById('chartKel'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: d.charts.kelurahan.labels.map(l => l.replace('Kel. ', '')),
|
||
datasets: [{
|
||
label: 'Jumlah KK',
|
||
data: d.charts.kelurahan.data,
|
||
backgroundColor: d.charts.kelurahan.data.map(v => v >= 4 ? 'rgba(240,62,90,.8)' : v >= 3 ? 'rgba(249,115,22,.8)' : 'rgba(26,95,255,.8)'),
|
||
borderRadius: 6
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: 'rgba(0,0,0,.04)' } }, x: { grid: { display: false }, ticks: { font: { size: 10 } } } }
|
||
}
|
||
});
|
||
|
||
// Chart 2: Kategori
|
||
if (charts.kat) charts.kat.destroy();
|
||
charts.kat = new Chart(document.getElementById('chartKat'), {
|
||
type: 'doughnut',
|
||
data: {
|
||
labels: d.charts.kategori.labels,
|
||
datasets: [{
|
||
data: d.charts.kategori.data,
|
||
backgroundColor: ['rgba(240,62,90,.85)', 'rgba(249,115,22,.85)', 'rgba(26,95,255,.85)'],
|
||
borderColor: '#fff',
|
||
borderWidth: 3
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
cutout: '65%',
|
||
plugins: { legend: { position: 'bottom', labels: { font: { size: 11 }, padding: 12 } } }
|
||
}
|
||
});
|
||
|
||
// Chart 3: Program Bantuan
|
||
const bnCol = ['rgba(26,95,255,.8)', 'rgba(0,201,170,.8)', 'rgba(124,58,237,.8)', 'rgba(240,62,90,.8)', 'rgba(249,115,22,.8)', 'rgba(16,185,129,.8)', 'rgba(245,158,11,.8)'];
|
||
if (charts.bn) charts.bn.destroy();
|
||
charts.bn = new Chart(document.getElementById('chartBn'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: d.charts.bantuan.labels,
|
||
datasets: [{
|
||
label: 'Penyaluran',
|
||
data: d.charts.bantuan.data,
|
||
backgroundColor: d.charts.bantuan.data.map((_, i) => bnCol[i % bnCol.length]),
|
||
borderRadius: 6
|
||
}]
|
||
},
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: 'rgba(0,0,0,.04)' } }, x: { grid: { display: false }, ticks: { font: { size: 10 } } } }
|
||
}
|
||
});
|
||
|
||
// Chart 4: Prevalensi Penyakit
|
||
const pyCol = ['rgba(240,62,90,.8)', 'rgba(249,115,22,.8)', 'rgba(124,58,237,.8)', 'rgba(26,95,255,.8)', 'rgba(0,201,170,.8)', 'rgba(16,185,129,.8)', 'rgba(245,158,11,.8)', 'rgba(239,68,68,.8)', 'rgba(99,102,241,.8)'];
|
||
if (charts.py) charts.py.destroy();
|
||
charts.py = new Chart(document.getElementById('chartPy'), {
|
||
type: 'bar',
|
||
data: {
|
||
labels: d.charts.penyakit.labels.map(k => PL[k] || k),
|
||
datasets: [{
|
||
label: 'Jumlah',
|
||
data: d.charts.penyakit.data,
|
||
backgroundColor: d.charts.penyakit.data.map((_, i) => pyCol[i % pyCol.length]),
|
||
borderRadius: 6
|
||
}]
|
||
},
|
||
options: {
|
||
indexAxis: 'y',
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
plugins: { legend: { display: false } },
|
||
scales: { x: { beginAtZero: true, ticks: { stepSize: 1 }, grid: { color: 'rgba(0,0,0,.04)' } }, y: { grid: { display: false }, ticks: { font: { size: 11 } } } }
|
||
}
|
||
});
|
||
|
||
// Tabel Ranking Kelurahan
|
||
rankData = d.ranking || [];
|
||
rankPage = 1;
|
||
renderRank();
|
||
|
||
} catch (e) {
|
||
console.error('Error rendering dashboard:', e);
|
||
}
|
||
}
|
||
|
||
/* ══ RANKING PAGINATION ══ */
|
||
let rankData = [], rankPage = 1;
|
||
const rankPS = 10;
|
||
function renderRank() {
|
||
const tp = Math.max(1, Math.ceil(rankData.length / rankPS));
|
||
if (rankPage > tp) rankPage = tp;
|
||
const slice = rankData.slice((rankPage - 1) * rankPS, rankPage * rankPS);
|
||
const offset = (rankPage - 1) * rankPS;
|
||
document.getElementById('dashTb').innerHTML = slice.map((r, i) =>
|
||
`<tr>
|
||
<td style="color:var(--ink3);font-weight:700;font-family:'Syne',sans-serif">${offset + i + 1}</td>
|
||
<td style="font-weight:600">${r.kelurahan}</td>
|
||
<td style="color:var(--ink3);font-size:12px">${r.kecamatan}</td>
|
||
<td><span class="kb2 sm">${r.sangat_miskin}</span></td>
|
||
<td><span class="kb2 m">${r.miskin}</span></td>
|
||
<td><span class="kb2 hm">${r.hampir_miskin}</span></td>
|
||
<td style="font-weight:700">${r.total}</td>
|
||
<td style="color:var(--teal);font-weight:600">${fmtRp(r.total_bantuan)}</td>
|
||
</tr>`
|
||
).join('');
|
||
// info
|
||
const info = document.getElementById('rankInfo');
|
||
if (info) info.textContent = `${offset+1}–${Math.min(offset+rankPS, rankData.length)} dari ${rankData.length} kelurahan`;
|
||
// pagination buttons
|
||
const pg = document.getElementById('rankPgn'); pg.innerHTML = '';
|
||
if (tp > 1) {
|
||
const prev = document.createElement('button');
|
||
prev.className = 'pgb'; prev.textContent = '←'; prev.disabled = rankPage <= 1;
|
||
prev.onclick = () => { rankPage--; renderRank(); };
|
||
pg.appendChild(prev);
|
||
// show at most 7 page buttons with ellipsis
|
||
for (let i = 1; i <= tp; i++) {
|
||
if (i === 1 || i === tp || (i >= rankPage - 2 && i <= rankPage + 2)) {
|
||
const b = document.createElement('button');
|
||
b.className = 'pgb' + (i === rankPage ? ' active' : '');
|
||
b.textContent = i;
|
||
b.onclick = (function(p){ return () => { rankPage = p; renderRank(); }; })(i);
|
||
pg.appendChild(b);
|
||
} else if (i === rankPage - 3 || i === rankPage + 3) {
|
||
const sp = document.createElement('span');
|
||
sp.textContent = '…'; sp.style.cssText = 'padding:0 4px;color:var(--ink3);font-size:13px;align-self:center';
|
||
pg.appendChild(sp);
|
||
}
|
||
}
|
||
const nxt = document.createElement('button');
|
||
nxt.className = 'pgb'; nxt.textContent = '→'; nxt.disabled = rankPage >= tp;
|
||
nxt.onclick = () => { rankPage++; renderRank(); };
|
||
pg.appendChild(nxt);
|
||
}
|
||
}
|
||
|
||
/* ══ DATA TABLE ══ */
|
||
let dtPage = 1, dtPS = 12;
|
||
function renderDT() {
|
||
const q = (document.getElementById('dtSearch')?.value || '').toLowerCase();
|
||
const k = document.getElementById('dtKat')?.value || '';
|
||
const l = document.getElementById('dtKel')?.value || '';
|
||
const p = document.getElementById('dtPy')?.value || '';
|
||
let f = warga.filter(w => w.status === 'aktif');
|
||
if (q) f = f.filter(w => w.nama.toLowerCase().includes(q) || (w.nik || '').includes(q) || w.kel.toLowerCase().includes(q));
|
||
if (k) f = f.filter(w => w.kat === k);
|
||
if (l) f = f.filter(w => w.kel === l);
|
||
if (p) f = f.filter(w => w.anggota?.some(a => a.py?.includes(p)) || w.rp?.includes(p));
|
||
document.getElementById('dtCount').textContent = f.length + ' data';
|
||
const tp = Math.max(1, Math.ceil(f.length / dtPS));
|
||
if (dtPage > tp) dtPage = tp;
|
||
const pd = f.slice((dtPage - 1) * dtPS, dtPage * dtPS);
|
||
const canEdit = currentUser?.can_edit;
|
||
document.getElementById('dtTb').innerHTML = pd.map((w, i) => {
|
||
const k2 = KL[w.kat]; const age = calcAge(w.tgl);
|
||
const pyAll = new Set(); w.anggota?.forEach(a => a.py?.forEach(p2 => pyAll.add(p2)));
|
||
const pyD = pyAll.size > 0 ? [...pyAll].slice(0, 2).map(p2 => `<span style="font-size:10px;color:var(--red)">${PL[p2] || p2}</span>`).join(' ') + (pyAll.size > 2 ? ` <span style="color:var(--ink3);font-size:10px">+${pyAll.size - 2}</span>` : '') : '<span style="color:var(--green);font-size:10px">✓ Sehat</span>';
|
||
const bc = w.bantuan?.length || 0;
|
||
return `<tr><td style="color:var(--ink3);font-size:11px">${(dtPage - 1) * dtPS + i + 1}</td><td><div class="td-name">${w.nama}</div><div class="td-nik">${w.nik || '—'}</div></td><td style="font-size:12px">${w.kel}</td><td><span class="kb2 ${w.kat === 'sangat_miskin' ? 'sm' : w.kat === 'miskin' ? 'm' : 'hm'}">${k2.label}</span></td><td style="font-size:12px">${age !== null ? age + ' thn<div class="td-nik">' + fmtD(w.tgl) + '</div>' : '—'}</td><td style="font-size:12px">${EDL[w.pend || '']}</td><td style="font-size:12px">${fmtRp(w.ph)}</td><td>${pyD}</td><td>${bc > 0 ? `<span style="font-size:11px;font-weight:700;color:var(--teal);background:var(--tdim);padding:2px 8px;border-radius:99px">${bc}x</span>` : '<span style="color:var(--ink3);font-size:11px">—</span>'}</td><td><div class="td-act">${canEdit ? `<button class="asm t" onclick="openBantuan('${w.id}')">💰</button><button class="asm t" onclick="openEditWarga('${w.id}')" title="Edit Warga">✏️</button>` : ''} <button class="asm b" onclick="openAnggota('${w.id}')">👨👩👧</button>${canEdit ? `<button class="asm r" onclick="delWData('${w.id}')">🗑</button>` : ''}</div></td></tr>`;
|
||
}).join('');
|
||
const pg = document.getElementById('pgn'); pg.innerHTML = '';
|
||
if (tp > 1) {
|
||
const prev = document.createElement('button'); prev.className = 'pgb'; prev.textContent = '←'; prev.disabled = dtPage <= 1; prev.onclick = () => { dtPage--; renderDT(); }; pg.appendChild(prev);
|
||
for (let i = 1; i <= tp; i++) { const b = document.createElement('button'); b.className = 'pgb' + (i === dtPage ? ' active' : ''); b.textContent = i; b.onclick = (() => { const p = i; return () => { dtPage = p; renderDT(); }; })(); pg.appendChild(b); }
|
||
const nxt = document.createElement('button'); nxt.className = 'pgb'; nxt.textContent = '→'; nxt.disabled = dtPage >= tp; nxt.onclick = () => { dtPage++; renderDT(); }; pg.appendChild(nxt);
|
||
}
|
||
}
|
||
|
||
function exportCSV() {
|
||
const akt = warga.filter(w => w.status === 'aktif');
|
||
const rows = [['Nama', 'NIK', 'Kelurahan', 'Kecamatan', 'Kategori', 'Tgl Lahir', 'Usia', 'Pendidikan', 'Penghasilan', 'Anggota', 'Penyakit', 'Jumlah Bantuan']];
|
||
akt.forEach(w => { const py = [...new Set(w.anggota?.flatMap(a => a.py) || [])].map(p => PL[p] || p).join('; '); rows.push([w.nama, w.nik || '', w.kel, getKec(w.kel), KL[w.kat].label, w.tgl || '', calcAge(w.tgl) || '', EDL[w.pend || ''] || '', w.ph || 0, w.jml || '', py, w.bantuan?.length || 0]); });
|
||
const csv = rows.map(r => r.map(c => `"${c}"`).join(',')).join('\n');
|
||
const blob = new Blob(['\ufeff' + csv], { type: 'text/csv;charset=utf-8;' });
|
||
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'geososial_' + new Date().toISOString().slice(0, 10) + '.csv'; a.click();
|
||
toast('Export CSV berhasil!');
|
||
}
|
||
|
||
/* ══ LAPORAN MASUK ══ */
|
||
function renderLap() {
|
||
const m2 = laporan.filter(l => l.status === 'menunggu').length;
|
||
const d2 = laporan.filter(l => l.status === 'disetujui').length;
|
||
const t2 = laporan.filter(l => l.status === 'ditolak').length;
|
||
document.getElementById('lsM').textContent = m2;
|
||
document.getElementById('lsD').textContent = d2;
|
||
document.getElementById('lsT').textContent = t2;
|
||
const kl = { sangat_miskin: 'Sangat Miskin', miskin: 'Miskin', hampir_miskin: 'Hampir Miskin' };
|
||
document.getElementById('lapTb').innerHTML = laporan.map(l => `<tr>
|
||
<td style="font-family:monospace;font-size:11px;font-weight:700;color:var(--blue)">${l.no}</td>
|
||
<td style="font-size:12px">${fmtD(l.tgl)}</td>
|
||
<td style="font-size:12px">${l.pelapor || 'Anonim'}</td>
|
||
<td style="font-weight:600">${l.nama}</td>
|
||
<td style="font-size:12px">${l.kel}</td>
|
||
<td><span class="kb2 ${l.kat === 'sangat_miskin' ? 'sm' : l.kat === 'miskin' ? 'm' : 'hm'}">${kl[l.kat]}</span></td>
|
||
<td><span class="sb ${l.status}">${l.status.charAt(0).toUpperCase() + l.status.slice(1)}</span></td>
|
||
<td><div style="display:flex;gap:5px">
|
||
<button class="asm b" onclick="openLapDetail('${l.id}')">Detail</button>
|
||
${l.status === 'menunggu' && currentUser?.can_edit ? `<button class="asm t" onclick="approveLap('${l.id}')">✓</button><button class="asm r" onclick="rejectLap('${l.id}')">✕</button>` : ''}
|
||
</div></td>
|
||
</tr>`).join('');
|
||
updateLapBadge();
|
||
}
|
||
|
||
function updateLapBadge() {
|
||
const n = laporan.filter(l => l.status === 'menunggu').length;
|
||
const b = document.getElementById('lbadge');
|
||
if (b) {
|
||
b.textContent = n;
|
||
b.style.display = n > 0 ? '' : 'none';
|
||
}
|
||
}
|
||
|
||
function openLapDetail(id) {
|
||
const l = laporan.find(x => x.id == id); if (!l) return; curLapId = id;
|
||
document.getElementById('lapNo').textContent = l.no + ' — ' + fmtD(l.tgl);
|
||
const kl = { sangat_miskin: 'Sangat Miskin', miskin: 'Miskin', hampir_miskin: 'Hampir Miskin' };
|
||
document.getElementById('lapDetailBody').innerHTML = `
|
||
<div class="lpd-row"><span class="lpd-k">Nama Dilaporkan</span><span class="lpd-v" style="font-weight:700">${l.nama}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">NIK</span><span class="lpd-v">${l.nik || 'Tidak diketahui'}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Kelurahan</span><span class="lpd-v">${l.kel}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Kategori</span><span class="lpd-v">${kl[l.kat]}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Jumlah Anggota</span><span class="lpd-v">${l.jml || '—'} orang</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Penghasilan</span><span class="lpd-v">${fmtRp(l.ph)}</span></div>
|
||
<div class="lpd-row" style="flex-direction:column;gap:6px"><span class="lpd-k">Keterangan</span><span style="font-size:13px;color:var(--ink);line-height:1.6;background:var(--paper);padding:10px;border-radius:8px">${l.ket}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Pelapor</span><span class="lpd-v">${l.pelapor || 'Anonim'}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Kontak HP</span><span class="lpd-v">${l.hp || '—'}</span></div>
|
||
<div class="lpd-row"><span class="lpd-k">Status</span><span class="lpd-v"><span class="sb ${l.status}">${l.status.charAt(0).toUpperCase() + l.status.slice(1)}</span></span></div>
|
||
`;
|
||
const foot = document.getElementById('lapFoot');
|
||
if (l.status === 'menunggu' && currentUser?.can_edit) {
|
||
foot.innerHTML = `<button class="btn-cancel" onclick="closeModal('modalLaporan')">Tutup</button><button class="btn-reject" onclick="rejectLap('${l.id}');closeModal('modalLaporan')">✕ Tolak</button><button class="btn-approve" onclick="approveLap('${l.id}');closeModal('modalLaporan')">✓ Setujui & Tambahkan Data</button>`;
|
||
} else { foot.innerHTML = `<button class="btn-cancel" onclick="closeModal('modalLaporan')">Tutup</button>`; }
|
||
openModal('modalLaporan');
|
||
setTimeout(() => {
|
||
const con = document.getElementById('lapMap');
|
||
if (l.lat && l.lng) { con.style.display = 'block'; if (window._lapMap) { window._lapMap.remove(); } window._lapMap = L.map(con, { zoomControl: false, dragging: false, scrollWheelZoom: false }).setView([l.lat, l.lng], 15); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(window._lapMap); L.circleMarker([l.lat, l.lng], { radius: 10, color: '#f03e5a', fillColor: '#f03e5a', fillOpacity: .7, weight: 2 }).addTo(window._lapMap); } else { con.style.display = 'none'; }
|
||
}, 200);
|
||
}
|
||
|
||
async function approveLap(id) {
|
||
if (!confirm('Setujui laporan ini dan tambahkan ke data warga?')) return;
|
||
try {
|
||
const res = await fetch(`api/laporan.php?id=${id}&action=approve`, { method: 'PUT' });
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
dataDirty = true;
|
||
await loadData();
|
||
renderLap();
|
||
render();
|
||
toast('✓ Laporan disetujui. Data warga ditambahkan.');
|
||
} else {
|
||
alert('Gagal memproses: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
async function rejectLap(id) {
|
||
if (!confirm('Tolak laporan ini?')) return;
|
||
try {
|
||
const res = await fetch(`api/laporan.php?id=${id}&action=reject`, { method: 'PUT' });
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
dataDirty = true;
|
||
await loadData();
|
||
renderLap();
|
||
toast('Laporan ditolak.');
|
||
} else {
|
||
alert('Gagal memproses: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* ══ BANTUAN ══ */
|
||
function openBantuan(wId) {
|
||
curBantuanId = wId; const w = warga.find(x => x.id == wId); if (!w) return;
|
||
if (map) map.closePopup();
|
||
document.getElementById('bnWargaNama').textContent = w.nama + ' — ' + w.kel;
|
||
document.getElementById('bnForm').style.display = currentUser?.can_edit ? '' : 'none';
|
||
renderBnTable(w); openModal('modalBantuan');
|
||
}
|
||
|
||
function renderBnTable(w) {
|
||
const list = w.bantuan || [];
|
||
const tbody = document.getElementById('bnTb');
|
||
if (!list.length) { tbody.innerHTML = ''; document.getElementById('bnEmpty').style.display = 'block'; document.getElementById('bnTot').textContent = ''; return; }
|
||
document.getElementById('bnEmpty').style.display = 'none';
|
||
const tot = list.reduce((s, b) => s + (b.nom || 0), 0);
|
||
document.getElementById('bnTot').innerHTML = `Total nilai bantuan: <b>${fmtRp(tot)}</b> dari ${list.length} penyaluran`;
|
||
const ce = currentUser?.can_edit;
|
||
tbody.innerHTML = list.map(b => `<tr><td>${fmtD(b.tgl)}</td><td><span class="jb ${JL[b.jenis] || 'lain'}">${b.jenis}</span></td><td style="font-weight:600;color:var(--teal)">${b.nom ? fmtRp(b.nom) : 'Non-tunai'}</td><td style="font-size:11px;color:var(--ink3)">${b.src}</td><td style="font-size:11px">${b.ket || '—'}</td><td>${ce ? `<button class="asm r" onclick="delBn('${w.id}','${b.id}')">🗑</button>` : ''}</td></tr>`).join('');
|
||
}
|
||
|
||
async function submitBantuan() {
|
||
const w = warga.find(x => x.id == curBantuanId); if (!w) return;
|
||
const tgl = document.getElementById('bh_tgl').value; if (!tgl) { alert('Tanggal wajib diisi.'); return; }
|
||
const jenis = document.getElementById('bh_jenis').value;
|
||
const nominal = parseInt(document.getElementById('bh_nom').value) || 0;
|
||
const sumber_dana = document.getElementById('bh_src').value;
|
||
const keterangan = document.getElementById('bh_ket').value.trim();
|
||
|
||
try {
|
||
const res = await fetch('api/bantuan.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ warga_id: curBantuanId, tanggal: tgl, jenis, nominal, sumber_dana, keterangan })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
document.getElementById('bh_tgl').value = '';
|
||
document.getElementById('bh_nom').value = '';
|
||
document.getElementById('bh_ket').value = '';
|
||
dataDirty = true;
|
||
await loadData();
|
||
const updatedW = warga.find(x => x.id == curBantuanId);
|
||
renderBnTable(updatedW);
|
||
toast('Bantuan berhasil ditambahkan');
|
||
} else {
|
||
alert('Gagal menambahkan bantuan: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
async function delBn(wId, bId) {
|
||
if (!confirm('Hapus data bantuan ini?')) return;
|
||
try {
|
||
const res = await fetch(`api/bantuan.php?id=${bId}`, { method: 'DELETE' });
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
dataDirty = true;
|
||
await loadData();
|
||
const updatedW = warga.find(x => x.id == wId);
|
||
renderBnTable(updatedW);
|
||
toast('Data bantuan dihapus');
|
||
} else {
|
||
alert('Gagal menghapus bantuan: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* ══ ANGGOTA ══ */
|
||
function openAnggota(wId) {
|
||
const w = warga.find(x => x.id == wId); if (!w) return;
|
||
if (map) map.closePopup();
|
||
document.getElementById('agWargaNama').textContent = w.nama + ' — ' + w.kel;
|
||
const body = document.getElementById('agBody');
|
||
if (!w.anggota?.length) {
|
||
body.innerHTML = '<div style="text-align:center;padding:32px;color:var(--ink3)">👤 Belum ada data anggota keluarga</div>';
|
||
} else { body.innerHTML = `<table class="bt"><thead><tr><th>Nama</th><th>Usia</th><th>Hubungan</th><th>Riwayat Penyakit</th></tr></thead><tbody>${w.anggota.map(a => `<tr><td style="font-weight:600">${a.nama}</td><td>${a.usia} thn</td><td style="color:var(--ink3)">${a.hub}</td><td>${a.py?.length ? a.py.map(p => `<span style="font-size:10px;font-weight:700;padding:2px 8px;border-radius:99px;background:var(--rdim);color:var(--red)">${PL[p] || p}</span>`).join(' ') : '<span style="color:var(--green);font-size:11px">✓ Tidak Ada</span>'}</td></tr>`).join('')}</tbody></table>`; }
|
||
openModal('modalAnggota');
|
||
}
|
||
|
||
/* ══ PENGGUNA ══ */
|
||
async function renderUsers() {
|
||
try {
|
||
const res = await fetch('api/users.php');
|
||
const json = await res.json();
|
||
if (!json.success) return;
|
||
const users = json.data;
|
||
const isAdmin = currentUser?.is_admin;
|
||
|
||
// Tampilkan tombol tambah hanya untuk admin
|
||
const addBtn = document.getElementById('addUsrBtn');
|
||
if (addBtn) addBtn.style.display = isAdmin ? '' : 'none';
|
||
|
||
const roleLabel = { admin: 'Super Admin', petugas: 'Petugas Dinsos', walikota: 'Walikota', viewer: 'Viewer' };
|
||
const roleColor = {
|
||
admin: { bg: 'var(--rdim)', fg: 'var(--red)' },
|
||
petugas: { bg: 'var(--bdim)', fg: 'var(--blue)' },
|
||
walikota: { bg: 'var(--pdim)', fg: 'var(--purple)' },
|
||
viewer: { bg: 'var(--paper)', fg: 'var(--ink3)' }
|
||
};
|
||
const avatarColors = ['#1a5fff','#10b981','#f97316','#7c3aed','#f03e5a','#00c9aa'];
|
||
|
||
document.getElementById('usrTb').innerHTML = users.map((u, i) => {
|
||
const rc = roleColor[u.role] || roleColor.viewer;
|
||
const rl = roleLabel[u.role] || u.role;
|
||
const ac = avatarColors[i % avatarColors.length];
|
||
const initials = u.username.substring(0, 2).toUpperCase();
|
||
const isSelf = u.id == currentUser?.id;
|
||
const canViewBtn = isAdmin || isSelf;
|
||
|
||
const permissionBadge = u.can_edit
|
||
? `<span style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--blue);font-weight:600">
|
||
<span style="font-size:11px">✏️</span> Baca & Tulis
|
||
</span>`
|
||
: `<span style="display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--ink3);font-weight:500">
|
||
<span style="font-size:11px">👁️</span> Baca saja
|
||
</span>`;
|
||
|
||
const statusBadge = u.aktif
|
||
? `<span style="display:inline-flex;align-items:center;font-size:11px;font-weight:700;padding:4px 10px;border-radius:99px;background:var(--gdim);color:var(--green)">
|
||
Aktif
|
||
</span>`
|
||
: `<span style="display:inline-flex;align-items:center;font-size:11px;font-weight:700;padding:4px 10px;border-radius:99px;background:var(--paper);color:var(--ink3)">
|
||
Nonaktif
|
||
</span>`;
|
||
|
||
return `<tr>
|
||
<td style="color:var(--ink3); text-align: center; font-weight:600">${i + 1}</td>
|
||
<td>
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<div style="width:32px;height:32px;border-radius:50%;background:${ac};display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;color:#fff;flex-shrink:0;box-shadow: 0 2px 6px rgba(0,0,0,0.1)">${initials}</div>
|
||
<div style="display:flex;flex-direction:column;gap:2px">
|
||
<span style="font-weight:700;color:var(--ink);font-size:13px">${u.username}</span>
|
||
${isSelf ? '<span style="font-size:9px;background:var(--tdim);color:var(--teal);padding:1px 6px;border-radius:99px;font-weight:800;width:fit-content">Akun Anda</span>' : ''}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td style="font-weight:500;color:var(--ink2)">${u.jabatan || '—'}</td>
|
||
<td><span style="font-size:11px;font-weight:700;padding:4px 11px;border-radius:99px;background:${rc.bg};color:${rc.fg};box-shadow: 0 1px 4px rgba(0,0,0,0.02)">${rl}</span></td>
|
||
<td>${permissionBadge}</td>
|
||
<td>${statusBadge}</td>
|
||
<td>${canViewBtn ? `<button class="btn-usr-prof" onclick="openUserProfile(${JSON.stringify(u).replace(/"/g,'"')})">👤 Profil</button>` : ''}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
} catch (e) { console.error(e); }
|
||
}
|
||
|
||
function openUserProfile(u) {
|
||
const isAdmin = currentUser?.is_admin;
|
||
const isSelf = u.id == currentUser?.id;
|
||
document.getElementById('prof_id').value = u.id;
|
||
document.getElementById('prof_jabatan').value = u.jabatan || '';
|
||
document.getElementById('prof_role').value = u.role;
|
||
document.getElementById('prof_can_edit').value = u.can_edit ? '1' : '0';
|
||
document.getElementById('prof_aktif').value = u.aktif ? '1' : '0';
|
||
document.getElementById('prof_password').value = '';
|
||
document.getElementById('profUsername').textContent = u.username;
|
||
document.getElementById('profModalSub').textContent = (u.jabatan || 'Pengguna') + ' — ' + (u.aktif ? 'Aktif' : 'Nonaktif');
|
||
document.getElementById('profCreated').textContent = 'Bergabung: ' + (u.created_at ? new Date(u.created_at).toLocaleDateString('id-ID', {day:'2-digit',month:'long',year:'numeric'}) : '—');
|
||
// Avatar
|
||
const colors = ['#1a5fff','#10b981','#f97316','#7c3aed','#f03e5a','#00c9aa'];
|
||
const av = document.getElementById('profAvatar');
|
||
av.textContent = u.username.substring(0, 2).toUpperCase();
|
||
av.style.background = colors[u.id % colors.length];
|
||
// Readonly jika bukan admin dan bukan diri sendiri
|
||
const editable = isAdmin;
|
||
['prof_jabatan','prof_role','prof_can_edit','prof_aktif'].forEach(id => {
|
||
document.getElementById(id).disabled = !editable;
|
||
});
|
||
// Password hanya bisa diganti oleh admin atau diri sendiri
|
||
document.getElementById('prof_pw_wrap').style.display = (isAdmin || isSelf) ? '' : 'none';
|
||
document.getElementById('profSaveBtn').style.display = (isAdmin || isSelf) ? '' : 'none';
|
||
openModal('modalProfile');
|
||
}
|
||
|
||
async function saveUserProfile() {
|
||
const id = document.getElementById('prof_id').value;
|
||
const jabatan = document.getElementById('prof_jabatan').value.trim();
|
||
const role = document.getElementById('prof_role').value;
|
||
const can_edit = document.getElementById('prof_can_edit').value === '1';
|
||
const aktif = document.getElementById('prof_aktif').value === '1';
|
||
const password = document.getElementById('prof_password').value;
|
||
const payload = { jabatan, role, can_edit, aktif };
|
||
if (password) payload.password = password;
|
||
try {
|
||
const res = await fetch(`api/users.php?id=${id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalProfile');
|
||
renderUsers();
|
||
toast('Profil pengguna berhasil diperbarui');
|
||
} else {
|
||
alert('Gagal: ' + json.message);
|
||
}
|
||
} catch (e) { alert('Koneksi server gagal.'); }
|
||
}
|
||
|
||
function openAddUser() {
|
||
['nu_username','nu_password','nu_jabatan'].forEach(id => document.getElementById(id).value = '');
|
||
document.getElementById('nu_role').value = 'petugas';
|
||
openModal('modalAddUser');
|
||
}
|
||
|
||
async function submitAddUser() {
|
||
const username = document.getElementById('nu_username').value.trim();
|
||
const password = document.getElementById('nu_password').value;
|
||
const jabatan = document.getElementById('nu_jabatan').value.trim();
|
||
const role = document.getElementById('nu_role').value;
|
||
if (!username) { alert('Username wajib diisi.'); return; }
|
||
if (!password || password.length < 6) { alert('Password minimal 6 karakter.'); return; }
|
||
try {
|
||
const res = await fetch('api/users.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password, jabatan, role })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalAddUser');
|
||
renderUsers();
|
||
toast(`Pengguna "${username}" berhasil ditambahkan`);
|
||
} else {
|
||
alert('Gagal: ' + json.message);
|
||
}
|
||
} catch (e) { alert('Koneksi server gagal.'); }
|
||
}
|
||
|
||
/* ══ MODALS ══ */
|
||
function openModal(id) { document.getElementById(id).classList.add('open'); }
|
||
function closeModal(id) { document.getElementById(id).classList.remove('open'); }
|
||
document.querySelectorAll('.mo').forEach(o => { o.addEventListener('click', e => { if (e.target === o) o.classList.remove('open'); }); });
|
||
|
||
/* MODAL RI */
|
||
function openModalRI() {
|
||
clearFormRI();
|
||
document.getElementById('ri_edit_id').value = '';
|
||
document.getElementById('riModalTitle').textContent = '🕌 Tambah Rumah Ibadah';
|
||
document.getElementById('riModalSub').textContent = 'Klik mini-peta untuk menentukan lokasi';
|
||
document.getElementById('riSubmitBtn').textContent = 'Simpan';
|
||
openModal('modalRI');
|
||
setTimeout(initRIMap, 200);
|
||
}
|
||
|
||
function openEditRI(id) {
|
||
const ri = rumahIbadah.find(r => r.id == id);
|
||
if (!ri) return;
|
||
map.closePopup();
|
||
clearFormRI();
|
||
document.getElementById('ri_edit_id').value = ri.id;
|
||
document.getElementById('ri_nama').value = ri.nama;
|
||
document.getElementById('ri_tipe').value = ri.tipe;
|
||
document.getElementById('ri_kap').value = ri.kap || '';
|
||
document.getElementById('ri_alamat').value = ri.alamat || '';
|
||
document.getElementById('ri_lat').value = ri.lat;
|
||
document.getElementById('ri_lng').value = ri.lng;
|
||
document.getElementById('riMapLbl').style.display = 'none';
|
||
document.getElementById('riModalTitle').textContent = '✏️ Edit Rumah Ibadah';
|
||
document.getElementById('riModalSub').textContent = ri.nama;
|
||
document.getElementById('riSubmitBtn').textContent = 'Simpan Perubahan';
|
||
// Set kelurahan
|
||
const kelEl = document.getElementById('ri_kel');
|
||
if (kelEl) {
|
||
const opt = [...kelEl.options].find(o => o.textContent === ri.kel);
|
||
if (opt) kelEl.value = opt.value;
|
||
}
|
||
openModal('modalRI');
|
||
setTimeout(() => {
|
||
initRIMap();
|
||
// place existing marker
|
||
if (riPickMap) {
|
||
riPickMap.setView([ri.lat, ri.lng], 15);
|
||
if (riPickMk) riPickMap.removeLayer(riPickMk);
|
||
riPickMk = L.marker([ri.lat, ri.lng], { icon: L.divIcon({ html: '<div class="pick-pin">📍</div>', iconSize: [26, 26], iconAnchor: [13, 26], className: '' }) }).addTo(riPickMap);
|
||
}
|
||
}, 200);
|
||
}
|
||
|
||
function clearFormRI() {
|
||
['ri_nama', 'ri_kap', 'ri_alamat', 'ri_lat', 'ri_lng'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
||
document.getElementById('ri_tipe').value = 'masjid';
|
||
document.getElementById('riMapLbl').style.display = '';
|
||
if (riPickMk && riPickMap) { riPickMap.removeLayer(riPickMk); riPickMk = null; }
|
||
}
|
||
function initRIMap() {
|
||
const con = document.getElementById('riMapPick');
|
||
if (riPickMap) { riPickMap.invalidateSize(); return; }
|
||
riPickMap = L.map(con, { zoomControl: false }).setView(CENTER, 14);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(riPickMap);
|
||
riPickMap.on('click', e => {
|
||
const { lat, lng } = e.latlng;
|
||
document.getElementById('ri_lat').value = lat.toFixed(6); document.getElementById('ri_lng').value = lng.toFixed(6);
|
||
document.getElementById('riMapLbl').style.display = 'none';
|
||
if (riPickMk) riPickMap.removeLayer(riPickMk);
|
||
riPickMk = L.marker([lat, lng], { icon: L.divIcon({ html: '<div class="pick-pin">📍</div>', iconSize: [26, 26], iconAnchor: [13, 26], className: '' }) }).addTo(riPickMap);
|
||
});
|
||
}
|
||
|
||
async function submitRI() {
|
||
const nama = document.getElementById('ri_nama').value.trim();
|
||
const lat = parseFloat(document.getElementById('ri_lat').value);
|
||
const lng = parseFloat(document.getElementById('ri_lng').value);
|
||
if (!nama) { alert('Nama wajib diisi.'); return; }
|
||
if (isNaN(lat) || isNaN(lng)) { alert('Pilih lokasi di peta.'); return; }
|
||
const tipe = document.getElementById('ri_tipe').value;
|
||
const kelurahan_id = document.getElementById('ri_kel').value;
|
||
const kapasitas = parseInt(document.getElementById('ri_kap').value) || 50;
|
||
const alamat = document.getElementById('ri_alamat').value.trim();
|
||
const editId = document.getElementById('ri_edit_id').value;
|
||
const isEdit = !!editId;
|
||
|
||
try {
|
||
const res = await fetch(isEdit ? `api/rumah_ibadah.php?id=${editId}` : 'api/rumah_ibadah.php', {
|
||
method: isEdit ? 'PUT' : 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama, tipe, lat, lng, kapasitas, kelurahan_id, alamat })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalRI');
|
||
dataDirty = true;
|
||
await loadData();
|
||
render();
|
||
toast(isEdit ? `"${nama}" berhasil diperbarui` : `"${nama}" berhasil ditambahkan`);
|
||
} else {
|
||
alert((isEdit ? 'Gagal memperbarui: ' : 'Gagal menambahkan: ') + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* MODAL KM */
|
||
function openModalKM() { clearFormKM(); openModal('modalKM'); setTimeout(initKMMap, 200); }
|
||
function clearFormKM() {
|
||
['km_nama', 'km_nik', 'km_jml', 'km_ph', 'km_lat', 'km_lng', 'km_ket', 'km_tgl'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
||
document.getElementById('km_kat').value = 'sangat_miskin'; document.getElementById('km_pend').value = '';
|
||
document.getElementById('kmMapLbl').style.display = '';
|
||
document.querySelectorAll('.pykk').forEach(cb => cb.checked = false);
|
||
document.getElementById('anggotaBody').innerHTML = '';
|
||
if (kmPickMk && kmPickMap) { kmPickMap.removeLayer(kmPickMk); kmPickMk = null; }
|
||
}
|
||
function initKMMap() {
|
||
const con = document.getElementById('kmMapPick');
|
||
if (kmPickMap) { kmPickMap.invalidateSize(); return; }
|
||
kmPickMap = L.map(con, { zoomControl: false }).setView(CENTER, 14);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(kmPickMap);
|
||
kmPickMap.on('click', e => {
|
||
const { lat, lng } = e.latlng;
|
||
document.getElementById('km_lat').value = lat.toFixed(6); document.getElementById('km_lng').value = lng.toFixed(6);
|
||
document.getElementById('kmMapLbl').style.display = 'none';
|
||
if (kmPickMk) kmPickMap.removeLayer(kmPickMk);
|
||
const kat = document.getElementById('km_kat').value;
|
||
const col = kat === 'sangat_miskin' ? '#f03e5a' : kat === 'miskin' ? '#f97316' : '#1a5fff';
|
||
kmPickMk = L.marker([lat, lng], { icon: L.divIcon({ html: `<div style="width:18px;height:18px;background:${col};border-radius:50%;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.3)"></div>`, iconSize: [18, 18], iconAnchor: [9, 9], className: '' }) }).addTo(kmPickMap);
|
||
});
|
||
}
|
||
function addAnggotaRow() {
|
||
const tbody = document.getElementById('anggotaBody');
|
||
const tr = document.createElement('tr');
|
||
const penyakitCbs = Object.entries(PL).map(([v, l]) =>
|
||
`<label title="${l}"><input type="checkbox" class="apy-cb" value="${v}">${PLIK[v] || ''} ${l}</label>`
|
||
).join('');
|
||
tr.innerHTML = `
|
||
<td><input class="ai" type="text" placeholder="Nama anggota"></td>
|
||
<td><input class="ai" type="number" placeholder="Usia" min="0" max="120" style="width:100%"></td>
|
||
<td><select class="asel"><option value="Istri">Istri</option><option value="Suami">Suami</option><option value="Anak">Anak</option><option value="Orang Tua">Orang Tua</option><option value="Cucu">Cucu</option><option value="Lainnya">Lainnya</option></select></td>
|
||
<td><div class="apy-wrap">${penyakitCbs}</div></td>
|
||
<td><button class="btn-del-a" onclick="this.closest('tr').remove()">✕</button></td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
}
|
||
|
||
async function submitKM() {
|
||
const nama = document.getElementById('km_nama').value.trim();
|
||
const lat = parseFloat(document.getElementById('km_lat').value);
|
||
const lng = parseFloat(document.getElementById('km_lng').value);
|
||
if (!nama) { alert('Nama KK wajib diisi.'); return; } if (isNaN(lat) || isNaN(lng)) { alert('Pilih lokasi di peta.'); return; }
|
||
const rp = [...document.querySelectorAll('.pykk:checked')].map(cb => cb.value);
|
||
const kelurahan_id = document.getElementById('km_kel').value;
|
||
const nik = document.getElementById('km_nik').value.trim();
|
||
const kategori = document.getElementById('km_kat').value;
|
||
const jumlah_anggota = parseInt(document.getElementById('km_jml').value) || 1;
|
||
const penghasilan = parseInt(document.getElementById('km_ph').value) || 0;
|
||
const tgl_lahir = document.getElementById('km_tgl').value;
|
||
const pendidikan = document.getElementById('km_pend').value;
|
||
const keterangan = document.getElementById('km_ket').value.trim();
|
||
|
||
const anggotaRows = document.querySelectorAll('#anggotaBody tr');
|
||
const anggota = [];
|
||
anggotaRows.forEach(tr => {
|
||
const inputs = tr.querySelectorAll('input[type=text], input[type=number]');
|
||
const sel = tr.querySelector('select');
|
||
const nm = inputs[0]?.value.trim();
|
||
const us = parseInt(inputs[1]?.value) || 0;
|
||
const hub = sel?.value || 'Anak';
|
||
const py = [...tr.querySelectorAll('.apy-cb:checked')].map(cb => cb.value);
|
||
if (nm) anggota.push({ nama: nm, usia: us, hub, py });
|
||
});
|
||
|
||
try {
|
||
const res = await fetch('api/warga.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama, nik, lat, lng, rp, kelurahan_id, kategori, jumlah_anggota, penghasilan, tgl_lahir, pendidikan, keterangan, anggota })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalKM');
|
||
dataDirty = true;
|
||
await loadData();
|
||
render();
|
||
renderDT();
|
||
toast(`Data "${nama}" berhasil ditambahkan`);
|
||
} else {
|
||
alert('Gagal menyimpan: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* EDIT WARGA MODAL LOGIC */
|
||
let curEditWargaId = null;
|
||
|
||
function openEditWarga(id) {
|
||
const w = warga.find(x => x.id == id);
|
||
if (!w) { alert('Data warga tidak ditemukan.'); return; }
|
||
curEditWargaId = id;
|
||
|
||
// Pre-fill inputs
|
||
document.getElementById('ew_nama').value = w.nama || '';
|
||
document.getElementById('ew_nik').value = w.nik || '';
|
||
document.getElementById('ew_tgl').value = w.tgl || '';
|
||
document.getElementById('ew_pend').value = w.pend || '';
|
||
document.getElementById('ew_kat').value = w.kat || 'miskin';
|
||
|
||
// Select kelurahan by ID
|
||
const kelOpt = kelurahanList.find(k => k.nama === w.kel);
|
||
document.getElementById('ew_kel').value = kelOpt ? kelOpt.id : '';
|
||
|
||
document.getElementById('ew_jml').value = w.jml || 1;
|
||
document.getElementById('ew_ph').value = w.ph || 0;
|
||
document.getElementById('ew_ket').value = w.ket || '';
|
||
document.getElementById('ew_lat').value = w.lat ? parseFloat(w.lat).toFixed(6) : '';
|
||
document.getElementById('ew_lng').value = w.lng ? parseFloat(w.lng).toFixed(6) : '';
|
||
|
||
// Uncheck all KK illnesses, then check the ones that match
|
||
document.querySelectorAll('.ewpykk').forEach(cb => {
|
||
cb.checked = (w.rp && w.rp.includes(cb.value));
|
||
});
|
||
|
||
// Clear family member body
|
||
const tbody = document.getElementById('ewAnggotaBody');
|
||
tbody.innerHTML = '';
|
||
|
||
// Add other family members (excluding KK)
|
||
if (w.anggota) {
|
||
w.anggota.forEach(a => {
|
||
if (a.hub === 'Kepala Keluarga') return;
|
||
addEwAnggotaRow(a.nama, a.usia, a.hub, a.py);
|
||
});
|
||
}
|
||
|
||
openModal('modalEditWarga');
|
||
setTimeout(() => initEwMap(w.lat, w.lng, w.kat), 200);
|
||
}
|
||
|
||
function initEwMap(lat, lng, kat) {
|
||
const con = document.getElementById('ewMapPick');
|
||
if (ewPickMap) {
|
||
ewPickMap.invalidateSize();
|
||
} else {
|
||
ewPickMap = L.map(con, { zoomControl: false }).setView(CENTER, 14);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(ewPickMap);
|
||
ewPickMap.on('click', e => {
|
||
const { lat, lng } = e.latlng;
|
||
document.getElementById('ew_lat').value = lat.toFixed(6);
|
||
document.getElementById('ew_lng').value = lng.toFixed(6);
|
||
if (ewPickMk) ewPickMap.removeLayer(ewPickMk);
|
||
const currentKat = document.getElementById('ew_kat').value;
|
||
const col = currentKat === 'sangat_miskin' ? '#f03e5a' : currentKat === 'miskin' ? '#f97316' : '#1a5fff';
|
||
ewPickMk = L.marker([lat, lng], {
|
||
icon: L.divIcon({
|
||
html: `<div style="width:18px;height:18px;background:${col};border-radius:50%;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.3)"></div>`,
|
||
iconSize: [18, 18],
|
||
iconAnchor: [9, 9],
|
||
className: ''
|
||
})
|
||
}).addTo(ewPickMap);
|
||
});
|
||
}
|
||
|
||
if (ewPickMk) ewPickMap.removeLayer(ewPickMk);
|
||
|
||
if (lat && lng) {
|
||
const parsedLat = parseFloat(lat);
|
||
const parsedLng = parseFloat(lng);
|
||
ewPickMap.setView([parsedLat, parsedLng], 14);
|
||
const col = kat === 'sangat_miskin' ? '#f03e5a' : kat === 'miskin' ? '#f97316' : '#1a5fff';
|
||
ewPickMk = L.marker([parsedLat, parsedLng], {
|
||
icon: L.divIcon({
|
||
html: `<div style="width:18px;height:18px;background:${col};border-radius:50%;border:2px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.3)"></div>`,
|
||
iconSize: [18, 18],
|
||
iconAnchor: [9, 9],
|
||
className: ''
|
||
})
|
||
}).addTo(ewPickMap);
|
||
}
|
||
}
|
||
|
||
function addEwAnggotaRow(nama = '', usia = '', hubungan = 'Anak', penyakit = []) {
|
||
const tbody = document.getElementById('ewAnggotaBody');
|
||
const tr = document.createElement('tr');
|
||
const penyakitCbs = Object.entries(PL).map(([v, l]) => {
|
||
const checked = penyakit.includes(v) ? 'checked' : '';
|
||
return `<label title="${l}"><input type="checkbox" class="ewapy-cb" value="${v}" ${checked}>${PLIK[v] || ''} ${l}</label>`;
|
||
}).join('');
|
||
tr.innerHTML = `
|
||
<td><input class="ai" type="text" placeholder="Nama anggota" value="${nama}"></td>
|
||
<td><input class="ai" type="number" placeholder="Usia" min="0" max="120" style="width:100%" value="${usia}"></td>
|
||
<td><select class="asel">
|
||
<option value="Istri" ${hubungan === 'Istri' ? 'selected' : ''}>Istri</option>
|
||
<option value="Suami" ${hubungan === 'Suami' ? 'selected' : ''}>Suami</option>
|
||
<option value="Anak" ${hubungan === 'Anak' ? 'selected' : ''}>Anak</option>
|
||
<option value="Orang Tua" ${hubungan === 'Orang Tua' ? 'selected' : ''}>Orang Tua</option>
|
||
<option value="Cucu" ${hubungan === 'Cucu' ? 'selected' : ''}>Cucu</option>
|
||
<option value="Lainnya" ${hubungan === 'Lainnya' ? 'selected' : ''}>Lainnya</option>
|
||
</select></td>
|
||
<td><div class="apy-wrap">${penyakitCbs}</div></td>
|
||
<td><button class="btn-del-a" onclick="this.closest('tr').remove()">✕</button></td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
}
|
||
|
||
async function submitEditWarga() {
|
||
if (!curEditWargaId) return;
|
||
const nama = document.getElementById('ew_nama').value.trim();
|
||
const lat = parseFloat(document.getElementById('ew_lat').value);
|
||
const lng = parseFloat(document.getElementById('ew_lng').value);
|
||
if (!nama) { alert('Nama KK wajib diisi.'); return; }
|
||
if (isNaN(lat) || isNaN(lng)) { alert('Pilih lokasi di peta.'); return; }
|
||
|
||
const rp = [...document.querySelectorAll('.ewpykk:checked')].map(cb => cb.value);
|
||
const kelurahan_id = document.getElementById('ew_kel').value;
|
||
const nik = document.getElementById('ew_nik').value.trim();
|
||
const kategori = document.getElementById('ew_kat').value;
|
||
const jumlah_anggota = parseInt(document.getElementById('ew_jml').value) || 1;
|
||
const penghasilan = parseInt(document.getElementById('ew_ph').value) || 0;
|
||
const tgl_lahir = document.getElementById('ew_tgl').value;
|
||
const pendidikan = document.getElementById('ew_pend').value;
|
||
const keterangan = document.getElementById('ew_ket').value.trim();
|
||
|
||
const anggotaRows = document.querySelectorAll('#ewAnggotaBody tr');
|
||
const anggota = [];
|
||
anggotaRows.forEach(tr => {
|
||
const inputs = tr.querySelectorAll('input[type=text], input[type=number]');
|
||
const sel = tr.querySelector('select');
|
||
const nm = inputs[0]?.value.trim();
|
||
const us = parseInt(inputs[1]?.value) || 0;
|
||
const hub = sel?.value || 'Anak';
|
||
const py = [...tr.querySelectorAll('.ewapy-cb:checked')].map(cb => cb.value);
|
||
if (nm) anggota.push({ nama: nm, usia: us, hub, py });
|
||
});
|
||
|
||
try {
|
||
const res = await fetch(`api/warga.php?id=${curEditWargaId}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ nama, nik, lat, lng, rp, kelurahan_id, kategori, jumlah_anggota, penghasilan, tgl_lahir, pendidikan, keterangan, anggota })
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalEditWarga');
|
||
dataDirty = true;
|
||
await loadData();
|
||
render();
|
||
renderDT();
|
||
toast(`Data "${nama}" berhasil diperbarui`);
|
||
} else {
|
||
alert('Gagal memperbarui: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* PUBLIC REPORT */
|
||
// repPickMap2 dan repPickMk2 sudah dideklarasikan di atas (global state)
|
||
function showPublicReportPage() {
|
||
showPage('pageRep');
|
||
setTimeout(() => {
|
||
const con = document.getElementById('repMap'); if (!con) return;
|
||
if (repPickMap2) { repPickMap2.invalidateSize(); return; }
|
||
repPickMap2 = L.map(con, { zoomControl: false }).setView(CENTER, 13);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(repPickMap2);
|
||
repPickMap2.on('click', e => {
|
||
const { lat, lng } = e.latlng;
|
||
document.getElementById('rep_lat').value = lat.toFixed(6); document.getElementById('rep_lng').value = lng.toFixed(6);
|
||
document.getElementById('repMapLbl').style.display = 'none';
|
||
if (repPickMk2) repPickMap2.removeLayer(repPickMk2);
|
||
repPickMk2 = L.marker([lat, lng], { icon: L.divIcon({ html: '<div class="pick-pin">📍</div>', iconSize: [26, 26], iconAnchor: [13, 26], className: '' }) }).addTo(repPickMap2);
|
||
});
|
||
}, 200);
|
||
}
|
||
|
||
async function submitPublicReport() {
|
||
const nama = document.getElementById('rep_nama').value.trim();
|
||
const kelurahan_id = document.getElementById('rep_kel').value;
|
||
const ket = document.getElementById('rep_ket').value.trim();
|
||
if (!nama) { alert('Nama wajib diisi.'); return; } if (!kelurahan_id) { alert('Kelurahan wajib dipilih.'); return; } if (!ket) { alert('Keterangan wajib diisi.'); return; }
|
||
|
||
const payload = {
|
||
pelapor: document.getElementById('rep_pelapor').value.trim() || 'Anonim',
|
||
hp: document.getElementById('rep_hp').value.trim(),
|
||
nama,
|
||
nik: document.getElementById('rep_nik').value.trim(),
|
||
kelurahan_id,
|
||
kategori: document.getElementById('rep_kat').value,
|
||
jumlah_anggota: parseInt(document.getElementById('rep_jml').value) || 0,
|
||
penghasilan: parseInt(document.getElementById('rep_ph').value) || 0,
|
||
keterangan: ket,
|
||
lat: parseFloat(document.getElementById('rep_lat').value) || null,
|
||
lng: parseFloat(document.getElementById('rep_lng').value) || null
|
||
};
|
||
|
||
try {
|
||
const res = await fetch('api/laporan.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
document.getElementById('repOkNo').textContent = json.data.no;
|
||
document.getElementById('repFormWrap').style.display = 'none';
|
||
document.getElementById('repOk').classList.add('show');
|
||
dataDirty = true; // laporan baru masuk, tandai data perlu reload
|
||
await loadData();
|
||
updateLapBadge();
|
||
} else {
|
||
alert('Gagal mengirim laporan: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
function resetRep() { document.getElementById('repFormWrap').style.display = ''; document.getElementById('repOk').classList.remove('show');['rep_pelapor', 'rep_hp', 'rep_nama', 'rep_nik', 'rep_ket', 'rep_lat', 'rep_lng', 'rep_jml', 'rep_ph'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('repMapLbl').style.display = ''; if (repPickMk2 && repPickMap2) { repPickMap2.removeLayer(repPickMk2); repPickMk2 = null; } }
|
||
|
||
/* ADMIN REPORT CREATION */
|
||
function openModalLaporanAdmin() {
|
||
clearFormLaporanAdmin();
|
||
openModal('modalLaporanAdmin');
|
||
setTimeout(initLapAdmMap, 200);
|
||
}
|
||
|
||
function clearFormLaporanAdmin() {
|
||
['lap_adm_pelapor', 'lap_adm_hp', 'lap_adm_nama', 'lap_adm_nik', 'lap_adm_kel', 'lap_adm_kat', 'lap_adm_jml', 'lap_adm_ph', 'lap_adm_ket', 'lap_adm_lat', 'lap_adm_lng'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) {
|
||
if (el.tagName === 'SELECT') el.value = el.options[0]?.value || '';
|
||
else el.value = '';
|
||
}
|
||
});
|
||
document.getElementById('lapAdmMapLbl').style.display = '';
|
||
if (lapAdmPickMk && lapAdmPickMap) {
|
||
lapAdmPickMap.removeLayer(lapAdmPickMk);
|
||
lapAdmPickMk = null;
|
||
}
|
||
}
|
||
|
||
function initLapAdmMap() {
|
||
const con = document.getElementById('lapAdmMapPick');
|
||
if (lapAdmPickMap) { lapAdmPickMap.invalidateSize(); return; }
|
||
lapAdmPickMap = L.map(con, { zoomControl: false }).setView(CENTER, 14);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '', maxZoom: 19 }).addTo(lapAdmPickMap);
|
||
lapAdmPickMap.on('click', e => {
|
||
const { lat, lng } = e.latlng;
|
||
document.getElementById('lap_adm_lat').value = lat.toFixed(6);
|
||
document.getElementById('lap_adm_lng').value = lng.toFixed(6);
|
||
document.getElementById('lapAdmMapLbl').style.display = 'none';
|
||
if (lapAdmPickMk) lapAdmPickMap.removeLayer(lapAdmPickMk);
|
||
lapAdmPickMk = L.marker([lat, lng], { icon: L.divIcon({ html: '<div class="pick-pin">📍</div>', iconSize: [26, 26], iconAnchor: [13, 26], className: '' }) }).addTo(lapAdmPickMap);
|
||
});
|
||
}
|
||
|
||
async function submitLaporanAdmin() {
|
||
const nama = document.getElementById('lap_adm_nama').value.trim();
|
||
const kelurahan_id = document.getElementById('lap_adm_kel').value;
|
||
const ket = document.getElementById('lap_adm_ket').value.trim();
|
||
if (!nama) { alert('Nama Kepala Keluarga wajib diisi.'); return; }
|
||
if (!kelurahan_id) { alert('Kelurahan wajib dipilih.'); return; }
|
||
if (!ket) { alert('Keterangan wajib diisi.'); return; }
|
||
|
||
const payload = {
|
||
pelapor: document.getElementById('lap_adm_pelapor').value.trim() || 'Anonim',
|
||
hp: document.getElementById('lap_adm_hp').value.trim(),
|
||
nama,
|
||
nik: document.getElementById('lap_adm_nik').value.trim(),
|
||
kelurahan_id,
|
||
kategori: document.getElementById('lap_adm_kat').value,
|
||
jumlah_anggota: parseInt(document.getElementById('lap_adm_jml').value) || 0,
|
||
penghasilan: parseInt(document.getElementById('lap_adm_ph').value) || 0,
|
||
keterangan: ket,
|
||
lat: parseFloat(document.getElementById('lap_adm_lat').value) || null,
|
||
lng: parseFloat(document.getElementById('lap_adm_lng').value) || null
|
||
};
|
||
|
||
try {
|
||
const res = await fetch('api/laporan.php', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const json = await res.json();
|
||
if (json.success) {
|
||
closeModal('modalLaporanAdmin');
|
||
dataDirty = true;
|
||
await loadData();
|
||
renderLap();
|
||
updateLapBadge();
|
||
toast(`✓ Laporan untuk "${nama}" berhasil dibuat: ${json.data.no}`);
|
||
} else {
|
||
alert('Gagal mengirim laporan: ' + json.message);
|
||
}
|
||
} catch (e) {
|
||
alert('Koneksi server gagal.');
|
||
}
|
||
}
|
||
|
||
/* TOAST */
|
||
let toastT = null;
|
||
function toast(msg) { document.getElementById('toastMsg').textContent = msg; const t = document.getElementById('toast'); t.classList.add('show'); if (toastT) clearTimeout(toastT); toastT = setTimeout(() => t.classList.remove('show'), 2800); }
|
||
|
||
/* APP INITIALIZATION */
|
||
async function initApp() {
|
||
if (mapReady) {
|
||
// Peta sudah siap — render ulang hanya jika data berubah
|
||
if (dataDirty) { await loadData(); render(); }
|
||
return;
|
||
}
|
||
// First load: fetch data lalu inisialisasi peta
|
||
dataDirty = true;
|
||
await loadData();
|
||
setTimeout(() => {
|
||
sharedRenderer = L.canvas({ padding: 0.5 });
|
||
map = L.map('map', {
|
||
zoomControl: true,
|
||
preferCanvas: true,
|
||
renderer: sharedRenderer,
|
||
zoomAnimation: true,
|
||
markerZoomAnimation: false
|
||
}).setView(CENTER, 14);
|
||
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
||
attribution: 'Leaflet | © OpenStreetMap | CartoDB',
|
||
maxZoom: 19, subdomains: 'abcd',
|
||
keepBuffer: 1
|
||
}).addTo(map);
|
||
mapReady = true;
|
||
render();
|
||
// renderUsers() dipanggil lazy saat user buka tab Pengguna
|
||
}, 50);
|
||
}
|
||
|
||
// Check session and load kelurahan on load (check readyState first to prevent race condition)
|
||
async function startApp() {
|
||
await fetchKelurahan();
|
||
await checkSession();
|
||
|
||
const btnLapor = document.querySelector('#pageLogin .btn-lapor');
|
||
if (btnLapor) btnLapor.onclick = showPublicReportPage;
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
window.addEventListener('DOMContentLoaded', startApp);
|
||
} else {
|
||
startApp();
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html> |