2122 lines
114 KiB
PHP
2122 lines
114 KiB
PHP
<?php
|
|
require_once 'auth.php';
|
|
requireLogin();
|
|
|
|
$role = $_SESSION['role'];
|
|
$namaUser = $_SESSION['nama'] ?? $_SESSION['username'];
|
|
$idIbadah = $_SESSION['id_ibadah'] ?? null;
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>WebGIS — Pemetaan Penduduk Miskin</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet"/>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.css"/>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
|
|
<style>
|
|
:root {
|
|
--bg: #0d1117; --surface: #161b22; --surface2: #21262d;
|
|
--border: #30363d; --accent: #e8b04b; --accent2: #f0c96d;
|
|
--green: #10b981; --red: #ef4444; --blue: #3b82f6;
|
|
--purple: #8b5cf6; --text: #e6edf3; --muted: #8b949e;
|
|
--radius: 11px; --shadow: 0 8px 32px rgba(0,0,0,.6);
|
|
}
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
body { font-family: 'Plus Jakarta Sans', sans-serif; background: var(--bg); color: var(--text); height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
|
|
|
|
/* ── HEADER ── */
|
|
header { display: flex; align-items: center; gap: 12px; padding: 10px 18px; background: var(--surface); border-bottom: 1px solid var(--border); flex-shrink: 0; z-index: 900; }
|
|
.logo { width: 36px; height: 36px; background: linear-gradient(135deg,#10b981,#059669); border-radius: 9px; display: grid; place-items: center; font-size: 17px; flex-shrink: 0; box-shadow: 0 4px 12px rgba(16,185,129,.35); }
|
|
header h1 { font-family: 'Space Grotesk',sans-serif; font-size: 16px; font-weight: 700; }
|
|
header h1 span { color: var(--green); }
|
|
.header-sub { font-size: 10.5px; color: var(--muted); }
|
|
.header-right { margin-left: auto; display: flex; gap: 7px; align-items: center; }
|
|
|
|
/* ── LEGEND BAR ── */
|
|
#legend-bar {
|
|
display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
|
|
padding: 6px 18px; background: var(--surface2); border-bottom: 1px solid var(--border);
|
|
font-size: 11px; flex-shrink: 0;
|
|
}
|
|
.leg-group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
.leg-title { font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); font-size: 10px; white-space: nowrap; }
|
|
.leg-item { display: flex; align-items: center; gap: 4px; white-space: nowrap; }
|
|
.leg-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
|
|
.leg-sep { width: 1px; height: 18px; background: var(--border); }
|
|
|
|
/* ── TOOLBAR ── */
|
|
#toolbar { display: flex; align-items: center; gap: 5px; padding: 7px 18px; background: var(--surface); border-bottom: 1px solid var(--border); flex-shrink: 0; flex-wrap: wrap; }
|
|
.tlabel { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); margin-right: 3px; white-space: nowrap; }
|
|
.tsep { width: 1px; height: 20px; background: var(--border); margin: 0 3px; }
|
|
.tbtn {
|
|
display: inline-flex; align-items: center; gap: 5px;
|
|
padding: 5px 12px; border-radius: 7px; font-size: 12px; font-weight: 600;
|
|
cursor: pointer; border: 1px solid var(--border);
|
|
background: transparent; color: var(--muted); font-family: inherit; transition: all .2s; white-space: nowrap;
|
|
}
|
|
.tbtn:hover { background: var(--surface2); color: var(--text); }
|
|
/* Active states ibadah */
|
|
.tbtn.a-masjid { background: rgba(139,92,246,.15); border-color: #8b5cf6; color: #8b5cf6; }
|
|
.tbtn.a-gereja { background: rgba(59,130,246,.15); border-color: #3b82f6; color: #3b82f6; }
|
|
.tbtn.a-gereja-katolik { background: rgba(29,78,216,.15); border-color: #1d4ed8; color: #1d4ed8; }
|
|
.tbtn.a-pura { background: rgba(245,158,11,.15); border-color: #f59e0b; color: #f59e0b; }
|
|
.tbtn.a-vihara { background: rgba(239,68,68,.15); border-color: #ef4444; color: #ef4444; }
|
|
.tbtn.a-klenteng { background: rgba(16,185,129,.15); border-color: #10b981; color: #10b981; }
|
|
.tbtn.a-miskin { background: rgba(248,113,113,.12); border-color: #f87171; color: #f87171; }
|
|
.tbtn.a-gps { background: rgba(59,130,246,.18); border-color: #3b82f6; color: #3b82f6; animation: pulse-gps .8s ease-in-out infinite alternate; }
|
|
@keyframes pulse-gps { from { box-shadow: 0 0 0 0 rgba(59,130,246,.4); } to { box-shadow: 0 0 0 6px rgba(59,130,246,0); } }
|
|
#toolbar-hint { margin-left: auto; font-size: 11.5px; color: var(--muted); display: flex; align-items: center; gap: 5px; }
|
|
#toolbar-hint.on { color: var(--green); }
|
|
|
|
/* ── WORKSPACE ── */
|
|
.workspace { flex: 1; display: flex; overflow: hidden; }
|
|
|
|
/* ── SIDEBAR ── */
|
|
#sidebar { width: 290px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; overflow: hidden; transition: width .3s; }
|
|
#sidebar.collapsed { width: 0; }
|
|
.stabs { display: flex; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
|
.stab { flex: 1; padding: 10px 6px; text-align: center; font-size: 11px; font-weight: 700; cursor: pointer; color: var(--muted); border-bottom: 2px solid transparent; transition: all .2s; white-space: nowrap; }
|
|
.stab.active { color: var(--green); border-color: var(--green); }
|
|
.tpanel { display: none; flex: 1; overflow-y: auto; padding: 8px; }
|
|
.tpanel.active { display: block; }
|
|
.tpanel::-webkit-scrollbar { width: 3px; }
|
|
.tpanel::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
.cpill { display: inline-block; background: var(--surface2); border: 1px solid var(--border); border-radius: 20px; font-size: 9.5px; font-weight: 700; padding: 1px 6px; color: var(--muted); margin-left: 3px; }
|
|
|
|
/* CARDS */
|
|
.icard { background: var(--surface2); border: 1px solid var(--border); border-radius: 9px; padding: 10px 12px; margin-bottom: 6px; cursor: pointer; transition: all .2s; }
|
|
.icard:hover { border-color: var(--green); transform: translateX(2px); }
|
|
.icard.active { border-color: var(--green); background: rgba(16,185,129,.07); }
|
|
.icard.kritis { border-color: rgba(239,68,68,.5); border-left: 4px solid #ef4444; background: rgba(239,68,68,.04); }
|
|
.icard.kritis:hover { border-color: #ef4444; }
|
|
.cn { font-weight: 600; font-size: 12.5px; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.cs { font-size: 11px; color: var(--muted); display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 6px; align-items: center; }
|
|
.cbadge { font-size: 10px; padding: 1px 7px; border-radius: 20px; font-weight: 700; }
|
|
.ca { display: flex; gap: 5px; }
|
|
.cbtn { flex: 1; padding: 4px; border-radius: 6px; font-size: 10.5px; font-weight: 600; cursor: pointer; border: none; font-family: inherit; transition: all .2s; text-align: center; }
|
|
.cbtn-e { background: rgba(232,176,75,.12); color: var(--accent); }
|
|
.cbtn-e:hover { background: var(--accent); color: #1a0f00; }
|
|
.cbtn-d { background: rgba(239,68,68,.1); color: var(--red); }
|
|
.cbtn-d:hover { background: var(--red); color: #fff; }
|
|
.empty { text-align: center; padding: 28px 14px; color: var(--muted); }
|
|
.empty i { font-size: 26px; opacity: .2; display: block; margin-bottom: 8px; }
|
|
.empty p { font-size: 11.5px; }
|
|
|
|
/* Badge jangkauan & kelas */
|
|
.badge-dalam { background: rgba(16,185,129,.18); color: #10b981; border: 1px solid rgba(16,185,129,.3); padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 700; }
|
|
.badge-luar { background: rgba(239,68,68,.12); color: #ef4444; border: 1px solid rgba(239,68,68,.25); padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 700; }
|
|
.badge-k1 { background: rgba(239,68,68,.18); color: #ef4444; border: 1px solid rgba(239,68,68,.3); padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 700; }
|
|
.badge-k2 { background: rgba(245,158,11,.18); color: #f59e0b; border: 1px solid rgba(245,158,11,.3); padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 700; }
|
|
.badge-k3 { background: rgba(59,130,246,.18); color: #3b82f6; border: 1px solid rgba(59,130,246,.3); padding: 1px 7px; border-radius: 20px; font-size: 10px; font-weight: 700; }
|
|
|
|
/* ── MAP ── */
|
|
#map { flex: 1; position: relative; }
|
|
#map-loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); z-index: 500; display: none; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.spinner { width: 28px; height: 28px; border: 3px solid var(--border); border-top-color: var(--green); border-radius: 50%; animation: spin .7s linear infinite; }
|
|
.emoji-icon { font-size: 22px; line-height: 1; filter: drop-shadow(0 2px 4px rgba(0,0,0,.5)); }
|
|
|
|
/* ── MODAL ── */
|
|
.moverlay { position: fixed; inset: 0; background: rgba(0,0,0,.72); backdrop-filter: blur(4px); z-index: 9999; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .25s; }
|
|
.moverlay.open { opacity: 1; pointer-events: all; }
|
|
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 15px; padding: 24px; width: 400px; max-width: 95vw; box-shadow: var(--shadow); transform: translateY(18px) scale(.97); transition: transform .25s; max-height: 90vh; overflow-y: auto; }
|
|
.moverlay.open .modal { transform: translateY(0) scale(1); }
|
|
.mhead { display: flex; align-items: center; gap: 11px; margin-bottom: 18px; }
|
|
.micon { width: 40px; height: 40px; border-radius: 10px; display: grid; place-items: center; font-size: 18px; flex-shrink: 0; }
|
|
.micon.ibadah { background: linear-gradient(135deg,#8b5cf6,#6d28d9); box-shadow: 0 4px 12px rgba(139,92,246,.3); }
|
|
.micon.miskin { background: linear-gradient(135deg,#ef4444,#b91c1c); box-shadow: 0 4px 12px rgba(239,68,68,.3); }
|
|
.mtitle { font-size: 15px; font-weight: 700; }
|
|
.msub { font-size: 10.5px; color: var(--muted); margin-top: 1px; font-family: monospace; }
|
|
.fg { margin-bottom: 13px; }
|
|
label { display: block; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin-bottom: 5px; }
|
|
input[type=text], input[type=tel], input[type=number], textarea, select {
|
|
width: 100%; background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 7px; padding: 8px 11px; color: var(--text);
|
|
font-size: 13px; font-family: inherit; transition: border-color .2s; outline: none;
|
|
}
|
|
input:focus, textarea:focus, select:focus { border-color: var(--green); }
|
|
input::placeholder, textarea::placeholder { color: var(--muted); opacity: .6; }
|
|
textarea { resize: vertical; min-height: 54px; }
|
|
select option { background: var(--surface2); }
|
|
|
|
/* Radius slider */
|
|
.slider-row { display: flex; align-items: center; gap: 10px; }
|
|
.slider-row input[type=range] { flex: 1; accent-color: var(--green); cursor: pointer; }
|
|
.slider-val { font-family: monospace; font-size: 13px; color: var(--accent2); font-weight: 700; min-width: 70px; text-align: right; }
|
|
|
|
/* Status box */
|
|
.status-box { display: flex; align-items: center; gap: 8px; background: var(--surface2); border: 1px solid var(--border); border-radius: 7px; padding: 8px 11px; font-size: 12.5px; }
|
|
.stat-field { background: var(--surface2); border: 1px solid var(--border); border-radius: 7px; padding: 8px 11px; font-size: 13px; color: var(--accent2); font-family: monospace; font-weight: 600; }
|
|
|
|
.toggle-row { display: flex; align-items: center; justify-content: space-between; background: var(--surface2); border: 1px solid var(--border); border-radius: 7px; padding: 8px 11px; }
|
|
.toggle-row span { font-size: 13px; font-weight: 500; }
|
|
.tw input[type=checkbox] { width: 38px; height: 21px; appearance: none; background: var(--border); border-radius: 99px; cursor: pointer; transition: background .25s; position: relative; padding: 0; }
|
|
.tw input[type=checkbox]:checked { background: var(--green); }
|
|
.tw input[type=checkbox]::after { content:''; position: absolute; top: 2.5px; left: 2.5px; width: 16px; height: 16px; background: #fff; border-radius: 50%; transition: left .25s; }
|
|
.tw input[type=checkbox]:checked::after { left: 19.5px; }
|
|
|
|
.mactions { display: flex; gap: 7px; margin-top: 18px; }
|
|
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 7px 15px; border-radius: 7px; font-size: 12.5px; font-weight: 600; cursor: pointer; border: none; font-family: inherit; transition: all .2s; }
|
|
.btn-p { background: var(--green); color: #fff; }
|
|
.btn-p:hover { background: #0ea371; }
|
|
.btn-g { background: transparent; color: var(--muted); border: 1px solid var(--border); }
|
|
.btn-g:hover { background: var(--surface2); color: var(--text); }
|
|
.btn-sb { background: transparent; color: var(--muted); border: 1px solid var(--border); padding: 6px 12px; border-radius: 7px; font-size: 12.5px; font-weight: 600; cursor: pointer; font-family: inherit; transition: all .2s; display: inline-flex; align-items: center; gap: 5px; }
|
|
.btn-sb:hover { background: var(--surface2); color: var(--text); }
|
|
.mactions .btn { flex: 1; justify-content: center; }
|
|
|
|
/* ── POPUP ── */
|
|
.leaflet-popup-content-wrapper { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: 11px !important; box-shadow: var(--shadow) !important; color: var(--text) !important; padding: 0 !important; }
|
|
.leaflet-popup-tip { background: var(--surface) !important; }
|
|
.leaflet-popup-content { margin: 0 !important; min-width: 210px; }
|
|
.pi { padding: 12px 14px; }
|
|
.pn { font-weight: 700; font-size: 13px; margin-bottom: 6px; }
|
|
.pr { font-size: 11.5px; color: var(--muted); margin-bottom: 4px; display: flex; gap: 5px; align-items: flex-start; }
|
|
.pr i { width: 13px; margin-top: 1px; flex-shrink: 0; }
|
|
.pacts { border-top: 1px solid var(--border); padding: 6px 8px; display: flex; gap: 5px; }
|
|
.pbtn { flex: 1; text-align: center; padding: 5px; border-radius: 6px; font-size: 10.5px; font-weight: 600; cursor: pointer; border: none; font-family: inherit; transition: all .2s; }
|
|
.pbtn-e { background: rgba(232,176,75,.13); color: var(--accent); }
|
|
.pbtn-e:hover { background: var(--accent); color: #1a0f00; }
|
|
.pbtn-d { background: rgba(239,68,68,.1); color: var(--red); }
|
|
.pbtn-d:hover { background: var(--red); color: #fff; }
|
|
|
|
/* ── LAYERS CONTROL dark theme ── */
|
|
.leaflet-control-layers { background: var(--surface) !important; border: 1px solid var(--border) !important; border-radius: var(--radius) !important; box-shadow: var(--shadow) !important; padding: 0 !important; min-width: 200px; }
|
|
.leaflet-control-layers::before { content: '🗂 Layer Ibadah'; display: block; padding: 8px 13px 6px; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .7px; color: var(--muted); border-bottom: 1px solid var(--border); font-family: 'Plus Jakarta Sans', sans-serif; }
|
|
.leaflet-control-layers-list { padding: 8px 12px 10px !important; }
|
|
.leaflet-control-layers-overlays label { display: flex !important; align-items: center !important; gap: 8px !important; padding: 4px 2px !important; font-size: 12px !important; color: var(--text) !important; cursor: pointer !important; font-family: 'Plus Jakarta Sans', sans-serif !important; font-weight: 500 !important; margin-bottom: 2px !important; }
|
|
.leaflet-control-layers-overlays label:hover { color: var(--green) !important; }
|
|
.leaflet-control-layers-overlays input[type="checkbox"] { width: 14px !important; height: 14px !important; accent-color: var(--green) !important; cursor: pointer !important; flex-shrink: 0 !important; }
|
|
|
|
/* ── TOAST ── */
|
|
#toast { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%) translateY(60px); background: var(--surface2); border: 1px solid var(--border); border-radius: 9px; padding: 10px 17px; font-size: 12.5px; font-weight: 500; box-shadow: var(--shadow); z-index: 99999; transition: transform .35s cubic-bezier(.34,1.56,.64,1); display: flex; align-items: center; gap: 7px; max-width: 90vw; }
|
|
#toast.show { transform: translateX(-50%) translateY(0); }
|
|
#toast.success { border-color: rgba(16,185,129,.4); color: var(--green); }
|
|
#toast.error { border-color: rgba(239,68,68,.4); color: var(--red); }
|
|
|
|
/* ── MODAL DETAIL PENDUDUK MISKIN ── */
|
|
#modal-detail .modal { width: 520px; }
|
|
.detail-tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 16px; gap: 0; }
|
|
.dtab { flex: 1; padding: 8px 6px; text-align: center; font-size: 11.5px; font-weight: 700; cursor: pointer; color: var(--muted); border-bottom: 2px solid transparent; transition: all .2s; }
|
|
.dtab.active { color: var(--green); border-color: var(--green); }
|
|
.dpanel { display: none; }
|
|
.dpanel.active { display: block; }
|
|
|
|
/* Foto upload area */
|
|
.foto-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 4px; }
|
|
.foto-box {
|
|
border: 2px dashed var(--border); border-radius: 10px; overflow: hidden;
|
|
aspect-ratio: 4/3; position: relative; cursor: pointer; background: var(--surface2);
|
|
transition: border-color .2s; display: flex; align-items: center; justify-content: center;
|
|
}
|
|
.foto-box:hover { border-color: var(--green); }
|
|
.foto-box img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
.foto-box .foto-placeholder { text-align: center; color: var(--muted); padding: 12px; }
|
|
.foto-box .foto-placeholder i { font-size: 28px; opacity: .3; display: block; margin-bottom: 6px; }
|
|
.foto-box .foto-placeholder span { font-size: 11px; }
|
|
.foto-box input[type=file] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
|
|
.foto-uploading { position: absolute; inset: 0; background: rgba(0,0,0,.6); display: flex; align-items: center; justify-content: center; font-size: 12px; color: #fff; }
|
|
.foto-label { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin-bottom: 6px; display: block; text-align: center; }
|
|
.foto-change { position: absolute; bottom: 6px; right: 6px; background: rgba(0,0,0,.7); color: #fff; border: none; border-radius: 5px; padding: 3px 7px; font-size: 10px; cursor: pointer; font-family: inherit; }
|
|
|
|
/* Riwayat bantuan */
|
|
.bantuan-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
|
.bantuan-hari-ini {
|
|
display: flex; align-items: center; gap: 8px; padding: 10px 13px;
|
|
border-radius: 9px; margin-bottom: 12px; font-size: 12.5px; font-weight: 600;
|
|
}
|
|
.bantuan-hari-ini.sudah { background: rgba(16,185,129,.12); border: 1px solid rgba(16,185,129,.3); color: var(--green); }
|
|
.bantuan-hari-ini.belum { background: rgba(245,158,11,.1); border: 1px solid rgba(245,158,11,.25); color: #f59e0b; }
|
|
.btn-konfirmasi { background: var(--green); color: #fff; border: none; border-radius: 7px; padding: 6px 13px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; display: inline-flex; align-items: center; gap: 5px; transition: all .2s; }
|
|
.btn-konfirmasi:hover { background: #0ea371; }
|
|
.btn-konfirmasi:disabled { opacity: .5; cursor: not-allowed; }
|
|
|
|
.riwayat-list { max-height: 260px; overflow-y: auto; }
|
|
.riwayat-list::-webkit-scrollbar { width: 3px; }
|
|
.riwayat-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
|
.riwayat-item {
|
|
display: flex; align-items: flex-start; gap: 10px;
|
|
padding: 9px 11px; background: var(--surface2); border: 1px solid var(--border);
|
|
border-radius: 8px; margin-bottom: 6px; position: relative;
|
|
}
|
|
.riwayat-item .hari-badge {
|
|
flex-shrink: 0; background: var(--surface); border: 1px solid var(--border);
|
|
border-radius: 7px; padding: 4px 8px; text-align: center; min-width: 52px;
|
|
}
|
|
.hari-badge .hari-num { font-size: 18px; font-weight: 700; color: var(--accent2); display: block; line-height: 1; }
|
|
.hari-badge .hari-txt { font-size: 9px; color: var(--muted); }
|
|
.riwayat-info { flex: 1; }
|
|
.riwayat-jenis { font-weight: 600; font-size: 12.5px; margin-bottom: 2px; }
|
|
.riwayat-meta { font-size: 11px; color: var(--muted); }
|
|
.riwayat-del { position: absolute; top: 7px; right: 7px; background: none; border: none; color: var(--muted); cursor: pointer; font-size: 11px; padding: 2px 4px; border-radius: 4px; transition: all .2s; }
|
|
.riwayat-del:hover { background: rgba(239,68,68,.15); color: var(--red); }
|
|
|
|
.form-bantuan { background: var(--surface2); border: 1px solid var(--border); border-radius: 9px; padding: 12px; margin-top: 10px; }
|
|
.form-bantuan-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .6px; color: var(--muted); margin-bottom: 10px; }
|
|
.fg-inline { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
|
|
/* ── Indikator kritis (>3 bulan) ── */
|
|
@keyframes pulse-red {
|
|
0%, 100% { border-color: rgba(239,68,68,.55); box-shadow: 0 0 0 0 rgba(239,68,68,.0); }
|
|
50% { border-color: rgba(239,68,68,.9); box-shadow: 0 0 0 3px rgba(239,68,68,.15); }
|
|
}
|
|
.icard.kritis {
|
|
animation: pulse-red 2.4s ease-in-out infinite;
|
|
background: rgba(239,68,68,.05) !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ── HEADER ── -->
|
|
<header>
|
|
<div class="logo">🗺</div>
|
|
<div>
|
|
<h1>WebGIS <span>Pemetaan Penduduk Miskin</span></h1>
|
|
<div class="header-sub">Berbasis Partisipasi Rumah Ibadah</div>
|
|
</div>
|
|
<div class="header-right">
|
|
<span style="font-size:12px;color:var(--muted)">
|
|
👤 <?= htmlspecialchars($namaUser) ?>
|
|
<span style="color:var(--green)">
|
|
[<?= htmlspecialchars($role) ?>]
|
|
</span>
|
|
</span>
|
|
|
|
<a href="logout.php"
|
|
class="btn-sb"
|
|
style="text-decoration:none">
|
|
<i class="fas fa-sign-out-alt"></i> Logout
|
|
</a>
|
|
|
|
<button class="btn-sb" id="btn-sidebar">
|
|
<i class="fas fa-list"></i> Daftar
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- ── LEGEND BAR ── -->
|
|
<div id="legend-bar">
|
|
<span class="leg-title">Rumah Ibadah</span>
|
|
<div class="leg-group">
|
|
<div class="leg-item"><span style="font-size:13px">🕌</span><span style="color:#8b5cf6">Masjid</span></div>
|
|
<div class="leg-item"><span style="font-size:13px">⛪</span><span style="color:#3b82f6">Gereja</span></div>
|
|
<div class="leg-item"><span style="font-size:13px">⛪</span><span style="color:#1d4ed8">Katolik</span></div>
|
|
<div class="leg-item"><span style="font-size:13px">🛕</span><span style="color:#f59e0b">Pura</span></div>
|
|
<div class="leg-item"><span style="font-size:13px">☸️</span><span style="color:#ef4444">Vihara</span></div>
|
|
<div class="leg-item"><span style="font-size:13px">🏮</span><span style="color:#10b981">Klenteng</span></div>
|
|
</div>
|
|
<div class="leg-sep"></div>
|
|
<span class="leg-title">Penduduk Miskin</span>
|
|
<div class="leg-group">
|
|
<div class="leg-item">
|
|
<div class="leg-dot" style="background:#f97316"></div>
|
|
<span>Terjangkau</span>
|
|
</div>
|
|
<div class="leg-item">
|
|
<div class="leg-dot" style="background:#8b5cf6"></div>
|
|
<span>Tidak Terjangkau</span>
|
|
</div>
|
|
</div>
|
|
<div class="leg-sep"></div>
|
|
<span class="leg-title">Kelas</span>
|
|
<div class="leg-group">
|
|
<div class="leg-item"><span class="badge-k1">Kelas I</span><span style="font-size:10px;color:var(--muted)">Sangat Miskin</span></div>
|
|
<div class="leg-item"><span class="badge-k2">Kelas II</span><span style="font-size:10px;color:var(--muted)">Miskin</span></div>
|
|
<div class="leg-item"><span class="badge-k3">Kelas III</span><span style="font-size:10px;color:var(--muted)">Hampir Miskin</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── TOOLBAR ── -->
|
|
<div id="toolbar">
|
|
<button class="tbtn" id="tbtn-none" onclick="setMode('none')"><i class="fas fa-hand-paper"></i> Normal</button>
|
|
<div class="tsep"></div>
|
|
<span class="tlabel">Rumah Ibadah</span>
|
|
<button class="tbtn" id="tbtn-masjid" onclick="setMode('masjid')"> <span>🕌</span> Masjid</button>
|
|
<button class="tbtn" id="tbtn-gereja" onclick="setMode('gereja')"> <span>⛪</span> Gereja</button>
|
|
<button class="tbtn" id="tbtn-gereja-katolik" onclick="setMode('gereja-katolik')"><span>⛪</span> Katolik</button>
|
|
<button class="tbtn" id="tbtn-pura" onclick="setMode('pura')"> <span>🛕</span> Pura</button>
|
|
<button class="tbtn" id="tbtn-vihara" onclick="setMode('vihara')"> <span>☸️</span> Vihara</button>
|
|
<button class="tbtn" id="tbtn-klenteng" onclick="setMode('klenteng')"> <span>🏮</span> Klenteng</button>
|
|
<div class="tsep"></div>
|
|
<span class="tlabel">Penduduk</span>
|
|
<button class="tbtn" id="tbtn-miskin" onclick="setMode('miskin')"><i class="fas fa-user"></i> Tambah Penduduk Miskin</button>
|
|
<button class="tbtn" id="tbtn-gps" onclick="tambahViaGPS()" title="Tambah penduduk miskin menggunakan GPS perangkat">
|
|
<i class="fas fa-location-crosshairs"></i> GPS Lapangan
|
|
</button>
|
|
<div class="tsep"></div>
|
|
<span class="tlabel">Ekspor</span>
|
|
<button class="tbtn" id="tbtn-pdf" onclick="eksporPDF()"><i class="fas fa-file-pdf" style="color:#ef4444"></i> PDF</button>
|
|
<button class="tbtn" id="tbtn-xls" onclick="eksporExcel()"><i class="fas fa-file-excel" style="color:#10b981"></i> Excel</button>
|
|
<div id="toolbar-hint"><i class="fas fa-info-circle"></i><span id="hint-text">Pilih mode lalu klik di peta untuk menambah data.</span></div>
|
|
</div>
|
|
|
|
<!-- ── WORKSPACE ── -->
|
|
<div class="workspace">
|
|
|
|
<!-- SIDEBAR -->
|
|
<div id="sidebar">
|
|
<div class="stabs">
|
|
<div class="stab active" onclick="switchTab('ibadah')">🕌 Ibadah <span class="cpill" id="cnt-ibadah">0</span></div>
|
|
<div class="stab" onclick="switchTab('miskin')">👤 Miskin <span class="cpill" id="cnt-miskin">0</span></div>
|
|
</div>
|
|
<div class="tpanel active" id="tab-ibadah">
|
|
<div class="empty"><i class="fas fa-mosque"></i><p>Belum ada data rumah ibadah.</p></div>
|
|
</div>
|
|
<div class="tpanel" id="tab-miskin">
|
|
<div class="empty"><i class="fas fa-user"></i><p>Belum ada data penduduk miskin.</p></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MAP -->
|
|
<div id="map">
|
|
<div id="map-loading"><div class="spinner"></div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── MODAL RUMAH IBADAH ── -->
|
|
<div class="moverlay" id="modal-ibadah">
|
|
<div class="modal">
|
|
<div class="mhead">
|
|
<div class="micon ibadah" id="ibadah-icon-box">🕌</div>
|
|
<div>
|
|
<div class="mtitle" id="ibadah-title">Tambah Rumah Ibadah</div>
|
|
<div class="msub" id="ibadah-coords">—</div>
|
|
</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Nama Rumah Ibadah</label>
|
|
<input type="text" id="ibadah-nama" placeholder="Masjid Al-Ikhlas, Gereja Santo Petrus, …"/>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Kategori</label>
|
|
<select id="ibadah-kategori">
|
|
<option value="Masjid">🕌 Masjid</option>
|
|
<option value="Gereja">⛪ Gereja (Protestan)</option>
|
|
<option value="Gereja Katolik">⛪ Gereja Katolik</option>
|
|
<option value="Pura">🛕 Pura</option>
|
|
<option value="Vihara">☸️ Vihara</option>
|
|
<option value="Klenteng">🏮 Klenteng</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Radius Jangkauan</label>
|
|
<div class="slider-row">
|
|
<input type="range" id="ibadah-radius" min="50" max="10000" step="50" value="500" oninput="updateRadiusLabel()"/>
|
|
<span class="slider-val" id="radius-label">500 m</span>
|
|
</div>
|
|
</div>
|
|
<div class="fg"><label>Alamat</label><input type="text" id="ibadah-alamat" placeholder="Alamat lengkap (opsional)"/></div>
|
|
<div class="fg"><label>Keterangan</label><textarea id="ibadah-ket" placeholder="Kapasitas pelayanan, program sosial, dll."></textarea></div>
|
|
<div class="mactions">
|
|
<button class="btn btn-g" onclick="closeModal('ibadah')"><i class="fas fa-times"></i> Batal</button>
|
|
<button class="btn btn-p" id="ibadah-save" onclick="saveIbadah()"><i class="fas fa-save"></i> Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── MODAL PENDUDUK MISKIN ── -->
|
|
<div class="moverlay" id="modal-miskin">
|
|
<div class="modal">
|
|
<div class="mhead">
|
|
<div class="micon miskin">👤</div>
|
|
<div style="flex:1;min-width:0">
|
|
<div class="mtitle" id="miskin-title">Tambah Penduduk Miskin</div>
|
|
<div class="msub" id="miskin-coords">—</div>
|
|
</div>
|
|
<button id="btn-gps-modal" onclick="pakaiGPSdiModal()"
|
|
style="background:rgba(59,130,246,.15);border:1px solid #3b82f6;color:#3b82f6;border-radius:7px;padding:5px 10px;font-size:11.5px;font-weight:600;cursor:pointer;font-family:inherit;display:flex;align-items:center;gap:5px;white-space:nowrap;flex-shrink:0"
|
|
title="Gunakan GPS perangkat untuk mengisi koordinat">
|
|
<i class="fas fa-location-crosshairs"></i> Pakai GPS
|
|
</button>
|
|
</div>
|
|
<!-- GPS status bar -->
|
|
<div id="gps-status-bar" style="display:none;align-items:center;gap:8px;background:rgba(59,130,246,.1);border:1px solid rgba(59,130,246,.3);border-radius:7px;padding:7px 11px;margin-bottom:12px;font-size:12px">
|
|
<i class="fas fa-circle-notch fa-spin" style="color:#3b82f6" id="gps-icon"></i>
|
|
<span id="gps-status-text" style="color:#93c5fd">Mendapatkan lokasi GPS…</span>
|
|
</div>
|
|
<div class="fg"><label>Nama Kepala Keluarga</label><input type="text" id="miskin-nama" placeholder="Nama KK"/></div>
|
|
<div class="fg"><label>Jumlah Jiwa</label><input type="number" id="miskin-jml" min="1" max="20" placeholder="Jumlah anggota keluarga"/></div>
|
|
<div class="fg">
|
|
<label>Kelas Kemiskinan</label>
|
|
<select id="miskin-kelas">
|
|
<option value="Kelas I">🔴 Kelas I — Sangat Miskin (Prioritas Tertinggi)</option>
|
|
<option value="Kelas II">🟡 Kelas II — Miskin</option>
|
|
<option value="Kelas III">🔵 Kelas III — Hampir Miskin</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Status Jangkauan (otomatis)</label>
|
|
<div class="status-box" id="miskin-status-box">
|
|
<i class="fas fa-circle-notch fa-spin" style="color:var(--muted)"></i>
|
|
<span style="color:var(--muted)">Menghitung jangkauan…</span>
|
|
</div>
|
|
</div>
|
|
<div class="fg"><label>Alamat</label><input type="text" id="miskin-alamat" placeholder="Alamat tempat tinggal (opsional)"/></div>
|
|
<div class="fg"><label>Keterangan</label><textarea id="miskin-ket" placeholder="Kondisi sosial, pekerjaan, catatan petugas…"></textarea></div>
|
|
<div class="mactions">
|
|
<button class="btn btn-g" onclick="closeModal('miskin')"><i class="fas fa-times"></i> Batal</button>
|
|
<button class="btn btn-p" id="miskin-save" onclick="saveMiskin()"><i class="fas fa-save"></i> Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── MODAL DETAIL PENDUDUK MISKIN ── -->
|
|
<div class="moverlay" id="modal-detail">
|
|
<div class="modal" style="width:520px;max-width:96vw">
|
|
<div class="mhead">
|
|
<div class="micon miskin">👤</div>
|
|
<div style="flex:1;min-width:0">
|
|
<div class="mtitle" id="detail-nama">—</div>
|
|
<div class="msub" id="detail-sub">—</div>
|
|
</div>
|
|
<button onclick="closeModal('detail')" style="background:none;border:none;color:var(--muted);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:6px" title="Tutup">✕</button>
|
|
</div>
|
|
|
|
<!-- Tab navigasi -->
|
|
<div class="detail-tabs">
|
|
<div class="dtab active" onclick="switchDetailTab('info')"><i class="fas fa-info-circle"></i> Info</div>
|
|
<div class="dtab" onclick="switchDetailTab('foto')"><i class="fas fa-camera"></i> Foto</div>
|
|
<div class="dtab" onclick="switchDetailTab('bantuan')"><i class="fas fa-hand-holding-heart"></i> Bantuan</div>
|
|
</div>
|
|
|
|
<!-- Panel: Info -->
|
|
<div class="dpanel active" id="dp-info">
|
|
<div id="detail-info-content"></div>
|
|
</div>
|
|
|
|
<!-- Panel: Foto -->
|
|
<div class="dpanel" id="dp-foto">
|
|
<div class="foto-grid">
|
|
<!-- Foto Rumah -->
|
|
<div>
|
|
<span class="foto-label"><i class="fas fa-home"></i> Foto Rumah</span>
|
|
<div class="foto-box" id="fotobox-rumah">
|
|
<div class="foto-placeholder" id="foto-rumah-placeholder">
|
|
<i class="fas fa-home"></i>
|
|
<span>Klik untuk upload</span>
|
|
</div>
|
|
<img id="foto-rumah-img" src="" alt="Foto Rumah" style="display:none"/>
|
|
<div class="foto-uploading" id="foto-rumah-loading" style="display:none">
|
|
<i class="fas fa-spinner fa-spin"></i> Mengupload…
|
|
</div>
|
|
<input type="file" id="foto-rumah-input" accept="image/jpeg,image/png,image/webp"
|
|
onchange="uploadFoto(this,'rumah')" title="Upload foto rumah"/>
|
|
<button class="foto-change" id="foto-rumah-change" style="display:none"
|
|
onclick="event.stopPropagation();document.getElementById('foto-rumah-input').click()">
|
|
<i class="fas fa-camera"></i> Ganti
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Foto KK -->
|
|
<div>
|
|
<span class="foto-label"><i class="fas fa-id-card"></i> Foto Kartu Keluarga</span>
|
|
<div class="foto-box" id="fotobox-kk">
|
|
<div class="foto-placeholder" id="foto-kk-placeholder">
|
|
<i class="fas fa-id-card"></i>
|
|
<span>Klik untuk upload</span>
|
|
</div>
|
|
<img id="foto-kk-img" src="" alt="Foto KK" style="display:none"/>
|
|
<div class="foto-uploading" id="foto-kk-loading" style="display:none">
|
|
<i class="fas fa-spinner fa-spin"></i> Mengupload…
|
|
</div>
|
|
<input type="file" id="foto-kk-input" accept="image/jpeg,image/png,image/webp"
|
|
onchange="uploadFoto(this,'kk')" title="Upload foto KK"/>
|
|
<button class="foto-change" id="foto-kk-change" style="display:none"
|
|
onclick="event.stopPropagation();document.getElementById('foto-kk-input').click()">
|
|
<i class="fas fa-camera"></i> Ganti
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p style="font-size:10.5px;color:var(--muted);text-align:center;margin-top:8px">
|
|
<i class="fas fa-info-circle"></i> Format: JPG, PNG, WebP. Maksimal 5 MB per foto.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Panel: Bantuan -->
|
|
<div class="dpanel" id="dp-bantuan">
|
|
<!-- Status hari ini -->
|
|
<div class="bantuan-hari-ini belum" id="bantuan-status-box">
|
|
<i class="fas fa-clock"></i>
|
|
<span id="bantuan-status-text">Belum mendapat bantuan hari ini</span>
|
|
<button class="btn-konfirmasi" id="btn-konfirmasi" onclick="konfirmasiBantuan()" style="margin-left:auto">
|
|
<i class="fas fa-check"></i> Konfirmasi
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Riwayat list -->
|
|
<div class="bantuan-header">
|
|
<span style="font-size:12px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.6px">
|
|
<i class="fas fa-history"></i> Riwayat Bantuan
|
|
</span>
|
|
<span id="bantuan-total" style="font-size:11px;color:var(--muted)"></span>
|
|
</div>
|
|
<div class="riwayat-list" id="riwayat-list">
|
|
<div class="empty" style="padding:20px">
|
|
<i class="fas fa-hand-holding-heart"></i>
|
|
<p>Belum ada riwayat bantuan</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form tambah bantuan manual -->
|
|
<div class="form-bantuan">
|
|
<div class="form-bantuan-title"><i class="fas fa-plus"></i> Tambah Riwayat Bantuan</div>
|
|
<div class="fg-inline">
|
|
<div class="fg" style="margin-bottom:8px">
|
|
<label>Jenis Bantuan</label>
|
|
<input type="text" id="b-jenis" placeholder="Sembako, Uang Tunai, …"/>
|
|
</div>
|
|
<div class="fg" style="margin-bottom:8px">
|
|
<label>Tanggal</label>
|
|
<input type="date" id="b-tanggal"/>
|
|
</div>
|
|
</div>
|
|
<div class="fg" style="margin-bottom:8px">
|
|
<label>Diberikan Oleh</label>
|
|
<input type="text" id="b-pemberi" placeholder="Nama pemberi / lembaga"/>
|
|
</div>
|
|
<div class="fg" style="margin-bottom:10px">
|
|
<label>Keterangan</label>
|
|
<input type="text" id="b-ket" placeholder="Catatan tambahan (opsional)"/>
|
|
</div>
|
|
<button class="btn btn-p" style="width:100%;justify-content:center" onclick="simpanBantuan()">
|
|
<i class="fas fa-save"></i> Simpan Riwayat
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── TOAST ── -->
|
|
<div id="toast"><i class="fas fa-check-circle"></i><span id="toast-msg">OK</span></div>
|
|
|
|
<!-- ── SCRIPTS ── -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.awesome-markers/2.0.2/leaflet.awesome-markers.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.8.2/jspdf.plugin.autotable.min.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
|
<script>
|
|
// ════════════════════════════════════════════════════════════════
|
|
// CONFIG
|
|
// ════════════════════════════════════════════════════════════════
|
|
const API = '/api.php';
|
|
const USER_ROLE = '<?= $role ?>';
|
|
const USER_ID_IBADAH = <?= $idIbadah ?? 'null' ?>;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// KONSTANTA IBADAH
|
|
// ════════════════════════════════════════════════════════════════
|
|
const IBADAH_COLOR = {
|
|
'Masjid': '#8b5cf6',
|
|
'Gereja': '#3b82f6',
|
|
'Gereja Katolik': '#1d4ed8',
|
|
'Pura': '#f59e0b',
|
|
'Vihara': '#ef4444',
|
|
'Klenteng': '#10b981',
|
|
};
|
|
const IBADAH_EMOJI = {
|
|
'Masjid': '🕌',
|
|
'Gereja': '⛪',
|
|
'Gereja Katolik': '⛪',
|
|
'Pura': '🛕',
|
|
'Vihara': '☸️',
|
|
'Klenteng': '🏮',
|
|
};
|
|
const MODE_TO_KATEGORI = {
|
|
'masjid': 'Masjid',
|
|
'gereja': 'Gereja',
|
|
'gereja-katolik': 'Gereja Katolik',
|
|
'pura': 'Pura',
|
|
'vihara': 'Vihara',
|
|
'klenteng': 'Klenteng',
|
|
};
|
|
const IBADAH_MODES = Object.keys(MODE_TO_KATEGORI);
|
|
const ALL_MODES = ['none', ...IBADAH_MODES, 'miskin'];
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// MAP INIT
|
|
// ════════════════════════════════════════════════════════════════
|
|
const map = L.map('map', { center: [-0.020, 109.340], zoom: 13 });
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors', maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// ICONS PENDUDUK MISKIN
|
|
// ════════════════════════════════════════════════════════════════
|
|
// ICONS PENDUDUK MISKIN — 4 varian berdasarkan jangkauan & status bantuan
|
|
// ════════════════════════════════════════════════════════════════
|
|
// Hijau = dalam jangkauan, bantuan normal
|
|
// Oranye = dalam jangkauan, tapi >3 bulan tanpa bantuan (kritis)
|
|
// Merah = luar jangkauan, bantuan normal
|
|
// Ungu = luar jangkauan, >3 bulan tanpa bantuan (kritis)
|
|
const iconMiskinDalam = L.AwesomeMarkers.icon({ icon:'user', prefix:'fa', markerColor:'green', iconColor:'#fff' });
|
|
const iconMiskinDalamKritis = L.AwesomeMarkers.icon({ icon:'user', prefix:'fa', markerColor:'orange', iconColor:'#fff' });
|
|
const iconMiskinLuar = L.AwesomeMarkers.icon({ icon:'user', prefix:'fa', markerColor:'red', iconColor:'#fff' });
|
|
const iconMiskinLuarKritis = L.AwesomeMarkers.icon({ icon:'user', prefix:'fa', markerColor:'purple', iconColor:'#fff' });
|
|
|
|
function getMiskinIcon(dalam, kritis) {
|
|
if ( dalam && !kritis) return iconMiskinDalam;
|
|
if ( dalam && kritis) return iconMiskinDalamKritis;
|
|
if (!dalam && !kritis) return iconMiskinLuar;
|
|
return iconMiskinLuarKritis;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// LAYER GROUPS (untuk Layer Control Leaflet)
|
|
// ════════════════════════════════════════════════════════════════
|
|
const layerGroups = {
|
|
'Masjid': L.layerGroup().addTo(map),
|
|
'Gereja': L.layerGroup().addTo(map),
|
|
'Gereja Katolik': L.layerGroup().addTo(map),
|
|
'Pura': L.layerGroup().addTo(map),
|
|
'Vihara': L.layerGroup().addTo(map),
|
|
'Klenteng': L.layerGroup().addTo(map),
|
|
};
|
|
const layerMiskinDalam = L.layerGroup().addTo(map);
|
|
const layerMiskinLuar = L.layerGroup().addTo(map);
|
|
|
|
// Overlay untuk Layer Control
|
|
const overlayMaps = {
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>🕌 <span style='color:#8b5cf6'>Masjid</span></span>": layerGroups['Masjid'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>⛪ <span style='color:#3b82f6'>Gereja Protestan</span></span>": layerGroups['Gereja'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>⛪ <span style='color:#1d4ed8'>Gereja Katolik</span></span>": layerGroups['Gereja Katolik'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>🛕 <span style='color:#f59e0b'>Pura</span></span>": layerGroups['Pura'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>☸️ <span style='color:#ef4444'>Vihara</span></span>": layerGroups['Vihara'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'>🏮 <span style='color:#10b981'>Klenteng</span></span>": layerGroups['Klenteng'],
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'><span style='width:10px;height:10px;border-radius:50%;background:#f97316;display:inline-block'></span> Miskin — Terjangkau</span>": layerMiskinDalam,
|
|
"<span style='display:inline-flex;align-items:center;gap:5px'><span style='width:10px;height:10px;border-radius:50%;background:#8b5cf6;display:inline-block'></span> Miskin — Tidak Terjangkau</span>": layerMiskinLuar,
|
|
};
|
|
L.control.layers(null, overlayMaps, { position:'topright', collapsed:false }).addTo(map);
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// STATE
|
|
// ════════════════════════════════════════════════════════════════
|
|
let currentMode = 'none';
|
|
let ibadahData = [];
|
|
let miskinData = [];
|
|
let bantuanCache = {}; // id_miskin → { hari_lalu, tanggal, jenis_bantuan, diberikan_oleh, sudah_hari_ini }
|
|
let ibadahLayerMap = {}; // id → { marker, circle }
|
|
let miskinLayerMap = {}; // id → marker
|
|
let pendingMark = null;
|
|
let pendingLL = null;
|
|
let editIbadahId = null;
|
|
let editMiskinId = null;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// MODE
|
|
// ════════════════════════════════════════════════════════════════
|
|
function setMode(mode) {
|
|
currentMode = mode;
|
|
ALL_MODES.forEach(m => {
|
|
const btn = document.getElementById('tbtn-' + m);
|
|
if (!btn) return;
|
|
btn.className = 'tbtn';
|
|
if (m === mode) btn.classList.add('a-' + m);
|
|
});
|
|
const th = document.getElementById('toolbar-hint');
|
|
const hintEl = document.getElementById('hint-text');
|
|
th.classList.remove('on');
|
|
map.getContainer().style.cursor = mode === 'none' ? '' : 'crosshair';
|
|
const hints = {
|
|
none: 'Pilih mode lalu klik di peta untuk menambah data.',
|
|
masjid: '🕌 Klik di peta untuk menambah Masjid beserta radius jangkauan.',
|
|
gereja: '⛪ Klik di peta untuk menambah Gereja (Protestan) beserta radius jangkauan.',
|
|
'gereja-katolik': '⛪ Klik di peta untuk menambah Gereja Katolik beserta radius jangkauan.',
|
|
pura: '🛕 Klik di peta untuk menambah Pura beserta radius jangkauan.',
|
|
vihara: '☸️ Klik di peta untuk menambah Vihara beserta radius jangkauan.',
|
|
klenteng: '🏮 Klik di peta untuk menambah Klenteng beserta radius jangkauan.',
|
|
miskin: '👤 Klik di peta untuk menambah titik Penduduk Miskin.',
|
|
};
|
|
hintEl.textContent = hints[mode] || '';
|
|
if (mode !== 'none') th.classList.add('on');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// MAP CLICK
|
|
// ════════════════════════════════════════════════════════════════
|
|
map.on('click', function(e) {
|
|
if (IBADAH_MODES.includes(currentMode)) openIbadahForm(e.latlng, currentMode);
|
|
else if (currentMode === 'miskin') openMiskinForm(e.latlng);
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// OPEN FORM — IBADAH
|
|
// ════════════════════════════════════════════════════════════════
|
|
function openIbadahForm(latlng, modeKey) {
|
|
pendingLL = latlng;
|
|
if (pendingMark) pendingMark.remove();
|
|
const kategori = MODE_TO_KATEGORI[modeKey] || 'Masjid';
|
|
const emoji = IBADAH_EMOJI[kategori] || '🏛️';
|
|
pendingMark = L.marker(latlng, {
|
|
icon: L.divIcon({ className:'', html:`<div class="emoji-icon">${emoji}</div>`, iconSize:[30,30], iconAnchor:[15,25] })
|
|
}).addTo(map);
|
|
editIbadahId = null;
|
|
document.getElementById('ibadah-title').textContent = `Tambah ${kategori}`;
|
|
document.getElementById('ibadah-icon-box').textContent = emoji;
|
|
document.getElementById('ibadah-coords').textContent = `${latlng.lat.toFixed(6)}, ${latlng.lng.toFixed(6)}`;
|
|
document.getElementById('ibadah-nama').value = '';
|
|
document.getElementById('ibadah-kategori').value = kategori;
|
|
document.getElementById('ibadah-radius').value = 500;
|
|
document.getElementById('ibadah-alamat').value = '';
|
|
document.getElementById('ibadah-ket').value = '';
|
|
updateRadiusLabel();
|
|
openModal('ibadah');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// OPEN FORM — MISKIN
|
|
// ════════════════════════════════════════════════════════════════
|
|
function openMiskinForm(latlng) {
|
|
pendingLL = latlng;
|
|
if (pendingMark) pendingMark.remove();
|
|
pendingMark = L.marker(latlng, { icon: iconMiskinLuar }).addTo(map);
|
|
editMiskinId = null;
|
|
document.getElementById('miskin-title').textContent = 'Tambah Penduduk Miskin';
|
|
document.getElementById('miskin-coords').textContent = `${latlng.lat.toFixed(6)}, ${latlng.lng.toFixed(6)}`;
|
|
document.getElementById('miskin-nama').value = '';
|
|
document.getElementById('miskin-jml').value = '';
|
|
document.getElementById('miskin-kelas').value = 'Kelas I';
|
|
document.getElementById('miskin-alamat').value = '';
|
|
document.getElementById('miskin-ket').value = '';
|
|
// Hitung jangkauan real-time
|
|
const { dalam, ibadah } = checkJangkauan(latlng.lat, latlng.lng);
|
|
renderStatusBox(dalam, ibadah);
|
|
openModal('miskin');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// KALKULASI JANGKAUAN — menggunakan Leaflet distanceTo()
|
|
// ════════════════════════════════════════════════════════════════
|
|
function checkJangkauan(lat, lng) {
|
|
const titikMiskin = L.latLng(lat, lng);
|
|
for (const ib of ibadahData) {
|
|
const titikIbadah = L.latLng(+ib.latitude, +ib.longitude);
|
|
const jarak = titikMiskin.distanceTo(titikIbadah); // meter
|
|
if (jarak <= +ib.radius_meter) {
|
|
return { dalam: true, ibadah: ib, jarak };
|
|
}
|
|
}
|
|
return { dalam: false, ibadah: null, jarak: null };
|
|
}
|
|
|
|
function renderStatusBox(dalam, ibadah) {
|
|
const box = document.getElementById('miskin-status-box');
|
|
if (dalam) {
|
|
const emoji = IBADAH_EMOJI[ibadah.kategori] || '🏛️';
|
|
const jarak = L.latLng(+ibadah.latitude, +ibadah.longitude)
|
|
.distanceTo(L.latLng(+pendingLL.lat, +pendingLL.lng));
|
|
box.innerHTML = `<i class="fas fa-check-circle" style="color:var(--green)"></i>
|
|
<span><span class="badge-dalam">✔ Dalam Jangkauan</span>
|
|
${emoji} ${esc(ibadah.nama_ibadah)}
|
|
<span style="font-family:monospace;font-size:10.5px;color:var(--muted)">(${jarak.toFixed(0)} m)</span></span>`;
|
|
} else {
|
|
box.innerHTML = `<i class="fas fa-times-circle" style="color:var(--red)"></i>
|
|
<span><span class="badge-luar">✘ Luar Jangkauan</span>
|
|
<span style="color:var(--muted)">Tidak ada rumah ibadah yang menjangkau titik ini</span></span>`;
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// SLIDER RADIUS
|
|
// ════════════════════════════════════════════════════════════════
|
|
function updateRadiusLabel() {
|
|
const val = +document.getElementById('ibadah-radius').value;
|
|
document.getElementById('radius-label').textContent = val >= 1000
|
|
? `${(val/1000).toFixed(1)} km` : `${val} m`;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// MODAL
|
|
// ════════════════════════════════════════════════════════════════
|
|
function openModal(id) {
|
|
document.getElementById('modal-'+id).classList.add('open');
|
|
setTimeout(() => {
|
|
const f = { ibadah:'ibadah-nama', miskin:'miskin-nama' }[id];
|
|
if (f) document.getElementById(f).focus();
|
|
}, 220);
|
|
}
|
|
function closeModal(id) {
|
|
document.getElementById('modal-'+id).classList.remove('open');
|
|
if (pendingMark) { pendingMark.remove(); pendingMark = null; }
|
|
editIbadahId = null; editMiskinId = null;
|
|
}
|
|
['ibadah','miskin'].forEach(id => {
|
|
document.getElementById('modal-'+id).addEventListener('click', e => {
|
|
if (e.target === e.currentTarget) closeModal(id);
|
|
});
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// SAVE — IBADAH
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function saveIbadah() {
|
|
const nama = document.getElementById('ibadah-nama').value.trim();
|
|
const kategori = document.getElementById('ibadah-kategori').value;
|
|
const radius = +document.getElementById('ibadah-radius').value;
|
|
const alamat = document.getElementById('ibadah-alamat').value.trim();
|
|
const ket = document.getElementById('ibadah-ket').value.trim();
|
|
if (!nama) { shakeEl('ibadah-nama'); showToast('Nama rumah ibadah wajib diisi','error'); return; }
|
|
setBtnLoad('ibadah-save', true);
|
|
try {
|
|
let r;
|
|
if (editIbadahId !== null) {
|
|
const ib = ibadahData.find(x => x.id == editIbadahId);
|
|
r = await apiFetch('ibadah','PUT',{ id:editIbadahId, nama_ibadah:nama, kategori, radius_meter:radius, alamat, keterangan:ket, latitude:ib.latitude, longitude:ib.longitude });
|
|
} else {
|
|
if (!pendingLL) return;
|
|
r = await apiFetch('ibadah','POST',{ nama_ibadah:nama, kategori, radius_meter:radius, alamat, keterangan:ket, latitude:pendingLL.lat, longitude:pendingLL.lng });
|
|
}
|
|
if (!r.success) throw new Error(r.message);
|
|
closeModal('ibadah');
|
|
showToast(editIbadahId ? 'Rumah ibadah diperbarui' : 'Rumah ibadah disimpan', 'success');
|
|
await loadAll();
|
|
// Recalculate warna miskin setelah radius ibadah berubah
|
|
recalcMiskinColors();
|
|
} catch(e) { showToast('Gagal: '+e.message,'error'); }
|
|
finally { setBtnLoad('ibadah-save', false); }
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// SAVE — MISKIN
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function saveMiskin() {
|
|
const nama = document.getElementById('miskin-nama').value.trim();
|
|
const jml = document.getElementById('miskin-jml').value;
|
|
const kelas = document.getElementById('miskin-kelas').value;
|
|
const alamat = document.getElementById('miskin-alamat').value.trim();
|
|
const ket = document.getElementById('miskin-ket').value.trim();
|
|
if (!nama) { shakeEl('miskin-nama'); showToast('Nama KK wajib diisi','error'); return; }
|
|
// Hitung status jangkauan saat simpan
|
|
const lat = editMiskinId ? miskinData.find(x=>x.id==editMiskinId)?.latitude : pendingLL?.lat;
|
|
const lng = editMiskinId ? miskinData.find(x=>x.id==editMiskinId)?.longitude : pendingLL?.lng;
|
|
const { dalam } = checkJangkauan(+lat, +lng);
|
|
setBtnLoad('miskin-save', true);
|
|
try {
|
|
let r;
|
|
if (editMiskinId !== null) {
|
|
r = await apiFetch('miskin','PUT',{ id:editMiskinId, nama_kk:nama, jumlah_jiwa:+jml||1, kelas_kemiskinan:kelas, alamat, keterangan:ket, dalam_jangkauan:dalam?1:0, latitude:+lat, longitude:+lng });
|
|
} else {
|
|
if (!pendingLL) return;
|
|
r = await apiFetch('miskin','POST',{ nama_kk:nama, jumlah_jiwa:+jml||1, kelas_kemiskinan:kelas, alamat, keterangan:ket, dalam_jangkauan:dalam?1:0, latitude:pendingLL.lat, longitude:pendingLL.lng });
|
|
}
|
|
if (!r.success) throw new Error(r.message);
|
|
closeModal('miskin');
|
|
showToast(editMiskinId ? 'Data diperbarui' : 'Penduduk miskin disimpan', 'success');
|
|
await loadAll();
|
|
} catch(e) { showToast('Gagal: '+e.message,'error'); }
|
|
finally { setBtnLoad('miskin-save', false); }
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// LOAD ALL
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function loadAll() {
|
|
showLoading(true);
|
|
try {
|
|
const [rI, rM] = await Promise.all([
|
|
apiFetch('ibadah','GET'),
|
|
apiFetch('miskin','GET'),
|
|
]);
|
|
ibadahData = rI.data || [];
|
|
miskinData = rM.data || [];
|
|
|
|
// Fetch bantuan terakhir untuk setiap KK secara paralel
|
|
await refreshBantuanCache();
|
|
|
|
renderAll();
|
|
} catch(e) { showToast('Gagal memuat data: '+e.message,'error'); }
|
|
finally { showLoading(false); }
|
|
}
|
|
|
|
async function refreshBantuanCache() {
|
|
if (!miskinData.length) { bantuanCache = {}; return; }
|
|
const results = await Promise.all(
|
|
miskinData.map(mk =>
|
|
apiFetch('bantuan','GET',null,null,`&id_miskin=${mk.id}`)
|
|
.then(r => ({ id: mk.id, data: r.data || [], sudahHariIni: r.sudah_hari_ini || false }))
|
|
.catch(() => ({ id: mk.id, data: [], sudahHariIni: false }))
|
|
)
|
|
);
|
|
bantuanCache = {};
|
|
results.forEach(({ id, data, sudahHariIni }) => {
|
|
if (data.length > 0) {
|
|
bantuanCache[id] = {
|
|
hari_lalu: +data[0].hari_lalu,
|
|
tanggal: data[0].tanggal,
|
|
jenis_bantuan: data[0].jenis_bantuan,
|
|
diberikan_oleh: data[0].diberikan_oleh || '',
|
|
sudah_hari_ini: sudahHariIni,
|
|
};
|
|
} else {
|
|
bantuanCache[id] = null; // belum pernah dapat bantuan
|
|
}
|
|
});
|
|
}
|
|
|
|
// Helper: kembalikan info hari bantuan terakhir untuk satu KK
|
|
function infoHariBantuan(idMiskin) {
|
|
const cache = bantuanCache[idMiskin];
|
|
if (!cache) return { label:'Belum pernah', hari: null, kritis: true };
|
|
const h = cache.hari_lalu;
|
|
const kritis = h > 90; // lebih dari 3 bulan
|
|
let label;
|
|
if (h === 0) label = 'Hari ini';
|
|
else if (h === 1) label = 'Kemarin';
|
|
else if (h < 30) label = `${h} hari lalu`;
|
|
else if (h < 365) label = `${Math.floor(h/30)} bulan lalu (${h} hari)`;
|
|
else label = `${Math.floor(h/365)} tahun lalu (${h} hari)`;
|
|
return { label, hari: h, kritis, tanggal: cache.tanggal, jenis: cache.jenis_bantuan, oleh: cache.diberikan_oleh, sudah_hari_ini: cache.sudah_hari_ini };
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// RENDER
|
|
// ════════════════════════════════════════════════════════════════
|
|
function renderAll() { renderIbadah(); renderMiskin(); renderSidebar(); }
|
|
|
|
// ─── IBADAH ──────────────────────────────────────────────────
|
|
function renderIbadah() {
|
|
// Hapus marker & circle lama
|
|
Object.values(ibadahLayerMap).forEach(({ marker, circle }) => { marker.remove(); circle.remove(); });
|
|
ibadahLayerMap = {};
|
|
// Bersihkan semua layer group
|
|
Object.values(layerGroups).forEach(lg => lg.clearLayers());
|
|
ibadahData.forEach(addIbadahLayer);
|
|
document.getElementById('cnt-ibadah').textContent = ibadahData.length;
|
|
}
|
|
|
|
function addIbadahLayer(ib) {
|
|
const emoji = IBADAH_EMOJI[ib.kategori] || '🏛️';
|
|
const color = IBADAH_COLOR[ib.kategori] || '#888';
|
|
const marker = L.marker([+ib.latitude, +ib.longitude], {
|
|
icon: L.divIcon({ className:'', html:`<div class="emoji-icon">${emoji}</div>`, iconSize:[30,30], iconAnchor:[15,25] })
|
|
}).bindPopup(ibadahPopup(ib));
|
|
const circle = L.circle([+ib.latitude, +ib.longitude], {
|
|
radius: +ib.radius_meter, color, weight:2, fillColor:color, fillOpacity:.08, dashArray:'6,4'
|
|
});
|
|
// Masukkan ke LayerGroup yang sesuai
|
|
const lg = layerGroups[ib.kategori];
|
|
if (lg) { lg.addLayer(marker); lg.addLayer(circle); }
|
|
ibadahLayerMap[ib.id] = { marker, circle };
|
|
}
|
|
|
|
function ibadahPopup(ib) {
|
|
const emoji = IBADAH_EMOJI[ib.kategori] || '🏛️';
|
|
const color = IBADAH_COLOR[ib.kategori] || '#888';
|
|
const r = +ib.radius_meter;
|
|
const rLabel = r >= 1000 ? `${(r/1000).toFixed(1)} km` : `${r} m`;
|
|
return `<div class="pi">
|
|
<div class="pn">${emoji} ${esc(ib.nama_ibadah)}</div>
|
|
<div class="pr"><i class="fas fa-tag" style="color:${color}"></i><strong style="color:${color}">${esc(ib.kategori)}</strong></div>
|
|
<div class="pr"><i class="fas fa-circle-dot" style="color:${color}"></i>Radius: <strong style="color:${color}">${rLabel}</strong></div>
|
|
${ib.alamat ? `<div class="pr"><i class="fas fa-map-pin"></i>${esc(ib.alamat)}</div>` : ''}
|
|
${ib.keterangan ? `<div class="pr"><i class="fas fa-info-circle"></i>${esc(ib.keterangan)}</div>` : ''}
|
|
</div>
|
|
${USER_ROLE === 'admin' ? `<div class="pacts">
|
|
<button class="pbtn pbtn-e" onclick="editIbadah(${ib.id})"><i class="fas fa-pen"></i> Edit</button>
|
|
<button class="pbtn pbtn-d" onclick="delIbadah(${ib.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>` : ''}`;
|
|
}
|
|
|
|
// ─── MISKIN ──────────────────────────────────────────────────
|
|
function renderMiskin() {
|
|
Object.values(miskinLayerMap).forEach(m => m.remove());
|
|
miskinLayerMap = {};
|
|
layerMiskinDalam.clearLayers();
|
|
layerMiskinLuar.clearLayers();
|
|
miskinData.forEach(addMiskinLayer);
|
|
document.getElementById('cnt-miskin').textContent = miskinData.length;
|
|
}
|
|
|
|
function addMiskinLayer(mk) {
|
|
const { dalam } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
const kritis = infoHariBantuan(mk.id).kritis;
|
|
const icon = getMiskinIcon(dalam, kritis);
|
|
const m = L.marker([+mk.latitude, +mk.longitude], { icon, draggable: USER_ROLE === 'admin' })
|
|
.bindPopup(miskinPopup(mk, dalam));
|
|
m.on('dragstart', () => m.closePopup());
|
|
m.on('dragend', async function(e) {
|
|
const { lat, lng } = e.target.getLatLng();
|
|
const idx = miskinData.findIndex(x => x.id == mk.id);
|
|
const { dalam: d2 } = checkJangkauan(lat, lng);
|
|
const kritis2 = infoHariBantuan(mk.id).kritis;
|
|
if (idx !== -1) { miskinData[idx].latitude=lat; miskinData[idx].longitude=lng; mk.latitude=lat; mk.longitude=lng; }
|
|
m.setIcon(getMiskinIcon(d2, kritis2));
|
|
m.setPopupContent(miskinPopup(mk, d2));
|
|
try {
|
|
const r = await apiFetch('miskin','PUT',{ id:mk.id, nama_kk:mk.nama_kk, jumlah_jiwa:mk.jumlah_jiwa, kelas_kemiskinan:mk.kelas_kemiskinan, alamat:mk.alamat||'', keterangan:mk.keterangan||'', dalam_jangkauan:d2?1:0, latitude:lat, longitude:lng });
|
|
if (!r.success) throw new Error(r.message);
|
|
showToast('Posisi & status jangkauan diperbarui','success');
|
|
renderSidebar();
|
|
} catch(err) { showToast('Gagal: '+err.message,'error'); }
|
|
});
|
|
// Masukkan ke layer group yang sesuai (berdasarkan jangkauan)
|
|
if (dalam) layerMiskinDalam.addLayer(m);
|
|
else layerMiskinLuar.addLayer(m);
|
|
miskinLayerMap[mk.id] = m;
|
|
}
|
|
|
|
function miskinPopup(mk, dalam) {
|
|
const kelasMap = { 'Kelas I':'badge-k1', 'Kelas II':'badge-k2', 'Kelas III':'badge-k3' };
|
|
const kelasBadge = mk.kelas_kemiskinan ? `<span class="${kelasMap[mk.kelas_kemiskinan]||'cbadge'}">${mk.kelas_kemiskinan}</span>` : '';
|
|
const statusBadge = dalam ? `<span class="badge-dalam">✔ Terjangkau</span>` : `<span class="badge-luar">✘ Tidak Terjangkau</span>`;
|
|
|
|
// Info bantuan terakhir
|
|
const info = infoHariBantuan(mk.id);
|
|
const bantuanStyle = info.kritis
|
|
? 'color:#ef4444;font-weight:700'
|
|
: info.hari === 0 ? 'color:#10b981;font-weight:600' : 'color:var(--muted)';
|
|
const bantuanIcon = info.kritis ? '🔴' : info.hari === 0 ? '✅' : '🕐';
|
|
const bantuanBadge = info.kritis
|
|
? `<span style="background:rgba(239,68,68,.15);border:1px solid rgba(239,68,68,.4);color:#ef4444;padding:1px 7px;border-radius:20px;font-size:10px;font-weight:700">⚠ ${info.label}</span>`
|
|
: `<span style="${bantuanStyle};font-size:11.5px">${bantuanIcon} ${info.label}</span>`;
|
|
|
|
return `<div class="pi">
|
|
<div class="pn" ${info.kritis ? 'style="color:#ef4444"' : ''}>👤 ${esc(mk.nama_kk)}</div>
|
|
<div class="pr" style="gap:5px;flex-wrap:wrap">${kelasBadge} ${statusBadge}</div>
|
|
<div class="pr"><i class="fas fa-hand-holding-heart" style="color:${info.kritis?'#ef4444':'var(--muted)'}"></i>${bantuanBadge}</div>
|
|
${mk.jumlah_jiwa ? `<div class="pr"><i class="fas fa-users"></i>${mk.jumlah_jiwa} jiwa</div>` : ''}
|
|
${mk.alamat ? `<div class="pr"><i class="fas fa-map-pin"></i>${esc(mk.alamat)}</div>` : ''}
|
|
</div>
|
|
<div class="pacts">
|
|
<button class="pbtn" style="background:rgba(16,185,129,.13);color:var(--green)" onclick="openDetail(${mk.id})"><i class="fas fa-folder-open"></i> Detail</button>
|
|
${USER_ROLE === 'admin' ? `
|
|
<button class="pbtn pbtn-e" onclick="editMiskin(${mk.id})"><i class="fas fa-pen"></i> Edit</button>
|
|
<button class="pbtn pbtn-d" onclick="delMiskin(${mk.id})"><i class="fas fa-trash"></i> Hapus</button>` : ''}
|
|
</div>`;
|
|
}
|
|
|
|
// Recalculate warna semua miskin (dipanggil setelah edit/hapus ibadah)
|
|
function recalcMiskinColors() {
|
|
miskinData.forEach(mk => {
|
|
const m = miskinLayerMap[mk.id]; if (!m) return;
|
|
const { dalam } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
const kritis = infoHariBantuan(mk.id).kritis;
|
|
m.setIcon(getMiskinIcon(dalam, kritis));
|
|
m.setPopupContent(miskinPopup(mk, dalam));
|
|
layerMiskinDalam.removeLayer(m);
|
|
layerMiskinLuar.removeLayer(m);
|
|
if (dalam) layerMiskinDalam.addLayer(m);
|
|
else layerMiskinLuar.addLayer(m);
|
|
});
|
|
renderSidebar();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// SIDEBAR
|
|
// ════════════════════════════════════════════════════════════════
|
|
function renderSidebar() {
|
|
// ─ IBADAH ─
|
|
document.getElementById('tab-ibadah').innerHTML = ibadahData.length
|
|
? ibadahData.map(ib => {
|
|
const emoji = IBADAH_EMOJI[ib.kategori] || '🏛️';
|
|
const color = IBADAH_COLOR[ib.kategori] || '#888';
|
|
const r = +ib.radius_meter;
|
|
const rLabel = r >= 1000 ? `${(r/1000).toFixed(1)} km` : `${r} m`;
|
|
return `<div class="icard" onclick="focusIbadah(${ib.id})">
|
|
<div class="cn">${emoji} ${esc(ib.nama_ibadah)}</div>
|
|
<div class="cs">
|
|
<span class="cbadge" style="background:${color}22;color:${color}">${esc(ib.kategori)}</span>
|
|
<span><i class="fas fa-circle-dot" style="color:${color}"></i> ${rLabel}</span>
|
|
</div>
|
|
${USER_ROLE === 'admin' ? `<div class="ca">
|
|
<button class="cbtn cbtn-e" onclick="event.stopPropagation();editIbadah(${ib.id})"><i class="fas fa-pen"></i> Edit</button>
|
|
<button class="cbtn cbtn-d" onclick="event.stopPropagation();delIbadah(${ib.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>` : ''}
|
|
</div>`;
|
|
}).join('')
|
|
: `<div class="empty"><i class="fas fa-mosque"></i><p>Belum ada data rumah ibadah.<br>Pilih kategori di toolbar lalu klik peta.</p></div>`;
|
|
|
|
// ─ MISKIN ─
|
|
const dalamCount = miskinData.filter(mk => checkJangkauan(+mk.latitude, +mk.longitude).dalam).length;
|
|
const luarCount = miskinData.length - dalamCount;
|
|
const kelasCount = { 'Kelas I': 0, 'Kelas II': 0, 'Kelas III': 0 };
|
|
const kritisCount = miskinData.filter(mk => infoHariBantuan(mk.id).kritis).length;
|
|
miskinData.forEach(mk => { if (kelasCount[mk.kelas_kemiskinan] !== undefined) kelasCount[mk.kelas_kemiskinan]++; });
|
|
|
|
document.getElementById('tab-miskin').innerHTML = miskinData.length ? `
|
|
<!-- Statistik ringkasan -->
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:6px">
|
|
<div style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.25);border-radius:8px;padding:7px 8px;text-align:center">
|
|
<div style="font-size:20px;font-weight:700;color:#10b981">${dalamCount}</div>
|
|
<div style="font-size:9.5px;color:var(--muted)">Terjangkau</div>
|
|
</div>
|
|
<div style="background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:8px;padding:7px 8px;text-align:center">
|
|
<div style="font-size:20px;font-weight:700;color:#ef4444">${luarCount}</div>
|
|
<div style="font-size:9.5px;color:var(--muted)">Tidak Terjangkau</div>
|
|
</div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin-bottom:6px">
|
|
<div style="background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:8px;padding:5px;text-align:center">
|
|
<div style="font-size:16px;font-weight:700;color:#ef4444">${kelasCount['Kelas I']}</div>
|
|
<div style="font-size:9px;color:var(--muted)">Kelas I</div>
|
|
</div>
|
|
<div style="background:rgba(245,158,11,.08);border:1px solid rgba(245,158,11,.2);border-radius:8px;padding:5px;text-align:center">
|
|
<div style="font-size:16px;font-weight:700;color:#f59e0b">${kelasCount['Kelas II']}</div>
|
|
<div style="font-size:9px;color:var(--muted)">Kelas II</div>
|
|
</div>
|
|
<div style="background:rgba(59,130,246,.08);border:1px solid rgba(59,130,246,.2);border-radius:8px;padding:5px;text-align:center">
|
|
<div style="font-size:16px;font-weight:700;color:#3b82f6">${kelasCount['Kelas III']}</div>
|
|
<div style="font-size:9px;color:var(--muted)">Kelas III</div>
|
|
</div>
|
|
</div>
|
|
${kritisCount > 0 ? `
|
|
<div style="background:rgba(239,68,68,.1);border:1px solid rgba(239,68,68,.35);border-radius:8px;padding:7px 10px;margin-bottom:8px;display:flex;align-items:center;gap:7px;font-size:11.5px;color:#ef4444;font-weight:600">
|
|
<i class="fas fa-triangle-exclamation"></i>
|
|
<span>${kritisCount} penduduk belum diberi bantuan >3 bulan</span>
|
|
</div>` : ''}
|
|
` + miskinData
|
|
.sort((a,b) => {
|
|
// Urutkan: kritis dulu, lalu kelas, lalu hari lalu terlama
|
|
const aInfo = infoHariBantuan(a.id);
|
|
const bInfo = infoHariBantuan(b.id);
|
|
if (aInfo.kritis !== bInfo.kritis) return aInfo.kritis ? -1 : 1;
|
|
const o = {'Kelas I':0,'Kelas II':1,'Kelas III':2};
|
|
if ((o[a.kelas_kemiskinan]||0) !== (o[b.kelas_kemiskinan]||0))
|
|
return (o[a.kelas_kemiskinan]||0) - (o[b.kelas_kemiskinan]||0);
|
|
return (bInfo.hari||0) - (aInfo.hari||0);
|
|
})
|
|
.map(mk => {
|
|
const { dalam } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
const kelasMap = { 'Kelas I':'badge-k1', 'Kelas II':'badge-k2', 'Kelas III':'badge-k3' };
|
|
const info = infoHariBantuan(mk.id);
|
|
const bantuanHtml = info.kritis
|
|
? `<span style="color:#ef4444;font-weight:700;font-size:10.5px">🔴 ${info.label}</span>`
|
|
: info.hari === 0
|
|
? `<span style="color:#10b981;font-size:10.5px">✅ Hari ini</span>`
|
|
: `<span style="color:var(--muted);font-size:10.5px">🕐 ${info.label}</span>`;
|
|
|
|
const caHtml = USER_ROLE === 'admin'
|
|
? `<div class="ca">
|
|
<button class="cbtn" style="background:rgba(16,185,129,.12);color:var(--green)" onclick="event.stopPropagation();openDetail(${mk.id})"><i class="fas fa-folder-open"></i> Detail</button>
|
|
<button class="cbtn cbtn-e" onclick="event.stopPropagation();editMiskin(${mk.id})"><i class="fas fa-pen"></i> Edit</button>
|
|
<button class="cbtn cbtn-d" onclick="event.stopPropagation();delMiskin(${mk.id})"><i class="fas fa-trash"></i> Hapus</button>
|
|
</div>`
|
|
: `<div class="ca">
|
|
<button class="cbtn" style="background:rgba(16,185,129,.12);color:var(--green)" onclick="event.stopPropagation();openDetail(${mk.id})"><i class="fas fa-folder-open"></i> Detail</button>
|
|
</div>`;
|
|
|
|
return `<div class="icard${info.kritis ? ' kritis' : ''}" onclick="focusMiskin(${mk.id})">
|
|
<div class="cn" ${info.kritis ? 'style="color:#ef4444"' : ''}>
|
|
${info.kritis ? '⚠ ' : ''}👤 ${esc(mk.nama_kk)}
|
|
</div>
|
|
<div class="cs">
|
|
<span class="${kelasMap[mk.kelas_kemiskinan]||'cbadge'}">${mk.kelas_kemiskinan||'—'}</span>
|
|
${dalam ? `<span class="badge-dalam">✔ Terjangkau</span>` : `<span class="badge-luar">✘ Tidak</span>`}
|
|
</div>
|
|
<div style="font-size:10.5px;margin-bottom:6px;display:flex;align-items:center;gap:4px">
|
|
<i class="fas fa-hand-holding-heart" style="color:${info.kritis?'#ef4444':'var(--muted)'}"></i>
|
|
${bantuanHtml}
|
|
</div>
|
|
${caHtml}
|
|
</div>`;
|
|
}).join('')
|
|
: `<div class="empty"><i class="fas fa-user"></i><p>Belum ada data penduduk miskin.<br>Pilih mode Penduduk Miskin lalu klik peta.</p></div>`;
|
|
}
|
|
|
|
function switchTab(t) {
|
|
document.querySelectorAll('.stab').forEach((el,i) => el.classList.toggle('active', ['ibadah','miskin'][i]===t));
|
|
document.querySelectorAll('.tpanel').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('tab-'+t).classList.add('active');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// FOCUS
|
|
// ════════════════════════════════════════════════════════════════
|
|
function focusIbadah(id) {
|
|
const ib = ibadahData.find(x => x.id==id); if (!ib) return;
|
|
map.flyTo([+ib.latitude, +ib.longitude], 16, { duration:.8 });
|
|
ibadahLayerMap[id]?.marker.openPopup();
|
|
}
|
|
function focusMiskin(id) {
|
|
const mk = miskinData.find(x => x.id==id); if (!mk) return;
|
|
map.flyTo([+mk.latitude, +mk.longitude], 17, { duration:.8 });
|
|
miskinLayerMap[id]?.openPopup();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// EDIT
|
|
// ════════════════════════════════════════════════════════════════
|
|
function editIbadah(id) {
|
|
const ib = ibadahData.find(x => x.id==id); if (!ib) return;
|
|
editIbadahId = id; map.closePopup();
|
|
const emoji = IBADAH_EMOJI[ib.kategori] || '🏛️';
|
|
document.getElementById('ibadah-title').textContent = `Edit ${ib.kategori}`;
|
|
document.getElementById('ibadah-icon-box').textContent = emoji;
|
|
document.getElementById('ibadah-coords').textContent = `${(+ib.latitude).toFixed(6)}, ${(+ib.longitude).toFixed(6)}`;
|
|
document.getElementById('ibadah-nama').value = ib.nama_ibadah;
|
|
document.getElementById('ibadah-kategori').value = ib.kategori;
|
|
document.getElementById('ibadah-radius').value = ib.radius_meter;
|
|
document.getElementById('ibadah-alamat').value = ib.alamat || '';
|
|
document.getElementById('ibadah-ket').value = ib.keterangan || '';
|
|
updateRadiusLabel();
|
|
openModal('ibadah');
|
|
}
|
|
function editMiskin(id) {
|
|
const mk = miskinData.find(x => x.id==id); if (!mk) return;
|
|
editMiskinId = id; map.closePopup();
|
|
pendingLL = { lat: +mk.latitude, lng: +mk.longitude };
|
|
const { dalam, ibadah } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
document.getElementById('miskin-title').textContent = 'Edit Penduduk Miskin';
|
|
document.getElementById('miskin-coords').textContent = `${(+mk.latitude).toFixed(6)}, ${(+mk.longitude).toFixed(6)}`;
|
|
document.getElementById('miskin-nama').value = mk.nama_kk;
|
|
document.getElementById('miskin-jml').value = mk.jumlah_jiwa || '';
|
|
document.getElementById('miskin-kelas').value = mk.kelas_kemiskinan || 'Kelas I';
|
|
document.getElementById('miskin-alamat').value = mk.alamat || '';
|
|
document.getElementById('miskin-ket').value = mk.keterangan || '';
|
|
renderStatusBox(dalam, ibadah);
|
|
openModal('miskin');
|
|
}
|
|
window.editIbadah = editIbadah;
|
|
window.editMiskin = editMiskin;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// DELETE
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function delIbadah(id) {
|
|
if (!confirm('Hapus rumah ibadah ini?')) return;
|
|
const r = await apiFetch('ibadah','DELETE',null,id);
|
|
if (r.success) {
|
|
map.closePopup(); showToast('Rumah ibadah dihapus','success');
|
|
await loadAll(); recalcMiskinColors();
|
|
} else showToast('Gagal: '+r.message,'error');
|
|
}
|
|
async function delMiskin(id) {
|
|
if (!confirm('Hapus data penduduk miskin ini?')) return;
|
|
const r = await apiFetch('miskin','DELETE',null,id);
|
|
if (r.success) { map.closePopup(); showToast('Data penduduk miskin dihapus','success'); loadAll(); }
|
|
else showToast('Gagal: '+r.message,'error');
|
|
}
|
|
window.delIbadah = delIbadah;
|
|
window.delMiskin = delMiskin;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// SIDEBAR TOGGLE
|
|
// ════════════════════════════════════════════════════════════════
|
|
document.getElementById('btn-sidebar').addEventListener('click', () => {
|
|
document.getElementById('sidebar').classList.toggle('collapsed');
|
|
setTimeout(() => map.invalidateSize(), 310);
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// API HELPER
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function apiFetch(resource, method, body=null, deleteId=null, extraQuery='') {
|
|
let url = `${API}?resource=${resource}`;
|
|
if (deleteId) url += `&id=${deleteId}`;
|
|
if (extraQuery) url += extraQuery;
|
|
const opts = { method, headers:{'Content-Type':'application/json'} };
|
|
if (body && method !== 'GET' && method !== 'DELETE') opts.body = JSON.stringify(body);
|
|
const res = await fetch(url, opts);
|
|
return res.json();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// HELPERS
|
|
// ════════════════════════════════════════════════════════════════
|
|
function esc(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
|
|
|
let toastT;
|
|
function showToast(msg, type='success') {
|
|
const t = document.getElementById('toast');
|
|
t.querySelector('i').className = `fas ${type==='success'?'fa-check-circle':'fa-exclamation-circle'}`;
|
|
document.getElementById('toast-msg').textContent = msg;
|
|
t.className = `show ${type}`;
|
|
clearTimeout(toastT);
|
|
toastT = setTimeout(() => t.className='', 3200);
|
|
}
|
|
function showLoading(v) { document.getElementById('map-loading').style.display = v ? 'block':'none'; }
|
|
function setBtnLoad(id, loading) {
|
|
const b = document.getElementById(id); if (!b) return;
|
|
b.disabled = loading;
|
|
b.innerHTML = loading ? '<i class="fas fa-spinner fa-spin"></i> Menyimpan...' : '<i class="fas fa-save"></i> Simpan';
|
|
}
|
|
function shakeEl(id) {
|
|
const el = document.getElementById(id);
|
|
el.style.borderColor = '#ef4444';
|
|
setTimeout(() => el.style.borderColor='', 1500);
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// MODAL DETAIL — buka dari popup atau sidebar
|
|
// ════════════════════════════════════════════════════════════════
|
|
const UPLOAD_URL = '/upload.php';
|
|
const FOTO_BASE = '/uploads/';
|
|
|
|
let detailMiskinId = null; // ID KK yang sedang dibuka di modal detail
|
|
|
|
async function openDetail(id) {
|
|
const mk = miskinData.find(x => x.id == id);
|
|
if (!mk) return;
|
|
detailMiskinId = id;
|
|
map.closePopup();
|
|
|
|
// ── Header info ──
|
|
const kelasMap = { 'Kelas I':'badge-k1', 'Kelas II':'badge-k2', 'Kelas III':'badge-k3' };
|
|
const { dalam, ibadah } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
document.getElementById('detail-nama').textContent = '👤 ' + (mk.nama_kk || '—');
|
|
document.getElementById('detail-sub').textContent =
|
|
`${mk.kelas_kemiskinan || '—'} · ${mk.jumlah_jiwa || 1} jiwa · ${(+mk.latitude).toFixed(5)}, ${(+mk.longitude).toFixed(5)}`;
|
|
|
|
// ── Tab Info ──
|
|
const info = infoHariBantuan(mk.id);
|
|
const statusBadge = dalam
|
|
? `<span class="badge-dalam">✔ Terjangkau</span>`
|
|
: `<span class="badge-luar">✘ Tidak Terjangkau</span>`;
|
|
|
|
// Info rumah ibadah yang menjangkau
|
|
let ibadahInfoHtml = '';
|
|
if (dalam && ibadah) {
|
|
const ibEmoji = IBADAH_EMOJI[ibadah.kategori] || '🏛️';
|
|
const ibColor = IBADAH_COLOR[ibadah.kategori] || '#888';
|
|
const jarakM = L.latLng(+mk.latitude, +mk.longitude)
|
|
.distanceTo(L.latLng(+ibadah.latitude, +ibadah.longitude));
|
|
const jarakLabel = jarakM >= 1000 ? `${(jarakM/1000).toFixed(2)} km` : `${jarakM.toFixed(0)} m`;
|
|
ibadahInfoHtml = `
|
|
<span style="color:var(--muted);font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Dibantu Oleh</span>
|
|
<div style="background:${ibColor}11;border:1px solid ${ibColor}44;border-radius:8px;padding:8px 10px;display:flex;align-items:center;gap:8px">
|
|
<span style="font-size:20px">${ibEmoji}</span>
|
|
<div>
|
|
<div style="font-weight:700;font-size:12.5px;color:${ibColor}">${esc(ibadah.nama_ibadah)}</div>
|
|
<div style="font-size:11px;color:var(--muted)">${esc(ibadah.kategori)} · Jarak: ${jarakLabel} · Radius: ${ibadah.radius_meter} m</div>
|
|
</div>
|
|
</div>`;
|
|
} else {
|
|
ibadahInfoHtml = `
|
|
<span style="color:var(--muted);font-size:11.5px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Dibantu Oleh</span>
|
|
<div style="background:rgba(239,68,68,.07);border:1px solid rgba(239,68,68,.2);border-radius:8px;padding:8px 10px;font-size:12px;color:var(--muted)">
|
|
<i class="fas fa-times-circle" style="color:#ef4444"></i> Belum ada rumah ibadah yang menjangkau titik ini
|
|
</div>`;
|
|
}
|
|
|
|
// Info bantuan terakhir di tab Info
|
|
const bantuanInfoHtml = (() => {
|
|
if (info.kritis) {
|
|
return `<div style="background:rgba(239,68,68,.12);border:1.5px solid rgba(239,68,68,.5);border-radius:8px;padding:8px 11px;display:flex;align-items:center;gap:8px;font-size:12.5px;color:#ef4444;font-weight:700">
|
|
<i class="fas fa-triangle-exclamation"></i>
|
|
<div>
|
|
<div>⚠ Belum dapat bantuan ${info.hari === null ? 'sama sekali' : `selama ${info.label}`}</div>
|
|
<div style="font-size:10.5px;font-weight:400;color:#f87171;margin-top:2px">Melebihi batas 3 bulan — segera tindak lanjut</div>
|
|
</div>
|
|
</div>`;
|
|
} else if (info.hari === 0) {
|
|
return `<div style="background:rgba(16,185,129,.1);border:1px solid rgba(16,185,129,.3);border-radius:8px;padding:8px 11px;font-size:12.5px;color:#10b981;font-weight:600">
|
|
<i class="fas fa-check-circle"></i> Sudah mendapat bantuan hari ini — ${esc(info.jenis||'')}
|
|
</div>`;
|
|
} else {
|
|
return `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:8px 11px;font-size:12.5px;color:var(--muted)">
|
|
<i class="fas fa-clock"></i> Terakhir: ${info.label}${info.tanggal ? ` (${info.tanggal})` : ''}
|
|
${info.jenis ? ` — <em>${esc(info.jenis)}</em>` : ''}
|
|
${info.oleh ? ` oleh <strong>${esc(info.oleh)}</strong>` : ''}
|
|
</div>`;
|
|
}
|
|
})();
|
|
|
|
document.getElementById('detail-info-content').innerHTML = `
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|
<!-- Bantuan alert di paling atas jika kritis -->
|
|
${info.kritis ? bantuanInfoHtml : ''}
|
|
|
|
<!-- Grid info utama -->
|
|
<div style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;align-items:start;background:var(--surface2);border:1px solid var(--border);border-radius:9px;padding:12px">
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Kelas</span>
|
|
<span><span class="${kelasMap[mk.kelas_kemiskinan]||'cbadge'}">${mk.kelas_kemiskinan||'—'}</span></span>
|
|
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Jangkauan</span>
|
|
<span>${statusBadge}</span>
|
|
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Jumlah Jiwa</span>
|
|
<span style="font-size:13px">${mk.jumlah_jiwa || 1} orang</span>
|
|
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Alamat</span>
|
|
<span style="font-size:12.5px">${mk.alamat ? esc(mk.alamat) : '<span style="color:var(--muted)">—</span>'}</span>
|
|
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Keterangan</span>
|
|
<span style="font-size:12.5px">${mk.keterangan ? esc(mk.keterangan) : '<span style="color:var(--muted)">—</span>'}</span>
|
|
|
|
<span style="color:var(--muted);font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.5px">Koordinat</span>
|
|
<span style="font-family:monospace;font-size:11px">${(+mk.latitude).toFixed(6)}, ${(+mk.longitude).toFixed(6)}</span>
|
|
</div>
|
|
|
|
<!-- Rumah ibadah yang membantu -->
|
|
<div style="display:grid;grid-template-columns:100px 1fr;gap:6px 12px;align-items:start">
|
|
${ibadahInfoHtml}
|
|
</div>
|
|
|
|
<!-- Info bantuan terakhir (jika tidak kritis, tampil di sini) -->
|
|
${!info.kritis ? `
|
|
<div>
|
|
<div style="font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin-bottom:5px">
|
|
<i class="fas fa-hand-holding-heart"></i> Bantuan Terakhir
|
|
</div>
|
|
${bantuanInfoHtml}
|
|
</div>` : ''}
|
|
|
|
<!-- Tombol Edit -->
|
|
<button class="btn btn-p" style="width:100%;justify-content:center"
|
|
onclick="closeModal('detail');editMiskin(${mk.id})">
|
|
<i class="fas fa-pen"></i> Edit Data Ini
|
|
</button>
|
|
</div>`;
|
|
|
|
// ── Tab Foto ──
|
|
loadFotoPreview(mk);
|
|
|
|
// ── Tab Bantuan ──
|
|
await loadRiwayatBantuan(id);
|
|
|
|
// Set tanggal default form bantuan ke hari ini
|
|
document.getElementById('b-tanggal').value = new Date().toISOString().split('T')[0];
|
|
document.getElementById('b-jenis').value = '';
|
|
document.getElementById('b-pemberi').value = '';
|
|
document.getElementById('b-ket').value = '';
|
|
|
|
// Reset ke tab pertama
|
|
switchDetailTab('info');
|
|
document.getElementById('modal-detail').classList.add('open');
|
|
}
|
|
window.openDetail = openDetail;
|
|
|
|
// Tutup modal detail
|
|
document.getElementById('modal-detail').addEventListener('click', e => {
|
|
if (e.target === e.currentTarget) closeModal('detail');
|
|
});
|
|
|
|
// ── Switch tab dalam modal detail ──
|
|
function switchDetailTab(tab) {
|
|
document.querySelectorAll('.dtab').forEach((el,i) => el.classList.toggle('active', ['info','foto','bantuan'][i]===tab));
|
|
document.querySelectorAll('.dpanel').forEach(p => p.classList.remove('active'));
|
|
document.getElementById('dp-'+tab).classList.add('active');
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// FOTO — preview & upload
|
|
// ════════════════════════════════════════════════════════════════
|
|
function loadFotoPreview(mk) {
|
|
['rumah','kk'].forEach(type => {
|
|
const field = type === 'rumah' ? mk.foto_rumah : mk.foto_kk;
|
|
const img = document.getElementById(`foto-${type}-img`);
|
|
const ph = document.getElementById(`foto-${type}-placeholder`);
|
|
const chBtn = document.getElementById(`foto-${type}-change`);
|
|
const fileInp = document.getElementById(`foto-${type}-input`);
|
|
|
|
if (field) {
|
|
img.src = FOTO_BASE + field;
|
|
img.style.display = 'block';
|
|
ph.style.display = 'none';
|
|
chBtn.style.display = 'block';
|
|
// Sembunyikan input file di bawah img, tombol Ganti yang trigger
|
|
fileInp.style.display = 'none';
|
|
} else {
|
|
img.style.display = 'none';
|
|
img.src = '';
|
|
ph.style.display = '';
|
|
chBtn.style.display = 'none';
|
|
fileInp.style.display = '';
|
|
}
|
|
});
|
|
}
|
|
|
|
async function uploadFoto(input, type) {
|
|
if (!input.files || !input.files[0]) return;
|
|
if (!detailMiskinId) return;
|
|
|
|
const loading = document.getElementById(`foto-${type}-loading`);
|
|
loading.style.display = 'flex';
|
|
|
|
const formData = new FormData();
|
|
formData.append('foto', input.files[0]);
|
|
formData.append('type', type);
|
|
formData.append('id_miskin', detailMiskinId);
|
|
|
|
try {
|
|
const res = await fetch(UPLOAD_URL, { method:'POST', body:formData });
|
|
const json = await res.json();
|
|
if (!json.success) throw new Error(json.message);
|
|
|
|
// Update preview langsung
|
|
const img = document.getElementById(`foto-${type}-img`);
|
|
const ph = document.getElementById(`foto-${type}-placeholder`);
|
|
const chBtn = document.getElementById(`foto-${type}-change`);
|
|
img.src = json.url + '?t=' + Date.now();
|
|
img.style.display = 'block';
|
|
ph.style.display = 'none';
|
|
chBtn.style.display = 'block';
|
|
input.style.display = 'none';
|
|
|
|
// Update data lokal
|
|
const idx = miskinData.findIndex(x => x.id == detailMiskinId);
|
|
if (idx !== -1) {
|
|
if (type === 'rumah') miskinData[idx].foto_rumah = json.filename;
|
|
else miskinData[idx].foto_kk = json.filename;
|
|
}
|
|
showToast(`Foto ${type === 'rumah' ? 'rumah' : 'KK'} berhasil diupload`, 'success');
|
|
} catch(e) {
|
|
showToast('Upload gagal: ' + e.message, 'error');
|
|
} finally {
|
|
loading.style.display = 'none';
|
|
input.value = '';
|
|
}
|
|
}
|
|
window.uploadFoto = uploadFoto;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// RIWAYAT BANTUAN
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function loadRiwayatBantuan(idMiskin) {
|
|
try {
|
|
const r = await apiFetch('bantuan','GET',null,null,`&id_miskin=${idMiskin}`);
|
|
const data = r.data || [];
|
|
const sudahHariIni = r.sudah_hari_ini || false;
|
|
|
|
// Update status hari ini
|
|
const box = document.getElementById('bantuan-status-box');
|
|
const txt = document.getElementById('bantuan-status-text');
|
|
const btn = document.getElementById('btn-konfirmasi');
|
|
|
|
if (sudahHariIni) {
|
|
box.className = 'bantuan-hari-ini sudah';
|
|
txt.textContent = '✔ Sudah mendapat bantuan hari ini';
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<i class="fas fa-check-double"></i> Terkonfirmasi';
|
|
} else {
|
|
box.className = 'bantuan-hari-ini belum';
|
|
txt.textContent = 'Belum mendapat bantuan hari ini';
|
|
btn.disabled = false;
|
|
btn.innerHTML = '<i class="fas fa-check"></i> Konfirmasi';
|
|
}
|
|
|
|
// Total riwayat
|
|
document.getElementById('bantuan-total').textContent =
|
|
data.length > 0 ? `${data.length} riwayat` : '';
|
|
|
|
// Render list riwayat
|
|
const list = document.getElementById('riwayat-list');
|
|
if (!data.length) {
|
|
list.innerHTML = `<div class="empty" style="padding:20px"><i class="fas fa-hand-holding-heart"></i><p>Belum ada riwayat bantuan</p></div>`;
|
|
return;
|
|
}
|
|
list.innerHTML = data.map(rb => {
|
|
const hariLalu = +rb.hari_lalu;
|
|
const hariLabel = hariLalu === 0 ? 'Hari ini' : `${hariLalu} hari lalu`;
|
|
const hariColor = hariLalu === 0 ? 'var(--green)' : hariLalu <= 7 ? 'var(--accent2)' : 'var(--muted)';
|
|
return `<div class="riwayat-item">
|
|
<div class="hari-badge">
|
|
<span class="hari-num" style="color:${hariColor}">${hariLalu === 0 ? '0' : hariLalu}</span>
|
|
<span class="hari-txt">${hariLalu === 0 ? 'hari ini' : 'hari lalu'}</span>
|
|
</div>
|
|
<div class="riwayat-info">
|
|
<div class="riwayat-jenis">${esc(rb.jenis_bantuan)}</div>
|
|
<div class="riwayat-meta">
|
|
📅 ${rb.tanggal}
|
|
${rb.diberikan_oleh ? ` · 🏢 ${esc(rb.diberikan_oleh)}` : ''}
|
|
${rb.keterangan ? ` · ${esc(rb.keterangan)}` : ''}
|
|
</div>
|
|
</div>
|
|
<button class="riwayat-del" onclick="hapusRiwayat(${rb.id})" title="Hapus riwayat ini">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>`;
|
|
}).join('');
|
|
} catch(e) {
|
|
showToast('Gagal memuat riwayat: ' + e.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function simpanBantuan() {
|
|
const jenis = document.getElementById('b-jenis').value.trim();
|
|
const tanggal = document.getElementById('b-tanggal').value;
|
|
const pemberi = document.getElementById('b-pemberi').value.trim();
|
|
const ket = document.getElementById('b-ket').value.trim();
|
|
if (!jenis) { shakeEl('b-jenis'); showToast('Jenis bantuan wajib diisi','error'); return; }
|
|
if (!tanggal) { shakeEl('b-tanggal'); showToast('Tanggal wajib diisi','error'); return; }
|
|
try {
|
|
const r = await apiFetch('bantuan','POST',{
|
|
id_miskin: detailMiskinId, jenis_bantuan: jenis,
|
|
tanggal, keterangan: ket, diberikan_oleh: pemberi
|
|
});
|
|
if (!r.success) throw new Error(r.message);
|
|
showToast('Riwayat bantuan disimpan','success');
|
|
document.getElementById('b-jenis').value = '';
|
|
document.getElementById('b-pemberi').value = '';
|
|
document.getElementById('b-ket').value = '';
|
|
await refreshBantuanCache();
|
|
await loadRiwayatBantuan(detailMiskinId);
|
|
recalcMiskinColors(); // update warna marker & sidebar
|
|
} catch(e) { showToast('Gagal: '+e.message,'error'); }
|
|
}
|
|
window.simpanBantuan = simpanBantuan;
|
|
|
|
async function konfirmasiBantuan() {
|
|
// Konfirmasi bantuan hari ini dengan jenis "Bantuan Harian"
|
|
const btn = document.getElementById('btn-konfirmasi');
|
|
btn.disabled = true;
|
|
try {
|
|
const r = await apiFetch('bantuan','POST',{
|
|
id_miskin: detailMiskinId,
|
|
jenis_bantuan: 'Bantuan Harian',
|
|
tanggal: new Date().toISOString().split('T')[0],
|
|
keterangan: 'Dikonfirmasi via sistem WebGIS',
|
|
diberikan_oleh: '',
|
|
});
|
|
if (!r.success) throw new Error(r.message);
|
|
showToast('Bantuan hari ini berhasil dikonfirmasi ✔','success');
|
|
await refreshBantuanCache();
|
|
await loadRiwayatBantuan(detailMiskinId);
|
|
recalcMiskinColors(); // update warna marker
|
|
} catch(e) {
|
|
showToast('Gagal: '+e.message,'error');
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
window.konfirmasiBantuan = konfirmasiBantuan;
|
|
|
|
async function hapusRiwayat(id) {
|
|
if (!confirm('Hapus riwayat bantuan ini?')) return;
|
|
const r = await apiFetch('bantuan','DELETE',null,id);
|
|
if (r.success) { showToast('Riwayat dihapus','success'); await loadRiwayatBantuan(detailMiskinId); }
|
|
else showToast('Gagal: '+r.message,'error');
|
|
}
|
|
window.hapusRiwayat = hapusRiwayat;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// GPS LAPANGAN
|
|
// ════════════════════════════════════════════════════════════════
|
|
let gpsWatcher = null; // ID untuk watchPosition (live tracking)
|
|
|
|
// Tombol di toolbar — langsung klik peta ke lokasi GPS dan buka form
|
|
function tambahViaGPS() {
|
|
if (!navigator.geolocation) {
|
|
showToast('Browser tidak mendukung GPS', 'error'); return;
|
|
}
|
|
const btn = document.getElementById('tbtn-gps');
|
|
btn.classList.add('a-gps');
|
|
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Mencari GPS…';
|
|
btn.disabled = true;
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
btn.classList.remove('a-gps');
|
|
btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> GPS Lapangan';
|
|
btn.disabled = false;
|
|
|
|
const lat = pos.coords.latitude;
|
|
const lng = pos.coords.longitude;
|
|
const acc = Math.round(pos.coords.accuracy);
|
|
|
|
// Pindahkan peta ke lokasi GPS
|
|
map.flyTo([lat, lng], 18, { duration: 1.2 });
|
|
|
|
// Buat marker sementara
|
|
if (pendingMark) pendingMark.remove();
|
|
pendingMark = L.marker([lat, lng], { icon: iconMiskinLuar }).addTo(map);
|
|
|
|
// Lingkaran akurasi GPS
|
|
const accCircle = L.circle([lat, lng], {
|
|
radius: acc, color: '#3b82f6', weight: 1,
|
|
fillColor: '#3b82f6', fillOpacity: 0.08, dashArray: '4,4'
|
|
}).addTo(map);
|
|
setTimeout(() => accCircle.remove(), 8000);
|
|
|
|
// Isi form
|
|
pendingLL = { lat, lng };
|
|
editMiskinId = null;
|
|
document.getElementById('miskin-title').textContent = 'Tambah Penduduk Miskin (GPS)';
|
|
document.getElementById('miskin-coords').textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)} · Akurasi ±${acc}m`;
|
|
document.getElementById('miskin-nama').value = '';
|
|
document.getElementById('miskin-jml').value = '';
|
|
document.getElementById('miskin-kelas').value = 'Kelas I';
|
|
document.getElementById('miskin-alamat').value = '';
|
|
document.getElementById('miskin-ket').value = '';
|
|
const { dalam, ibadah } = checkJangkauan(lat, lng);
|
|
renderStatusBox(dalam, ibadah);
|
|
|
|
// Tampilkan info akurasi di GPS status bar
|
|
showGPSStatus(acc < 20 ? 'ok' : acc < 50 ? 'warn' : 'bad', acc);
|
|
openModal('miskin');
|
|
|
|
showToast(`Lokasi GPS ditemukan (±${acc}m)`, 'success');
|
|
},
|
|
(err) => {
|
|
btn.classList.remove('a-gps');
|
|
btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> GPS Lapangan';
|
|
btn.disabled = false;
|
|
const msg = {
|
|
1: 'Izin GPS ditolak. Aktifkan izin lokasi di browser.',
|
|
2: 'GPS tidak tersedia. Pastikan GPS perangkat aktif.',
|
|
3: 'GPS timeout. Coba lagi.',
|
|
};
|
|
showToast(msg[err.code] || 'GPS error', 'error');
|
|
},
|
|
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
|
|
);
|
|
}
|
|
|
|
// Tombol di dalam modal — perbarui koordinat dengan GPS terbaru
|
|
function pakaiGPSdiModal() {
|
|
if (!navigator.geolocation) {
|
|
showToast('Browser tidak mendukung GPS', 'error'); return;
|
|
}
|
|
const btn = document.getElementById('btn-gps-modal');
|
|
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Mencari…';
|
|
btn.disabled = true;
|
|
showGPSStatus('loading', null);
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> Pakai GPS';
|
|
btn.disabled = false;
|
|
const lat = pos.coords.latitude;
|
|
const lng = pos.coords.longitude;
|
|
const acc = Math.round(pos.coords.accuracy);
|
|
|
|
pendingLL = { lat, lng };
|
|
document.getElementById('miskin-coords').textContent = `${lat.toFixed(6)}, ${lng.toFixed(6)} · Akurasi ±${acc}m`;
|
|
|
|
// Pindahkan marker pending
|
|
if (pendingMark) pendingMark.remove();
|
|
pendingMark = L.marker([lat, lng], { icon: iconMiskinLuar }).addTo(map);
|
|
map.flyTo([lat, lng], 18, { duration: 1 });
|
|
|
|
// Recalculate jangkauan
|
|
const { dalam, ibadah } = checkJangkauan(lat, lng);
|
|
renderStatusBox(dalam, ibadah);
|
|
showGPSStatus(acc < 20 ? 'ok' : acc < 50 ? 'warn' : 'bad', acc);
|
|
showToast(`Koordinat diperbarui (±${acc}m)`, 'success');
|
|
},
|
|
(err) => {
|
|
btn.innerHTML = '<i class="fas fa-location-crosshairs"></i> Pakai GPS';
|
|
btn.disabled = false;
|
|
hideGPSStatus();
|
|
showToast('GPS gagal: ' + (err.message || 'Unknown error'), 'error');
|
|
},
|
|
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
|
|
);
|
|
}
|
|
window.pakaiGPSdiModal = pakaiGPSdiModal;
|
|
window.tambahViaGPS = tambahViaGPS;
|
|
window.eksporPDF = eksporPDF;
|
|
window.eksporExcel = eksporExcel;
|
|
|
|
function showGPSStatus(level, acc) {
|
|
const bar = document.getElementById('gps-status-bar');
|
|
const icon = document.getElementById('gps-icon');
|
|
const txt = document.getElementById('gps-status-text');
|
|
bar.style.display = 'flex';
|
|
if (level === 'loading') {
|
|
icon.className = 'fas fa-circle-notch fa-spin'; icon.style.color = '#3b82f6';
|
|
txt.textContent = 'Mendapatkan lokasi GPS…'; txt.style.color = '#93c5fd';
|
|
bar.style.borderColor = 'rgba(59,130,246,.3)'; bar.style.background = 'rgba(59,130,246,.1)';
|
|
} else if (level === 'ok') {
|
|
icon.className = 'fas fa-check-circle'; icon.style.color = '#10b981';
|
|
txt.textContent = `✔ Akurasi tinggi ±${acc}m — koordinat siap digunakan`; txt.style.color = '#6ee7b7';
|
|
bar.style.borderColor = 'rgba(16,185,129,.3)'; bar.style.background = 'rgba(16,185,129,.08)';
|
|
} else if (level === 'warn') {
|
|
icon.className = 'fas fa-exclamation-triangle'; icon.style.color = '#f59e0b';
|
|
txt.textContent = `⚠ Akurasi sedang ±${acc}m — posisikan perangkat di area terbuka`; txt.style.color = '#fcd34d';
|
|
bar.style.borderColor = 'rgba(245,158,11,.3)'; bar.style.background = 'rgba(245,158,11,.08)';
|
|
} else {
|
|
icon.className = 'fas fa-times-circle'; icon.style.color = '#ef4444';
|
|
txt.textContent = `✘ Akurasi rendah ±${acc}m — hasil mungkin tidak tepat`; txt.style.color = '#fca5a5';
|
|
bar.style.borderColor = 'rgba(239,68,68,.3)'; bar.style.background = 'rgba(239,68,68,.08)';
|
|
}
|
|
}
|
|
function hideGPSStatus() {
|
|
document.getElementById('gps-status-bar').style.display = 'none';
|
|
}
|
|
|
|
// Sembunyikan GPS bar saat modal ditutup
|
|
const origCloseMiskin = closeModal;
|
|
// Override closeModal untuk reset GPS bar
|
|
document.getElementById('modal-miskin').addEventListener('click', e => {
|
|
if (e.target === e.currentTarget) hideGPSStatus();
|
|
});
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// EKSPOR PDF
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function eksporPDF() {
|
|
if (!miskinData.length) { showToast('Tidak ada data penduduk miskin', 'error'); return; }
|
|
|
|
const btn = document.getElementById('tbtn-pdf');
|
|
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Membuat PDF…';
|
|
|
|
try {
|
|
const { jsPDF } = window.jspdf;
|
|
const doc = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
|
|
|
|
const today = new Date().toLocaleDateString('id-ID', { day:'2-digit', month:'long', year:'numeric' });
|
|
const pageW = doc.internal.pageSize.getWidth();
|
|
const pageH = doc.internal.pageSize.getHeight();
|
|
const marginL = 14;
|
|
const colW = pageW - marginL * 2;
|
|
|
|
// ── Header ──
|
|
doc.setFillColor(13, 17, 23);
|
|
doc.rect(0, 0, pageW, 28, 'F');
|
|
doc.setTextColor(16, 185, 129);
|
|
doc.setFontSize(15); doc.setFont('helvetica', 'bold');
|
|
doc.text('LAPORAN DATA PENDUDUK MISKIN', marginL, 10);
|
|
doc.setFontSize(9); doc.setFont('helvetica', 'normal');
|
|
doc.setTextColor(139, 148, 158);
|
|
doc.text('WebGIS Pemetaan Penduduk Miskin — Berbasis Partisipasi Rumah Ibadah', marginL, 16);
|
|
doc.text(`Dicetak: ${today}`, marginL, 22);
|
|
|
|
// ── Statistik ringkasan ──
|
|
const dalamCount = miskinData.filter(mk => checkJangkauan(+mk.latitude, +mk.longitude).dalam).length;
|
|
const luarCount = miskinData.length - dalamCount;
|
|
const k1 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas I').length;
|
|
const k2 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas II').length;
|
|
const k3 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas III').length;
|
|
const kritisCount = miskinData.filter(m => infoHariBantuan(m.id).kritis).length;
|
|
|
|
const stats = [
|
|
['Total KK', miskinData.length],
|
|
['Terjangkau Ibadah', dalamCount],
|
|
['Tidak Terjangkau', luarCount],
|
|
['Kelas I (Sangat Miskin)', k1],
|
|
['Kelas II (Miskin)', k2],
|
|
['Kelas III (Hampir Miskin)', k3],
|
|
['Kritis (>3 bln)', kritisCount],
|
|
];
|
|
|
|
let sx = marginL;
|
|
const statBoxW = (colW - 6 * 4) / stats.length;
|
|
stats.forEach(([label, val], i) => {
|
|
const bx = sx + i * (statBoxW + 4);
|
|
doc.setFillColor(33, 38, 45);
|
|
doc.roundedRect(bx, 31, statBoxW, 16, 2, 2, 'F');
|
|
doc.setTextColor(232, 176, 75); doc.setFontSize(13); doc.setFont('helvetica','bold');
|
|
doc.text(String(val), bx + statBoxW/2, 40, { align:'center' });
|
|
doc.setTextColor(139,148,158); doc.setFontSize(6.5); doc.setFont('helvetica','normal');
|
|
doc.text(label, bx + statBoxW/2, 44.5, { align:'center' });
|
|
});
|
|
|
|
// ── Tabel ──
|
|
const rows = miskinData.map((mk, i) => {
|
|
const { dalam, ibadah } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
const info = infoHariBantuan(mk.id);
|
|
const kritis = info.kritis;
|
|
return [
|
|
i + 1,
|
|
mk.nama_kk || '—',
|
|
mk.kelas_kemiskinan || '—',
|
|
mk.jumlah_jiwa || 1,
|
|
dalam ? (ibadah?.nama_ibadah || '✔') : '✘ Tidak',
|
|
mk.alamat || '—',
|
|
info.label,
|
|
mk.keterangan || '—',
|
|
];
|
|
});
|
|
|
|
doc.autoTable({
|
|
startY: 51,
|
|
head: [['No','Nama KK','Kelas','Jiwa','Jangkauan Ibadah','Alamat','Bantuan Terakhir','Keterangan']],
|
|
body: rows,
|
|
styles: {
|
|
fontSize: 7.5, cellPadding: 2.5,
|
|
textColor: [230, 237, 243],
|
|
fillColor: [22, 27, 34],
|
|
lineColor: [48, 54, 61], lineWidth: 0.3,
|
|
font: 'helvetica',
|
|
halign: 'left',
|
|
},
|
|
headStyles: {
|
|
fillColor: [16, 185, 129], textColor: [255, 255, 255],
|
|
fontStyle: 'bold', fontSize: 8, halign: 'center',
|
|
},
|
|
alternateRowStyles: { fillColor: [33, 38, 45] },
|
|
columnStyles: {
|
|
0: { halign:'center', cellWidth: 8 },
|
|
2: { halign:'center', cellWidth: 22 },
|
|
3: { halign:'center', cellWidth: 10 },
|
|
4: { cellWidth: 38 },
|
|
6: { cellWidth: 28, halign:'center' },
|
|
},
|
|
didParseCell(data) {
|
|
if (data.section === 'body') {
|
|
const mk = miskinData[data.row.index];
|
|
const info = infoHariBantuan(mk?.id);
|
|
// Warna baris kritis
|
|
if (info?.kritis) {
|
|
data.cell.styles.textColor = [239, 68, 68];
|
|
}
|
|
// Kolom kelas
|
|
if (data.column.index === 2) {
|
|
const kColors = { 'Kelas I':[239,68,68], 'Kelas II':[245,158,11], 'Kelas III':[59,130,246] };
|
|
data.cell.styles.textColor = kColors[mk?.kelas_kemiskinan] || [230,237,243];
|
|
data.cell.styles.fontStyle = 'bold';
|
|
}
|
|
// Kolom jangkauan
|
|
if (data.column.index === 4) {
|
|
const { dalam } = checkJangkauan(+mk?.latitude, +mk?.longitude);
|
|
data.cell.styles.textColor = dalam ? [16,185,129] : [239,68,68];
|
|
}
|
|
}
|
|
},
|
|
margin: { left: marginL, right: marginL },
|
|
});
|
|
|
|
// ── Footer tiap halaman ──
|
|
const pageCount = doc.internal.getNumberOfPages();
|
|
for (let i = 1; i <= pageCount; i++) {
|
|
doc.setPage(i);
|
|
doc.setFillColor(22, 27, 34);
|
|
doc.rect(0, pageH - 8, pageW, 8, 'F');
|
|
doc.setFontSize(7); doc.setTextColor(139,148,158);
|
|
doc.text('WebGIS Pemetaan Penduduk Miskin', marginL, pageH - 2.5);
|
|
doc.text(`Halaman ${i} dari ${pageCount}`, pageW - marginL, pageH - 2.5, { align:'right' });
|
|
}
|
|
|
|
doc.save(`Laporan_Penduduk_Miskin_${today.replace(/ /g,'_')}.pdf`);
|
|
showToast('PDF berhasil diunduh', 'success');
|
|
} catch(e) {
|
|
showToast('Gagal membuat PDF: ' + e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false; btn.innerHTML = '<i class="fas fa-file-pdf" style="color:#ef4444"></i> PDF';
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// EKSPOR EXCEL
|
|
// ════════════════════════════════════════════════════════════════
|
|
async function eksporExcel() {
|
|
if (!miskinData.length) { showToast('Tidak ada data penduduk miskin', 'error'); return; }
|
|
|
|
const btn = document.getElementById('tbtn-xls');
|
|
btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Membuat Excel…';
|
|
|
|
try {
|
|
const today = new Date().toLocaleDateString('id-ID');
|
|
const wb = XLSX.utils.book_new();
|
|
|
|
// ── Sheet 1: Data Penduduk Miskin ──
|
|
const header = [
|
|
'No','Nama KK','Kelas Kemiskinan','Jumlah Jiwa',
|
|
'Jangkauan Ibadah','Nama Ibadah Terdekat','Kategori Ibadah','Jarak ke Ibadah (m)',
|
|
'Alamat','Keterangan',
|
|
'Bantuan Terakhir','Hari Sejak Bantuan','Status Kritis',
|
|
'Latitude','Longitude'
|
|
];
|
|
|
|
const rows = miskinData.map((mk, i) => {
|
|
const { dalam, ibadah, jarak } = checkJangkauan(+mk.latitude, +mk.longitude);
|
|
const info = infoHariBantuan(mk.id);
|
|
return [
|
|
i + 1,
|
|
mk.nama_kk || '',
|
|
mk.kelas_kemiskinan || '',
|
|
mk.jumlah_jiwa || 1,
|
|
dalam ? 'Terjangkau' : 'Tidak Terjangkau',
|
|
ibadah?.nama_ibadah || '',
|
|
ibadah?.kategori || '',
|
|
jarak ? Math.round(jarak) : '',
|
|
mk.alamat || '',
|
|
mk.keterangan || '',
|
|
info.tanggal || 'Belum pernah',
|
|
info.hari !== null ? info.hari : '',
|
|
info.kritis ? 'KRITIS' : 'Aman',
|
|
+mk.latitude,
|
|
+mk.longitude,
|
|
];
|
|
});
|
|
|
|
const wsData = [header, ...rows];
|
|
const ws1 = XLSX.utils.aoa_to_sheet(wsData);
|
|
|
|
// Lebar kolom
|
|
ws1['!cols'] = [
|
|
{wch:5},{wch:25},{wch:18},{wch:12},
|
|
{wch:18},{wch:30},{wch:18},{wch:18},
|
|
{wch:35},{wch:35},
|
|
{wch:18},{wch:18},{wch:12},
|
|
{wch:14},{wch:14}
|
|
];
|
|
|
|
// Styling header row (warna hijau)
|
|
const range = XLSX.utils.decode_range(ws1['!ref']);
|
|
for (let c = range.s.c; c <= range.e.c; c++) {
|
|
const cell = ws1[XLSX.utils.encode_cell({ r:0, c })];
|
|
if (cell) {
|
|
cell.s = {
|
|
fill: { fgColor: { rgb: '0D7F5C' } },
|
|
font: { bold: true, color: { rgb: 'FFFFFF' } },
|
|
alignment: { horizontal: 'center' },
|
|
border: { bottom: { style:'thin', color:{ rgb:'10b981' } } }
|
|
};
|
|
}
|
|
}
|
|
|
|
XLSX.utils.book_append_sheet(wb, ws1, 'Data Penduduk Miskin');
|
|
|
|
// ── Sheet 2: Ringkasan per Kelas ──
|
|
const k1 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas I');
|
|
const k2 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas II');
|
|
const k3 = miskinData.filter(m => m.kelas_kemiskinan === 'Kelas III');
|
|
const dalamData = miskinData.filter(m => checkJangkauan(+m.latitude, +m.longitude).dalam);
|
|
const kritisData = miskinData.filter(m => infoHariBantuan(m.id).kritis);
|
|
|
|
const ws2Data = [
|
|
['RINGKASAN DATA PENDUDUK MISKIN', ''],
|
|
['Tanggal Cetak', today],
|
|
['', ''],
|
|
['KATEGORI', 'JUMLAH KK'],
|
|
['Total Keseluruhan', miskinData.length],
|
|
['Kelas I — Sangat Miskin', k1.length],
|
|
['Kelas II — Miskin', k2.length],
|
|
['Kelas III — Hampir Miskin', k3.length],
|
|
['', ''],
|
|
['STATUS JANGKAUAN', 'JUMLAH KK'],
|
|
['Terjangkau Rumah Ibadah', dalamData.length],
|
|
['Tidak Terjangkau', miskinData.length - dalamData.length],
|
|
['', ''],
|
|
['STATUS BANTUAN', 'JUMLAH KK'],
|
|
['Kritis (>3 bulan tanpa bantuan)', kritisData.length],
|
|
['Total Jiwa', miskinData.reduce((s, m) => s + (+m.jumlah_jiwa||1), 0)],
|
|
];
|
|
const ws2 = XLSX.utils.aoa_to_sheet(ws2Data);
|
|
ws2['!cols'] = [{ wch:35 }, { wch:15 }];
|
|
XLSX.utils.book_append_sheet(wb, ws2, 'Ringkasan');
|
|
|
|
// ── Sheet 3: Data Rumah Ibadah ──
|
|
const ws3Header = ['No','Nama Ibadah','Kategori','Radius (m)','Alamat','Keterangan','Latitude','Longitude'];
|
|
const ws3Rows = ibadahData.map((ib, i) => [
|
|
i+1, ib.nama_ibadah, ib.kategori, ib.radius_meter,
|
|
ib.alamat||'', ib.keterangan||'', +ib.latitude, +ib.longitude
|
|
]);
|
|
const ws3 = XLSX.utils.aoa_to_sheet([ws3Header, ...ws3Rows]);
|
|
ws3['!cols'] = [{wch:5},{wch:30},{wch:18},{wch:12},{wch:35},{wch:35},{wch:14},{wch:14}];
|
|
XLSX.utils.book_append_sheet(wb, ws3, 'Rumah Ibadah');
|
|
|
|
// Download
|
|
XLSX.writeFile(wb, `Data_Penduduk_Miskin_${today.replace(/\//g,'-')}.xlsx`);
|
|
showToast('Excel berhasil diunduh (3 sheet)', 'success');
|
|
} catch(e) {
|
|
showToast('Gagal membuat Excel: ' + e.message, 'error');
|
|
} finally {
|
|
btn.disabled = false; btn.innerHTML = '<i class="fas fa-file-excel" style="color:#10b981"></i> Excel';
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// INIT
|
|
// ════════════════════════════════════════════════════════════════
|
|
loadAll().then(() => {
|
|
if (USER_ROLE === 'operator') {
|
|
// Sembunyikan toolbar tambah data
|
|
document.querySelectorAll(
|
|
'#tbtn-masjid,#tbtn-gereja,#tbtn-gereja-katolik,#tbtn-pura,#tbtn-vihara,#tbtn-klenteng,#tbtn-miskin,#tbtn-gps'
|
|
).forEach(el => el.style.display = 'none');
|
|
|
|
// Sembunyikan tombol edit & hapus di sidebar dan popup
|
|
document.querySelectorAll('.cbtn-e,.cbtn-d,.pbtn-e,.pbtn-d').forEach(el => el.style.display = 'none');
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|