2884 lines
134 KiB
HTML
2884 lines
134 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>WebGIS Pontianak — Pengentasan Kemiskinan Berbasis Rumah Ibadah</title>
|
|
<meta name="description" content="Sistem Informasi Geografis untuk pengentasan kemiskinan berbasis rumah ibadah di Kota Pontianak. Pemetaan keluarga miskin, laporan masyarakat, dan manajemen bantuan sosial." />
|
|
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" />
|
|
|
|
<style>
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--primary: #0E2240;
|
|
--primary-mid: #1A3A5E;
|
|
--primary-light: #2A5580;
|
|
--accent: #E8611A;
|
|
--accent-hover: #CF5516;
|
|
--accent-glow: rgba(232,97,26,0.15);
|
|
--accent-light: #FEF3EC;
|
|
|
|
--bg-page: #EDF1F7;
|
|
--bg-panel: #FFFFFF;
|
|
--bg-card: #FFFFFF;
|
|
--bg-input: #FFFFFF;
|
|
--bg-muted: #F4F7FC;
|
|
|
|
--border: #DDE3ED;
|
|
--border-light: #EDF1F7;
|
|
--border-focus: #E8611A;
|
|
|
|
--text-primary: #0D1B2A;
|
|
--text-secondary: #475569;
|
|
--text-muted: #8FA3BB;
|
|
|
|
--success: #059669;
|
|
--success-bg: #ECFDF5;
|
|
--warning: #D97706;
|
|
--warning-bg: #FFFBEB;
|
|
--danger: #DC2626;
|
|
--danger-bg: #FEF2F2;
|
|
--info: #2563EB;
|
|
--info-bg: #EFF6FF;
|
|
|
|
--color-sangat: #DC2626;
|
|
--bg-sangat: #FEF2F2;
|
|
--color-miskin: #D97706;
|
|
--bg-miskin: #FFFBEB;
|
|
--color-hampir: #2563EB;
|
|
--bg-hampir: #EFF6FF;
|
|
|
|
--shadow-xs: 0 1px 3px rgba(14,34,64,0.07);
|
|
--shadow-sm: 0 2px 8px rgba(14,34,64,0.09);
|
|
--shadow-md: 0 4px 20px rgba(14,34,64,0.11);
|
|
--shadow-lg: 0 8px 32px rgba(14,34,64,0.15);
|
|
--shadow-xl: 0 16px 48px rgba(14,34,64,0.2);
|
|
|
|
--radius-sm: 6px;
|
|
--radius: 10px;
|
|
--radius-lg: 14px;
|
|
--radius-xl: 20px;
|
|
|
|
--transition: all 0.2s cubic-bezier(0.4,0,0.2,1);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', -apple-system, system-ui, sans-serif;
|
|
background: var(--bg-page);
|
|
color: var(--text-secondary);
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
line-height: 1.6;
|
|
-webkit-font-smoothing: antialiased;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* === TOPBAR === */
|
|
.topbar {
|
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-mid) 100%);
|
|
height: 64px;
|
|
padding: 0 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-shrink: 0;
|
|
z-index: 1000;
|
|
box-shadow: 0 2px 16px rgba(14,34,64,0.4);
|
|
border-bottom: 1px solid rgba(255,255,255,0.04);
|
|
}
|
|
.topbar-brand { display: flex; align-items: center; gap: 12px; }
|
|
.brand-logo {
|
|
width: 40px; height: 40px;
|
|
background: linear-gradient(135deg, var(--accent) 0%, #FF8340 100%);
|
|
border-radius: 10px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 1rem; color: #fff; flex-shrink: 0;
|
|
box-shadow: 0 2px 10px rgba(232,97,26,0.5);
|
|
}
|
|
.brand-name {
|
|
font-size: 0.95rem; font-weight: 800; color: #fff;
|
|
letter-spacing: -0.02em; line-height: 1.15;
|
|
}
|
|
.brand-sub {
|
|
font-size: 0.62rem; color: rgba(255,255,255,0.42);
|
|
letter-spacing: 0.05em; font-weight: 500; margin-top: 1px;
|
|
}
|
|
.topbar-nav {
|
|
position: absolute; left: 50%; transform: translateX(-50%);
|
|
display: flex; gap: 3px;
|
|
background: rgba(0,0,0,0.28);
|
|
padding: 4px; border-radius: 12px;
|
|
border: 1px solid rgba(255,255,255,0.07);
|
|
}
|
|
.tab-btn {
|
|
padding: 7px 20px; border: none; border-radius: 9px;
|
|
background: transparent; color: rgba(255,255,255,0.52);
|
|
font-family: inherit; font-size: 0.8rem; font-weight: 600;
|
|
cursor: pointer; transition: var(--transition); white-space: nowrap;
|
|
letter-spacing: 0.01em;
|
|
}
|
|
.tab-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
|
|
.tab-btn.active {
|
|
background: var(--accent);
|
|
color: #fff; font-weight: 700;
|
|
box-shadow: 0 2px 10px rgba(232,97,26,0.55);
|
|
}
|
|
.topbar-right { display: flex; align-items: center; gap: 12px; }
|
|
#user-info {
|
|
color: rgba(255,255,255,0.68); font-size: 0.75rem; font-weight: 500;
|
|
display: flex; align-items: center; gap: 6px;
|
|
}
|
|
#user-info i { opacity: 0.65; }
|
|
#btn-auth {
|
|
background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.18);
|
|
color: #fff; padding: 7px 16px; border-radius: var(--radius-sm);
|
|
font-size: 0.75rem; font-weight: 600; cursor: pointer;
|
|
transition: var(--transition); font-family: inherit;
|
|
display: inline-flex; align-items: center; gap: 7px;
|
|
}
|
|
#btn-auth:hover { background: rgba(255,255,255,0.2); }
|
|
|
|
/* === LAYOUT === */
|
|
.app-body { display: flex; flex: 1; overflow: hidden; }
|
|
#map { flex: 1; height: 100%; z-index: 1; }
|
|
|
|
/* === SIDEBAR === */
|
|
.sidebar {
|
|
width: 390px; min-width: 390px;
|
|
background: var(--bg-page);
|
|
border-left: 1px solid var(--border);
|
|
display: flex; flex-direction: column;
|
|
overflow: hidden; z-index: 2;
|
|
}
|
|
.sidebar-header {
|
|
height: 56px; padding: 0 16px;
|
|
background: var(--bg-panel);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
flex-shrink: 0;
|
|
box-shadow: 0 1px 4px rgba(14,34,64,0.06);
|
|
}
|
|
.sidebar-title {
|
|
display: flex; align-items: center; gap: 10px;
|
|
}
|
|
.sidebar-title-badge {
|
|
width: 30px; height: 30px;
|
|
background: var(--accent-light);
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
.sidebar-title-badge i { font-size: 0.78rem; color: var(--accent); }
|
|
.sidebar-title-text {
|
|
font-size: 0.82rem; font-weight: 800; color: var(--text-primary);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
.status-dot {
|
|
width: 6px; height: 6px; border-radius: 50%;
|
|
background: var(--success);
|
|
box-shadow: 0 0 0 2px rgba(5,150,105,0.2);
|
|
animation: dotPulse 2.5s infinite;
|
|
}
|
|
@keyframes dotPulse {
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
50% { opacity: 0.35; transform: scale(0.6); }
|
|
}
|
|
|
|
/* === BUTTONS === */
|
|
.btn-primary {
|
|
padding: 7px 14px; background: var(--accent); color: #fff;
|
|
border: none; border-radius: var(--radius-sm);
|
|
font-family: inherit; font-size: 0.75rem; font-weight: 700;
|
|
cursor: pointer; transition: var(--transition);
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
box-shadow: 0 2px 6px rgba(232,97,26,0.3);
|
|
}
|
|
.btn-primary:hover {
|
|
background: var(--accent-hover);
|
|
box-shadow: 0 4px 12px rgba(232,97,26,0.45);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
/* === FILTER BAR === */
|
|
.filter-bar {
|
|
display: flex; gap: 8px; padding: 10px 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
flex-wrap: wrap; background: var(--bg-panel);
|
|
}
|
|
.filter-select {
|
|
padding: 6px 10px; border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm); background: var(--bg-muted);
|
|
color: var(--text-primary); font-family: inherit;
|
|
font-size: 0.72rem; font-weight: 600; outline: none;
|
|
cursor: pointer; transition: var(--transition); flex: 1; min-width: 100px;
|
|
}
|
|
.filter-select:focus { border-color: var(--border-focus); background: #fff; }
|
|
|
|
/* === LEGEND === */
|
|
.legend-bar {
|
|
padding: 8px 12px; border-bottom: 1px solid var(--border);
|
|
display: flex; flex-wrap: wrap; gap: 10px;
|
|
flex-shrink: 0; background: var(--bg-panel);
|
|
}
|
|
.legend-item {
|
|
display: flex; align-items: center; gap: 6px;
|
|
font-size: 0.66rem; color: var(--text-secondary); font-weight: 700;
|
|
text-transform: uppercase; letter-spacing: 0.04em;
|
|
}
|
|
.legend-dot {
|
|
width: 9px; height: 9px; border-radius: 50%;
|
|
border: 2px solid rgba(255,255,255,0.9);
|
|
box-shadow: 0 0 0 1px rgba(0,0,0,0.14); flex-shrink: 0;
|
|
}
|
|
|
|
/* === DATA LIST === */
|
|
.data-list {
|
|
flex: 1; overflow-y: auto; padding: 10px 10px;
|
|
background: var(--bg-page);
|
|
}
|
|
.data-list::-webkit-scrollbar { width: 3px; }
|
|
.data-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
|
|
/* === DATA CARD === */
|
|
.data-card {
|
|
background: var(--bg-card); border: 1px solid var(--border);
|
|
border-left: 3px solid transparent;
|
|
border-radius: var(--radius); padding: 12px 14px 12px 13px;
|
|
margin-bottom: 7px; cursor: pointer;
|
|
transition: var(--transition); box-shadow: var(--shadow-xs);
|
|
}
|
|
.data-card:hover {
|
|
border-left-color: var(--accent);
|
|
box-shadow: var(--shadow-sm);
|
|
transform: translateX(2px);
|
|
}
|
|
.data-card.active-card {
|
|
border-left-color: var(--accent);
|
|
border-color: rgba(232,97,26,0.25);
|
|
box-shadow: 0 0 0 3px var(--accent-glow), var(--shadow-sm);
|
|
}
|
|
|
|
.card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
|
|
.card-name {
|
|
font-size: 0.875rem; font-weight: 700; color: var(--text-primary);
|
|
line-height: 1.35; letter-spacing: -0.01em;
|
|
}
|
|
.badge {
|
|
font-size: 0.62rem; font-weight: 700; padding: 3px 8px;
|
|
border-radius: 4px; white-space: nowrap; flex-shrink: 0;
|
|
letter-spacing: 0.04em; text-transform: uppercase;
|
|
}
|
|
.card-meta {
|
|
font-size: 0.7rem; color: var(--text-muted); margin-top: 6px;
|
|
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
|
}
|
|
.card-meta span { display: inline-flex; align-items: center; gap: 3px; }
|
|
.card-meta i { font-size: 0.62rem; }
|
|
.card-actions { display: flex; gap: 5px; align-items: center; margin-top: 10px; }
|
|
|
|
.btn-sm {
|
|
padding: 5px 10px; border-radius: var(--radius-sm);
|
|
font-size: 0.68rem; font-weight: 700; font-family: inherit;
|
|
cursor: pointer; transition: var(--transition);
|
|
display: inline-flex; align-items: center; gap: 4px;
|
|
}
|
|
.btn-zoom { background: var(--bg-muted); border: 1px solid var(--border); color: var(--text-secondary); }
|
|
.btn-zoom:hover { background: var(--border); color: var(--text-primary); }
|
|
.btn-info-sm { background: var(--accent); border: 1px solid var(--accent); color: #fff; }
|
|
.btn-info-sm:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
|
|
.btn-edit { background: var(--bg-muted); border: 1px solid var(--border); color: var(--text-secondary); }
|
|
.btn-edit:hover { background: var(--border); color: var(--text-primary); }
|
|
.btn-del { background: transparent; border: 1px solid var(--border); color: var(--danger); }
|
|
.btn-del:hover { background: var(--danger-bg); border-color: rgba(220,38,38,0.3); }
|
|
|
|
.empty-state { text-align: center; padding: 52px 24px; color: var(--text-muted); }
|
|
.empty-state i { font-size: 2rem; margin-bottom: 12px; opacity: 0.14; display: block; }
|
|
.empty-state p { font-size: 0.8rem; line-height: 1.6; }
|
|
|
|
/* === STATS STRIP === */
|
|
.stats-strip {
|
|
display: flex; border-top: 1px solid var(--border);
|
|
flex-shrink: 0; background: var(--bg-panel);
|
|
}
|
|
.stat-item {
|
|
flex: 1; padding: 10px 8px; text-align: center;
|
|
border-right: 1px solid var(--border-light);
|
|
}
|
|
.stat-item:last-child { border-right: none; }
|
|
.stat-num {
|
|
font-size: 1.15rem; font-weight: 800; color: var(--text-primary);
|
|
line-height: 1.1; letter-spacing: -0.03em;
|
|
}
|
|
.stat-label {
|
|
font-size: 0.57rem; color: var(--text-muted); margin-top: 2px;
|
|
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 700;
|
|
}
|
|
|
|
/* === WALIKOTA DASHBOARD === */
|
|
.dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; padding: 10px; }
|
|
.dash-card {
|
|
background: var(--bg-panel); border: 1px solid var(--border);
|
|
border-radius: var(--radius); padding: 14px 15px; box-shadow: var(--shadow-xs);
|
|
}
|
|
.dash-card-title {
|
|
font-size: 0.6rem; font-weight: 800; color: var(--text-muted);
|
|
text-transform: uppercase; letter-spacing: 0.09em; margin-bottom: 8px;
|
|
}
|
|
.dash-card-value { font-size: 1.5rem; font-weight: 800; color: var(--text-primary); letter-spacing: -0.03em; }
|
|
.dash-card-sub { font-size: 0.68rem; color: var(--text-muted); margin-top: 4px; }
|
|
.dash-list { margin-top: 4px; }
|
|
.dash-list-item {
|
|
display: flex; justify-content: space-between;
|
|
font-size: 0.7rem; padding: 5px 0; border-bottom: 1px solid var(--border-light);
|
|
}
|
|
.dash-list-item:last-child { border: none; }
|
|
.dash-list-item span:first-child { color: var(--text-secondary); }
|
|
.dash-list-item span:last-child { font-weight: 700; color: var(--text-primary); }
|
|
|
|
/* === MAP INSTRUCTION BANNER === */
|
|
.map-instruction {
|
|
position: absolute; top: 76px; left: 50%; transform: translateX(-50%);
|
|
background: var(--primary); color: #fff;
|
|
border-radius: var(--radius); padding: 10px 24px;
|
|
font-size: 0.78rem; font-weight: 600;
|
|
z-index: 1000; display: none; pointer-events: none;
|
|
white-space: nowrap; box-shadow: var(--shadow-lg);
|
|
}
|
|
.map-instruction.visible {
|
|
display: flex; align-items: center; gap: 8px;
|
|
animation: bannerIn .25s cubic-bezier(0.4,0,0.2,1);
|
|
}
|
|
@keyframes bannerIn {
|
|
from { opacity:0; transform:translateX(-50%) translateY(-10px); }
|
|
to { opacity:1; transform:translateX(-50%) translateY(0); }
|
|
}
|
|
|
|
/* === FAB REPORT BUTTON === */
|
|
.fab-report {
|
|
position: fixed; bottom: 28px; left: 28px; z-index: 999;
|
|
background: linear-gradient(135deg, var(--accent) 0%, #FF8340 100%);
|
|
color: #fff; border: none; border-radius: 100px;
|
|
padding: 13px 26px;
|
|
font-family: inherit; font-size: 0.82rem; font-weight: 700;
|
|
cursor: pointer; display: flex; align-items: center; gap: 9px;
|
|
box-shadow: 0 4px 20px rgba(232,97,26,0.5);
|
|
transition: var(--transition); letter-spacing: 0.01em;
|
|
}
|
|
.fab-report:hover {
|
|
box-shadow: 0 8px 32px rgba(232,97,26,0.62);
|
|
transform: translateY(-3px);
|
|
}
|
|
.fab-report:active { transform: translateY(-1px); }
|
|
|
|
/* === MODALS === */
|
|
.modal-overlay {
|
|
position: fixed; inset: 0;
|
|
background: rgba(8,18,40,0.6);
|
|
backdrop-filter: blur(5px);
|
|
z-index: 9000; display: flex; align-items: center; justify-content: center;
|
|
opacity: 0; pointer-events: none; transition: opacity .2s ease;
|
|
}
|
|
.modal-overlay.open { opacity: 1; pointer-events: all; }
|
|
.modal {
|
|
background: var(--bg-panel); border: 1px solid var(--border);
|
|
border-radius: var(--radius-xl); width: 520px;
|
|
max-width: 95vw; max-height: 90vh; overflow-y: auto;
|
|
box-shadow: var(--shadow-xl);
|
|
transform: scale(.96) translateY(-18px);
|
|
transition: transform .3s cubic-bezier(0.34,1.56,0.64,1);
|
|
}
|
|
.modal-overlay.open .modal { transform: scale(1) translateY(0); }
|
|
.modal::-webkit-scrollbar { width: 4px; }
|
|
.modal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
|
|
|
|
.modal-header {
|
|
padding: 18px 22px; border-bottom: 1px solid var(--border);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
background: var(--bg-muted);
|
|
border-radius: var(--radius-xl) var(--radius-xl) 0 0;
|
|
}
|
|
.modal-title {
|
|
font-size: 0.9rem; font-weight: 800; color: var(--text-primary);
|
|
display: flex; align-items: center; gap: 10px; letter-spacing: -0.01em;
|
|
}
|
|
.modal-icon {
|
|
width: 32px; height: 32px; background: var(--accent-light);
|
|
border-radius: 8px; display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.modal-icon i { color: var(--accent); font-size: 0.82rem; }
|
|
.modal-close {
|
|
background: none; border: none; color: var(--text-muted);
|
|
cursor: pointer; font-size: 1rem;
|
|
width: 32px; height: 32px; border-radius: var(--radius-sm);
|
|
display: flex; align-items: center; justify-content: center;
|
|
transition: var(--transition);
|
|
}
|
|
.modal-close:hover { background: var(--border); color: var(--text-primary); }
|
|
.modal-body { padding: 22px 24px; }
|
|
|
|
/* === FORMS === */
|
|
.form-group { margin-bottom: 14px; }
|
|
.form-row { display: flex; gap: 12px; }
|
|
.form-row .form-group { flex: 1; }
|
|
.form-label {
|
|
display: block; font-size: 0.67rem; font-weight: 800;
|
|
color: var(--text-secondary); margin-bottom: 6px;
|
|
text-transform: uppercase; letter-spacing: 0.06em;
|
|
}
|
|
.form-control {
|
|
width: 100%; background: var(--bg-input);
|
|
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
|
color: var(--text-primary); font-family: inherit;
|
|
font-size: 0.85rem; padding: 9px 12px;
|
|
transition: var(--transition); outline: none;
|
|
}
|
|
.form-control:focus {
|
|
border-color: var(--border-focus);
|
|
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
}
|
|
.form-control::placeholder { color: var(--text-muted); }
|
|
textarea.form-control { resize: vertical; min-height: 70px; }
|
|
.form-info {
|
|
font-size: 0.65rem; color: var(--text-muted);
|
|
margin-top: 4px; display: flex; align-items: center; gap: 4px;
|
|
}
|
|
.btn-pick-map {
|
|
display: flex; align-items: center; gap: 8px;
|
|
padding: 10px 16px; background: var(--bg-muted);
|
|
border: 1.5px dashed var(--border); color: var(--text-secondary);
|
|
border-radius: var(--radius-sm); font-family: inherit;
|
|
font-size: 0.78rem; font-weight: 600;
|
|
cursor: pointer; transition: var(--transition);
|
|
width: 100%; justify-content: center;
|
|
}
|
|
.btn-pick-map:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-light); }
|
|
.btn-pick-map i { color: var(--accent); }
|
|
.coord-display { display: flex; gap: 8px; margin-top: 8px; }
|
|
.coord-display .form-control {
|
|
background: var(--bg-muted); border-color: var(--border-light);
|
|
color: var(--text-secondary); font-size: 0.72rem; font-family: 'Courier New', monospace;
|
|
}
|
|
.checkbox-group { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
.checkbox-tag {
|
|
display: flex; align-items: center; gap: 4px;
|
|
padding: 5px 10px; border: 1px solid var(--border);
|
|
border-radius: 4px; font-size: 0.7rem; font-weight: 600;
|
|
color: var(--text-secondary); cursor: pointer;
|
|
transition: var(--transition); user-select: none;
|
|
}
|
|
.checkbox-tag:hover { border-color: var(--accent); color: var(--accent); }
|
|
.checkbox-tag.checked { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
.checkbox-tag input { display: none; }
|
|
.penyakit-tag {
|
|
display: inline-block; padding: 2px 7px;
|
|
border-radius: 4px; font-size: 0.65rem; font-weight: 600;
|
|
border: 1px solid var(--border); color: var(--text-secondary);
|
|
background: var(--bg-muted); margin: 2px;
|
|
}
|
|
.modal-footer {
|
|
padding: 14px 24px 20px; border-top: 1px solid var(--border);
|
|
display: flex; gap: 10px; justify-content: flex-end;
|
|
background: var(--bg-muted);
|
|
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
|
}
|
|
.btn-cancel {
|
|
padding: 9px 20px; background: transparent;
|
|
border: 1px solid var(--border); color: var(--text-secondary);
|
|
border-radius: var(--radius-sm); font-family: inherit;
|
|
font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: var(--transition);
|
|
}
|
|
.btn-cancel:hover { border-color: #94A3B8; color: var(--text-primary); background: var(--bg-muted); }
|
|
.btn-save {
|
|
padding: 9px 22px; background: var(--accent); border: none;
|
|
color: #fff; border-radius: var(--radius-sm);
|
|
font-family: inherit; font-size: 0.8rem; font-weight: 700;
|
|
cursor: pointer; transition: var(--transition);
|
|
display: flex; align-items: center; gap: 6px;
|
|
box-shadow: 0 2px 8px rgba(232,97,26,0.3);
|
|
}
|
|
.btn-save:hover { background: var(--accent-hover); box-shadow: 0 4px 14px rgba(232,97,26,0.45); transform: translateY(-1px); }
|
|
.btn-save:disabled { opacity: .45; cursor: not-allowed; transform: none; }
|
|
|
|
/* === TOAST === */
|
|
.toast-container { position: fixed; bottom: 28px; right: 28px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; }
|
|
.toast {
|
|
color: #fff; border-radius: var(--radius);
|
|
padding: 11px 16px; font-size: 0.75rem; font-weight: 600;
|
|
display: flex; align-items: center; gap: 10px;
|
|
box-shadow: var(--shadow-lg);
|
|
animation: toastIn .3s cubic-bezier(0.34,1.56,0.64,1);
|
|
max-width: 320px; min-width: 200px;
|
|
}
|
|
.toast.success { background: var(--success); }
|
|
.toast.error { background: var(--danger); }
|
|
.toast.info { background: var(--primary-mid); }
|
|
.toast.warning { background: var(--warning); }
|
|
@keyframes toastIn { from{opacity:0;transform:translateX(36px)} to{opacity:1;transform:translateX(0)} }
|
|
|
|
/* === LEAFLET POPUP === */
|
|
.leaflet-popup-content-wrapper {
|
|
background: #fff !important; color: var(--text-secondary) !important;
|
|
border: 1px solid var(--border) !important;
|
|
border-radius: var(--radius) !important; box-shadow: var(--shadow-lg) !important;
|
|
}
|
|
.leaflet-popup-tip { background: #fff !important; }
|
|
.leaflet-popup-content {
|
|
font-family: 'Inter', sans-serif !important;
|
|
font-size: 0.78rem !important; line-height: 1.6 !important; padding: 12px 16px !important;
|
|
}
|
|
.leaflet-popup-content b { color: var(--text-primary); font-size: 0.82rem; font-weight: 700; }
|
|
.leaflet-popup-content table { width: 100%; font-size: 0.7rem; border-collapse: collapse; margin-top: 8px; }
|
|
.leaflet-popup-content th {
|
|
background: var(--bg-muted); padding: 5px 8px; text-align: left;
|
|
border-bottom: 1px solid var(--border); color: var(--text-muted);
|
|
font-weight: 800; text-transform: uppercase; font-size: 0.6rem; letter-spacing: 0.06em;
|
|
}
|
|
.leaflet-popup-content td { padding: 5px 8px; border-bottom: 1px solid var(--border-light); }
|
|
.leaflet-bar a, .leaflet-bar a:hover {
|
|
background-color: #fff !important; color: var(--text-secondary) !important; border-color: var(--border) !important;
|
|
}
|
|
.leaflet-control-attribution { background: rgba(255,255,255,0.92) !important; color: var(--text-muted) !important; font-size: 0.58rem !important; }
|
|
.leaflet-control-attribution a { color: var(--accent) !important; }
|
|
|
|
/* === ALERT BOX === */
|
|
.alert-box {
|
|
padding: 10px 13px; border-radius: var(--radius-sm);
|
|
font-size: 0.75rem; line-height: 1.6; margin-bottom: 14px;
|
|
display: flex; align-items: flex-start; gap: 9px; font-weight: 500;
|
|
}
|
|
.alert-box.info { background: var(--info-bg); border: 1px solid rgba(37,99,235,0.15); color: #1e40af; }
|
|
.alert-box.success { background: var(--success-bg); border: 1px solid rgba(5,150,105,0.2); color: #065f46; }
|
|
.alert-box i { margin-top: 1px; flex-shrink: 0; }
|
|
|
|
.demo-creds {
|
|
margin-top: 14px; padding: 12px 14px;
|
|
background: var(--bg-muted); border-radius: var(--radius-sm); border: 1px solid var(--border);
|
|
}
|
|
.demo-creds-title { font-size: 0.62rem; font-weight: 800; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.08em; margin-bottom: 8px; }
|
|
.demo-creds-row { font-size: 0.72rem; color: var(--text-secondary); line-height: 2; display: flex; align-items: center; gap: 6px; }
|
|
.demo-creds-row code {
|
|
font-family: 'Courier New', monospace;
|
|
background: var(--accent-light); color: var(--accent);
|
|
padding: 1px 5px; border-radius: 3px; font-size: 0.7rem; font-weight: 700;
|
|
}
|
|
|
|
/* === DRILLDOWN WALIKOTA === */
|
|
.dd-table { width:100%; border-collapse:collapse; font-size:.78rem; }
|
|
.dd-table thead tr { position:sticky; top:0; z-index:2; }
|
|
.dd-table th { padding:8px 12px; text-align:left; background:var(--bg-muted); color:var(--text-muted); font-weight:700; font-size:.65rem; text-transform:uppercase; letter-spacing:.06em; border-bottom:1px solid var(--border); }
|
|
.dd-table td { padding:7px 12px; border-bottom:1px solid rgba(55,65,95,.2); color:var(--text-secondary); vertical-align:middle; }
|
|
.dd-table tbody tr:hover td { background:rgba(99,102,241,.06); }
|
|
.dd-row-eligible td { background:rgba(5,150,105,.07) !important; }
|
|
.dd-row-eligible:hover td { background:rgba(5,150,105,.13) !important; }
|
|
.dd-lap-row { cursor:pointer; }
|
|
.dd-lap-row:hover td { background:rgba(99,102,241,.1) !important; }
|
|
.dd-total-bar { padding:9px 14px; border-top:1px solid var(--border); font-size:.75rem; font-weight:700; color:var(--accent); background:rgba(99,102,241,.06); }
|
|
.dd-laporan-detail { background:var(--bg-muted); border-radius:var(--radius); padding:14px 16px; margin:0 0 10px; border:1px solid var(--border); font-size:.8rem; line-height:1.75; border-left:3px solid var(--accent); }
|
|
.dash-card.clickable { cursor:pointer; }
|
|
.dash-card.clickable:hover { border-color:var(--accent); box-shadow:0 0 0 2px rgba(99,102,241,.2); transform:translateY(-1px); }
|
|
.dash-list-item.clickable { cursor:pointer; border-radius:5px; padding:3px 5px; transition:background .15s; margin:0 -5px; }
|
|
.dash-list-item.clickable:hover { background:rgba(99,102,241,.12); }
|
|
.dd-badge { display:inline-block; padding:2px 7px; border-radius:20px; font-size:.66rem; font-weight:700; }
|
|
|
|
/* === RESPONSIVE === */
|
|
@media (max-width: 768px) {
|
|
.sidebar { width: 100%; min-width: 100%; position: fixed; bottom: 0; height: 50vh; z-index: 999; }
|
|
.topbar-nav { display: none; }
|
|
.fab-report { bottom: calc(50vh + 14px); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ═══════════ TOP BAR ═══════════ -->
|
|
<header class="topbar" id="topbar">
|
|
<div class="topbar-brand">
|
|
<div class="brand-logo"><i class="fas fa-map-location-dot"></i></div>
|
|
<div>
|
|
<div class="brand-name">Poverty Mapping</div>
|
|
<div class="brand-sub">WebGIS Kota Pontianak</div>
|
|
</div>
|
|
</div>
|
|
<nav class="topbar-nav" id="topbar-tabs">
|
|
<button class="tab-btn active" id="tab-ri" onclick="switchTab('ri')">Rumah Ibadah</button>
|
|
<button class="tab-btn" id="tab-kk" onclick="switchTab('kk')">Keluarga Miskin</button>
|
|
<button class="tab-btn" id="tab-laporan" onclick="switchTab('laporan')" style="display:none">Laporan</button>
|
|
</nav>
|
|
<div class="topbar-right">
|
|
<span id="user-info"><i class="fas fa-circle-user"></i> <span id="user-name"></span> <span style="opacity:0.6">(<span id="user-role"></span>)</span></span>
|
|
<button id="btn-auth" onclick="toggleAuth()"><i class="fas fa-sign-in-alt"></i> Login</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ═══════════ BODY ═══════════ -->
|
|
<div class="app-body">
|
|
<div style="position:relative;flex:1;">
|
|
<div id="map"></div>
|
|
<div class="map-instruction" id="map-instruction">
|
|
<i class="fas fa-crosshairs"></i>
|
|
<strong id="instruction-text"></strong>
|
|
</div>
|
|
</div>
|
|
|
|
<aside class="sidebar" id="sidebar">
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-title">
|
|
<div class="sidebar-title-badge"><i class="fas fa-place-of-worship" id="sidebar-title-icon"></i></div>
|
|
<span class="sidebar-title-text" id="sidebar-title-text">Rumah Ibadah</span>
|
|
<span class="status-dot" style="margin-left:4px"></span>
|
|
</div>
|
|
<button class="btn-primary" id="btn-add-main" onclick="startAdd()">
|
|
<i class="fas fa-plus"></i> Tambah
|
|
</button>
|
|
</div>
|
|
<!-- Export buttons — hanya muncul di tab KK untuk admin/walikota -->
|
|
<div id="export-bar" style="display:none;padding:6px 12px;background:var(--bg-panel);border-bottom:1px solid var(--border);display:none;gap:6px;">
|
|
<button onclick="exportToExcel()" style="flex:1;padding:6px 10px;background:#1d6f42;color:#fff;border:none;border-radius:var(--radius-sm);font-size:0.72rem;font-weight:700;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;transition:var(--transition)" onmouseover="this.style.background='#155232'" onmouseout="this.style.background='#1d6f42'">
|
|
<i class="fas fa-file-excel"></i> Ekspor Excel
|
|
</button>
|
|
<button onclick="exportToPDF()" style="flex:1;padding:6px 10px;background:#c0392b;color:#fff;border:none;border-radius:var(--radius-sm);font-size:0.72rem;font-weight:700;cursor:pointer;font-family:inherit;display:inline-flex;align-items:center;justify-content:center;gap:5px;transition:var(--transition)" onmouseover="this.style.background='#992d22'" onmouseout="this.style.background='#c0392b'">
|
|
<i class="fas fa-file-pdf"></i> Ekspor PDF
|
|
</button>
|
|
</div>
|
|
<div id="filter-container"></div>
|
|
<div class="legend-bar" id="legend-bar"></div>
|
|
<div class="data-list" id="data-list">
|
|
<div class="empty-state"><i class="fas fa-spinner fa-spin"></i><p>Memuat data…</p></div>
|
|
</div>
|
|
<div class="stats-strip" id="stats-strip"></div>
|
|
</aside>
|
|
</div>
|
|
|
|
<!-- ═══════════ FLOATING REPORT BUTTON ═══════════ -->
|
|
<button class="fab-report" id="fab-report" onclick="openModal('laporan-form')">
|
|
<i class="fas fa-bullhorn"></i> Laporkan Warga Miskin
|
|
</button>
|
|
|
|
<!-- ═══════════ MODAL: LOGIN ═══════════ -->
|
|
<div class="modal-overlay" id="modal-login-overlay">
|
|
<div class="modal" style="width:400px">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<div class="modal-icon"><i class="fas fa-lock"></i></div>
|
|
Masuk ke Sistem
|
|
</div>
|
|
<button class="modal-close" onclick="closeModal('login')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="form-login" onsubmit="event.preventDefault(); doLogin();">
|
|
<div class="form-group">
|
|
<label class="form-label">Username / Kode Pengurus</label>
|
|
<input class="form-control" id="login-username" type="text" placeholder="admin / walikota / RI-MSJ-4821" autocomplete="username" required />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Password</label>
|
|
<input class="form-control" id="login-pass" type="password" placeholder="Masukkan password" autocomplete="current-password" required />
|
|
</div>
|
|
<button type="submit" class="btn-save" style="width:100%;justify-content:center;margin-top:6px"><i class="fas fa-right-to-bracket"></i> Masuk</button>
|
|
</form>
|
|
<div class="demo-creds">
|
|
<div class="demo-creds-title">Demo Akun</div>
|
|
<div class="demo-creds-row">Admin: <code>admin</code></div>
|
|
<div class="demo-creds-row">Walikota: <code>walikota</code></div>
|
|
<div class="demo-creds-row">Pengurus: <code>RI-MSJ-4821</code></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: RUMAH IBADAH ═══════════ -->
|
|
<div class="modal-overlay" id="modal-ri-overlay">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<div class="modal-icon"><i class="fas fa-place-of-worship"></i></div>
|
|
<span id="modal-ri-title">Tambah Rumah Ibadah</span>
|
|
</div>
|
|
<button class="modal-close" onclick="closeModal('ri')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="ri-id" />
|
|
<div class="form-group">
|
|
<label class="form-label">Nama Rumah Ibadah *</label>
|
|
<input class="form-control" id="ri-nama" type="text" placeholder="Masjid Raya Mujahidin" />
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Jenis *</label>
|
|
<select class="form-control" id="ri-jenis">
|
|
<option value="">-- Pilih --</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>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Kecamatan *</label>
|
|
<select class="form-control" id="ri-kecamatan">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="Pontianak Utara">Pontianak Utara</option>
|
|
<option value="Pontianak Selatan">Pontianak Selatan</option>
|
|
<option value="Pontianak Timur">Pontianak Timur</option>
|
|
<option value="Pontianak Barat">Pontianak Barat</option>
|
|
<option value="Pontianak Tenggara">Pontianak Tenggara</option>
|
|
<option value="Pontianak Kota">Pontianak Kota</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Lokasi Peta *</label>
|
|
<button class="btn-pick-map" onclick="activatePick('ri')"><i class="fas fa-map-marker-alt"></i> Klik Peta untuk Tentukan Lokasi</button>
|
|
<div class="coord-display">
|
|
<input class="form-control" id="ri-lat" type="text" readonly placeholder="Latitude" />
|
|
<input class="form-control" id="ri-lng" type="text" readonly placeholder="Longitude" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Alamat (auto-fill dari peta)</label>
|
|
<textarea class="form-control" id="ri-alamat" rows="2" placeholder="Otomatis terisi saat klik peta..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Radius Cakupan (<span id="ri-radius-val">2.0</span> km)</label>
|
|
<input type="range" id="ri-radius" min="0.5" max="5" step="0.1" value="2.0" style="width:100%;accent-color:var(--accent)" oninput="document.getElementById('ri-radius-val').innerText=parseFloat(this.value).toFixed(1)" />
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeModal('ri')">Batal</button>
|
|
<button class="btn-save" onclick="saveRI()"><i class="fas fa-floppy-disk"></i> Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: KELUARGA MISKIN ═══════════ -->
|
|
<div class="modal-overlay" id="modal-kk-overlay">
|
|
<div class="modal" style="width:560px">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<div class="modal-icon"><i class="fas fa-users"></i></div>
|
|
<span id="modal-kk-title">Tambah Keluarga Miskin</span>
|
|
</div>
|
|
<button class="modal-close" onclick="closeModal('kk')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" id="kk-id" />
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">NIK *</label>
|
|
<input class="form-control" id="kk-nik" type="text" maxlength="16" placeholder="6171xxxxxxxxxxxx" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Nama Kepala Keluarga *</label>
|
|
<input class="form-control" id="kk-nama" type="text" placeholder="Budi Santoso" />
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Tanggal Lahir</label>
|
|
<input class="form-control" id="kk-tgl-lahir" type="date" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Pendidikan</label>
|
|
<select class="form-control" id="kk-pendidikan">
|
|
<option value="Tidak Sekolah">Tidak Sekolah</option>
|
|
<option value="SD">SD</option>
|
|
<option value="SMP">SMP</option>
|
|
<option value="SMA" selected>SMA</option>
|
|
<option value="D3">D3</option>
|
|
<option value="S1">S1</option>
|
|
<option value="S2/S3">S2/S3</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Jumlah Anggota</label>
|
|
<input class="form-control" id="kk-jumlah" type="number" min="1" value="1" />
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Tingkat Kemiskinan *</label>
|
|
<select class="form-control" id="kk-tingkat">
|
|
<option value="Sangat Miskin">Sangat Miskin</option>
|
|
<option value="Miskin" selected>Miskin</option>
|
|
<option value="Hampir Miskin">Hampir Miskin</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Status DTKS</label>
|
|
<select class="form-control" id="kk-dtks">
|
|
<option value="Terdaftar DTKS">Terdaftar DTKS</option>
|
|
<option value="Belum DTKS" selected>Belum DTKS</option>
|
|
<option value="Sedang Diusulkan">Sedang Diusulkan</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Kecamatan *</label>
|
|
<select class="form-control" id="kk-kecamatan">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="Pontianak Utara">Pontianak Utara</option>
|
|
<option value="Pontianak Selatan">Pontianak Selatan</option>
|
|
<option value="Pontianak Timur">Pontianak Timur</option>
|
|
<option value="Pontianak Barat">Pontianak Barat</option>
|
|
<option value="Pontianak Tenggara">Pontianak Tenggara</option>
|
|
<option value="Pontianak Kota">Pontianak Kota</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Riwayat Penyakit</label>
|
|
<div class="checkbox-group" id="kk-penyakit-group"></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Lokasi Peta *</label>
|
|
<button class="btn-pick-map" onclick="activatePick('kk')"><i class="fas fa-map-marker-alt"></i> Klik Peta untuk Tentukan Lokasi</button>
|
|
<div class="coord-display">
|
|
<input class="form-control" id="kk-lat" type="text" readonly placeholder="Latitude" />
|
|
<input class="form-control" id="kk-lng" type="text" readonly placeholder="Longitude" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Alamat *</label>
|
|
<textarea class="form-control" id="kk-alamat" rows="2" placeholder="Alamat lengkap keluarga"></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeModal('kk')">Batal</button>
|
|
<button class="btn-save" onclick="saveKK()"><i class="fas fa-floppy-disk"></i> Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: LAPORAN MASYARAKAT (Form Publik) ═══════════ -->
|
|
<div class="modal-overlay" id="modal-laporan-form-overlay">
|
|
<div class="modal" style="width:480px">
|
|
<div class="modal-header">
|
|
<div class="modal-title"><i class="fas fa-flag"></i> Laporkan Warga Miskin</div>
|
|
<button class="modal-close" onclick="closeModal('laporan-form')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert-box info">
|
|
<i class="fas fa-info-circle"></i>
|
|
<span>Anda tidak perlu login. Identitas pelapor boleh dikosongkan (anonim). Maksimal 5 laporan per jam.</span>
|
|
</div>
|
|
|
|
<!-- Identitas Warga yang Dilaporkan -->
|
|
<div style="font-size:0.72rem;font-weight:800;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border)">
|
|
<i class="fas fa-user" style="color:var(--accent)"></i> Identitas Warga yang Dilaporkan
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Nama Warga (opsional)</label>
|
|
<input class="form-control" id="lap-nama-terlapor" type="text" placeholder="Nama kepala keluarga" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">NIK (opsional)</label>
|
|
<input class="form-control" id="lap-nik-terlapor" type="text" maxlength="16" placeholder="16 digit NIK" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lokasi -->
|
|
<div style="font-size:0.72rem;font-weight:800;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border)">
|
|
<i class="fas fa-map-pin" style="color:var(--accent)"></i> Lokasi & Kondisi
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Lokasi Warga yang Dilaporkan *</label>
|
|
<button class="btn-pick-map" onclick="activatePick('laporan')"><i class="fas fa-map-marker-alt"></i> Klik Peta untuk Tentukan Lokasi</button>
|
|
<div class="coord-display">
|
|
<input class="form-control" id="lap-lat" type="text" readonly placeholder="Latitude" />
|
|
<input class="form-control" id="lap-lng" type="text" readonly placeholder="Longitude" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Alamat Warga *</label>
|
|
<textarea class="form-control" id="lap-alamat" rows="2" placeholder="Alamat lengkap warga yang dilaporkan"></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Deskripsi Kondisi *</label>
|
|
<textarea class="form-control" id="lap-deskripsi" rows="3" placeholder="Jelaskan kondisi keluarga yang perlu dibantu..."></textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Foto (opsional, maks 5MB)</label>
|
|
<input class="form-control" id="lap-foto" type="file" accept="image/*" style="padding:8px" />
|
|
</div>
|
|
|
|
<!-- Identitas Pelapor -->
|
|
<div style="font-size:0.72rem;font-weight:800;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border);margin-top:4px">
|
|
<i class="fas fa-user-secret" style="color:var(--text-muted)"></i> Identitas Pelapor (opsional)
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Nama Pelapor</label>
|
|
<input class="form-control" id="lap-nama" type="text" placeholder="Boleh kosong / anonim" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">No. HP</label>
|
|
<input class="form-control" id="lap-hp" type="text" placeholder="08xxxxxxxxxx" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeModal('laporan-form')">Batal</button>
|
|
<button class="btn-save" onclick="submitLaporan()"><i class="fas fa-paper-plane"></i> Kirim Laporan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: HISTORI BANTUAN ═══════════ -->
|
|
<div class="modal-overlay" id="modal-bantuan-overlay">
|
|
<div class="modal" style="width:580px">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<div class="modal-icon"><i class="fas fa-hand-holding-heart"></i></div>
|
|
<span id="modal-bantuan-title">Histori Bantuan</span>
|
|
</div>
|
|
<button class="modal-close" onclick="closeModal('bantuan')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="bantuan-list"></div>
|
|
<div id="bantuan-form-wrap" style="display:none;margin-top:14px;padding:14px 16px;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-muted)">
|
|
<div style="font-weight:700;font-size:0.82rem;margin-bottom:10px"><i class="fas fa-plus" style="color:var(--accent)"></i> Tambah Bantuan Baru</div>
|
|
<input type="hidden" id="bantuan-kk-id" />
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Jenis Bantuan *</label>
|
|
<select class="form-control" id="bantuan-jenis">
|
|
<option value="PKH">PKH</option>
|
|
<option value="BPNT/Sembako">BPNT/Sembako</option>
|
|
<option value="Beras Bansos">Beras Bansos</option>
|
|
<option value="KIS-PBI">KIS-PBI</option>
|
|
<option value="Bansos Tunai Pemkot">Bansos Tunai Pemkot</option>
|
|
<option value="Rutilahu">Rutilahu</option>
|
|
<option value="PMT Stunting">PMT Stunting</option>
|
|
<option value="CSR Swasta">CSR Swasta</option>
|
|
<option value="Lainnya">Lainnya</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Nominal (Rp) *</label>
|
|
<input class="form-control" id="bantuan-nominal" type="number" min="0" step="1000" placeholder="Contoh: 600000" oninput="updateNominalPreview()" />
|
|
<div id="bantuan-nominal-preview" style="font-size:0.72rem;color:var(--accent);font-weight:700;margin-top:4px;min-height:16px"></div>
|
|
</div>
|
|
</div>
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label class="form-label">Tanggal Penyaluran *</label>
|
|
<input class="form-control" id="bantuan-tanggal" type="date" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Sumber Dana</label>
|
|
<select class="form-control" id="bantuan-sumber">
|
|
<option value="APBN">APBN</option>
|
|
<option value="APBD">APBD</option>
|
|
<option value="CSR">CSR</option>
|
|
<option value="Lainnya">Lainnya</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Keterangan (opsional)</label>
|
|
<input class="form-control" id="bantuan-ket" type="text" placeholder="Opsional: 10 kg beras, 3 karung, dll" />
|
|
</div>
|
|
<button class="btn-save" onclick="saveBantuan()" style="width:100%;justify-content:center"><i class="fas fa-floppy-disk"></i> Simpan Bantuan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: KELOLA LAPORAN (Admin) ═══════════ -->
|
|
<div class="modal-overlay" id="modal-kelola-laporan-overlay">
|
|
<div class="modal" style="width:480px">
|
|
<div class="modal-header">
|
|
<div class="modal-title"><i class="fas fa-clipboard-check"></i> Kelola Laporan</div>
|
|
<button class="modal-close" onclick="closeModal('kelola-laporan')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div id="kelola-laporan-detail"></div>
|
|
<div id="kelola-laporan-actions" style="margin-top:14px">
|
|
<input type="hidden" id="kelola-lap-id" />
|
|
<div class="form-group">
|
|
<label class="form-label">Ubah Status</label>
|
|
<select class="form-control" id="kelola-lap-status">
|
|
<option value="Baru">Baru</option>
|
|
<option value="Diverifikasi">Diverifikasi</option>
|
|
<option value="Ditolak">Ditolak</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group" id="kelola-alasan-wrap" style="display:none">
|
|
<label class="form-label">Alasan Penolakan *</label>
|
|
<textarea class="form-control" id="kelola-alasan" rows="2" placeholder="Jelaskan alasan penolakan..."></textarea>
|
|
</div>
|
|
<div class="form-group" id="kelola-convert-wrap" style="display:none">
|
|
<label class="form-label">Konversi ke KK ID (opsional)</label>
|
|
<input class="form-control" id="kelola-convert-id" type="number" placeholder="ID keluarga miskin" />
|
|
<div class="form-info"><i class="fas fa-info-circle"></i> Isi jika laporan ini dikonversi menjadi data KK baru</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer" id="kelola-footer">
|
|
<!-- Tombol buat KK: hanya tampil saat status Baru -->
|
|
<button id="btn-buat-kk-dari-laporan"
|
|
style="margin-right:auto;padding:8px 14px;background:linear-gradient(135deg,#059669,#10b981);color:#fff;border:none;border-radius:var(--radius-sm);font-size:0.78rem;font-weight:700;cursor:pointer;font-family:inherit;display:none;align-items:center;gap:6px;transition:var(--transition)"
|
|
onmouseover="this.style.opacity='.85'" onmouseout="this.style.opacity='1'"
|
|
onclick="buatKKdariLaporan()">
|
|
<i class="fas fa-user-plus"></i> Buat KK dari Laporan Ini
|
|
</button>
|
|
<button class="btn-cancel" onclick="closeModal('kelola-laporan')">Batal</button>
|
|
<button class="btn-save" onclick="updateLaporan()"><i class="fas fa-check"></i> Simpan Perubahan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: KONFIRMASI HAPUS ═══════════ -->
|
|
<div class="modal-overlay" id="modal-confirm-overlay">
|
|
<div class="modal" style="width:380px">
|
|
<div class="modal-header">
|
|
<div class="modal-title" style="color:var(--danger)"><i class="fas fa-triangle-exclamation"></i> Konfirmasi Hapus</div>
|
|
<button class="modal-close" onclick="closeModal('confirm')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p style="color:var(--text-secondary);font-size:0.86rem;line-height:1.6">
|
|
Hapus data <strong id="confirm-name" style="color:var(--text-primary)"></strong>? Tindakan ini tidak dapat dibatalkan.
|
|
</p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-cancel" onclick="closeModal('confirm')">Batal</button>
|
|
<button class="btn-save" style="background:var(--danger)" id="btn-confirm-del"><i class="fas fa-trash-alt"></i> Hapus</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═════════ MODAL: DRILLDOWN WALIKOTA ═════════ -->
|
|
<div class="modal-overlay" id="modal-dd-overlay">
|
|
<div class="modal" style="width:860px;max-width:97vw">
|
|
<div class="modal-header">
|
|
<div class="modal-title" id="dd-title"><i class="fas fa-table"></i> Detail</div>
|
|
<button class="modal-close" onclick="closeModal('dd')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body" id="dd-body" style="max-height:58vh;overflow-y:auto;padding:0"></div>
|
|
<div class="modal-footer" style="justify-content:space-between;align-items:center">
|
|
<span id="dd-count" style="font-size:.74rem;color:var(--text-muted)"></span>
|
|
<div style="display:flex;gap:8px">
|
|
<button onclick="exportDDExcel()" style="padding:7px 14px;background:#1d6f42;color:#fff;border:none;border-radius:var(--radius-sm);font-size:.75rem;font-weight:700;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:5px">
|
|
<i class="fas fa-file-excel"></i> Ekspor Excel
|
|
</button>
|
|
<button class="btn-cancel" onclick="closeModal('dd')">Tutup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: INFO PENGURUS BARU ═══════════ -->
|
|
<div class="modal-overlay" id="modal-pengurus-info-overlay">
|
|
<div class="modal" style="width:420px">
|
|
<div class="modal-header">
|
|
<div class="modal-title"><i class="fas fa-key"></i> Akun Pengurus Baru</div>
|
|
<button class="modal-close" onclick="closeModal('pengurus-info')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert-box success">
|
|
<i class="fas fa-circle-check"></i>
|
|
<span>Berikan kredensial ini kepada pengurus rumah ibadah.</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Kode Pengurus (Username)</label>
|
|
<input class="form-control" id="info-kode" type="text" readonly style="font-family:'Courier New',monospace;color:var(--accent)" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Password</label>
|
|
<input class="form-control" id="info-pass" type="text" readonly style="font-family:'Courier New',monospace;color:var(--success)" />
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-save" onclick="closeModal('pengurus-info')"><i class="fas fa-check"></i> Mengerti</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ═══════════ MODAL: RESET PASSWORD PENGURUS ═══════════ -->
|
|
<div class="modal-overlay" id="modal-reset-pw-overlay">
|
|
<div class="modal" style="width:420px">
|
|
<div class="modal-header">
|
|
<div class="modal-title">
|
|
<div class="modal-icon" style="background:#FEF3C7"><i class="fas fa-rotate-right" style="color:#D97706"></i></div>
|
|
Reset Password Pengurus
|
|
</div>
|
|
<button class="modal-close" onclick="closeModal('reset-pw')"><i class="fas fa-xmark"></i></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert-box" style="background:#FFFBEB;border:1px solid rgba(217,119,6,0.25);color:#92400e" id="reset-pw-confirm-box">
|
|
<i class="fas fa-triangle-exclamation" style="color:#D97706"></i>
|
|
<span>Password lama akan <strong>tidak bisa digunakan</strong> lagi. Catat password baru dan berikan ke pengurus.</span>
|
|
</div>
|
|
<!-- Konfirmasi sebelum reset -->
|
|
<div id="reset-pw-confirm-wrap">
|
|
<p style="font-size:0.85rem;color:var(--text-secondary);line-height:1.6;margin-bottom:4px">
|
|
Reset password pengurus untuk: <strong id="reset-pw-nama" style="color:var(--text-primary)"></strong>?
|
|
</p>
|
|
</div>
|
|
<!-- Hasil setelah reset -->
|
|
<div id="reset-pw-result-wrap" style="display:none">
|
|
<div class="alert-box success" style="margin-bottom:14px">
|
|
<i class="fas fa-circle-check"></i>
|
|
<span>Password berhasil direset! Berikan kredensial berikut kepada pengurus.</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Kode Pengurus (Username)</label>
|
|
<div style="position:relative">
|
|
<input class="form-control" id="reset-pw-kode" type="text" readonly
|
|
style="font-family:'Courier New',monospace;color:var(--accent);padding-right:40px" />
|
|
<button onclick="copyToClipboard('reset-pw-kode')" title="Salin"
|
|
style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:0.85rem">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">Password Baru</label>
|
|
<div style="position:relative">
|
|
<input class="form-control" id="reset-pw-pass" type="text" readonly
|
|
style="font-family:'Courier New',monospace;color:var(--success);font-size:1rem;font-weight:700;letter-spacing:0.06em;padding-right:40px" />
|
|
<button onclick="copyToClipboard('reset-pw-pass')" title="Salin"
|
|
style="position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:0.85rem">
|
|
<i class="fas fa-copy"></i>
|
|
</button>
|
|
</div>
|
|
<div class="form-info"><i class="fas fa-info-circle"></i> Password ini hanya tampil sekali. Simpan sebelum menutup.</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer" id="reset-pw-footer">
|
|
<button class="btn-cancel" onclick="closeModal('reset-pw')">Batal</button>
|
|
<button class="btn-save" id="btn-do-reset" style="background:var(--warning);box-shadow:0 2px 8px rgba(217,119,6,0.3)"
|
|
onclick="doResetPassword()">
|
|
<i class="fas fa-rotate-right"></i> Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toast-container"></div>
|
|
|
|
<!-- ═══════════ SCRIPTS ═══════════ -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.28/jspdf.plugin.autotable.min.js"></script>
|
|
|
|
<script>
|
|
/* ================================================================
|
|
KONFIGURASI & STATE GLOBAL
|
|
================================================================ */
|
|
const BASE = window.location.href.replace(/\/[^/]*$/, '/');
|
|
const API = {
|
|
auth: BASE + 'api_auth.php',
|
|
ri: BASE + 'api_rumah_ibadah.php',
|
|
kk: BASE + 'api_keluarga_miskin.php',
|
|
laporan: BASE + 'api_laporan.php',
|
|
bantuan: BASE + 'api_histori_bantuan.php',
|
|
dashboard: BASE + 'api_dashboard.php',
|
|
};
|
|
|
|
const MAP_CENTER = [-0.0263, 109.3425];
|
|
const MAP_ZOOM = 14;
|
|
|
|
const WARNA_RI = { Masjid:'#22c55e', Gereja:'#3b82f6', Pura:'#f97316', Vihara:'#ef4444', Klenteng:'#eab308', Lainnya:'#a855f7' };
|
|
const WARNA_KM = { 'Sangat Miskin':'#ef4444', Miskin:'#f97316', 'Hampir Miskin':'#eab308' };
|
|
const DAFTAR_PENYAKIT = ['Stunting','TBC','Hipertensi','Diabetes','Jantung','ODGJ','DBD','Gizi Buruk','Lainnya'];
|
|
|
|
let currentTab = 'ri';
|
|
let currentUser = null;
|
|
let dataRI = [], dataKK = [], dataLaporan = [];
|
|
let pickMode = null;
|
|
let tempMarker = null;
|
|
let deleteTarget = null;
|
|
let pendingLaporanConvert = null;
|
|
|
|
/* ================================================================
|
|
INISIALISASI PETA
|
|
================================================================ */
|
|
const map = L.map('map', { zoomControl: true }).setView(MAP_CENTER, MAP_ZOOM);
|
|
|
|
// Tile layer terang
|
|
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
|
subdomains: 'abcd',
|
|
maxZoom: 20,
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>'
|
|
}).addTo(map);
|
|
|
|
const riLayerGroup = L.layerGroup().addTo(map);
|
|
const riRadiusGroup = L.layerGroup().addTo(map);
|
|
const kkLayerGroup = L.layerGroup().addTo(map);
|
|
|
|
/* ================================================================
|
|
UTILITAS
|
|
================================================================ */
|
|
function esc(s) {
|
|
return String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
}
|
|
function fetchJSON(url, opts = {}) {
|
|
return fetch(url, { credentials: 'same-origin', ...opts }).then(r => r.json());
|
|
}
|
|
function showToast(msg, type = 'success') {
|
|
const icons = { success:'fa-circle-check', error:'fa-circle-xmark', info:'fa-info-circle', warning:'fa-triangle-exclamation' };
|
|
const el = document.createElement('div');
|
|
el.className = `toast ${type}`;
|
|
el.innerHTML = `<i class="fas ${icons[type]||icons.info}"></i> ${msg}`;
|
|
document.getElementById('toast-container').appendChild(el);
|
|
setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateX(40px)'; setTimeout(() => el.remove(), 300); }, 3500);
|
|
}
|
|
function openModal(name) { document.getElementById(`modal-${name}-overlay`).classList.add('open'); }
|
|
function closeModal(name) {
|
|
document.getElementById(`modal-${name}-overlay`).classList.remove('open');
|
|
if (['ri','kk','laporan-form'].includes(name)) stopPick();
|
|
}
|
|
function showInstruction(show, text = '') {
|
|
document.getElementById('instruction-text').textContent = text;
|
|
document.getElementById('map-instruction').classList.toggle('visible', show);
|
|
}
|
|
function stopPick() {
|
|
pickMode = 'idle';
|
|
map.getContainer().style.cursor = '';
|
|
showInstruction(false);
|
|
}
|
|
|
|
/* ================================================================
|
|
AUTENTIKASI
|
|
================================================================ */
|
|
async function checkAuth() {
|
|
try {
|
|
const data = await fetchJSON(API.auth + '?action=check');
|
|
if (data.success) {
|
|
currentUser = data.data;
|
|
document.getElementById('user-info').style.display = 'inline';
|
|
document.getElementById('user-name').textContent = currentUser.nama;
|
|
document.getElementById('user-role').textContent = currentUser.role;
|
|
document.getElementById('btn-auth').innerHTML = '<i class="fas fa-right-from-bracket"></i> Logout';
|
|
// Tampilkan tab Laporan untuk admin/walikota
|
|
document.getElementById('tab-laporan').style.display = (currentUser.role === 'admin' || currentUser.role === 'walikota') ? '' : 'none';
|
|
} else {
|
|
currentUser = null;
|
|
document.getElementById('user-info').style.display = 'none';
|
|
document.getElementById('btn-auth').innerHTML = '<i class="fas fa-sign-in-alt"></i> Login';
|
|
document.getElementById('tab-laporan').style.display = 'none';
|
|
}
|
|
updateUIForRole();
|
|
} catch(e) { console.error('Auth check error', e); }
|
|
}
|
|
|
|
function updateUIForRole() {
|
|
const addBtn = document.getElementById('btn-add-main');
|
|
if (!currentUser) {
|
|
addBtn.style.display = 'none';
|
|
return;
|
|
}
|
|
if (currentUser.role === 'walikota') {
|
|
addBtn.style.display = 'none';
|
|
// Otomatis switch ke dashboard walikota jika belum
|
|
return;
|
|
}
|
|
if (currentTab === 'ri' && currentUser.role !== 'admin') {
|
|
addBtn.style.display = 'none';
|
|
} else if (currentTab === 'laporan') {
|
|
addBtn.style.display = 'none';
|
|
} else {
|
|
addBtn.style.display = '';
|
|
}
|
|
}
|
|
|
|
function toggleAuth() {
|
|
if (currentUser) {
|
|
fetchJSON(API.auth + '?action=logout', { method: 'POST' }).then(() => {
|
|
showToast('Logout berhasil', 'success');
|
|
currentUser = null;
|
|
checkAuth();
|
|
if (currentTab === 'laporan') switchTab('ri');
|
|
else loadAllData();
|
|
});
|
|
} else {
|
|
openModal('login');
|
|
}
|
|
}
|
|
|
|
async function doLogin() {
|
|
const username = document.getElementById('login-username').value.trim();
|
|
const password = document.getElementById('login-pass').value;
|
|
if (!username || !password) return showToast('Username dan password wajib diisi', 'error');
|
|
try {
|
|
const data = await fetchJSON(API.auth + '?action=login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password })
|
|
});
|
|
if (data.success) {
|
|
closeModal('login');
|
|
showToast(data.message, 'success');
|
|
document.getElementById('login-username').value = '';
|
|
document.getElementById('login-pass').value = '';
|
|
await checkAuth();
|
|
await loadAllData();
|
|
} else {
|
|
showToast(data.message, 'error');
|
|
}
|
|
} catch(e) { showToast('Gagal terhubung ke server', 'error'); }
|
|
}
|
|
|
|
/* ================================================================
|
|
KLIK PETA — PICK LOKASI & REVERSE GEOCODING
|
|
================================================================ */
|
|
async function reverseGeocode(lat, lng) {
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`);
|
|
const data = await res.json();
|
|
return data.display_name || 'Alamat tidak ditemukan';
|
|
} catch(e) { return 'Gagal mengambil alamat'; }
|
|
}
|
|
|
|
map.on('click', async function(e) {
|
|
if (pickMode === 'idle') {
|
|
riRadiusGroup.clearLayers();
|
|
return;
|
|
}
|
|
const lat = parseFloat(e.latlng.lat.toFixed(7));
|
|
const lng = parseFloat(e.latlng.lng.toFixed(7));
|
|
|
|
if (tempMarker) map.removeLayer(tempMarker);
|
|
const clr = pickMode === 'ri' ? '#638cff' : '#f97316';
|
|
tempMarker = L.circleMarker([lat, lng], { radius: 10, fillColor: clr, color: '#fff', weight: 2, fillOpacity: 1 }).addTo(map);
|
|
|
|
if (pickMode === 'ri') {
|
|
document.getElementById('ri-lat').value = lat;
|
|
document.getElementById('ri-lng').value = lng;
|
|
showToast('Mengambil alamat otomatis...', 'info');
|
|
const addr = await reverseGeocode(lat, lng);
|
|
document.getElementById('ri-alamat').value = addr;
|
|
openModal('ri');
|
|
} else if (pickMode === 'kk') {
|
|
document.getElementById('kk-lat').value = lat;
|
|
document.getElementById('kk-lng').value = lng;
|
|
showToast('Mengambil alamat otomatis...', 'info');
|
|
const addr = await reverseGeocode(lat, lng);
|
|
document.getElementById('kk-alamat').value = addr;
|
|
openModal('kk');
|
|
} else if (pickMode === 'laporan') {
|
|
document.getElementById('lap-lat').value = lat;
|
|
document.getElementById('lap-lng').value = lng;
|
|
showToast('Mengambil alamat otomatis...', 'info');
|
|
const addr = await reverseGeocode(lat, lng);
|
|
document.getElementById('lap-alamat').value = addr;
|
|
openModal('laporan-form');
|
|
}
|
|
stopPick();
|
|
});
|
|
|
|
function activatePick(type) {
|
|
closeModal(type === 'ri' ? 'ri' : type === 'kk' ? 'kk' : 'laporan-form');
|
|
pickMode = type;
|
|
map.getContainer().style.cursor = 'crosshair';
|
|
showInstruction(true, 'Klik pada peta untuk menentukan lokasi. Alamat akan terisi otomatis.');
|
|
}
|
|
|
|
/* ================================================================
|
|
TAB SWITCHING
|
|
================================================================ */
|
|
function switchTab(tab) {
|
|
currentTab = tab;
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById(`tab-${tab}`).classList.add('active');
|
|
|
|
const titles = { ri: 'Rumah Ibadah', kk: 'Keluarga Miskin', laporan: 'Laporan Masuk' };
|
|
const icons = { ri: 'fa-place-of-worship', kk: 'fa-users', laporan: 'fa-bullhorn' };
|
|
document.getElementById('sidebar-title-text').textContent = titles[tab] || '';
|
|
const iconEl = document.getElementById('sidebar-title-icon');
|
|
if (iconEl) iconEl.className = 'fas ' + (icons[tab] || 'fa-layer-group');
|
|
|
|
// Tampilkan export bar hanya di tab KK untuk admin/walikota
|
|
const exportBar = document.getElementById('export-bar');
|
|
const canExport = currentUser && (currentUser.role === 'admin' || currentUser.role === 'walikota');
|
|
exportBar.style.display = (tab === 'kk' && canExport) ? 'flex' : 'none';
|
|
|
|
stopPick();
|
|
riRadiusGroup.clearLayers();
|
|
if (tempMarker) { map.removeLayer(tempMarker); tempMarker = null; }
|
|
|
|
renderLegend();
|
|
renderFilterBar();
|
|
renderList();
|
|
updateUIForRole();
|
|
}
|
|
|
|
/* ================================================================
|
|
LEGEND
|
|
================================================================ */
|
|
function renderLegend() {
|
|
const bar = document.getElementById('legend-bar');
|
|
if (currentTab === 'ri') {
|
|
bar.innerHTML = Object.entries(WARNA_RI).map(([k,v]) =>
|
|
`<div class="legend-item"><div class="legend-dot" style="background:${v}"></div><span>${k}</span></div>`).join('');
|
|
} else if (currentTab === 'kk') {
|
|
bar.innerHTML = Object.entries(WARNA_KM).map(([k,v]) =>
|
|
`<div class="legend-item"><div class="legend-dot" style="background:${v};border-radius:3px"></div><span>${k}</span></div>`).join('');
|
|
} else {
|
|
bar.innerHTML = `
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--warning)"></div><span>Baru</span></div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--success)"></div><span>Diverifikasi</span></div>
|
|
<div class="legend-item"><div class="legend-dot" style="background:var(--danger)"></div><span>Ditolak</span></div>`;
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
FILTER BAR
|
|
================================================================ */
|
|
function renderFilterBar() {
|
|
const container = document.getElementById('filter-container');
|
|
if (currentTab === 'kk') {
|
|
container.innerHTML = `<div class="filter-bar">
|
|
<select class="filter-select" id="filter-kecamatan" onchange="renderList()">
|
|
<option value="">Semua Kecamatan</option>
|
|
<option>Pontianak Utara</option><option>Pontianak Selatan</option><option>Pontianak Timur</option>
|
|
<option>Pontianak Barat</option><option>Pontianak Tenggara</option><option>Pontianak Kota</option>
|
|
</select>
|
|
<select class="filter-select" id="filter-tingkat" onchange="renderList()">
|
|
<option value="">Semua Tingkat</option>
|
|
<option>Sangat Miskin</option><option>Miskin</option><option>Hampir Miskin</option>
|
|
</select>
|
|
<select class="filter-select" id="filter-dtks" onchange="renderList()">
|
|
<option value="">Semua DTKS</option>
|
|
<option>Terdaftar DTKS</option><option>Belum DTKS</option><option>Sedang Diusulkan</option>
|
|
</select>
|
|
</div>`;
|
|
} else if (currentTab === 'laporan') {
|
|
container.innerHTML = `<div class="filter-bar">
|
|
<select class="filter-select" id="filter-lap-status" onchange="renderList()">
|
|
<option value="">Semua Status</option>
|
|
<option>Baru</option><option>Diverifikasi</option><option>Ditolak</option>
|
|
</select>
|
|
</div>`;
|
|
} else {
|
|
container.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
RENDER LIST
|
|
================================================================ */
|
|
async function renderList() {
|
|
const list = document.getElementById('data-list');
|
|
const strip = document.getElementById('stats-strip');
|
|
|
|
// Walikota → tampilkan dashboard
|
|
if (currentUser && currentUser.role === 'walikota' && currentTab !== 'laporan') {
|
|
await renderWalikotaDashboard(list, strip);
|
|
return;
|
|
}
|
|
|
|
if (currentTab === 'ri') renderRIList(list, strip);
|
|
else if (currentTab === 'kk') renderKKList(list, strip);
|
|
else if (currentTab === 'laporan') renderLaporanList(list, strip);
|
|
}
|
|
|
|
function renderRIList(list, strip) {
|
|
if (!dataRI.length) { list.innerHTML = emptyEl('place-of-worship','Belum ada data rumah ibadah.'); strip.innerHTML=''; return; }
|
|
const isAdmin = currentUser && currentUser.role === 'admin';
|
|
list.innerHTML = dataRI.map(d => {
|
|
const w = WARNA_RI[d.jenis] || '#888';
|
|
return `<div class="data-card" id="card-ri-${d.id}" onclick="zoomToRI(${d.id})">
|
|
<div class="card-top">
|
|
<div class="card-name">${esc(d.nama)}</div>
|
|
<span class="badge" style="background:${w}22;color:${w}">${d.jenis}</span>
|
|
</div>
|
|
<div class="card-meta">
|
|
<span><i class="fas fa-bullseye"></i> Radius: ${parseFloat(d.radius_km).toFixed(1)} km</span>
|
|
<span>${d.jumlah_kk_binaan || 0} KK</span>
|
|
<span><i class="fas fa-map-pin"></i> ${esc(d.kecamatan)}</span>
|
|
${isAdmin ? `<span style="width:100%"><i class="fas fa-key" style="color:var(--warning)"></i> <code style="color:var(--accent)">${esc(d.kode_pengurus||'-')}</code></span>` : ''}
|
|
</div>
|
|
<div class="card-actions">
|
|
<button class="btn-sm btn-zoom" onclick="event.stopPropagation();zoomToRI(${d.id})"><i class="fas fa-search-location"></i> Zoom</button>
|
|
${currentUser && (currentUser.role==='admin' || (currentUser.role==='pengurus' && currentUser.rumah_ibadah_id==d.id)) ? `<button class="btn-sm btn-edit" onclick="event.stopPropagation();editRI(${d.id})"><i class="fas fa-pen"></i> Edit</button>` : ''}
|
|
${isAdmin ? `<button class="btn-sm" onclick="event.stopPropagation();resetPasswordPengurus(${d.id},'${esc(d.nama)}')" style="background:#FFFBEB;border:1px solid rgba(217,119,6,0.35);color:#D97706" title="Reset password pengurus"><i class="fas fa-rotate-right"></i> Reset PW</button>` : ''}
|
|
${isAdmin ? `<button class="btn-sm btn-del" onclick="event.stopPropagation();confirmDelete('ri',${d.id},'${esc(d.nama)}')"><i class="fas fa-trash"></i></button>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
strip.innerHTML = `<div class="stat-item"><div class="stat-num">${dataRI.length}</div><div class="stat-label">Total RI</div></div>
|
|
<div class="stat-item"><div class="stat-num">${dataRI.reduce((a,d)=>a+(parseInt(d.jumlah_kk_binaan)||0),0)}</div><div class="stat-label">KK Binaan</div></div>`;
|
|
}
|
|
|
|
function renderKKList(list, strip) {
|
|
let filtered = [...dataKK];
|
|
const fKec = document.getElementById('filter-kecamatan')?.value;
|
|
const fTingkat = document.getElementById('filter-tingkat')?.value;
|
|
const fDtks = document.getElementById('filter-dtks')?.value;
|
|
if (fKec) filtered = filtered.filter(d => d.kecamatan === fKec);
|
|
if (fTingkat) filtered = filtered.filter(d => d.tingkat_kemiskinan === fTingkat);
|
|
if (fDtks) filtered = filtered.filter(d => d.status_dtks === fDtks);
|
|
|
|
if (!filtered.length) { list.innerHTML = emptyEl('users','Tidak ada data sesuai filter.'); strip.innerHTML=''; return; }
|
|
const isAdmin = currentUser && currentUser.role === 'admin';
|
|
|
|
list.innerHTML = filtered.map(d => {
|
|
const w = WARNA_KM[d.tingkat_kemiskinan] || '#888';
|
|
const penyakit = (d.riwayat_penyakit || []);
|
|
const penyakitHtml = penyakit.length ? penyakit.map(p => `<span class="penyakit-tag">${esc(p)}</span>`).join('') : '';
|
|
return `<div class="data-card" id="card-kk-${d.id}" onclick="zoomToKK(${d.id})">
|
|
<div class="card-top">
|
|
<div class="card-name">${esc(d.nama_kepala_keluarga)}</div>
|
|
<span class="badge" style="background:${w}22;color:${w}">${d.tingkat_kemiskinan}</span>
|
|
</div>
|
|
<div class="card-meta">
|
|
<span>${d.jumlah_anggota} orang</span>
|
|
<span><i class="fas fa-map-pin"></i> ${esc(d.kecamatan)}</span>
|
|
<span><i class="fas fa-id-card"></i> ${esc(d.status_dtks)}</span>
|
|
${d.nama_ri ? `<span>${esc(d.nama_ri)} (${d.jarak_ke_ibadah ? parseFloat(d.jarak_ke_ibadah).toFixed(2) + ' km' : '-'})</span>` : ''}
|
|
${d.eligible_bansos_tunai ? '<span style="color:var(--success)"><i class="fas fa-check-circle"></i> Eligible Bansos Tunai</span>' : ''}
|
|
</div>
|
|
${penyakitHtml ? `<div style="margin-top:6px">${penyakitHtml}</div>` : ''}
|
|
<div class="card-actions">
|
|
<button class="btn-sm btn-zoom" onclick="event.stopPropagation();zoomToKK(${d.id})"><i class="fas fa-search-location"></i></button>
|
|
<button class="btn-sm btn-info-sm" onclick="event.stopPropagation();openBantuan(${d.id},'${esc(d.nama_kepala_keluarga)}')"><i class="fas fa-hand-holding-heart"></i> Bantuan</button>
|
|
${currentUser && currentUser.role !== 'walikota' ? `<button class="btn-sm btn-edit" onclick="event.stopPropagation();editKK(${d.id})"><i class="fas fa-pen"></i></button>` : ''}
|
|
${isAdmin ? `<button class="btn-sm btn-del" onclick="event.stopPropagation();confirmDelete('kk',${d.id},'${esc(d.nama_kepala_keluarga)}')"><i class="fas fa-trash"></i></button>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
const sm = dataKK.filter(d=>d.tingkat_kemiskinan==='Sangat Miskin').length;
|
|
const ms = dataKK.filter(d=>d.tingkat_kemiskinan==='Miskin').length;
|
|
strip.innerHTML = `
|
|
<div class="stat-item"><div class="stat-num">${filtered.length}</div><div class="stat-label">Tampil</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="-webkit-text-fill-color:#ef4444">${sm}</div><div class="stat-label">S.Miskin</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="-webkit-text-fill-color:#f97316">${ms}</div><div class="stat-label">Miskin</div></div>`;
|
|
}
|
|
|
|
function renderLaporanList(list, strip) {
|
|
let filtered = [...dataLaporan];
|
|
const fStatus = document.getElementById('filter-lap-status')?.value;
|
|
if (fStatus) filtered = filtered.filter(d => d.status === fStatus);
|
|
|
|
if (!filtered.length) { list.innerHTML = emptyEl('bullhorn','Tidak ada laporan.'); strip.innerHTML=''; return; }
|
|
const isAdmin = currentUser && currentUser.role === 'admin';
|
|
|
|
list.innerHTML = filtered.map(d => {
|
|
const statusColors = { Baru:'var(--warning)', Diverifikasi:'var(--success)', Ditolak:'var(--danger)' };
|
|
const sc = statusColors[d.status] || 'var(--text-muted)';
|
|
return `<div class="data-card" onclick="zoomToLaporan(${d.id},${d.lat},${d.lng})">
|
|
<div class="card-top">
|
|
<div class="card-name"><i class="fas fa-bullhorn" style="color:${sc}"></i> ${esc(d.nama_pelapor || 'Anonim')}</div>
|
|
<span class="badge" style="background:${sc}22;color:${sc}">${d.status}</span>
|
|
</div>
|
|
<div class="card-meta">
|
|
<span><i class="fas fa-clock"></i> ${new Date(d.created_at).toLocaleDateString('id-ID')}</span>
|
|
<span style="width:100%"><i class="fas fa-map-pin"></i> ${esc(d.alamat_terlapor).substring(0,60)}...</span>
|
|
</div>
|
|
<p style="font-size:0.72rem;color:var(--text-secondary);margin-top:6px;line-height:1.5">${esc(d.deskripsi).substring(0,120)}${d.deskripsi.length > 120 ? '...' : ''}</p>
|
|
<div class="card-actions">
|
|
${isAdmin ? `<button class="btn-sm btn-edit" onclick="event.stopPropagation();kelolaLaporan(${d.id})"><i class="fas fa-clipboard-check"></i> Kelola</button>` : ''}
|
|
</div>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
const baru = dataLaporan.filter(d=>d.status==='Baru').length;
|
|
strip.innerHTML = `<div class="stat-item"><div class="stat-num">${dataLaporan.length}</div><div class="stat-label">Total</div></div>
|
|
<div class="stat-item"><div class="stat-num" style="-webkit-text-fill-color:var(--warning)">${baru}</div><div class="stat-label">Baru</div></div>`;
|
|
}
|
|
|
|
/* ── Walikota Dashboard ── */
|
|
async function renderWalikotaDashboard(list, strip) {
|
|
strip.innerHTML = '';
|
|
const tahun = new Date().getFullYear();
|
|
|
|
list.innerHTML = `<div class="empty-state"><i class="fas fa-spinner fa-spin"></i><p>Memuat data dashboard…</p></div>`;
|
|
|
|
let db;
|
|
try {
|
|
const res = await fetchJSON(API.dashboard + '?tahun=' + tahun);
|
|
if (!res.success) throw new Error(res.message);
|
|
db = res.data;
|
|
} catch(e) {
|
|
list.innerHTML = `<div class="empty-state"><i class="fas fa-circle-xmark" style="color:var(--danger)"></i><p>Gagal memuat dashboard.</p></div>`;
|
|
return;
|
|
}
|
|
|
|
const pct = (v, t) => t > 0 ? Math.round(v/t*100) : 0;
|
|
const bar = (val, max, color) => {
|
|
const w = max > 0 ? Math.round(val/max*100) : 0;
|
|
return `<div style="height:5px;background:var(--border);border-radius:3px;margin-top:4px"><div style="height:5px;width:${w}%;background:${color};border-radius:3px;transition:width .5s"></div></div>`;
|
|
};
|
|
|
|
const maxKK = Math.max(...db.kk_per_kecamatan.map(k => parseInt(k.jumlah)), 1);
|
|
const maxPenyakit = db.distribusi_penyakit.length ? db.distribusi_penyakit[0].jumlah : 1;
|
|
const warnaSumber = { APBN:'#3b82f6', APBD:'#8b5cf6', CSR:'#10b981', Lainnya:'#f59e0b' };
|
|
const warnaKM = { 'Sangat Miskin':'#ef4444', Miskin:'#f97316', 'Hampir Miskin':'#eab308' };
|
|
const warnaDTKS = { 'Terdaftar DTKS':'#059669', 'Sedang Diusulkan':'#d97706', 'Belum DTKS':'#dc2626' };
|
|
|
|
list.innerHTML = `
|
|
<!-- 3 Kartu Ringkasan (klikabel) -->
|
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:8px;padding:10px 10px 0">
|
|
<div class="dash-card clickable" onclick="drillTotalKK()" title="Klik untuk detail" style="text-align:center;border-top:3px solid var(--accent)">
|
|
<div class="dash-card-title"><i class="fas fa-users" style="color:var(--accent)"></i> Total KK Miskin</div>
|
|
<div class="dash-card-value">${db.total_kk}</div>
|
|
<div class="dash-card-sub">Terdaftar sistem · <span style="color:var(--accent);font-size:.62rem">▶ Lihat semua</span></div>
|
|
</div>
|
|
<div class="dash-card clickable" onclick="drillSudahBantuan(${tahun})" title="Klik untuk detail" style="text-align:center;border-top:3px solid #059669">
|
|
<div class="dash-card-title"><i class="fas fa-circle-check" style="color:#059669"></i> Sudah Dapat Bantuan</div>
|
|
<div class="dash-card-value" style="color:#059669">${db.kk_sudah_bantuan}</div>
|
|
<div class="dash-card-sub">KK tahun ${tahun} (${pct(db.kk_sudah_bantuan, db.total_kk)}%) · <span style="color:#059669;font-size:.62rem">▶ Detail</span></div>
|
|
</div>
|
|
<div class="dash-card clickable" onclick="drillBelumBantuan(${tahun})" title="Klik untuk detail" style="text-align:center;border-top:3px solid var(--danger)">
|
|
<div class="dash-card-title"><i class="fas fa-circle-exclamation" style="color:var(--danger)"></i> Belum Dapat Bantuan</div>
|
|
<div class="dash-card-value" style="color:var(--danger)">${db.kk_belum_bantuan}</div>
|
|
<div class="dash-card-sub">KK tahun ${tahun} (${pct(db.kk_belum_bantuan, db.total_kk)}%) · <span style="color:var(--danger);font-size:.62rem">▶ Detail</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="dashboard-grid" style="padding:8px 10px 10px">
|
|
|
|
<!-- KK per Kecamatan -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-map-pin" style="color:var(--accent)"></i> KK per Kecamatan</div>
|
|
<div class="dash-list">
|
|
${db.kk_per_kecamatan.map(k => `
|
|
<div class="dash-list-item clickable" onclick="drillKecamatan('${esc(k.kecamatan)}')" title="Klik: lihat di peta & detail">
|
|
<span>${esc(k.kecamatan).replace('Pontianak ','')}</span>
|
|
<span style="display:flex;align-items:center;gap:4px">${k.jumlah} <span style="font-size:.58rem;color:var(--accent)">▸</span></span>
|
|
</div>
|
|
${bar(k.jumlah, maxKK, 'var(--accent)')}
|
|
`).join('')}
|
|
${db.kk_per_kecamatan.length === 0 ? '<div style="font-size:.72rem;color:var(--text-muted)">Belum ada data</div>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sebaran Penyakit -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-heart-pulse" style="color:#ef4444"></i> Sebaran Penyakit</div>
|
|
<div class="dash-list">
|
|
${db.distribusi_penyakit.slice(0,7).map(p => `
|
|
<div class="dash-list-item clickable" onclick="drillPenyakit('${esc(p.penyakit)}')" title="Klik untuk detail">
|
|
<span>${esc(p.penyakit)}</span>
|
|
<span style="display:flex;align-items:center;gap:4px">${p.jumlah} KK <span style="font-size:.58rem;color:#ef4444">▸</span></span>
|
|
</div>
|
|
${bar(p.jumlah, maxPenyakit, '#ef4444')}
|
|
`).join('')}
|
|
${db.distribusi_penyakit.length === 0 ? '<div style="font-size:.72rem;color:var(--text-muted)">Belum ada data</div>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bantuan per Jenis -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-hand-holding-heart" style="color:#8b5cf6"></i> Bantuan ${tahun} (per Jenis)</div>
|
|
<div class="dash-list">
|
|
${db.bantuan_per_jenis.map(b => `
|
|
<div class="dash-list-item clickable" onclick="drillBantuanJenis('${esc(b.jenis_bantuan)}', ${tahun})" title="Klik untuk detail">
|
|
<span style="font-size:.67rem">${esc(b.jenis_bantuan)}</span>
|
|
<span style="text-align:right">
|
|
<div style="font-weight:700;color:var(--accent);font-size:.7rem">${esc(b.total_nominal_formatted)} <span style="font-size:.56rem">▸</span></div>
|
|
<div style="font-size:.6rem;color:var(--text-muted)">${b.jumlah_kk} KK</div>
|
|
</span>
|
|
</div>
|
|
`).join('')}
|
|
${db.bantuan_per_jenis.length === 0 ? '<div style="font-size:.72rem;color:var(--text-muted)">Belum ada bantuan tahun ini</div>' : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sumber Dana -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-coins" style="color:#f59e0b"></i> Sumber Dana ${tahun}</div>
|
|
<div class="dash-list">
|
|
${db.bantuan_per_sumber.map(b => `
|
|
<div class="dash-list-item clickable" onclick="drillBantuanSumber('${esc(b.sumber_dana)}', ${tahun})" title="Klik untuk detail">
|
|
<span style="display:flex;align-items:center;gap:5px">
|
|
<span style="width:8px;height:8px;border-radius:50%;background:${warnaSumber[b.sumber_dana]||'#888'};flex-shrink:0"></span>
|
|
${esc(b.sumber_dana)}
|
|
</span>
|
|
<span style="text-align:right">
|
|
<div style="font-weight:700;font-size:.7rem">${esc(b.total_nominal_formatted)} <span style="font-size:.56rem;color:var(--accent)">▸</span></div>
|
|
<div style="font-size:.6rem;color:var(--text-muted)">${b.jumlah_kk} KK</div>
|
|
</span>
|
|
</div>
|
|
`).join('')}
|
|
${db.bantuan_per_sumber.length === 0 ? '<div style="font-size:.72rem;color:var(--text-muted)">Belum ada data</div>' : ''}
|
|
</div>
|
|
${db.total_nominal_bantuan > 0 ? `<div style="margin-top:8px;padding-top:8px;border-top:1px solid var(--border);font-size:.7rem;color:var(--text-muted)">Total tersalurkan: <b style="color:var(--accent);font-size:.78rem">${db.total_nominal_formatted}</b></div>` : ''}
|
|
</div>
|
|
|
|
<!-- Status DTKS -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-id-card" style="color:#3b82f6"></i> Status DTKS</div>
|
|
<div class="dash-list">
|
|
${db.distribusi_dtks.map(d => `
|
|
<div class="dash-list-item clickable" onclick="drillDTKS('${esc(d.status_dtks)}')" title="Klik untuk detail">
|
|
<span style="display:flex;align-items:center;gap:5px">
|
|
<span style="width:8px;height:8px;border-radius:50%;background:${warnaDTKS[d.status_dtks]||'#888'};flex-shrink:0"></span>
|
|
${esc(d.status_dtks)}
|
|
</span>
|
|
<span style="display:flex;align-items:center;gap:4px">${d.jumlah} KK <span style="font-size:.58rem;color:var(--accent)">▸</span></span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Laporan & Kemiskinan -->
|
|
<div class="dash-card">
|
|
<div class="dash-card-title"><i class="fas fa-bullhorn" style="color:var(--warning)"></i> Laporan Masyarakat</div>
|
|
<div class="dash-card clickable" onclick="drillLaporan()" style="display:flex;align-items:center;gap:10px;margin-bottom:10px;padding:8px 10px;border-radius:var(--radius-sm);border:1px solid var(--warning)">
|
|
<div style="font-size:2rem;font-weight:800;color:var(--warning);line-height:1">${db.laporan_baru}</div>
|
|
<div style="font-size:.72rem;color:var(--text-muted)">laporan belum<br>ditangani</div>
|
|
<div style="margin-left:auto;font-size:.65rem;color:var(--warning);font-weight:700">▶ Lihat</div>
|
|
</div>
|
|
<div class="dash-card-title" style="margin-top:8px">Tingkat Kemiskinan</div>
|
|
<div class="dash-list">
|
|
${db.distribusi_kemiskinan.map(d => `
|
|
<div class="dash-list-item">
|
|
<span style="color:${warnaKM[d.tingkat_kemiskinan]||'#888'};font-weight:600;font-size:.7rem">${esc(d.tingkat_kemiskinan)}</span>
|
|
<span>${d.jumlah} KK</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
</div>`;
|
|
}
|
|
|
|
function emptyEl(icon, msg) {
|
|
return `<div class="empty-state"><i class="fas fa-${icon}"></i><p>${msg}</p></div>`;
|
|
}
|
|
|
|
/* ================================================================
|
|
LOAD DATA & RENDER MAP LAYERS
|
|
================================================================ */
|
|
async function loadAllData() {
|
|
try {
|
|
const [riRes, kkRes] = await Promise.all([
|
|
fetchJSON(API.ri),
|
|
fetchJSON(API.kk),
|
|
]);
|
|
dataRI = riRes.data || [];
|
|
dataKK = kkRes.data || [];
|
|
renderRILayers();
|
|
renderKKLayers();
|
|
|
|
// Muat laporan jika admin/walikota
|
|
if (currentUser && (currentUser.role === 'admin' || currentUser.role === 'walikota')) {
|
|
const lapRes = await fetchJSON(API.laporan);
|
|
dataLaporan = lapRes.data || [];
|
|
} else {
|
|
dataLaporan = [];
|
|
}
|
|
|
|
renderLegend();
|
|
renderFilterBar();
|
|
renderList();
|
|
} catch(e) { console.error('Gagal memuat data', e); }
|
|
}
|
|
|
|
function renderRILayers() {
|
|
riLayerGroup.clearLayers();
|
|
riRadiusGroup.clearLayers();
|
|
layersRI = {};
|
|
layersRIRadius = {};
|
|
|
|
dataRI.forEach(d => {
|
|
const w = WARNA_RI[d.jenis] || '#888';
|
|
const radiusM = parseFloat(d.radius_km) * 1000;
|
|
|
|
const marker = L.circleMarker([d.lat, d.lng], {
|
|
radius: 9, fillColor: w, color: '#fff', weight: 2.5, fillOpacity: 0.95
|
|
}).addTo(riLayerGroup);
|
|
|
|
const circle = L.circle([d.lat, d.lng], {
|
|
radius: radiusM, color: w, weight: 1.5, fillOpacity: 0.08, dashArray: '6,4'
|
|
});
|
|
|
|
marker.bindPopup(`
|
|
<div style="min-width:200px">
|
|
<b style="font-size:.9rem">${esc(d.nama)}</b><br>
|
|
<span style="color:${w};font-weight:600">${esc(d.jenis)}</span> — ${esc(d.kecamatan)}<br>
|
|
<span style="color:var(--text-secondary)">Radius: <b>${parseFloat(d.radius_km).toFixed(1)} km</b> | KK Binaan: <b>${d.jumlah_kk_binaan||0}</b></span><br>
|
|
<small style="color:var(--text-muted)">${esc(d.alamat)}</small>
|
|
</div>
|
|
`);
|
|
|
|
marker.on('click', () => {
|
|
riRadiusGroup.clearLayers();
|
|
circle.addTo(riRadiusGroup);
|
|
highlightCard('ri', d.id);
|
|
});
|
|
|
|
layersRI[d.id] = marker;
|
|
layersRIRadius[d.id] = circle;
|
|
});
|
|
}
|
|
|
|
function renderKKLayers() {
|
|
kkLayerGroup.clearLayers();
|
|
layersKK = {};
|
|
|
|
dataKK.forEach(d => {
|
|
const w = WARNA_KM[d.tingkat_kemiskinan] || '#888';
|
|
const penyakit = (d.riwayat_penyakit || []);
|
|
const penyakitHtml = penyakit.length ? '<br><b>Riwayat Penyakit:</b><br>' + penyakit.map(p=>`<span class="penyakit-tag">${esc(p)}</span>`).join(' ') : '';
|
|
|
|
const marker = L.circleMarker([d.lat, d.lng], {
|
|
radius: 6, fillColor: w, color: '#111', weight: 1, fillOpacity: 0.9
|
|
}).addTo(kkLayerGroup);
|
|
|
|
marker.bindPopup(`
|
|
<div style="min-width:220px">
|
|
<b style="font-size:.88rem">${esc(d.nama_kepala_keluarga)}</b><br>
|
|
<span style="color:${w};font-weight:600">${esc(d.tingkat_kemiskinan)}</span> — ${d.jumlah_anggota} orang<br>
|
|
<small>NIK: ${esc(d.nik)} | ${esc(d.pendidikan)} | ${esc(d.status_dtks)}</small><br>
|
|
${d.nama_ri ? `<small>${esc(d.nama_ri)} (${d.jarak_ke_ibadah ? parseFloat(d.jarak_ke_ibadah).toFixed(2)+' km' : '-'})</small><br>` : ''}
|
|
${d.eligible_bansos_tunai ? '<small style="color:var(--success)">✓ Eligible Bansos Tunai Pemkot</small><br>' : ''}
|
|
${d.total_bantuan > 0 ? `<small style="color:var(--info);font-weight:600">💰 Total Bantuan: ${esc(d.total_bantuan_formatted)}</small><br>` : ''}
|
|
${penyakitHtml}
|
|
<small style="color:var(--text-muted)">${esc(d.alamat)}</small>
|
|
<div id="popup-bantuan-${d.id}" style="margin-top:6px"></div>
|
|
</div>
|
|
`);
|
|
|
|
marker.on('click', () => {
|
|
riRadiusGroup.clearLayers();
|
|
if (d.rumah_ibadah_id && layersRIRadius[d.rumah_ibadah_id]) {
|
|
layersRIRadius[d.rumah_ibadah_id].addTo(riRadiusGroup);
|
|
}
|
|
highlightCard('kk', d.id);
|
|
// Muat histori bantuan di popup
|
|
loadBantuanPopup(d.id);
|
|
});
|
|
|
|
layersKK[d.id] = marker;
|
|
});
|
|
}
|
|
|
|
async function loadBantuanPopup(kkId) {
|
|
try {
|
|
const data = await fetchJSON(API.bantuan + '?kk_id=' + kkId);
|
|
const rows = data.data || [];
|
|
const el = document.getElementById('popup-bantuan-' + kkId);
|
|
if (!el) return;
|
|
if (!rows.length) { el.innerHTML = '<small style="color:var(--text-muted)">Belum ada histori bantuan.</small>'; return; }
|
|
const total = data.total_bantuan_formatted || '';
|
|
el.innerHTML = `<table><tr><th>Bantuan</th><th>Nominal</th><th>Tanggal</th></tr>
|
|
${rows.map(r=>`<tr><td>${esc(r.jenis_bantuan)}</td><td>${esc(r.nominal_formatted)}</td><td>${r.tanggal_penyaluran}</td></tr>`).join('')}
|
|
</table>
|
|
${total ? `<small style="color:var(--accent);font-weight:700">Total: ${total}</small>` : ''}`;
|
|
} catch(e) {}
|
|
}
|
|
|
|
function zoomToRI(id) {
|
|
const m = layersRI[id]; if (!m) return;
|
|
riRadiusGroup.clearLayers();
|
|
if (layersRIRadius[id]) layersRIRadius[id].addTo(riRadiusGroup);
|
|
map.setView(m.getLatLng(), 15);
|
|
m.openPopup();
|
|
highlightCard('ri', id);
|
|
}
|
|
function zoomToKK(id) {
|
|
const m = layersKK[id]; if (!m) return;
|
|
const d = dataKK.find(k => k.id == id);
|
|
riRadiusGroup.clearLayers();
|
|
if (d && d.rumah_ibadah_id && layersRIRadius[d.rumah_ibadah_id]) {
|
|
layersRIRadius[d.rumah_ibadah_id].addTo(riRadiusGroup);
|
|
}
|
|
map.setView(m.getLatLng(), 17);
|
|
m.openPopup();
|
|
highlightCard('kk', id);
|
|
loadBantuanPopup(id);
|
|
}
|
|
function zoomToLaporan(id, lat, lng) {
|
|
map.setView([lat, lng], 17);
|
|
}
|
|
function highlightCard(type, id) {
|
|
document.querySelectorAll('.data-card').forEach(c => c.classList.remove('active-card'));
|
|
const c = document.getElementById(`card-${type}-${id}`);
|
|
if (c) { c.classList.add('active-card'); c.scrollIntoView({ behavior:'smooth', block:'nearest' }); }
|
|
}
|
|
|
|
/* ================================================================
|
|
CRUD: RUMAH IBADAH
|
|
================================================================ */
|
|
function startAdd() {
|
|
if (!currentUser) return showToast('Silakan login terlebih dahulu', 'error');
|
|
if (currentTab === 'ri') {
|
|
if (currentUser.role !== 'admin') return showToast('Hanya admin yang dapat menambah rumah ibadah', 'error');
|
|
document.getElementById('modal-ri-title').textContent = 'Tambah Rumah Ibadah';
|
|
document.getElementById('ri-id').value = '';
|
|
document.getElementById('ri-nama').value = '';
|
|
document.getElementById('ri-jenis').value = '';
|
|
document.getElementById('ri-kecamatan').value = '';
|
|
document.getElementById('ri-lat').value = '';
|
|
document.getElementById('ri-lng').value = '';
|
|
document.getElementById('ri-alamat').value = '';
|
|
document.getElementById('ri-radius').value = '2.0';
|
|
document.getElementById('ri-radius-val').textContent = '2.0';
|
|
openModal('ri');
|
|
} else if (currentTab === 'kk') {
|
|
document.getElementById('modal-kk-title').textContent = 'Tambah Keluarga Miskin';
|
|
resetKKForm();
|
|
openModal('kk');
|
|
}
|
|
}
|
|
|
|
function editRI(id) {
|
|
if (!currentUser) return showToast('Silakan login', 'error');
|
|
const d = dataRI.find(x => x.id == id); if (!d) return;
|
|
document.getElementById('modal-ri-title').textContent = 'Edit Rumah Ibadah';
|
|
document.getElementById('ri-id').value = d.id;
|
|
document.getElementById('ri-nama').value = d.nama;
|
|
document.getElementById('ri-jenis').value = d.jenis;
|
|
document.getElementById('ri-kecamatan').value = d.kecamatan;
|
|
document.getElementById('ri-lat').value = d.lat;
|
|
document.getElementById('ri-lng').value = d.lng;
|
|
document.getElementById('ri-alamat').value = d.alamat;
|
|
document.getElementById('ri-radius').value = parseFloat(d.radius_km).toFixed(1);
|
|
document.getElementById('ri-radius-val').textContent = parseFloat(d.radius_km).toFixed(1);
|
|
openModal('ri');
|
|
}
|
|
|
|
async function saveRI() {
|
|
const id = document.getElementById('ri-id').value;
|
|
const body = {
|
|
id: id || null,
|
|
nama: document.getElementById('ri-nama').value.trim(),
|
|
jenis: document.getElementById('ri-jenis').value,
|
|
kecamatan: document.getElementById('ri-kecamatan').value,
|
|
lat: document.getElementById('ri-lat').value,
|
|
lng: document.getElementById('ri-lng').value,
|
|
alamat: document.getElementById('ri-alamat').value.trim(),
|
|
radius_km: parseFloat(document.getElementById('ri-radius').value),
|
|
};
|
|
if (!body.nama || !body.jenis || !body.kecamatan || !body.lat || !body.lng || !body.alamat) {
|
|
return showToast('Lengkapi semua field yang wajib', 'error');
|
|
}
|
|
try {
|
|
const data = await fetchJSON(API.ri, {
|
|
method: id ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
closeModal('ri');
|
|
// Tampilkan info pengurus baru jika ada
|
|
if (data.data && data.data.kode_pengurus) {
|
|
document.getElementById('info-kode').value = data.data.kode_pengurus;
|
|
document.getElementById('info-pass').value = data.data.password_pengurus || '';
|
|
openModal('pengurus-info');
|
|
}
|
|
await loadAllData();
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal terhubung ke server', 'error'); }
|
|
}
|
|
|
|
/* ================================================================
|
|
RESET PASSWORD PENGURUS
|
|
================================================================ */
|
|
let _resetPwTargetId = null;
|
|
|
|
function resetPasswordPengurus(id, nama) {
|
|
_resetPwTargetId = id;
|
|
// Reset modal ke state awal (konfirmasi)
|
|
document.getElementById('reset-pw-nama').textContent = nama;
|
|
document.getElementById('reset-pw-confirm-wrap').style.display = '';
|
|
document.getElementById('reset-pw-result-wrap').style.display = 'none';
|
|
document.getElementById('reset-pw-confirm-box').style.display = '';
|
|
document.getElementById('reset-pw-kode').value = '';
|
|
document.getElementById('reset-pw-pass').value = '';
|
|
// Tampilkan tombol Reset
|
|
const footer = document.getElementById('reset-pw-footer');
|
|
footer.innerHTML = `
|
|
<button class="btn-cancel" onclick="closeModal('reset-pw')">Batal</button>
|
|
<button class="btn-save" id="btn-do-reset"
|
|
style="background:var(--warning);box-shadow:0 2px 8px rgba(217,119,6,0.3)"
|
|
onclick="doResetPassword()">
|
|
<i class="fas fa-rotate-right"></i> Reset Password
|
|
</button>`;
|
|
openModal('reset-pw');
|
|
}
|
|
|
|
async function doResetPassword() {
|
|
if (!_resetPwTargetId) return;
|
|
const btn = document.getElementById('btn-do-reset');
|
|
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Memproses...'; }
|
|
try {
|
|
const data = await fetchJSON(API.ri, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id: _resetPwTargetId })
|
|
});
|
|
if (data.success) {
|
|
// Tampilkan hasil — sembunyikan konfirmasi, tampilkan kredensial baru
|
|
document.getElementById('reset-pw-confirm-wrap').style.display = 'none';
|
|
document.getElementById('reset-pw-confirm-box').style.display = 'none';
|
|
document.getElementById('reset-pw-kode').value = data.data.kode_pengurus;
|
|
document.getElementById('reset-pw-pass').value = data.data.password_pengurus;
|
|
document.getElementById('reset-pw-result-wrap').style.display = '';
|
|
// Ganti footer: hanya tombol tutup
|
|
document.getElementById('reset-pw-footer').innerHTML = `
|
|
<button class="btn-save" onclick="closeModal('reset-pw')">
|
|
<i class="fas fa-check"></i> Sudah Dicatat, Tutup
|
|
</button>`;
|
|
showToast('Password pengurus berhasil direset', 'success');
|
|
} else {
|
|
showToast(data.message || 'Gagal mereset password', 'error');
|
|
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-rotate-right"></i> Reset Password'; }
|
|
}
|
|
} catch(e) {
|
|
showToast('Gagal terhubung ke server', 'error');
|
|
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-rotate-right"></i> Reset Password'; }
|
|
}
|
|
}
|
|
|
|
function copyToClipboard(inputId) {
|
|
const el = document.getElementById(inputId);
|
|
if (!el) return;
|
|
navigator.clipboard.writeText(el.value).then(() => {
|
|
showToast('Disalin ke clipboard!', 'info');
|
|
}).catch(() => {
|
|
// Fallback untuk browser lama
|
|
el.select();
|
|
document.execCommand('copy');
|
|
showToast('Disalin!', 'info');
|
|
});
|
|
}
|
|
|
|
/* ================================================================
|
|
CRUD: KELUARGA MISKIN
|
|
================================================================ */
|
|
function resetKKForm() {
|
|
document.getElementById('kk-id').value = '';
|
|
document.getElementById('kk-nik').value = '';
|
|
document.getElementById('kk-nama').value = '';
|
|
document.getElementById('kk-tgl-lahir').value = '';
|
|
document.getElementById('kk-pendidikan').value = 'SMA';
|
|
document.getElementById('kk-jumlah').value = '1';
|
|
document.getElementById('kk-tingkat').value = 'Miskin';
|
|
document.getElementById('kk-dtks').value = 'Belum DTKS';
|
|
document.getElementById('kk-kecamatan').value = '';
|
|
document.getElementById('kk-lat').value = '';
|
|
document.getElementById('kk-lng').value = '';
|
|
document.getElementById('kk-alamat').value = '';
|
|
renderPenyakitCheckboxes([]);
|
|
}
|
|
|
|
function renderPenyakitCheckboxes(selected = []) {
|
|
const group = document.getElementById('kk-penyakit-group');
|
|
group.innerHTML = DAFTAR_PENYAKIT.map(p => {
|
|
const checked = selected.includes(p);
|
|
return `<label class="checkbox-tag ${checked?'checked':''}" onclick="this.classList.toggle('checked')">
|
|
<input type="checkbox" value="${p}" ${checked?'checked':''} onchange="this.parentElement.classList.toggle('checked',this.checked)"> ${p}
|
|
</label>`;
|
|
}).join('');
|
|
}
|
|
|
|
function getSelectedPenyakit() {
|
|
return Array.from(document.querySelectorAll('#kk-penyakit-group input:checked')).map(i => i.value);
|
|
}
|
|
|
|
function editKK(id) {
|
|
if (!currentUser) return showToast('Silakan login', 'error');
|
|
const d = dataKK.find(x => x.id == id); if (!d) return;
|
|
document.getElementById('modal-kk-title').textContent = 'Edit Keluarga Miskin';
|
|
document.getElementById('kk-id').value = d.id;
|
|
document.getElementById('kk-nik').value = d.nik;
|
|
document.getElementById('kk-nama').value = d.nama_kepala_keluarga;
|
|
document.getElementById('kk-tgl-lahir').value = d.tanggal_lahir || '';
|
|
document.getElementById('kk-pendidikan').value = d.pendidikan;
|
|
document.getElementById('kk-jumlah').value = d.jumlah_anggota;
|
|
document.getElementById('kk-tingkat').value = d.tingkat_kemiskinan;
|
|
document.getElementById('kk-dtks').value = d.status_dtks;
|
|
document.getElementById('kk-kecamatan').value = d.kecamatan;
|
|
document.getElementById('kk-lat').value = d.lat;
|
|
document.getElementById('kk-lng').value = d.lng;
|
|
document.getElementById('kk-alamat').value = d.alamat;
|
|
renderPenyakitCheckboxes(d.riwayat_penyakit || []);
|
|
openModal('kk');
|
|
}
|
|
|
|
async function saveKK() {
|
|
const id = document.getElementById('kk-id').value;
|
|
const body = {
|
|
id: id || null,
|
|
nik: document.getElementById('kk-nik').value.trim(),
|
|
nama_kepala_keluarga: document.getElementById('kk-nama').value.trim(),
|
|
tanggal_lahir: document.getElementById('kk-tgl-lahir').value,
|
|
pendidikan: document.getElementById('kk-pendidikan').value,
|
|
jumlah_anggota: parseInt(document.getElementById('kk-jumlah').value),
|
|
tingkat_kemiskinan: document.getElementById('kk-tingkat').value,
|
|
status_dtks: document.getElementById('kk-dtks').value,
|
|
kecamatan: document.getElementById('kk-kecamatan').value,
|
|
lat: document.getElementById('kk-lat').value,
|
|
lng: document.getElementById('kk-lng').value,
|
|
alamat: document.getElementById('kk-alamat').value.trim(),
|
|
riwayat_penyakit: getSelectedPenyakit(),
|
|
};
|
|
if (!body.nik || !body.nama_kepala_keluarga || !body.kecamatan || !body.lat || !body.lng || !body.alamat) {
|
|
return showToast('Lengkapi semua field yang wajib', 'error');
|
|
}
|
|
try {
|
|
const data = await fetchJSON(API.kk, {
|
|
method: id ? 'PUT' : 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (data.success) {
|
|
const newKKId = data.data?.id;
|
|
|
|
// ─ Cek apakah ini konversi dari laporan ─
|
|
if (pendingLaporanConvert && newKKId) {
|
|
const lapId = pendingLaporanConvert.laporan_id;
|
|
try {
|
|
await fetchJSON(API.laporan, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
id: lapId,
|
|
status: 'Diverifikasi',
|
|
converted_to_kk_id: newKKId,
|
|
})
|
|
});
|
|
showToast('KK berhasil dibuat dan laporan ditandai Diverifikasi', 'success');
|
|
} catch(e) {
|
|
showToast('KK dibuat, tapi gagal update status laporan', 'warning');
|
|
}
|
|
pendingLaporanConvert = null;
|
|
} else {
|
|
showToast(data.message, 'success');
|
|
}
|
|
|
|
closeModal('kk');
|
|
await loadAllData();
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal terhubung ke server', 'error'); }
|
|
}
|
|
|
|
/* ================================================================
|
|
LAPORAN MASYARAKAT
|
|
================================================================ */
|
|
async function submitLaporan() {
|
|
const alamat = document.getElementById('lap-alamat').value.trim();
|
|
const lat = document.getElementById('lap-lat').value;
|
|
const lng = document.getElementById('lap-lng').value;
|
|
const deskripsi = document.getElementById('lap-deskripsi').value.trim();
|
|
|
|
if (!alamat || !lat || !lng || !deskripsi) {
|
|
return showToast('Alamat, lokasi peta, dan deskripsi wajib diisi', 'error');
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama_terlapor', document.getElementById('lap-nama-terlapor').value.trim());
|
|
formData.append('nik_terlapor', document.getElementById('lap-nik-terlapor').value.trim());
|
|
formData.append('nama_pelapor', document.getElementById('lap-nama').value.trim());
|
|
formData.append('no_hp', document.getElementById('lap-hp').value.trim());
|
|
formData.append('alamat_terlapor', alamat);
|
|
formData.append('lat', lat);
|
|
formData.append('lng', lng);
|
|
formData.append('deskripsi', deskripsi);
|
|
|
|
const fotoInput = document.getElementById('lap-foto');
|
|
if (fotoInput.files.length > 0) {
|
|
formData.append('foto', fotoInput.files[0]);
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(API.laporan, { method: 'POST', body: formData, credentials: 'same-origin' });
|
|
const data = await res.json();
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
closeModal('laporan-form');
|
|
// Reset form
|
|
document.getElementById('lap-nama-terlapor').value = '';
|
|
document.getElementById('lap-nik-terlapor').value = '';
|
|
document.getElementById('lap-nama').value = '';
|
|
document.getElementById('lap-hp').value = '';
|
|
document.getElementById('lap-alamat').value = '';
|
|
document.getElementById('lap-lat').value = '';
|
|
document.getElementById('lap-lng').value = '';
|
|
document.getElementById('lap-deskripsi').value = '';
|
|
fotoInput.value = '';
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal mengirim laporan', 'error'); }
|
|
}
|
|
|
|
function kelolaLaporan(id) {
|
|
const d = dataLaporan.find(x => x.id == id); if (!d) return;
|
|
document.getElementById('kelola-lap-id').value = d.id;
|
|
document.getElementById('kelola-lap-status').value = d.status;
|
|
document.getElementById('kelola-alasan').value = d.alasan_tolak || '';
|
|
|
|
// Detail laporan
|
|
document.getElementById('kelola-laporan-detail').innerHTML = `
|
|
<div style="font-size:.8rem;line-height:1.8;color:var(--text-secondary)">
|
|
<div style="background:var(--bg-muted);border-radius:var(--radius-sm);padding:10px 12px;margin-bottom:10px;border:1px solid var(--border)">
|
|
<div style="font-size:.62rem;font-weight:800;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:5px">Identitas Warga Dilaporkan</div>
|
|
<b style="color:var(--text-primary)">Nama:</b> ${esc(d.nama_terlapor || '— tidak diisi')}<br>
|
|
<b style="color:var(--text-primary)">NIK:</b> ${esc(d.nik_terlapor || '— tidak diisi')}
|
|
</div>
|
|
<b style="color:var(--text-primary)">Pelapor:</b> ${esc(d.nama_pelapor || 'Anonim')} ${d.no_hp ? '('+esc(d.no_hp)+')' : ''}<br>
|
|
<b style="color:var(--text-primary)">Alamat:</b> ${esc(d.alamat_terlapor)}<br>
|
|
<b style="color:var(--text-primary)">Deskripsi:</b><br>${esc(d.deskripsi)}
|
|
${d.foto ? `<br><br><img src="${d.foto}" style="max-width:100%;border-radius:8px;border:1px solid var(--border)">` : ''}
|
|
</div>`;
|
|
|
|
toggleKelolaFields(d.status);
|
|
document.getElementById('kelola-lap-status').addEventListener('change', function() { toggleKelolaFields(this.value); });
|
|
|
|
// Tombol "Buat KK" hanya tampil saat status = Baru dan user adalah admin
|
|
const btnBuatKK = document.getElementById('btn-buat-kk-dari-laporan');
|
|
const isAdmin = currentUser && currentUser.role === 'admin';
|
|
btnBuatKK.style.display = (isAdmin && d.status === 'Baru') ? 'inline-flex' : 'none';
|
|
// Simpan data laporan ke tombol agar bisa diakses saat diklik
|
|
btnBuatKK._lapData = d;
|
|
|
|
// Sembunyikan aksi jika walikota
|
|
document.getElementById('kelola-footer').style.display = isAdmin ? '' : 'none';
|
|
document.getElementById('kelola-laporan-actions').style.display = isAdmin ? '' : 'none';
|
|
|
|
openModal('kelola-laporan');
|
|
}
|
|
|
|
function toggleKelolaFields(status) {
|
|
document.getElementById('kelola-alasan-wrap').style.display = status === 'Ditolak' ? '' : 'none';
|
|
document.getElementById('kelola-convert-wrap').style.display = status === 'Diverifikasi' ? '' : 'none';
|
|
}
|
|
|
|
async function updateLaporan() {
|
|
const id = document.getElementById('kelola-lap-id').value;
|
|
const status = document.getElementById('kelola-lap-status').value;
|
|
const body = {
|
|
id: parseInt(id),
|
|
status: status,
|
|
alasan_tolak: document.getElementById('kelola-alasan').value.trim(),
|
|
};
|
|
const convertId = document.getElementById('kelola-convert-id').value;
|
|
if (convertId) body.converted_to_kk_id = parseInt(convertId);
|
|
|
|
try {
|
|
const data = await fetchJSON(API.laporan, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (data.success) {
|
|
showToast(data.message, 'success');
|
|
closeModal('kelola-laporan');
|
|
await loadAllData();
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal mengupdate laporan', 'error'); }
|
|
}
|
|
|
|
function buatKKdariLaporan() {
|
|
// Ambil data laporan dari tombol
|
|
const btnBuatKK = document.getElementById('btn-buat-kk-dari-laporan');
|
|
const d = btnBuatKK._lapData;
|
|
if (!d) return;
|
|
|
|
// Simpan konteks konversi agar saveKK bisa ambil setelah berhasil
|
|
pendingLaporanConvert = { laporan_id: d.id };
|
|
|
|
// Tutup modal laporan
|
|
closeModal('kelola-laporan');
|
|
|
|
// Reset form KK lalu pre-fill dari data laporan
|
|
resetKKForm();
|
|
document.getElementById('modal-kk-title').textContent = 'Tambah KK — dari Laporan #' + d.id;
|
|
|
|
// Isi koordinat & alamat dari laporan
|
|
if (d.lat) document.getElementById('kk-lat').value = d.lat;
|
|
if (d.lng) document.getElementById('kk-lng').value = d.lng;
|
|
if (d.alamat_terlapor) document.getElementById('kk-alamat').value = d.alamat_terlapor;
|
|
|
|
// Isi identitas jika ada
|
|
if (d.nama_terlapor) document.getElementById('kk-nama').value = d.nama_terlapor;
|
|
if (d.nik_terlapor) document.getElementById('kk-nik').value = d.nik_terlapor;
|
|
|
|
// Tampilkan alert di dalam form KK bahwa ini dari laporan
|
|
showToast('Form diisi dari data laporan #' + d.id + '. Lengkapi field yang masih kosong.', 'info');
|
|
|
|
openModal('kk');
|
|
}
|
|
|
|
/* ================================================================
|
|
HISTORI BANTUAN
|
|
================================================================ */
|
|
async function openBantuan(kkId, namaKK) {
|
|
document.getElementById('modal-bantuan-title').textContent = 'Histori Bantuan — ' + namaKK;
|
|
document.getElementById('bantuan-kk-id').value = kkId;
|
|
|
|
// Tampilkan form tambah hanya untuk admin/pengurus
|
|
const canAdd = currentUser && (currentUser.role === 'admin' || currentUser.role === 'pengurus');
|
|
document.getElementById('bantuan-form-wrap').style.display = canAdd ? '' : 'none';
|
|
|
|
// Reset form
|
|
document.getElementById('bantuan-jenis').value = 'PKH';
|
|
document.getElementById('bantuan-nominal').value = '';
|
|
document.getElementById('bantuan-nominal-preview').textContent = '';
|
|
document.getElementById('bantuan-tanggal').value = '';
|
|
document.getElementById('bantuan-sumber').value = 'APBD';
|
|
document.getElementById('bantuan-ket').value = '';
|
|
|
|
// Muat data
|
|
await refreshBantuanList(kkId);
|
|
openModal('bantuan');
|
|
}
|
|
|
|
async function refreshBantuanList(kkId) {
|
|
const listEl = document.getElementById('bantuan-list');
|
|
try {
|
|
const data = await fetchJSON(API.bantuan + '?kk_id=' + kkId);
|
|
const rows = data.data || [];
|
|
if (!rows.length) {
|
|
listEl.innerHTML = '<div class="empty-state" style="padding:20px"><i class="fas fa-hand-holding-heart"></i><p>Belum ada histori bantuan.</p></div>';
|
|
return;
|
|
}
|
|
const isAdmin = currentUser && currentUser.role === 'admin';
|
|
const total = data.total_bantuan_formatted || '';
|
|
listEl.innerHTML = `<table style="width:100%;font-size:.76rem;border-collapse:collapse">
|
|
<tr><th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted)">Jenis</th>
|
|
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted)">Nominal</th>
|
|
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted)">Keterangan</th>
|
|
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted)">Tanggal</th>
|
|
<th style="padding:6px 8px;text-align:left;border-bottom:1px solid var(--border);color:var(--text-muted)">Sumber</th>
|
|
${isAdmin ? '<th></th>' : ''}</tr>
|
|
${rows.map(r => `<tr>
|
|
<td style="padding:6px 8px;border-bottom:1px solid rgba(55,65,95,.3)">${esc(r.jenis_bantuan)}</td>
|
|
<td style="padding:6px 8px;border-bottom:1px solid rgba(55,65,95,.3);font-weight:600;color:var(--accent)">${esc(r.nominal_formatted)}</td>
|
|
<td style="padding:6px 8px;border-bottom:1px solid rgba(55,65,95,.3);color:var(--text-muted);font-size:.68rem">${esc(r.keterangan || '—')}</td>
|
|
<td style="padding:6px 8px;border-bottom:1px solid rgba(55,65,95,.3)">${r.tanggal_penyaluran}</td>
|
|
<td style="padding:6px 8px;border-bottom:1px solid rgba(55,65,95,.3)">${esc(r.sumber_dana)}</td>
|
|
${isAdmin ? `<td><button class="btn-sm btn-del" style="padding:2px 6px" onclick="deleteBantuan(${r.id})"><i class="fas fa-trash"></i></button></td>` : ''}
|
|
</tr>`).join('')}
|
|
</table>
|
|
${total ? `<div style="margin-top:8px;padding:6px 8px;background:var(--accent-light);border-radius:var(--radius-sm);font-size:.75rem;font-weight:700;color:var(--accent)">Total Bantuan Diterima: ${total}</div>` : ''}`;
|
|
} catch(e) { listEl.innerHTML = '<p style="color:var(--danger);font-size:.8rem">Gagal memuat data.</p>'; }
|
|
}
|
|
|
|
async function saveBantuan() {
|
|
const kkId = document.getElementById('bantuan-kk-id').value;
|
|
const nominalRaw = parseInt(document.getElementById('bantuan-nominal').value || '0');
|
|
const body = {
|
|
kk_id: parseInt(kkId),
|
|
jenis_bantuan: document.getElementById('bantuan-jenis').value,
|
|
nominal: nominalRaw,
|
|
keterangan: document.getElementById('bantuan-ket').value.trim(),
|
|
tanggal_penyaluran: document.getElementById('bantuan-tanggal').value,
|
|
sumber_dana: document.getElementById('bantuan-sumber').value,
|
|
};
|
|
if (!body.tanggal_penyaluran) {
|
|
return showToast('Tanggal penyaluran wajib diisi', 'error');
|
|
}
|
|
if (body.nominal < 0) {
|
|
return showToast('Nominal tidak boleh negatif', 'error');
|
|
}
|
|
try {
|
|
const data = await fetchJSON(API.bantuan, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
if (data.success) {
|
|
showToast(data.message, data.duplikasi_warning ? 'warning' : 'success');
|
|
document.getElementById('bantuan-nominal').value = '';
|
|
document.getElementById('bantuan-nominal-preview').textContent = '';
|
|
document.getElementById('bantuan-tanggal').value = '';
|
|
document.getElementById('bantuan-ket').value = '';
|
|
await refreshBantuanList(kkId);
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal menyimpan bantuan', 'error'); }
|
|
}
|
|
|
|
async function deleteBantuan(id) {
|
|
if (!confirm('Hapus data bantuan ini?')) return;
|
|
try {
|
|
const data = await fetchJSON(API.bantuan + '?id=' + id, { method: 'DELETE' });
|
|
if (data.success) {
|
|
showToast('Bantuan dihapus', 'success');
|
|
const kkId = document.getElementById('bantuan-kk-id').value;
|
|
await refreshBantuanList(kkId);
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal menghapus', 'error'); }
|
|
}
|
|
|
|
/* ================================================================
|
|
DELETE (Rumah Ibadah / KK)
|
|
================================================================ */
|
|
function confirmDelete(type, id, name) {
|
|
if (!currentUser) return showToast('Silakan login', 'error');
|
|
deleteTarget = { type, id };
|
|
document.getElementById('confirm-name').textContent = name;
|
|
openModal('confirm');
|
|
}
|
|
|
|
document.getElementById('btn-confirm-del').addEventListener('click', async () => {
|
|
if (!deleteTarget) return;
|
|
const endpoint = deleteTarget.type === 'ri' ? API.ri : API.kk;
|
|
try {
|
|
const data = await fetchJSON(`${endpoint}?id=${deleteTarget.id}`, { method: 'DELETE' });
|
|
if (data.success) {
|
|
showToast('Data berhasil dihapus', 'success');
|
|
await loadAllData();
|
|
} else { showToast(data.message, 'error'); }
|
|
} catch(e) { showToast('Gagal menghapus', 'error'); }
|
|
closeModal('confirm');
|
|
deleteTarget = null;
|
|
});
|
|
|
|
/* ================================================================
|
|
PENYAKIT CHECKBOXES INIT
|
|
================================================================ */
|
|
function initPenyakitCheckboxes() {
|
|
renderPenyakitCheckboxes([]);
|
|
}
|
|
|
|
/* ================================================================
|
|
HELPER: FORMAT RUPIAH
|
|
================================================================ */
|
|
function formatRp(angka) {
|
|
return 'Rp ' + Number(angka || 0).toLocaleString('id-ID');
|
|
}
|
|
|
|
function updateNominalPreview() {
|
|
const val = parseInt(document.getElementById('bantuan-nominal').value || '0');
|
|
const preview = document.getElementById('bantuan-nominal-preview');
|
|
preview.textContent = val > 0 ? formatRp(val) : '';
|
|
}
|
|
|
|
/* ================================================================
|
|
EKSPOR DATA KK
|
|
================================================================ */
|
|
function getFilteredKK() {
|
|
let filtered = [...dataKK];
|
|
const fKec = document.getElementById('filter-kecamatan')?.value;
|
|
const fTingkat = document.getElementById('filter-tingkat')?.value;
|
|
const fDtks = document.getElementById('filter-dtks')?.value;
|
|
if (fKec) filtered = filtered.filter(d => d.kecamatan === fKec);
|
|
if (fTingkat) filtered = filtered.filter(d => d.tingkat_kemiskinan === fTingkat);
|
|
if (fDtks) filtered = filtered.filter(d => d.status_dtks === fDtks);
|
|
return filtered;
|
|
}
|
|
|
|
function exportToExcel() {
|
|
if (!window.XLSX) return showToast('Library Excel belum termuat', 'error');
|
|
const data = getFilteredKK();
|
|
if (!data.length) return showToast('Tidak ada data untuk diekspor', 'warning');
|
|
|
|
const rows = data.map(d => ({
|
|
'NIK': d.nik,
|
|
'Nama KK': d.nama_kepala_keluarga,
|
|
'Tanggal Lahir': d.tanggal_lahir || '',
|
|
'Pendidikan': d.pendidikan,
|
|
'Jumlah Anggota': d.jumlah_anggota,
|
|
'Alamat': d.alamat,
|
|
'Kecamatan': d.kecamatan,
|
|
'Tingkat Kemiskinan': d.tingkat_kemiskinan,
|
|
'Status DTKS': d.status_dtks,
|
|
'Riwayat Penyakit': (d.riwayat_penyakit || []).join(', '),
|
|
'Rumah Ibadah Binaan': d.nama_ri || '-',
|
|
'Total Bantuan (Rp)': d.total_bantuan || 0,
|
|
}));
|
|
|
|
const ws = XLSX.utils.json_to_sheet(rows);
|
|
|
|
// Set lebar kolom
|
|
ws['!cols'] = [
|
|
{wch:18},{wch:25},{wch:14},{wch:12},{wch:8},
|
|
{wch:35},{wch:20},{wch:16},{wch:18},{wch:35},{wch:30},{wch:16}
|
|
];
|
|
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Data KK Miskin');
|
|
|
|
const tgl = new Date().toISOString().slice(0,10);
|
|
XLSX.writeFile(wb, `KK-Miskin-Pontianak-${tgl}.xlsx`);
|
|
showToast(`${data.length} data diekspor ke Excel`, 'success');
|
|
}
|
|
|
|
function exportToPDF() {
|
|
if (!window.jspdf) return showToast('Library PDF belum termuat', 'error');
|
|
const data = getFilteredKK();
|
|
if (!data.length) return showToast('Tidak ada data untuk diekspor', 'warning');
|
|
|
|
const { jsPDF } = window.jspdf;
|
|
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
|
|
|
|
const tgl = new Date().toLocaleDateString('id-ID', { day:'2-digit', month:'long', year:'numeric' });
|
|
const tglFile = new Date().toISOString().slice(0,10);
|
|
|
|
// Header
|
|
doc.setFontSize(13);
|
|
doc.setFont('helvetica','bold');
|
|
doc.text('LAPORAN DATA KELUARGA MISKIN', 148.5, 14, { align: 'center' });
|
|
doc.setFontSize(10);
|
|
doc.setFont('helvetica','normal');
|
|
doc.text('Kota Pontianak', 148.5, 20, { align: 'center' });
|
|
|
|
// Filter info
|
|
const fKec = document.getElementById('filter-kecamatan')?.value || 'Semua Kecamatan';
|
|
const fTingkat = document.getElementById('filter-tingkat')?.value || 'Semua Tingkat';
|
|
const fDtks = document.getElementById('filter-dtks')?.value || 'Semua Status DTKS';
|
|
doc.setFontSize(8);
|
|
doc.setTextColor(100);
|
|
doc.text(`Filter: ${fKec} | ${fTingkat} | ${fDtks} | Total: ${data.length} KK`, 148.5, 26, { align: 'center' });
|
|
doc.setTextColor(0);
|
|
|
|
// Tabel
|
|
const head = [['No','NIK','Nama KK','Kecamatan','Tingkat Kemiskinan','Status DTKS','Penyakit','Total Bantuan']];
|
|
const body = data.map((d, i) => [
|
|
i + 1,
|
|
d.nik,
|
|
d.nama_kepala_keluarga,
|
|
d.kecamatan.replace('Pontianak ', ''),
|
|
d.tingkat_kemiskinan,
|
|
d.status_dtks,
|
|
(d.riwayat_penyakit || []).join(', ') || '-',
|
|
d.total_bantuan > 0 ? formatRp(d.total_bantuan) : '-',
|
|
]);
|
|
|
|
doc.autoTable({
|
|
head,
|
|
body,
|
|
startY: 30,
|
|
styles: { fontSize: 7, cellPadding: 2 },
|
|
headStyles: { fillColor: [14, 34, 64], textColor: 255, fontStyle: 'bold' },
|
|
alternateRowStyles: { fillColor: [245, 247, 252] },
|
|
columnStyles: {
|
|
0: { cellWidth: 8 },
|
|
1: { cellWidth: 30 },
|
|
2: { cellWidth: 45 },
|
|
3: { cellWidth: 22 },
|
|
4: { cellWidth: 28 },
|
|
5: { cellWidth: 28 },
|
|
6: { cellWidth: 38 },
|
|
7: { cellWidth: 30 },
|
|
},
|
|
didDrawPage(data) {
|
|
// Footer di setiap halaman
|
|
const pageH = doc.internal.pageSize.height;
|
|
doc.setFontSize(7);
|
|
doc.setTextColor(130);
|
|
doc.text(`Dicetak: ${tgl} | Dinas Sosial Kota Pontianak`, 14, pageH - 8);
|
|
doc.text(`Halaman ${data.pageNumber}`, 283, pageH - 8, { align: 'right' });
|
|
doc.setTextColor(0);
|
|
},
|
|
});
|
|
|
|
doc.save(`Laporan-KK-Pontianak-${tglFile}.pdf`);
|
|
showToast(`${data.length} data diekspor ke PDF`, 'success');
|
|
}
|
|
|
|
/* ================================================================
|
|
DRILLDOWN WALIKOTA — Generic Modal Engine
|
|
================================================================ */
|
|
let _ddData = []; // current rows di drilldown modal
|
|
let _ddCols = []; // column defs: {label, key, render?, excelRender?}
|
|
let _ddFileName = 'export';
|
|
let _ddLaporanMap = {}; // id→obj untuk laporan expanded detail
|
|
|
|
// Kecamatan center coords (Pontianak)
|
|
const KEC_CENTER = {
|
|
'Pontianak Utara': [-0.002, 109.338],
|
|
'Pontianak Selatan': [-0.082, 109.355],
|
|
'Pontianak Timur': [-0.035, 109.395],
|
|
'Pontianak Barat': [-0.022, 109.305],
|
|
'Pontianak Tenggara': [-0.072, 109.375],
|
|
'Pontianak Kota': [-0.038, 109.342],
|
|
};
|
|
|
|
/* Buka drilldown modal dengan data & kolom yang diberikan */
|
|
function openDD(title, rows, cols, { fileName = 'export', extra = '', totalText = '' } = {}) {
|
|
_ddData = rows;
|
|
_ddCols = cols;
|
|
_ddFileName = fileName;
|
|
|
|
document.getElementById('dd-title').innerHTML = `<i class="fas fa-table"></i> ${title}`;
|
|
document.getElementById('dd-count').textContent = rows.length + ' data';
|
|
|
|
let tableHtml = '';
|
|
if (!rows.length) {
|
|
tableHtml = '<div class="empty-state" style="padding:40px"><i class="fas fa-inbox"></i><p>Tidak ada data</p></div>';
|
|
} else {
|
|
const thead = `<thead><tr>${cols.map(c => `<th>${c.label}</th>`).join('')}</tr></thead>`;
|
|
const tbody = rows.map(r => {
|
|
const cls = r._rowClass ? ` class="${r._rowClass}"` : '';
|
|
const attr = r._onclick ? ` onclick="${r._onclick}"` : '';
|
|
return `<tr${cls}${attr}>${cols.map(c => {
|
|
const val = c.render ? c.render(r) : esc(String(r[c.key] ?? '-'));
|
|
return `<td>${val}</td>`;
|
|
}).join('')}</tr>`;
|
|
}).join('');
|
|
tableHtml = `<table class="dd-table">${thead}<tbody>${tbody}</tbody></table>`;
|
|
}
|
|
|
|
if (totalText) {
|
|
tableHtml += `<div class="dd-total-bar">${totalText}</div>`;
|
|
}
|
|
if (extra) tableHtml += extra;
|
|
|
|
document.getElementById('dd-body').innerHTML = tableHtml;
|
|
openModal('dd');
|
|
}
|
|
|
|
/* Loading state untuk async drilldowns */
|
|
function openDDLoading(title) {
|
|
document.getElementById('dd-title').innerHTML = `<i class="fas fa-spinner fa-spin"></i> Memuat ${title}…`;
|
|
document.getElementById('dd-count').textContent = '';
|
|
document.getElementById('dd-body').innerHTML = '<div class="empty-state" style="padding:40px"><i class="fas fa-spinner fa-spin"></i><p>Mengambil data…</p></div>';
|
|
openModal('dd');
|
|
}
|
|
|
|
/* Error state */
|
|
function openDDError(msg = 'Gagal memuat data') {
|
|
document.getElementById('dd-title').innerHTML = `<i class="fas fa-circle-xmark" style="color:var(--danger)"></i> Error`;
|
|
document.getElementById('dd-body').innerHTML = `<div class="empty-state" style="padding:40px"><i class="fas fa-circle-xmark" style="color:var(--danger)"></i><p>${esc(msg)}</p></div>`;
|
|
}
|
|
|
|
/* Ekspor drilldown ke Excel */
|
|
function exportDDExcel() {
|
|
if (!window.XLSX || !_ddData.length) return showToast('Tidak ada data untuk diekspor', 'warning');
|
|
const rows = _ddData.map(r => {
|
|
const obj = {};
|
|
_ddCols.forEach(c => {
|
|
obj[c.label] = c.excelRender ? c.excelRender(r) : (r[c.key] ?? '');
|
|
});
|
|
return obj;
|
|
});
|
|
const ws = XLSX.utils.json_to_sheet(rows);
|
|
const wb = XLSX.utils.book_new();
|
|
XLSX.utils.book_append_sheet(wb, ws, 'Data');
|
|
XLSX.writeFile(wb, _ddFileName + '-' + new Date().toISOString().slice(0,10) + '.xlsx');
|
|
showToast('Data diekspor ke Excel', 'success');
|
|
}
|
|
|
|
/* ── Kolom KK standar ─────────────────────────────────────── */
|
|
const COL_KK_STANDARD = [
|
|
{ label: 'Nama KK', key: 'nama_kepala_keluarga' },
|
|
{ label: 'Kecamatan', key: 'kecamatan' },
|
|
{ label: 'Tingkat Kemiskinan',key: 'tingkat_kemiskinan',
|
|
render: r => `<span style="color:${WARNA_KM[r.tingkat_kemiskinan]||'#888'};font-weight:600">${esc(r.tingkat_kemiskinan)}</span>` },
|
|
{ label: 'Status DTKS', key: 'status_dtks' },
|
|
{ label: 'Total Bantuan', key: 'total_bantuan_formatted',
|
|
render: r => r.total_bantuan > 0 ? `<b style="color:var(--accent)">${esc(r.total_bantuan_formatted)}</b>` : '<span style="color:var(--text-muted)">—</span>',
|
|
excelRender: r => r.total_bantuan || 0 },
|
|
];
|
|
|
|
/* ── Stat Card Drilldowns ────────────────────────────────── */
|
|
function drillTotalKK() {
|
|
openDD('Semua KK Miskin Terdaftar', dataKK, COL_KK_STANDARD, { fileName: 'Total-KK' });
|
|
}
|
|
function drillSudahBantuan(tahun) {
|
|
const rows = dataKK.filter(d => (d.total_bantuan || 0) > 0);
|
|
openDD(`KK Sudah Dapat Bantuan — ${tahun}`, rows, COL_KK_STANDARD, { fileName: 'KK-Sudah-Bantuan' });
|
|
}
|
|
function drillBelumBantuan(tahun) {
|
|
const rows = dataKK.filter(d => (d.total_bantuan || 0) === 0);
|
|
openDD(`KK Belum Dapat Bantuan — ${tahun}`, rows, COL_KK_STANDARD, { fileName: 'KK-Belum-Bantuan' });
|
|
}
|
|
|
|
/* ── Kecamatan Drilldown ─────────────────────────────────── */
|
|
function drillKecamatan(kec) {
|
|
const rows = dataKK.filter(d => d.kecamatan === kec);
|
|
|
|
// Fly peta ke kecamatan, pulse markers
|
|
const center = KEC_CENTER[kec];
|
|
if (center) {
|
|
map.flyTo(center, 15, { duration: 1.2 });
|
|
// Sementara perbesar marker kecamatan ini
|
|
dataKK.forEach(d => {
|
|
const m = layersKK[d.id];
|
|
if (!m) return;
|
|
if (d.kecamatan === kec) {
|
|
m.setRadius(11);
|
|
m.setStyle({ weight: 3, color: '#fff' });
|
|
setTimeout(() => { m.setRadius(6); m.setStyle({ weight: 1, color: '#111' }); }, 3000);
|
|
} else {
|
|
m.setStyle({ fillOpacity: 0.25 });
|
|
setTimeout(() => m.setStyle({ fillOpacity: 0.9 }), 3000);
|
|
}
|
|
});
|
|
}
|
|
|
|
openDD(`KK di ${kec} (${rows.length})`, rows, COL_KK_STANDARD, { fileName: `KK-${kec.replace(/\s/g,'-')}` });
|
|
}
|
|
|
|
/* ── Penyakit Drilldown ──────────────────────────────────── */
|
|
function drillPenyakit(penyakit) {
|
|
const rows = dataKK.filter(d => (d.riwayat_penyakit || []).includes(penyakit));
|
|
const cols = [
|
|
{ label: 'Nama KK', key: 'nama_kepala_keluarga' },
|
|
{ label: 'Alamat', key: 'alamat' },
|
|
{ label: 'Kecamatan', key: 'kecamatan' },
|
|
{ label: 'Rumah Ibadah', key: 'nama_ri',
|
|
render: r => esc(r.nama_ri || '—'),
|
|
excelRender: r => r.nama_ri || '' },
|
|
];
|
|
openDD(`KK dengan Riwayat ${penyakit} (${rows.length})`, rows, cols, { fileName: `Penyakit-${penyakit}` });
|
|
}
|
|
|
|
/* ── DTKS Drilldown ──────────────────────────────────────── */
|
|
function drillDTKS(status) {
|
|
const rows = dataKK.filter(d => d.status_dtks === status);
|
|
const cols = [
|
|
{ label: 'Nama KK', key: 'nama_kepala_keluarga' },
|
|
{ label: 'NIK', key: 'nik' },
|
|
{ label: 'Kecamatan', key: 'kecamatan' },
|
|
{ label: 'Tingkat Kemiskinan', key: 'tingkat_kemiskinan',
|
|
render: r => `<span style="color:${WARNA_KM[r.tingkat_kemiskinan]||'#888'};font-weight:600">${esc(r.tingkat_kemiskinan)}</span>` },
|
|
{ label: 'Rumah Ibadah', key: 'nama_ri',
|
|
render: r => esc(r.nama_ri || '—'),
|
|
excelRender: r => r.nama_ri || '' },
|
|
{ label: 'Eligible Bansos Tunai', key: 'eligible_bansos_tunai',
|
|
render: r => r.eligible_bansos_tunai ? '<span style="color:#059669;font-weight:700">✓ Ya</span>' : '<span style="color:var(--text-muted)">—</span>',
|
|
excelRender: r => r.eligible_bansos_tunai ? 'Ya' : 'Tidak' },
|
|
];
|
|
|
|
// Tandai baris eligible
|
|
const processedRows = rows.map(r => ({ ...r, _rowClass: r.eligible_bansos_tunai ? 'dd-row-eligible' : '' }));
|
|
|
|
const eligibleCount = rows.filter(r => r.eligible_bansos_tunai).length;
|
|
const extra = eligibleCount > 0
|
|
? `<div style="padding:8px 14px;background:rgba(5,150,105,.1);border-top:1px solid rgba(5,150,105,.3);font-size:.74rem;color:#059669;font-weight:700">
|
|
✓ ${eligibleCount} KK eligible Bansos Tunai Pemkot Rp600.000 (latar hijau)
|
|
</div>`
|
|
: '';
|
|
|
|
openDD(`Status DTKS: ${status} (${rows.length})`, processedRows, cols, { fileName: `DTKS-${status.replace(/\s/g,'-')}`, extra });
|
|
}
|
|
|
|
/* ── Bantuan per Jenis (async API) ───────────────────────── */
|
|
async function drillBantuanJenis(jenis, tahun) {
|
|
openDDLoading(jenis);
|
|
try {
|
|
const res = await fetchJSON(`${API.bantuan}?jenis_bantuan=${encodeURIComponent(jenis)}&tahun=${tahun}`);
|
|
if (!res.success) throw new Error(res.message);
|
|
const rows = res.data || [];
|
|
const totalText = res.total_nominal_formatted
|
|
? `Total tersalurkan: <b>${res.total_nominal_formatted}</b> dari ${rows.length} penyaluran`
|
|
: '';
|
|
const cols = [
|
|
{ label: 'Nama KK', key: 'nama_kepala_keluarga' },
|
|
{ label: 'Kecamatan', key: 'kecamatan' },
|
|
{ label: 'Nominal', key: 'nominal_formatted',
|
|
render: r => `<b style="color:var(--accent)">${esc(r.nominal_formatted)}</b>`,
|
|
excelRender: r => r.nominal || 0 },
|
|
{ label: 'Keterangan', key: 'keterangan',
|
|
render: r => esc(r.keterangan || '—') },
|
|
{ label: 'Tanggal', key: 'tanggal_penyaluran' },
|
|
{ label: 'Penanggung Jawab', key: 'nama_penanggung_jawab',
|
|
render: r => esc(r.nama_penanggung_jawab || '—') },
|
|
];
|
|
openDD(`Bantuan ${jenis} — ${tahun}`, rows, cols, { fileName: `Bantuan-${jenis}`, totalText });
|
|
} catch(e) { openDDError('Gagal memuat data bantuan per jenis'); }
|
|
}
|
|
|
|
/* ── Bantuan per Sumber Dana (async API) ─────────────────── */
|
|
async function drillBantuanSumber(sumber, tahun) {
|
|
openDDLoading(sumber);
|
|
try {
|
|
const res = await fetchJSON(`${API.bantuan}?sumber_dana=${encodeURIComponent(sumber)}&tahun=${tahun}`);
|
|
if (!res.success) throw new Error(res.message);
|
|
const rows = res.data || [];
|
|
const totalText = res.total_nominal_formatted
|
|
? `Total tersalurkan: <b>${res.total_nominal_formatted}</b> dari ${rows.length} penyaluran`
|
|
: '';
|
|
const cols = [
|
|
{ label: 'Nama KK', key: 'nama_kepala_keluarga' },
|
|
{ label: 'Kecamatan', key: 'kecamatan' },
|
|
{ label: 'Jenis Bantuan',key: 'jenis_bantuan' },
|
|
{ label: 'Nominal', key: 'nominal_formatted',
|
|
render: r => `<b style="color:var(--accent)">${esc(r.nominal_formatted)}</b>`,
|
|
excelRender: r => r.nominal || 0 },
|
|
{ label: 'Tanggal', key: 'tanggal_penyaluran' },
|
|
{ label: 'Penanggung Jawab', key: 'nama_penanggung_jawab',
|
|
render: r => esc(r.nama_penanggung_jawab || '—') },
|
|
];
|
|
openDD(`Sumber Dana ${sumber} — ${tahun}`, rows, cols, { fileName: `Sumber-${sumber}`, totalText });
|
|
} catch(e) { openDDError('Gagal memuat data bantuan per sumber dana'); }
|
|
}
|
|
|
|
/* ── Laporan Drilldown ───────────────────────────────────── */
|
|
function drillLaporan() {
|
|
const rows = dataLaporan.filter(d => d.status === 'Baru');
|
|
_ddLaporanMap = {};
|
|
rows.forEach(r => { _ddLaporanMap[r.id] = r; });
|
|
|
|
const cols = [
|
|
{ label: 'Tanggal', key: 'created_at',
|
|
render: r => (r.created_at || '').slice(0, 10),
|
|
excelRender: r => (r.created_at || '').slice(0, 10) },
|
|
{ label: 'Pelapor', key: 'nama_pelapor',
|
|
render: r => esc(r.nama_pelapor || 'Anonim') },
|
|
{ label: 'Alamat Terlapor', key: 'alamat_terlapor',
|
|
render: r => esc(r.alamat_terlapor) },
|
|
{ label: 'Deskripsi', key: 'deskripsi',
|
|
render: r => `<span title="${esc(r.deskripsi)}">${esc((r.deskripsi||'').slice(0,80))}${(r.deskripsi||'').length>80?'…':''}</span>`,
|
|
excelRender: r => r.deskripsi || '' },
|
|
{ label: 'Status', key: 'status',
|
|
render: r => `<span class="dd-badge" style="background:#d97706;color:#fff">${r.status}</span>` },
|
|
{ label: '', key: '_expand',
|
|
render: r => `<button onclick="showLaporanDetail(${r.id})" style="padding:3px 8px;font-size:.65rem;background:var(--accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-family:inherit">Detail</button>`,
|
|
excelRender: () => '' },
|
|
];
|
|
|
|
// Tandai baris klikabel
|
|
const processedRows = rows.map(r => ({ ...r, _rowClass: 'dd-lap-row' }));
|
|
openDD(`Laporan Belum Ditangani (${rows.length})`, processedRows, cols, { fileName: 'Laporan-Baru' });
|
|
}
|
|
|
|
function showLaporanDetail(id) {
|
|
const d = _ddLaporanMap[id];
|
|
if (!d) return;
|
|
|
|
// Hapus detail lama jika ada
|
|
document.getElementById('dd-laporan-detail-panel')?.remove();
|
|
|
|
const panel = document.createElement('div');
|
|
panel.id = 'dd-laporan-detail-panel';
|
|
panel.className = 'dd-laporan-detail';
|
|
panel.style.margin = '0 14px 14px';
|
|
panel.innerHTML = `
|
|
<div style="font-size:.63rem;font-weight:800;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:8px;display:flex;justify-content:space-between">
|
|
<span>Detail Laporan #${d.id}</span>
|
|
<button onclick="document.getElementById('dd-laporan-detail-panel').remove()" style="background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:.8rem">✕</button>
|
|
</div>
|
|
${d.nama_terlapor || d.nik_terlapor ? `
|
|
<div style="background:rgba(99,102,241,.1);padding:7px 10px;border-radius:6px;margin-bottom:8px;font-size:.78rem">
|
|
<b>Warga dilaporkan:</b> ${esc(d.nama_terlapor || '—')}
|
|
${d.nik_terlapor ? `<span style="color:var(--text-muted)"> | NIK: ${esc(d.nik_terlapor)}</span>` : ''}
|
|
</div>` : ''}
|
|
<div style="font-size:.78rem;line-height:1.8">
|
|
<b>Pelapor:</b> ${esc(d.nama_pelapor || 'Anonim')} ${d.no_hp ? `<span style="color:var(--text-muted)">(${esc(d.no_hp)})</span>` : ''}<br>
|
|
<b>Alamat:</b> ${esc(d.alamat_terlapor)}<br>
|
|
<b>Koordinat:</b> <code style="font-size:.72rem;background:var(--bg-muted);padding:1px 5px;border-radius:3px">${d.lat}, ${d.lng}</code><br>
|
|
<b>Tanggal lapor:</b> ${(d.created_at || '').slice(0,16).replace('T',' ')}<br>
|
|
<b>Deskripsi:</b>
|
|
<div style="margin:5px 0 6px;padding:8px 10px;background:var(--bg-panel);border-radius:6px;border-left:3px solid var(--accent);font-size:.78rem">${esc(d.deskripsi)}</div>
|
|
</div>
|
|
${d.foto ? `<img src="${d.foto}" style="max-width:100%;border-radius:8px;border:1px solid var(--border);margin-top:6px">` : ''}
|
|
`;
|
|
|
|
document.getElementById('dd-body').appendChild(panel);
|
|
panel.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
}
|
|
|
|
/* ================================================================
|
|
INISIALISASI APLIKASI
|
|
================================================================ */
|
|
(async function init() {
|
|
initPenyakitCheckboxes();
|
|
await checkAuth();
|
|
renderLegend();
|
|
renderFilterBar();
|
|
await loadAllData();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |