573 lines
33 KiB
HTML
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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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: "© 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>
|