2287 lines
102 KiB
PHP
2287 lines
102 KiB
PHP
<!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()">➡️ 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 | pengurus1 / password<br>pimpinan1 / password | 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">❮</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 & 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 7–12 tahun wajib SD, 13–15 tahun wajib SMP, 16–18 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">💬 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="">📢 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 ➤</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">📋 Log Aktivitas Sistem</h2>
|
||
<button class="btn-primary" onclick="loadLog()" style="background:var(--surface2);color:var(--text);border:1px solid var(--border)">🔄 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%">📍 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%">📍 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%">📍 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 = '❯';
|
||
toggle.title = 'Buka panel';
|
||
} else {
|
||
toggle.classList.remove('collapsed-state');
|
||
toggle.innerHTML = '❮';
|
||
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: '© <a href="https://openstreetmap.org">OSM</a> © <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: '© <a href="https://openstreetmap.org">OSM</a> © <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: '© <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>
|