2238 lines
95 KiB
HTML
2238 lines
95 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="id">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>WebGIS Poverty Mapping (PHP Native)</title>
|
||
<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>
|
||
<style>
|
||
/* [COPY-PASTE SELURUH CSS ANDA DARI VERSI SEBELUMNYA DI SINI] */
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--green: #2d6a4f;
|
||
--green-l: #40916c;
|
||
--green-ll: #d8f3dc;
|
||
--red: #c1121f;
|
||
--yellow: #e9c46a;
|
||
--blue: #457b9d;
|
||
--light: #f8f9fa;
|
||
--white: #fff;
|
||
--text: #212529;
|
||
--text2: #6c757d;
|
||
--border: #dee2e6;
|
||
--shadow: 0 2px 8px rgba(0, 0, 0, .1);
|
||
--r: 8px;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||
background: var(--light);
|
||
color: var(--text);
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#login-page,
|
||
#register-page {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100vh;
|
||
background: linear-gradient(135deg, #1b4332 0%, #2d6a4f 50%, #40916c 100%);
|
||
}
|
||
|
||
.login-box {
|
||
background: var(--white);
|
||
border-radius: 16px;
|
||
padding: 40px;
|
||
width: 380px;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
|
||
}
|
||
|
||
.login-logo {
|
||
text-align: center;
|
||
margin-bottom: 28px;
|
||
}
|
||
|
||
.login-logo .icon {
|
||
font-size: 48px;
|
||
}
|
||
|
||
.login-logo h1 {
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: var(--green);
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.login-logo p {
|
||
font-size: 13px;
|
||
color: var(--text2);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.login-box label {
|
||
display: block;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: var(--text2);
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.login-box input {
|
||
width: 100%;
|
||
padding: 10px 14px;
|
||
border: 1.5px solid var(--border);
|
||
border-radius: var(--r);
|
||
font-size: 14px;
|
||
outline: none;
|
||
transition: border .15s;
|
||
}
|
||
|
||
.login-box input:focus {
|
||
border-color: var(--green);
|
||
}
|
||
|
||
.login-box .form-group {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.btn-login {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: var(--green);
|
||
color: #fff;
|
||
border: none;
|
||
border-radius: var(--r);
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
margin-top: 8px;
|
||
transition: background .15s;
|
||
}
|
||
|
||
.btn-login:hover {
|
||
background: var(--green-l);
|
||
}
|
||
|
||
.login-err {
|
||
color: var(--red);
|
||
font-size: 12.5px;
|
||
text-align: center;
|
||
margin-top: 10px;
|
||
min-height: 18px;
|
||
}
|
||
|
||
.login-demo {
|
||
margin-top: 20px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid var(--border);
|
||
}
|
||
|
||
.login-demo p {
|
||
font-size: 11.5px;
|
||
color: var(--text2);
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.demo-btn {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 7px 12px;
|
||
margin-bottom: 5px;
|
||
background: var(--light);
|
||
border: 1px solid var(--border);
|
||
border-radius: 6px;
|
||
font-size: 12px;
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: background .1s;
|
||
}
|
||
|
||
.demo-btn:hover {
|
||
background: var(--green-ll);
|
||
}
|
||
|
||
.demo-btn strong {
|
||
color: var(--green);
|
||
}
|
||
|
||
.register-link {
|
||
text-align: center;
|
||
margin-top: 14px;
|
||
font-size: 12.5px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.register-link a {
|
||
color: var(--green);
|
||
cursor: pointer;
|
||
text-decoration: underline;
|
||
}
|
||
|
||
#register-page {
|
||
display: none;
|
||
}
|
||
|
||
#topbar {
|
||
height: 52px;
|
||
background: var(--green);
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0 20px;
|
||
gap: 16px;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 2px 6px rgba(0, 0, 0, .15);
|
||
}
|
||
|
||
.topbar-logo {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.topbar-logo span {
|
||
opacity: .7;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.nav {
|
||
display: flex;
|
||
gap: 2px;
|
||
flex: 1;
|
||
}
|
||
|
||
.nav-btn {
|
||
padding: 6px 14px;
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
color: rgba(255, 255, 255, .8);
|
||
border: none;
|
||
background: none;
|
||
cursor: pointer;
|
||
transition: all .15s;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.nav-btn:hover,
|
||
.nav-btn.active {
|
||
background: rgba(255, 255, 255, .2);
|
||
color: #fff;
|
||
}
|
||
|
||
.topbar-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.user-chip {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: rgba(255, 255, 255, .15);
|
||
border-radius: 20px;
|
||
padding: 4px 12px 4px 6px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.user-avatar {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 50%;
|
||
background: rgba(255, 255, 255, .3);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
|
||
.user-name {
|
||
font-size: 12.5px;
|
||
color: #fff;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.role-badge {
|
||
font-size: 10px;
|
||
padding: 2px 8px;
|
||
border-radius: 10px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.rb-super_admin {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.rb-admin_daerah {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
}
|
||
|
||
.rb-pengurus_ri {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.rb-donatur {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.rb-masyarakat {
|
||
background: #e2e3e5;
|
||
color: #383d41;
|
||
}
|
||
|
||
.logout-btn {
|
||
padding: 6px 14px;
|
||
border-radius: 6px;
|
||
font-size: 12.5px;
|
||
color: rgba(255, 255, 255, .8);
|
||
border: 1px solid rgba(255, 255, 255, .3);
|
||
background: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.logout-btn:hover {
|
||
background: rgba(255, 255, 255, .15);
|
||
color: #fff;
|
||
}
|
||
|
||
#main {
|
||
display: flex;
|
||
flex: 1;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#sidebar {
|
||
width: 260px;
|
||
background: var(--white);
|
||
border-right: 1px solid var(--border);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.sb-stats {
|
||
padding: 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.sb-stats h3 {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .8px;
|
||
color: var(--text2);
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.stat-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 8px;
|
||
}
|
||
|
||
.stat-box {
|
||
background: var(--light);
|
||
border-radius: var(--r);
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.stat-box .n {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
}
|
||
|
||
.stat-box .l {
|
||
font-size: 10.5px;
|
||
color: var(--text2);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.stat-box.green .n {
|
||
color: var(--green);
|
||
}
|
||
|
||
.stat-box.red .n {
|
||
color: var(--red);
|
||
}
|
||
|
||
.stat-box.blue .n {
|
||
color: var(--blue);
|
||
}
|
||
|
||
.stat-box.yellow .n {
|
||
color: #b5830a;
|
||
}
|
||
|
||
.layer-ctrl {
|
||
padding: 14px 16px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.layer-ctrl h3 {
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .8px;
|
||
color: var(--text2);
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.layer-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 5px 0;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
|
||
.layer-icon {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 5px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.layer-name {
|
||
font-size: 12.5px;
|
||
color: var(--text);
|
||
flex: 1;
|
||
}
|
||
|
||
.layer-cnt {
|
||
font-size: 11px;
|
||
color: var(--text2);
|
||
background: var(--light);
|
||
padding: 1px 6px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.toggle {
|
||
width: 32px;
|
||
height: 18px;
|
||
background: var(--border);
|
||
border-radius: 9px;
|
||
position: relative;
|
||
transition: background .2s;
|
||
}
|
||
|
||
.toggle.on {
|
||
background: var(--green);
|
||
}
|
||
|
||
.toggle::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 12px;
|
||
height: 12px;
|
||
background: #fff;
|
||
border-radius: 50%;
|
||
top: 3px;
|
||
left: 3px;
|
||
transition: left .2s;
|
||
}
|
||
|
||
.toggle.on::after {
|
||
left: 17px;
|
||
}
|
||
|
||
.sb-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.sb-list::-webkit-scrollbar {
|
||
width: 3px;
|
||
}
|
||
|
||
.sb-list::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.sb-hdr {
|
||
padding: 10px 16px 5px;
|
||
font-size: 10.5px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .8px;
|
||
color: var(--text2);
|
||
position: sticky;
|
||
top: 0;
|
||
background: var(--white);
|
||
}
|
||
|
||
.sb-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 9px 16px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid rgba(0, 0, 0, .04);
|
||
transition: background .1s;
|
||
}
|
||
|
||
.sb-item:hover {
|
||
background: var(--light);
|
||
}
|
||
|
||
.sb-item-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.sb-item-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.sb-item-name {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.sb-item-sub {
|
||
font-size: 10.5px;
|
||
color: var(--text2);
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.badge {
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 2px 7px;
|
||
border-radius: 10px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.badge-verified {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.badge-pending {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.badge-rejected {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
}
|
||
|
||
.badge-dispute {
|
||
background: #e2d9f3;
|
||
color: #6610f2;
|
||
}
|
||
|
||
.badge-aktif {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.badge-selesai {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
}
|
||
|
||
#map-view {
|
||
flex: 1;
|
||
position: relative;
|
||
overflow: hidden;
|
||
display: flex;
|
||
}
|
||
|
||
#map {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.map-actions {
|
||
position: absolute;
|
||
top: 14px;
|
||
right: 14px;
|
||
z-index: 500;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.map-btn {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 8px 14px;
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow);
|
||
transition: all .15s;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.map-btn:hover {
|
||
background: var(--light);
|
||
border-color: var(--green);
|
||
}
|
||
|
||
.map-btn.green {
|
||
background: var(--green);
|
||
color: #fff;
|
||
border-color: var(--green);
|
||
}
|
||
|
||
.map-btn.green:hover {
|
||
background: var(--green-l);
|
||
}
|
||
|
||
#map-container.picking {
|
||
cursor: crosshair !important;
|
||
}
|
||
|
||
#map-container.picking::after {
|
||
content: '📍 Klik di peta untuk pilih lokasi';
|
||
position: absolute;
|
||
top: 14px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: var(--green);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
padding: 8px 20px;
|
||
border-radius: 20px;
|
||
z-index: 1000;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.panel {
|
||
display: none;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
background: var(--light);
|
||
}
|
||
|
||
.panel-inner {
|
||
padding: 24px;
|
||
max-width: 900px;
|
||
}
|
||
|
||
.panel h2 {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.panel .sub {
|
||
font-size: 13px;
|
||
color: var(--text2);
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.kpi-row {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.kpi {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 16px;
|
||
border-top: 3px solid transparent;
|
||
}
|
||
|
||
.kpi.green {
|
||
border-top-color: var(--green);
|
||
}
|
||
|
||
.kpi.red {
|
||
border-top-color: var(--red);
|
||
}
|
||
|
||
.kpi.blue {
|
||
border-top-color: var(--blue);
|
||
}
|
||
|
||
.kpi.yellow {
|
||
border-top-color: #e9c46a;
|
||
}
|
||
|
||
.kpi .n {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.kpi.green .n {
|
||
color: var(--green);
|
||
}
|
||
|
||
.kpi.red .n {
|
||
color: var(--red);
|
||
}
|
||
|
||
.kpi.blue .n {
|
||
color: var(--blue);
|
||
}
|
||
|
||
.kpi.yellow .n {
|
||
color: #b5830a;
|
||
}
|
||
|
||
.kpi .l {
|
||
font-size: 12px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.card {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 18px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.card h3 {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 14px;
|
||
color: var(--text);
|
||
}
|
||
|
||
.card-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 14px;
|
||
}
|
||
|
||
.bar-chart {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 9px;
|
||
}
|
||
|
||
.br {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.bl {
|
||
font-size: 12px;
|
||
width: 130px;
|
||
color: var(--text2);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.bt {
|
||
flex: 1;
|
||
height: 8px;
|
||
background: var(--light);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bf {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width .6s;
|
||
}
|
||
|
||
.bv {
|
||
font-size: 11px;
|
||
color: var(--text2);
|
||
width: 22px;
|
||
text-align: right;
|
||
}
|
||
|
||
.tbl {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.tbl th {
|
||
padding: 9px 12px;
|
||
text-align: left;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .6px;
|
||
color: var(--text2);
|
||
border-bottom: 2px solid var(--border);
|
||
background: var(--light);
|
||
}
|
||
|
||
.tbl td {
|
||
padding: 10px 12px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.tbl tr:hover td {
|
||
background: var(--light);
|
||
}
|
||
|
||
.form-card {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 20px;
|
||
margin-bottom: 14px;
|
||
}
|
||
|
||
.form-card h3 {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
}
|
||
|
||
.form-full {
|
||
grid-column: 1/-1;
|
||
}
|
||
|
||
.fg {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
}
|
||
|
||
.fg label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.fg input,
|
||
.fg select,
|
||
.fg textarea {
|
||
padding: 9px 12px;
|
||
border: 1.5px solid var(--border);
|
||
border-radius: 6px;
|
||
font-size: 13px;
|
||
font-family: inherit;
|
||
outline: none;
|
||
background: var(--white);
|
||
color: var(--text);
|
||
}
|
||
|
||
.fg input:focus,
|
||
.fg select:focus,
|
||
.fg textarea:focus {
|
||
border-color: var(--green);
|
||
}
|
||
|
||
.fg textarea {
|
||
resize: vertical;
|
||
min-height: 70px;
|
||
}
|
||
|
||
.pick-loc {
|
||
padding: 10px 14px;
|
||
border: 1.5px dashed var(--border);
|
||
border-radius: 6px;
|
||
text-align: center;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: var(--text2);
|
||
transition: all .15s;
|
||
}
|
||
|
||
.pick-loc:hover {
|
||
border-color: var(--green);
|
||
color: var(--green);
|
||
}
|
||
|
||
.pick-loc.picked {
|
||
border-color: var(--green);
|
||
color: var(--green);
|
||
border-style: solid;
|
||
background: var(--green-ll);
|
||
}
|
||
|
||
.stepper {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 22px;
|
||
}
|
||
|
||
.step {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
flex: 1;
|
||
}
|
||
|
||
.step-n {
|
||
width: 26px;
|
||
height: 26px;
|
||
border-radius: 50%;
|
||
border: 2px solid var(--border);
|
||
background: var(--white);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: var(--text2);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.step.active .step-n {
|
||
background: var(--green);
|
||
border-color: var(--green);
|
||
color: #fff;
|
||
}
|
||
|
||
.step.done .step-n {
|
||
background: var(--green-l);
|
||
border-color: var(--green-l);
|
||
color: #fff;
|
||
}
|
||
|
||
.step-label {
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.step.active .step-label {
|
||
color: var(--green);
|
||
}
|
||
|
||
.step.done .step-label {
|
||
color: var(--green-l);
|
||
}
|
||
|
||
.step-line {
|
||
flex: 1;
|
||
height: 2px;
|
||
background: var(--border);
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.step-line.done {
|
||
background: var(--green-l);
|
||
}
|
||
|
||
.btn {
|
||
padding: 9px 20px;
|
||
border-radius: var(--r);
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
border: 1px solid transparent;
|
||
transition: all .15s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--green);
|
||
color: #fff;
|
||
border-color: var(--green);
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--green-l);
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: var(--white);
|
||
color: var(--text);
|
||
border-color: var(--border);
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: var(--light);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f8d7da;
|
||
color: var(--red);
|
||
border-color: #f5c6cb;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: var(--red);
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-confirm {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border-color: #c3e6cb;
|
||
}
|
||
|
||
.btn-confirm:hover {
|
||
background: #155724;
|
||
color: #fff;
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 5px 12px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.verify-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.vcard {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 16px;
|
||
}
|
||
|
||
.vcard-top {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.vcard-name {
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.vcard-loc {
|
||
font-size: 12px;
|
||
color: var(--text2);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.vcard-meta {
|
||
display: flex;
|
||
gap: 6px;
|
||
flex-wrap: wrap;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.pill {
|
||
font-size: 11.5px;
|
||
padding: 3px 9px;
|
||
border-radius: 8px;
|
||
background: var(--light);
|
||
color: var(--text2);
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.progress-wrap {
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.progress-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 11.5px;
|
||
color: var(--text2);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 6px;
|
||
background: var(--light);
|
||
border-radius: 3px;
|
||
overflow: hidden;
|
||
border: 1px solid var(--border);
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: var(--green);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.vcard-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tang-tbl {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
font-size: 12.5px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.tang-tbl th {
|
||
padding: 7px 10px;
|
||
text-align: left;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: .5px;
|
||
color: var(--text2);
|
||
border-bottom: 1.5px solid var(--border);
|
||
}
|
||
|
||
.tang-tbl td {
|
||
padding: 8px 10px;
|
||
border-bottom: 1px solid var(--border);
|
||
vertical-align: top;
|
||
}
|
||
|
||
.age-tag {
|
||
display: inline-block;
|
||
font-size: 10px;
|
||
font-weight: 700;
|
||
padding: 2px 7px;
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.rec-wrap {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 3px;
|
||
margin-top: 3px;
|
||
}
|
||
|
||
.rec {
|
||
font-size: 10px;
|
||
padding: 2px 6px;
|
||
border-radius: 6px;
|
||
background: var(--green-ll);
|
||
color: var(--green);
|
||
border: 1px solid #b7e4c7;
|
||
}
|
||
|
||
.rec.work {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
border-color: #b8daff;
|
||
}
|
||
|
||
.work-alert {
|
||
background: var(--green-ll);
|
||
border: 1px solid #b7e4c7;
|
||
border-radius: var(--r);
|
||
padding: 12px 16px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.work-alert h4 {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
color: var(--green);
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.ri-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||
gap: 12px;
|
||
}
|
||
|
||
.ri-card {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 16px;
|
||
cursor: pointer;
|
||
transition: all .15s;
|
||
}
|
||
|
||
.ri-card:hover {
|
||
border-color: var(--green);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.ri-card-top {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.ri-emoji {
|
||
font-size: 28px;
|
||
}
|
||
|
||
.ri-name {
|
||
font-size: 13.5px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.ri-type {
|
||
font-size: 11.5px;
|
||
color: var(--text2);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.ri-stats {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
.ri-s {
|
||
text-align: center;
|
||
background: var(--light);
|
||
border-radius: 6px;
|
||
padding: 6px 4px;
|
||
}
|
||
|
||
.ri-s .n {
|
||
font-size: 15px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.ri-s .l {
|
||
font-size: 9.5px;
|
||
color: var(--text2);
|
||
margin-top: 1px;
|
||
}
|
||
|
||
.cap-bar {
|
||
height: 4px;
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.cap-fill {
|
||
height: 100%;
|
||
background: var(--green);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.don-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.don-card {
|
||
background: var(--white);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--r);
|
||
padding: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
|
||
.don-icon {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.don-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.don-jumlah {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--green);
|
||
}
|
||
|
||
.don-meta {
|
||
font-size: 12px;
|
||
color: var(--text2);
|
||
margin-top: 2px;
|
||
}
|
||
|
||
#modal-ov {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0, 0, 0, .5);
|
||
z-index: 9999;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
#modal-ov.open {
|
||
display: flex;
|
||
}
|
||
|
||
.modal {
|
||
background: var(--white);
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
width: 580px;
|
||
max-width: 95vw;
|
||
max-height: 88vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 20px 60px rgba(0, 0, 0, .3);
|
||
}
|
||
|
||
.modal::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
|
||
.modal::-webkit-scrollbar-thumb {
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
}
|
||
|
||
.modal-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
flex: 1;
|
||
}
|
||
|
||
.modal-close {
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--border);
|
||
background: var(--light);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 16px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.modal-close:hover {
|
||
background: var(--border);
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
padding: 7px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
font-size: 13px;
|
||
}
|
||
|
||
.detail-key {
|
||
color: var(--text2);
|
||
width: 150px;
|
||
flex-shrink: 0;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.detail-val {
|
||
font-weight: 600;
|
||
}
|
||
|
||
#toast {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: #212529;
|
||
color: #fff;
|
||
padding: 10px 20px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
z-index: 99999;
|
||
opacity: 0;
|
||
transition: opacity .25s;
|
||
pointer-events: none;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#toast.show {
|
||
opacity: 1;
|
||
}
|
||
|
||
.leaflet-control-zoom a {
|
||
color: var(--text) !important;
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
border-radius: 10px !important;
|
||
box-shadow: var(--shadow) !important;
|
||
}
|
||
|
||
.pop {
|
||
min-width: 180px;
|
||
}
|
||
|
||
.pop h4 {
|
||
font-size: 13px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.pop p {
|
||
font-size: 12px;
|
||
color: var(--text2);
|
||
margin-bottom: 3px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.pop-acts {
|
||
display: flex;
|
||
gap: 6px;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.pop-btn {
|
||
flex: 1;
|
||
padding: 6px;
|
||
border-radius: 6px;
|
||
font-size: 11.5px;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
border: none;
|
||
text-align: center;
|
||
}
|
||
|
||
.pop-btn-b {
|
||
background: #cce5ff;
|
||
color: #004085;
|
||
}
|
||
|
||
.pop-btn-g {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.spinner {
|
||
display: inline-block;
|
||
width: 16px;
|
||
height: 16px;
|
||
border: 2px solid var(--border);
|
||
border-top-color: var(--green);
|
||
border-radius: 50%;
|
||
animation: spin .7s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.loading {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 20px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.empty {
|
||
text-align: center;
|
||
padding: 50px 20px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.empty .ei {
|
||
font-size: 40px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.empty p {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.user-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.user-av {
|
||
width: 34px;
|
||
height: 34px;
|
||
border-radius: 50%;
|
||
background: var(--green-ll);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
font-weight: 700;
|
||
color: var(--green);
|
||
}
|
||
|
||
.user-info {
|
||
flex: 1;
|
||
}
|
||
|
||
.user-name-2 {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.user-email {
|
||
font-size: 11.5px;
|
||
color: var(--text2);
|
||
}
|
||
|
||
.inactive {
|
||
opacity: .5;
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<div id="login-page">
|
||
<div class="login-box">
|
||
<div class="login-logo">
|
||
<div class="icon">🗺️</div>
|
||
<h1>WebGIS Poverty Mapping</h1>
|
||
<p>Sistem Pemetaan Kemiskinan (PHP Native)</p>
|
||
</div>
|
||
<div class="form-group"><label>Email</label><input type="email" id="l-email" placeholder="admin@webgis.com"
|
||
onkeydown="if(event.key==='Enter')doLogin()"></div>
|
||
<div class="form-group"><label>Password</label><input type="password" id="l-pass" placeholder="admin123"
|
||
onkeydown="if(event.key==='Enter')doLogin()"></div>
|
||
<button class="btn-login" onclick="doLogin()">Masuk</button>
|
||
<div class="login-err" id="login-err"></div>
|
||
<div class="register-link">Belum punya akun? <a onclick="showRegister()">Daftar Akun</a></div>
|
||
|
||
<div class="login-demo">
|
||
<p>Akun Demo:</p>
|
||
<button class="demo-btn" onclick="fillLogin('superadmin@webgis.id','admin123')"><strong>Super
|
||
Admin</strong>
|
||
— superadmin@webgis.id / admin123</button>
|
||
<button class="demo-btn" onclick="fillLogin('admin.jaksel@webgis.id','admin123')"><strong>Admin
|
||
Daerah</strong> — admin.jaksel@webgis.id / admin123</button>
|
||
<button class="demo-btn" onclick="fillLogin('ahmad@masjid.id','pengurus123')"><strong>Pengurus
|
||
RI</strong> —
|
||
ahmad@masjid.id / pengurus123</button>
|
||
<button class="demo-btn" onclick="fillLogin('budi@donatur.id','donatur123')"><strong>Donatur</strong> —
|
||
budi@donatur.id / donatur123</button>
|
||
<button class="demo-btn" onclick="fillLogin('sari@gmail.com','user123')"><strong>Masyarakat</strong> —
|
||
sari@gmail.com / user123</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="register-page">
|
||
<div class="login-box">
|
||
<div class="login-logo">
|
||
<div class="icon">📝</div>
|
||
<h1>Daftar Akun Baru</h1>
|
||
</div>
|
||
<div class="form-group"><label>Nama Lengkap</label><input type="text" id="r-nama"
|
||
placeholder="Nama Anda..."></div>
|
||
<div class="form-group"><label>Email</label><input type="email" id="r-email" placeholder="email@contoh.com">
|
||
</div>
|
||
<div class="form-group"><label>Password</label><input type="password" id="r-pass"
|
||
placeholder="Min. 6 karakter"></div>
|
||
<div class="form-group"><label>Wilayah</label><input type="text" id="r-wilayah"
|
||
placeholder="Kelurahan/Kecamatan..."></div>
|
||
<button class="btn-login" onclick="doRegister()">Daftar</button>
|
||
<div class="login-err" id="reg-err"></div>
|
||
<div class="register-link"><a onclick="showLogin()">← Kembali ke Login</a></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="app" style="display:none;flex-direction:column;height:100vh">
|
||
|
||
<div id="topbar">
|
||
<div class="topbar-logo">WebGIS <span>Poverty Mapping</span></div>
|
||
<div class="nav" id="nav-tabs">
|
||
<button class="nav-btn active" onclick="goView('map')" id="t-map">🗺️ Peta</button>
|
||
<button class="nav-btn" onclick="goView('dashboard')" id="t-dashboard">📊 Dashboard</button>
|
||
<button class="nav-btn" onclick="goView('laporan')" id="t-laporan"
|
||
data-roles="super_admin,admin_daerah,pengurus_ri,masyarakat">📝 Laporan</button>
|
||
<button class="nav-btn" onclick="goView('verifikasi')" id="t-verifikasi"
|
||
data-roles="super_admin,admin_daerah,masyarakat">✅ Verifikasi</button>
|
||
<button class="nav-btn" onclick="goView('ri')" id="t-ri">🕌 Rumah Ibadah</button>
|
||
<button class="nav-btn" onclick="goView('program')" id="t-program"
|
||
data-roles="super_admin,admin_daerah,pengurus_ri">📦 Program</button>
|
||
<button class="nav-btn" onclick="goView('donasi')" id="t-donasi"
|
||
data-roles="super_admin,admin_daerah,donatur">💚 Donasi</button>
|
||
<button class="nav-btn" onclick="goView('users')" id="t-users" data-roles="super_admin">👥
|
||
Pengguna</button>
|
||
</div>
|
||
<div class="topbar-right">
|
||
<span class="role-badge" id="role-badge"></span>
|
||
<div class="user-chip" onclick="goView('profile')">
|
||
<div class="user-avatar" id="user-av">?</div>
|
||
<span class="user-name" id="user-name">—</span>
|
||
</div>
|
||
<button class="logout-btn" onclick="doLogout()">Keluar</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="main">
|
||
|
||
<div id="sidebar">
|
||
<div class="sb-stats">
|
||
<h3>Ringkasan</h3>
|
||
<div class="stat-row">
|
||
<div class="stat-box red">
|
||
<div class="n" id="sb-total">—</div>
|
||
<div class="l">Total KK Miskin</div>
|
||
</div>
|
||
<div class="stat-box green">
|
||
<div class="n" id="sb-ver">—</div>
|
||
<div class="l">Terverifikasi</div>
|
||
</div>
|
||
<div class="stat-box blue">
|
||
<div class="n" id="sb-ri">—</div>
|
||
<div class="l">Rumah Ibadah</div>
|
||
</div>
|
||
<div class="stat-box yellow">
|
||
<div class="n" id="sb-cov">—%</div>
|
||
<div class="l">Cakupan RI</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="layer-ctrl">
|
||
<h3>Layer Peta</h3>
|
||
<div class="layer-row" onclick="toggleLayer('poor')">
|
||
<div class="layer-icon" style="background:#f8d7da">🏠</div><span class="layer-name">Rumah
|
||
Miskin</span><span class="layer-cnt" id="lc-poor">0</span>
|
||
<div class="toggle on" id="lt-poor"></div>
|
||
</div>
|
||
<div class="layer-row" onclick="toggleLayer('ri')">
|
||
<div class="layer-icon" style="background:#cce5ff">🕌</div><span class="layer-name">Rumah
|
||
Ibadah</span><span class="layer-cnt" id="lc-ri">0</span>
|
||
<div class="toggle on" id="lt-ri"></div>
|
||
</div>
|
||
<div class="layer-row" onclick="toggleLayer('radius')">
|
||
<div class="layer-icon" style="background:#d4edda">⭕</div><span class="layer-name">Radius
|
||
Coverage</span><span class="layer-cnt" id="lc-rad">0</span>
|
||
<div class="toggle on" id="lt-radius"></div>
|
||
</div>
|
||
</div>
|
||
<div class="sb-list">
|
||
<div class="sb-hdr">Data Terbaru</div>
|
||
<div id="sb-list-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="map-view">
|
||
<div id="map-container" style="flex:1;position:relative">
|
||
<div id="map"></div>
|
||
<div class="map-actions">
|
||
<button class="map-btn green" onclick="goView('laporan')" id="btn-lapor">+ Lapor
|
||
Keluarga</button>
|
||
<button class="map-btn" onclick="loadMap()">🔄 Refresh</button>
|
||
<button class="map-btn" onclick="locateMe()">📍 Lokasi Saya</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-dashboard">
|
||
<div class="panel-inner">
|
||
<h2>📊 Dashboard</h2>
|
||
<div class="sub">Statistik real-time dari MySQL API</div>
|
||
<div class="kpi-row">
|
||
<div class="kpi red">
|
||
<div class="n" id="kpi-total">—</div>
|
||
<div class="l">Total KK Dilaporkan</div>
|
||
</div>
|
||
<div class="kpi green">
|
||
<div class="n" id="kpi-ver">—</div>
|
||
<div class="l">Terverifikasi</div>
|
||
</div>
|
||
<div class="kpi blue">
|
||
<div class="n" id="kpi-ri">—</div>
|
||
<div class="l">Rumah Ibadah</div>
|
||
</div>
|
||
<div class="kpi yellow">
|
||
<div class="n" id="kpi-cov">—%</div>
|
||
<div class="l">Cakupan RI</div>
|
||
</div>
|
||
</div>
|
||
<div class="card-grid">
|
||
<div class="card">
|
||
<h3>Distribusi per Kelurahan</h3>
|
||
<div id="dash-kel" class="bar-chart"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>⚠️ KK Tanpa Rumah Ibadah</h3>
|
||
<div id="dash-urgent"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Status Verifikasi</h3>
|
||
<div id="dash-status" class="bar-chart"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>Usia Tanggungan</h3>
|
||
<div id="dash-age" class="bar-chart"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-laporan">
|
||
<div class="panel-inner">
|
||
<h2>📝 Laporan Keluarga Miskin</h2>
|
||
<div class="sub">Data langsung tersimpan ke database MySQL</div>
|
||
|
||
<div class="stepper">
|
||
<div class="step active" id="sp-1">
|
||
<div class="step-n">1</div>
|
||
<div class="step-label">Lokasi</div>
|
||
</div>
|
||
<div class="step-line" id="sl-1"></div>
|
||
<div class="step" id="sp-2">
|
||
<div class="step-n">2</div>
|
||
<div class="step-label">Profil</div>
|
||
</div>
|
||
<div class="step-line" id="sl-2"></div>
|
||
<div class="step" id="sp-3">
|
||
<div class="step-n">3</div>
|
||
<div class="step-label">Kondisi</div>
|
||
</div>
|
||
<div class="step-line" id="sl-3"></div>
|
||
<div class="step" id="sp-4">
|
||
<div class="step-n">4</div>
|
||
<div class="step-label">Tanggungan</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="fs-1">
|
||
<div class="form-card">
|
||
<h3>📍 Lokasi Rumah Tangga</h3>
|
||
<div class="form-grid">
|
||
<div class="fg form-full"><label>Pilih Titik di Peta</label>
|
||
<div class="pick-loc" id="pick-btn" onclick="startPick()">🗺️ Klik untuk pilih
|
||
koordinat dari peta</div><small id="coord-info"
|
||
style="color:var(--text2);font-size:11.5px;margin-top:4px">Belum dipilih</small>
|
||
</div>
|
||
<div class="fg form-full"><label>Alamat Lengkap</label><input type="text" id="f-alamat"
|
||
placeholder="Jl. Nama RT/RW..." /></div>
|
||
<div class="fg"><label>Kelurahan</label><input type="text" id="f-kel"
|
||
placeholder="Nama Kelurahan..." /></div>
|
||
<div class="fg"><label>Kecamatan</label><input type="text" id="f-kec"
|
||
placeholder="Nama Kecamatan..." /></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end"><button class="btn btn-primary"
|
||
onclick="goStep(2)">Lanjut →</button></div>
|
||
</div>
|
||
|
||
<div id="fs-2" style="display:none">
|
||
<div class="form-card">
|
||
<h3>👤 Profil Rumah Tangga</h3>
|
||
<div class="form-grid">
|
||
<div class="fg"><label>Nama Kepala Keluarga</label><input type="text" id="f-nama"
|
||
placeholder="Nama lengkap..." /></div>
|
||
<div class="fg"><label>Jumlah Anggota</label><input type="number" id="f-anggota" min="1"
|
||
max="20" placeholder="Jumlah orang" /></div>
|
||
<div class="fg"><label>Estimasi Penghasilan/Bulan</label><select id="f-peng">
|
||
<option>Tidak berpenghasilan</option>
|
||
<option>Di bawah Rp 500.000</option>
|
||
<option>Rp 500.000 – Rp 1.000.000</option>
|
||
<option>Rp 1.000.000 – Rp 2.000.000</option>
|
||
</select></div>
|
||
<div class="fg"><label>Kebutuhan Mendesak</label><select id="f-keb">
|
||
<option>Pangan</option>
|
||
<option>Sandang</option>
|
||
<option>Pendidikan</option>
|
||
<option>Kesehatan</option>
|
||
<option>Modal usaha</option>
|
||
<option>Perbaikan rumah</option>
|
||
</select></div>
|
||
<div class="fg form-full"><label>Catatan</label><textarea id="f-catatan"
|
||
placeholder="Kondisi singkat..."></textarea></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between"><button class="btn btn-secondary"
|
||
onclick="goStep(1)">← Kembali</button><button class="btn btn-primary"
|
||
onclick="goStep(3)">Lanjut →</button></div>
|
||
</div>
|
||
|
||
<div id="fs-3" style="display:none">
|
||
<div class="form-card">
|
||
<h3>🏠 Kondisi Hunian</h3>
|
||
<div class="form-grid">
|
||
<div class="fg"><label>Status Kepemilikan</label><select id="f-srumah">
|
||
<option>Tidak memiliki (menumpang)</option>
|
||
<option>Kontrak / sewa</option>
|
||
<option>Milik sendiri (tidak layak)</option>
|
||
<option>Milik sendiri (layak)</option>
|
||
</select></div>
|
||
<div class="fg"><label>Kondisi Bangunan</label><select id="f-kondisi">
|
||
<option>Sangat tidak layak (darurat)</option>
|
||
<option>Tidak layak huni</option>
|
||
<option>Kurang layak</option>
|
||
<option>Cukup layak</option>
|
||
</select></div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between"><button class="btn btn-secondary"
|
||
onclick="goStep(2)">← Kembali</button><button class="btn btn-primary"
|
||
onclick="goStep(4)">Lanjut →</button></div>
|
||
</div>
|
||
|
||
<div id="fs-4" style="display:none">
|
||
<div class="form-card">
|
||
<h3>👨👩👧👦 Data Tanggungan</h3>
|
||
<div style="font-size:12.5px;color:var(--text2);margin-bottom:14px">Sistem akan menganalisis
|
||
usia dan memberikan rekomendasi program yang sesuai untuk setiap anggota keluarga.</div>
|
||
<div
|
||
style="background:var(--light);border:1px solid var(--border);border-radius:var(--r);padding:14px;margin-bottom:14px">
|
||
<div
|
||
style="font-size:12px;font-weight:700;color:var(--text2);text-transform:uppercase;letter-spacing:.6px;margin-bottom:10px">
|
||
+ Tambah Anggota</div>
|
||
<div class="form-grid">
|
||
<div class="fg"><label>Nama</label><input type="text" id="t-nama"
|
||
placeholder="Nama..." /></div>
|
||
<div class="fg"><label>Tanggal Lahir</label><input type="date" id="t-tl" /></div>
|
||
<div class="fg"><label>Jenis Kelamin</label><select id="t-jk">
|
||
<option value="L">Laki-laki</option>
|
||
<option value="P">Perempuan</option>
|
||
</select></div>
|
||
<div class="fg"><label>Hubungan dg KK</label><select id="t-hub">
|
||
<option>Kepala Keluarga</option>
|
||
<option>Istri/Suami</option>
|
||
<option>Anak</option>
|
||
<option>Orang Tua</option>
|
||
<option>Saudara</option>
|
||
<option>Lainnya</option>
|
||
</select></div>
|
||
<div class="fg"><label>Pendidikan</label><select id="t-pend">
|
||
<option>Belum sekolah</option>
|
||
<option>SD</option>
|
||
<option>SMP</option>
|
||
<option>SMA/SMK</option>
|
||
<option>D3/S1</option>
|
||
</select></div>
|
||
<div class="fg"><label>Pekerjaan</label><input type="text" id="t-kerja"
|
||
placeholder="Pelajar / Tidak bekerja..." /></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:10px"><button
|
||
class="btn btn-primary btn-sm" onclick="addTang()">+ Tambahkan</button></div>
|
||
</div>
|
||
<div id="tang-preview"></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<button class="btn btn-secondary" onclick="goStep(3)">← Kembali</button>
|
||
<button class="btn btn-primary" style="background:#1b4332" onclick="submitLaporan()">✅ Kirim
|
||
Laporan ke Database</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-verifikasi">
|
||
<div class="panel-inner">
|
||
<h2>✅ Verifikasi Komunitas</h2>
|
||
<div class="sub">Minimum 2 konfirmasi komunitas diperlukan sebelum admin dapat memverifikasi final
|
||
</div>
|
||
<div id="verif-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-ri">
|
||
<div class="panel-inner">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||
<h2 style="margin:0">🕌 Rumah Ibadah</h2><button class="btn btn-primary btn-sm"
|
||
onclick="openAddRI()" id="btn-add-ri" style="display:none">+ Tambah</button>
|
||
</div>
|
||
<div class="sub">Agen bantuan sosial berbasis komunitas</div>
|
||
<div class="ri-grid" id="ri-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-program">
|
||
<div class="panel-inner">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||
<h2 style="margin:0">📦 Program Bantuan</h2><button class="btn btn-primary btn-sm"
|
||
onclick="openAddProgram()">+ Buat Program</button>
|
||
</div>
|
||
<div class="sub">Distribusi bantuan dari Rumah Ibadah ke penerima manfaat</div>
|
||
<div id="prog-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-donasi">
|
||
<div class="panel-inner">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
|
||
<h2 style="margin:0">💚 Donasi</h2><button class="btn btn-primary btn-sm"
|
||
onclick="openAddDonasi()" id="btn-add-don">+ Donasi Baru</button>
|
||
</div>
|
||
<div class="sub">Riwayat donasi dari donatur ke Rumah Ibadah</div>
|
||
<div id="don-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-users">
|
||
<div class="panel-inner">
|
||
<h2>👥 Manajemen Pengguna</h2>
|
||
<div class="sub">Kelola akses semua pengguna sistem</div>
|
||
<div class="card" id="users-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel" id="v-profile">
|
||
<div class="panel-inner">
|
||
<h2>👤 Profil Saya</h2>
|
||
<div class="sub">Informasi akun yang sedang login</div>
|
||
<div class="card" style="max-width:420px" id="profile-body"></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
<div id="modal-ov" onclick="closeModal(event)">
|
||
<div class="modal" id="modal-body"></div>
|
||
</div>
|
||
|
||
<div id="toast"></div>
|
||
|
||
<script>
|
||
// ════════════════════════════════════════════
|
||
// STATE & API SETUP (PHP NATIVE)
|
||
// ════════════════════════════════════════════
|
||
const S = {
|
||
user: null, pickCoords: null, isPicking: false, step: 1,
|
||
newTang: [], layers: { poor: true, ri: true, radius: true }, allRT: [], allRI: [],
|
||
};
|
||
|
||
// Fungsi fetch yang telah disesuaikan dengan PHP router (?route=)
|
||
async function api(path, opts = {}) {
|
||
const r = await fetch('api.php?route=' + path, {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
...opts
|
||
});
|
||
const text = await r.text(); // Tangkap error HTML jika ada dari PHP
|
||
try {
|
||
const data = JSON.parse(text);
|
||
if (!r.ok) throw new Error(data.error || r.statusText);
|
||
return data;
|
||
} catch (e) {
|
||
console.error('API Error Response:', text);
|
||
throw new Error(r.ok ? e.message : text.substring(0, 50));
|
||
}
|
||
}
|
||
const GET = p => api(p);
|
||
const POST = (p, b) => api(p, { method: 'POST', body: JSON.stringify(b) });
|
||
|
||
// ════════════════════════════════════════════
|
||
// AUTH
|
||
// ════════════════════════════════════════════
|
||
function fillLogin(e, p) { document.getElementById('l-email').value = e; document.getElementById('l-pass').value = p; }
|
||
function showRegister() { document.getElementById('login-page').style.display = 'none'; document.getElementById('register-page').style.display = 'flex'; }
|
||
function showLogin() { document.getElementById('register-page').style.display = 'none'; document.getElementById('login-page').style.display = 'flex'; }
|
||
|
||
async function doLogin() {
|
||
const email = document.getElementById('l-email').value.trim();
|
||
const pw = document.getElementById('l-pass').value;
|
||
document.getElementById('login-err').textContent = '';
|
||
try {
|
||
const u = await POST('/login', { email, password: pw });
|
||
S.user = u;
|
||
startApp();
|
||
} catch (e) { document.getElementById('login-err').textContent = e.message; }
|
||
}
|
||
|
||
async function doRegister() {
|
||
const nama = document.getElementById('r-nama').value.trim();
|
||
const email = document.getElementById('r-email').value.trim();
|
||
const pw = document.getElementById('r-pass').value;
|
||
const wilayah = document.getElementById('r-wilayah').value.trim();
|
||
document.getElementById('reg-err').textContent = '';
|
||
try {
|
||
const u = await POST('/register', { nama, email, password: pw, wilayah });
|
||
S.user = u;
|
||
startApp();
|
||
} catch (e) { document.getElementById('reg-err').textContent = e.message; }
|
||
}
|
||
|
||
async function doLogout() {
|
||
await POST('/logout', {});
|
||
S.user = null;
|
||
document.getElementById('app').style.display = 'none';
|
||
document.getElementById('login-page').style.display = 'flex';
|
||
document.getElementById('l-pass').value = '';
|
||
}
|
||
|
||
function getRoleLabel(role) {
|
||
const labels = { 'super_admin': 'Super Admin', 'admin_daerah': 'Admin Daerah', 'pengurus_ri': 'Pengurus Rumah Ibadah', 'donatur': 'Donatur', 'masyarakat': 'Masyarakat Umum' };
|
||
return labels[role] || role;
|
||
}
|
||
|
||
function startApp() {
|
||
document.getElementById('login-page').style.display = 'none';
|
||
document.getElementById('register-page').style.display = 'none';
|
||
document.getElementById('app').style.display = 'flex';
|
||
|
||
document.getElementById('user-name').textContent = S.user.nama;
|
||
document.getElementById('user-av').textContent = S.user.nama[0].toUpperCase();
|
||
|
||
const rb = document.getElementById('role-badge');
|
||
rb.textContent = getRoleLabel(S.user.role);
|
||
rb.className = 'role-badge rb-' + S.user.role;
|
||
|
||
document.querySelectorAll('#nav-tabs .nav-btn[data-roles]').forEach(btn => {
|
||
const roles = btn.getAttribute('data-roles').split(',');
|
||
btn.style.display = roles.includes(S.user.role) ? '' : 'none';
|
||
});
|
||
|
||
if (['super_admin', 'admin_daerah'].includes(S.user.role)) document.getElementById('btn-add-ri').style.display = '';
|
||
if (S.user.role === 'donatur') document.getElementById('btn-lapor').style.display = 'none';
|
||
|
||
if (!window._mapInited) { initMap(); window._mapInited = true; }
|
||
goView('map'); loadMap(); loadSidebarStats();
|
||
}
|
||
|
||
// ════════════════════════════════════════════
|
||
// MAP & UI LOGIC (TIDAK ADA PERUBAHAN)
|
||
// ════════════════════════════════════════════
|
||
let map, poorLayer, riLayer, radLayer;
|
||
|
||
function initMap() {
|
||
// Memusatkan peta di Pontianak
|
||
map = L.map('map').setView([-0.0227, 109.3340], 13);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 }).addTo(map);
|
||
poorLayer = L.layerGroup().addTo(map); riLayer = L.layerGroup().addTo(map); radLayer = L.layerGroup().addTo(map);
|
||
map.on('click', onMapClick);
|
||
}
|
||
|
||
const sColor = s => ({ verified: '#2d6a4f', pending: '#e9c46a', rejected: '#c1121f', dispute: '#6610f2' })[s] || '#6c757d';
|
||
const riColor = t => ({ Masjid: '#457b9d', Gereja: '#e07a5f', Pura: '#9b72aa', Vihara: '#81b29a', Kelenteng: '#f2cc8f' })[t] || '#457b9d';
|
||
const riEmoji = t => ({ Masjid: '🕌', Gereja: '⛪', Pura: '🛕', Vihara: '☸️', Kelenteng: '🏮' })[t] || '🕌';
|
||
|
||
function mkIcon(emoji, color, size = 36) {
|
||
return L.divIcon({
|
||
html: `<div style="width:${size}px;height:${size}px;border-radius:50% 50% 50% 0;background:${color};transform:rotate(-45deg);border:2px solid rgba(255,255,255,.6);box-shadow:0 2px 8px rgba(0,0,0,.25);display:flex;align-items:center;justify-content:center"><span style="transform:rotate(45deg);font-size:${Math.round(size * .4)}px">${emoji}</span></div>`,
|
||
className: '', iconSize: [size, size], iconAnchor: [size / 2, size], popupAnchor: [0, -size]
|
||
});
|
||
}
|
||
|
||
async function loadMap() {
|
||
try {
|
||
const [rts, ris] = await Promise.all([GET('/rumah-tangga'), GET('/rumah-ibadah')]);
|
||
S.allRT = rts; S.allRI = ris;
|
||
renderMarkers(rts, ris); renderSidebar(rts, ris);
|
||
await loadSidebarStats();
|
||
} catch (e) { toast('⚠️ Gagal memuat peta: ' + e.message); }
|
||
}
|
||
|
||
function renderMarkers(rts, ris) {
|
||
poorLayer.clearLayers(); riLayer.clearLayers(); radLayer.clearLayers();
|
||
const sLbl = { verified: 'Terverifikasi', pending: 'Menunggu', rejected: 'Ditolak', dispute: 'Sengketa' };
|
||
|
||
rts.forEach(h => {
|
||
const col = sColor(h.status);
|
||
const m = L.marker([h.lat, h.lng], { icon: mkIcon('🏠', col) }).bindPopup(`<div class="pop"><h4>🏠 ${h.nama_kk}</h4><p>📍 ${h.kelurahan || '—'}</p><p>Status: <b style="color:${col}">${sLbl[h.status] || h.status}</b></p><p>Konfirmasi: ${h.confirmations}/2 · ${h.ri_nama ? '🕌 ' + h.ri_nama : '⚠️ Belum ada RI'}</p><div class="pop-acts"><div class="pop-btn pop-btn-b" onclick="openDetailRT(${h.id})">Detail</div><div class="pop-btn pop-btn-g" onclick="quickConfirm(${h.id})">Konfirmasi</div></div></div>`);
|
||
if (S.layers.poor) poorLayer.addLayer(m);
|
||
});
|
||
|
||
ris.forEach(r => {
|
||
const col = riColor(r.tipe); const em = riEmoji(r.tipe);
|
||
const m = L.marker([r.lat, r.lng], { icon: mkIcon(em, col, 40) }).bindPopup(`<div class="pop"><h4>${em} ${r.nama}</h4><p>🏛️ ${r.tipe}</p><p>📏 Radius: ${r.radius}m</p><div class="pop-acts"><div class="pop-btn pop-btn-b" onclick="openDetailRI(${r.id})">Detail</div></div></div>`);
|
||
if (S.layers.ri) riLayer.addLayer(m);
|
||
const circle = L.circle([r.lat, r.lng], { radius: r.radius, color: col, fillColor: col, fillOpacity: .06, weight: 1.5, dashArray: '5,4' });
|
||
if (S.layers.radius) radLayer.addLayer(circle);
|
||
});
|
||
|
||
document.getElementById('lc-poor').textContent = rts.length;
|
||
document.getElementById('lc-ri').textContent = ris.length;
|
||
document.getElementById('lc-rad').textContent = ris.length;
|
||
}
|
||
|
||
function renderSidebar(rts, ris) {
|
||
const sLbl = { verified: 'Terverifikasi', pending: 'Menunggu', rejected: 'Ditolak', dispute: 'Sengketa' };
|
||
document.getElementById('sb-list-body').innerHTML = rts.slice(0, 6).map(h => `<div class="sb-item" onclick="flyTo(${h.lat},${h.lng})"><div class="sb-item-icon" style="background:${sColor(h.status)}22">🏠</div><div class="sb-item-info"><div class="sb-item-name">${h.nama_kk}</div><div class="sb-item-sub">${h.kelurahan || '—'} · ${h.anggota} org</div></div><span class="badge badge-${h.status}">${sLbl[h.status] || h.status}</span></div>`).join('') +
|
||
ris.slice(0, 4).map(r => `<div class="sb-item" onclick="flyTo(${r.lat},${r.lng})"><div class="sb-item-icon" style="background:${riColor(r.tipe)}22">${riEmoji(r.tipe)}</div><div class="sb-item-info"><div class="sb-item-name">${r.nama}</div><div class="sb-item-sub">${r.tipe} · R=${r.radius}m</div></div><span class="badge badge-aktif">Aktif</span></div>`).join('');
|
||
}
|
||
|
||
async function loadSidebarStats() {
|
||
try {
|
||
const s = await GET('/stats');
|
||
['sb-total', 'kpi-total'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = s.total; });
|
||
['sb-ver', 'kpi-ver'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = s.verified; });
|
||
['sb-ri', 'kpi-ri'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = s.ri_count; });
|
||
['sb-cov', 'kpi-cov'].forEach(id => { const el = document.getElementById(id); if (el) el.textContent = s.pct_covered + '%'; });
|
||
} catch (e) { }
|
||
}
|
||
|
||
function toggleLayer(n) { S.layers[n] = !S.layers[n]; document.getElementById('lt-' + n).classList.toggle('on', S.layers[n]); if (n === 'poor') { S.layers.poor ? map.addLayer(poorLayer) : map.removeLayer(poorLayer); } else if (n === 'ri') { S.layers.ri ? map.addLayer(riLayer) : map.removeLayer(riLayer); } else if (n === 'radius') { S.layers.radius ? map.addLayer(radLayer) : map.removeLayer(radLayer); } }
|
||
|
||
function onMapClick(e) {
|
||
if (S.isPickingRI) { S.pickCoordsRI = e.latlng; S.isPickingRI = false; document.getElementById('map-container').classList.remove('picking'); toast('📍 Koordinat Rumah Ibadah dipilih!'); openAddRI(); return; }
|
||
if (!S.isPicking) return;
|
||
S.pickCoords = e.latlng; S.isPicking = false; document.getElementById('map-container').classList.remove('picking');
|
||
document.getElementById('pick-btn').className = 'pick-loc picked'; document.getElementById('pick-btn').innerHTML = `✅ ${e.latlng.lat.toFixed(5)}, ${e.latlng.lng.toFixed(5)}`; document.getElementById('coord-info').textContent = `Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}`;
|
||
goView('laporan'); toast('📍 Koordinat dipilih!');
|
||
}
|
||
|
||
function startPick() { S.isPicking = true; goView('map'); document.getElementById('map-container').classList.add('picking'); toast('Klik lokasi di peta...'); }
|
||
function flyTo(lat, lng) { goView('map'); setTimeout(() => map.flyTo([lat, lng], 16, { duration: 1 }), 100); }
|
||
function locateMe() { if (!navigator.geolocation) return; navigator.geolocation.getCurrentPosition(p => { map.flyTo([p.coords.latitude, p.coords.longitude], 16); toast('📍 Lokasi ditemukan!'); }); }
|
||
|
||
function goView(v) {
|
||
const views = ['map', 'dashboard', 'laporan', 'verifikasi', 'ri', 'program', 'donasi', 'users', 'profile'];
|
||
views.forEach(x => {
|
||
const el = document.getElementById('v-' + x) || document.getElementById(x === 'map' ? 'map-view' : null);
|
||
const tab = document.getElementById('t-' + x);
|
||
if (x === 'map') { document.getElementById('map-view').style.display = v === 'map' ? 'flex' : 'none'; }
|
||
else { const p = document.getElementById('v-' + x); if (p) { p.style.display = v === x ? 'block' : 'none'; } }
|
||
if (tab) { if (v === x) tab.classList.add('active'); else tab.classList.remove('active'); }
|
||
});
|
||
document.getElementById('sidebar').style.display = v === 'map' ? 'flex' : 'none';
|
||
if (v === 'dashboard') loadDashboard(); if (v === 'verifikasi') loadVerifikasi(); if (v === 'ri') loadRI(); if (v === 'program') loadProgram(); if (v === 'donasi') loadDonasi(); if (v === 'users') loadUsers(); if (v === 'profile') renderProfile();
|
||
}
|
||
|
||
// ── DASHBOARD ──
|
||
const COLORS = ['#2d6a4f', '#457b9d', '#e9c46a', '#e07a5f', '#9b72aa', '#81b29a'];
|
||
async function loadDashboard() {
|
||
try {
|
||
const s = await GET('/stats');
|
||
document.getElementById('kpi-total').textContent = s.total; document.getElementById('kpi-ver').textContent = s.verified; document.getElementById('kpi-ri').textContent = s.ri_count; document.getElementById('kpi-cov').textContent = s.pct_covered + '%';
|
||
const barHTML = (items, labelKey, valKey, colorArr) => { const max = Math.max(...items.map(x => x[valKey]), 1); return items.map((x, i) => `<div class="br"><div class="bl" title="${x[labelKey]}">${x[labelKey]}</div><div class="bt"><div class="bf" style="width:${x[valKey] / max * 100}%;background:${colorArr[i % colorArr.length]}"></div></div><div class="bv">${x[valKey]}</div></div>`).join(''); };
|
||
document.getElementById('dash-kel').innerHTML = barHTML(s.by_kelurahan, 'kelurahan', 'n', COLORS);
|
||
const uncov = S.allRT.filter(h => !h.assigned_ri);
|
||
document.getElementById('dash-urgent').innerHTML = uncov.length === 0 ? '<div style="color:var(--green);font-size:13px;font-weight:600;padding:8px 0">✅ Semua keluarga sudah terjangkau!</div>' : uncov.map(h => `<div style="display:flex;align-items:center;gap:8px;padding:7px 0;border-bottom:1px solid var(--border)"><span style="font-size:13px;font-weight:600;flex:1">${h.nama_kk}</span><span style="font-size:11.5px;color:var(--text2)">${h.kelurahan || '—'}</span><span class="badge badge-${h.status}">${h.status}</span></div>`).join('');
|
||
const sLbl = { verified: 'Terverifikasi', pending: 'Menunggu', dispute: 'Sengketa', rejected: 'Ditolak' }; const sCol = { verified: COLORS[0], pending: '#e9c46a', dispute: '#9b72aa', rejected: '#c1121f' }; const total = s.by_status.reduce((a, x) => a + x.n, 0) || 1;
|
||
document.getElementById('dash-status').innerHTML = s.by_status.map(x => `<div class="br"><div class="bl">${sLbl[x.status] || x.status}</div><div class="bt"><div class="bf" style="width:${x.n / total * 100}%;background:${sCol[x.status] || '#ccc'}"></div></div><div class="bv">${x.n}</div></div>`).join('');
|
||
} catch (e) { toast('⚠️ Gagal load dashboard'); }
|
||
}
|
||
|
||
// ── VERIFIKASI ──
|
||
async function loadVerifikasi() {
|
||
document.getElementById('verif-body').innerHTML = '<div class="loading"><div class="spinner"></div> Memuat...</div>';
|
||
try {
|
||
const rts = await GET('/rumah-tangga');
|
||
const pend = rts.filter(h => h.status === 'pending' || h.status === 'dispute'); const verif = rts.filter(h => h.status === 'verified');
|
||
const sLbl = { verified: 'Terverifikasi', pending: 'Menunggu', dispute: 'Sengketa', rejected: 'Ditolak' }; const role = S.user?.role;
|
||
const cardHtml = (list, showActions) => list.map(h => `<div class="vcard"><div class="vcard-top"><div><div class="vcard-name">${h.nama_kk}</div><div class="vcard-loc">📍 ${h.alamat || '—'}, ${h.kelurahan || '—'}</div></div><span class="badge badge-${h.status}">${sLbl[h.status] || h.status}</span></div><div class="vcard-meta"><span class="pill">👨👩👧 ${h.anggota} anggota</span><span class="pill">${h.ri_nama ? '🕌 ' + h.ri_nama : '⚠️ Belum ada RI'}</span></div><div class="progress-wrap"><div class="progress-label"><span>Konfirmasi: ${h.confirmations}/2</span></div><div class="progress-bar"><div class="progress-fill" style="width:${Math.min(h.confirmations / 2 * 100, 100)}%"></div></div></div>${showActions ? `<div class="vcard-actions"><button class="btn btn-confirm btn-sm" onclick="doVerif(${h.id},'confirm')">✅ Konfirmasi</button><button class="btn btn-danger btn-sm" onclick="doVerif(${h.id},'reject')">❌ Tolak</button>${h.confirmations >= 2 && ['super_admin', 'admin_daerah'].includes(role) ? `<button class="btn btn-primary btn-sm" onclick="doVerif(${h.id},'admin_approve')">🔵 Admin Verifikasi Final</button>` : ''}<button class="btn btn-secondary btn-sm" onclick="openDetailRT(${h.id})">Detail</button></div>` : ''}</div>`).join('');
|
||
document.getElementById('verif-body').innerHTML = (pend.length === 0 ? '<div class="empty"><div class="ei">✅</div><p>Tidak ada laporan</p></div>' : `<div class="verify-list">${cardHtml(pend, true)}</div>`) + (verif.length > 0 ? `<div style="font-size:11px;font-weight:700;margin:20px 0 10px">Sudah Terverifikasi</div><div class="verify-list">${cardHtml(verif, false)}</div>` : '');
|
||
} catch (e) { document.getElementById('verif-body').innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>' + e.message + '</p></div>'; }
|
||
}
|
||
async function doVerif(id, aksi) { try { const res = await POST(`/rumah-tangga/${id}/verifikasi`, { aksi }); toast('✅ ' + res.message); loadVerifikasi(); loadMap(); } catch (e) { toast('⚠️ ' + e.message); } }
|
||
async function quickConfirm(id) { try { const r = await POST(`/rumah-tangga/${id}/verifikasi`, { aksi: 'confirm', alasan: 'Dari peta' }); toast('✅ ' + r.message); loadMap(); } catch (e) { toast('⚠️ ' + e.message); } }
|
||
|
||
// ── RUMAH IBADAH ──
|
||
async function loadRI() {
|
||
document.getElementById('ri-body').innerHTML = '<div class="loading"><div class="spinner"></div> Memuat...</div>';
|
||
try {
|
||
const ris = await GET('/rumah-ibadah');
|
||
document.getElementById('ri-body').innerHTML = ris.length === 0 ? '<div class="empty"><div class="ei">🕌</div><p>Belum ada data</p></div>' : ris.map(r => `<div class="ri-card" onclick="openDetailRI(${r.id})"><div class="ri-card-top"><div class="ri-emoji">${riEmoji(r.tipe)}</div><div><div class="ri-name">${r.nama}</div><div class="ri-type">${r.tipe}</div></div></div><div class="ri-stats"><div class="ri-s"><div class="n" style="color:var(--blue)">${r.radius}m</div><div class="l">Radius</div></div><div class="ri-s"><div class="n" style="color:var(--green)">${r.assigned_count || 0}</div><div class="l">KK</div></div></div></div>`).join('');
|
||
} catch (e) { document.getElementById('ri-body').innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>' + e.message + '</p></div>'; }
|
||
}
|
||
function openAddRI() {
|
||
const f = S.riForm || {}; const lat = S.pickCoordsRI?.lat || (map ? map.getCenter().lat : -0.0227); const lng = S.pickCoordsRI?.lng || (map ? map.getCenter().lng : 109.3340); const coordText = S.pickCoordsRI ? `✅ ${lat.toFixed(5)}, ${lng.toFixed(5)}` : '🗺️ Pilih Lokasi di Peta';
|
||
modal(`<div class="modal-head"><div style="font-size:24px">🕌</div><div class="modal-title">Tambah Rumah Ibadah</div><div class="modal-close" onclick="closeModal()">✕</div></div><div class="form-grid" style="gap:10px"><div class="fg form-full"><label>Nama Rumah Ibadah</label><input type="text" id="nri-nama" placeholder="Nama..." value="${f.nama || ''}"/></div><div class="fg"><label>Jenis</label><select id="nri-tipe"><option ${f.tipe === 'Masjid' ? 'selected' : ''}>Masjid</option><option ${f.tipe === 'Gereja' ? 'selected' : ''}>Gereja</option><option ${f.tipe === 'Pura' ? 'selected' : ''}>Pura</option><option ${f.tipe === 'Vihara' ? 'selected' : ''}>Vihara</option><option ${f.tipe === 'Kelenteng' ? 'selected' : ''}>Kelenteng</option></select></div><div class="fg"><label>Kelurahan</label><input type="text" id="nri-kel" placeholder="Kelurahan..." value="${f.kelurahan || ''}"/></div><div class="fg"><label>Radius (meter)</label><input type="number" id="nri-rad" value="${f.radius || 500}" min="100"/></div><div class="fg form-full"><label>Koordinat Lokasi</label><button class="btn btn-secondary" style="width:100%;text-align:left;background:var(--light);border:1px solid var(--border);color:var(--text)" onclick="pickLocRI()">${coordText}</button></div></div><div style="display:flex;gap:8px;margin-top:16px"><button class="btn btn-primary" onclick="submitRI()">Simpan</button><button class="btn btn-secondary" onclick="closeModal()">Batal</button></div>`);
|
||
}
|
||
function pickLocRI() { S.riForm = { nama: document.getElementById('nri-nama').value, tipe: document.getElementById('nri-tipe').value, kelurahan: document.getElementById('nri-kel').value, radius: document.getElementById('nri-rad').value }; S.isPickingRI = true; closeModal(); goView('map'); document.getElementById('map-container').classList.add('picking'); toast('Klik lokasi Rumah Ibadah di peta...'); }
|
||
async function submitRI() {
|
||
const nama = document.getElementById('nri-nama').value.trim(); if (!nama) return toast('⚠️ Nama wajib diisi!');
|
||
const tipe = document.getElementById('nri-tipe').value; const lat = S.pickCoordsRI?.lat || (map ? map.getCenter().lat : -0.0227); const lng = S.pickCoordsRI?.lng || (map ? map.getCenter().lng : 109.3340);
|
||
try { await POST('/rumah-ibadah', { nama, tipe, kelurahan: document.getElementById('nri-kel').value, radius: parseInt(document.getElementById('nri-rad').value) || 500, lat: lat, lng: lng }); S.pickCoordsRI = null; S.riForm = {}; closeModal(); loadRI(); loadMap(); toast('✅ Rumah Ibadah berhasil ditambahkan!'); } catch (e) { toast('⚠️ ' + e.message); }
|
||
}
|
||
|
||
// ── PROGRAM & DONASI ──
|
||
const pEmoji = { Pangan: '🍚', Sandang: '👕', Pendidikan: '📚', Kesehatan: '🏥', Ekonomi: '💼' }; const pColor = { Pangan: '#e07a5f', Sandang: '#457b9d', Pendidikan: '#9b72aa', Kesehatan: '#c1121f', Ekonomi: '#2d6a4f' };
|
||
async function loadProgram() {
|
||
document.getElementById('prog-body').innerHTML = '<div class="loading"><div class="spinner"></div> Memuat...</div>';
|
||
try { const ps = await GET('/program'); document.getElementById('prog-body').innerHTML = ps.length === 0 ? '<div class="empty"><div class="ei">📦</div><p>Belum ada program</p></div>' : `<table class="tbl"><thead><tr><th>Program</th><th>Jenis</th><th>Rumah Ibadah</th></tr></thead><tbody>${ps.map(p => `<tr><td><b>${pEmoji[p.tipe] || '📦'} ${p.nama}</b><div style="font-size:11.5px;color:var(--text2)">${p.tanggal || '—'}</div></td><td><span class="badge" style="background:${pColor[p.tipe] || '#ccc'}22;color:${pColor[p.tipe] || '#333'}">${p.tipe}</span></td><td style="font-size:12.5px">${p.ri_nama || '—'}</td></tr>`).join('')}</tbody></table>`; } catch (e) { document.getElementById('prog-body').innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>' + e.message + '</p></div>'; }
|
||
}
|
||
function openAddProgram() { const riOpts = S.allRI.map(r => `<option value="${r.id}">${riEmoji(r.tipe)} ${r.nama}</option>`).join(''); modal(`<div class="modal-head"><div style="font-size:22px">📦</div><div class="modal-title">Buat Program Bantuan</div><div class="modal-close" onclick="closeModal()">✕</div></div><div class="form-grid" style="gap:10px"><div class="fg form-full"><label>Nama Program</label><input type="text" id="np-nama"/></div><div class="fg"><label>Jenis</label><select id="np-tipe"><option>Pangan</option><option>Sandang</option><option>Pendidikan</option><option>Kesehatan</option><option>Ekonomi</option></select></div><div class="fg"><label>Rumah Ibadah</label><select id="np-ri">${riOpts}</select></div></div><div style="display:flex;gap:8px;margin-top:16px"><button class="btn btn-primary" onclick="submitProgram()">Buat</button><button class="btn btn-secondary" onclick="closeModal()">Batal</button></div>`); }
|
||
async function submitProgram() { try { await POST('/program', { nama: document.getElementById('np-nama').value, tipe: document.getElementById('np-tipe').value, ri_id: parseInt(document.getElementById('np-ri').value) }); closeModal(); loadProgram(); toast('✅ Program dibuat!'); } catch (e) { toast('⚠️ ' + e.message); } }
|
||
async function loadDonasi() { document.getElementById('don-body').innerHTML = '<div class="loading"><div class="spinner"></div> Memuat...</div>'; try { const ds = await GET('/donasi'); const total = ds.reduce((a, d) => a + (parseFloat(d.jumlah) || 0), 0); document.getElementById('don-body').innerHTML = `<div style="margin-bottom:14px;padding:14px;background:var(--green-ll);border-radius:var(--r);border:1px solid #b7e4c7"><div style="font-size:12px;color:var(--green);font-weight:700">Total Donasi Terkumpul</div><div style="font-size:24px;font-weight:700;color:var(--green)">Rp ${total.toLocaleString('id-ID')}</div></div>` + (ds.length === 0 ? '<div class="empty"><div class="ei">💚</div><p>Belum ada donasi</p></div>' : `<div class="don-list">${ds.map(d => `<div class="don-card"><div class="don-icon">💚</div><div class="don-info"><div class="don-jumlah">Rp ${parseFloat(d.jumlah).toLocaleString('id-ID')}</div><div class="don-meta">${d.nama_donatur || 'Anonim'} → ${d.nama_ri || '—'} · ${d.keterangan || '—'}</div></div><div style="font-size:11px;color:var(--text2)">${(d.created_at || '').slice(0, 10)}</div></div>`).join('')}</div>`); } catch (e) { document.getElementById('don-body').innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>' + e.message + '</p></div>'; } }
|
||
function openAddDonasi() { const riOpts = S.allRI.map(r => `<option value="${r.id}">${riEmoji(r.tipe)} ${r.nama}</option>`).join(''); modal(`<div class="modal-head"><div style="font-size:22px">💚</div><div class="modal-title">Donasi Baru</div><div class="modal-close" onclick="closeModal()">✕</div></div><div class="form-grid" style="gap:10px"><div class="fg form-full"><label>Tujuan Rumah Ibadah</label><select id="don-ri">${riOpts}</select></div><div class="fg form-full"><label>Jumlah (Rp)</label><input type="number" id="don-jml" min="10000"/></div><div class="fg form-full"><label>Keterangan</label><input type="text" id="don-ket"/></div></div><div style="display:flex;gap:8px;margin-top:16px"><button class="btn btn-primary" onclick="submitDonasi()">Donasikan</button></div>`); }
|
||
async function submitDonasi() { const jml = parseFloat(document.getElementById('don-jml').value); if (!jml) return toast('⚠️ Jumlah tidak valid!'); try { await POST('/donasi', { ri_id: parseInt(document.getElementById('don-ri').value), jumlah: jml, keterangan: document.getElementById('don-ket').value }); closeModal(); loadDonasi(); toast('✅ Donasi berhasil dicatat!'); } catch (e) { toast('⚠️ ' + e.message); } }
|
||
|
||
// ── USERS & PROFILE ──
|
||
async function loadUsers() { document.getElementById('users-body').innerHTML = '<div class="loading"><div class="spinner"></div> Memuat...</div>'; try { const users = await GET('/users'); document.getElementById('users-body').innerHTML = users.map(u => `<div class="user-row ${u.is_active ? '' : 'inactive'}"><div class="user-av">${u.nama[0].toUpperCase()}</div><div class="user-info"><div class="user-name-2">${u.nama}</div><div class="user-email">${u.email}</div></div><span class="badge rb-${u.role}" style="font-size:10px;padding:2px 8px;border-radius:10px">${getRoleLabel(u.role)}</span><button class="btn ${u.is_active ? 'btn-danger' : 'btn-confirm'} btn-sm" onclick="toggleUser(${u.id},this)">${u.is_active ? 'Nonaktifkan' : 'Aktifkan'}</button></div>`).join(''); } catch (e) { document.getElementById('users-body').innerHTML = '<div class="empty"><div class="ei">⚠️</div><p>' + e.message + '</p></div>'; } }
|
||
async function toggleUser(id, btn) { try { await POST(`/users/${id}/toggle`, {}); toast('✅ Status pengguna diperbarui'); loadUsers(); } catch (e) { toast('⚠️ ' + e.message); } }
|
||
function renderProfile() { const u = S.user; document.getElementById('profile-body').innerHTML = `<div style="text-align:center;padding:20px 0 16px"><div style="width:60px;height:60px;border-radius:50%;background:var(--green-ll);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--green);margin:0 auto">${u.nama[0].toUpperCase()}</div><div style="font-size:18px;font-weight:700;margin-top:10px">${u.nama}</div><span class="badge rb-${u.role}" style="font-size:11px;padding:3px 10px;border-radius:10px;margin-top:6px;display:inline-block">${getRoleLabel(u.role)}</span></div><div class="detail-row"><span class="detail-key">Database</span><span class="detail-val" style="color:var(--green)">✅ MySQL Native PHP</span></div><div style="margin-top:16px"><button class="btn btn-danger" style="width:100%" onclick="doLogout()">Keluar dari Akun</button></div>`; }
|
||
|
||
// ── FORM LAPORAN ──
|
||
function goStep(s) { S.step = s;[1, 2, 3, 4].forEach(i => { document.getElementById('fs-' + i).style.display = i === s ? 'block' : 'none'; const st = document.getElementById('sp-' + i); st.classList.remove('active', 'done'); if (i < s) st.classList.add('done'); else if (i === s) st.classList.add('active'); const ln = document.getElementById('sl-' + i); if (ln) { if (i < s) ln.classList.add('done'); else ln.classList.remove('done'); } }); if (s === 4) renderTangPreview(); }
|
||
function addTang() { const nama = document.getElementById('t-nama').value.trim(); const tl = document.getElementById('t-tl').value; if (!nama || !tl) return toast('⚠️ Nama dan tanggal lahir wajib!'); S.newTang.push({ nama, tanggal_lahir: tl, jenis_kelamin: document.getElementById('t-jk').value, hubungan: document.getElementById('t-hub').value, pendidikan: document.getElementById('t-pend').value, pekerjaan: document.getElementById('t-kerja').value });['t-nama', 't-tl', 't-kerja'].forEach(id => document.getElementById(id).value = ''); renderTangPreview(); toast(`✅ ${nama} ditambahkan`); }
|
||
function removeTang(i) { S.newTang.splice(i, 1); renderTangPreview(); }
|
||
function renderTangPreview() { const t = S.newTang; if (t.length === 0) { document.getElementById('tang-preview').innerHTML = '<div class="empty" style="padding:20px"><div class="ei" style="font-size:28px">👪</div><p>Belum ada anggota ditambahkan</p></div>'; return; } document.getElementById('tang-preview').innerHTML = `<table class="tang-tbl"><thead><tr><th>Nama</th><th>Tgl Lahir</th><th>Hubungan</th><th></th></tr></thead><tbody>${t.map((x, i) => `<tr><td><b>${x.nama}</b><div style="font-size:11px;color:var(--text2)">${x.jenis_kelamin === 'L' ? '♂' : '♀'} · ${x.pekerjaan || '—'}</div></td><td>${x.tanggal_lahir}</td><td style="font-size:12px;color:var(--text2)">${x.hubungan}</td><td><button class="btn btn-danger btn-sm" onclick="removeTang(${i})" style="padding:3px 8px;font-size:11px">✕</button></td></tr>`).join('')}</tbody></table>`; }
|
||
|
||
async function submitLaporan() {
|
||
const lat = S.pickCoords?.lat || (-0.0227 + (Math.random() - .5) * .02); const lng = S.pickCoords?.lng || (109.3340 + (Math.random() - .5) * .02);
|
||
const nama = document.getElementById('f-nama').value.trim() || 'Keluarga Anonim';
|
||
try {
|
||
await POST('/rumah-tangga', {
|
||
nama_kk: nama, lat, lng, alamat: document.getElementById('f-alamat').value, kelurahan: document.getElementById('f-kel').value, kecamatan: document.getElementById('f-kec').value, anggota: parseInt(document.getElementById('f-anggota').value) || 1, penghasilan: document.getElementById('f-peng').value, kebutuhan: document.getElementById('f-keb').value, kondisi: document.getElementById('f-kondisi')?.value || '', catatan: document.getElementById('f-catatan').value, tanggungan: S.newTang
|
||
});
|
||
toast(`✅ Laporan "${nama}" tersimpan ke MySQL!`);
|
||
S.pickCoords = null; S.newTang = []; S.step = 1;['f-alamat', 'f-nama', 'f-anggota', 'f-catatan'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; });
|
||
document.getElementById('pick-btn').className = 'pick-loc'; document.getElementById('pick-btn').innerHTML = '🗺️ Klik untuk pilih koordinat dari peta'; document.getElementById('coord-info').textContent = 'Belum dipilih';
|
||
goStep(1); loadMap(); setTimeout(() => goView('verifikasi'), 1000);
|
||
} catch (e) { toast('⚠️ ' + e.message); }
|
||
}
|
||
|
||
// ── MODALS DETAIL ──
|
||
async function openDetailRT(id) {
|
||
modal('<div class="loading" style="justify-content:center;padding:30px"><div class="spinner"></div> Memuat...</div>');
|
||
try {
|
||
const h = await GET('/rumah-tangga/' + id); const sLbl = { verified: '✅ Terverifikasi', pending: '⏳ Menunggu', dispute: '⚠️ Sengketa', rejected: '❌ Ditolak' };
|
||
modal(`<div class="modal-head"><div style="font-size:24px">🏠</div><div class="modal-title">${h.nama_kk}</div><div class="modal-close" onclick="closeModal()">✕</div></div><div class="detail-row"><span class="detail-key">Lokasi</span><span class="detail-val">${h.alamat || '—'}, ${h.kelurahan || '—'}</span></div><div class="detail-row"><span class="detail-key">Status</span><span class="detail-val" style="color:${sColor(h.status)}">${sLbl[h.status] || h.status}</span></div><div class="detail-row"><span class="detail-key">Konfirmasi</span><span class="detail-val">${h.confirmations}/2</span></div><div class="detail-row"><span class="detail-key">Rumah Ibadah</span><span class="detail-val">${h.ri_nama ? riEmoji(h.ri_tipe) + ' ' + h.ri_nama : '⚠️ Belum ditugaskan'}</span></div><div class="detail-row"><span class="detail-key">Anggota</span><span class="detail-val">${h.anggota} orang</span></div><div class="detail-row"><span class="detail-key">Penghasilan</span><span class="detail-val">${h.penghasilan || '—'}</span></div><div class="detail-row"><span class="detail-key">Kebutuhan</span><span class="detail-val">${h.kebutuhan || '—'}</span></div>${h.catatan ? `<div style="background:var(--light);border-radius:var(--r);padding:10px;margin:10px 0;font-size:13px;color:var(--text2)">💬 ${h.catatan}</div>` : ''}${h.tanggungan?.length > 0 ? `<div style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--text2);margin:14px 0 8px">Tanggungan (${h.tanggungan.length} orang)</div><table class="tang-tbl"><thead><tr><th>Nama</th><th>Tgl Lahir</th></tr></thead><tbody>${h.tanggungan.map(t => `<tr><td><b>${t.nama}</b><div style="font-size:10.5px;color:var(--text2)">${t.hubungan || '—'} · ${t.jenis_kelamin === 'L' ? '♂' : '♀'}</div></td><td>${t.tanggal_lahir}</td></tr>`).join('')}</tbody></table>` : ''}<div style="display:flex;gap:8px;margin-top:16px"><button class="btn btn-primary btn-sm" onclick="flyTo(${h.lat},${h.lng});closeModal();goView('map')">📍 Peta</button>${h.status === 'pending' ? `<button class="btn btn-confirm btn-sm" onclick="doVerif(${h.id},'confirm');closeModal()">✅ Konfirmasi</button>` : ''}<button class="btn btn-secondary btn-sm" onclick="closeModal()">Tutup</button></div>`);
|
||
} catch (e) { modal(`<div class="modal-head"><div class="modal-title">Error</div><div class="modal-close" onclick="closeModal()">✕</div></div><p style="color:var(--red)">${e.message}</p>`); }
|
||
}
|
||
function openDetailRI(id) { const r = S.allRI.find(x => x.id === id); if (!r) return; modal(`<div class="modal-head"><div style="font-size:26px">${riEmoji(r.tipe)}</div><div class="modal-title">${r.nama}</div><div class="modal-close" onclick="closeModal()">✕</div></div><div class="detail-row"><span class="detail-key">Jenis</span><span class="detail-val">${r.tipe}</span></div><div class="detail-row"><span class="detail-key">Kelurahan</span><span class="detail-val">${r.kelurahan || '—'}</span></div><div class="detail-row"><span class="detail-key">Pengurus</span><span class="detail-val">${r.nama_pengurus || '—'}</span></div><div class="detail-row"><span class="detail-key">Radius Coverage</span><span class="detail-val">${r.radius} meter</span></div><div style="display:flex;gap:8px;margin-top:16px"><button class="btn btn-primary btn-sm" onclick="flyTo(${r.lat},${r.lng});closeModal();goView('map')">📍 Peta</button><button class="btn btn-secondary btn-sm" onclick="closeModal()">Tutup</button></div>`); }
|
||
|
||
// ── INIT ──
|
||
function modal(html) { document.getElementById('modal-body').innerHTML = html; document.getElementById('modal-ov').classList.add('open'); }
|
||
function closeModal(e) { if (e && e.target !== document.getElementById('modal-ov')) return; document.getElementById('modal-ov').classList.remove('open'); }
|
||
let _toastTmr; function toast(msg) { const el = document.getElementById('toast'); el.textContent = msg; el.classList.add('show'); clearTimeout(_toastTmr); _toastTmr = setTimeout(() => el.classList.remove('show'), 3000); }
|
||
|
||
document.addEventListener('DOMContentLoaded', async () => {
|
||
try { const u = await GET('/me'); S.user = u; startApp(); } catch (e) { } // Tidak login, diam di halaman depan
|
||
});
|
||
</script>
|
||
</body>
|
||
|
||
</html> |