tambah fitur add tempat ibadah

This commit is contained in:
tistopandita
2026-06-11 13:04:28 +07:00
parent 6535ae815f
commit 7ef7620d2a
3 changed files with 426 additions and 8 deletions
+131
View File
@@ -1,6 +1,59 @@
const express = require("express");
const db = require("../db");
const TEMPAT_IBADAH_TYPES = ["Gereja Katolik", "GPIB", "GKI", "HKBP", "Pentakosta"];
const createTempatIbadah = `
CREATE TABLE IF NOT EXISTS tempat_ibadah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL,
alamat TEXT NOT NULL,
kelurahan VARCHAR(100) NOT NULL,
lat DOUBLE NOT NULL,
lng DOUBLE NOT NULL,
aktif TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`;
async function initTempatIbadahTable() {
await db.execute(createTempatIbadah);
const columns = await tableColumns("tempat_ibadah");
if (!columns.includes("aktif")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN aktif TINYINT(1) NOT NULL DEFAULT 1");
}
if (!columns.includes("created_at")) {
await db.execute("ALTER TABLE tempat_ibadah ADD COLUMN created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP");
}
await db.execute(`
UPDATE tempat_ibadah
SET jenis = 'Gereja Katolik'
WHERE jenis IS NULL
OR jenis = ''
OR jenis NOT IN ('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta')
`);
await db.execute(`
ALTER TABLE tempat_ibadah
MODIFY jenis ENUM('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta') NOT NULL
`);
await db.execute(`
UPDATE tempat_ibadah
SET jenis = 'Gereja Katolik'
WHERE jenis IS NULL
OR jenis = ''
OR jenis NOT IN ('Gereja Katolik','GPIB','GKI','HKBP','Pentakosta')
`);
}
(async () => {
try {
await initTempatIbadahTable();
} catch (e) {
console.error(e);
}
})();
const router = express.Router();
let tablesReady = false;
@@ -128,6 +181,8 @@ function buildFeature(item) {
async function ensureTables() {
if (tablesReady) return;
await initTempatIbadahTable();
await db.execute(`
CREATE TABLE IF NOT EXISTS users (
id_user INT AUTO_INCREMENT PRIMARY KEY,
@@ -492,6 +547,22 @@ function validateFamily(body) {
return null;
}
function validateIbadah(body) {
const required = ["nama", "jenis", "alamat", "kelurahan", "lat", "lng"];
for (const key of required) {
if (body[key] === undefined || body[key] === null || body[key] === "") {
return `${key} wajib diisi.`;
}
}
if (!TEMPAT_IBADAH_TYPES.includes(body.jenis)) {
return "Jenis tempat ibadah wajib valid.";
}
if (!Number.isFinite(Number(body.lat)) || !Number.isFinite(Number(body.lng))) {
return "Latitude dan longitude wajib valid.";
}
return null;
}
router.use(async (req, res, next) => {
try {
await ensureTables();
@@ -904,4 +975,64 @@ router.delete("/riwayat-bantuan/:id", async (req, res) => {
res.json({ success: true, message: "Riwayat bantuan berhasil dihapus." });
});
router.get("/ibadah", async (req, res) => {
try {
const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE aktif = 1 ORDER BY nama ASC");
res.json({ success: true, data: rows });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.get("/ibadah/:id", async (req, res) => {
try {
const [rows] = await db.execute("SELECT * FROM tempat_ibadah WHERE id = ? AND aktif = 1", [req.params.id]);
if (!rows.length) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, data: rows[0] });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
router.post("/ibadah", async (req, res) => {
const error = validateIbadah(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
"INSERT INTO tempat_ibadah (nama, jenis, alamat, kelurahan, lat, lng) VALUES (?, ?, ?, ?, ?, ?)",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng)],
);
res.status(201).json({ success: true, id: result.insertId, message: "Tempat ibadah berhasil ditambahkan." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
router.put("/ibadah/:id", async (req, res) => {
const error = validateIbadah(req.body);
if (error) return res.status(400).json({ success: false, message: error });
const body = req.body;
try {
const [result] = await db.execute(
"UPDATE tempat_ibadah SET nama=?, jenis=?, alamat=?, kelurahan=?, lat=?, lng=? WHERE id=? AND aktif = 1",
[body.nama, body.jenis, body.alamat, body.kelurahan, Number(body.lat), Number(body.lng), req.params.id],
);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, message: "Tempat ibadah berhasil diperbarui." });
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
});
router.delete("/ibadah/:id", async (req, res) => {
try {
const [result] = await db.execute("UPDATE tempat_ibadah SET aktif = 0 WHERE id = ?", [req.params.id]);
if (!result.affectedRows) return res.status(404).json({ success: false, message: "Tempat ibadah tidak ditemukan." });
res.json({ success: true, message: "Tempat ibadah berhasil dihapus." });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
module.exports = router;
+204 -7
View File
@@ -157,6 +157,25 @@
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; }
@@ -183,6 +202,7 @@
<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="peta">Preview Peta</button>
<a href="/poverty-map.html">Buka Peta Poverty</a>
<a href="/admin.html">Admin SPBU</a>
@@ -312,6 +332,46 @@
</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>
<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>Aksi</th></tr></thead>
<tbody id="ibadahBody"></tbody>
</table>
</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>
@@ -324,8 +384,11 @@
<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 state = { keluarga: [], bantuan: [], riwayat: [], kriteria: [], ibadah: [], 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" },
@@ -338,6 +401,7 @@
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 });
@@ -423,6 +487,87 @@
`).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><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="7">Belum ada data tempat ibadah.</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),
};
}
let map, layer;
function ensureMap() {
if (map) return;
@@ -449,11 +594,11 @@
}
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")
const [dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah] = await Promise.all([
api("/dashboard"), api("/analytics"), api("/keluarga"), api("/bantuan"), api("/riwayat-bantuan"), api("/kriteria"), api("/ibadah")
]);
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria });
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria();
Object.assign(state, { dashboard, analytics, keluarga, bantuan, riwayat, kriteria, ibadah });
renderDashboard(); renderKeluarga(); renderBantuan(); renderKriteria(); renderIbadah();
}
function familyPayload() {
@@ -486,6 +631,18 @@
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_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);
@@ -509,6 +666,22 @@
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("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.hapus = async (resource, id) => {
if (!confirm("Hapus/nonaktifkan data ini?")) return;
await api(`/${resource}/${id}`, { method: "DELETE" });
@@ -522,7 +695,13 @@
document.querySelectorAll(".section").forEach((item) => item.classList.remove("active"));
button.classList.add("active");
el(button.dataset.section).classList.add("active");
setTimeout(() => map?.invalidateSize(), 80);
setTimeout(() => {
map?.invalidateSize();
if (button.dataset.section === "ibadah") {
ensureIbadahMap();
ibadahMap?.invalidateSize();
}
}, 80);
});
});
@@ -553,11 +732,28 @@
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(); });
@@ -565,7 +761,8 @@
fillOptions("pendidikan", labels.pendidikan);
fillOptions("kondisi_rumah", labels.kondisi_rumah);
fillOptions("kondisi_kesehatan", labels.kondisi_kesehatan);
resetKeluarga(); resetBantuan(); resetRiwayat(); resetKriteria();
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>
+91 -1
View File
@@ -170,6 +170,19 @@
z-index: 2000;
}
.toast.show { opacity: 1; transform: translateY(0); }
.ibadah-legend { display: none; }
.ibadah-legend.active { display: block; }
.ibadah-map-marker {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 999px;
color: white;
border: 3px solid white;
box-shadow: 0 10px 24px rgba(15, 23, 42, .28);
font-size: 15px;
}
@media (max-width: 880px) {
body { overflow: hidden; }
.app { grid-template-columns: 1fr; }
@@ -222,6 +235,7 @@
<input id="searchInput" placeholder="Nama, alamat, atau wilayah" />
</label>
<label class="switch"><span>Heatmap persebaran</span><input id="heatToggle" type="checkbox" checked /></label>
<label class="switch"><span>⛪ Tempat Ibadah</span><input id="ibadahToggle" type="checkbox" /></label>
</section>
<section class="panel">
@@ -234,6 +248,17 @@
</div>
</section>
<section class="panel ibadah-legend" id="ibadahLegendPanel">
<h2>Legenda Tempat Ibadah</h2>
<div class="legend">
<span><i class="dot" style="background:#7c3aed"></i>Gereja Katolik</span>
<span><i class="dot" style="background:#2563eb"></i>GPIB</span>
<span><i class="dot" style="background:#0891b2"></i>GKI</span>
<span><i class="dot" style="background:#16a34a"></i>HKBP</span>
<span><i class="dot" style="background:#ea580c"></i>Pentakosta</span>
</div>
</section>
<section class="panel">
<h2>Statistik Wilayah</h2>
<div id="areaStats"></div>
@@ -262,11 +287,12 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const API = "/api/poverty";
const state = { rows: [], dashboard: null, markers: new Map() };
const state = { rows: [], dashboard: null, markers: new Map(), ibadah: [], ibadahLoaded: false };
const map = L.map("map", { zoomControl: true }).setView([-0.0263, 109.3425], 12);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19, attribution: "&copy; OpenStreetMap" }).addTo(map);
const markerLayer = L.layerGroup().addTo(map);
const heatLayer = L.layerGroup().addTo(map);
const ibadahLayer = L.layerGroup();
const el = (id) => document.getElementById(id);
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
@@ -274,6 +300,13 @@
const score = (value) => Number(value || 0).toFixed(3);
const badgeClass = (kategori) => kategori === "Sangat Membutuhkan" ? "sangat" : kategori === "Membutuhkan" ? "butuh" : kategori === "Rentan" ? "rentan" : "mampu";
const color = (kategori) => kategori === "Sangat Membutuhkan" ? "#dc2626" : kategori === "Membutuhkan" ? "#f97316" : kategori === "Rentan" ? "#facc15" : "#22c55e";
const ibadahColors = {
"Gereja Katolik": "#7c3aed",
GPIB: "#2563eb",
GKI: "#0891b2",
HKBP: "#16a34a",
Pentakosta: "#ea580c",
};
function toast(message) {
el("toast").textContent = message;
@@ -350,6 +383,47 @@
`;
}
function ibadahIcon(jenis) {
const markerColor = ibadahColors[jenis] || "#64748b";
return L.divIcon({
className: "",
html: `<div class="ibadah-map-marker" style="background:${markerColor}">⛪</div>`,
iconSize: [30, 30],
iconAnchor: [15, 30],
popupAnchor: [0, -28],
});
}
function ibadahPopupHtml(item) {
const markerColor = ibadahColors[item.jenis] || "#64748b";
return `
<div class="popup">
<h3>${esc(item.nama)}</h3>
<span class="badge" style="background:${markerColor}22;color:${markerColor};border:1px solid ${markerColor}44">${esc(item.jenis)}</span>
<p style="margin-top:10px">${esc(item.alamat)}<br>${esc(item.kelurahan)}</p>
</div>
`;
}
function renderIbadahMarkers() {
ibadahLayer.clearLayers();
state.ibadah.forEach((item) => {
const latlng = [Number(item.lat), Number(item.lng)];
if (!Number.isFinite(latlng[0]) || !Number.isFinite(latlng[1])) return;
L.marker(latlng, { icon: ibadahIcon(item.jenis) })
.bindPopup(ibadahPopupHtml(item))
.addTo(ibadahLayer);
});
}
async function loadIbadahLayer(force = false) {
if (state.ibadahLoaded && !force) return;
const payload = await fetchJson("/ibadah");
state.ibadah = payload.data || [];
state.ibadahLoaded = true;
renderIbadahMarkers();
}
function renderMarkers() {
markerLayer.clearLayers();
heatLayer.clearLayers();
@@ -408,6 +482,22 @@
let debounce;
["categoryFilter", "heatToggle"].forEach((id) => el(id).addEventListener("change", () => loadData().catch((error) => toast(error.message))));
el("ibadahToggle").addEventListener("change", async () => {
try {
if (el("ibadahToggle").checked) {
await loadIbadahLayer(true);
ibadahLayer.addTo(map);
el("ibadahLegendPanel").classList.add("active");
} else {
map.removeLayer(ibadahLayer);
el("ibadahLegendPanel").classList.remove("active");
}
} catch (error) {
el("ibadahToggle").checked = false;
el("ibadahLegendPanel").classList.remove("active");
toast(error.message);
}
});
el("searchInput").addEventListener("input", () => {
clearTimeout(debounce);
debounce = setTimeout(() => loadData().catch((error) => toast(error.message)), 260);