Files
WebGIS_Project/05/index.php
T
2026-06-10 19:26:40 +07:00

2287 lines
102 KiB
PHP
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" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Pengentasan Kemiskinan Pontianak</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&family=Fraunces:opsz,wght@9..144,300;9..144,700&display=swap" rel="stylesheet">
<style>
/* ===== THEME SYSTEM ===== */
[data-theme="dark"] {
--bg: #0f1117;
--surface: #181c25;
--surface2: #1f2433;
--border: #2a3045;
--text: #e8eaf0;
--muted: #7a849a;
--input-bg: #1a1f2e;
--shadow: 0 4px 24px rgba(0,0,0,.55);
--login-bg: radial-gradient(ellipse at 20% 50%, #1a2040 0%, #0f1117 60%);
}
[data-theme="light"] {
--bg: #f0f2f7;
--surface: #ffffff;
--surface2: #f5f7fc;
--border: #dde1ec;
--text: #1a1f2e;
--muted: #6b7694;
--input-bg: #f5f7fc;
--shadow: 0 4px 24px rgba(0,0,0,.12);
--login-bg: radial-gradient(ellipse at 20% 50%, #dce6ff 0%, #f0f2f7 60%);
}
:root {
--accent: #4f8ef7;
--accent2: #38d9a9;
--miskin: #f05252;
--rentan: #f59e0b;
--mampu: #38d9a9;
--ibadah: #818cf8;
--laporan: #fb923c;
--r: 10px; --r2: 16px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
transition: background .3s, color .3s;
}
/* ======================================================
LOGIN PAGE
====================================================== */
#login-page {
position: fixed; inset: 0;
background: var(--login-bg);
display: flex; align-items: center; justify-content: center;
z-index: 9999;
transition: opacity .4s;
}
#login-page.hidden { opacity: 0; pointer-events: none; }
.login-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 20px;
padding: 40px 36px 32px;
width: 100%;
max-width: 420px;
box-shadow: var(--shadow);
}
.login-logo {
text-align: center;
margin-bottom: 6px;
}
.login-logo .brand {
font-family: 'Fraunces', serif;
font-size: 28px;
font-weight: 700;
}
.login-logo .brand .b1 { color: var(--accent); }
.login-logo .brand .b2 { color: var(--accent2); }
.login-logo .sub {
font-size: 13px;
color: var(--muted);
margin-top: 6px;
line-height: 1.4;
}
.login-divider {
height: 1px; background: var(--border);
margin: 20px 0;
}
.login-form { display: flex; flex-direction: column; gap: 14px; }
.lf-group { display: flex; flex-direction: column; gap: 5px; }
.lf-group label {
font-size: 12px; font-weight: 700;
text-transform: uppercase; letter-spacing: .7px;
color: var(--muted);
}
.lf-group input {
background: var(--input-bg);
border: 1.5px solid var(--border);
border-radius: 10px;
padding: 11px 14px;
font-family: inherit;
font-size: 14px;
color: var(--text);
transition: border-color .15s;
width: 100%;
}
.lf-group input:focus { outline: none; border-color: var(--accent); }
.btn-login {
background: var(--accent);
color: #fff;
border: none;
border-radius: 10px;
padding: 13px;
font-family: inherit;
font-size: 15px;
font-weight: 700;
cursor: pointer;
margin-top: 4px;
transition: opacity .15s, transform .1s;
display: flex; align-items: center; justify-content: center; gap: 8px;
}
.btn-login:hover { opacity: .9; transform: translateY(-1px); }
.btn-login:active { transform: translateY(0); }
.login-footer {
text-align: center;
font-size: 13px;
color: var(--muted);
margin-top: 16px;
}
.login-footer a {
color: var(--accent);
cursor: pointer;
font-weight: 600;
text-decoration: none;
}
.demo-box {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 10px 14px;
margin-top: 16px;
font-size: 12px;
color: var(--muted);
line-height: 1.7;
}
.demo-box strong { color: var(--text); }
.login-error {
background: #f0525222;
border: 1px solid var(--miskin);
border-radius: 8px;
padding: 9px 12px;
font-size: 13px;
color: var(--miskin);
display: none;
}
.login-error.show { display: block; }
/* ======================================================
MAIN APP
====================================================== */
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
/* ---- HEADER ---- */
header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 0 16px;
height: 52px;
display: flex;
align-items: center;
gap: 12px;
z-index: 1000;
flex-shrink: 0;
transition: background .3s, border-color .3s;
}
.logo {
font-family: 'Fraunces', serif;
font-size: 16px;
font-weight: 700;
white-space: nowrap;
display: flex; align-items: center; gap: 6px;
}
.logo .l1 { color: var(--accent); }
.logo .l2 { color: var(--accent2); }
nav { display: flex; gap: 2px; margin-left: 6px; }
.nav-btn {
background: transparent;
border: none;
color: var(--muted);
padding: 5px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 12.5px;
font-family: inherit;
font-weight: 500;
transition: all .15s;
white-space: nowrap;
}
.nav-btn:hover { background: var(--surface2); color: var(--text); }
.nav-btn.active { background: var(--surface2); color: var(--accent); font-weight: 600; }
.header-right {
margin-left: auto;
display: flex; align-items: center; gap: 8px;
}
.badge-alert {
background: var(--laporan);
color: #fff;
border-radius: 20px;
padding: 2px 8px;
font-size: 11px;
font-weight: 700;
}
.btn-primary {
background: var(--accent);
color: #fff;
border: none;
padding: 7px 14px;
border-radius: 7px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 600;
transition: opacity .15s;
white-space: nowrap;
}
.btn-primary:hover { opacity: .85; }
/* Theme toggle */
.theme-toggle {
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px;
width: 52px; height: 26px;
cursor: pointer;
position: relative;
transition: background .3s;
flex-shrink: 0;
}
.theme-toggle::after {
content: '';
position: absolute;
top: 3px; left: 3px;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--accent);
transition: transform .3s, background .3s;
}
[data-theme="light"] .theme-toggle::after { transform: translateX(26px); background: var(--rentan); }
.theme-icon {
position: absolute;
font-size: 11px;
top: 50%; transform: translateY(-50%);
}
.theme-icon.moon { right: 6px; }
.theme-icon.sun { left: 6px; }
.user-chip {
display: flex; align-items: center; gap: 6px;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 20px;
padding: 4px 12px 4px 6px;
font-size: 12px;
font-weight: 500;
}
.user-avatar {
width: 22px; height: 22px; border-radius: 50%;
background: var(--accent);
display: flex; align-items: center; justify-content: center;
font-size: 10px; font-weight: 700; color: #fff;
}
.btn-logout {
background: transparent;
border: 1px solid var(--border);
color: var(--muted);
padding: 5px 12px;
border-radius: 7px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 500;
transition: all .15s;
}
.btn-logout:hover { border-color: var(--miskin); color: var(--miskin); }
/* ---- MAIN LAYOUT ---- */
.main {
display: flex;
flex: 1;
overflow: hidden;
position: relative; /* tombol toggle pakai absolute di sini */
}
/* ---- SIDEBAR ---- */
.sidebar {
width: 300px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
overflow: hidden;
flex-shrink: 0;
transition: width .3s ease, background .3s, border-color .3s;
position: relative;
}
.sidebar.collapsed {
width: 0;
border-right: none;
overflow: hidden;
}
/* Tombol toggle — menempel di tepi kiri, LUAR sidebar */
.sidebar-toggle {
position: absolute;
top: 50%;
left: 300px;
transform: translate(-50%, -50%);
z-index: 850;
width: 22px; height: 44px;
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 0 8px 8px 0;
border-left: none;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 11px;
font-weight: 700;
color: var(--muted);
transition: left .3s ease, color .15s, background .15s;
box-shadow: 3px 0 8px rgba(0,0,0,.25);
}
.sidebar-toggle:hover { color: var(--accent); background: var(--surface2); }
.sidebar-toggle.collapsed-state { left: 0; transform: translate(0, -50%); }
.sidebar-header {
padding: 12px 14px 8px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.sidebar-header h2 {
font-size: 11px;
font-weight: 700;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 1px;
white-space: nowrap;
}
.stats-strip {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
flex-shrink: 0;
border-bottom: 1px solid var(--border);
}
.stat-card {
background: var(--surface);
padding: 14px 14px 12px;
border-right: 1px solid var(--border);
border-bottom: 1px solid var(--border);
position: relative;
transition: background .15s;
}
.stat-card:hover { background: var(--surface2); }
.stat-card:nth-child(2n) { border-right: none; }
.stat-card:nth-last-child(-n+2) { border-bottom: none; }
.stat-card .label {
font-size: 9.5px; font-weight: 700; text-transform: uppercase;
letter-spacing: .8px; color: var(--muted); margin-bottom: 4px;
display: flex; align-items: center; gap: 5px;
}
.stat-card .value {
font-size: 28px; font-weight: 800;
font-family: 'Fraunces', serif; line-height: 1;
}
.stat-card .stat-icon {
position: absolute; top: 10px; right: 12px;
font-size: 18px; opacity: .25;
}
.stat-card.miskin .value { color: var(--miskin); }
.stat-card.rentan .value { color: var(--rentan); }
.stat-card.bantuan .value { color: var(--accent2); }
.stat-card.lapp .value { color: var(--laporan); }
.filter-bar {
padding: 8px 12px;
border-bottom: 1px solid var(--border);
display: flex; gap: 5px; flex-wrap: wrap;
flex-shrink: 0;
}
.filter-chip {
border: 1.5px solid var(--border);
background: transparent;
color: var(--muted);
border-radius: 20px;
padding: 3px 10px;
font-size: 11px;
font-family: inherit;
cursor: pointer;
transition: all .15s;
font-weight: 500;
}
.filter-chip:hover { border-color: var(--accent); color: var(--accent); }
.filter-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; }
.filter-chip.miskin.active { background: var(--miskin); border-color: var(--miskin); }
.filter-chip.rentan.active { background: var(--rentan); border-color: var(--rentan); }
.filter-chip.mampu.active { background: var(--mampu); border-color: var(--mampu); color: #0f1117; }
.filter-chip.ibadah.active { background: var(--ibadah); border-color: var(--ibadah); }
.filter-chip.laporan.active { background: var(--laporan);border-color: var(--laporan); }
.list-container { flex: 1; overflow-y: auto; padding: 6px; }
.list-container::-webkit-scrollbar { width: 3px; }
.list-container::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
.list-item {
display: flex; align-items: center; gap: 9px;
padding: 9px 9px;
border-radius: var(--r);
cursor: pointer;
transition: background .12s;
margin-bottom: 2px;
}
.list-item:hover { background: var(--surface2); }
.list-item.selected { background: var(--surface2); outline: 1.5px solid var(--accent); }
.list-dot { width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0; }
.list-info { flex: 1; min-width: 0; }
.list-nama { font-size: 12.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.list-sub { font-size: 11px; color: var(--muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pill { font-size: 10px; font-weight: 700; padding: 2px 7px; border-radius: 20px; flex-shrink: 0; }
/* ---- MAP ---- */
#map { flex: 1; position: relative; }
/* ---- LAYER SWITCHER (kiri atas map) ---- */
.layer-switcher {
position: absolute;
bottom: 32px; left: 50%;
transform: translateX(-50%);
z-index: 800;
display: flex;
gap: 6px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 30px;
padding: 5px 8px;
box-shadow: var(--shadow);
}
.layer-btn {
background: transparent;
border: none;
border-radius: 22px;
padding: 5px 14px;
font-family: inherit;
font-size: 12px;
font-weight: 600;
color: var(--muted);
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
transition: all .15s;
}
.layer-btn:hover { color: var(--text); background: var(--surface2); }
.layer-btn.active { background: var(--accent); color: #fff; }
.layer-btn.active[id="btn-layer-satelit"] { background: var(--ibadah); }
.layer-btn.active[id="btn-layer-terang"] { background: var(--rentan); color: #0f1117; }
/* ---- LEGEND (kanan bawah) ---- */
.legend-box {
position: absolute;
bottom: 24px; right: 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 10px 13px;
z-index: 800;
min-width: 140px;
box-shadow: var(--shadow);
}
.legend-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 7px; }
.legend-row { display: flex; align-items: center; gap: 7px; font-size: 11.5px; margin-bottom: 4px; }
.legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
/* ---- DETAIL PANEL ---- */
.detail-panel {
position: absolute; top: 0; right: 0;
width: 360px;
height: 100%;
background: var(--surface);
border-left: 1px solid var(--border);
overflow-y: auto;
z-index: 900;
display: none;
flex-direction: column;
box-shadow: var(--shadow);
transition: background .3s;
}
.detail-panel.open { display: flex; }
.detail-panel::-webkit-scrollbar { width: 3px; }
.detail-panel::-webkit-scrollbar-thumb { background: var(--border); }
.dp-header {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border);
position: sticky; top: 0;
background: var(--surface);
z-index: 1;
display: flex; align-items: flex-start; gap: 8px;
}
.dp-header h3 { font-family: 'Fraunces', serif; font-size: 15px; font-weight: 700; flex: 1; line-height: 1.3; }
.dp-close { background: var(--surface2); border: none; color: var(--muted); width: 26px; height: 26px; border-radius: 50%; cursor: pointer; font-size: 14px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.dp-close:hover { color: var(--text); }
.dp-body { padding: 12px 16px; }
.dp-section { margin-bottom: 14px; }
.dp-section-title { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: var(--muted); margin-bottom: 6px; }
.dp-row { display: flex; justify-content: space-between; align-items: baseline; padding: 4px 0; border-bottom: 1px solid var(--border); font-size: 12.5px; gap: 10px; }
.dp-row:last-child { border-bottom: none; }
.dp-row .key { color: var(--muted); flex-shrink: 0; }
.dp-row .val { font-weight: 500; text-align: right; word-break: break-word; }
.bantuan-item { background: var(--surface2); border-radius: 7px; padding: 7px 9px; margin-bottom: 5px; display: flex; justify-content: space-between; align-items: center; font-size: 11.5px; }
.tag-selesai { color: var(--mampu); font-weight: 700; }
.tag-proses { color: var(--rentan); font-weight: 700; }
.dp-actions { display: flex; gap: 7px; padding: 0 16px 16px; flex-wrap: wrap; }
.btn-sm { padding: 6px 12px; border-radius: 7px; font-size: 11.5px; font-family: inherit; font-weight: 600; cursor: pointer; border: none; transition: opacity .15s; }
.btn-sm:hover { opacity: .8; }
.btn-edit { background: var(--accent); color: #fff; }
.btn-hapus { background: var(--miskin); color: #fff; }
.btn-bantuan { background: var(--accent2); color: #0f1117; }
/* ---- FULL-PAGE PANELS ---- */
.full-panel {
position: absolute; top: 0; left: 300px; right: 0; bottom: 0;
background: var(--bg);
z-index: 700;
overflow-y: auto;
padding: 22px 24px;
display: none;
transition: background .3s, left .3s;
}
.sidebar.collapsed ~ #map ~ .full-panel,
.sidebar-collapsed .full-panel { left: 0; }
.full-panel.open { display: block; }
/* Dashboard */
.dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(185px,1fr)); gap: 14px; margin-bottom: 20px; }
.dash-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); padding: 18px; }
.dash-card h4 { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .9px; color: var(--muted); margin-bottom: 6px; }
.dash-card .big-num { font-family: 'Fraunces', serif; font-size: 32px; font-weight: 700; line-height: 1; }
.dash-card .sub-num { font-size: 11.5px; color: var(--muted); margin-top: 3px; }
.dash-table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden; margin-bottom: 18px; }
.dash-table-wrap h3 { padding: 12px 16px 10px; font-family: 'Fraunces', serif; font-size: 14px; font-weight: 700; border-bottom: 1px solid var(--border); }
table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
th { text-align: left; padding: 9px 14px; font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); border-bottom: 1px solid var(--border); }
td { padding: 9px 14px; border-bottom: 1px solid var(--border); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--surface2); }
/* Laporan */
.laporan-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); padding: 14px 16px; margin-bottom: 10px; display: flex; gap: 12px; align-items: flex-start; }
.urg-dot { width: 11px; height: 11px; border-radius: 50%; flex-shrink: 0; margin-top: 3px; }
.laporan-info { flex: 1; }
.laporan-info h4 { font-size: 13px; font-weight: 700; margin-bottom: 3px; }
.laporan-info p { font-size: 11.5px; color: var(--muted); line-height: 1.5; }
.laporan-meta { margin-top: 7px; display: flex; gap: 6px; flex-wrap: wrap; }
/* Keluarga panel */
.panel-toolbar {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 18px; gap: 12px; flex-wrap: wrap;
}
.panel-title { font-family: 'Fraunces', serif; font-size: 20px; font-weight: 700; }
.panel-toolbar .search-box {
flex: 1; min-width: 200px; max-width: 420px;
background: var(--surface);
border: 1.5px solid var(--border);
border-radius: 8px;
padding: 8px 13px;
font-family: inherit; font-size: 13px;
color: var(--text); transition: border-color .15s;
}
.panel-toolbar .search-box:focus { outline: none; border-color: var(--accent); }
.data-table-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); overflow: hidden; }
.data-table-wrap .table-scroll { overflow-x: auto; }
.data-table-wrap .table-scroll table { min-width: 900px; }
.data-table-wrap .tbl-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border); }
.data-table-wrap .tbl-header h3 { font-size: 14px; font-weight: 700; }
/* Status badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: 20px; font-size: 10.5px; font-weight: 700; }
.badge-miskin { background: #f0525222; color: var(--miskin); }
.badge-rentan { background: #f59e0b22; color: var(--rentan); }
.badge-mampu { background: #38d9a922; color: var(--mampu); }
.badge-verif { background: #38d9a922; color: var(--mampu); }
.badge-belum { background: #f59e0b22; color: var(--rentan); }
/* Pendidikan */
.pend-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
.pend-form-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); padding: 18px; }
.pend-form-card h3 { font-family: 'Fraunces', serif; font-size: 15px; margin-bottom: 14px; }
.validasi-info { background: #f59e0b14; border: 1px solid #f59e0b44; border-radius: 10px; padding: 10px 14px; margin-bottom: 14px; font-size: 12px; color: var(--rentan); line-height: 1.6; }
.validasi-info strong { font-weight: 700; }
/* ---- MODAL ---- */
.overlay { position: fixed; inset: 0; background: rgba(0,0,0,.65); z-index: 2000; display: none; align-items: center; justify-content: center; padding: 20px; }
.overlay.open { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r2); width: 100%; max-width: 540px; max-height: 90vh; overflow-y: auto; box-shadow: var(--shadow); }
.modal-header { padding: 16px 18px 12px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; }
.modal-header h2 { font-family: 'Fraunces', serif; font-size: 17px; font-weight: 700; }
.modal-body { padding: 18px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.form-grid .full { grid-column: 1/-1; }
.form-group { display: flex; flex-direction: column; gap: 4px; }
.form-group label { font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); }
.form-group input,
.form-group select,
.form-group textarea {
background: var(--input-bg); border: 1.5px solid var(--border); border-radius: 8px;
color: var(--text); padding: 8px 11px; font-family: inherit; font-size: 12.5px;
transition: border-color .15s; width: 100%;
}
.form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: var(--accent); }
.form-group textarea { resize: vertical; min-height: 65px; }
.form-group select option { background: var(--surface); }
.modal-footer { padding: 12px 18px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }
.btn-cancel { background: var(--surface2); border: none; color: var(--muted); padding: 8px 16px; border-radius: 7px; cursor: pointer; font-family: inherit; font-size: 12.5px; font-weight: 600; }
.coord-note { font-size: 11px; color: var(--accent2); margin-top: 3px; }
/* ---- CHAT ---- */
.chat-bubble {
max-width: 75%;
padding: 9px 13px;
border-radius: 12px;
font-size: 13px;
line-height: 1.5;
position: relative;
}
.chat-bubble.saya {
background: var(--accent);
color: #fff;
align-self: flex-end;
border-bottom-right-radius: 4px;
}
.chat-bubble.lain {
background: var(--surface2);
color: var(--text);
align-self: flex-start;
border-bottom-left-radius: 4px;
}
.chat-bubble.broadcast {
background: #818cf822;
border: 1px solid var(--ibadah);
color: var(--text);
align-self: center;
text-align: center;
border-radius: 20px;
font-size: 12px;
max-width: 90%;
}
.chat-meta { font-size: 10px; margin-top: 3px; opacity: .7; }
.chat-wrap { display: flex; flex-direction: column; }
.chat-wrap.saya { align-items: flex-end; }
.chat-wrap.lain { align-items: flex-start; }
.chat-sender { font-size: 10.5px; font-weight: 700; color: var(--muted); margin-bottom: 2px; }
/* ---- TOAST ---- */
.toast { position: fixed; bottom: 20px; right: 20px; background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 11px 16px; font-size: 12.5px; font-weight: 500; z-index: 9999; transform: translateY(60px); opacity: 0; transition: all .3s; pointer-events: none; box-shadow: var(--shadow); }
.toast.show { transform: translateY(0); opacity: 1; }
.toast.success { border-color: var(--mampu); color: var(--mampu); }
.toast.error { border-color: var(--miskin); color: var(--miskin); }
.marker-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid rgba(255,255,255,.3); cursor: pointer; transition: transform .1s; }
.marker-dot:hover { transform: scale(1.4); }
/* Leaflet light mode */
[data-theme="light"] .leaflet-tile { filter: brightness(.95) saturate(.9); }
</style>
</head>
<body>
<!-- ================================================================
LOGIN PAGE
================================================================ -->
<div id="login-page">
<div class="login-card">
<div class="login-logo">
<div class="brand"><span class="b1">WebGIS</span><span class="b2">KM</span></div>
<div class="sub">Sistem Pengentasan Kemiskinan<br>Berbasis Komunitas</div>
</div>
<div class="login-divider"></div>
<div class="login-form">
<div class="login-error" id="login-error">Username atau password salah.</div>
<div class="lf-group">
<label>Username</label>
<input type="text" id="login-user" placeholder="masukkan username" autocomplete="username">
</div>
<div class="lf-group">
<label>Password</label>
<input type="password" id="login-pass" placeholder="••••••••" autocomplete="current-password">
</div>
<button class="btn-login" onclick="doLogin()">&#10145;&#65039; Masuk</button>
</div>
<div class="login-footer">
Belum punya akun? <a onclick="showToast('Hubungi administrator untuk mendaftarkan akun','success')">Daftar</a>
</div>
<div class="demo-box">
<strong>Demo:</strong> admin / password &nbsp;|&nbsp; pengurus1 / password<br>pimpinan1 / password &nbsp;|&nbsp; warga1 / password
</div>
</div>
</div>
<!-- ================================================================
MAIN APP
================================================================ -->
<div id="app" style="display:none">
<!-- HEADER -->
<header>
<div class="logo">🗺️ <span class="l1">WebGIS</span><span class="l2">KM</span></div>
<nav>
<button class="nav-btn active" id="nav-peta" onclick="setView('peta',this)">🗺 Peta</button>
<button class="nav-btn" id="nav-dashboard" onclick="setView('dashboard',this)">📊 Dashboard</button>
<button class="nav-btn" id="nav-ibadah" onclick="setView('ibadah',this)">🕌 Rumah Ibadah</button>
<button class="nav-btn" id="nav-keluarga" onclick="setView('keluarga',this)">🏠 Keluarga</button>
<button class="nav-btn" id="nav-pendidikan"onclick="setView('pendidikan',this)">🎓 Pendidikan</button>
<button class="nav-btn" id="nav-laporan" onclick="setView('laporan',this)">🚨 Laporan</button>
<button class="nav-btn" id="nav-chat" onclick="setView('chat',this)">💬 Chat</button>
<button class="nav-btn" id="nav-log" onclick="setView('log',this)">📋 Log</button>
</nav>
<div class="header-right">
<span class="badge-alert" id="badge-laporan">0 Pending</span>
<button class="btn-primary" onclick="openModal('penduduk')">+ Penduduk</button>
<button class="btn-primary" onclick="openModal('ibadah')" style="background:var(--ibadah)">+ Ibadah</button>
<!-- Theme toggle -->
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle tema">
<span class="theme-icon moon">🌙</span>
<span class="theme-icon sun">☀️</span>
</button>
<div class="user-chip">
<div class="user-avatar" id="user-avatar">A</div>
<span id="user-label">Admin</span>
</div>
<button class="btn-logout" onclick="doLogout()">Logout</button>
</div>
</header>
<!-- MAIN -->
<div class="main">
<!-- SIDEBAR -->
<div class="sidebar" id="sidebar">
<div class="sidebar-header">
<h2>Data Wilayah</h2>
</div>
<div class="stats-strip">
<div class="stat-card miskin">
<div class="stat-icon">🔴</div>
<div class="label">Warga Miskin</div>
<div class="value" id="st-miskin"></div>
</div>
<div class="stat-card rentan">
<div class="stat-icon">🟡</div>
<div class="label">Rentan</div>
<div class="value" id="st-rentan"></div>
</div>
<div class="stat-card bantuan">
<div class="stat-icon">🟢</div>
<div class="label">Dibantu</div>
<div class="value" id="st-bantuan"></div>
</div>
<div class="stat-card lapp">
<div class="stat-icon">🚨</div>
<div class="label">Laporan Aktif</div>
<div class="value" id="st-laporan"></div>
</div>
</div>
<div class="filter-bar">
<button class="filter-chip active" data-f="semua" onclick="doFilter(this)">Semua</button>
<button class="filter-chip miskin" data-f="miskin" onclick="doFilter(this)">Miskin</button>
<button class="filter-chip rentan" data-f="rentan" onclick="doFilter(this)">Rentan</button>
<button class="filter-chip mampu" data-f="mampu" onclick="doFilter(this)">Mampu</button>
<button class="filter-chip ibadah" data-f="ibadah" onclick="doFilter(this)">Ibadah</button>
<button class="filter-chip laporan"data-f="laporan" onclick="doFilter(this)">Laporan</button>
</div>
<div class="list-container" id="list-container">
<div style="text-align:center;padding:20px;color:var(--muted);font-size:12px">Memuat data…</div>
</div>
</div>
<!-- Tombol toggle sidebar selalu terlihat -->
<button class="sidebar-toggle" id="sidebar-toggle" onclick="toggleSidebar()" title="Tutup panel">&#x276E;</button>
<!-- MAP -->
<div id="map"></div>
<!-- Layer Switcher satelit saja bawah tengah map -->
<div class="layer-switcher" id="layer-switcher">
<button class="layer-btn" id="btn-layer-satelit" onclick="toggleSatelit()" title="Aktifkan/nonaktifkan peta satelit">
🛰️ <span id="satelit-label">Satelit</span>
</button>
</div>
<!-- Legend kanan bawah map -->
<div class="legend-box" id="legend-box">
<div class="legend-title">Legenda</div>
<div class="legend-row"><div class="legend-dot" style="background:var(--miskin)"></div> Miskin</div>
<div class="legend-row"><div class="legend-dot" style="background:var(--rentan)"></div> Rentan</div>
<div class="legend-row"><div class="legend-dot" style="background:var(--mampu)"></div> Mampu</div>
<div class="legend-row"><div class="legend-dot" style="background:var(--ibadah)"></div> Rumah Ibadah</div>
<div class="legend-row"><div class="legend-dot" style="background:var(--laporan)"></div> Laporan</div>
</div>
<!-- Detail Panel -->
<div class="detail-panel" id="detail-panel"></div>
<!-- ===== DASHBOARD PANEL ===== -->
<div class="full-panel" id="panel-dashboard">
<h2 class="panel-title" style="margin-bottom:16px">📊 Dashboard</h2>
<!-- Stat cards -->
<div class="dash-grid" id="dash-grid">
<div style="color:var(--muted);font-size:13px">Memuat…</div>
</div>
<!-- 3 tabel sejajar: Rumah Ibadah | Laporan Terbaru | Keluarga Miskin -->
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:16px;margin-top:16px">
<div class="dash-table-wrap" id="dash-ibadah-table">
<div style="padding:14px 16px 10px;border-bottom:1px solid var(--border)">
<h3 style="font-family:'Fraunces',serif;font-size:14px;font-weight:700">🕌 Sebaran Rumah Ibadah</h3>
</div>
<table>
<thead><tr><th>NAMA</th><th>WARGA</th><th>MISKIN</th></tr></thead>
<tbody id="dash-ibadah-tbody"><tr><td colspan="3" style="text-align:center;color:var(--muted);padding:14px">Memuat…</td></tr></tbody>
</table>
</div>
<div class="dash-table-wrap" id="dash-laporan-table">
<div style="padding:14px 16px 10px;border-bottom:1px solid var(--border)">
<h3 style="font-family:'Fraunces',serif;font-size:14px;font-weight:700">📋 Laporan Terbaru</h3>
</div>
<table>
<thead><tr><th>PELAPOR</th><th>URGENSI</th><th>STATUS</th><th>TGL</th></tr></thead>
<tbody id="dash-laporan-tbody"><tr><td colspan="4" style="text-align:center;color:var(--muted);padding:14px">Memuat…</td></tr></tbody>
</table>
</div>
<div class="dash-table-wrap" id="dash-keluarga-table">
<div style="padding:14px 16px 10px;border-bottom:1px solid var(--border)">
<h3 style="font-family:'Fraunces',serif;font-size:14px;font-weight:700">🏠 Keluarga Belum Dibantu</h3>
</div>
<table>
<thead><tr><th>NO. KK</th><th>ALAMAT</th><th>ANGGOTA</th></tr></thead>
<tbody id="dash-keluarga-tbody"><tr><td colspan="3" style="text-align:center;color:var(--muted);padding:14px">Memuat…</td></tr></tbody>
</table>
</div>
</div>
</div>
<!-- ===== RUMAH IBADAH PANEL ===== -->
<div class="full-panel" id="panel-ibadah">
<div class="panel-toolbar">
<h2 class="panel-title">🕌 Manajemen Rumah Ibadah</h2>
<input type="text" class="search-box" id="ibadah-search" placeholder="🔍 Cari nama / alamat…" oninput="filterIbadah()">
<select id="ibadah-filter-jenis" onchange="filterIbadah()" style="background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:8px 12px;font-family:inherit;font-size:12.5px;color:var(--text)">
<option value="">Semua Jenis</option>
<option value="masjid">Masjid</option>
<option value="gereja">Gereja</option>
<option value="pura">Pura</option>
<option value="vihara">Vihara</option>
<option value="klenteng">Klenteng</option>
<option value="lainnya">Lainnya</option>
</select>
<button class="btn-primary" onclick="openModal('ibadah')" style="background:var(--ibadah)">+ Tambah</button>
</div>
<div class="data-table-wrap">
<div class="table-scroll">
<table>
<thead>
<tr>
<th>NAMA</th><th>JENIS</th><th>ALAMAT</th><th>KONTAK</th>
<th>RADIUS (m)</th><th>KOORDINAT</th><th>WARGA</th><th>MISKIN</th><th>AKSI</th>
</tr>
</thead>
<tbody id="ibadah-tbody">
<tr><td colspan="9" style="text-align:center;color:var(--muted);padding:20px">Memuat data rumah ibadah…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== KELUARGA PANEL ===== -->
<div class="full-panel" id="panel-keluarga">
<div class="panel-toolbar">
<h2 class="panel-title">🏠 Manajemen Data Keluarga (Kartu Keluarga)</h2>
<input type="text" class="search-box" id="kk-search" placeholder="🔍 Cari No. KK / Alamat…" oninput="filterKK()">
<select id="kk-filter-verif" onchange="filterKK()" style="background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:8px 12px;font-family:inherit;font-size:12.5px;color:var(--text)">
<option value="">Semua Verifikasi</option>
<option value="TERVERIFIKASI">Terverifikasi</option>
<option value="BELUM">Belum</option>
</select>
<button class="btn-primary" onclick="openModal('keluarga')">+ Tambah KK</button>
</div>
<div class="data-table-wrap">
<div class="table-scroll">
<table>
<thead>
<tr>
<th>NO. KK</th><th>ALAMAT</th><th>RT/RW</th><th>KECAMATAN</th>
<th>ANGGOTA</th><th>MISKIN</th><th>STATUS EKONOMI</th>
<th>VERIFIKASI</th><th>RUMAH IBADAH</th><th>AKSI</th>
</tr>
</thead>
<tbody id="kk-tbody">
<tr><td colspan="10" style="text-align:center;color:var(--muted);padding:20px">Memuat data keluarga…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- ===== PENDIDIKAN PANEL ===== -->
<div class="full-panel" id="panel-pendidikan">
<h2 class="panel-title" style="margin-bottom:6px">🎓 Riwayat Pendidikan &amp; Pelatihan</h2>
<p style="color:var(--muted);font-size:13px;margin-bottom:14px">Kelola data pendidikan formal dan pelatihan kerja penduduk, lengkap dengan validasi logika usia.</p>
<div class="validasi-info">
⚠️ <strong>Validasi Aktif:</strong> Sistem otomatis memvalidasi kesesuaian usia dengan jenjang pendidikan. Usia 712 tahun wajib SD, 1315 tahun wajib SMP, 1618 tahun wajib SMA/SMK.
</div>
<div class="pend-grid">
<!-- Form kiri -->
<div class="pend-form-card">
<h3>+ Tambah Riwayat Pendidikan / Pelatihan</h3>
<div class="form-group" style="margin-bottom:10px">
<label>Cari Penduduk *</label>
<input type="text" id="pend-cari" placeholder="Ketik nama atau NIK…" oninput="cariPendudukPend()">
<div id="pend-suggest" style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;margin-top:4px;display:none;max-height:150px;overflow-y:auto"></div>
<input type="hidden" id="pend-pid">
</div>
<div class="form-group" style="margin-bottom:10px">
<label>Jenis *</label>
<select id="pend-jenis">
<option value="formal">Pendidikan Formal</option>
<option value="pelatihan">Pelatihan / Kursus</option>
</select>
</div>
<div class="form-group" style="margin-bottom:10px">
<label>Nama Institusi</label>
<input type="text" id="pend-inst" placeholder="SDN 1 Pontianak / BLK Pontianak">
</div>
<div class="form-group" style="margin-bottom:10px">
<label>Jenjang / Nama Program</label>
<input type="text" id="pend-jenjang" placeholder="SD / SMP / Menjahit / Las">
</div>
<div class="form-group" style="margin-bottom:10px">
<label>Jurusan (jika ada)</label>
<input type="text" id="pend-jurusan" placeholder="IPA / Teknik / dll">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:10px">
<div class="form-group">
<label>Tahun Masuk</label>
<input type="number" id="pend-masuk" placeholder="2015">
</div>
<div class="form-group">
<label>Tahun Lulus</label>
<input type="number" id="pend-lulus" placeholder="2021 (kosong jika berjalan)">
</div>
</div>
<div class="form-group" style="margin-bottom:14px">
<label>Status</label>
<select id="pend-status">
<option value="lulus">Lulus</option>
<option value="sedang_belajar">Sedang Belajar</option>
<option value="putus">Putus Sekolah/Pelatihan</option>
</select>
</div>
<div id="pend-validasi-warn" style="display:none;color:var(--miskin);font-size:12px;margin-bottom:8px"></div>
<button class="btn-primary" onclick="simpanPendidikan()">💾 Simpan Riwayat</button>
</div>
<!-- Tabel kanan -->
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<h3 style="font-family:'Fraunces',serif;font-size:15px;font-weight:700">📋 Semua Riwayat Pendidikan</h3>
<input type="text" id="pend-search" placeholder="Cari nama…" oninput="filterPendidikan()" style="background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:6px 10px;font-family:inherit;font-size:12px;color:var(--text);width:140px">
</div>
<div class="data-table-wrap">
<table>
<thead><tr><th>PENDUDUK</th><th>JENIS</th><th>INSTITUSI</th><th>JENJANG</th><th>THN</th><th>STATUS</th><th>AKSI</th></tr></thead>
<tbody id="pend-tbody">
<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:16px">Memuat…</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ===== LAPORAN PANEL ===== -->
<div class="full-panel" id="panel-laporan">
<div class="panel-toolbar">
<h2 class="panel-title">🚨 Laporan Masyarakat</h2>
<button class="btn-primary" onclick="openModal('laporan')" style="background:var(--laporan)">+ Buat Laporan</button>
</div>
<div id="laporan-list">Memuat…</div>
</div>
<!-- ===== CHAT PANEL ===== -->
<div class="full-panel" id="panel-chat">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 class="panel-title">&#x1F4AC; Chat Koordinasi Pengurus</h2>
<select id="chat-to" style="background:var(--surface);border:1.5px solid var(--border);border-radius:8px;padding:7px 12px;font-family:inherit;font-size:12.5px;color:var(--text)">
<option value="">&#x1F4E2; Broadcast ke semua</option>
</select>
</div>
<div style="display:flex;flex-direction:column;height:calc(100vh - 160px);gap:12px">
<div id="chat-messages" style="flex:1;overflow-y:auto;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;display:flex;flex-direction:column;gap:8px">
<div style="text-align:center;color:var(--muted);font-size:13px;padding:20px">Memuat pesan...</div>
</div>
<div style="display:flex;gap:8px">
<input type="text" id="chat-input" placeholder="Ketik pesan koordinasi..." style="flex:1;background:var(--surface);border:1.5px solid var(--border);border-radius:10px;padding:11px 14px;font-family:inherit;font-size:13px;color:var(--text)" onkeydown="if(event.key==='Enter')kirimChat()">
<button class="btn-primary" onclick="kirimChat()" style="padding:11px 20px;border-radius:10px">Kirim &#10148;</button>
</div>
</div>
</div>
<!-- ===== LOG PANEL ===== -->
<div class="full-panel" id="panel-log">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
<h2 class="panel-title">&#x1F4CB; Log Aktivitas Sistem</h2>
<button class="btn-primary" onclick="loadLog()" style="background:var(--surface2);color:var(--text);border:1px solid var(--border)">&#x1F504; Refresh</button>
</div>
<div class="data-table-wrap">
<table>
<thead><tr><th>WAKTU</th><th>PENGGUNA</th><th>AKSI</th><th>TABEL</th><th>DETAIL</th><th>IP</th></tr></thead>
<tbody id="log-tbody"><tr><td colspan="6" style="text-align:center;color:var(--muted);padding:20px">Memuat log...</td></tr></tbody>
</table>
</div>
</div>
</div><!-- .main -->
</div><!-- #app -->
<!-- ================================================================
MODALS
================================================================ -->
<!-- Modal Penduduk -->
<div class="overlay" id="modal-penduduk">
<div class="modal">
<div class="modal-header">
<h2>Tambah Penduduk</h2>
<button class="dp-close" onclick="closeModal('penduduk')"></button>
</div>
<div class="modal-body">
<div class="form-grid">
<div class="form-group full"><label>NIK (16 digit)</label><input type="text" id="p-nik" maxlength="16" placeholder="6171010101800001"></div>
<div class="form-group full"><label>Nama Lengkap</label><input type="text" id="p-nama" placeholder="Nama lengkap sesuai KTP"></div>
<div class="form-group"><label>Jenis Kelamin</label><select id="p-jk"><option value="L">Laki-laki</option><option value="P">Perempuan</option></select></div>
<div class="form-group"><label>Tanggal Lahir</label><input type="date" id="p-tgl"></div>
<div class="form-group"><label>Status Keluarga</label><select id="p-statuskel"><option value="kepala_keluarga">Kepala Keluarga</option><option value="istri">Istri</option><option value="anak">Anak</option><option value="orang_tua">Orang Tua</option><option value="lainnya">Lainnya</option></select></div>
<div class="form-group"><label>Status Perkawinan</label><select id="p-kawin"><option value="belum_kawin">Belum Kawin</option><option value="kawin">Kawin</option><option value="cerai_hidup">Cerai Hidup</option><option value="cerai_mati">Cerai Mati</option></select></div>
<div class="form-group full"><label>Alamat</label><input type="text" id="p-alamat" placeholder="Jl. …"></div>
<div class="form-group"><label>No. HP</label><input type="text" id="p-hp" placeholder="08xxxxxxxxxx"></div>
<div class="form-group"><label>Pekerjaan</label><input type="text" id="p-pekerjaan" placeholder="Buruh, Pedagang…"></div>
<div class="form-group"><label>Penghasilan / Bulan (Rp)</label><input type="number" id="p-penghasilan" placeholder="0"></div>
<div class="form-group"><label>Status Ekonomi</label><select id="p-ekonomi"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div>
<div class="form-group"><label>Pendidikan Terakhir</label><select id="p-pendidikan"><option value="tidak_sekolah">Tidak Sekolah</option><option value="sd">SD</option><option value="smp">SMP</option><option value="sma">SMA/SMK</option><option value="d3">D3</option><option value="s1">S1</option><option value="s2">S2</option></select></div>
<div class="form-group"><label>Status Pendidikan</label><select id="p-statuspend"><option value="sekolah">Sedang Sekolah</option><option value="lulus">Lulus</option><option value="tidak_sekolah">Tidak Sekolah</option><option value="putus_sekolah">Putus Sekolah</option></select></div>
<div class="form-group"><label>Latitude</label><input type="number" step="any" id="p-lat" placeholder="-0.0231"></div>
<div class="form-group"><label>Longitude</label><input type="number" step="any" id="p-lng" placeholder="109.3294"></div>
<div class="form-group full"><button type="button" onclick="startPickCoord()" style="background:var(--accent2);color:#0f1117;border:none;border-radius:8px;padding:8px 14px;font-family:inherit;font-size:12.5px;font-weight:700;cursor:pointer;width:100%">&#x1F4CD; Klik Peta untuk Pilih Lokasi</button>
<div id="coord-display-penduduk" style="font-size:11px;color:var(--accent2);margin-top:4px;font-weight:600;min-height:16px"></div></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('penduduk')">Batal</button>
<button class="btn-primary" onclick="simpanPenduduk()">Simpan</button>
</div>
</div>
</div>
<!-- Modal Ibadah -->
<div class="overlay" id="modal-ibadah">
<div class="modal">
<div class="modal-header">
<h2>Tambah Rumah Ibadah</h2>
<button class="dp-close" onclick="closeModal('ibadah')"></button>
</div>
<div class="modal-body">
<div class="form-grid">
<div class="form-group full"><label>Nama</label><input type="text" id="i-nama" placeholder="Masjid Al-Ikhlas…"></div>
<div class="form-group"><label>Jenis</label><select id="i-jenis"><option value="masjid">Masjid</option><option value="gereja">Gereja</option><option value="pura">Pura</option><option value="vihara">Vihara</option><option value="klenteng">Klenteng</option><option value="lainnya">Lainnya</option></select></div>
<div class="form-group"><label>Kontak</label><input type="text" id="i-kontak" placeholder="0561-xxxxxx"></div>
<div class="form-group full"><label>Alamat</label><input type="text" id="i-alamat" placeholder="Jl. …"></div>
<div class="form-group"><label>Radius (meter)</label><input type="number" id="i-radius" value="500"></div>
<div class="form-group"><label>Latitude</label><input type="number" step="any" id="i-lat" placeholder="-0.0231"></div>
<div class="form-group"><label>Longitude</label><input type="number" step="any" id="i-lng" placeholder="109.3294"></div>
<div class="form-group full"><button type="button" onclick="startPickCoord()" style="background:var(--accent2);color:#0f1117;border:none;border-radius:8px;padding:8px 14px;font-family:inherit;font-size:12.5px;font-weight:700;cursor:pointer;width:100%">📍 Klik Peta untuk Pilih Lokasi</button></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('ibadah')">Batal</button>
<button class="btn-primary" onclick="simpanIbadah()" style="background:var(--ibadah)">Simpan</button>
</div>
</div>
</div>
<!-- Modal Laporan -->
<div class="overlay" id="modal-laporan">
<div class="modal">
<div class="modal-header">
<h2>🚨 Buat Laporan</h2>
<button class="dp-close" onclick="closeModal('laporan')"></button>
</div>
<div class="modal-body">
<div class="form-grid">
<div class="form-group full"><label>Nama Pelapor (Opsional)</label><input type="text" id="l-nama" placeholder="Anonim / Nama Anda"></div>
<div class="form-group full"><label>Deskripsi Kondisi</label><textarea id="l-desk" rows="3" placeholder="Jelaskan kondisi yang perlu dibantu…"></textarea></div>
<div class="form-group"><label>Tingkat Urgensi</label><select id="l-urg"><option value="rendah">Rendah</option><option value="sedang" selected>Sedang</option><option value="tinggi">Tinggi</option><option value="darurat">⚠️ DARURAT</option></select></div>
<div class="form-group full">
<label>Lokasi</label>
<input type="text" id="l-coord" placeholder="Belum dipilih…" readonly style="margin-bottom:6px">
<button type="button" onclick="startPickCoord()" style="background:var(--accent2);color:#0f1117;border:none;border-radius:8px;padding:8px 14px;font-family:inherit;font-size:12.5px;font-weight:700;cursor:pointer;width:100%">&#x1F4CD; Klik Peta untuk Pilih Lokasi Laporan</button>
<div id="coord-display-laporan" style="font-size:11px;color:var(--accent2);margin-top:4px;font-weight:600;min-height:16px"></div>
</div>
<input type="hidden" id="l-lat"><input type="hidden" id="l-lng">
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('laporan')">Batal</button>
<button class="btn-primary" onclick="simpanLaporan()" style="background:var(--laporan)">Kirim Laporan</button>
</div>
</div>
</div>
<!-- Modal Keluarga -->
<div class="overlay" id="modal-keluarga">
<div class="modal">
<div class="modal-header">
<h2>Tambah Kartu Keluarga</h2>
<button class="dp-close" onclick="closeModal('keluarga')"></button>
</div>
<div class="modal-body">
<div class="form-grid">
<div class="form-group full"><label>No. KK (16 digit)</label><input type="text" id="kk-nokk" maxlength="16" placeholder="6171012501700007"></div>
<div class="form-group full"><label>Alamat</label><input type="text" id="kk-alamat" placeholder="Jl. …"></div>
<div class="form-group"><label>RT/RW</label><input type="text" id="kk-rtrw" placeholder="001/003"></div>
<div class="form-group"><label>Kecamatan</label><input type="text" id="kk-kec" placeholder="Pontianak Kota"></div>
<div class="form-group"><label>Status Ekonomi</label><select id="kk-ekonomi"><option value="miskin">Miskin</option><option value="rentan" selected>Rentan</option><option value="mampu">Mampu</option></select></div>
<div class="form-group"><label>Latitude</label><input type="number" step="any" id="kk-lat" placeholder="-0.0231"></div>
<div class="form-group"><label>Longitude</label><input type="number" step="any" id="kk-lng" placeholder="109.3294"></div>
<div class="form-group full"><button type="button" onclick="startPickCoord()" style="background:var(--accent2);color:#0f1117;border:none;border-radius:8px;padding:8px 14px;font-family:inherit;font-size:12.5px;font-weight:700;cursor:pointer;width:100%">&#x1F4CD; Klik Peta untuk Pilih Lokasi</button>
<div id="coord-display-ibadah" style="font-size:11px;color:var(--accent2);margin-top:4px;font-weight:600;min-height:16px"></div></div>
</div>
</div>
<div class="modal-footer">
<button class="btn-cancel" onclick="closeModal('keluarga')">Batal</button>
<button class="btn-primary" onclick="simpanKeluarga()">Simpan KK</button>
</div>
</div>
</div>
<div class="toast" id="toast"></div>
<!-- ================================================================
JAVASCRIPT
================================================================ -->
<script>
// ===== DEMO USERS =====
const DEMO_USERS = {
'admin': { pass:'password', nama:'Administrator', role:'admin', avatar:'A' },
'pengurus1': { pass:'password', nama:'H. Ruslan', role:'pengurus', avatar:'R' },
'pengurus2': { pass:'password', nama:'Budi Santoso', role:'pengurus', avatar:'B' },
'pimpinan1': { pass:'password', nama:'Camat PKT', role:'pimpinan', avatar:'C' },
'warga1': { pass:'password', nama:'Dewi Lestari', role:'masyarakat',avatar:'D'},
};
let currentUser = null;
// ===== LOGIN =====
function doLogin() {
const u = document.getElementById('login-user').value.trim();
const p = document.getElementById('login-pass').value;
const errEl = document.getElementById('login-error');
if (DEMO_USERS[u] && DEMO_USERS[u].pass === p) {
currentUser = { username: u, ...DEMO_USERS[u] };
errEl.classList.remove('show');
document.getElementById('login-page').classList.add('hidden');
setTimeout(() => {
document.getElementById('login-page').style.display = 'none';
document.getElementById('app').style.display = 'flex';
document.getElementById('app').style.flexDirection = 'column';
document.getElementById('user-label').textContent = currentUser.nama;
document.getElementById('user-avatar').textContent = currentUser.avatar;
initApp();
}, 400);
} else {
errEl.classList.add('show');
document.getElementById('login-pass').value = '';
document.getElementById('login-pass').focus();
}
}
document.getElementById('login-pass').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
document.getElementById('login-user').addEventListener('keydown', e => {
if (e.key === 'Enter') document.getElementById('login-pass').focus();
});
function doLogout() {
currentUser = null;
document.getElementById('app').style.display = 'none';
document.getElementById('login-page').style.display = 'flex';
document.getElementById('login-page').classList.remove('hidden');
document.getElementById('login-user').value = '';
document.getElementById('login-pass').value = '';
}
// ===== THEME =====
function toggleTheme() {
const html = document.documentElement;
const isDark = html.getAttribute('data-theme') === 'dark';
html.setAttribute('data-theme', isDark ? 'light' : 'dark');
if (map) {
// refresh tile layer untuk light mode
}
}
// ===== TOGGLE SIDEBAR =====
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const toggle = document.getElementById('sidebar-toggle');
const panels = document.querySelectorAll('.full-panel');
const collapsed = sidebar.classList.toggle('collapsed');
// Update tombol: geser ikut sidebar, ikon berganti
if (collapsed) {
toggle.classList.add('collapsed-state');
toggle.innerHTML = '&#x276F;';
toggle.title = 'Buka panel';
} else {
toggle.classList.remove('collapsed-state');
toggle.innerHTML = '&#x276E;';
toggle.title = 'Tutup panel';
}
// Geser full-panel agar rata kiri saat sidebar ditutup
panels.forEach(p => {
p.style.left = collapsed ? '0' : '300px';
});
// Invalidate ukuran peta Leaflet
setTimeout(() => { if (map) map.invalidateSize(); }, 310);
}
// ===== TOGGLE SATELIT =====
let satelitOn = false;
function toggleSatelit() {
if (!map) return;
const btn = document.getElementById('btn-layer-satelit');
const label = document.getElementById('satelit-label');
if (!satelitOn) {
// Aktifkan satelit di atas layer utama
if (!tileLayers.satelit._map) tileLayers.satelit.addTo(map);
tileLayers.satelit.setZIndex(200);
btn.classList.add('active');
label.textContent = 'Satelit ✓';
satelitOn = true;
} else {
map.removeLayer(tileLayers.satelit);
btn.classList.remove('active');
label.textContent = 'Satelit';
satelitOn = false;
}
}
// ===== GANTI LAYER PETA (tetap ada untuk kompatibilitas) =====
function gantiLayer(nama) {
if (!map || nama === currentLayer) return;
if (tileLayers[currentLayer]) map.removeLayer(tileLayers[currentLayer]);
tileLayers[nama].addTo(map);
tileLayers[nama].bringToBack();
currentLayer = nama;
document.querySelectorAll('.layer-btn').forEach(b => b.classList.remove('active'));
document.getElementById('btn-layer-' + nama)?.classList.add('active');
const html = document.documentElement;
if (nama === 'gelap') html.setAttribute('data-theme', 'dark');
if (nama === 'terang') html.setAttribute('data-theme', 'light');
}
// ===== MAP =====
let map, mapInited = false;
let tileLayers = {};
let currentLayer = 'gelap';
let allData = [];
let markers = [];
let activeFilter = 'semua';
let activeModal = null;
const COLORS = { miskin:'#f05252', rentan:'#f59e0b', mampu:'#38d9a9', ibadah:'#818cf8', laporan:'#fb923c' };
const URG_COLORS = { rendah:'#38d9a9', sedang:'#f59e0b', tinggi:'#f05252', darurat:'#ff0000' };
const JENIS_EMOJI = { masjid:'🕌', gereja:'⛪', pura:'🛕', vihara:'🪷', klenteng:'🏮', lainnya:'🙏' };
function initApp() {
if (!mapInited) {
map = L.map('map', { center: [-0.0274, 109.3425], zoom: 14, zoomControl: true });
// Definisi semua layer tile
tileLayers = {
gelap: L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://openstreetmap.org">OSM</a> &copy; <a href="https://carto.com">CARTO</a>',
maxZoom: 19
}),
terang: L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://openstreetmap.org">OSM</a> &copy; <a href="https://carto.com">CARTO</a>',
maxZoom: 19
}),
satelit: L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: '&copy; <a href="https://www.esri.com">Esri</a>, Maxar, Earthstar Geographics',
maxZoom: 19
})
};
// Layer aktif default: gelap
currentLayer = 'gelap';
tileLayers.gelap.addTo(map);
map.on('click', e => {
const lat = e.latlng.lat.toFixed(6);
const lng = e.latlng.lng.toFixed(6);
handleMapClick(lat, lng);
});
// Leaflet tooltip styling
const s = document.createElement('style');
s.textContent = `.leaflet-tooltip-dark{background:#181c25!important;border:1px solid #2a3045!important;color:#e8eaf0!important;border-radius:8px!important;padding:5px 9px!important;font-family:'Plus Jakarta Sans',sans-serif!important;font-size:11.5px!important;box-shadow:0 3px 10px rgba(0,0,0,.5)!important}.leaflet-tooltip-dark::before{border-top-color:#2a3045!important}`;
document.head.appendChild(s);
mapInited = true;
}
loadData();
}
function loadData() {
fetch('ambil.php')
.then(r => r.json())
.then(data => {
allData = data;
renderMarkers();
renderList();
updateStripStats();
renderKKTable();
renderPendidikanTable();
})
.catch(() => showToast('Gagal memuat data','error'));
}
// ===== MARKERS =====
function renderMarkers() {
markers.forEach(m => { try { map.removeLayer(m); } catch(e){} });
markers = [];
allData.forEach(d => {
if (!d.lat || !d.lng) return;
if (!shouldShow(d)) return;
let color;
if (d.tipe==='ibadah') color = COLORS.ibadah;
else if (d.tipe==='laporan') color = URG_COLORS[d.urgensi] || COLORS.laporan;
else color = COLORS[d.status_ekonomi] || COLORS.rentan;
const icon = L.divIcon({
className: '',
html: `<div class="marker-dot" style="background:${color};box-shadow:0 0 0 3px ${color}33"></div>`,
iconSize: [14,14], iconAnchor: [7,7]
});
const lbl = d.tipe==='ibadah' ? d.nama : (d.tipe==='laporan' ? '⚠️ Laporan' : d.nama);
const m = L.marker([d.lat,d.lng],{icon}).addTo(map).on('click',()=>showDetail(d));
m.bindTooltip(`<b>${lbl}</b><br><small>${d.tipe}</small>`,{className:'leaflet-tooltip-dark',direction:'top',offset:[0,-10]});
markers.push(m);
if (d.tipe==='ibadah' && d.radius) {
const c = L.circle([d.lat,d.lng],{radius:d.radius,color:COLORS.ibadah,fillColor:COLORS.ibadah,fillOpacity:.05,weight:1.5,dashArray:'4 4'}).addTo(map);
markers.push(c);
}
});
}
function shouldShow(d) {
if (activeFilter==='semua') return true;
if (activeFilter==='ibadah') return d.tipe==='ibadah';
if (activeFilter==='laporan') return d.tipe==='laporan';
if (['miskin','rentan','mampu'].includes(activeFilter)) return d.tipe==='penduduk' && d.status_ekonomi===activeFilter;
return true;
}
// ===== LIST =====
function renderList() {
const c = document.getElementById('list-container');
const filtered = allData.filter(shouldShow);
if (!filtered.length) { c.innerHTML=`<div style="text-align:center;padding:20px;color:var(--muted);font-size:12px">Tidak ada data</div>`; return; }
c.innerHTML = filtered.map(d => {
let color, label, sub;
if (d.tipe==='ibadah') { color=COLORS.ibadah; label=d.nama; sub=`${JENIS_EMOJI[d.jenis]||'🙏'} ${d.jenis}`; }
else if (d.tipe==='penduduk') { color=COLORS[d.status_ekonomi]||COLORS.rentan; label=d.nama; sub=`${d.pekerjaan||'Tidak bekerja'} · Rp${fmtNum(d.penghasilan)}/bln`; }
else { color=URG_COLORS[d.urgensi]||COLORS.laporan; label=d.nama_pelapor||'Anonim'; sub=`⚠️ ${(d.urgensi||'').toUpperCase()} · ${d.status}`; }
const pillText = d.tipe==='ibadah' ? 'Ibadah' : (d.tipe==='laporan' ? 'Laporan' : cap(d.status_ekonomi));
return `<div class="list-item" onclick='showDetail(${safeJson(d)})'>
<div class="list-dot" style="background:${color}"></div>
<div class="list-info"><div class="list-nama">${label}</div><div class="list-sub">${sub}</div></div>
<div class="pill" style="background:${color}22;color:${color}">${pillText}</div>
</div>`;
}).join('');
}
// ===== DETAIL PANEL =====
function showDetail(d) {
const panel = document.getElementById('detail-panel');
let html = '';
if (d.tipe==='ibadah') {
html=`<div class="dp-header"><div><h3>${JENIS_EMOJI[d.jenis]||'🙏'} ${d.nama}</h3><div style="font-size:11px;color:var(--muted);margin-top:3px">${cap(d.jenis)} · Radius ${d.radius}m</div></div><button class="dp-close" onclick="closeDetail()">✕</button></div>
<div class="dp-body"><div class="dp-section"><div class="dp-section-title">Informasi</div>
<div class="dp-row"><span class="key">Alamat</span><span class="val">${d.alamat||'—'}</span></div>
<div class="dp-row"><span class="key">Kontak</span><span class="val">${d.kontak||'—'}</span></div>
<div class="dp-row"><span class="key">Koordinat</span><span class="val">${d.lat}, ${d.lng}</span></div>
</div></div>
<div class="dp-actions"><button class="btn-sm btn-hapus" onclick="hapus(${d.id},'ibadah')">🗑 Hapus</button></div>`;
} else if (d.tipe==='penduduk') {
const ec = COLORS[d.status_ekonomi]||COLORS.rentan;
html=`<div class="dp-header"><div><h3>${d.nama}</h3><div style="font-size:11px;margin-top:3px">
<span style="background:${ec}22;color:${ec};padding:1px 7px;border-radius:20px;font-size:10px;font-weight:700">${cap(d.status_ekonomi)}</span>
<span style="color:var(--muted);margin-left:6px">${d.jenis_kelamin==='L'?'♂':'♀'} ${d.umur??'?'} thn</span>
</div></div><button class="dp-close" onclick="closeDetail()">✕</button></div>
<div class="dp-body">
<div class="dp-section"><div class="dp-section-title">Data Diri</div>
<div class="dp-row"><span class="key">NIK</span><span class="val" style="font-family:monospace;font-size:11px">${d.nik||'—'}</span></div>
<div class="dp-row"><span class="key">Status KK</span><span class="val">${fmt(d.status_keluarga)}</span></div>
<div class="dp-row"><span class="key">Pekerjaan</span><span class="val">${d.pekerjaan||'—'}</span></div>
<div class="dp-row"><span class="key">Penghasilan</span><span class="val">Rp${fmtNum(d.penghasilan)}/bln</span></div>
<div class="dp-row"><span class="key">No. HP</span><span class="val">${d.no_hp||'—'}</span></div>
<div class="dp-row"><span class="key">Alamat</span><span class="val">${d.alamat||'—'}</span></div>
</div>
<div class="dp-section"><div class="dp-section-title">Pendidikan</div>
<div class="dp-row"><span class="key">Terakhir</span><span class="val">${(d.pendidikan_terakhir||'—').toUpperCase()}</span></div>
<div class="dp-row"><span class="key">Status</span><span class="val">${fmt(d.status_pendidikan)}</span></div>
</div>
<div class="dp-section"><div class="dp-section-title">Rumah Ibadah</div>
<div class="dp-row"><span class="key">Nama</span><span class="val">${d.nama_ibadah||'—'}</span></div>
</div>
<div class="dp-section"><div class="dp-section-title">Bantuan (${d.total_bantuan||0}x)</div>
<div id="dp-bantuan-list"><em style="color:var(--muted);font-size:11px">Memuat histori…</em></div>
</div>
</div>
<div class="dp-actions">
<button class="btn-sm btn-bantuan" onclick="showToast('Fitur catat bantuan tersedia','success')">🎁 Bantuan</button>
<button class="btn-sm btn-hapus" onclick="hapus(${d.id},'penduduk')">🗑 Hapus</button>
</div>`;
setTimeout(()=>{
fetch(`detail.php?id=${d.id}&tipe=penduduk`).then(r=>r.json()).then(full=>{
const el=document.getElementById('dp-bantuan-list');
if (!el) return;
if (!full.histori_bantuan?.length) { el.innerHTML='<em style="color:var(--muted);font-size:11px">Belum pernah menerima bantuan</em>'; return; }
el.innerHTML=full.histori_bantuan.slice(0,3).map(h=>`<div class="bantuan-item"><div><div style="font-weight:600">${h.nama_bantuan}</div><div style="color:var(--muted);font-size:10.5px">${h.tanggal} · ${h.sumber_bantuan||'—'}</div></div><span class="${h.status==='selesai'?'tag-selesai':'tag-proses'}">${h.status}</span></div>`).join('');
});
}, 100);
} else {
const uc = URG_COLORS[d.urgensi]||COLORS.laporan;
html=`<div class="dp-header"><div><h3>⚠️ Laporan</h3><div style="font-size:11px;margin-top:3px">
<span style="background:${uc}22;color:${uc};padding:1px 7px;border-radius:20px;font-size:10px;font-weight:700">${(d.urgensi||'').toUpperCase()}</span>
<span style="color:var(--muted);margin-left:6px">${d.status}</span>
</div></div><button class="dp-close" onclick="closeDetail()">✕</button></div>
<div class="dp-body">
<div class="dp-section"><div class="dp-section-title">Detail</div>
<div class="dp-row"><span class="key">Pelapor</span><span class="val">${d.nama_pelapor||'Anonim'}</span></div>
<div class="dp-row"><span class="key">Tanggal</span><span class="val">${(d.created_at||'—').slice(0,10)}</span></div>
</div>
<div class="dp-section"><div class="dp-section-title">Deskripsi</div><p style="font-size:12.5px;line-height:1.6">${d.deskripsi}</p></div>
</div>
<div class="dp-actions">
<button class="btn-sm btn-bantuan" onclick="ubahStatusLaporan(${d.id},'diproses')">✅ Proses</button>
<button class="btn-sm btn-hapus" onclick="hapus(${d.id},'laporan')">🗑 Hapus</button>
</div>`;
}
panel.innerHTML = html;
panel.classList.add('open');
if (d.lat && d.lng && map) map.panTo([d.lat,d.lng],{animate:true});
}
function closeDetail() { document.getElementById('detail-panel').classList.remove('open'); }
// ===== FILTER =====
function doFilter(btn) {
document.querySelectorAll('.filter-chip').forEach(b=>b.classList.remove('active'));
btn.classList.add('active');
activeFilter = btn.dataset.f;
renderMarkers(); renderList();
}
// ===== STATS =====
function updateStripStats() {
const p = allData.filter(d=>d.tipe==='penduduk');
document.getElementById('st-miskin').textContent = p.filter(d=>d.status_ekonomi==='miskin').length;
document.getElementById('st-rentan').textContent = p.filter(d=>d.status_ekonomi==='rentan').length;
document.getElementById('st-bantuan').textContent = p.filter(d=>d.total_bantuan>0).length;
const lp = allData.filter(d=>d.tipe==='laporan');
document.getElementById('st-laporan').textContent = lp.length;
document.getElementById('badge-laporan').textContent = `${lp.filter(d=>d.status==='pending').length} Pending`;
}
// ===== VIEWS =====
function setView(view, btn) {
document.querySelectorAll('.nav-btn').forEach(b=>b.classList.remove('active'));
if (btn) btn.classList.add('active');
document.querySelectorAll('.full-panel').forEach(p=>p.classList.remove('open'));
const legend = document.getElementById('legend-box');
const switcher = document.getElementById('layer-switcher');
if (view==='peta') {
legend.style.display='block';
if (switcher) switcher.style.display='flex';
} else {
legend.style.display='none';
if (switcher) switcher.style.display='none';
closeDetail();
const pEl = document.getElementById(`panel-${view}`);
pEl.classList.add('open');
const sb = document.getElementById('sidebar');
if (sb && sb.classList.contains('collapsed')) pEl.style.left = '0';
if (view==='dashboard') loadDashboard();
else if (view==='laporan') renderLaporanPanel();
else if (view==='ibadah') renderIbadahTable();
else if (view==='keluarga') renderKKTable();
else if (view==='pendidikan') renderPendidikanTable();
else if (view==='chat') {
loadChatUsers();
loadChat();
if (chatPollInterval) clearInterval(chatPollInterval);
chatPollInterval = setInterval(loadChat, 5000); // auto-refresh 5 detik
}
else if (view==='log') loadLog();
}
}
// ===== DASHBOARD =====
function loadDashboard() {
fetch('dashboard.php').then(r=>r.json()).then(s=>{
const p = s.penduduk;
const tot = p.total || 1;
// ---- Stat cards ----
document.getElementById('dash-grid').innerHTML=`
<div class="dash-card"><h4>Total Penduduk</h4><div class="big-num" style="color:var(--accent)">${p.total}</div><div class="sub-num">Warga terdaftar aktif</div></div>
<div class="dash-card"><h4>Miskin</h4><div class="big-num" style="color:var(--miskin)">${p.miskin}</div><div class="sub-num">${Math.round((p.miskin/tot)*100)||0}% dari total</div></div>
<div class="dash-card"><h4>Rentan</h4><div class="big-num" style="color:var(--rentan)">${p.rentan}</div><div class="sub-num">${Math.round((p.rentan/tot)*100)||0}% dari total</div></div>
<div class="dash-card"><h4>Belum Dibantu</h4><div class="big-num" style="color:var(--laporan)">${s.miskin_belum_dibantu.total}</div><div class="sub-num">Perlu intervensi segera</div></div>
<div class="dash-card"><h4>Bantuan Bulan Ini</h4><div class="big-num" style="color:var(--accent2)">${s.bantuan_bulan_ini.total}</div><div class="sub-num">${s.bantuan_bulan_ini.selesai||0} selesai disalurkan</div></div>
<div class="dash-card"><h4>Laporan Pending</h4><div class="big-num" style="color:var(--laporan)">${s.laporan.pending}</div><div class="sub-num">${s.laporan.darurat||0} darurat</div></div>`;
// ---- Tabel Rumah Ibadah ----
const ibadahRows = (s.per_rumah_ibadah||[]).map(r=>
`<tr>
<td style="font-size:12px">${JENIS_EMOJI[r.jenis]||'🙏'} ${r.nama}</td>
<td style="text-align:center">${r.jumlah_penduduk||0}</td>
<td style="text-align:center;color:var(--miskin);font-weight:700">${r.miskin||0}</td>
</tr>`
).join('');
const ibadahTbody = document.getElementById('dash-ibadah-tbody');
if (ibadahTbody) ibadahTbody.innerHTML = ibadahRows ||
'<tr><td colspan="3" style="text-align:center;color:var(--muted);padding:14px">Tidak ada data</td></tr>';
// ---- Tabel Laporan Terbaru ----
const laps = allData.filter(d=>d.tipe==='laporan').slice(0,7);
const sc = { pending:'var(--rentan)', diverifikasi:'var(--accent)', diproses:'var(--accent2)', selesai:'var(--mampu)' };
const lapRows = laps.map(l=>
`<tr>
<td style="font-size:12px">${l.nama_pelapor||'Anonim'}</td>
<td><span class="pill" style="background:${URG_COLORS[l.urgensi]||'#888'}22;color:${URG_COLORS[l.urgensi]||'#888'};font-size:10px">${(l.urgensi||'').toUpperCase()}</span></td>
<td><span class="pill" style="background:${sc[l.status]||'#88822'};color:${sc[l.status]||'var(--muted)'};font-size:10px">${l.status}</span></td>
<td style="font-size:11px;color:var(--muted)">${(l.created_at||'—').slice(0,10)}</td>
</tr>`
).join('');
const lapTbody = document.getElementById('dash-laporan-tbody');
if (lapTbody) lapTbody.innerHTML = lapRows ||
'<tr><td colspan="4" style="text-align:center;color:var(--muted);padding:14px">Tidak ada laporan</td></tr>';
// ---- Tabel Keluarga Miskin Belum Dibantu (dari allData) ----
const miskinBelum = allData.filter(d =>
d.tipe==='penduduk' && d.status_ekonomi==='miskin' && (d.total_bantuan||0)===0
).slice(0,7);
const kkRows = miskinBelum.map(d=>
`<tr>
<td style="font-family:monospace;font-size:11px">${d.no_kk||'—'}</td>
<td style="font-size:12px">${(d.alamat||'—').slice(0,30)}</td>
<td style="text-align:center;font-weight:700;color:var(--miskin)">${d.jumlah_anggota||1}</td>
</tr>`
).join('');
const kkTbody = document.getElementById('dash-keluarga-tbody');
if (kkTbody) kkTbody.innerHTML = kkRows ||
'<tr><td colspan="3" style="text-align:center;color:var(--mampu);padding:14px">Semua keluarga miskin sudah dibantu ✓</td></tr>';
}).catch(()=>{
document.getElementById('dash-grid').innerHTML =
'<div style="color:var(--miskin);font-size:13px;padding:10px">Gagal memuat dashboard. Pastikan dashboard.php ada.</div>';
});
}
// ===== RUMAH IBADAH TABLE =====
let ibadahData = [];
function renderIbadahTable(filter='', filterJenis='') {
const tbody = document.getElementById('ibadah-tbody');
if (!tbody) return;
ibadahData = allData.filter(d => d.tipe === 'ibadah');
let filtered = ibadahData;
if (filter) {
const q = filter.toLowerCase();
filtered = filtered.filter(d => (d.nama||'').toLowerCase().includes(q) || (d.alamat||'').toLowerCase().includes(q));
}
if (filterJenis) filtered = filtered.filter(d => d.jenis === filterJenis);
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:var(--muted);padding:20px">Tidak ada data rumah ibadah</td></tr>';
return;
}
// Hitung warga & miskin per rumah ibadah dari allData
const stats = {};
allData.filter(d=>d.tipe==='penduduk').forEach(p => {
const rid = p.rumah_ibadah_id;
if (!rid) return;
if (!stats[rid]) stats[rid] = { total:0, miskin:0 };
stats[rid].total++;
if (p.status_ekonomi==='miskin') stats[rid].miskin++;
});
tbody.innerHTML = filtered.map(d => {
const s = stats[d.id] || { total:0, miskin:0 };
const emoji = { masjid:'🕌', gereja:'⛪', pura:'🛕', vihara:'🪷', klenteng:'🏮', lainnya:'🙏' }[d.jenis] || '🙏';
return `<tr>
<td style="font-weight:600;font-size:12.5px">${d.nama}</td>
<td><span class="pill" style="background:var(--ibadah)22;color:var(--ibadah)">${emoji} ${d.jenis}</span></td>
<td style="font-size:12px;max-width:180px">${d.alamat||'—'}</td>
<td style="font-size:12px">${d.kontak||'—'}</td>
<td style="text-align:center">${d.radius||500}</td>
<td style="font-size:11px;color:var(--muted)">${d.lat?d.lat+', '+d.lng:'—'}</td>
<td style="text-align:center;font-weight:600">${s.total}</td>
<td style="text-align:center;font-weight:700;color:var(--miskin)">${s.miskin}</td>
<td>
<div style="display:flex;gap:5px;align-items:center">
<button class="btn-sm" style="background:var(--surface2);color:var(--text);padding:4px 8px" onclick='showDetail(${safeJson(d)})' title="Lihat di peta">📍</button>
<button class="btn-sm btn-hapus" style="padding:4px 8px" onclick="hapus(${d.id},'ibadah')" title="Hapus">🗑</button>
</div>
</td>
</tr>`;
}).join('');
}
function filterIbadah() {
renderIbadahTable(
document.getElementById('ibadah-search').value,
document.getElementById('ibadah-filter-jenis').value
);
}
// ===== KELUARGA TABLE =====
let kkData = [];
function renderKKTable(filter='', filterVerif='') {
// Ambil data keluarga dari endpoint baru, atau generate dari allData
const keluargaMap = {};
allData.filter(d=>d.tipe==='penduduk').forEach(p=>{
const kk = p.no_kk || `KK-${p.keluarga_id||p.id}`;
if (!keluargaMap[kk]) keluargaMap[kk]={ no_kk:kk, alamat:p.alamat, anggota:0, miskin:0, status_ekonomi:'mampu', nama_ibadah:p.nama_ibadah, verifikasi:'TERVERIFIKASI', rtrw:'—', kecamatan:'Pontianak Kota', id:p.keluarga_id };
keluargaMap[kk].anggota++;
if (p.status_ekonomi==='miskin') { keluargaMap[kk].miskin++; keluargaMap[kk].status_ekonomi='miskin'; }
else if (p.status_ekonomi==='rentan' && keluargaMap[kk].status_ekonomi!=='miskin') keluargaMap[kk].status_ekonomi='rentan';
});
kkData = Object.values(keluargaMap);
let filtered = kkData;
if (filter) filtered = filtered.filter(k=>(k.no_kk+k.alamat).toLowerCase().includes(filter.toLowerCase()));
if (filterVerif) filtered = filtered.filter(k=>k.verifikasi===filterVerif);
const statusColor = { miskin:'badge-miskin', rentan:'badge-rentan', mampu:'badge-mampu' };
const tbody = document.getElementById('kk-tbody');
if (!tbody) return;
if (!filtered.length) { tbody.innerHTML=`<tr><td colspan="10" style="text-align:center;color:var(--muted);padding:20px">Tidak ada data keluarga</td></tr>`; return; }
tbody.innerHTML = filtered.map(k=>`<tr>
<td style="font-family:monospace;font-size:11px">${k.no_kk}</td>
<td>${k.alamat||'—'}</td>
<td>${k.rtrw}</td>
<td>${k.kecamatan}</td>
<td>${k.anggota} org</td>
<td>${k.miskin>0?`<span style="background:var(--miskin);color:#fff;padding:1px 8px;border-radius:20px;font-weight:700;font-size:11px">${k.miskin}</span>`:'—'}</td>
<td><span class="badge badge-${k.status_ekonomi}">${k.status_ekonomi.toUpperCase()}</span></td>
<td><span class="badge ${k.verifikasi==='TERVERIFIKASI'?'badge-verif':'badge-belum'}">${k.verifikasi}</span></td>
<td style="font-size:11.5px">${k.nama_ibadah||'—'}</td>
<td>
<div style="display:flex;gap:5px;align-items:center">
<button class="btn-sm" style="background:var(--surface2);color:var(--muted);padding:4px 8px" title="Detail">👁</button>
<button class="btn-sm btn-edit" style="padding:4px 8px" title="Edit">✏️</button>
${k.verifikasi!=='TERVERIFIKASI'?`<button class="btn-sm btn-bantuan" style="padding:4px 8px" title="Verifikasi" onclick="showToast('KK berhasil diverifikasi','success')"></button>`:''}
<button class="btn-sm btn-hapus" style="padding:4px 8px" title="Hapus">🗑</button>
</div>
</td>
</tr>`).join('');
}
function filterKK() {
renderKKTable(
document.getElementById('kk-search').value,
document.getElementById('kk-filter-verif').value
);
}
// ===== PENDIDIKAN TABLE =====
let pendData = [];
function renderPendidikanTable(filter='') {
const tbody = document.getElementById('pend-tbody');
if (!tbody) return;
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:16px">Memuat...</td></tr>';
fetch('riwayat_pendidikan.php?action=list')
.then(r => r.json())
.then(rows => {
pendData = rows;
let filtered = rows;
if (filter) {
const q = filter.toLowerCase();
filtered = rows.filter(d =>
(d.penduduk_nama||'').toLowerCase().includes(q) ||
(d.nik||'').includes(filter)
);
}
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--muted);padding:16px">Belum ada riwayat pendidikan</td></tr>';
return;
}
tbody.innerHTML = filtered.map(d => `<tr>
<td><div style="font-weight:600;font-size:12.5px">${d.penduduk_nama||'—'}</div><div style="color:var(--muted);font-size:10.5px">${d.nik||''}</div></td>
<td><span class="pill" style="background:${d.jenis==='formal'?'var(--accent)':'var(--ibadah)'}22;color:${d.jenis==='formal'?'var(--accent)':'var(--ibadah)'}">${d.jenis}</span></td>
<td style="font-size:12px">${d.institusi||'—'}</td>
<td><span class="pill" style="background:var(--accent2)22;color:var(--accent2)">${(d.jenjang||'—').toUpperCase()}</span></td>
<td style="font-size:11.5px;color:var(--muted)">${d.thn_masuk||'—'} ${d.thn_lulus||'skrg'}</td>
<td><span class="badge ${d.status==='lulus'?'badge-mampu':d.status==='sedang_belajar'?'badge-rentan':'badge-belum'}">${d.status==='sedang_belajar'?'aktif':d.status||'—'}</span></td>
<td><button class="btn-sm btn-hapus" style="padding:3px 8px;font-size:11px" onclick="hapusPendidikan(${d.id})">🗑</button></td>
</tr>`).join('');
})
.catch(() => {
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center;color:var(--miskin);padding:16px">Gagal memuat data. Pastikan riwayat_pendidikan.php ada.</td></tr>';
});
}
function filterPendidikan() { renderPendidikanTable(document.getElementById('pend-search').value); }
// Cari penduduk untuk form pendidikan
function cariPendudukPend() {
const q = document.getElementById('pend-cari').value.toLowerCase();
const sug = document.getElementById('pend-suggest');
if (q.length < 2) { sug.style.display='none'; return; }
const res = allData.filter(d=>d.tipe==='penduduk' && (d.nama.toLowerCase().includes(q)||d.nik.includes(q))).slice(0,6);
if (!res.length) { sug.style.display='none'; return; }
sug.style.display='block';
sug.innerHTML = res.map(p=>`<div onclick="pilihPendudukPend(${p.id},'${p.nama}',${p.umur??0})" style="padding:8px 12px;cursor:pointer;font-size:12.5px;border-bottom:1px solid var(--border)" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background=''">${p.nama} <span style="color:var(--muted);font-size:11px">${p.nik}</span></div>`).join('');
}
function pilihPendudukPend(id, nama, umur) {
document.getElementById('pend-cari').value = nama;
document.getElementById('pend-pid').value = id;
document.getElementById('pend-suggest').style.display = 'none';
// Validasi usia
const warn = document.getElementById('pend-validasi-warn');
let msg = '';
if (umur>=7 && umur<=12) msg = `⚠️ Usia ${umur} thn: seharusnya masih SD.`;
else if (umur>=13 && umur<=15) msg = `⚠️ Usia ${umur} thn: seharusnya masih SMP.`;
else if (umur>=16 && umur<=18) msg = `⚠️ Usia ${umur} thn: seharusnya masih SMA/SMK.`;
warn.textContent = msg; warn.style.display = msg ? 'block' : 'none';
}
function simpanPendidikan() {
const pid = document.getElementById('pend-pid').value;
const jenjang = document.getElementById('pend-jenjang').value.trim();
if (!pid) return showToast('Pilih penduduk terlebih dahulu','error');
if (!jenjang) return showToast('Jenjang / nama program wajib diisi','error');
const me = chatUsers.find(u=>u.username===currentUser?.username);
const uid = me ? me.id : 0;
const fd = new FormData();
fd.append('action', 'simpan');
fd.append('penduduk_id', pid);
fd.append('jenis', document.getElementById('pend-jenis').value);
fd.append('institusi', document.getElementById('pend-inst').value);
fd.append('jenjang', jenjang);
fd.append('jurusan', document.getElementById('pend-jurusan').value);
fd.append('thn_masuk', document.getElementById('pend-masuk').value);
fd.append('thn_lulus', document.getElementById('pend-lulus').value);
fd.append('status', document.getElementById('pend-status').value);
fd.append('user_id', uid);
fetch('riwayat_pendidikan.php', {method:'POST', body:fd})
.then(r => r.json())
.then(res => {
if (res.status === 'success') {
showToast('Riwayat pendidikan disimpan ✓','success');
// Reset form
document.getElementById('pend-cari').value = '';
document.getElementById('pend-pid').value = '';
document.getElementById('pend-inst').value = '';
document.getElementById('pend-jenjang').value = '';
document.getElementById('pend-jurusan').value = '';
document.getElementById('pend-masuk').value = '';
document.getElementById('pend-lulus').value = '';
document.getElementById('pend-validasi-warn').style.display = 'none';
// Refresh tabel
renderPendidikanTable();
// Log
tambahLog('tambah_pendidikan','riwayat_pendidikan', res.id, 'Tambah riwayat pendidikan');
} else {
showToast('Error: '+(res.msg||'Gagal simpan'),'error');
}
})
.catch(() => showToast('Koneksi ke server gagal','error'));
}
function hapusPendidikan(id) {
if (!confirm('Hapus riwayat pendidikan ini?')) return;
const fd = new FormData();
fd.append('action','hapus'); fd.append('id', id);
fetch('riwayat_pendidikan.php', {method:'POST', body:fd})
.then(r=>r.json()).then(res=>{
if (res.status==='success') { showToast('Riwayat dihapus','success'); renderPendidikanTable(); }
else showToast('Gagal hapus','error');
});
}
// ===== LAPORAN PANEL =====
function renderLaporanPanel() {
const panel = document.getElementById('laporan-list');
const laps = allData.filter(d=>d.tipe==='laporan');
const statusColor = { pending:'var(--rentan)', diverifikasi:'var(--accent)', diproses:'var(--accent2)', selesai:'var(--mampu)' };
if (!laps.length) {
panel.innerHTML=`<div style="color:var(--muted);text-align:center;padding:40px;font-size:13px">Belum ada laporan</div>`;
return;
}
// Pakai tabel seperti section keluarga
panel.innerHTML = `<div class="data-table-wrap">
<table>
<thead>
<tr>
<th>PELAPOR</th>
<th>DESKRIPSI</th>
<th>URGENSI</th>
<th>STATUS</th>
<th>KOORDINAT</th>
<th>TANGGAL</th>
<th>AKSI</th>
</tr>
</thead>
<tbody>
${laps.map(l => `<tr>
<td style="font-weight:600;font-size:12.5px">${l.nama_pelapor||'Anonim'}</td>
<td style="max-width:220px;font-size:12px">${(l.deskripsi||'').slice(0,70)}${(l.deskripsi||'').length>70?'…':''}</td>
<td><span class="pill" style="background:${URG_COLORS[l.urgensi]||'#888'}22;color:${URG_COLORS[l.urgensi]||'#888'}">${(l.urgensi||'').toUpperCase()}</span></td>
<td><span class="badge badge-${l.status==='selesai'?'mampu':l.status==='pending'?'belum':'rentan'}">${l.status}</span></td>
<td style="font-size:11px;color:var(--muted)">${l.lat?l.lat+', '+l.lng:'—'}</td>
<td style="font-size:11.5px;color:var(--muted);white-space:nowrap">${(l.created_at||'—').slice(0,10)}</td>
<td>
<div style="display:flex;gap:5px;align-items:center;flex-wrap:nowrap">
<button class="btn-sm" style="background:var(--surface2);color:var(--text);padding:4px 8px" title="Detail" onclick='showLaporanDetail(${l.id})'>👁</button>
<button class="btn-sm btn-bantuan" style="padding:4px 8px" title="Proses" onclick="ubahStatusLaporan(${l.id},'diproses')">✅</button>
<button class="btn-sm btn-edit" style="padding:4px 8px" title="Selesai" onclick="ubahStatusLaporan(${l.id},'selesai')">🏁</button>
<button class="btn-sm btn-hapus" style="padding:4px 8px" title="Hapus" onclick="hapus(${l.id},'laporan')">🗑</button>
</div>
</td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
// Detail laporan via fetch langsung
function showLaporanDetail(id) {
fetch(`detail.php?id=${id}&tipe=laporan`)
.then(r => r.json())
.then(d => {
if (!d) return showToast('Data laporan tidak ditemukan','error');
const uc = URG_COLORS[d.urgensi] || '#888';
const sc = { pending:'var(--rentan)', diverifikasi:'var(--accent)', diproses:'var(--accent2)', selesai:'var(--mampu)' };
const panel = document.getElementById('detail-panel');
panel.innerHTML = `
<div class="dp-header">
<div>
<h3>⚠️ Laporan Masyarakat</h3>
<div style="font-size:11px;margin-top:3px">
<span style="background:${uc}22;color:${uc};padding:1px 7px;border-radius:20px;font-size:10px;font-weight:700">${(d.urgensi||'').toUpperCase()}</span>
<span style="background:${sc[d.status]||'#88822'};color:${sc[d.status]||'var(--muted)'};padding:1px 7px;border-radius:20px;font-size:10px;font-weight:700;margin-left:4px">${d.status||'—'}</span>
</div>
</div>
<button class="dp-close" onclick="closeDetail()">✕</button>
</div>
<div class="dp-body">
<div class="dp-section">
<div class="dp-section-title">Informasi</div>
<div class="dp-row"><span class="key">Pelapor</span><span class="val">${d.nama_pelapor||'Anonim'}</span></div>
<div class="dp-row"><span class="key">Terkait</span><span class="val">${d.nama_penduduk||'—'}</span></div>
<div class="dp-row"><span class="key">Tanggal</span><span class="val">${(d.created_at||'—').slice(0,16)}</span></div>
<div class="dp-row"><span class="key">Koordinat</span><span class="val">${d.lat?d.lat+', '+d.lng:'—'}</span></div>
</div>
<div class="dp-section">
<div class="dp-section-title">Deskripsi Kondisi</div>
<p style="font-size:12.5px;line-height:1.7;color:var(--text)">${d.deskripsi||'—'}</p>
</div>
</div>
<div class="dp-actions">
<button class="btn-sm btn-bantuan" onclick="ubahStatusLaporan(${d.id},'diproses')">✅ Proses</button>
<button class="btn-sm btn-edit" onclick="ubahStatusLaporan(${d.id},'selesai')">🏁 Selesai</button>
<button class="btn-sm btn-hapus" onclick="hapus(${d.id},'laporan')">🗑 Hapus</button>
</div>`;
panel.classList.add('open');
if (d.lat && d.lng && map) map.panTo([d.lat, d.lng], {animate:true});
})
.catch(() => showToast('Gagal memuat detail laporan','error'));
}
// ===== SIMPAN PENDUDUK =====
function simpanPenduduk() {
const nik=document.getElementById('p-nik').value.trim();
const nama=document.getElementById('p-nama').value.trim();
const lat=document.getElementById('p-lat').value;
const lng=document.getElementById('p-lng').value;
if (!nik||!nama) return showToast('NIK dan Nama wajib diisi','error');
if (!lat||!lng) return showToast('Klik peta untuk mengisi koordinat','error');
const fd=new FormData();
fd.append('tipe','penduduk'); fd.append('nik',nik); fd.append('nama',nama);
fd.append('jenis_kelamin',document.getElementById('p-jk').value);
fd.append('tanggal_lahir',document.getElementById('p-tgl').value);
fd.append('status_keluarga',document.getElementById('p-statuskel').value);
fd.append('status_perkawinan',document.getElementById('p-kawin').value);
fd.append('alamat',document.getElementById('p-alamat').value);
fd.append('no_hp',document.getElementById('p-hp').value);
fd.append('pekerjaan',document.getElementById('p-pekerjaan').value);
fd.append('penghasilan',document.getElementById('p-penghasilan').value||0);
fd.append('status_ekonomi',document.getElementById('p-ekonomi').value);
fd.append('pendidikan_terakhir',document.getElementById('p-pendidikan').value);
fd.append('status_pendidikan',document.getElementById('p-statuspend').value);
fd.append('lat',lat); fd.append('lng',lng);
fetch('simpan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
showToast('Penduduk berhasil disimpan ✓','success');
closeModal('penduduk');
loadData();
tambahLog('tambah_penduduk','penduduk',res.id,'Tambah penduduk: '+nama);
}
else showToast('Error: '+(res.msg||'Gagal'),'error');
});
}
// ===== SIMPAN IBADAH =====
function simpanIbadah() {
const nama=document.getElementById('i-nama').value.trim();
const lat=document.getElementById('i-lat').value;
const lng=document.getElementById('i-lng').value;
if (!nama) return showToast('Nama wajib diisi','error');
if (!lat||!lng) return showToast('Klik peta untuk mengisi koordinat','error');
const fd=new FormData();
fd.append('tipe','ibadah'); fd.append('nama',nama);
fd.append('jenis',document.getElementById('i-jenis').value);
fd.append('kontak',document.getElementById('i-kontak').value);
fd.append('alamat',document.getElementById('i-alamat').value);
fd.append('radius',document.getElementById('i-radius').value);
fd.append('lat',lat); fd.append('lng',lng);
fetch('simpan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
showToast('Rumah Ibadah ditambahkan ✓','success');
closeModal('ibadah');
loadData();
tambahLog('tambah_ibadah','rumah_ibadah',res.id,'Tambah rumah ibadah: '+document.getElementById('i-nama').value);
}
else showToast('Error: '+(res.msg||'Gagal'),'error');
});
}
// ===== SIMPAN LAPORAN =====
function simpanLaporan() {
const desk=document.getElementById('l-desk').value.trim();
const lat=document.getElementById('l-lat').value;
if (!desk) return showToast('Deskripsi wajib diisi','error');
if (!lat) return showToast('Klik peta untuk menandai lokasi','error');
const fd=new FormData();
fd.append('tipe','laporan');
fd.append('nama_pelapor',document.getElementById('l-nama').value||'Anonim');
fd.append('deskripsi',desk);
fd.append('urgensi',document.getElementById('l-urg').value);
fd.append('lat',lat); fd.append('lng',document.getElementById('l-lng').value);
fetch('simpan.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
showToast('Laporan berhasil dikirim ✓','success');
closeModal('laporan');
loadData();
renderLaporanPanel();
tambahLog('buat_laporan','laporan',res.id,'Buat laporan: '+document.getElementById('l-desk').value.slice(0,60));
}
else showToast('Gagal mengirim laporan','error');
});
}
// ===== SIMPAN KELUARGA =====
function simpanKeluarga() {
const nokk = document.getElementById('kk-nokk').value.trim();
const alamat = document.getElementById('kk-alamat').value.trim();
const lat = document.getElementById('kk-lat').value;
const lng = document.getElementById('kk-lng').value;
if (!nokk) return showToast('No. KK wajib diisi','error');
if (!lat || !lng) return showToast('Klik peta untuk mengisi koordinat','error');
const fd = new FormData();
fd.append('tipe','keluarga');
fd.append('no_kk', nokk);
fd.append('alamat', alamat);
fd.append('rtrw', document.getElementById('kk-rtrw').value);
fd.append('kecamatan', document.getElementById('kk-kec').value);
fd.append('lat', lat);
fd.append('lng', lng);
fetch('simpan.php', {method:'POST', body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
showToast('Kartu Keluarga berhasil disimpan ✓','success');
closeModal('keluarga');
loadData();
tambahLog('tambah_keluarga','keluarga',res.id,'Tambah KK: '+document.getElementById('kk-nokk').value);
} else {
showToast('Error: '+(res.msg||'Gagal simpan KK'),'error');
}
});
}
// ===== HAPUS =====
function hapus(id, tipe) {
if (!confirm('Yakin hapus data ini?')) return;
const fd=new FormData(); fd.append('id',id); fd.append('tipe',tipe);
fetch('hapus.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
showToast('Data berhasil dihapus','success');
closeDetail();
loadData();
tambahLog('hapus_data', tipe, id, 'Hapus data '+tipe+' ID '+id);
} else showToast('Gagal menghapus','error');
});
}
function ubahStatusLaporan(id, status) {
const fd=new FormData(); fd.append('id',id); fd.append('tipe','laporan'); fd.append('status',status);
fetch('edit.php',{method:'POST',body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') { showToast(`Status laporan → ${status}`,'success'); closeDetail(); loadData(); renderLaporanPanel(); }
});
}
// ===== MODAL =====
let pickingCoord = false; // mode pilih koordinat dari peta
function openModal(type) {
activeModal = type;
document.getElementById(`modal-${type}`).classList.add('open');
}
function closeModal(type) {
activeModal = null;
pickingCoord = false;
setPickMode(false);
document.getElementById(`modal-${type}`).classList.remove('open');
}
// Mode pilih koordinat: sembunyikan modal sementara, klik peta, lalu munculkan lagi
function startPickCoord() {
if (!activeModal) return;
pickingCoord = true;
setPickMode(true);
// Sembunyikan SELURUH overlay supaya peta bisa diklik
const overlay = document.getElementById(`modal-${activeModal}`);
overlay.style.opacity = '0';
overlay.style.pointerEvents = 'none';
// Tampilkan banner hint di atas peta
let hint = document.getElementById('pick-hint');
if (!hint) {
hint = document.createElement('div');
hint.id = 'pick-hint';
hint.style.cssText = 'position:absolute;top:60px;left:50%;transform:translateX(-50%);z-index:9000;background:var(--accent2);color:#0f1117;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:700;pointer-events:none;box-shadow:0 4px 16px rgba(0,0,0,.4)';
document.querySelector('.main').appendChild(hint);
}
hint.textContent = '📍 Klik titik di peta untuk memilih lokasi...';
hint.style.display = 'block';
}
function setPickMode(on) {
document.getElementById('map').style.cursor = on ? 'crosshair' : '';
}
// Saat map diklik — tangani koordinat atau keluar dari pick mode
function handleMapClick(lat, lng) {
if (!activeModal) return;
if (activeModal === 'penduduk') {
document.getElementById('p-lat').value = lat;
document.getElementById('p-lng').value = lng;
} else if (activeModal === 'ibadah') {
document.getElementById('i-lat').value = lat;
document.getElementById('i-lng').value = lng;
} else if (activeModal === 'laporan') {
document.getElementById('l-lat').value = lat;
document.getElementById('l-lng').value = lng;
document.getElementById('l-coord').value = lat + ', ' + lng;
} else if (activeModal === 'keluarga') {
document.getElementById('kk-lat').value = lat;
document.getElementById('kk-lng').value = lng;
}
// Update label koordinat di modal
const coordDisplay = document.getElementById(`coord-display-${activeModal}`);
if (coordDisplay) coordDisplay.textContent = '📍 ' + lat + ', ' + lng;
// Kembalikan modal
if (pickingCoord) {
pickingCoord = false;
setPickMode(false);
const el = document.getElementById(`modal-${activeModal}`);
el.style.opacity = '';
el.style.pointerEvents = '';
// Sembunyikan banner hint
const hint = document.getElementById('pick-hint');
if (hint) hint.style.display = 'none';
showToast('Koordinat berhasil diisi ✓','success');
}
}
// ===== HELPERS =====
function fmtNum(n) { return Number(n||0).toLocaleString('id-ID'); }
function fmt(s) { return (s||'').replace(/_/g,' ').replace(/\b\w/g,c=>c.toUpperCase()); }
function cap(s) { return (s||'').charAt(0).toUpperCase()+(s||'').slice(1); }
function safeJson(d) { return `JSON.parse(decodeURIComponent('${encodeURIComponent(JSON.stringify(d))}'))`; }
function showToast(msg, type='success') {
const t=document.getElementById('toast');
t.textContent=msg; t.className=`toast ${type} show`;
setTimeout(()=>t.classList.remove('show'),3000);
}
// ===== CHAT =====
let chatUsers = [];
let chatPollInterval = null;
function loadChatUsers() {
fetch('chat.php?action=users').then(r=>r.json()).then(users => {
chatUsers = users;
const sel = document.getElementById('chat-to');
if (!sel) return;
sel.innerHTML = '<option value="">📢 Broadcast ke semua</option>';
users.forEach(u => {
if (u.username !== currentUser?.username) {
const opt = document.createElement('option');
opt.value = u.id;
opt.textContent = u.nama_lengkap + ' (' + u.username + ')';
sel.appendChild(opt);
}
});
}).catch(()=>{});
}
function loadChat() {
fetch('chat.php?action=list&limit=60').then(r=>r.json()).then(msgs => {
const box = document.getElementById('chat-messages');
if (!box) return;
if (!msgs.length) { box.innerHTML = '<div style="text-align:center;color:var(--muted);font-size:13px;padding:20px">Belum ada pesan</div>'; return; }
box.innerHTML = msgs.map(m => {
const isSaya = m.username === currentUser?.username;
const isBcast = !m.ke;
const cls = isBcast ? 'broadcast' : (isSaya ? 'saya' : 'lain');
const wrapCls = isBcast ? '' : (isSaya ? 'saya' : 'lain');
const tgl = m.created_at ? m.created_at.slice(0,16).replace('T',' ') : '';
return `<div class="chat-wrap ${wrapCls}">
${!isSaya ? `<div class="chat-sender">${m.dari}</div>` : ''}
<div class="chat-bubble ${cls}">
${isBcast ? '<b>📢 Broadcast: </b>' : ''}${m.pesan}
<div class="chat-meta">${tgl}${m.ke ? ' → '+m.ke : ''}</div>
</div>
</div>`;
}).join('');
box.scrollTop = box.scrollHeight;
}).catch(()=>{});
}
function kirimChat() {
const input = document.getElementById('chat-input');
const pesan = input.value.trim();
if (!pesan) return;
if (!currentUser) return showToast('Login terlebih dahulu','error');
// Cari user_id dari username
const me = chatUsers.find(u=>u.username===currentUser.username);
const fromId = me ? me.id : 1;
const toId = document.getElementById('chat-to')?.value || '';
const fd = new FormData();
fd.append('action','kirim');
fd.append('from_user', fromId);
fd.append('to_user', toId);
fd.append('pesan', pesan);
fetch('chat.php', {method:'POST', body:fd}).then(r=>r.json()).then(res=>{
if (res.status==='success') {
input.value = '';
loadChat();
tambahLog('kirim_pesan','chat',res.id,'Mengirim pesan: '+pesan.slice(0,50));
} else {
showToast('Gagal mengirim pesan','error');
}
});
}
// ===== LOG =====
function loadLog() {
fetch('log.php?action=list&limit=100').then(r=>r.json()).then(logs => {
const tbody = document.getElementById('log-tbody');
if (!tbody) return;
if (!logs.length) { tbody.innerHTML='<tr><td colspan="6" style="text-align:center;color:var(--muted);padding:20px">Belum ada log aktivitas</td></tr>'; return; }
const AKSI_COLOR = {
create:'var(--accent2)', update:'var(--accent)', delete:'var(--miskin)',
kirim_pesan:'var(--ibadah)', login:'var(--rentan)'
};
tbody.innerHTML = logs.map(l => {
const aksiBase = (l.aksi||'').split('_')[0];
const col = AKSI_COLOR[aksiBase] || AKSI_COLOR[l.aksi] || 'var(--muted)';
const waktu = (l.created_at||'').slice(0,16).replace('T',' ');
return `<tr>
<td style="font-size:11.5px;color:var(--muted);white-space:nowrap">${waktu}</td>
<td>
<div style="font-weight:600;font-size:12px">${l.nama_user||'—'}</div>
<div style="color:var(--muted);font-size:10.5px">${l.username||''}</div>
</td>
<td><span class="pill" style="background:${col}22;color:${col}">${l.aksi||'—'}</span></td>
<td style="font-size:11.5px;color:var(--muted)">${l.tabel||'—'}</td>
<td style="font-size:12px;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${l.detail||'—'}</td>
<td style="font-size:11px;color:var(--muted)">${l.ip||'—'}</td>
</tr>`;
}).join('');
}).catch(()=>{ document.getElementById('log-tbody').innerHTML='<tr><td colspan="6" style="text-align:center;color:var(--miskin);padding:20px">Gagal memuat log. Pastikan file log.php ada.</td></tr>'; });
}
function tambahLog(aksi, tabel, recordId, detail) {
if (!currentUser) return;
// Coba dari chatUsers, fallback ke currentUser.id jika ada
const me = chatUsers.find(u=>u.username===currentUser.username);
const uid = me ? me.id : (currentUser.id || 1);
const fd = new FormData();
fd.append('action','tambah');
fd.append('user_id', uid);
fd.append('aksi', aksi);
fd.append('tabel', tabel);
fd.append('record_id', recordId||0);
fd.append('detail', detail);
fetch('log.php', {method:'POST', body:fd}).catch(()=>{});
}
</script>
</body>
</html>