Files
2026-06-11 13:54:32 +07:00

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 &nbsp;|&nbsp; © 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, '&quot;'); }
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>