1293 lines
41 KiB
HTML
1293 lines
41 KiB
HTML
<!doctype html>
|
||
<html lang="id">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>WebGIS Poverty Mapping – Pontianak</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
|
||
<style>
|
||
:root {
|
||
--bg-dark: #0f172a;
|
||
--bg-panel: #111827;
|
||
--border: rgba(148, 163, 184, 0.22);
|
||
--text: #f8fafc;
|
||
--muted: #94a3b8;
|
||
--accent: #7c3aed;
|
||
--accent-l: #a78bfa;
|
||
--shadow: 0 18px 45px rgba(15, 23, 42, 0.28);
|
||
--sangat: #dc2626;
|
||
--miskin: #f97316;
|
||
--rentan: #eab308;
|
||
--sudah: #16a34a;
|
||
--belum: #dc2626;
|
||
--outside: #2563eb;
|
||
--gold: #f4a261;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
overflow: hidden;
|
||
color: var(--text);
|
||
background: #eef2f7;
|
||
font-family: "Inter", system-ui, sans-serif;
|
||
}
|
||
|
||
button,
|
||
input,
|
||
select {
|
||
font: inherit;
|
||
}
|
||
|
||
.app {
|
||
display: grid;
|
||
grid-template-columns: 370px minmax(0, 1fr);
|
||
height: 100vh;
|
||
}
|
||
|
||
.sidebar {
|
||
position: relative;
|
||
z-index: 900;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
padding: 20px;
|
||
background:
|
||
radial-gradient(circle at top left, rgba(124, 58, 237, 0.22), transparent 38%),
|
||
linear-gradient(180deg, var(--bg-dark), var(--bg-panel));
|
||
box-shadow: var(--shadow);
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.brand {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
}
|
||
|
||
.brand h1 {
|
||
margin: 0;
|
||
font-size: 1.3rem;
|
||
line-height: 1.15;
|
||
letter-spacing: -0.03em;
|
||
}
|
||
|
||
.brand p {
|
||
margin: 6px 0 0;
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.brand-links {
|
||
display: flex;
|
||
flex-shrink: 0;
|
||
gap: 6px;
|
||
}
|
||
|
||
.icon-btn {
|
||
display: inline-grid;
|
||
width: 40px;
|
||
height: 40px;
|
||
place-items: center;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
color: var(--text);
|
||
background: rgba(255, 255, 255, 0.08);
|
||
cursor: pointer;
|
||
text-decoration: none;
|
||
transition: 0.2s;
|
||
}
|
||
|
||
.icon-btn:hover {
|
||
border-color: rgba(167, 139, 250, 0.75);
|
||
background: rgba(124, 58, 237, 0.3);
|
||
}
|
||
|
||
.summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 8px;
|
||
}
|
||
|
||
.s-card {
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
background: rgba(15, 23, 42, 0.62);
|
||
}
|
||
|
||
.s-card span {
|
||
display: block;
|
||
color: var(--muted);
|
||
font-size: 0.68rem;
|
||
letter-spacing: 0.07em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.s-card strong {
|
||
display: block;
|
||
margin-top: 3px;
|
||
font-size: 1.5rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
.s-card.sangat strong {
|
||
color: var(--sangat);
|
||
}
|
||
|
||
.s-card.miskin strong {
|
||
color: var(--miskin);
|
||
}
|
||
|
||
.s-card.rentan strong {
|
||
color: var(--rentan);
|
||
}
|
||
|
||
.panel {
|
||
padding: 14px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 18px;
|
||
background: rgba(17, 24, 39, 0.76);
|
||
backdrop-filter: blur(14px);
|
||
}
|
||
|
||
.panel h2 {
|
||
margin: 0 0 10px;
|
||
color: #ddd6fe;
|
||
font-size: 0.82rem;
|
||
letter-spacing: 0.09em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.field {
|
||
display: grid;
|
||
gap: 6px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.field:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
label {
|
||
color: var(--muted);
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
input[type="text"],
|
||
select {
|
||
width: 100%;
|
||
min-height: 40px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 11px;
|
||
padding: 8px 10px;
|
||
color: var(--text);
|
||
background: rgba(15, 23, 42, 0.88);
|
||
outline: none;
|
||
}
|
||
|
||
input[type="text"]:focus,
|
||
select:focus {
|
||
border-color: var(--accent-l);
|
||
box-shadow: 0 0 0 3px rgba(167, 139, 250, 0.18);
|
||
}
|
||
|
||
.ibadah-list,
|
||
.family-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
max-height: 220px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.ibadah-item,
|
||
.family-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 9px 11px;
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
background: rgba(15, 23, 42, 0.5);
|
||
cursor: pointer;
|
||
transition: 0.15s;
|
||
}
|
||
|
||
.family-item {
|
||
align-items: flex-start;
|
||
cursor: default;
|
||
}
|
||
|
||
.ibadah-item:hover {
|
||
border-color: rgba(167, 139, 250, 0.5);
|
||
background: rgba(124, 58, 237, 0.18);
|
||
}
|
||
|
||
.ibadah-item.active {
|
||
border-color: var(--accent-l);
|
||
background: rgba(124, 58, 237, 0.28);
|
||
}
|
||
|
||
.ibadah-icon {
|
||
flex-shrink: 0;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.ibadah-info,
|
||
.family-info {
|
||
min-width: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
.ibadah-info strong,
|
||
.family-info strong {
|
||
display: block;
|
||
overflow: hidden;
|
||
font-size: 0.86rem;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.ibadah-info small,
|
||
.family-info small {
|
||
color: var(--muted);
|
||
font-size: 0.74rem;
|
||
}
|
||
|
||
.radius-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.radius-row input[type="range"] {
|
||
flex: 1;
|
||
accent-color: var(--accent-l);
|
||
}
|
||
|
||
.radius-val {
|
||
min-width: 62px;
|
||
color: var(--accent-l);
|
||
font-size: 0.88rem;
|
||
font-weight: 700;
|
||
text-align: right;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.primary-btn {
|
||
width: 100%;
|
||
min-height: 38px;
|
||
margin-top: 10px;
|
||
border: 0;
|
||
border-radius: 11px;
|
||
color: #fff;
|
||
background: var(--accent);
|
||
cursor: pointer;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.primary-btn:hover {
|
||
background: #6d28d9;
|
||
}
|
||
|
||
.hint {
|
||
margin: 8px 0 0;
|
||
color: var(--muted);
|
||
font-size: 0.75rem;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.legend-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 6px;
|
||
}
|
||
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.leg-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
flex-shrink: 0;
|
||
border-radius: 50%;
|
||
}
|
||
|
||
.leg-rect {
|
||
width: 18px;
|
||
height: 10px;
|
||
flex-shrink: 0;
|
||
border-radius: 3px;
|
||
opacity: 0.75;
|
||
}
|
||
|
||
.mini-stats {
|
||
position: fixed;
|
||
z-index: 850;
|
||
top: 18px;
|
||
right: 18px;
|
||
width: min(360px, calc(100vw - 36px));
|
||
padding: 14px;
|
||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||
border-radius: 18px;
|
||
color: var(--text);
|
||
background: rgba(17, 24, 39, 0.9);
|
||
box-shadow: var(--shadow);
|
||
backdrop-filter: blur(16px);
|
||
}
|
||
|
||
.mini-stats h2 {
|
||
margin: 0 0 10px;
|
||
color: #ddd6fe;
|
||
font-size: 0.78rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.mini-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 8px;
|
||
}
|
||
|
||
.mini-card {
|
||
padding: 9px 10px;
|
||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||
border-radius: 12px;
|
||
background: rgba(15, 23, 42, 0.64);
|
||
}
|
||
|
||
.mini-card span {
|
||
display: block;
|
||
color: var(--muted);
|
||
font-size: 0.68rem;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.mini-card strong {
|
||
display: block;
|
||
margin-top: 3px;
|
||
font-size: 1.1rem;
|
||
}
|
||
|
||
.kk-panel,
|
||
.list-panel {
|
||
position: fixed;
|
||
z-index: 1100;
|
||
right: 24px;
|
||
width: min(380px, calc(100vw - 48px));
|
||
border: 1px solid rgba(124, 58, 237, 0.3);
|
||
border-radius: 20px;
|
||
color: var(--text);
|
||
background: rgba(17, 24, 39, 0.96);
|
||
box-shadow: var(--shadow);
|
||
backdrop-filter: blur(16px);
|
||
}
|
||
|
||
.kk-panel {
|
||
display: none;
|
||
bottom: 24px;
|
||
padding: 18px;
|
||
}
|
||
|
||
.kk-panel.show {
|
||
display: block;
|
||
}
|
||
|
||
.list-panel {
|
||
top: 112px;
|
||
display: none;
|
||
max-height: calc(100vh - 136px);
|
||
padding: 16px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.list-panel.show {
|
||
display: block;
|
||
}
|
||
|
||
.panel-header,
|
||
.kk-panel-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
justify-content: space-between;
|
||
gap: 12px;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.panel-header h3,
|
||
.kk-panel-header h3 {
|
||
margin: 0;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.panel-header p {
|
||
margin: 4px 0 0;
|
||
color: var(--muted);
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.close-kk,
|
||
.close-list {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 0;
|
||
border-radius: 8px;
|
||
color: var(--text);
|
||
background: rgba(255, 255, 255, 0.1);
|
||
cursor: pointer;
|
||
}
|
||
|
||
.kk-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
||
font-size: 0.83rem;
|
||
}
|
||
|
||
.kk-row:last-child {
|
||
border-bottom: 0;
|
||
}
|
||
|
||
.kk-row span {
|
||
color: var(--muted);
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
border-radius: 999px;
|
||
padding: 3px 9px;
|
||
color: #fff;
|
||
font-size: 0.7rem;
|
||
font-weight: 800;
|
||
letter-spacing: 0.04em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.badge.sangat {
|
||
background: var(--sangat);
|
||
}
|
||
|
||
.badge.miskin {
|
||
background: var(--miskin);
|
||
}
|
||
|
||
.badge.rentan {
|
||
color: #0f172a;
|
||
background: var(--rentan);
|
||
}
|
||
|
||
.badge.sudah {
|
||
background: var(--sudah);
|
||
}
|
||
|
||
.badge.belum {
|
||
background: var(--belum);
|
||
}
|
||
|
||
.popup-btn {
|
||
width: 100%;
|
||
min-height: 36px;
|
||
margin-top: 10px;
|
||
border: 0;
|
||
border-radius: 10px;
|
||
color: #fff;
|
||
background: var(--accent);
|
||
cursor: pointer;
|
||
font-weight: 800;
|
||
}
|
||
|
||
.toast {
|
||
position: fixed;
|
||
z-index: 1300;
|
||
top: 16px;
|
||
left: 50%;
|
||
padding: 11px 18px;
|
||
border-radius: 999px;
|
||
color: #0f172a;
|
||
background: rgba(255, 255, 255, 0.93);
|
||
box-shadow: var(--shadow);
|
||
font-weight: 800;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transform: translate(-50%, -16px);
|
||
transition: 0.25s ease;
|
||
}
|
||
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translate(-50%, 0);
|
||
}
|
||
|
||
.loading {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 800;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(15, 23, 42, 0.5);
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
.loading-box {
|
||
padding: 18px 28px;
|
||
border-radius: 14px;
|
||
background: var(--bg-panel);
|
||
font-size: 0.9rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.worship-marker {
|
||
display: grid;
|
||
width: 38px;
|
||
height: 38px;
|
||
place-items: center;
|
||
border: 3px solid #fff;
|
||
border-radius: 50% 50% 50% 10px;
|
||
background: var(--gold);
|
||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.3);
|
||
transform: rotate(-45deg);
|
||
}
|
||
|
||
.worship-marker span {
|
||
font-size: 20px;
|
||
transform: rotate(45deg);
|
||
}
|
||
|
||
.family-dot {
|
||
width: 20px;
|
||
height: 20px;
|
||
border-radius: 50%;
|
||
border: 3px solid rgba(255, 255, 255, 0.72);
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
|
||
transition: background-color 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
|
||
}
|
||
|
||
.family-dot.inside {
|
||
background: var(--belum);
|
||
border-color: #fff;
|
||
transform: scale(1.12);
|
||
}
|
||
|
||
.family-dot.outside {
|
||
background: var(--outside);
|
||
}
|
||
|
||
.family-dot.sudah {
|
||
outline: 3px solid var(--sudah);
|
||
outline-offset: 1px;
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
border-radius: 16px;
|
||
}
|
||
|
||
.leaflet-popup-content h3 {
|
||
margin: 0 0 5px;
|
||
color: #4c1d95;
|
||
}
|
||
|
||
.leaflet-popup-content p {
|
||
margin: 0 0 7px;
|
||
}
|
||
|
||
#map {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100vh;
|
||
}
|
||
|
||
@media (max-width: 860px) {
|
||
.app {
|
||
display: block;
|
||
}
|
||
|
||
.sidebar {
|
||
position: fixed;
|
||
inset: 0 auto 0 0;
|
||
width: min(88vw, 360px);
|
||
transform: translateX(-101%);
|
||
transition: transform 0.24s;
|
||
}
|
||
|
||
.sidebar.is-open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.mobile-toggle {
|
||
position: fixed;
|
||
z-index: 1200;
|
||
top: 14px;
|
||
left: 14px;
|
||
display: inline-grid !important;
|
||
}
|
||
|
||
.mini-stats {
|
||
top: 66px;
|
||
right: 12px;
|
||
left: 12px;
|
||
width: auto;
|
||
}
|
||
|
||
.kk-panel,
|
||
.list-panel {
|
||
right: 12px;
|
||
left: 12px;
|
||
width: auto;
|
||
}
|
||
|
||
.list-panel {
|
||
top: auto;
|
||
bottom: 12px;
|
||
max-height: 50vh;
|
||
}
|
||
|
||
#map {
|
||
height: 100vh;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button class="icon-btn mobile-toggle" id="mobileToggle" style="display: none" aria-label="Menu">☰</button>
|
||
<div class="toast" id="toast"></div>
|
||
|
||
<main class="app">
|
||
<aside class="sidebar" id="sidebar">
|
||
<div class="brand">
|
||
<div>
|
||
<h1>Poverty Mapping</h1>
|
||
<p>Pemetaan keluarga berdasarkan tingkat kemiskinan per zona tempat ibadah.</p>
|
||
</div>
|
||
<div class="brand-links">
|
||
<a class="icon-btn" href="/" title="Peta SPBU">⛽</a>
|
||
<a class="icon-btn" href="/admin-poverty.html" title="Kelola Data">✏️</a>
|
||
</div>
|
||
</div>
|
||
|
||
<section class="summary">
|
||
<div class="s-card sangat"><span>Sangat Miskin</span><strong id="cntSangat">—</strong></div>
|
||
<div class="s-card miskin"><span>Miskin</span><strong id="cntMiskin">—</strong></div>
|
||
<div class="s-card rentan"><span>Rentan Miskin</span><strong id="cntRentan">—</strong></div>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Filter</h2>
|
||
<div class="field">
|
||
<label for="filterKategori">Kategori Kemiskinan</label>
|
||
<select id="filterKategori">
|
||
<option value="all">Semua Kategori</option>
|
||
<option value="Sangat Miskin">Sangat Miskin</option>
|
||
<option value="Miskin">Miskin</option>
|
||
<option value="Rentan Miskin">Rentan Miskin</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="filterBantuan">Status Bantuan</label>
|
||
<select id="filterBantuan">
|
||
<option value="all">Semua Status</option>
|
||
<option value="Belum Menerima">Belum Menerima</option>
|
||
<option value="Sudah Menerima">Sudah Menerima</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label for="filterRumah">Kondisi Rumah</label>
|
||
<select id="filterRumah">
|
||
<option value="all">Semua Kondisi</option>
|
||
<option value="Tidak Permanen">Tidak Permanen</option>
|
||
<option value="Semi Permanen">Semi Permanen</option>
|
||
<option value="Permanen">Permanen</option>
|
||
</select>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Tempat Ibadah <span id="ibadahCount" style="font-weight: 400; color: var(--muted)"></span></h2>
|
||
<div class="ibadah-list" id="ibadahList">
|
||
<div style="color: var(--muted); font-size: 0.82rem; padding: 8px 0">Memuat...</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="panel" id="radiusPanel" style="display: none">
|
||
<h2>Radius Zona: <span id="radiusLabel" style="color: var(--accent-l)">500 m</span></h2>
|
||
<div class="radius-row">
|
||
<input type="range" id="radiusSlider" min="100" max="2000" step="50" value="500" />
|
||
<span class="radius-val" id="radiusVal">500 m</span>
|
||
</div>
|
||
<p class="hint">Geser untuk mengatur radius zona. Radius tersimpan di browser untuk prototype.</p>
|
||
<button class="primary-btn" id="saveRadiusBtn" type="button">Simpan Radius</button>
|
||
<button class="primary-btn" id="openFamilyListBtn" type="button">Lihat Daftar Keluarga</button>
|
||
</section>
|
||
|
||
<section class="panel">
|
||
<h2>Legenda</h2>
|
||
<div class="legend-grid">
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--sangat)"></div> Sangat Miskin</div>
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--miskin)"></div> Miskin</div>
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--rentan)"></div> Rentan Miskin</div>
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--belum); border: 2px solid #fff"></div> Dalam Radius</div>
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--outside); border: 2px solid #fff"></div> Luar Radius</div>
|
||
<div class="legend-item"><div class="leg-dot" style="background: var(--sudah); border: 2px solid #fff"></div> Sudah Bantuan</div>
|
||
<div class="legend-item"><div class="leg-rect" style="background: var(--accent)"></div> Zona Ibadah</div>
|
||
</div>
|
||
</section>
|
||
</aside>
|
||
|
||
<section id="map">
|
||
<div class="loading" id="loadingOverlay">
|
||
<div class="loading-box">⏳ Memuat data peta kemiskinan...</div>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<section class="mini-stats">
|
||
<h2>Dashboard Ringkas</h2>
|
||
<div class="mini-grid">
|
||
<div class="mini-card"><span>Terdampak</span><strong id="totalAffected">0</strong></div>
|
||
<div class="mini-card"><span>Cakupan</span><strong id="coveragePercent">0%</strong></div>
|
||
<div class="mini-card"><span>Sangat Miskin</span><strong id="veryPoorCount">0</strong></div>
|
||
<div class="mini-card"><span>Miskin</span><strong id="poorCount">0</strong></div>
|
||
<div class="mini-card"><span>Rentan</span><strong id="vulnerableCount">0</strong></div>
|
||
<div class="mini-card"><span>Dipilih</span><strong id="selectedName">Semua</strong></div>
|
||
</div>
|
||
</section>
|
||
|
||
<aside class="list-panel" id="familyListPanel">
|
||
<div class="panel-header">
|
||
<div>
|
||
<h3>Daftar Keluarga Dalam Radius</h3>
|
||
<p id="familyPanelSubtitle">Pilih tempat ibadah untuk melihat daftar.</p>
|
||
</div>
|
||
<button class="close-list" id="closeFamilyList" type="button">×</button>
|
||
</div>
|
||
<div class="family-list" id="familyList"></div>
|
||
</aside>
|
||
|
||
<div class="kk-panel" id="kkPanel">
|
||
<div class="kk-panel-header">
|
||
<h3 id="kkNama">—</h3>
|
||
<button class="close-kk" id="closeKk" type="button">×</button>
|
||
</div>
|
||
<div id="kkRows"></div>
|
||
</div>
|
||
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||
<script>
|
||
const API_BASE = "/api/poverty";
|
||
const KAT_COLOR = {
|
||
"Sangat Miskin": "#dc2626",
|
||
"Miskin": "#f97316",
|
||
"Rentan Miskin": "#eab308"
|
||
};
|
||
const IBADAH_ICON = {
|
||
masjid: "🕌",
|
||
gereja: "⛪",
|
||
pura: "🛕",
|
||
vihara: "☸️",
|
||
kelenteng: "🏮",
|
||
lainnya: "⭐"
|
||
};
|
||
const kategoriLabel = {
|
||
sangat_miskin: "Sangat Miskin",
|
||
miskin: "Miskin",
|
||
rentan_miskin: "Rentan Miskin"
|
||
};
|
||
|
||
const fallbackTempatIbadah = [
|
||
{ id: 1, nama: "Masjid Jami Sultan Syarif Abdurrahman", jenis: "masjid", alamat: "Jl. Tanjungpura", kelurahan: "Dalam Bugis", lat: -0.0226, lng: 109.3316 },
|
||
{ id: 2, nama: "Gereja Bethel Indonesia", jenis: "gereja", alamat: "Jl. Gajahmada", kelurahan: "Sungai Bangkong", lat: -0.0334, lng: 109.3253 },
|
||
{ id: 3, nama: "Vihara Bodhisattva", jenis: "vihara", alamat: "Jl. Diponegoro", kelurahan: "Mariana", lat: -0.0289, lng: 109.3401 }
|
||
];
|
||
const fallbackKeluarga = [
|
||
{ id: 1, nama_kk: "Ahmad Fauzi", jumlah_anggota: 5, kategori: "sangat_miskin", alamat: "Dalam Bugis", kelurahan: "Dalam Bugis", keterangan: "Prioritas bantuan pangan.", lat: -0.0231, lng: 109.3321 },
|
||
{ id: 2, nama_kk: "Siti Rahmah", jumlah_anggota: 3, kategori: "miskin", alamat: "Dalam Bugis", kelurahan: "Dalam Bugis", keterangan: "Membutuhkan dukungan pendidikan.", lat: -0.0219, lng: 109.3308 },
|
||
{ id: 3, nama_kk: "Budi Santoso", jumlah_anggota: 6, kategori: "rentan_miskin", alamat: "Sungai Bangkong", kelurahan: "Sungai Bangkong", keterangan: "Pendapatan tidak tetap.", lat: -0.0341, lng: 109.3261 },
|
||
{ id: 4, nama_kk: "Maria Goretti", jumlah_anggota: 4, kategori: "miskin", alamat: "Mariana", kelurahan: "Mariana", keterangan: "Butuh verifikasi lanjutan.", lat: -0.0295, lng: 109.3412 },
|
||
{ id: 5, nama_kk: "Lim Ah Kow", jumlah_anggota: 2, kategori: "sangat_miskin", alamat: "Mariana", kelurahan: "Mariana", keterangan: "Lansia tinggal sendiri.", lat: -0.0501, lng: 109.36 }
|
||
];
|
||
|
||
const map = L.map("map").setView([-0.0263, 109.3425], 13);
|
||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||
maxZoom: 19,
|
||
attribution: "© OpenStreetMap contributors"
|
||
}).addTo(map);
|
||
|
||
const ibadahLayer = L.layerGroup().addTo(map);
|
||
const kkLayer = L.layerGroup().addTo(map);
|
||
|
||
let allIbadah = [];
|
||
let allKeluarga = [];
|
||
let activeIbadah = null;
|
||
let radiusCircle = null;
|
||
let toastTimer;
|
||
const ibadahMarkers = new Map();
|
||
const kkMarkers = new Map();
|
||
|
||
const elements = {
|
||
sidebar: document.getElementById("sidebar"),
|
||
mobileToggle: document.getElementById("mobileToggle"),
|
||
toast: document.getElementById("toast"),
|
||
loadingOverlay: document.getElementById("loadingOverlay"),
|
||
loadingBox: document.querySelector(".loading-box"),
|
||
filterKategori: document.getElementById("filterKategori"),
|
||
filterBantuan: document.getElementById("filterBantuan"),
|
||
filterRumah: document.getElementById("filterRumah"),
|
||
ibadahList: document.getElementById("ibadahList"),
|
||
ibadahCount: document.getElementById("ibadahCount"),
|
||
radiusPanel: document.getElementById("radiusPanel"),
|
||
radiusSlider: document.getElementById("radiusSlider"),
|
||
radiusVal: document.getElementById("radiusVal"),
|
||
radiusLabel: document.getElementById("radiusLabel"),
|
||
saveRadiusBtn: document.getElementById("saveRadiusBtn"),
|
||
openFamilyListBtn: document.getElementById("openFamilyListBtn"),
|
||
cntSangat: document.getElementById("cntSangat"),
|
||
cntMiskin: document.getElementById("cntMiskin"),
|
||
cntRentan: document.getElementById("cntRentan"),
|
||
totalAffected: document.getElementById("totalAffected"),
|
||
coveragePercent: document.getElementById("coveragePercent"),
|
||
veryPoorCount: document.getElementById("veryPoorCount"),
|
||
poorCount: document.getElementById("poorCount"),
|
||
vulnerableCount: document.getElementById("vulnerableCount"),
|
||
selectedName: document.getElementById("selectedName"),
|
||
familyListPanel: document.getElementById("familyListPanel"),
|
||
familyPanelSubtitle: document.getElementById("familyPanelSubtitle"),
|
||
familyList: document.getElementById("familyList"),
|
||
closeFamilyList: document.getElementById("closeFamilyList"),
|
||
kkPanel: document.getElementById("kkPanel"),
|
||
kkNama: document.getElementById("kkNama"),
|
||
kkRows: document.getElementById("kkRows"),
|
||
closeKk: document.getElementById("closeKk")
|
||
};
|
||
|
||
function geoJsonToRows(payload) {
|
||
if (payload?.type === "FeatureCollection") {
|
||
return payload.features.map((feature) => ({
|
||
...feature.properties,
|
||
lng: Number(feature.geometry.coordinates[0]),
|
||
lat: Number(feature.geometry.coordinates[1])
|
||
}));
|
||
}
|
||
if (Array.isArray(payload?.data)) return payload.data;
|
||
if (Array.isArray(payload)) return payload;
|
||
return [];
|
||
}
|
||
|
||
async function fetchRows(url, fallback) {
|
||
try {
|
||
const response = await fetch(url);
|
||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||
const rows = geoJsonToRows(await response.json());
|
||
return rows.length ? rows : fallback;
|
||
} catch {
|
||
return fallback;
|
||
}
|
||
}
|
||
|
||
function storedRadius(id) {
|
||
return Number(localStorage.getItem(`poverty-radius-${id}`)) || 500;
|
||
}
|
||
|
||
function normalizeIbadah(item) {
|
||
return {
|
||
...item,
|
||
id: Number(item.id),
|
||
jenis: String(item.jenis || "lainnya").toLowerCase(),
|
||
lat: Number(item.lat),
|
||
lng: Number(item.lng),
|
||
radius: Number(item.radius) || storedRadius(item.id)
|
||
};
|
||
}
|
||
|
||
function normalizeKeluarga(item, index) {
|
||
const kategori = item.kategori || item.kategori_kemiskinan || "miskin";
|
||
const kategoriKemiskinan = kategoriLabel[kategori] || kategori;
|
||
const bantuan = item.status_bantuan || (index % 3 === 0 ? "Sudah Menerima" : "Belum Menerima");
|
||
const kondisi = item.kondisi_rumah || ["Tidak Permanen", "Semi Permanen", "Permanen"][index % 3];
|
||
return {
|
||
...item,
|
||
id: Number(item.id),
|
||
lat: Number(item.lat),
|
||
lng: Number(item.lng),
|
||
kategori,
|
||
kategori_kemiskinan: kategoriKemiskinan,
|
||
status_bantuan: bantuan,
|
||
kondisi_rumah: kondisi,
|
||
penghasilan: Number(item.penghasilan) || (kategori === "sangat_miskin" ? 600000 : kategori === "miskin" ? 1000000 : 1500000)
|
||
};
|
||
}
|
||
|
||
function ibadahIcon(jenis) {
|
||
return IBADAH_ICON[jenis] || IBADAH_ICON.lainnya;
|
||
}
|
||
|
||
function slugKat(label) {
|
||
if (label === "Sangat Miskin") return "sangat";
|
||
if (label === "Miskin") return "miskin";
|
||
return "rentan";
|
||
}
|
||
|
||
function formatRupiah(value) {
|
||
return `Rp ${Number(value || 0).toLocaleString("id-ID")}`;
|
||
}
|
||
|
||
function formatMeters(value) {
|
||
if (value == null || Number.isNaN(value)) return "-";
|
||
return `${Math.round(value).toLocaleString("id-ID")} m`;
|
||
}
|
||
|
||
function showToast(message) {
|
||
elements.toast.textContent = message;
|
||
elements.toast.classList.add("show");
|
||
clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(() => elements.toast.classList.remove("show"), 4000);
|
||
}
|
||
|
||
function distanceBetween(a, b) {
|
||
return L.latLng(a.lat, a.lng).distanceTo(L.latLng(b.lat, b.lng));
|
||
}
|
||
|
||
function nearestWorshipDistance(family) {
|
||
if (!allIbadah.length) return null;
|
||
return Math.min(...allIbadah.map((ibadah) => distanceBetween(family, ibadah)));
|
||
}
|
||
|
||
function getFiltered(keluarga) {
|
||
const kat = elements.filterKategori.value;
|
||
const bantuan = elements.filterBantuan.value;
|
||
const rumah = elements.filterRumah.value;
|
||
return keluarga.filter((item) =>
|
||
(kat === "all" || item.kategori_kemiskinan === kat) &&
|
||
(bantuan === "all" || item.status_bantuan === bantuan) &&
|
||
(rumah === "all" || item.kondisi_rumah === rumah)
|
||
);
|
||
}
|
||
|
||
function familiesInRadius() {
|
||
if (!activeIbadah) return [];
|
||
return getFiltered(allKeluarga)
|
||
.map((item) => ({ ...item, distance: distanceBetween(item, activeIbadah) }))
|
||
.filter((item) => item.distance <= activeIbadah.radius)
|
||
.sort((a, b) => a.distance - b.distance);
|
||
}
|
||
|
||
function updateSummary(keluarga) {
|
||
elements.cntSangat.textContent = keluarga.filter((item) => item.kategori_kemiskinan === "Sangat Miskin").length;
|
||
elements.cntMiskin.textContent = keluarga.filter((item) => item.kategori_kemiskinan === "Miskin").length;
|
||
elements.cntRentan.textContent = keluarga.filter((item) => item.kategori_kemiskinan === "Rentan Miskin").length;
|
||
}
|
||
|
||
function updateMiniStats(rows) {
|
||
const counts = rows.reduce((acc, item) => {
|
||
acc[item.kategori_kemiskinan] = (acc[item.kategori_kemiskinan] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
const denominator = getFiltered(allKeluarga).length || allKeluarga.length;
|
||
elements.totalAffected.textContent = rows.length;
|
||
elements.coveragePercent.textContent = denominator ? `${Math.round((rows.length / denominator) * 100)}%` : "0%";
|
||
elements.veryPoorCount.textContent = counts["Sangat Miskin"] || 0;
|
||
elements.poorCount.textContent = counts.Miskin || 0;
|
||
elements.vulnerableCount.textContent = counts["Rentan Miskin"] || 0;
|
||
elements.selectedName.textContent = activeIbadah ? activeIbadah.nama.split(" ").slice(0, 2).join(" ") : "Semua";
|
||
}
|
||
|
||
function makeWorshipIcon(jenis) {
|
||
return L.divIcon({
|
||
className: "",
|
||
html: `<div class="worship-marker"><span>${ibadahIcon(jenis)}</span></div>`,
|
||
iconSize: [38, 38],
|
||
iconAnchor: [19, 36],
|
||
popupAnchor: [0, -34]
|
||
});
|
||
}
|
||
|
||
function makeFamilyIcon(item, isInside) {
|
||
const color = isInside ? "#dc2626" : "#2563eb";
|
||
const bantuanClass = item.status_bantuan === "Sudah Menerima" ? "sudah" : "";
|
||
return L.divIcon({
|
||
className: "",
|
||
html: `<div class="family-dot ${isInside ? "inside" : "outside"} ${bantuanClass}" style="background:${color}"></div>`,
|
||
iconSize: [24, 24],
|
||
iconAnchor: [12, 12],
|
||
popupAnchor: [0, -10]
|
||
});
|
||
}
|
||
|
||
function badgeHtml(label) {
|
||
return `<span class="badge ${slugKat(label)}">${label}</span>`;
|
||
}
|
||
|
||
function familyPopup(item) {
|
||
const nearest = nearestWorshipDistance(item);
|
||
return `
|
||
<h3>${item.nama_kk}</h3>
|
||
<p><strong>Anggota:</strong> ${item.jumlah_anggota} orang</p>
|
||
<p><strong>Kategori:</strong> ${badgeHtml(item.kategori_kemiskinan)}</p>
|
||
<p><strong>Status Bantuan:</strong> ${item.status_bantuan}</p>
|
||
<p><strong>Kondisi Rumah:</strong> ${item.kondisi_rumah}</p>
|
||
<p><strong>Catatan:</strong> ${item.keterangan || "-"}</p>
|
||
<p><strong>Jarak tempat ibadah terdekat:</strong> ${formatMeters(nearest)}</p>
|
||
`;
|
||
}
|
||
|
||
function worshipPopup(item) {
|
||
const count = allKeluarga.filter((family) => distanceBetween(family, item) <= item.radius).length;
|
||
return `
|
||
<h3>${item.nama}</h3>
|
||
<p><strong>Jenis:</strong> ${item.jenis}</p>
|
||
<p><strong>Alamat:</strong> ${item.alamat || "-"}</p>
|
||
<p><strong>Radius:</strong> ${item.radius} m</p>
|
||
<p><strong>Keluarga dalam radius:</strong> ${count}</p>
|
||
<button class="popup-btn" onclick="window.selectIbadahFromPopup(${item.id})">Pilih Zona</button>
|
||
<button class="popup-btn" onclick="window.openFamilyPanel()">Lihat Daftar Keluarga</button>
|
||
`;
|
||
}
|
||
|
||
function renderIbadahMarkers() {
|
||
ibadahLayer.clearLayers();
|
||
ibadahMarkers.clear();
|
||
allIbadah.forEach((item) => {
|
||
const marker = L.marker([item.lat, item.lng], {
|
||
icon: makeWorshipIcon(item.jenis),
|
||
title: item.nama
|
||
})
|
||
.bindPopup(() => worshipPopup(item))
|
||
.on("click", () => selectIbadah(item));
|
||
marker.addTo(ibadahLayer);
|
||
ibadahMarkers.set(item.id, marker);
|
||
});
|
||
}
|
||
|
||
function renderIbadahList() {
|
||
elements.ibadahCount.textContent = `(${allIbadah.length})`;
|
||
if (!allIbadah.length) {
|
||
elements.ibadahList.innerHTML = `<div style="color:var(--muted);font-size:.82rem;padding:8px 0">Belum ada tempat ibadah.</div>`;
|
||
return;
|
||
}
|
||
|
||
elements.ibadahList.innerHTML = [
|
||
`<div class="ibadah-item ${!activeIbadah ? "active" : ""}" data-id="all">
|
||
<span class="ibadah-icon">🗺️</span>
|
||
<div class="ibadah-info"><strong>Semua Tempat Ibadah</strong><small>${allKeluarga.length} keluarga total</small></div>
|
||
</div>`
|
||
].concat(allIbadah.map((item) => {
|
||
const count = allKeluarga.filter((family) => distanceBetween(family, item) <= item.radius).length;
|
||
return `<div class="ibadah-item ${activeIbadah?.id === item.id ? "active" : ""}" data-id="${item.id}">
|
||
<span class="ibadah-icon">${ibadahIcon(item.jenis)}</span>
|
||
<div class="ibadah-info">
|
||
<strong>${item.nama}</strong>
|
||
<small>${item.jenis} · ${count} keluarga · r=${item.radius}m</small>
|
||
</div>
|
||
</div>`;
|
||
})).join("");
|
||
|
||
elements.ibadahList.querySelectorAll(".ibadah-item").forEach((item) => {
|
||
item.addEventListener("click", () => {
|
||
const id = item.dataset.id;
|
||
if (id === "all") {
|
||
selectAllIbadah();
|
||
} else {
|
||
const ibadah = allIbadah.find((row) => row.id === Number(id));
|
||
if (ibadah) selectIbadah(ibadah);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderKkMarkers() {
|
||
kkLayer.clearLayers();
|
||
kkMarkers.clear();
|
||
const filtered = getFiltered(allKeluarga);
|
||
const insideIds = new Set(familiesInRadius().map((item) => item.id));
|
||
|
||
filtered.forEach((item) => {
|
||
const marker = L.marker([item.lat, item.lng], {
|
||
icon: makeFamilyIcon(item, activeIbadah ? insideIds.has(item.id) : false),
|
||
title: item.nama_kk
|
||
})
|
||
.bindPopup(() => familyPopup(item))
|
||
.on("click", () => showKkPanel(item));
|
||
marker.addTo(kkLayer);
|
||
kkMarkers.set(item.id, marker);
|
||
});
|
||
|
||
updateSummary(filtered);
|
||
}
|
||
|
||
function clearRadiusCircle() {
|
||
if (radiusCircle) {
|
||
map.removeLayer(radiusCircle);
|
||
radiusCircle = null;
|
||
}
|
||
}
|
||
|
||
function drawRadiusCircle() {
|
||
clearRadiusCircle();
|
||
if (!activeIbadah) return;
|
||
radiusCircle = L.circle([activeIbadah.lat, activeIbadah.lng], {
|
||
radius: activeIbadah.radius,
|
||
color: "#7c3aed",
|
||
weight: 2,
|
||
opacity: 0.7,
|
||
fillColor: "#7c3aed",
|
||
fillOpacity: 0.08,
|
||
dashArray: "6,6"
|
||
}).addTo(map);
|
||
}
|
||
|
||
function selectIbadah(item) {
|
||
activeIbadah = item;
|
||
elements.radiusPanel.style.display = "";
|
||
elements.radiusSlider.value = item.radius;
|
||
elements.radiusVal.textContent = `${item.radius} m`;
|
||
elements.radiusLabel.textContent = `${item.radius} m`;
|
||
drawRadiusCircle();
|
||
map.setView([item.lat, item.lng], 15);
|
||
renderIbadahList();
|
||
updateCoverage();
|
||
}
|
||
|
||
function selectAllIbadah() {
|
||
activeIbadah = null;
|
||
clearRadiusCircle();
|
||
elements.radiusPanel.style.display = "none";
|
||
elements.familyListPanel.classList.remove("show");
|
||
map.setView([-0.0263, 109.3425], 13);
|
||
renderIbadahList();
|
||
updateCoverage();
|
||
}
|
||
|
||
function updateCoverage() {
|
||
const rows = familiesInRadius();
|
||
if (activeIbadah) {
|
||
drawRadiusCircle();
|
||
}
|
||
renderKkMarkers();
|
||
updateMiniStats(activeIbadah ? rows : getFiltered(allKeluarga));
|
||
renderFamilyPanel();
|
||
|
||
if (activeIbadah) {
|
||
const marker = ibadahMarkers.get(activeIbadah.id);
|
||
if (marker?.isPopupOpen()) marker.setPopupContent(worshipPopup(activeIbadah));
|
||
}
|
||
}
|
||
|
||
function renderFamilyPanel() {
|
||
const rows = familiesInRadius();
|
||
elements.familyPanelSubtitle.textContent = activeIbadah
|
||
? `${activeIbadah.nama} • radius ${activeIbadah.radius} meter`
|
||
: "Pilih tempat ibadah untuk melihat daftar.";
|
||
elements.familyList.innerHTML = rows.length
|
||
? rows.map((item) => `
|
||
<article class="family-item">
|
||
<div class="ibadah-icon">👥</div>
|
||
<div class="family-info">
|
||
<strong>${item.nama_kk}</strong>
|
||
<small>${formatMeters(item.distance)} • ${item.jumlah_anggota} anggota • ${item.kelurahan || "-"}</small><br>
|
||
<span class="badge ${slugKat(item.kategori_kemiskinan)}">${item.kategori_kemiskinan}</span>
|
||
</div>
|
||
</article>
|
||
`).join("")
|
||
: `<p style="color:var(--muted);font-size:.82rem">Tidak ada keluarga dalam radius dan filter saat ini.</p>`;
|
||
}
|
||
|
||
function showKkPanel(item) {
|
||
elements.kkNama.textContent = item.nama_kk;
|
||
elements.kkPanel.classList.add("show");
|
||
elements.kkRows.innerHTML = `
|
||
<div class="kk-row"><span>Alamat</span><strong style="text-align:right;max-width:200px">${item.alamat || "-"}</strong></div>
|
||
<div class="kk-row"><span>Jumlah Anggota</span><strong>${item.jumlah_anggota} orang</strong></div>
|
||
<div class="kk-row"><span>Penghasilan/bln</span><strong>${formatRupiah(item.penghasilan)}</strong></div>
|
||
<div class="kk-row"><span>Kondisi Rumah</span><strong>${item.kondisi_rumah}</strong></div>
|
||
<div class="kk-row"><span>Kategori</span><span class="badge ${slugKat(item.kategori_kemiskinan)}">${item.kategori_kemiskinan}</span></div>
|
||
<div class="kk-row"><span>Status Bantuan</span><span class="badge ${item.status_bantuan === "Sudah Menerima" ? "sudah" : "belum"}">${item.status_bantuan}</span></div>
|
||
<div class="kk-row"><span>Jarak Terdekat</span><strong>${formatMeters(nearestWorshipDistance(item))}</strong></div>
|
||
${item.keterangan ? `<div class="kk-row"><span>Keterangan</span><strong style="text-align:right;max-width:200px">${item.keterangan}</strong></div>` : ""}
|
||
`;
|
||
}
|
||
|
||
window.openFamilyPanel = function () {
|
||
if (!activeIbadah && allIbadah.length) {
|
||
selectIbadah(allIbadah[0]);
|
||
}
|
||
renderFamilyPanel();
|
||
elements.familyListPanel.classList.add("show");
|
||
};
|
||
|
||
window.selectIbadahFromPopup = function (id) {
|
||
const ibadah = allIbadah.find((item) => item.id === Number(id));
|
||
if (ibadah) selectIbadah(ibadah);
|
||
};
|
||
|
||
elements.radiusSlider.addEventListener("input", (event) => {
|
||
if (!activeIbadah) return;
|
||
activeIbadah.radius = Number(event.target.value);
|
||
elements.radiusVal.textContent = `${activeIbadah.radius} m`;
|
||
elements.radiusLabel.textContent = `${activeIbadah.radius} m`;
|
||
updateCoverage();
|
||
});
|
||
|
||
elements.saveRadiusBtn.addEventListener("click", () => {
|
||
if (!activeIbadah) return;
|
||
localStorage.setItem(`poverty-radius-${activeIbadah.id}`, String(activeIbadah.radius));
|
||
showToast(`Radius ${activeIbadah.nama} disimpan: ${activeIbadah.radius} m`);
|
||
renderIbadahList();
|
||
});
|
||
|
||
elements.openFamilyListBtn.addEventListener("click", () => window.openFamilyPanel());
|
||
elements.closeFamilyList.addEventListener("click", () => elements.familyListPanel.classList.remove("show"));
|
||
elements.closeKk.addEventListener("click", () => elements.kkPanel.classList.remove("show"));
|
||
elements.mobileToggle.addEventListener("click", () => elements.sidebar.classList.toggle("is-open"));
|
||
|
||
["filterKategori", "filterBantuan", "filterRumah"].forEach((id) => {
|
||
document.getElementById(id).addEventListener("change", () => {
|
||
updateCoverage();
|
||
renderIbadahList();
|
||
});
|
||
});
|
||
|
||
window.addEventListener("resize", () => setTimeout(() => map.invalidateSize(), 100));
|
||
|
||
async function loadAll() {
|
||
try {
|
||
const [ibadahRows, keluargaRows] = await Promise.all([
|
||
fetchRows(`${API_BASE}/tempat-ibadah`, fallbackTempatIbadah),
|
||
fetchRows(`${API_BASE}/keluarga`, fallbackKeluarga)
|
||
]);
|
||
|
||
allIbadah = ibadahRows.map(normalizeIbadah);
|
||
allKeluarga = keluargaRows.map(normalizeKeluarga);
|
||
|
||
elements.loadingOverlay.style.display = "none";
|
||
renderIbadahMarkers();
|
||
renderIbadahList();
|
||
updateCoverage();
|
||
showToast(`Data dimuat: ${allIbadah.length} tempat ibadah, ${allKeluarga.length} keluarga`);
|
||
} catch (error) {
|
||
elements.loadingBox.textContent = `❌ Gagal memuat: ${error.message}`;
|
||
}
|
||
}
|
||
|
||
loadAll();
|
||
</script>
|
||
</body>
|
||
</html>
|