1822 lines
69 KiB
HTML
1822 lines
69 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="id">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>WebGIS Partisipasi Rumah Ibadah</title>
|
|
|
|
<!-- Leaflet CSS -->
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
|
|
|
<style>
|
|
/* ===== RESET ===== */
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: #f0f2f5;
|
|
}
|
|
|
|
/* ===== HEADER ===== */
|
|
header {
|
|
background: linear-gradient(135deg, #1a6b3c, #2d9e5f);
|
|
color: #fff;
|
|
padding: 10px 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .3);
|
|
z-index: 1000;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
header .title-area {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
header h1 {
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
header small {
|
|
font-size: .7rem;
|
|
opacity: .85;
|
|
}
|
|
|
|
.back-btn {
|
|
color: white;
|
|
text-decoration: none;
|
|
font-size: 0.9rem;
|
|
border: 1px solid rgba(255,255,255,0.5);
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
background: rgba(255,255,255,0.2);
|
|
}
|
|
|
|
/* ===== LAYOUT ===== */
|
|
.app-body {
|
|
display: flex;
|
|
flex: 1;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
/* ===== SIDEBAR ===== */
|
|
#sidebar {
|
|
width: 270px;
|
|
background: #fff;
|
|
display: flex;
|
|
flex-direction: column;
|
|
border-right: 1px solid #dde;
|
|
z-index: 900;
|
|
transition: transform .3s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
#sidebar.hide {
|
|
transform: translateX(-100%);
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
box-shadow: 4px 0 12px rgba(0, 0, 0, .2);
|
|
}
|
|
|
|
.sb-head {
|
|
background: #1a6b3c;
|
|
color: #fff;
|
|
padding: 10px 14px;
|
|
font-weight: 700;
|
|
font-size: .88rem;
|
|
}
|
|
|
|
/* Radius */
|
|
.radius-box {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid #eee;
|
|
background: #f8fdf9;
|
|
}
|
|
|
|
.radius-box label {
|
|
font-size: .78rem;
|
|
font-weight: 700;
|
|
color: #1a6b3c;
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.radius-box input[type=range] {
|
|
width: 100%;
|
|
accent-color: #1a6b3c;
|
|
cursor: pointer;
|
|
}
|
|
|
|
#radius-val {
|
|
font-size: .85rem;
|
|
font-weight: 700;
|
|
color: #333;
|
|
text-align: center;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* Tombol tambah */
|
|
.btn-row {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 8px 14px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
padding: 7px 10px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: .78rem;
|
|
font-weight: 700;
|
|
transition: filter .15s;
|
|
min-height: 44px;
|
|
min-width: 44px;
|
|
}
|
|
|
|
.btn:hover {
|
|
filter: brightness(.9);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: .5;
|
|
cursor: not-allowed;
|
|
filter: none;
|
|
}
|
|
|
|
.btn-green {
|
|
background: #1a6b3c;
|
|
color: #fff;
|
|
flex: 1;
|
|
}
|
|
|
|
.btn-orange {
|
|
background: #e08020;
|
|
color: #fff;
|
|
flex: 1;
|
|
}
|
|
|
|
.btn-red {
|
|
background: #d93025;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-blue {
|
|
background: #1976d2;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-purple {
|
|
background: #6a1b9a;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-teal {
|
|
background: #00796b;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-gray {
|
|
background: #e0e0e0;
|
|
color: #333;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: 4px 8px;
|
|
font-size: .72rem;
|
|
min-height: 30px;
|
|
min-width: auto;
|
|
}
|
|
|
|
.btn-xs {
|
|
padding: 3px 6px;
|
|
font-size: .68rem;
|
|
min-height: 26px;
|
|
min-width: auto;
|
|
}
|
|
|
|
.btn-full {
|
|
width: 100%;
|
|
}
|
|
|
|
/* ===== SIDEBAR TABS ===== */
|
|
.sb-tabs {
|
|
display: flex;
|
|
background: #fff;
|
|
border-bottom: 1px solid #ddd;
|
|
flex-shrink: 0;
|
|
}
|
|
.sb-tab {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 12px 0;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
color: #777;
|
|
cursor: pointer;
|
|
border-bottom: 3px solid transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
.sb-tab:hover {
|
|
background: #f8f8f8;
|
|
color: #333;
|
|
}
|
|
.sb-tab.active {
|
|
color: #1a6b3c;
|
|
border-bottom: 3px solid #1a6b3c;
|
|
background: #f8fdf9;
|
|
}
|
|
|
|
/* Search */
|
|
.sb-search {
|
|
padding: 7px 14px;
|
|
border-bottom: 1px solid #eee;
|
|
}
|
|
|
|
.sb-search input {
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
font-size: .82rem;
|
|
}
|
|
|
|
/* List */
|
|
#sb-list {
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
}
|
|
|
|
.list-item {
|
|
padding: 10px 14px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
transition: background .15s;
|
|
}
|
|
|
|
.list-item:hover {
|
|
background: #f0faf4;
|
|
}
|
|
|
|
.list-item .ico {
|
|
font-size: 1.3rem;
|
|
width: 26px;
|
|
text-align: center;
|
|
}
|
|
|
|
.list-item .inf .nm {
|
|
font-weight: 700;
|
|
font-size: .84rem;
|
|
}
|
|
|
|
.list-item .inf .jn {
|
|
font-size: .7rem;
|
|
color: #999;
|
|
}
|
|
|
|
.list-item .badge {
|
|
background: #e8f5e9;
|
|
color: #1a6b3c;
|
|
font-size: .68rem;
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.empty {
|
|
padding: 16px;
|
|
text-align: center;
|
|
color: #bbb;
|
|
font-size: .82rem;
|
|
}
|
|
|
|
/* ===== MAP ===== */
|
|
#map {
|
|
flex: 1;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Toggle btn */
|
|
#btn-toggle {
|
|
position: absolute;
|
|
top: 10px;
|
|
left: 0;
|
|
z-index: 950;
|
|
background: #1a6b3c;
|
|
color: #fff;
|
|
border: none;
|
|
padding: 8px 7px;
|
|
border-radius: 0 6px 6px 0;
|
|
cursor: pointer;
|
|
font-size: 1rem;
|
|
box-shadow: 2px 0 6px rgba(0, 0, 0, .25);
|
|
}
|
|
|
|
/* ===== MODAL ===== */
|
|
.overlay {
|
|
display: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, .5);
|
|
z-index: 2000;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.overlay.on {
|
|
display: flex;
|
|
}
|
|
|
|
.modal {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
width: 92%;
|
|
max-width: 500px;
|
|
max-height: 92vh;
|
|
overflow-y: auto;
|
|
padding: 22px;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, .25);
|
|
}
|
|
|
|
.modal-lg {
|
|
max-width: 620px;
|
|
}
|
|
|
|
.modal h2 {
|
|
font-size: 1rem;
|
|
color: #1a6b3c;
|
|
margin-bottom: 14px;
|
|
border-bottom: 2px solid #e8f5e9;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.modal h3 {
|
|
font-size: .88rem;
|
|
color: #333;
|
|
margin: 12px 0 8px;
|
|
}
|
|
|
|
.fg {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.fg label {
|
|
display: block;
|
|
font-size: .78rem;
|
|
font-weight: 700;
|
|
color: #444;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.fg input,
|
|
.fg select,
|
|
.fg textarea {
|
|
width: 100%;
|
|
padding: 8px 11px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 6px;
|
|
font-size: .84rem;
|
|
}
|
|
|
|
.fg input:focus,
|
|
.fg select:focus,
|
|
.fg textarea:focus {
|
|
border-color: #1a6b3c;
|
|
outline: none;
|
|
box-shadow: 0 0 0 3px rgba(26, 107, 60, .1);
|
|
}
|
|
|
|
.fg textarea {
|
|
resize: vertical;
|
|
min-height: 64px;
|
|
}
|
|
|
|
.fg-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.fg-row .fg {
|
|
flex: 1;
|
|
}
|
|
|
|
.fg-hint {
|
|
font-size: .7rem;
|
|
color: #aaa;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.err {
|
|
color: #d93025;
|
|
font-size: .75rem;
|
|
margin-top: 3px;
|
|
display: none;
|
|
}
|
|
|
|
.err.on {
|
|
display: block;
|
|
}
|
|
|
|
.modal-footer {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: flex-end;
|
|
margin-top: 14px;
|
|
}
|
|
|
|
.btn-petaclick {
|
|
background: #e8f5e9;
|
|
color: #1a6b3c;
|
|
border: 1px dashed #1a6b3c;
|
|
width: 100%;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.btn-petaclick.active {
|
|
background: #1a6b3c;
|
|
color: #fff;
|
|
}
|
|
|
|
/* Hapus konfirmasi */
|
|
.confirm-box {
|
|
text-align: center;
|
|
}
|
|
|
|
.confirm-box p {
|
|
margin-bottom: 14px;
|
|
font-size: .9rem;
|
|
color: #333;
|
|
}
|
|
|
|
/* ===== TABEL STOK ===== */
|
|
.tbl {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: .8rem;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.tbl th {
|
|
background: #f0faf4;
|
|
color: #1a6b3c;
|
|
padding: 6px 8px;
|
|
text-align: left;
|
|
border-bottom: 2px solid #ddeedd;
|
|
}
|
|
|
|
.tbl td {
|
|
padding: 6px 8px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.tbl tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.stok-bar {
|
|
background: #eee;
|
|
border-radius: 4px;
|
|
height: 6px;
|
|
margin-top: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stok-bar-inner {
|
|
background: #1a6b3c;
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width .4s;
|
|
}
|
|
|
|
.stok-bar-inner.low {
|
|
background: #e08020;
|
|
}
|
|
|
|
.stok-bar-inner.empty {
|
|
background: #d93025;
|
|
}
|
|
|
|
.badge-stok {
|
|
display: inline-block;
|
|
font-size: .68rem;
|
|
padding: 2px 7px;
|
|
border-radius: 10px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.badge-stok.ok {
|
|
background: #e8f5e9;
|
|
color: #1a6b3c;
|
|
}
|
|
|
|
.badge-stok.low {
|
|
background: #fff3e0;
|
|
color: #e08020;
|
|
}
|
|
|
|
.badge-stok.empty {
|
|
background: #fdecea;
|
|
color: #d93025;
|
|
}
|
|
|
|
/* Riwayat penyaluran */
|
|
.riwayat-item {
|
|
padding: 8px 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
font-size: .78rem;
|
|
}
|
|
|
|
.riwayat-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
/* ===== TOAST ===== */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 22px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #323232;
|
|
color: #fff;
|
|
padding: 9px 20px;
|
|
border-radius: 8px;
|
|
font-size: .84rem;
|
|
z-index: 3000;
|
|
opacity: 0;
|
|
transition: opacity .3s;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
#toast.on {
|
|
opacity: 1;
|
|
}
|
|
|
|
#toast.ok {
|
|
background: #1a6b3c;
|
|
}
|
|
|
|
#toast.err {
|
|
background: #d93025;
|
|
}
|
|
|
|
/* Leaflet legend */
|
|
.legenda {
|
|
background: rgba(255, 255, 255, .95);
|
|
padding: 9px 13px;
|
|
border-radius: 8px;
|
|
font-size: .77rem;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, .15);
|
|
}
|
|
|
|
.legenda h4 {
|
|
font-size: .78rem;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.leg-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
margin-top: 3px;
|
|
}
|
|
|
|
.dot {
|
|
width: 13px;
|
|
height: 13px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
border: 2px solid rgba(0, 0, 0, .15);
|
|
}
|
|
|
|
/* Popup section */
|
|
.popup-section {
|
|
margin-top: 8px;
|
|
padding-top: 8px;
|
|
border-top: 1px solid #eee;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
#sidebar {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
height: 100%;
|
|
box-shadow: 4px 0 12px rgba(0, 0, 0, .2);
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<header>
|
|
<div class="title-area">
|
|
<i class="fa-solid fa-mosque fa-lg"></i>
|
|
<div>
|
|
<h1>WebGIS Partisipasi Rumah Ibadah — Pengentasan Kemiskinan</h1>
|
|
<small>Universitas Tanjungpura | © OpenStreetMap contributors</small>
|
|
</div>
|
|
</div>
|
|
<a href="index.html" class="back-btn"><i class="fa-solid fa-arrow-left"></i> Kembali ke Dashboard</a>
|
|
</header>
|
|
|
|
<div class="app-body">
|
|
|
|
<!-- SIDEBAR -->
|
|
<div id="sidebar">
|
|
<div class="sb-tabs">
|
|
<div class="sb-tab active" id="tab-daftar" onclick="gantiTab('daftar')"><i class="fa-solid fa-map-location-dot"></i> Peta Data</div>
|
|
<div class="sb-tab" id="tab-mutasi" onclick="gantiTab('mutasi')"><i class="fa-solid fa-clock-rotate-left"></i> Mutasi Bantuan</div>
|
|
</div>
|
|
|
|
<div id="content-daftar" style="display: flex; flex-direction: column; flex: 1; overflow: hidden;">
|
|
<div class="sb-head"><i class="fa-solid fa-list-ul"></i> Daftar Rumah Ibadah</div>
|
|
|
|
<!-- Radius Slider -->
|
|
<div class="radius-box">
|
|
<label><i class="fa-solid fa-circle-dot"></i> Radius Jangkauan</label>
|
|
<input type="range" id="radius-slider" min="0" max="1500" value="500" step="50">
|
|
<div id="radius-val">500 m</div>
|
|
</div>
|
|
|
|
<!-- Tombol Tambah -->
|
|
<div class="btn-row">
|
|
<button class="btn btn-green" onclick="bukaFormRI()">
|
|
<i class="fa-solid fa-plus"></i> Rumah Ibadah
|
|
</button>
|
|
<button class="btn btn-orange" onclick="bukaFormLB()">
|
|
<i class="fa-solid fa-plus"></i> Penerima Bantuan
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="sb-search">
|
|
<input type="text" id="cari-input" placeholder="🔍 Cari rumah ibadah..."
|
|
oninput="filterList(this.value)">
|
|
</div>
|
|
|
|
<!-- List -->
|
|
<div id="sb-list">
|
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
|
|
</div>
|
|
</div> <!-- end content-daftar -->
|
|
|
|
<!-- Tab Mutasi -->
|
|
<div id="content-mutasi" style="display: none; flex-direction: column; flex: 1; overflow-y: auto; background:#fafafa;">
|
|
<div class="sb-head" style="background:#1976d2;"><i class="fa-solid fa-hand-holding-heart"></i> Riwayat Semua Mutasi</div>
|
|
<div id="sb-mutasi-list" style="padding: 10px;">
|
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat mutasi...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- PETA -->
|
|
<div id="map"></div>
|
|
|
|
<!-- Toggle Sidebar -->
|
|
<button id="btn-toggle" onclick="toggleSidebar()"><i class="fa-solid fa-bars"></i></button>
|
|
|
|
</div>
|
|
|
|
<!-- =========================================================== -->
|
|
<!-- MODAL: Form Rumah Ibadah -->
|
|
<!-- =========================================================== -->
|
|
<div class="overlay" id="ovl-ri">
|
|
<div class="modal">
|
|
<h2 id="ri-title"><i class="fa-solid fa-mosque"></i> Tambah Rumah Ibadah</h2>
|
|
<input type="hidden" id="ri-id">
|
|
<div class="fg">
|
|
<label>Nama <span style="color:red">*</span></label>
|
|
<input type="text" id="ri-nama" placeholder="Masjid Al-Ikhlas">
|
|
<div class="err" id="err-ri-nama">Nama wajib diisi.</div>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Jenis <span style="color:red">*</span></label>
|
|
<select id="ri-jenis">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="Masjid">🕌 Masjid</option>
|
|
<option value="Gereja">⛪ Gereja</option>
|
|
<option value="Pura">🛕 Pura</option>
|
|
<option value="Vihara">🏯 Vihara</option>
|
|
<option value="Kelenteng">🏮 Kelenteng</option>
|
|
</select>
|
|
<div class="err" id="err-ri-jenis">Pilih jenis.</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Radius Aktif (m)</label>
|
|
<input type="number" id="ri-radius" min="0" max="1500" value="500">
|
|
</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Alamat <span style="color:red">*</span></label>
|
|
<textarea id="ri-alamat" placeholder="Alamat lengkap..."></textarea>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Narahubung</label>
|
|
<input type="text" id="ri-narahubung" placeholder="Nama & nomor HP">
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Latitude <span style="color:red">*</span></label>
|
|
<input type="number" id="ri-lat" step="any" placeholder="-0.0354">
|
|
</div>
|
|
<div class="fg">
|
|
<label>Longitude <span style="color:red">*</span></label>
|
|
<input type="number" id="ri-lng" step="any" placeholder="109.3424">
|
|
</div>
|
|
</div>
|
|
<div class="err" id="err-ri-coord">Koordinat tidak valid.</div>
|
|
<button class="btn btn-petaclick" id="btn-pc-ri" onclick="aktifKlik('ri')">
|
|
<i class="fa-solid fa-map-pin"></i> Klik Peta untuk Koordinat
|
|
</button>
|
|
<p class="fg-hint">Tutup form lalu klik langsung di peta untuk memilih koordinat.</p>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-gray" onclick="tutup('ovl-ri')">Batal</button>
|
|
<button class="btn btn-green" onclick="simpanRI()"><i class="fa-solid fa-floppy-disk"></i>
|
|
Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =========================================================== -->
|
|
<!-- MODAL: Form Lokasi Bantuan -->
|
|
<!-- =========================================================== -->
|
|
<div class="overlay" id="ovl-lb">
|
|
<div class="modal">
|
|
<h2 id="lb-title"><i class="fa-solid fa-house-chimney-user"></i> Tambah Lokasi Penerima Bantuan</h2>
|
|
<input type="hidden" id="lb-id">
|
|
<div class="fg">
|
|
<label>Nama Kepala Keluarga <span style="color:red">*</span></label>
|
|
<input type="text" id="lb-nama" placeholder="Bapak Suryadi">
|
|
<div class="err" id="err-lb-nama">Nama wajib diisi.</div>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Jml. Anggota</label>
|
|
<input type="number" id="lb-jml" min="1" value="1">
|
|
</div>
|
|
<div class="fg">
|
|
<label>Kategori <span style="color:red">*</span></label>
|
|
<select id="lb-kat">
|
|
<option value="">-- Pilih --</option>
|
|
<option value="Kemiskinan">Kemiskinan</option>
|
|
<option value="Bencana">Bencana</option>
|
|
<option value="Lansia">Lansia</option>
|
|
<option value="Lainnya">Lainnya</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Alamat <span style="color:red">*</span></label>
|
|
<textarea id="lb-alamat" placeholder="Alamat lengkap..."></textarea>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Keterangan</label>
|
|
<textarea id="lb-ket" placeholder="Catatan tambahan..."></textarea>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Latitude <span style="color:red">*</span></label>
|
|
<input type="number" id="lb-lat" step="any" placeholder="-0.0380">
|
|
</div>
|
|
<div class="fg">
|
|
<label>Longitude <span style="color:red">*</span></label>
|
|
<input type="number" id="lb-lng" step="any" placeholder="109.3440">
|
|
</div>
|
|
</div>
|
|
<div class="err" id="err-lb-coord">Koordinat tidak valid.</div>
|
|
<button class="btn btn-petaclick" id="btn-pc-lb" onclick="aktifKlik('lb')">
|
|
<i class="fa-solid fa-map-pin"></i> Klik Peta untuk Koordinat
|
|
</button>
|
|
<p class="fg-hint">Tutup form lalu klik langsung di peta untuk memilih koordinat.</p>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-gray" onclick="tutup('ovl-lb')">Batal</button>
|
|
<button class="btn btn-orange" onclick="simpanLB()"><i class="fa-solid fa-floppy-disk"></i>
|
|
Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =========================================================== -->
|
|
<!-- MODAL: Kelola Sumbangan Rumah Ibadah -->
|
|
<!-- =========================================================== -->
|
|
<div class="overlay" id="ovl-sumbangan">
|
|
<div class="modal modal-lg">
|
|
<h2 id="sumbangan-title"><i class="fa-solid fa-box-open"></i> Kelola Sumbangan</h2>
|
|
|
|
<!-- Form tambah sumbangan -->
|
|
<div style="background:#f8fdf9;border-radius:8px;padding:14px;border:1px solid #ddeedd;margin-bottom:14px">
|
|
<h3><i class="fa-solid fa-plus-circle" style="color:#1a6b3c"></i> Tambah Sumbangan Baru</h3>
|
|
<input type="hidden" id="sb-ri-id">
|
|
<input type="hidden" id="sb-id">
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Jenis Sumbangan <span style="color:red">*</span></label>
|
|
<input type="text" id="sb-jenis" placeholder="Beras, Uang, Pakaian, dll"
|
|
list="jenis-sumbangan-list">
|
|
<datalist id="jenis-sumbangan-list">
|
|
<option value="Beras">
|
|
<option value="Uang">
|
|
<option value="Pakaian">
|
|
<option value="Sembako">
|
|
<option value="Obat-obatan">
|
|
<option value="Peralatan Rumah">
|
|
<option value="Susu Formula">
|
|
</datalist>
|
|
</div>
|
|
<div class="fg" style="max-width:110px">
|
|
<label>Jumlah <span style="color:red">*</span></label>
|
|
<input type="number" id="sb-jumlah" min="0.01" step="any" placeholder="50">
|
|
</div>
|
|
<div class="fg" style="max-width:100px">
|
|
<label>Satuan <span style="color:red">*</span></label>
|
|
<input type="text" id="sb-satuan" placeholder="kg" list="satuan-list">
|
|
<datalist id="satuan-list">
|
|
<option value="kg">
|
|
<option value="Rp">
|
|
<option value="pcs">
|
|
<option value="liter">
|
|
<option value="dus">
|
|
<option value="karung">
|
|
</datalist>
|
|
</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Keterangan</label>
|
|
<input type="text" id="sb-ket" placeholder="Catatan tambahan (opsional)">
|
|
</div>
|
|
<div class="err on" id="err-sb" style="display:none"></div>
|
|
<div style="display:flex;gap:8px">
|
|
<button class="btn btn-green btn-sm" onclick="simpanSumbangan()"><i
|
|
class="fa-solid fa-floppy-disk"></i> Simpan Sumbangan</button>
|
|
<button class="btn btn-gray btn-sm" id="btn-batal-sb" onclick="batalEditSb()"
|
|
style="display:none">Batal Edit</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Daftar stok -->
|
|
<h3><i class="fa-solid fa-warehouse" style="color:#1976d2"></i> Stok Sumbangan Tersedia</h3>
|
|
<div id="tbl-sumbangan-wrap">
|
|
<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn btn-gray" onclick="tutup('ovl-sumbangan')">Tutup</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =========================================================== -->
|
|
<!-- MODAL: Salurkan Bantuan -->
|
|
<!-- =========================================================== -->
|
|
<div class="overlay" id="ovl-penyaluran">
|
|
<div class="modal modal-lg">
|
|
<h2 id="penyaluran-title"><i class="fa-solid fa-hand-holding-heart"></i> Salurkan Bantuan</h2>
|
|
<input type="hidden" id="pny-lb-id">
|
|
|
|
<div id="pny-ri-info"
|
|
style="background:#e8f5e9;border-radius:8px;padding:10px 14px;margin-bottom:14px;font-size:.83rem">
|
|
</div>
|
|
|
|
<div id="pny-form-wrap">
|
|
<div class="fg">
|
|
<label>Pilih Rumah Ibadah Asal <span style="color:red">*</span></label>
|
|
<select id="pny-ri-id">
|
|
<option value="">-- Pilih rumah ibadah --</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Pilih Sumbangan <span style="color:red">*</span></label>
|
|
<select id="pny-sumbangan-id">
|
|
<option value="">-- Pilih jenis sumbangan --</option>
|
|
</select>
|
|
</div>
|
|
<div class="fg-row">
|
|
<div class="fg">
|
|
<label>Jumlah Disalurkan <span style="color:red">*</span></label>
|
|
<input type="number" id="pny-jumlah" min="0.01" step="any" placeholder="0">
|
|
<div class="fg-hint" id="pny-sisa-hint"></div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Tanggal</label>
|
|
<input type="date" id="pny-tanggal">
|
|
</div>
|
|
</div>
|
|
<div class="fg">
|
|
<label>Keterangan</label>
|
|
<textarea id="pny-ket" placeholder="Catatan penyaluran..."></textarea>
|
|
</div>
|
|
<div class="err on" id="err-pny" style="display:none"></div>
|
|
</div>
|
|
<div id="pny-no-stok" style="display:none;text-align:center;padding:20px;color:#888">
|
|
<i class="fa-solid fa-box-open fa-2x" style="margin-bottom:8px;display:block;color:#ccc"></i>
|
|
Tidak ada stok sumbangan yang tersedia di rumah ibadah terdekat.
|
|
</div>
|
|
|
|
<!-- Riwayat penyaluran -->
|
|
<div class="popup-section">
|
|
<h3><i class="fa-solid fa-clock-rotate-left" style="color:#777"></i> Riwayat Penyaluran ke Keluarga Ini
|
|
</h3>
|
|
<div id="pny-riwayat">
|
|
<div class="empty" style="padding:10px">Belum ada riwayat.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-footer">
|
|
<button class="btn btn-gray" onclick="tutup('ovl-penyaluran')">Tutup</button>
|
|
<button class="btn btn-teal" id="btn-salurkan" onclick="simpanPenyaluran()">
|
|
<i class="fa-solid fa-paper-plane"></i> Salurkan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MODAL: Konfirmasi Hapus -->
|
|
<div class="overlay" id="ovl-hapus">
|
|
<div class="modal" style="max-width:340px">
|
|
<div class="confirm-box">
|
|
<i class="fa-solid fa-triangle-exclamation fa-2x" style="color:#e08020;margin-bottom:10px"></i>
|
|
<p id="hapus-teks">Apakah Anda yakin ingin menghapus data ini?</p>
|
|
<div class="modal-footer" style="justify-content:center">
|
|
<button class="btn btn-gray" onclick="tutup('ovl-hapus')">Batal</button>
|
|
<button class="btn btn-red" id="btn-hapus-ok"><i class="fa-solid fa-trash"></i> Hapus</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TOAST -->
|
|
<div id="toast"></div>
|
|
|
|
<!-- Leaflet JS -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
|
|
<script>
|
|
// ============================================================
|
|
// CONFIG
|
|
// ============================================================
|
|
const API = 'api.php';
|
|
let map, radiusAktif = 500;
|
|
let layerRI = {}; // { id: { marker, circle } }
|
|
let layerLB = {}; // { id: circleMarker }
|
|
let dataRI = [];
|
|
let dataLB = [];
|
|
let modeKlik = null; // 'ri' | 'lb'
|
|
let hapusPending = null;
|
|
let sumbanganCache = {}; // { sumbangan_id: { jenis, satuan, sisa } }
|
|
|
|
// ============================================================
|
|
// INIT
|
|
// ============================================================
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
await setupDB();
|
|
initMap();
|
|
await muatSemua();
|
|
});
|
|
|
|
async function setupDB() {
|
|
try { await get(`${API}?resource=setup`); } catch (e) { }
|
|
}
|
|
|
|
// ============================================================
|
|
// MAP
|
|
// ============================================================
|
|
function initMap() {
|
|
map = L.map('map').setView([-0.02, 109.34], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
// Legenda
|
|
const leg = L.control({ position: 'bottomright' });
|
|
leg.onAdd = () => {
|
|
const d = L.DomUtil.create('div', 'legenda');
|
|
d.innerHTML = `
|
|
<h4><i class="fa-solid fa-circle-info"></i> Keterangan</h4>
|
|
<div class="leg-row"><span class="dot" style="background:#1565c0"></span> Rumah Ibadah</div>
|
|
<div class="leg-row"><span class="dot" style="background:#2e7d32"></span> Dalam Jangkauan</div>
|
|
<div class="leg-row"><span class="dot" style="background:#d93025"></span> Luar Jangkauan</div>`;
|
|
return d;
|
|
};
|
|
leg.addTo(map);
|
|
|
|
// Klik peta untuk koordinat
|
|
map.on('click', e => {
|
|
if (!modeKlik) return;
|
|
const { lat, lng } = e.latlng;
|
|
if (modeKlik === 'ri') {
|
|
document.getElementById('ri-lat').value = lat.toFixed(7);
|
|
document.getElementById('ri-lng').value = lng.toFixed(7);
|
|
buka('ovl-ri');
|
|
} else {
|
|
document.getElementById('lb-lat').value = lat.toFixed(7);
|
|
document.getElementById('lb-lng').value = lng.toFixed(7);
|
|
buka('ovl-lb');
|
|
}
|
|
nonaktifKlik();
|
|
toast('Koordinat dipilih dari peta.', 'ok');
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// LOAD DATA
|
|
// ============================================================
|
|
async function muatSemua() {
|
|
await Promise.all([muatRI(), muatLB()]);
|
|
}
|
|
|
|
async function muatRI() {
|
|
try {
|
|
dataRI = await get(`${API}?resource=rumah_ibadah&action=list`);
|
|
renderRI(dataRI);
|
|
renderSidebar(dataRI);
|
|
} catch (e) { toast('Gagal memuat rumah ibadah.', 'err'); }
|
|
}
|
|
|
|
async function muatLB() {
|
|
try {
|
|
dataLB = await get(`${API}?resource=lokasi_bantuan&action=list&radius=${radiusAktif}`);
|
|
renderLB(dataLB);
|
|
} catch (e) { toast('Gagal memuat lokasi bantuan.', 'err'); }
|
|
}
|
|
|
|
// ============================================================
|
|
// HAVERSINE — hitung jarak (meter) antara dua koordinat
|
|
// ============================================================
|
|
function haversine(lat1, lng1, lat2, lng2) {
|
|
const R = 6371000;
|
|
const toRad = d => d * Math.PI / 180;
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLng = toRad(lng2 - lng1);
|
|
const a = Math.sin(dLat / 2) ** 2 +
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2;
|
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
}
|
|
|
|
// Cek apakah lb dalam radius menggunakan dataRI saat ini
|
|
function dalamJangkauan(lb) {
|
|
if (!dataRI.length) return false;
|
|
return dataRI.some(ri => {
|
|
const jarak = haversine(
|
|
parseFloat(lb.latitude), parseFloat(lb.longitude),
|
|
parseFloat(ri.latitude), parseFloat(ri.longitude)
|
|
);
|
|
return jarak <= radiusAktif;
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDER RUMAH IBADAH
|
|
// ============================================================
|
|
const WARNA_RI = { Masjid: '#1565c0', Gereja: '#6a1b9a', Pura: '#e65100', Vihara: '#c62828', Kelenteng: '#ad1457' };
|
|
const IKON_RI = { Masjid: '🕌', Gereja: '⛪', Pura: '🛕', Vihara: '🏯', Kelenteng: '🏮' };
|
|
|
|
function ikonRI(jenis) {
|
|
const w = WARNA_RI[jenis] || '#333';
|
|
return L.divIcon({
|
|
html: `<div style="background:${w};width:26px;height:26px;border-radius:50% 50% 50% 0;
|
|
transform:rotate(-45deg);border:3px solid #fff;box-shadow:0 2px 6px rgba(0,0,0,.4)"></div>`,
|
|
className: '', iconSize: [26, 26], iconAnchor: [13, 26], popupAnchor: [0, -30]
|
|
});
|
|
}
|
|
|
|
function renderRI(list) {
|
|
Object.values(layerRI).forEach(l => { map.removeLayer(l.marker); if (l.circle) map.removeLayer(l.circle); });
|
|
layerRI = {};
|
|
|
|
list.forEach(ri => {
|
|
const m = L.marker([ri.latitude, ri.longitude], { icon: ikonRI(ri.jenis) }).addTo(map);
|
|
const c = L.circle([ri.latitude, ri.longitude], {
|
|
radius: radiusAktif,
|
|
color: '#1976d2', fillColor: '#1976d2', fillOpacity: .06, weight: 1.5, dashArray: '5,5'
|
|
}).addTo(map);
|
|
|
|
m.bindPopup(() => buatPopupRI(ri), { maxWidth: 300 });
|
|
m.on('popupopen', () => isiJumlahPopupRI(ri.id));
|
|
layerRI[ri.id] = { marker: m, circle: c };
|
|
});
|
|
}
|
|
|
|
function buatPopupRI(ri) {
|
|
const w = WARNA_RI[ri.jenis] || '#333';
|
|
const el = document.createElement('div');
|
|
el.style.cssText = 'font-size:.84rem;min-width:230px';
|
|
el.innerHTML = `
|
|
<strong style="color:${w};font-size:.95rem">${ri.nama}</strong><br>
|
|
<span style="color:#888">${ri.jenis}</span><br><br>
|
|
<i class="fa-solid fa-location-dot" style="color:#d93025"></i> ${ri.alamat}<br>
|
|
<i class="fa-solid fa-phone" style="color:#777"></i> ${ri.narahubung || '-'}<br>
|
|
<i class="fa-solid fa-circle-dot" style="color:#1976d2"></i> Radius: <b>${ri.radius_aktif} m</b><br>
|
|
<span id="ri-jml-${ri.id}" style="color:#2e7d32"><i class="fa-solid fa-spinner fa-spin"></i></span>
|
|
<hr style="margin:7px 0">
|
|
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
<button class="btn btn-blue btn-sm" onclick="editRI(${ri.id})"><i class="fa-solid fa-pen"></i> Edit</button>
|
|
<button class="btn btn-purple btn-sm" onclick="bukaKelolaSumbangan(${ri.id},'${esc(ri.nama)}')">
|
|
<i class="fa-solid fa-box-open"></i> Sumbangan
|
|
</button>
|
|
<button class="btn btn-red btn-sm" onclick="konfirmasiHapus('ri',${ri.id},'${esc(ri.nama)}')"><i class="fa-solid fa-trash"></i></button>
|
|
</div>`;
|
|
return el;
|
|
}
|
|
|
|
async function isiJumlahPopupRI(id) {
|
|
try {
|
|
const d = await get(`${API}?resource=rumah_ibadah&action=detail&id=${id}`);
|
|
const el = document.getElementById(`ri-jml-${id}`);
|
|
if (el) el.innerHTML = `<i class="fa-solid fa-users"></i> Dalam Radius: <b>${d.jumlah_dalam_radius} keluarga</b>`;
|
|
} catch { }
|
|
}
|
|
|
|
// ============================================================
|
|
// RENDER LOKASI BANTUAN
|
|
// ============================================================
|
|
function renderLB(list) {
|
|
Object.values(layerLB).forEach(l => map.removeLayer(l));
|
|
layerLB = {};
|
|
|
|
list.forEach(lb => {
|
|
// Gunakan haversine client-side untuk warna real-time
|
|
const didalam = dalamJangkauan(lb);
|
|
const w = didalam ? '#2e7d32' : '#d93025';
|
|
const m = L.circleMarker([lb.latitude, lb.longitude], {
|
|
radius: 9, color: w, fillColor: w, fillOpacity: .75, weight: 2.5
|
|
}).addTo(map);
|
|
|
|
// Simpan referensi data ke marker untuk update real-time
|
|
m._lbData = lb;
|
|
m.bindPopup(() => buatPopupLB(lb, didalam), { maxWidth: 300 });
|
|
layerLB[lb.id] = m;
|
|
});
|
|
}
|
|
|
|
// Update warna marker LB berdasarkan radiusAktif saat ini (tanpa API)
|
|
function updateWarnLB() {
|
|
Object.values(layerLB).forEach(m => {
|
|
const lb = m._lbData;
|
|
if (!lb) return;
|
|
const didalam = dalamJangkauan(lb);
|
|
const w = didalam ? '#2e7d32' : '#d93025';
|
|
m.setStyle({ color: w, fillColor: w });
|
|
// Rebind popup agar status di popup juga ikut update
|
|
m.unbindPopup();
|
|
m.bindPopup(() => buatPopupLB(lb, didalam), { maxWidth: 300 });
|
|
});
|
|
}
|
|
|
|
function buatPopupLB(lb, didalam) {
|
|
const statusHtml = didalam
|
|
? '<span style="color:#2e7d32;font-weight:700">● Dalam Jangkauan</span>'
|
|
: '<span style="color:#d93025;font-weight:700">● Di Luar Jangkauan</span>';
|
|
|
|
const jarak = lb.jarak_meter != null
|
|
? `${lb.jarak_meter} m ke <em>${lb.nama_ri || '-'}</em>`
|
|
: '-';
|
|
|
|
const tombolSalur = didalam
|
|
? `<button class="btn btn-teal btn-sm" onclick="bukaPenyaluran(${lb.id},'${esc(lb.nama_kk)}')">
|
|
<i class="fa-solid fa-hand-holding-heart"></i> Salurkan Bantuan
|
|
</button>`
|
|
: `<button class="btn btn-gray btn-sm" disabled title="Hanya tersedia jika dalam jangkauan">
|
|
<i class="fa-solid fa-hand-holding-heart"></i> Salurkan Bantuan
|
|
</button>`;
|
|
|
|
return `
|
|
<div style="font-size:.84rem;min-width:220px">
|
|
<strong style="font-size:.93rem">${lb.nama_kk}</strong><br>
|
|
<span style="color:#888">${lb.kategori} — ${lb.jml_anggota} anggota</span><br><br>
|
|
<i class="fa-solid fa-location-dot" style="color:#d93025"></i> ${lb.alamat}<br>
|
|
<i class="fa-solid fa-ruler" style="color:#777"></i> Jarak: <b>${jarak}</b><br>
|
|
Status: ${statusHtml}
|
|
${lb.keterangan ? `<br><em style="color:#aaa;font-size:.75rem">${lb.keterangan}</em>` : ''}
|
|
<hr style="margin:7px 0">
|
|
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
<button class="btn btn-blue btn-sm" onclick="editLB(${lb.id})"><i class="fa-solid fa-pen"></i> Edit</button>
|
|
${tombolSalur}
|
|
<button class="btn btn-red btn-sm" onclick="konfirmasiHapus('lb',${lb.id},'${esc(lb.nama_kk)}')"><i class="fa-solid fa-trash"></i></button>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ============================================================
|
|
// SIDEBAR
|
|
// ============================================================
|
|
function renderSidebar(list) {
|
|
const el = document.getElementById('sb-list');
|
|
if (!list.length) { el.innerHTML = '<div class="empty">Belum ada data rumah ibadah.</div>'; return; }
|
|
el.innerHTML = list.map(ri => `
|
|
<div class="list-item" onclick="flyKeRI(${ri.id})">
|
|
<span class="ico">${IKON_RI[ri.jenis] || '🏛️'}</span>
|
|
<div class="inf">
|
|
<div class="nm">${ri.nama}</div>
|
|
<div class="jn">${ri.jenis}</div>
|
|
</div>
|
|
<span class="badge">${ri.radius_aktif}m</span>
|
|
</div>`).join('');
|
|
}
|
|
|
|
function filterList(q) {
|
|
const f = q.toLowerCase();
|
|
renderSidebar(dataRI.filter(r => r.nama.toLowerCase().includes(f) || r.jenis.toLowerCase().includes(f)));
|
|
}
|
|
|
|
function flyKeRI(id) {
|
|
const ri = dataRI.find(r => r.id == id);
|
|
if (!ri) { toast('Lokasi tidak dapat ditampilkan.', 'err'); return; }
|
|
map.flyTo([ri.latitude, ri.longitude], 16, { duration: 1.2 });
|
|
setTimeout(() => { if (layerRI[id]) layerRI[id].marker.openPopup(); }, 1300);
|
|
}
|
|
|
|
function toggleSidebar() { document.getElementById('sidebar').classList.toggle('hide'); }
|
|
|
|
// ============================================================
|
|
// RADIUS SLIDER — update real-time tanpa API call
|
|
// ============================================================
|
|
document.getElementById('radius-slider').addEventListener('input', function () {
|
|
radiusAktif = +this.value;
|
|
document.getElementById('radius-val').textContent = radiusAktif + ' m';
|
|
|
|
// Update lingkaran radius di peta
|
|
Object.keys(layerRI).forEach(id => {
|
|
if (layerRI[id].circle) layerRI[id].circle.setRadius(radiusAktif);
|
|
});
|
|
|
|
// Update warna marker LB secara real-time (client-side)
|
|
updateWarnLB();
|
|
});
|
|
|
|
// Saat slider dilepas, sync ulang dari server untuk pastikan akurasi
|
|
document.getElementById('radius-slider').addEventListener('change', function () {
|
|
muatLB();
|
|
});
|
|
|
|
// ============================================================
|
|
// FORM RI
|
|
// ============================================================
|
|
function bukaFormRI() {
|
|
set('ri-title', '<i class="fa-solid fa-mosque"></i> Tambah Rumah Ibadah');
|
|
['ri-id', 'ri-nama', 'ri-alamat', 'ri-narahubung', 'ri-lat', 'ri-lng'].forEach(id => set(id, ''));
|
|
set('ri-jenis', ''); set('ri-radius', 500);
|
|
clearErr(['err-ri-nama', 'err-ri-jenis', 'err-ri-coord']);
|
|
buka('ovl-ri');
|
|
}
|
|
|
|
async function editRI(id) {
|
|
const ri = dataRI.find(r => r.id == id);
|
|
if (!ri) return;
|
|
set('ri-title', '<i class="fa-solid fa-pen"></i> Edit Rumah Ibadah');
|
|
set('ri-id', ri.id);
|
|
set('ri-nama', ri.nama);
|
|
set('ri-jenis', ri.jenis);
|
|
set('ri-alamat', ri.alamat);
|
|
set('ri-narahubung', ri.narahubung || '');
|
|
set('ri-lat', ri.latitude);
|
|
set('ri-lng', ri.longitude);
|
|
set('ri-radius', ri.radius_aktif);
|
|
clearErr(['err-ri-nama', 'err-ri-jenis', 'err-ri-coord']);
|
|
buka('ovl-ri');
|
|
}
|
|
|
|
async function simpanRI() {
|
|
const nama = val('ri-nama').trim();
|
|
const jenis = val('ri-jenis');
|
|
const lat = parseFloat(val('ri-lat'));
|
|
const lng = parseFloat(val('ri-lng'));
|
|
let ok = true;
|
|
|
|
if (!nama) { showErr('err-ri-nama'); ok = false; } else hideErr('err-ri-nama');
|
|
if (!jenis) { showErr('err-ri-jenis'); ok = false; } else hideErr('err-ri-jenis');
|
|
if (isNaN(lat) || isNaN(lng)) { showErr('err-ri-coord'); ok = false; } else hideErr('err-ri-coord');
|
|
if (!ok) return;
|
|
|
|
const id = val('ri-id');
|
|
const payload = {
|
|
nama, jenis, lat, lng,
|
|
alamat: val('ri-alamat'),
|
|
narahubung: val('ri-narahubung'),
|
|
latitude: lat,
|
|
longitude: lng,
|
|
radius_aktif: parseInt(val('ri-radius')) || 500
|
|
};
|
|
const url = id
|
|
? `${API}?resource=rumah_ibadah&action=update&id=${id}`
|
|
: `${API}?resource=rumah_ibadah&action=create`;
|
|
|
|
try {
|
|
const res = await post(url, payload, id ? 'PUT' : 'POST');
|
|
toast(res.message, 'ok');
|
|
tutup('ovl-ri');
|
|
await muatRI(); await muatLB();
|
|
} catch (e) { toast(e.message || 'Gagal menyimpan.', 'err'); }
|
|
}
|
|
|
|
// ============================================================
|
|
// FORM LB
|
|
// ============================================================
|
|
function bukaFormLB() {
|
|
set('lb-title', '<i class="fa-solid fa-house-chimney-user"></i> Tambah Lokasi Penerima Bantuan');
|
|
['lb-id', 'lb-nama', 'lb-alamat', 'lb-ket', 'lb-lat', 'lb-lng'].forEach(id => set(id, ''));
|
|
set('lb-kat', ''); set('lb-jml', 1);
|
|
clearErr(['err-lb-nama', 'err-lb-coord']);
|
|
buka('ovl-lb');
|
|
}
|
|
|
|
async function editLB(id) {
|
|
const lb = dataLB.find(l => l.id == id);
|
|
if (!lb) return;
|
|
set('lb-title', '<i class="fa-solid fa-pen"></i> Edit Lokasi Penerima Bantuan');
|
|
set('lb-id', lb.id);
|
|
set('lb-nama', lb.nama_kk);
|
|
set('lb-jml', lb.jml_anggota);
|
|
set('lb-kat', lb.kategori);
|
|
set('lb-alamat', lb.alamat);
|
|
set('lb-ket', lb.keterangan || '');
|
|
set('lb-lat', lb.latitude);
|
|
set('lb-lng', lb.longitude);
|
|
clearErr(['err-lb-nama', 'err-lb-coord']);
|
|
buka('ovl-lb');
|
|
}
|
|
|
|
async function simpanLB() {
|
|
const nama = val('lb-nama').trim();
|
|
const lat = parseFloat(val('lb-lat'));
|
|
const lng = parseFloat(val('lb-lng'));
|
|
let ok = true;
|
|
|
|
if (!nama) { showErr('err-lb-nama'); ok = false; } else hideErr('err-lb-nama');
|
|
if (isNaN(lat) || isNaN(lng)) { showErr('err-lb-coord'); ok = false; } else hideErr('err-lb-coord');
|
|
if (!ok) return;
|
|
|
|
const id = val('lb-id');
|
|
const payload = {
|
|
nama_kk: nama,
|
|
jml_anggota: parseInt(val('lb-jml')) || 1,
|
|
kategori: val('lb-kat'),
|
|
alamat: val('lb-alamat'),
|
|
keterangan: val('lb-ket'),
|
|
latitude: lat,
|
|
longitude: lng
|
|
};
|
|
const url = id
|
|
? `${API}?resource=lokasi_bantuan&action=update&id=${id}`
|
|
: `${API}?resource=lokasi_bantuan&action=create`;
|
|
|
|
try {
|
|
const res = await post(url, payload, id ? 'PUT' : 'POST');
|
|
toast(res.message, 'ok');
|
|
tutup('ovl-lb');
|
|
await muatLB();
|
|
} catch (e) { toast(e.message || 'Gagal menyimpan.', 'err'); }
|
|
}
|
|
|
|
// ============================================================
|
|
// KELOLA SUMBANGAN (Rumah Ibadah)
|
|
// ============================================================
|
|
async function bukaKelolaSumbangan(riId, riNama) {
|
|
document.getElementById('sumbangan-title').innerHTML =
|
|
`<i class="fa-solid fa-box-open"></i> Sumbangan — ${riNama}`;
|
|
set('sb-ri-id', riId);
|
|
set('sb-id', '');
|
|
set('sb-jenis', '');
|
|
set('sb-jumlah', '');
|
|
set('sb-satuan', '');
|
|
set('sb-ket', '');
|
|
sembunyikanErr('err-sb');
|
|
document.getElementById('btn-batal-sb').style.display = 'none';
|
|
buka('ovl-sumbangan');
|
|
await muatDaftarSumbangan(riId);
|
|
}
|
|
|
|
async function muatDaftarSumbangan(riId) {
|
|
const wrap = document.getElementById('tbl-sumbangan-wrap');
|
|
wrap.innerHTML = '<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat...</div>';
|
|
try {
|
|
const list = await get(`${API}?resource=sumbangan&action=list&rumah_ibadah_id=${riId}`);
|
|
if (!list.length) {
|
|
wrap.innerHTML = '<div class="empty">Belum ada sumbangan. Tambahkan di atas.</div>';
|
|
return;
|
|
}
|
|
wrap.innerHTML = `
|
|
<table class="tbl">
|
|
<thead>
|
|
<tr>
|
|
<th>Jenis</th>
|
|
<th>Jumlah Awal</th>
|
|
<th>Tersalur</th>
|
|
<th>Sisa</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${list.map(s => {
|
|
const pct = s.jumlah > 0 ? (s.sisa / s.jumlah * 100) : 0;
|
|
const cls = pct <= 0 ? 'empty' : pct < 30 ? 'low' : 'ok';
|
|
const barCls = pct <= 0 ? 'empty' : pct < 30 ? 'low' : '';
|
|
return `<tr>
|
|
<td><b>${s.jenis}</b><br><small style="color:#999">${s.keterangan || ''}</small></td>
|
|
<td>${fmt(s.jumlah)} ${s.satuan}</td>
|
|
<td>${fmt(s.total_disalurkan)} ${s.satuan}</td>
|
|
<td>
|
|
<span class="badge-stok ${cls}">${fmt(s.sisa)} ${s.satuan}</span>
|
|
<div class="stok-bar"><div class="stok-bar-inner ${barCls}" style="width:${pct}%"></div></div>
|
|
</td>
|
|
<td>
|
|
<div style="display:flex;gap:3px">
|
|
<button class="btn btn-blue btn-xs" onclick="editSumbangan(${s.id},'${esc(s.jenis)}',${s.jumlah},'${esc(s.satuan)}','${esc(s.keterangan || '')}')">
|
|
<i class="fa-solid fa-pen"></i>
|
|
</button>
|
|
<button class="btn btn-red btn-xs" onclick="hapusSumbangan(${s.id},'${esc(s.jenis)}')">
|
|
<i class="fa-solid fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
}).join('')}
|
|
</tbody>
|
|
</table>`;
|
|
} catch (e) {
|
|
wrap.innerHTML = `<div class="empty" style="color:#d93025">${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
function editSumbangan(id, jenis, jumlah, satuan, ket) {
|
|
set('sb-id', id);
|
|
set('sb-jenis', jenis);
|
|
set('sb-jumlah', jumlah);
|
|
set('sb-satuan', satuan);
|
|
set('sb-ket', ket);
|
|
document.getElementById('btn-batal-sb').style.display = '';
|
|
document.getElementById('sb-jenis').focus();
|
|
}
|
|
|
|
function batalEditSb() {
|
|
set('sb-id', '');
|
|
set('sb-jenis', '');
|
|
set('sb-jumlah', '');
|
|
set('sb-satuan', '');
|
|
set('sb-ket', '');
|
|
document.getElementById('btn-batal-sb').style.display = 'none';
|
|
sembunyikanErr('err-sb');
|
|
}
|
|
|
|
async function simpanSumbangan() {
|
|
const riId = val('sb-ri-id');
|
|
const sbId = val('sb-id');
|
|
const jenis = val('sb-jenis').trim();
|
|
const jumlah = parseFloat(val('sb-jumlah'));
|
|
const satuan = val('sb-satuan').trim();
|
|
|
|
if (!jenis || isNaN(jumlah) || jumlah <= 0 || !satuan) {
|
|
tampilkanErr('err-sb', 'Jenis, jumlah (> 0), dan satuan wajib diisi.');
|
|
return;
|
|
}
|
|
sembunyikanErr('err-sb');
|
|
|
|
const payload = {
|
|
rumah_ibadah_id: parseInt(riId),
|
|
jenis, jumlah, satuan,
|
|
keterangan: val('sb-ket')
|
|
};
|
|
const url = sbId
|
|
? `${API}?resource=sumbangan&action=update&id=${sbId}`
|
|
: `${API}?resource=sumbangan&action=create`;
|
|
try {
|
|
const res = await post(url, payload, sbId ? 'PUT' : 'POST');
|
|
toast(res.message, 'ok');
|
|
batalEditSb();
|
|
await muatDaftarSumbangan(riId);
|
|
} catch (e) { tampilkanErr('err-sb', e.message); }
|
|
}
|
|
|
|
async function hapusSumbangan(id, nama) {
|
|
if (!confirm(`Hapus sumbangan "${nama}"?`)) return;
|
|
try {
|
|
await post(`${API}?resource=sumbangan&action=delete&id=${id}`, {}, 'DELETE');
|
|
toast('Sumbangan dihapus.', 'ok');
|
|
await muatDaftarSumbangan(val('sb-ri-id'));
|
|
} catch (e) { toast(e.message, 'err'); }
|
|
}
|
|
|
|
// ============================================================
|
|
// PENYALURAN BANTUAN
|
|
// ============================================================
|
|
async function bukaPenyaluran(lbId, lbNama) {
|
|
document.getElementById('penyaluran-title').innerHTML =
|
|
`<i class="fa-solid fa-hand-holding-heart"></i> Salurkan Bantuan — ${lbNama}`;
|
|
set('pny-lb-id', lbId);
|
|
set('pny-jumlah', '');
|
|
set('pny-ket', '');
|
|
set('pny-tanggal', new Date().toISOString().split('T')[0]);
|
|
sembunyikanErr('err-pny');
|
|
document.getElementById('pny-sisa-hint').textContent = '';
|
|
document.getElementById('pny-riwayat').innerHTML =
|
|
'<div class="empty" style="padding:10px"><i class="fa-solid fa-spinner fa-spin"></i></div>';
|
|
document.getElementById('pny-form-wrap').style.display = '';
|
|
document.getElementById('pny-no-stok').style.display = 'none';
|
|
document.getElementById('btn-salurkan').style.display = '';
|
|
buka('ovl-penyaluran');
|
|
|
|
// Muat data tersedia & riwayat paralel
|
|
await Promise.all([
|
|
muatSumbanganTersedia(lbId),
|
|
muatRiwayatPenyaluran(lbId)
|
|
]);
|
|
}
|
|
|
|
let riTersediaData = [];
|
|
|
|
async function muatSumbanganTersedia(lbId) {
|
|
const selRI = document.getElementById('pny-ri-id');
|
|
const selSb = document.getElementById('pny-sumbangan-id');
|
|
selRI.innerHTML = '<option value="">Memuat...</option>';
|
|
selSb.innerHTML = '<option value="">-- Pilih jenis sumbangan --</option>';
|
|
sumbanganCache = {};
|
|
riTersediaData = [];
|
|
|
|
try {
|
|
const data = await get(`${API}?resource=penyaluran&action=tersedia&lokasi_bantuan_id=${lbId}&radius=${radiusAktif}`);
|
|
riTersediaData = data;
|
|
|
|
document.getElementById('pny-ri-info').innerHTML =
|
|
`<i class="fa-solid fa-mosque" style="color:#1a6b3c"></i>
|
|
Ditemukan <b>${data.length}</b> Rumah Ibadah dalam jangkauan (${radiusAktif} m)`;
|
|
|
|
selRI.innerHTML = '<option value="">-- Pilih rumah ibadah asal --</option>' +
|
|
data.map((ri, i) => `<option value="${i}">${ri.rumah_ibadah.nama} (Jarak: ${ri.jarak_meter} m)</option>`).join('');
|
|
|
|
document.getElementById('pny-form-wrap').style.display = '';
|
|
document.getElementById('pny-no-stok').style.display = 'none';
|
|
document.getElementById('btn-salurkan').style.display = '';
|
|
} catch (e) {
|
|
document.getElementById('pny-ri-info').innerHTML =
|
|
`<span style="color:#d93025"><i class="fa-solid fa-circle-exclamation"></i> ${e.message}</span>`;
|
|
document.getElementById('pny-form-wrap').style.display = 'none';
|
|
document.getElementById('btn-salurkan').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
document.getElementById('pny-ri-id').addEventListener('change', function () {
|
|
const idx = this.value;
|
|
const selSb = document.getElementById('pny-sumbangan-id');
|
|
const hint = document.getElementById('pny-sisa-hint');
|
|
hint.textContent = '';
|
|
document.getElementById('pny-jumlah').value = '';
|
|
sumbanganCache = {};
|
|
|
|
if (idx === "") {
|
|
selSb.innerHTML = '<option value="">-- Pilih jenis sumbangan --</option>';
|
|
return;
|
|
}
|
|
|
|
const riData = riTersediaData[idx];
|
|
if (!riData.sumbangan.length) {
|
|
selSb.innerHTML = '<option value="">Tidak ada stok tersedia</option>';
|
|
return;
|
|
}
|
|
|
|
selSb.innerHTML = '<option value="">-- Pilih jenis sumbangan --</option>' +
|
|
riData.sumbangan.map(s => {
|
|
sumbanganCache[s.id] = s;
|
|
return `<option value="${s.id}">${s.jenis} (Sisa: ${fmt(s.sisa)} ${s.satuan})</option>`;
|
|
}).join('');
|
|
});
|
|
|
|
document.getElementById('pny-sumbangan-id').addEventListener('change', function () {
|
|
const s = sumbanganCache[this.value];
|
|
const hint = document.getElementById('pny-sisa-hint');
|
|
if (s) {
|
|
hint.textContent = `Stok tersedia: ${fmt(s.sisa)} ${s.satuan}`;
|
|
document.getElementById('pny-jumlah').max = s.sisa;
|
|
} else {
|
|
hint.textContent = '';
|
|
}
|
|
});
|
|
|
|
async function muatRiwayatPenyaluran(lbId) {
|
|
const wrap = document.getElementById('pny-riwayat');
|
|
try {
|
|
const list = await get(`${API}?resource=penyaluran&action=list&lokasi_bantuan_id=${lbId}`);
|
|
if (!list.length) {
|
|
wrap.innerHTML = '<div class="empty" style="padding:10px">Belum ada riwayat penyaluran.</div>';
|
|
return;
|
|
}
|
|
wrap.innerHTML = list.map(p => `
|
|
<div class="riwayat-item">
|
|
<b>${fmt(p.jumlah_disalurkan)} ${p.satuan}</b> ${p.jenis_sumbangan}
|
|
dari <em>${p.nama_ri}</em>
|
|
<span style="color:#aaa;margin-left:6px">${p.tanggal}</span>
|
|
${p.keterangan ? `<br><span style="color:#999">${p.keterangan}</span>` : ''}
|
|
</div>`).join('');
|
|
} catch {
|
|
wrap.innerHTML = '<div class="empty" style="padding:10px;color:#d93025">Gagal memuat riwayat.</div>';
|
|
}
|
|
}
|
|
|
|
async function simpanPenyaluran() {
|
|
const lbId = val('pny-lb-id');
|
|
const sbId = val('pny-sumbangan-id');
|
|
const jumlah = parseFloat(val('pny-jumlah'));
|
|
const tanggal = val('pny-tanggal');
|
|
|
|
if (!sbId) { tampilkanErr('err-pny', 'Pilih jenis sumbangan.'); return; }
|
|
if (isNaN(jumlah) || jumlah <= 0) { tampilkanErr('err-pny', 'Jumlah harus lebih dari 0.'); return; }
|
|
|
|
const s = sumbanganCache[sbId];
|
|
if (s && jumlah > s.sisa) {
|
|
tampilkanErr('err-pny', `Jumlah melebihi stok. Sisa: ${fmt(s.sisa)} ${s.satuan}.`);
|
|
return;
|
|
}
|
|
sembunyikanErr('err-pny');
|
|
|
|
try {
|
|
const res = await post(`${API}?resource=penyaluran&action=create`, {
|
|
sumbangan_id: parseInt(sbId),
|
|
lokasi_bantuan_id: parseInt(lbId),
|
|
jumlah_disalurkan: jumlah,
|
|
tanggal: tanggal,
|
|
keterangan: val('pny-ket')
|
|
});
|
|
toast(res.message, 'ok');
|
|
set('pny-jumlah', '');
|
|
set('pny-ket', '');
|
|
// Reload stok & riwayat
|
|
await Promise.all([
|
|
muatSumbanganTersedia(lbId),
|
|
muatRiwayatPenyaluran(lbId)
|
|
]);
|
|
} catch (e) { tampilkanErr('err-pny', e.message); }
|
|
}
|
|
|
|
// ============================================================
|
|
// HAPUS
|
|
// ============================================================
|
|
function konfirmasiHapus(tipe, id, nama) {
|
|
document.getElementById('hapus-teks').textContent =
|
|
`Apakah Anda yakin ingin menghapus "${nama}"? Tindakan ini tidak dapat dibatalkan.`;
|
|
hapusPending = { tipe, id };
|
|
buka('ovl-hapus');
|
|
}
|
|
|
|
document.getElementById('btn-hapus-ok').addEventListener('click', async () => {
|
|
if (!hapusPending) return;
|
|
const { tipe, id } = hapusPending;
|
|
const url = `${API}?resource=${tipe === 'ri' ? 'rumah_ibadah' : 'lokasi_bantuan'}&action=delete&id=${id}`;
|
|
try {
|
|
const res = await post(url, {}, 'DELETE');
|
|
toast(res.message, 'ok');
|
|
tutup('ovl-hapus');
|
|
if (tipe === 'ri') await muatRI();
|
|
await muatLB();
|
|
} catch (e) { toast('Gagal menghapus.', 'err'); }
|
|
hapusPending = null;
|
|
});
|
|
|
|
// ============================================================
|
|
// KLIK PETA KOORDINAT
|
|
// ============================================================
|
|
function aktifKlik(mode) {
|
|
modeKlik = mode;
|
|
const btn = document.getElementById(`btn-pc-${mode}`);
|
|
btn.classList.add('active');
|
|
btn.innerHTML = '<i class="fa-solid fa-crosshairs fa-beat"></i> Klik pada peta sekarang...';
|
|
tutup(mode === 'ri' ? 'ovl-ri' : 'ovl-lb');
|
|
toast('Klik pada peta untuk memilih koordinat.', 'ok');
|
|
}
|
|
|
|
function nonaktifKlik() {
|
|
if (!modeKlik) return;
|
|
const btn = document.getElementById(`btn-pc-${modeKlik}`);
|
|
if (btn) { btn.classList.remove('active'); btn.innerHTML = '<i class="fa-solid fa-map-pin"></i> Klik Peta untuk Koordinat'; }
|
|
modeKlik = null;
|
|
}
|
|
|
|
// ============================================================
|
|
// HELPERS UI
|
|
// ============================================================
|
|
function buka(id) { document.getElementById(id).classList.add('on'); }
|
|
function tutup(id) { document.getElementById(id).classList.remove('on'); }
|
|
function val(id) { return document.getElementById(id).value; }
|
|
function set(id, v) { const el = document.getElementById(id); if (el) el.tagName === 'H2' ? el.innerHTML = v : el.value = v; }
|
|
function showErr(id) { document.getElementById(id).classList.add('on'); }
|
|
function hideErr(id) { document.getElementById(id).classList.remove('on'); }
|
|
function clearErr(ids) { ids.forEach(hideErr); }
|
|
function esc(s) { return String(s).replace(/'/g, "\\'").replace(/"/g, '"'); }
|
|
function fmt(n) { return parseFloat(n).toLocaleString('id-ID', { maximumFractionDigits: 2 }); }
|
|
|
|
function tampilkanErr(id, msg) {
|
|
const el = document.getElementById(id);
|
|
if (el) { el.textContent = msg; el.style.display = 'block'; }
|
|
}
|
|
function sembunyikanErr(id) {
|
|
const el = document.getElementById(id);
|
|
if (el) { el.textContent = ''; el.style.display = 'none'; }
|
|
}
|
|
|
|
let toastT;
|
|
function toast(msg, tipe = '') {
|
|
const el = document.getElementById('toast');
|
|
el.textContent = msg; el.className = 'on ' + tipe;
|
|
clearTimeout(toastT);
|
|
toastT = setTimeout(() => el.className = '', 3000);
|
|
}
|
|
|
|
// ============================================================
|
|
// TABS & MUTASI
|
|
// ============================================================
|
|
function gantiTab(tab) {
|
|
document.getElementById('tab-daftar').classList.remove('active');
|
|
document.getElementById('tab-mutasi').classList.remove('active');
|
|
document.getElementById('content-daftar').style.display = 'none';
|
|
document.getElementById('content-mutasi').style.display = 'none';
|
|
|
|
document.getElementById(`tab-${tab}`).classList.add('active');
|
|
document.getElementById(`content-${tab}`).style.display = 'flex';
|
|
|
|
if (tab === 'mutasi') {
|
|
muatMutasiGlobal();
|
|
}
|
|
}
|
|
|
|
async function muatMutasiGlobal() {
|
|
const wrap = document.getElementById('sb-mutasi-list');
|
|
wrap.innerHTML = '<div class="empty"><i class="fa-solid fa-spinner fa-spin"></i> Memuat mutasi...</div>';
|
|
try {
|
|
const list = await get(`${API}?resource=penyaluran&action=list`);
|
|
if (!list.length) {
|
|
wrap.innerHTML = '<div class="empty" style="padding:10px">Belum ada riwayat mutasi.</div>';
|
|
return;
|
|
}
|
|
wrap.innerHTML = list.map(p => `
|
|
<div class="list-item" style="display:block; cursor:default; background:#fff; border-radius:6px; margin-bottom:8px; border:1px solid #eee;">
|
|
<div style="font-size:0.75rem; color:#888; margin-bottom:6px;"><i class="fa-solid fa-calendar-day"></i> ${p.tanggal}</div>
|
|
<div style="font-size:0.85rem; color:#1a6b3c; margin-bottom:6px;"><b>${fmt(p.jumlah_disalurkan)} ${p.satuan} ${p.jenis_sumbangan}</b></div>
|
|
<div style="font-size:0.8rem; line-height:1.5;">
|
|
<div><i class="fa-solid fa-mosque" style="color:#888; width:16px;"></i> ${p.nama_ri}</div>
|
|
<div><i class="fa-solid fa-arrow-right" style="color:#e08020; width:16px;"></i> ${p.nama_kk}</div>
|
|
</div>
|
|
</div>`).join('');
|
|
} catch (e) {
|
|
wrap.innerHTML = '<div class="empty" style="padding:10px;color:#d93025">Gagal memuat mutasi.</div>';
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// HTTP HELPERS
|
|
// ============================================================
|
|
async function get(url) {
|
|
const r = await fetch(url);
|
|
const d = await r.json();
|
|
if (d.error) throw new Error(d.error);
|
|
return d;
|
|
}
|
|
|
|
async function post(url, body, method = 'POST') {
|
|
const r = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
});
|
|
const d = await r.json();
|
|
if (d.error) throw new Error(d.error);
|
|
return d;
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |