Files
WEBGIS_Project/frontend/admin-poverty.html
T
tistopandita 5386300498 add poverty
2026-06-11 13:56:18 +07:00

925 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Poverty Mapping Gereja</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
:root {
--bg-dark: #0f172a;
--bg-panel: #111827;
--bg-soft: rgba(30, 41, 59, 0.72);
--bg-card: rgba(255, 255, 255, 0.92);
--text-main: #f8fafc;
--text-dark: #0f172a;
--text-muted: #64748b;
--border: rgba(148, 163, 184, 0.25);
--accent: #7c3aed;
--accent-2: #06b6d4;
--danger: #dc2626;
--orange: #f97316;
--yellow: #eab308;
--green: #22c55e;
--radius: 22px;
--shadow: 0 24px 70px rgba(15, 23, 42, 0.24);
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
color: var(--text-dark);
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(124, 58, 237, 0.32), transparent 34%),
linear-gradient(135deg, #130f2e 0%, #111827 52%, #312e81 100%);
}
a { color: inherit; text-decoration: none; }
button, input, select, textarea { font: inherit; }
.shell { display: grid; grid-template-columns: 280px minmax(0, 1fr); min-height: 100vh; }
.sidebar {
background: linear-gradient(180deg, var(--bg-dark), var(--bg-panel));
color: var(--text-main);
padding: 24px 18px;
border-right: 1px solid rgba(255,255,255,.08);
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.brand { margin-bottom: 26px; }
.brand small { color: #a5b4fc; font-weight: 700; text-transform: uppercase; }
.brand h1 { margin: 8px 0 6px; font-size: 1.45rem; }
.brand p { margin: 0; color: #cbd5e1; line-height: 1.45; font-size: .88rem; }
.nav { display: grid; gap: 9px; }
.nav button, .nav a {
border: 1px solid transparent;
background: rgba(255,255,255,.06);
color: #e5e7eb;
border-radius: 16px;
padding: 12px 14px;
text-align: left;
cursor: pointer;
font-weight: 700;
}
.nav button.active, .nav button:hover, .nav a:hover {
background: rgba(124, 58, 237, .22);
border-color: rgba(167, 139, 250, .42);
}
.main { padding: 26px; min-width: 0; }
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
color: var(--text-main);
margin-bottom: 22px;
}
.topbar h2 { margin: 0; font-size: clamp(1.4rem, 2.3vw, 2.1rem); }
.topbar p { margin: 5px 0 0; color: #cbd5e1; }
.btn {
border: 0;
border-radius: 15px;
padding: 11px 16px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: white;
font-weight: 800;
cursor: pointer;
box-shadow: 0 14px 32px rgba(37, 99, 235, .2);
}
.btn.secondary { background: rgba(15, 23, 42, .08); color: var(--text-dark); box-shadow: none; border: 1px solid var(--border); }
.btn.danger { background: #fee2e2; color: var(--danger); box-shadow: none; }
.btn.small { padding: 8px 10px; border-radius: 12px; font-size: .78rem; }
.section { display: none; }
.section.active { display: block; }
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 16px; margin-bottom: 18px; }
.stat, .card {
background: var(--bg-card);
border: 1px solid rgba(255,255,255,.5);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}
.stat { padding: 18px; }
.stat span { color: var(--text-muted); font-size: .84rem; font-weight: 700; }
.stat strong { display: block; margin-top: 8px; font-size: 1.7rem; }
.grid-2 { display: grid; grid-template-columns: minmax(340px, .78fr) minmax(0, 1.22fr); gap: 18px; }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
.card { overflow: hidden; margin-bottom: 18px; }
.card-head { padding: 16px 18px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; gap: 10px; }
.card-head h3 { margin: 0; font-size: 1rem; }
.card-body { padding: 18px; }
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 13px; }
.form-grid .full { grid-column: 1 / -1; }
label { display: grid; gap: 7px; color: #334155; font-size: .82rem; font-weight: 800; }
input, select, textarea {
width: 100%;
border: 1px solid #dbe2ea;
border-radius: 14px;
padding: 11px 12px;
outline: none;
background: rgba(248,250,252,.9);
color: var(--text-dark);
}
textarea { min-height: 78px; resize: vertical; }
input:focus, select:focus, textarea:focus { border-color: #a78bfa; box-shadow: 0 0 0 4px rgba(124,58,237,.12); }
.actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 14px; }
.table-wrap { overflow: auto; }
table { width: 100%; border-collapse: collapse; min-width: 760px; }
th, td { padding: 12px 14px; border-bottom: 1px solid #e5e7eb; text-align: left; vertical-align: top; }
th { color: #475569; font-size: .75rem; text-transform: uppercase; background: #f8fafc; }
td { font-size: .88rem; }
.badge { display: inline-flex; border-radius: 999px; padding: 5px 10px; font-weight: 800; font-size: .72rem; }
.sangat { background: #fee2e2; color: #7f1d1d; }
.butuh { background: #ffedd5; color: #7c2d12; }
.rentan { background: #fef9c3; color: #713f12; }
.mampu { background: #dcfce7; color: #14532d; }
/* Status pengajuan */
.status-menunggu { background: #fef3c7; color: #92400e; }
.status-diverifikasi { background: #dbeafe; color: #1e40af; }
.status-disetujui { background: #dcfce7; color: #14532d; }
.status-ditolak { background: #fee2e2; color: #7f1d1d; }
.status-selesai { background: #e0e7ff; color: #3730a3; }
.bar-row { display: grid; grid-template-columns: 150px 1fr 48px; gap: 10px; align-items: center; margin: 10px 0; font-size: .86rem; }
.bar-track { height: 12px; border-radius: 99px; background: #e2e8f0; overflow: hidden; }
.bar-fill { height: 100%; border-radius: inherit; background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
#adminMap { height: 520px; border-radius: var(--radius); overflow: hidden; }
.toast {
position: fixed;
right: 22px;
bottom: 22px;
background: #0f172a;
color: white;
padding: 13px 16px;
border-radius: 14px;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(10px);
pointer-events: none;
transition: .2s ease;
z-index: 2000;
}
.toast.show { opacity: 1; transform: translateY(0); }
.ibadah-map { height: 300px; position: relative; border-radius: var(--radius); overflow: hidden; border: 1px solid #dbe2ea; background: #eef2f7; }
.coord-actions { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.ibadah-badge-gereja-katolik { background: #ede9fe; color: #4c1d95; }
.ibadah-badge-gpib { background: #dbeafe; color: #1e3a8a; }
.ibadah-badge-gki { background: #cffafe; color: #164e63; }
.ibadah-badge-hkbp { background: #dcfce7; color: #14532d; }
.ibadah-badge-pentakosta { background: #ffedd5; color: #7c2d12; }
.ibadah-pin {
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 999px;
background: #7c3aed;
color: #fff;
border: 3px solid #fff;
box-shadow: 0 10px 22px rgba(15, 23, 42, .25);
font-size: 16px;
}
@media (max-width: 980px) {
.shell { grid-template-columns: 1fr; }
.sidebar { position: relative; height: auto; }
.stats, .grid-2, .grid-3 { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.main { padding: 16px; }
.topbar { align-items: flex-start; flex-direction: column; }
.form-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="toast" id="toast"></div>
<div class="shell">
<aside class="sidebar">
<div class="brand">
<small>WebGIS Gereja</small>
<h1>Admin Poverty Mapping</h1>
<p>Pengelolaan data keluarga, bantuan, kriteria SAW, dan ranking prioritas bantuan.</p>
</div>
<nav class="nav">
<button class="active" data-section="dashboard">Dashboard</button>
<button data-section="keluarga">Data Keluarga</button>
<button data-section="bantuan">Data Bantuan</button>
<button data-section="kriteria">Kriteria SAW</button>
<button data-section="ibadah">⛪ Tempat Ibadah</button>
<button data-section="pengajuan">📋 Pengajuan Bantuan</button>
<button data-section="peta">Preview Peta</button>
<a href="/poverty-map.html">Buka Peta Poverty</a>
<a href="/pengajuan.html">Form Pengajuan Publik</a>
<a href="/admin.html">Admin SPBU</a>
</nav>
</aside>
<main class="main">
<header class="topbar">
<div>
<h2>Dashboard Bantuan Gereja</h2>
<p>SAW otomatis: normalisasi, nilai terbobot, kategori, dan ranking prioritas.</p>
</div>
<button class="btn" id="recalculateBtn">Hitung Ulang SAW</button>
</header>
<section class="section active" id="dashboard">
<div class="stats">
<article class="stat"><span>Total keluarga</span><strong id="statTotal">0</strong></article>
<article class="stat"><span>Prioritas tinggi</span><strong id="statPrioritas">0</strong></article>
<article class="stat"><span>Bantuan tersalurkan</span><strong id="statBantuan">Rp0</strong></article>
<article class="stat"><span>Kriteria aktif</span><strong id="statKriteria">0</strong></article>
</div>
<div class="grid-3">
<article class="card"><div class="card-head"><h3>Grafik Kategori Ekonomi</h3></div><div class="card-body" id="chartKategori"></div></article>
<article class="card"><div class="card-head"><h3>Bantuan Tersalurkan</h3></div><div class="card-body" id="chartBantuan"></div></article>
<article class="card"><div class="card-head"><h3>Distribusi Wilayah</h3></div><div class="card-body" id="chartWilayah"></div></article>
</div>
<div class="grid-2">
<article class="card"><div class="card-head"><h3>Ranking 10 Prioritas</h3></div><div class="table-wrap"><table><thead><tr><th>Rank</th><th>Keluarga</th><th>Skor</th><th>Kategori</th></tr></thead><tbody id="rankBody"></tbody></table></div></article>
<article class="card"><div class="card-head"><h3>Tren Kondisi Ekonomi</h3></div><div class="card-body" id="chartTren"></div></article>
</div>
</section>
<section class="section" id="keluarga">
<div class="grid-2">
<article class="card">
<div class="card-head"><h3>Form Data Keluarga</h3><button class="btn secondary small" id="resetKeluarga">Reset</button></div>
<div class="card-body">
<form id="keluargaForm">
<input type="hidden" id="id_keluarga" />
<div class="form-grid">
<label>Nomor KK<input id="nomor_kartu_keluarga" required /></label>
<label>Nama Kepala Keluarga<input id="nama_kepala_keluarga" required /></label>
<label class="full">Alamat<textarea id="alamat" required></textarea></label>
<label>Wilayah/Paroki<input id="wilayah" required /></label>
<label>Tanggal Update<input id="tanggal_update" type="date" required /></label>
<label>Latitude<input id="latitude" type="number" step="any" required /></label>
<label>Longitude<input id="longitude" type="number" step="any" required /></label>
<label>Jumlah Anggota<input id="jumlah_anggota" type="number" min="1" required /></label>
<label>Jumlah Tanggungan<input id="jumlah_tanggungan" type="number" min="0" required /></label>
<label>Pendapatan Bulanan<input id="pendapatan_bulanan" type="number" min="0" required /></label>
<label>Pekerjaan<select id="pekerjaan" required></select></label>
<label>Pendidikan<select id="pendidikan" required></select></label>
<label>Kondisi Rumah<select id="kondisi_rumah" required></select></label>
<label>Kondisi Kesehatan<select id="kondisi_kesehatan" required></select></label>
<label>Status Verifikasi<select id="status_verifikasi"><option value="pending">Pending</option><option value="terverifikasi">Terverifikasi</option><option value="ditolak">Ditolak</option></select></label>
</div>
<div class="actions"><button class="btn" type="submit">Simpan Keluarga</button></div>
</form>
</div>
</article>
<article class="card">
<div class="card-head"><h3>Daftar Keluarga</h3><input id="familySearch" placeholder="Cari nama atau wilayah" /></div>
<div class="table-wrap"><table><thead><tr><th>Ranking</th><th>Keluarga</th><th>Ekonomi</th><th>Skor</th><th>Aksi</th></tr></thead><tbody id="keluargaBody"></tbody></table></div>
</article>
</div>
</section>
<section class="section" id="bantuan">
<div class="grid-2">
<article class="card">
<div class="card-head"><h3>Form Bantuan</h3><button class="btn secondary small" id="resetBantuan">Reset</button></div>
<div class="card-body">
<form id="bantuanForm">
<input type="hidden" id="id_bantuan" />
<div class="form-grid">
<label>Jenis Bantuan<input id="jenis_bantuan" required /></label>
<label>Tanggal Bantuan<input id="tanggal_bantuan" type="date" required /></label>
<label>Nilai Bantuan<input id="nilai_bantuan" type="number" min="0" required /></label>
<label class="full">Keterangan<textarea id="keterangan_bantuan"></textarea></label>
</div>
<div class="actions"><button class="btn" type="submit">Simpan Bantuan</button></div>
</form>
</div>
</article>
<article class="card">
<div class="card-head"><h3>Catat Penerima Bantuan</h3><button class="btn secondary small" id="resetRiwayat">Reset</button></div>
<div class="card-body">
<form id="riwayatForm">
<input type="hidden" id="id_riwayat" />
<div class="form-grid">
<label>Penerima<select id="riwayat_keluarga" required></select></label>
<label>Jenis Bantuan<select id="riwayat_bantuan" required></select></label>
<label>Tanggal Diterima<input id="tanggal_diterima" type="date" required /></label>
<label>Jumlah/Nilai<input id="jumlah_bantuan" type="number" min="0" required /></label>
<label class="full">Keterangan<textarea id="keterangan_riwayat"></textarea></label>
</div>
<div class="actions"><button class="btn" type="submit">Simpan Riwayat</button></div>
</form>
</div>
</article>
</div>
<div class="grid-2">
<article class="card"><div class="card-head"><h3>Data Bantuan</h3></div><div class="table-wrap"><table><thead><tr><th>Jenis</th><th>Tanggal</th><th>Nilai</th><th>Aksi</th></tr></thead><tbody id="bantuanBody"></tbody></table></div></article>
<article class="card"><div class="card-head"><h3>Riwayat Bantuan</h3></div><div class="table-wrap"><table><thead><tr><th>Penerima</th><th>Bantuan</th><th>Tanggal</th><th>Jumlah</th><th>Aksi</th></tr></thead><tbody id="riwayatBody"></tbody></table></div></article>
</div>
</section>
<section class="section" id="kriteria">
<div class="grid-2">
<article class="card">
<div class="card-head"><h3>Form Kriteria SAW</h3><button class="btn secondary small" id="resetKriteria">Reset</button></div>
<div class="card-body">
<form id="kriteriaForm">
<input type="hidden" id="id_kriteria" />
<div class="form-grid">
<label>Kode<input id="kode" required /></label>
<label>Nama Kriteria<input id="nama_kriteria" required /></label>
<label>Bobot<input id="bobot" type="number" min="0" max="1" step="0.01" required /></label>
<label>Atribut<select id="atribut"><option value="benefit">Benefit</option><option value="cost">Cost</option></select></label>
</div>
<div class="actions"><button class="btn" type="submit">Simpan Kriteria</button></div>
</form>
</div>
</article>
<article class="card"><div class="card-head"><h3>Data Kriteria</h3></div><div class="table-wrap"><table><thead><tr><th>Kriteria</th><th>Bobot</th><th>Atribut</th><th>Status</th><th>Aksi</th></tr></thead><tbody id="kriteriaBody"></tbody></table></div></article>
</div>
</section>
<section class="section" id="ibadah">
<article class="card">
<div class="card-head"><h3>Form Tempat Ibadah</h3><button class="btn secondary small" id="resetIbadah">Reset</button></div>
<div class="card-body">
<form id="ibadahForm">
<input type="hidden" id="id_ibadah" />
<div class="form-grid">
<label>Nama<input id="ibadah_nama" required /></label>
<label>Jenis<select id="ibadah_jenis" required></select></label>
<label>Alamat<input id="ibadah_alamat" required /></label>
<label>Kelurahan<input id="ibadah_kelurahan" required /></label>
<label>Kapasitas Bantuan (KK)<input id="ibadah_kapasitas" type="number" min="0" value="0" /></label>
<div></div>
<label class="full">Keterangan / Catatan<textarea id="ibadah_keterangan" placeholder="Jadwal bantuan, kontak, dll."></textarea></label>
<div class="full">
<label>Koordinat dari Peta</label>
<div id="ibadahMap" class="ibadah-map"></div>
</div>
<label>Lat<input id="ibadah_lat" type="number" step="any" readonly required /></label>
<label>Lng<input id="ibadah_lng" type="number" step="any" readonly required /></label>
<div class="full coord-actions">
<button class="btn secondary small" id="toggleManualIbadah" type="button">✏️ Edit Manual</button>
<button class="btn danger small" id="clearIbadahPin" type="button">× Hapus Pin</button>
</div>
</div>
<div class="actions">
<button class="btn" id="ibadahSubmit" type="submit">Simpan Tempat Ibadah</button>
<button class="btn secondary" id="cancelIbadahEdit" type="button" style="display:none">Batal Edit</button>
</div>
</form>
</div>
</article>
<article class="card">
<div class="card-head"><h3>Daftar Tempat Ibadah</h3><span id="ibadahCount">0 data</span></div>
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Nama</th><th>Jenis</th><th>Alamat</th><th>Kelurahan</th><th>Koordinat</th><th>Kapasitas</th><th>Aksi</th></tr></thead>
<tbody id="ibadahBody"></tbody>
</table>
</div>
</article>
</section>
<!-- ═══════════════════════ PENGAJUAN BANTUAN ═══════════════════════ -->
<section class="section" id="pengajuan">
<div class="stats" style="grid-template-columns:repeat(3,minmax(0,1fr))">
<article class="stat"><span>Total Pengajuan</span><strong id="statPengajuanTotal">0</strong></article>
<article class="stat"><span>Menunggu</span><strong id="statPengajuanMenunggu">0</strong></article>
<article class="stat"><span>Disetujui</span><strong id="statPengajuanDisetujui">0</strong></article>
</div>
<article class="card" style="margin-top:18px">
<div class="card-head">
<h3>Daftar Pengajuan Bantuan</h3>
<select id="filterPengajuanStatus" style="min-width:160px;border-radius:10px;padding:7px 10px;border:1px solid #dbe2ea;font-size:.82rem">
<option value="">Semua Status</option>
<option value="menunggu">Menunggu</option>
<option value="diverifikasi">Diverifikasi</option>
<option value="disetujui">Disetujui</option>
<option value="ditolak">Ditolak</option>
<option value="selesai">Selesai</option>
</select>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>#</th><th>Pemohon</th><th>NIK</th><th>Wilayah</th><th>Alasan</th><th>Tgl Pengajuan</th><th>Status</th><th>Aksi</th></tr></thead>
<tbody id="pengajuanBody"></tbody>
</table>
</div>
</article>
<!-- Detail / Update Status Modal (inline) -->
<article class="card" id="pengajuanDetailCard" style="display:none;margin-top:18px">
<div class="card-head">
<h3>Update Status Pengajuan</h3>
<button class="btn secondary small" onclick="el('pengajuanDetailCard').style.display='none'">Tutup</button>
</div>
<div class="card-body">
<p id="pengajuanDetailInfo" style="color:#475569;margin:0 0 14px"></p>
<div class="form-grid">
<label>Status Baru
<select id="pengajuanNewStatus">
<option value="menunggu">Menunggu</option>
<option value="diverifikasi">Diverifikasi</option>
<option value="disetujui">Disetujui</option>
<option value="ditolak">Ditolak</option>
<option value="selesai">Selesai</option>
</select>
</label>
<label>Hubungkan ke Keluarga (opsional)
<select id="pengajuanKeluarga"><option value="">— Tidak dihubungkan —</option></select>
</label>
<label class="full">Catatan Admin
<textarea id="pengajuanCatatan" placeholder="Alasan penolakan, catatan verifikasi, dll."></textarea>
</label>
</div>
<input type="hidden" id="pengajuanEditId" />
<div class="actions">
<button class="btn" id="savePengajuanStatus">Simpan Status</button>
</div>
</div>
</article>
</section>
<section class="section" id="peta">
<article class="card">
<div class="card-head"><h3>Preview Peta Keluarga Prioritas</h3><span>Klik peta untuk mengisi koordinat form keluarga.</span></div>
<div class="card-body"><div id="adminMap"></div></div>
</article>
</section>
</main>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API = "/api/poverty";
const state = { keluarga: [], bantuan: [], riwayat: [], kriteria: [], ibadah: [], pengajuan: [], dashboard: null, analytics: null };
const today = new Date().toISOString().slice(0, 10);
const DEFAULT_CENTER = [-0.0263, 109.3425];
const IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"];
const mapsInitialized = { ibadah: false };
const labels = {
pekerjaan: { pengangguran: "Pengangguran", buruh_harian: "Buruh Harian", petani: "Petani", nelayan: "Nelayan", pedagang_kecil: "Pedagang Kecil", karyawan: "Karyawan", wiraswasta: "Wiraswasta", pns: "PNS", pensiunan: "Pensiunan", lainnya: "Lainnya" },
pendidikan: { tidak_sekolah: "Tidak Sekolah", sd: "SD", smp: "SMP", sma: "SMA", diploma: "Diploma", sarjana: "Sarjana" },
kondisi_rumah: { sangat_buruk: "Sangat Buruk", buruk: "Buruk", semi_permanen: "Semi Permanen", sederhana: "Sederhana", layak: "Layak" },
kondisi_kesehatan: { disabilitas: "Disabilitas", kronis: "Kronis", lansia: "Lansia", sakit_sering: "Sakit Sering", sakit_ringan: "Sakit Ringan", sehat: "Sehat" }
};
const el = (id) => document.getElementById(id);
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
const rp = (value) => new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR", maximumFractionDigits: 0 }).format(Number(value || 0));
const fmt = (value) => value ? String(value).slice(0, 10) : "";
const badgeClass = (kategori) => kategori === "Sangat Membutuhkan" ? "sangat" : kategori === "Membutuhkan" ? "butuh" : kategori === "Rentan" ? "rentan" : "mampu";
const ibadahBadgeClass = (jenis) => `ibadah-badge-${String(jenis || "").toLowerCase().replace(/\s+/g, "-")}`;
async function api(path, options = {}) {
const response = await fetch(`${API}${path}`, { headers: { "Content-Type": "application/json", ...(options.headers || {}) }, ...options });
const payload = await response.json();
if (!response.ok || payload.success === false) throw new Error(payload.message || "Request gagal");
return payload.data ?? payload;
}
function toast(message) {
el("toast").textContent = message;
el("toast").classList.add("show");
setTimeout(() => el("toast").classList.remove("show"), 2200);
}
function fillOptions(id, items) {
el(id).innerHTML = Object.entries(items).map(([value, label]) => `<option value="${value}">${label}</option>`).join("");
}
function renderBars(containerId, data, formatter = (value) => value) {
const entries = Object.entries(data || {});
const max = Math.max(1, ...entries.map(([, value]) => Number(value) || 0));
el(containerId).innerHTML = entries.length ? entries.map(([label, value]) => `
<div class="bar-row"><span>${esc(label)}</span><div class="bar-track"><div class="bar-fill" style="width:${Math.max(3, (Number(value) / max) * 100)}%"></div></div><strong>${formatter(value)}</strong></div>
`).join("") : "<p>Belum ada data.</p>";
}
function renderDashboard() {
const d = state.dashboard || {};
el("statTotal").textContent = d.total_keluarga || 0;
el("statPrioritas").textContent = d.prioritas_tinggi || 0;
el("statBantuan").textContent = rp(d.total_bantuan_tersalurkan || 0);
el("statKriteria").textContent = state.kriteria.filter((item) => item.aktif).length;
renderBars("chartKategori", d.kategori_ekonomi || {});
renderBars("chartWilayah", d.wilayah || {});
const bantuanBulanan = {};
(state.analytics?.bantuan_bulanan || []).forEach((item) => bantuanBulanan[item.bulan] = Number(item.total || 0));
renderBars("chartBantuan", bantuanBulanan, (value) => rp(value));
const tren = {};
(state.analytics?.tren_ekonomi || []).forEach((item) => tren[item.bulan] = Number(item.rata_skor || 0).toFixed(2));
renderBars("chartTren", tren);
el("rankBody").innerHTML = (d.ranking_top10 || []).map((item) => `
<tr><td><strong>#${item.ranking || "-"}</strong></td><td>${esc(item.nama_kepala_keluarga)}<br><small>${esc(item.wilayah)}</small></td><td>${Number(item.skor_saw || 0).toFixed(3)}</td><td><span class="badge ${badgeClass(item.kategori)}">${esc(item.kategori)}</span></td></tr>
`).join("") || `<tr><td colspan="4">Belum ada data.</td></tr>`;
}
function filteredFamilies() {
const query = el("familySearch").value.toLowerCase().trim();
return state.keluarga.filter((item) => !query || item.nama_kepala_keluarga.toLowerCase().includes(query) || item.wilayah.toLowerCase().includes(query));
}
function renderKeluarga() {
el("keluargaBody").innerHTML = filteredFamilies().map((item) => `
<tr>
<td><strong>#${item.ranking || "-"}</strong><br><small>${esc(item.status_verifikasi)}</small></td>
<td><strong>${esc(item.nama_kepala_keluarga)}</strong><br><small>${esc(item.alamat)}</small><br><small>${esc(item.wilayah)}</small></td>
<td>${rp(item.pendapatan_bulanan)}<br><small>${item.jumlah_tanggungan} tanggungan</small></td>
<td>${Number(item.skor_saw || 0).toFixed(3)}<br><span class="badge ${badgeClass(item.kategori)}">${esc(item.kategori)}</span></td>
<td><button class="btn secondary small" onclick="editKeluarga(${item.id_keluarga})">Edit</button> <button class="btn danger small" onclick="hapus('keluarga', ${item.id_keluarga})">Hapus</button></td>
</tr>
`).join("") || `<tr><td colspan="5">Belum ada data keluarga.</td></tr>`;
refreshSelects();
renderMap();
}
function refreshSelects() {
el("riwayat_keluarga").innerHTML = state.keluarga.map((item) => `<option value="${item.id_keluarga}">${esc(item.nama_kepala_keluarga)} - ${esc(item.kategori || "")}</option>`).join("");
el("riwayat_bantuan").innerHTML = state.bantuan.map((item) => `<option value="${item.id_bantuan}">${esc(item.jenis_bantuan)} (${rp(item.nilai_bantuan)})</option>`).join("");
// Populate pengajuan keluarga select (with leading empty option)
const pengajuanSel = el("pengajuanKeluarga");
if (pengajuanSel) {
pengajuanSel.innerHTML = `<option value="">— Tidak dihubungkan —</option>` +
state.keluarga.map((item) => `<option value="${item.id_keluarga}">#${item.ranking||'-'} ${esc(item.nama_kepala_keluarga)} (${esc(item.wilayah)})</option>`).join("");
}
}
function renderBantuan() {
el("bantuanBody").innerHTML = state.bantuan.map((item) => `
<tr><td><strong>${esc(item.jenis_bantuan)}</strong><br><small>${esc(item.keterangan || "-")}</small></td><td>${fmt(item.tanggal_bantuan)}</td><td>${rp(item.nilai_bantuan)}</td><td><button class="btn secondary small" onclick="editBantuan(${item.id_bantuan})">Edit</button> <button class="btn danger small" onclick="hapus('bantuan', ${item.id_bantuan})">Hapus</button></td></tr>
`).join("") || `<tr><td colspan="4">Belum ada data bantuan.</td></tr>`;
el("riwayatBody").innerHTML = state.riwayat.map((item) => `
<tr><td>${esc(item.nama_kepala_keluarga)}</td><td>${esc(item.jenis_bantuan)}</td><td>${fmt(item.tanggal_diterima)}</td><td>${rp(item.jumlah_bantuan)}</td><td><button class="btn secondary small" onclick="editRiwayat(${item.id_riwayat})">Edit</button> <button class="btn danger small" onclick="hapus('riwayat-bantuan', ${item.id_riwayat})">Hapus</button></td></tr>
`).join("") || `<tr><td colspan="5">Belum ada riwayat bantuan.</td></tr>`;
refreshSelects();
}
function renderKriteria() {
el("kriteriaBody").innerHTML = state.kriteria.map((item) => `
<tr><td><strong>${esc(item.nama_kriteria)}</strong><br><small>${esc(item.kode)}</small></td><td>${Number(item.bobot).toFixed(2)}</td><td>${esc(item.atribut)}</td><td>${item.aktif ? "Aktif" : "Nonaktif"}</td><td><button class="btn secondary small" onclick="editKriteria(${item.id_kriteria})">Edit</button> <button class="btn danger small" onclick="hapus('kriteria', ${item.id_kriteria})">Nonaktif</button></td></tr>
`).join("") || `<tr><td colspan="5">Belum ada data kriteria.</td></tr>`;
}
function renderIbadah() {
el("ibadahCount").textContent = `${state.ibadah.length} data`;
el("ibadahBody").innerHTML = state.ibadah.map((item, index) => `
<tr>
<td>${index + 1}</td>
<td><strong>${esc(item.nama)}</strong></td>
<td><span class="badge ${ibadahBadgeClass(item.jenis)}">${esc(item.jenis)}</span></td>
<td>${esc(item.alamat)}</td>
<td>${esc(item.kelurahan)}</td>
<td>${Number(item.lat).toFixed(6)}, ${Number(item.lng).toFixed(6)}</td>
<td>${item.kapasitas_bantuan || 0} KK</td>
<td><button class="btn secondary small" onclick="editIbadah(${item.id})">Edit</button> <button class="btn danger small" onclick="hapus('ibadah', ${item.id})">Hapus</button></td>
</tr>
`).join("") || `<tr><td colspan="8">Belum ada data tempat ibadah.</td></tr>`;
}
function pengajuanBadgeClass(status) {
return `status-${status || 'menunggu'}`;
}
function renderPengajuan() {
const rows = state.pengajuan;
el("statPengajuanTotal").textContent = rows.length;
el("statPengajuanMenunggu").textContent = rows.filter(r => r.status === "menunggu").length;
el("statPengajuanDisetujui").textContent = rows.filter(r => r.status === "disetujui").length;
el("pengajuanBody").innerHTML = rows.map((item, idx) => `
<tr>
<td>${idx + 1}</td>
<td><strong>${esc(item.nama_pemohon)}</strong></td>
<td>${esc(item.nik)}</td>
<td>${esc(item.wilayah)}</td>
<td style="max-width:200px;white-space:normal;font-size:.82rem">${esc(item.alasan)}</td>
<td>${fmt(item.tanggal_pengajuan)}</td>
<td><span class="badge ${pengajuanBadgeClass(item.status)}">${esc(item.status)}</span></td>
<td>
<button class="btn secondary small" onclick="openPengajuanEdit(${item.id_pengajuan})">Review</button>
<button class="btn danger small" onclick="hapusPengajuan(${item.id_pengajuan})">Hapus</button>
</td>
</tr>
`).join("") || `<tr><td colspan="8">Belum ada pengajuan.</td></tr>`;
}
let ibadahMap, ibadahMarker, ibadahManual = false;
const ibadahIcon = L.divIcon({
className: "",
html: `<div class="ibadah-pin">⛪</div>`,
iconSize: [32, 32],
iconAnchor: [16, 32],
});
function ensureIbadahMap() {
if (mapsInitialized.ibadah) return;
mapsInitialized.ibadah = true;
ibadahMap = L.map("ibadahMap").setView(DEFAULT_CENTER, 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "&copy; OpenStreetMap" }).addTo(ibadahMap);
ibadahMap.on("click", (event) => setIbadahPin(event.latlng.lat, event.latlng.lng));
setTimeout(() => ibadahMap.invalidateSize(), 80);
}
function setIbadahFields(lat, lng) {
el("ibadah_lat").value = Number(lat).toFixed(7);
el("ibadah_lng").value = Number(lng).toFixed(7);
}
function setIbadahPin(lat, lng, zoom = false) {
const point = [Number(lat), Number(lng)];
if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) return;
ensureIbadahMap();
setIbadahFields(point[0], point[1]);
if (!ibadahMarker) {
ibadahMarker = L.marker(point, { draggable: true, icon: ibadahIcon }).addTo(ibadahMap);
ibadahMarker.on("dragend", () => {
const latlng = ibadahMarker.getLatLng();
setIbadahFields(latlng.lat, latlng.lng);
});
} else {
ibadahMarker.setLatLng(point);
}
if (zoom) ibadahMap.setView(point, 15);
}
function clearIbadahPin() {
if (ibadahMarker) {
ibadahMarker.remove();
ibadahMarker = null;
}
el("ibadah_lat").value = "";
el("ibadah_lng").value = "";
}
function syncIbadahManualPin() {
const lat = Number(el("ibadah_lat").value);
const lng = Number(el("ibadah_lng").value);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
setIbadahPin(lat, lng, true);
}
function ibadahPayload() {
return {
nama: el("ibadah_nama").value.trim(),
jenis: el("ibadah_jenis").value,
alamat: el("ibadah_alamat").value.trim(),
kelurahan: el("ibadah_kelurahan").value.trim(),
lat: Number(el("ibadah_lat").value),
lng: Number(el("ibadah_lng").value),
kapasitas_bantuan: Number(el("ibadah_kapasitas").value) || 0,
keterangan: el("ibadah_keterangan").value.trim() || null,
};
}
let map, layer;
function ensureMap() {
if (map) return;
map = L.map("adminMap").setView([-0.0263, 109.3425], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "&copy; OpenStreetMap" }).addTo(map);
layer = L.layerGroup().addTo(map);
map.on("click", (event) => {
el("latitude").value = event.latlng.lat.toFixed(7);
el("longitude").value = event.latlng.lng.toFixed(7);
toast("Koordinat keluarga diisi dari peta.");
});
}
function color(kategori) {
return kategori === "Sangat Membutuhkan" ? "#dc2626" : kategori === "Membutuhkan" ? "#f97316" : kategori === "Rentan" ? "#eab308" : "#22c55e";
}
function renderMap() {
ensureMap();
layer.clearLayers();
state.keluarga.forEach((item) => {
L.circleMarker([item.latitude, item.longitude], { radius: 8, color: color(item.kategori), fillColor: color(item.kategori), fillOpacity: .85 })
.bindPopup(`<strong>${esc(item.nama_kepala_keluarga)}</strong><br>${esc(item.alamat)}<br>Skor SAW: ${Number(item.skor_saw || 0).toFixed(3)}<br>${esc(item.kategori)}`)
.addTo(layer);
});
}
async function loadAll() {
const [dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah, pengajuan] = await Promise.all([
api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"),
api("/riwayat-bantuan"), api("/kriteria"), api("/ibadah"), api("/pengajuan")
]);
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah, pengajuan });
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah(); renderPengajuan();
}
function familyPayload() {
return {
nomor_kartu_keluarga: el("nomor_kartu_keluarga").value.trim(),
nama_kepala_keluarga: el("nama_kepala_keluarga").value.trim(),
alamat: el("alamat").value.trim(),
wilayah: el("wilayah").value.trim(),
latitude: Number(el("latitude").value),
longitude: Number(el("longitude").value),
jumlah_anggota: Number(el("jumlah_anggota").value),
jumlah_tanggungan: Number(el("jumlah_tanggungan").value),
pendapatan_bulanan: Number(el("pendapatan_bulanan").value),
pekerjaan: el("pekerjaan").value,
pendidikan: el("pendidikan").value,
kondisi_rumah: el("kondisi_rumah").value,
kondisi_kesehatan: el("kondisi_kesehatan").value,
status_verifikasi: el("status_verifikasi").value,
tanggal_update: el("tanggal_update").value
};
}
function resetKeluarga() {
el("keluargaForm").reset();
el("id_keluarga").value = "";
el("tanggal_update").value = today;
el("jumlah_anggota").value = 1;
el("jumlah_tanggungan").value = 0;
}
function resetBantuan() { el("bantuanForm").reset(); el("id_bantuan").value = ""; el("tanggal_bantuan").value = today; }
function resetRiwayat() { el("riwayatForm").reset(); el("id_riwayat").value = ""; el("tanggal_diterima").value = today; }
function resetKriteria() { el("kriteriaForm").reset(); el("id_kriteria").value = ""; }
function resetIbadah() {
el("ibadahForm").reset();
el("id_ibadah").value = "";
el("ibadah_kapasitas").value = 0;
el("ibadah_keterangan").value = "";
el("ibadah_lat").readOnly = true;
el("ibadah_lng").readOnly = true;
ibadahManual = false;
el("toggleManualIbadah").textContent = "✏️ Edit Manual";
el("ibadahSubmit").textContent = "Simpan Tempat Ibadah";
el("cancelIbadahEdit").style.display = "none";
clearIbadahPin();
if (ibadahMap) ibadahMap.setView(DEFAULT_CENTER, 12);
}
window.editKeluarga = (id) => {
const item = state.keluarga.find((row) => row.id_keluarga === id);
if (!item) return;
Object.keys(familyPayload()).forEach((key) => { if (el(key)) el(key).value = key === "tanggal_update" ? fmt(item[key]) : (item[key] ?? ""); });
el("id_keluarga").value = id;
document.querySelector('[data-section="keluarga"]').click();
};
window.editBantuan = (id) => {
const item = state.bantuan.find((row) => row.id_bantuan === id);
if (!item) return;
el("id_bantuan").value = id; el("jenis_bantuan").value = item.jenis_bantuan; el("tanggal_bantuan").value = fmt(item.tanggal_bantuan); el("nilai_bantuan").value = item.nilai_bantuan; el("keterangan_bantuan").value = item.keterangan || "";
};
window.editRiwayat = (id) => {
const item = state.riwayat.find((row) => row.id_riwayat === id);
if (!item) return;
el("id_riwayat").value = id; el("riwayat_keluarga").value = item.id_keluarga; el("riwayat_bantuan").value = item.id_bantuan; el("tanggal_diterima").value = fmt(item.tanggal_diterima); el("jumlah_bantuan").value = item.jumlah_bantuan; el("keterangan_riwayat").value = item.keterangan || "";
};
window.editKriteria = (id) => {
const item = state.kriteria.find((row) => row.id_kriteria === id);
if (!item) return;
el("id_kriteria").value = id; el("kode").value = item.kode; el("nama_kriteria").value = item.nama_kriteria; el("bobot").value = item.bobot; el("atribut").value = item.atribut;
};
window.editIbadah = (id) => {
const item = state.ibadah.find((row) => row.id === id);
if (!item) return;
document.querySelector('[data-section="ibadah"]').click();
el("id_ibadah").value = id;
el("ibadah_nama").value = item.nama;
el("ibadah_jenis").value = item.jenis;
el("ibadah_alamat").value = item.alamat;
el("ibadah_kelurahan").value = item.kelurahan;
el("ibadah_kapasitas").value = item.kapasitas_bantuan || 0;
el("ibadah_keterangan").value = item.keterangan || "";
el("ibadahSubmit").textContent = "Update Tempat Ibadah";
el("cancelIbadahEdit").style.display = "inline-flex";
setTimeout(() => {
setIbadahPin(item.lat, item.lng, true);
el("ibadah").scrollIntoView({ behavior: "smooth", block: "start" });
}, 100);
};
window.openPengajuanEdit = (id) => {
const item = state.pengajuan.find(r => r.id_pengajuan === id);
if (!item) return;
document.querySelector('[data-section="pengajuan"]').click();
el("pengajuanEditId").value = id;
el("pengajuanNewStatus").value = item.status;
el("pengajuanCatatan").value = item.catatan_admin || "";
el("pengajuanKeluarga").value = item.id_keluarga || "";
el("pengajuanDetailInfo").textContent =
`Pemohon: ${item.nama_pemohon} | NIK: ${item.nik} | Wilayah: ${item.wilayah} | Tgl: ${String(item.tanggal_pengajuan||'').slice(0,10)}`;
el("pengajuanDetailCard").style.display = "block";
el("pengajuanDetailCard").scrollIntoView({ behavior: "smooth" });
};
window.hapusPengajuan = async (id) => {
if (!confirm("Hapus pengajuan ini?")) return;
await api(`/pengajuan/${id}`, { method: "DELETE" });
toast("Pengajuan berhasil dihapus.");
await loadAll();
};
window.hapus = async (resource, id) => {
if (!confirm("Hapus/nonaktifkan data ini?")) return;
await api(`/${resource}/${id}`, { method: "DELETE" });
toast("Data berhasil diperbarui.");
await loadAll();
};
document.querySelectorAll(".nav button").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll(".nav button").forEach((item) => item.classList.remove("active"));
document.querySelectorAll(".section").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
el(button.dataset.section).classList.add("active");
setTimeout(() => {
map?.invalidateSize();
if (button.dataset.section === "ibadah") {
ensureIbadahMap();
ibadahMap?.invalidateSize();
}
}, 80);
});
});
document.getElementById("keluargaForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = el("id_keluarga").value;
await api(id ? `/keluarga/${id}` : "/keluarga", { method: id ? "PUT" : "POST", body: JSON.stringify(familyPayload()) });
resetKeluarga(); toast("Data keluarga tersimpan."); await loadAll();
});
document.getElementById("bantuanForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = el("id_bantuan").value;
const payload = { jenis_bantuan: el("jenis_bantuan").value.trim(), tanggal_bantuan: el("tanggal_bantuan").value, nilai_bantuan: Number(el("nilai_bantuan").value), keterangan: el("keterangan_bantuan").value.trim() };
await api(id ? `/bantuan/${id}` : "/bantuan", { method: id ? "PUT" : "POST", body: JSON.stringify(payload) });
resetBantuan(); toast("Data bantuan tersimpan."); await loadAll();
});
document.getElementById("riwayatForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = el("id_riwayat").value;
const payload = { id_keluarga: el("riwayat_keluarga").value, id_bantuan: el("riwayat_bantuan").value, tanggal_diterima: el("tanggal_diterima").value, jumlah_bantuan: Number(el("jumlah_bantuan").value), keterangan: el("keterangan_riwayat").value.trim() };
await api(id ? `/riwayat-bantuan/${id}` : "/riwayat-bantuan", { method: id ? "PUT" : "POST", body: JSON.stringify(payload) });
resetRiwayat(); toast("Riwayat bantuan tersimpan."); await loadAll();
});
document.getElementById("kriteriaForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = el("id_kriteria").value;
const payload = { kode: el("kode").value.trim(), nama_kriteria: el("nama_kriteria").value.trim(), bobot: Number(el("bobot").value), atribut: el("atribut").value, aktif: true };
await api(id ? `/kriteria/${id}` : "/kriteria", { method: id ? "PUT" : "POST", body: JSON.stringify(payload) });
resetKriteria(); toast("Kriteria tersimpan."); await loadAll();
});
document.getElementById("ibadahForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = el("id_ibadah").value;
await api(id ? `/ibadah/${id}` : "/ibadah", { method: id ? "PUT" : "POST", body: JSON.stringify(ibadahPayload()) });
resetIbadah(); toast("Tempat ibadah tersimpan."); await loadAll();
});
el("resetKeluarga").addEventListener("click", resetKeluarga);
el("resetBantuan").addEventListener("click", resetBantuan);
el("resetRiwayat").addEventListener("click", resetRiwayat);
el("resetKriteria").addEventListener("click", resetKriteria);
el("resetIbadah").addEventListener("click", resetIbadah);
el("cancelIbadahEdit").addEventListener("click", resetIbadah);
el("clearIbadahPin").addEventListener("click", clearIbadahPin);
el("toggleManualIbadah").addEventListener("click", () => {
ibadahManual = !ibadahManual;
el("ibadah_lat").readOnly = !ibadahManual;
el("ibadah_lng").readOnly = !ibadahManual;
el("toggleManualIbadah").textContent = ibadahManual ? "🔒 Kunci Koordinat" : "✏️ Edit Manual";
});
el("ibadah_lat").addEventListener("blur", syncIbadahManualPin);
el("ibadah_lng").addEventListener("blur", syncIbadahManualPin);
el("familySearch").addEventListener("input", renderKeluarga);
el("recalculateBtn").addEventListener("click", async () => { await api("/recalculate", { method: "POST" }); toast("SAW berhasil dihitung ulang."); await loadAll(); });
el("filterPengajuanStatus").addEventListener("change", async () => {
const status = el("filterPengajuanStatus").value;
state.pengajuan = await api(`/pengajuan${status ? '?status='+status : ''}`);
renderPengajuan();
});
el("savePengajuanStatus").addEventListener("click", async () => {
const id = el("pengajuanEditId").value;
if (!id) return;
await api(`/pengajuan/${id}/status`, {
method: "PUT",
body: JSON.stringify({
status: el("pengajuanNewStatus").value,
catatan_admin: el("pengajuanCatatan").value.trim(),
id_keluarga: el("pengajuanKeluarga").value || null,
}),
});
toast("Status pengajuan berhasil diperbarui.");
el("pengajuanDetailCard").style.display = "none";
await loadAll();
});
fillOptions("pekerjaan", labels.pekerjaan);
fillOptions("pendidikan", labels.pendidikan);
fillOptions("kondisi_rumah", labels.kondisi_rumah);
fillOptions("kondisi_kesehatan", labels.kondisi_kesehatan);
el("ibadah_jenis").innerHTML = IBADAH_TYPES.map((jenis) => `<option value="${jenis}">${jenis}</option>`).join("");
resetKeluarga(); resetBantuan(); resetRiwayat(); resetKriteria(); resetIbadah();
loadAll().catch((error) => toast(error.message));
</script>
</body>
</html>