Files
WebGIS-Poverty/index.html
T
2026-06-09 12:39:17 +07:00

2238 lines
95 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>&nbsp;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>