Files
WEBGIS_Project/frontend/admin-poverty.html
T
2026-06-10 22:46:14 +07:00

886 lines
32 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</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;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin="" />
<style>
:root {
--accent: #1b4332;
--accent-h: #2d6a4f;
--gold: #f4a261;
--bg: #f1f5f9;
--card: #ffffff;
--border: #e2e8f0;
--text: #0f172a;
--muted: #64748b;
--danger: #dc2626;
--blue: #2563eb;
--radius: 16px;
--shadow: 0 4px 24px rgba(15, 23, 42, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: "Inter", system-ui, sans-serif;
color: var(--text);
background: var(--bg);
}
button,
input,
select,
textarea {
font: inherit;
}
.topnav {
position: sticky;
top: 0;
z-index: 200;
display: flex;
align-items: center;
gap: 12px;
min-height: 62px;
padding: 0 24px;
color: #fff;
background: var(--text);
box-shadow: var(--shadow);
}
.topnav h1 {
flex: 1;
margin: 0;
font-size: 1.05rem;
}
.topnav a {
color: #fcd34d;
text-decoration: none;
font-size: 0.88rem;
}
.tab-bar {
display: flex;
gap: 4px;
max-width: 1240px;
margin: 0 auto;
padding: 16px 20px 0;
}
.tab-btn {
padding: 10px 20px;
border: 1px solid var(--border);
border-bottom: 0;
border-radius: 12px 12px 0 0;
color: var(--muted);
background: #e2e8f0;
cursor: pointer;
font-weight: 700;
}
.tab-btn.active {
color: var(--accent);
background: var(--card);
}
.page {
max-width: 1240px;
margin: 0 auto;
padding: 0 20px 56px;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.grid {
display: grid;
grid-template-columns: 380px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.card {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
box-shadow: var(--shadow);
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px;
border-bottom: 1px solid var(--border);
}
.card-header h2 {
margin: 0;
color: var(--accent);
font-size: 1rem;
}
.card-body {
padding: 18px;
}
.form-grid {
display: grid;
gap: 12px;
}
.row-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
label {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 0.82rem;
font-weight: 700;
}
input,
select,
textarea {
width: 100%;
min-height: 40px;
border: 1px solid var(--border);
border-radius: 12px;
padding: 9px 11px;
color: var(--text);
background: #fff;
outline: none;
}
textarea {
min-height: 76px;
resize: vertical;
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--accent-h);
box-shadow: 0 0 0 3px rgba(27, 67, 50, 0.12);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.btn {
min-height: 40px;
border: 0;
border-radius: 12px;
padding: 0 14px;
color: #fff;
background: var(--accent);
cursor: pointer;
font-weight: 800;
}
.btn:hover {
background: var(--accent-h);
}
.btn.secondary {
color: var(--text);
background: #e2e8f0;
}
.btn.danger {
background: var(--danger);
}
.btn.gold {
color: #422006;
background: var(--gold);
}
.filters {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid var(--border);
}
.table-wrap {
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
}
th,
td {
padding: 12px 14px;
border-bottom: 1px solid var(--border);
text-align: left;
vertical-align: middle;
}
th {
color: var(--muted);
font-size: 0.74rem;
text-transform: uppercase;
letter-spacing: 0.08em;
background: #f8fafc;
}
.badge {
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 4px 8px;
color: #fff;
font-size: 0.72rem;
font-weight: 800;
text-transform: uppercase;
}
.badge.sangat_miskin {
background: #7f1d1d;
}
.badge.miskin {
background: #ea580c;
}
.badge.rentan_miskin {
color: #422006;
background: #eab308;
}
.mini-actions {
display: flex;
gap: 6px;
}
.mini-actions button {
min-height: 32px;
border: 0;
border-radius: 10px;
padding: 0 9px;
color: #fff;
background: var(--blue);
cursor: pointer;
font-weight: 700;
}
.mini-actions button:last-child {
background: var(--danger);
}
#previewMap,
#ibadahMap,
#keluargaMap {
width: 100%;
min-height: 330px;
border-radius: var(--radius);
overflow: hidden;
}
.notice {
margin: 12px 0 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
}
.export-grid {
display: grid;
grid-template-columns: 1fr 140px auto auto;
gap: 10px;
padding: 18px;
}
@media (max-width: 980px) {
.grid,
.filters,
.export-grid {
grid-template-columns: 1fr;
}
.tab-bar {
overflow-x: auto;
}
}
</style>
</head>
<body>
<nav class="topnav">
<h1>Admin Poverty Mapping</h1>
<a href="poverty-map.html">Peta Publik</a>
<a href="admin.html">Admin SPBU</a>
</nav>
<div class="tab-bar">
<button class="tab-btn active" data-tab="ibadah">Tempat Ibadah</button>
<button class="tab-btn" data-tab="keluarga">Keluarga Miskin</button>
<button class="tab-btn" data-tab="preview">Peta Preview</button>
</div>
<main class="page">
<section class="tab-content active" id="tab-ibadah">
<div class="grid">
<article class="card">
<div class="card-header">
<h2>Form Tempat Ibadah</h2>
</div>
<div class="card-body">
<form class="form-grid" id="ibadahForm">
<input type="hidden" id="ibadahId" />
<label>Nama
<input id="ibadahNama" required placeholder="Masjid/Gereja/Vihara..." />
</label>
<label>Jenis
<select id="ibadahJenis" required>
<option value="masjid">Masjid</option>
<option value="gereja">Gereja</option>
<option value="pura">Pura</option>
<option value="vihara">Vihara</option>
<option value="kelenteng">Kelenteng</option>
<option value="lainnya">Lainnya</option>
</select>
</label>
<label>Alamat
<textarea id="ibadahAlamat"></textarea>
</label>
<label>Kelurahan
<input id="ibadahKelurahan" />
</label>
<div class="row-2">
<label>Latitude
<input id="ibadahLat" type="number" step="any" required />
</label>
<label>Longitude
<input id="ibadahLng" type="number" step="any" required />
</label>
</div>
<div id="ibadahMap"></div>
<p class="notice">Klik peta untuk mengisi koordinat tempat ibadah otomatis.</p>
<div class="actions">
<button class="btn" type="submit">Simpan</button>
<button class="btn secondary" type="button" id="resetIbadah">Reset</button>
</div>
</form>
</div>
</article>
<article class="card">
<div class="card-header">
<h2>Daftar Tempat Ibadah</h2>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Nama</th>
<th>Jenis</th>
<th>Kelurahan</th>
<th>Koordinat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="ibadahTable"></tbody>
</table>
</div>
</article>
</div>
</section>
<section class="tab-content" id="tab-keluarga">
<div class="grid">
<article class="card">
<div class="card-header">
<h2>Form Keluarga Miskin</h2>
</div>
<div class="card-body">
<form class="form-grid" id="keluargaForm">
<input type="hidden" id="keluargaId" />
<label>Nama Kepala Keluarga
<input id="keluargaNama" required />
</label>
<div class="row-2">
<label>Jumlah Anggota
<input id="keluargaAnggota" type="number" min="1" value="1" required />
</label>
<label>Kategori
<select id="keluargaKategori" required>
<option value="sangat_miskin">Sangat Miskin</option>
<option value="miskin">Miskin</option>
<option value="rentan_miskin">Rentan Miskin</option>
</select>
</label>
</div>
<label>Alamat
<textarea id="keluargaAlamat"></textarea>
</label>
<div class="row-2">
<label>RT
<input id="keluargaRt" maxlength="5" />
</label>
<label>RW
<input id="keluargaRw" maxlength="5" />
</label>
</div>
<label>Kelurahan
<input id="keluargaKelurahan" />
</label>
<label>Keterangan
<textarea id="keluargaKeterangan"></textarea>
</label>
<div class="row-2">
<label>Latitude
<input id="keluargaLat" type="number" step="any" required />
</label>
<label>Longitude
<input id="keluargaLng" type="number" step="any" required />
</label>
</div>
<div id="keluargaMap"></div>
<p class="notice">Klik peta untuk mengisi koordinat keluarga otomatis.</p>
<div class="actions">
<button class="btn" type="submit">Simpan</button>
<button class="btn secondary" type="button" id="resetKeluarga">Reset</button>
</div>
</form>
</div>
</article>
<article class="card">
<div class="card-header">
<h2>Daftar Keluarga</h2>
</div>
<div class="filters">
<select id="filterKategori">
<option value="">Semua Kategori</option>
<option value="sangat_miskin">Sangat Miskin</option>
<option value="miskin">Miskin</option>
<option value="rentan_miskin">Rentan Miskin</option>
</select>
<input id="filterKelurahan" placeholder="Filter kelurahan" />
<input id="searchNama" placeholder="Cari nama KK" />
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Nama KK</th>
<th>Anggota</th>
<th>Kategori</th>
<th>Kelurahan</th>
<th>Koordinat</th>
<th>Aksi</th>
</tr>
</thead>
<tbody id="keluargaTable"></tbody>
</table>
</div>
</article>
</div>
</section>
<section class="tab-content" id="tab-preview">
<article class="card">
<div class="card-header">
<h2>Peta Preview & Export</h2>
</div>
<div class="export-grid">
<select id="exportIbadah"></select>
<input id="exportRadius" type="number" min="100" max="2000" value="500" />
<button class="btn gold" id="exportCsv" type="button">Export CSV</button>
<button class="btn" id="printReport" type="button">Cetak</button>
</div>
<div class="card-body">
<div id="previewMap"></div>
<p class="notice">Preview menampilkan seluruh tempat ibadah dan keluarga. Export memakai tempat ibadah dan radius yang dipilih.</p>
</div>
</article>
</section>
</main>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<script>
const API_BASE = "/api/poverty";
const kategoriLabel = {
sangat_miskin: "Sangat Miskin",
miskin: "Miskin",
rentan_miskin: "Rentan Miskin"
};
const worshipIcon = { masjid: "🕌", gereja: "⛪", pura: "🛕", vihara: "☸️", kelenteng: "🏮", lainnya: "⭐" };
let apiAvailable = true;
let tempatIbadah = [
{ id: 1, nama: "Masjid Jami Sultan Syarif Abdurrahman", jenis: "masjid", alamat: "Jl. Tanjungpura", kelurahan: "Dalam Bugis", lat: -0.0226, lng: 109.3316 },
{ id: 2, nama: "Gereja Bethel Indonesia", jenis: "gereja", alamat: "Jl. Gajahmada", kelurahan: "Sungai Bangkong", lat: -0.0334, lng: 109.3253 },
{ id: 3, nama: "Vihara Bodhisattva", jenis: "vihara", alamat: "Jl. Diponegoro", kelurahan: "Mariana", lat: -0.0289, lng: 109.3401 }
];
let keluarga = [
{ id: 1, nama_kk: "Ahmad Fauzi", jumlah_anggota: 5, kategori: "sangat_miskin", alamat: "Dalam Bugis", rt: "01", rw: "02", kelurahan: "Dalam Bugis", keterangan: "Prioritas bantuan pangan.", lat: -0.0231, lng: 109.3321 },
{ id: 2, nama_kk: "Siti Rahmah", jumlah_anggota: 3, kategori: "miskin", alamat: "Dalam Bugis", rt: "02", rw: "02", kelurahan: "Dalam Bugis", keterangan: "Membutuhkan dukungan pendidikan.", lat: -0.0219, lng: 109.3308 },
{ id: 3, nama_kk: "Budi Santoso", jumlah_anggota: 6, kategori: "rentan_miskin", alamat: "Sungai Bangkong", rt: "03", rw: "01", kelurahan: "Sungai Bangkong", keterangan: "Pendapatan tidak tetap.", lat: -0.0341, lng: 109.3261 },
{ id: 4, nama_kk: "Maria Goretti", jumlah_anggota: 4, kategori: "miskin", alamat: "Mariana", rt: "04", rw: "01", kelurahan: "Mariana", keterangan: "Butuh verifikasi lanjutan.", lat: -0.0295, lng: 109.3412 },
{ id: 5, nama_kk: "Lim Ah Kow", jumlah_anggota: 2, kategori: "sangat_miskin", alamat: "Mariana", rt: "05", rw: "03", kelurahan: "Mariana", keterangan: "Lansia tinggal sendiri.", lat: -0.0501, lng: 109.36 }
];
const elements = {
ibadahTable: document.getElementById("ibadahTable"),
keluargaTable: document.getElementById("keluargaTable"),
exportIbadah: document.getElementById("exportIbadah"),
exportRadius: document.getElementById("exportRadius"),
filterKategori: document.getElementById("filterKategori"),
filterKelurahan: document.getElementById("filterKelurahan"),
searchNama: document.getElementById("searchNama")
};
const ibadahMap = createMap("ibadahMap");
const keluargaMap = createMap("keluargaMap");
const previewMap = createMap("previewMap");
let ibadahPickMarker;
let keluargaPickMarker;
const previewLayer = L.layerGroup().addTo(previewMap);
function createMap(id) {
const map = L.map(id).setView([-0.0263, 109.3425], 13);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
maxZoom: 19,
attribution: '&copy; OpenStreetMap'
}).addTo(map);
return map;
}
function geoJsonToRows(payload) {
if (payload?.type === "FeatureCollection") {
return payload.features.map((feature) => ({
...feature.properties,
lng: Number(feature.geometry.coordinates[0]),
lat: Number(feature.geometry.coordinates[1])
}));
}
if (Array.isArray(payload?.data)) return payload.data;
if (Array.isArray(payload)) return payload;
return [];
}
async function apiRequest(path, options = {}) {
const response = await fetch(`${API_BASE}${path}`, {
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const contentType = response.headers.get("content-type") || "";
return contentType.includes("application/json") ? response.json() : response.text();
}
async function loadData() {
try {
const [ibadahPayload, keluargaPayload] = await Promise.all([
apiRequest("/tempat-ibadah"),
apiRequest("/keluarga")
]);
tempatIbadah = geoJsonToRows(ibadahPayload);
keluarga = geoJsonToRows(keluargaPayload);
apiAvailable = true;
} catch {
apiAvailable = false;
}
renderAll();
}
function localCreate(collection, payload) {
const nextId = Math.max(0, ...collection.map((item) => Number(item.id))) + 1;
collection.push({ ...payload, id: nextId });
}
function localUpdate(collection, id, payload) {
const index = collection.findIndex((item) => String(item.id) === String(id));
if (index >= 0) collection[index] = { ...collection[index], ...payload, id: collection[index].id };
}
function localDelete(collection, id) {
const index = collection.findIndex((item) => String(item.id) === String(id));
if (index >= 0) collection.splice(index, 1);
}
function getIbadahForm() {
return {
nama: document.getElementById("ibadahNama").value.trim(),
jenis: document.getElementById("ibadahJenis").value,
alamat: document.getElementById("ibadahAlamat").value.trim(),
kelurahan: document.getElementById("ibadahKelurahan").value.trim(),
lat: Number(document.getElementById("ibadahLat").value),
lng: Number(document.getElementById("ibadahLng").value)
};
}
function getKeluargaForm() {
return {
nama_kk: document.getElementById("keluargaNama").value.trim(),
jumlah_anggota: Number(document.getElementById("keluargaAnggota").value),
kategori: document.getElementById("keluargaKategori").value,
alamat: document.getElementById("keluargaAlamat").value.trim(),
rt: document.getElementById("keluargaRt").value.trim(),
rw: document.getElementById("keluargaRw").value.trim(),
kelurahan: document.getElementById("keluargaKelurahan").value.trim(),
keterangan: document.getElementById("keluargaKeterangan").value.trim(),
lat: Number(document.getElementById("keluargaLat").value),
lng: Number(document.getElementById("keluargaLng").value)
};
}
function setIbadahForm(item) {
document.getElementById("ibadahId").value = item?.id || "";
document.getElementById("ibadahNama").value = item?.nama || "";
document.getElementById("ibadahJenis").value = item?.jenis || "masjid";
document.getElementById("ibadahAlamat").value = item?.alamat || "";
document.getElementById("ibadahKelurahan").value = item?.kelurahan || "";
document.getElementById("ibadahLat").value = item?.lat ?? "";
document.getElementById("ibadahLng").value = item?.lng ?? "";
if (item?.lat && item?.lng) setPickMarker(ibadahMap, "ibadah", item.lat, item.lng);
}
function setKeluargaForm(item) {
document.getElementById("keluargaId").value = item?.id || "";
document.getElementById("keluargaNama").value = item?.nama_kk || "";
document.getElementById("keluargaAnggota").value = item?.jumlah_anggota || 1;
document.getElementById("keluargaKategori").value = item?.kategori || "miskin";
document.getElementById("keluargaAlamat").value = item?.alamat || "";
document.getElementById("keluargaRt").value = item?.rt || "";
document.getElementById("keluargaRw").value = item?.rw || "";
document.getElementById("keluargaKelurahan").value = item?.kelurahan || "";
document.getElementById("keluargaKeterangan").value = item?.keterangan || "";
document.getElementById("keluargaLat").value = item?.lat ?? "";
document.getElementById("keluargaLng").value = item?.lng ?? "";
if (item?.lat && item?.lng) setPickMarker(keluargaMap, "keluarga", item.lat, item.lng);
}
function setPickMarker(map, type, lat, lng) {
const markerRef = type === "ibadah" ? "ibadahPickMarker" : "keluargaPickMarker";
if (markerRef === "ibadahPickMarker" && ibadahPickMarker) ibadahPickMarker.remove();
if (markerRef === "keluargaPickMarker" && keluargaPickMarker) keluargaPickMarker.remove();
const marker = L.marker([lat, lng]).addTo(map);
map.setView([lat, lng], 16);
if (markerRef === "ibadahPickMarker") ibadahPickMarker = marker;
if (markerRef === "keluargaPickMarker") keluargaPickMarker = marker;
}
function renderIbadahTable() {
elements.ibadahTable.innerHTML = tempatIbadah.map((item) => `
<tr>
<td><strong>${item.nama}</strong><br><small>${item.alamat || "-"}</small></td>
<td>${worshipIcon[item.jenis] || "⭐"} ${item.jenis}</td>
<td>${item.kelurahan || "-"}</td>
<td>${Number(item.lat).toFixed(5)}, ${Number(item.lng).toFixed(5)}</td>
<td><div class="mini-actions"><button type="button" onclick="editIbadah(${item.id})">Edit</button><button type="button" onclick="deleteIbadah(${item.id})">Hapus</button></div></td>
</tr>
`).join("");
}
function filteredKeluarga() {
const kategori = elements.filterKategori.value;
const kelurahan = elements.filterKelurahan.value.toLowerCase().trim();
const search = elements.searchNama.value.toLowerCase().trim();
return keluarga.filter((item) => {
if (kategori && item.kategori !== kategori) return false;
if (kelurahan && !(item.kelurahan || "").toLowerCase().includes(kelurahan)) return false;
if (search && !(item.nama_kk || "").toLowerCase().includes(search)) return false;
return true;
});
}
function renderKeluargaTable() {
elements.keluargaTable.innerHTML = filteredKeluarga().map((item) => `
<tr>
<td><strong>${item.nama_kk}</strong><br><small>${item.alamat || "-"}</small></td>
<td>${item.jumlah_anggota}</td>
<td><span class="badge ${item.kategori}">${kategoriLabel[item.kategori]}</span></td>
<td>${item.kelurahan || "-"}</td>
<td>${Number(item.lat).toFixed(5)}, ${Number(item.lng).toFixed(5)}</td>
<td><div class="mini-actions"><button type="button" onclick="editKeluarga(${item.id})">Edit</button><button type="button" onclick="deleteKeluarga(${item.id})">Hapus</button></div></td>
</tr>
`).join("");
}
function makeWorshipMarker(item) {
return L.divIcon({
className: "",
html: `<div style="display:grid;width:34px;height:34px;place-items:center;border:2px solid #fff;border-radius:50%;background:#f4a261;box-shadow:0 8px 18px #0002">${worshipIcon[item.jenis] || "⭐"}</div>`,
iconSize: [34, 34],
iconAnchor: [17, 17]
});
}
function renderPreview() {
previewLayer.clearLayers();
tempatIbadah.forEach((item) => L.marker([item.lat, item.lng], { icon: makeWorshipMarker(item) }).bindPopup(`<strong>${item.nama}</strong>`).addTo(previewLayer));
keluarga.forEach((item) => L.circleMarker([item.lat, item.lng], { radius: 7, color: "#2563eb", fillColor: "#2563eb", fillOpacity: 0.8 }).bindPopup(`<strong>${item.nama_kk}</strong>`).addTo(previewLayer));
elements.exportIbadah.innerHTML = tempatIbadah.map((item) => `<option value="${item.id}">${item.nama}</option>`).join("");
}
function renderAll() {
renderIbadahTable();
renderKeluargaTable();
renderPreview();
}
function familiesInExportRadius() {
const ibadah = tempatIbadah.find((item) => String(item.id) === String(elements.exportIbadah.value));
const radius = Number(elements.exportRadius.value || 500);
if (!ibadah) return [];
return keluarga
.map((item) => ({ ...item, distance: L.latLng(item.lat, item.lng).distanceTo(L.latLng(ibadah.lat, ibadah.lng)) }))
.filter((item) => item.distance <= radius)
.sort((a, b) => a.distance - b.distance);
}
function downloadCsv(rows) {
const header = ["nama_kk", "jumlah_anggota", "kategori", "kelurahan", "alamat", "rt", "rw", "jarak_meter"];
const body = rows.map((item) => header.map((key) => `"${String(key === "jarak_meter" ? Math.round(item.distance || 0) : item[key] || "").replaceAll('"', '""')}"`).join(","));
const blob = new Blob([[header.join(","), ...body].join("\n")], { type: "text/csv;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "laporan-keluarga-radius.csv";
link.click();
URL.revokeObjectURL(link.href);
}
function printRows(rows) {
const ibadah = tempatIbadah.find((item) => String(item.id) === String(elements.exportIbadah.value));
const html = `
<title>Laporan Keluarga Dalam Radius</title>
<style>body{font-family:Inter,Arial,sans-serif;padding:24px}table{width:100%;border-collapse:collapse}td,th{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f3f4f6}</style>
<h1>Laporan Keluarga Dalam Radius</h1>
<p>Tempat ibadah: <strong>${ibadah?.nama || "-"}</strong> • Radius: ${elements.exportRadius.value} meter</p>
<table><thead><tr><th>Nama KK</th><th>Anggota</th><th>Kategori</th><th>Kelurahan</th><th>Jarak</th></tr></thead><tbody>
${rows.map((item) => `<tr><td>${item.nama_kk}</td><td>${item.jumlah_anggota}</td><td>${kategoriLabel[item.kategori]}</td><td>${item.kelurahan || "-"}</td><td>${Math.round(item.distance)} m</td></tr>`).join("")}
</tbody></table>
`;
const win = window.open("", "_blank");
win.document.write(html);
win.document.close();
win.print();
}
window.editIbadah = (id) => setIbadahForm(tempatIbadah.find((item) => String(item.id) === String(id)));
window.editKeluarga = (id) => setKeluargaForm(keluarga.find((item) => String(item.id) === String(id)));
window.deleteIbadah = async (id) => {
if (!confirm("Hapus tempat ibadah ini?")) return;
try {
if (apiAvailable) await apiRequest(`/tempat-ibadah/${id}`, { method: "DELETE" });
localDelete(tempatIbadah, id);
renderAll();
} catch (error) {
alert(`Gagal menghapus: ${error.message}`);
}
};
window.deleteKeluarga = async (id) => {
if (!confirm("Hapus keluarga ini?")) return;
try {
if (apiAvailable) await apiRequest(`/keluarga/${id}`, { method: "DELETE" });
localDelete(keluarga, id);
renderAll();
} catch (error) {
alert(`Gagal menghapus: ${error.message}`);
}
};
document.getElementById("ibadahForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("ibadahId").value;
const payload = getIbadahForm();
try {
if (apiAvailable) await apiRequest(id ? `/tempat-ibadah/${id}` : "/tempat-ibadah", { method: id ? "PUT" : "POST", body: JSON.stringify(payload) });
id ? localUpdate(tempatIbadah, id, payload) : localCreate(tempatIbadah, payload);
setIbadahForm(null);
renderAll();
} catch (error) {
alert(`Gagal menyimpan: ${error.message}`);
}
});
document.getElementById("keluargaForm").addEventListener("submit", async (event) => {
event.preventDefault();
const id = document.getElementById("keluargaId").value;
const payload = getKeluargaForm();
try {
if (apiAvailable) await apiRequest(id ? `/keluarga/${id}` : "/keluarga", { method: id ? "PUT" : "POST", body: JSON.stringify(payload) });
id ? localUpdate(keluarga, id, payload) : localCreate(keluarga, payload);
setKeluargaForm(null);
renderAll();
} catch (error) {
alert(`Gagal menyimpan: ${error.message}`);
}
});
document.getElementById("resetIbadah").addEventListener("click", () => setIbadahForm(null));
document.getElementById("resetKeluarga").addEventListener("click", () => setKeluargaForm(null));
[elements.filterKategori, elements.filterKelurahan, elements.searchNama].forEach((item) => item.addEventListener("input", renderKeluargaTable));
document.getElementById("exportCsv").addEventListener("click", async () => {
const ibadahId = elements.exportIbadah.value;
const radius = elements.exportRadius.value || 500;
if (apiAvailable) {
window.location.href = `${API_BASE}/export?ibadah_id=${encodeURIComponent(ibadahId)}&radius=${encodeURIComponent(radius)}`;
return;
}
downloadCsv(familiesInExportRadius());
});
document.getElementById("printReport").addEventListener("click", () => printRows(familiesInExportRadius()));
document.querySelectorAll(".tab-btn").forEach((button) => {
button.addEventListener("click", () => {
document.querySelectorAll(".tab-btn").forEach((item) => item.classList.remove("active"));
document.querySelectorAll(".tab-content").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
document.getElementById(`tab-${button.dataset.tab}`).classList.add("active");
setTimeout(() => {
ibadahMap.invalidateSize();
keluargaMap.invalidateSize();
previewMap.invalidateSize();
}, 100);
});
});
ibadahMap.on("click", (event) => {
document.getElementById("ibadahLat").value = event.latlng.lat.toFixed(7);
document.getElementById("ibadahLng").value = event.latlng.lng.toFixed(7);
setPickMarker(ibadahMap, "ibadah", event.latlng.lat, event.latlng.lng);
});
keluargaMap.on("click", (event) => {
document.getElementById("keluargaLat").value = event.latlng.lat.toFixed(7);
document.getElementById("keluargaLng").value = event.latlng.lng.toFixed(7);
setPickMarker(keluargaMap, "keluarga", event.latlng.lat, event.latlng.lng);
});
loadData();
</script>
</body>
</html>