Files
WEBGIS_Project/frontend/admin-poverty.html
T
tistopandita 9d22253e40 update_poverty
2026-06-11 00:41:49 +07:00

573 lines
33 KiB
HTML

<!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; }
.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); }
@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="peta">Preview Peta</button>
<a href="/poverty-map.html">Buka Peta Poverty</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="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: [], dashboard: null, analytics: null };
const today = new Date().toISOString().slice(0, 10);
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";
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("");
}
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>`;
}
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] = await Promise.all([
api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"), api("/riwayat-bantuan"), api("/kriteria")
]);
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria });
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria();
}
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 = ""; }
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.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(), 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();
});
el("resetKeluarga").addEventListener("click", resetKeluarga);
el("resetBantuan").addEventListener("click", resetBantuan);
el("resetRiwayat").addEventListener("click", resetRiwayat);
el("resetKriteria").addEventListener("click", resetKriteria);
el("familySearch").addEventListener("input", renderKeluarga);
el("recalculateBtn").addEventListener("click", async () => { await api("/recalculate", { method: "POST" }); toast("SAW berhasil dihitung ulang."); await loadAll(); });
fillOptions("pekerjaan", labels.pekerjaan);
fillOptions("pendidikan", labels.pendidikan);
fillOptions("kondisi_rumah", labels.kondisi_rumah);
fillOptions("kondisi_kesehatan", labels.kondisi_kesehatan);
resetKeluarga(); resetBantuan(); resetRiwayat(); resetKriteria();
loadAll().catch((error) => toast(error.message));
</script>
</body>
</html>