diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 0000000..3ea1645
--- /dev/null
+++ b/assets/js/app.js
@@ -0,0 +1,1357 @@
+let map;
+let rumahIbadah = [];
+let keluarga = [];
+let users = [];
+let musibah = [];
+let pemberdayaan = [];
+let rumahIbadahLayer;
+let keluargaLayer;
+let radiusLayer;
+let routingControl = null;
+let selectedMarker = null;
+
+const user = window.SIPANTAS_USER || {};
+const role = window.SIPANTAS_ROLE || 'guest';
+const caps = window.SIPANTAS_CAPS || {};
+const linkedRumahIbadahId = Number(user.rumah_ibadah_id || 0);
+
+const guideConfig = {
+ admin: {
+ title: 'Kelola sistem dari data sampai laporan',
+ text: 'Mulai dari validasi data, jalankan analisis, lalu pantau blank spot.',
+ actionTab: 'validasi',
+ actionLabel: 'Buka Verifikasi',
+ helper: 'Admin dapat mengelola data, verifikasi akun, menjalankan analisis, dan membuat laporan.',
+ steps: [
+ ['Verifikasi akun dan usulan', 'Buka menu Akun atau Verifikasi untuk memastikan data yang masuk layak dipakai.'],
+ ['Atur rumah ibadah', 'Pastikan radius, kuota, dan dana sosial setiap rumah ibadah sudah benar.'],
+ ['Jalankan analisis', 'Gunakan tombol analisis agar status Ditanggung dan Blank Spot diperbarui.'],
+ ['Pantau laporan', 'Export blank spot untuk prioritas bantuan pemerintah.']
+ ]
+ },
+ dinas: {
+ title: 'Pantau cakupan dan prioritas bantuan',
+ text: 'Fokus pada keluarga valid, blank spot, dan laporan prioritas.',
+ actionTab: 'peta',
+ actionLabel: 'Lihat Peta Data',
+ helper: 'Dinas dapat melihat cakupan bantuan, menjalankan analisis, dan mengunduh data blank spot.',
+ steps: [
+ ['Cek data valid', 'Buka Verifikasi untuk melihat usulan yang masih perlu dicek.'],
+ ['Jalankan analisis', 'Sistem mencocokkan keluarga dengan rumah ibadah berdasarkan radius dan kuota.'],
+ ['Lihat blank spot', 'Gunakan filter status Blank Spot pada peta.'],
+ ['Export laporan', 'Unduh data prioritas untuk koordinasi tindak lanjut.']
+ ]
+ },
+ rumah_ibadah: {
+ title: 'Atur layanan rumah ibadah kamu',
+ text: 'Perbarui radius, kuota, dan catat bantuan untuk keluarga tanggungan.',
+ actionTab: 'rumah-ibadah',
+ actionLabel: 'Atur Layanan',
+ helper: 'Pengurus rumah ibadah bisa mengatur radius layanan, kuota, dana sosial, dan mencatat penyaluran bantuan.',
+ steps: [
+ ['Buka Layanan Saya', 'Cek profil rumah ibadah yang terhubung dengan akunmu.'],
+ ['Atur radius dan kuota', 'Sesuaikan jangkauan bantuan dengan kemampuan layanan.'],
+ ['Cek tanggungan', 'Lihat keluarga yang masuk dalam radius setelah analisis dijalankan.'],
+ ['Catat bantuan', 'Simpan riwayat bantuan agar tidak terjadi tumpang tindih.']
+ ]
+ },
+ relawan: {
+ title: 'Bantu validasi data lapangan',
+ text: 'Input usulan, cek lokasi, lalu tandai status verifikasinya.',
+ actionTab: 'validasi',
+ actionLabel: 'Buka Validasi',
+ helper: 'Relawan membantu memasukkan usulan dan memvalidasi kondisi keluarga di lapangan.',
+ steps: [
+ ['Input usulan', 'Buka menu Input Keluarga dan klik lokasi rumah pada peta.'],
+ ['Lengkapi data', 'Isi identitas, tingkat kerentanan, dan kebutuhan utama.'],
+ ['Verifikasi', 'Tandai data valid atau tidak valid setelah survei.'],
+ ['Catat bantuan', 'Catat bantuan bila ikut mendistribusikan.']
+ ]
+ },
+ masyarakat: {
+ title: 'Ajukan keluarga yang membutuhkan bantuan',
+ text: 'Pilih lokasi, isi data keluarga, lalu kirim usulan untuk diverifikasi.',
+ actionTab: 'input',
+ actionLabel: 'Ajukan Bantuan',
+ helper: 'Masyarakat dapat mengajukan data keluarga yang membutuhkan bantuan dan melihat status usulannya.',
+ steps: [
+ ['Klik Ajukan Bantuan', 'Buka menu Ajukan Bantuan dari panel sebelah kiri.'],
+ ['Pilih lokasi rumah', 'Klik titik rumah pada peta sampai koordinat terisi.'],
+ ['Isi data keluarga', 'Lengkapi data KK, anggota keluarga, kerentanan, dan kebutuhan.'],
+ ['Kirim usulan', 'Data akan masuk sebagai Menunggu Verifikasi.']
+ ]
+ }
+};
+
+const statusColors = {
+ 'Ditanggung': '#16a34a',
+ 'Blank Spot': '#dc2626',
+ 'Menunggu Verifikasi': '#f59e0b',
+ 'Tidak Valid': '#6b7280',
+ 'Bantuan Pemerintah': '#7c3aed',
+ 'Butuh Bantuan Lanjutan': '#fb7185'
+};
+
+function initMap() {
+ map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
+ L.control.zoom({ position: 'bottomright' }).addTo(map);
+
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(map);
+
+ radiusLayer = L.layerGroup().addTo(map);
+ rumahIbadahLayer = L.layerGroup().addTo(map);
+ keluargaLayer = L.layerGroup().addTo(map);
+
+ map.on('click', handleMapClick);
+}
+
+async function handleMapClick(e) {
+ const lat = e.latlng.lat.toFixed(7);
+ const lng = e.latlng.lng.toFixed(7);
+ const activeTab = document.querySelector('.tab.active')?.dataset.tab || 'dashboard';
+
+ if (activeTab !== 'input' && activeTab !== 'musibah' && !(activeTab === 'rumah-ibadah' && caps.canCreateRI)) {
+ toast('Untuk mengisi titik lokasi, buka menu Input Keluarga, Musibah, atau Rumah Ibadah terlebih dahulu.');
+ setHelpContext(activeTab);
+ return;
+ }
+
+ if (selectedMarker) map.removeLayer(selectedMarker);
+ selectedMarker = L.marker([lat, lng]).addTo(map).bindPopup('Titik lokasi dipilih').openPopup();
+
+ if (activeTab === 'rumah-ibadah' && caps.canCreateRI) {
+ setValue('riLat', lat);
+ setValue('riLng', lng);
+ await fillAddress(lat, lng, 'riAlamat');
+ toast('Titik rumah ibadah dipilih. Lanjut lengkapi data layanan.');
+ } else if (activeTab === 'musibah') {
+ setValue('musibahLat', lat);
+ setValue('musibahLng', lng);
+ await fillAddress(lat, lng, 'musibahAlamat');
+ toast('Lokasi kejadian dipilih. Lanjut lengkapi laporan musibah.');
+ } else {
+ setValue('lat', lat);
+ setValue('lng', lng);
+ await fillAddress(lat, lng, 'kelAlamat');
+ toast('Titik rumah keluarga dipilih. Lanjut lengkapi form.');
+ }
+}
+
+async function fillAddress(lat, lng, targetId) {
+ try {
+ const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`;
+ const res = await fetch(url);
+ const data = await res.json();
+ setValue(targetId, data.display_name || '');
+ } catch (error) {
+ toast('Alamat otomatis gagal dimuat. Isi alamat secara manual.');
+ }
+}
+
+function initUI() {
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.addEventListener('click', () => setActiveTab(btn.dataset.tab));
+ });
+
+ onClick('btnSimpanKeluarga', simpanKeluarga);
+ onClick('btnBersihkanKeluarga', bersihkanFormKeluarga);
+ onClick('btnAnalisis', jalankanAnalisis);
+ onClick('btnRefresh', loadAllData);
+ onClick('btnLocate', locateMe);
+ onClick('btnFitMap', fitAllMarkers);
+ onClick('btnSimpanRI', simpanRumahIbadah);
+ onClick('btnRIBaru', resetFormRumahIbadah);
+ onClick('btnPrintSummary', () => window.print());
+ onClick('btnTambahAnggota', () => addAnggotaForm());
+ onClick('btnSimpanMusibah', simpanMusibah);
+ onInput('musibahKeluarga', isiLokasiMusibahDariKeluarga);
+ onClick('btnGoMusibah', () => setActiveTab('musibah'));
+ initAnggotaDefault();
+ const tanggalMusibah = document.getElementById('musibahTanggal');
+ if (tanggalMusibah && !tanggalMusibah.value) tanggalMusibah.value = new Date().toISOString().slice(0, 10);
+
+ onInput('filterStatus', renderAll);
+ onInput('searchText', renderAll);
+
+ const riRadius = document.getElementById('riRadius');
+ if (riRadius) {
+ riRadius.addEventListener('input', () => setText('riRadiusLabel', riRadius.value));
+ }
+
+ setupLayerButton('btnLayerRadius', radiusLayer);
+ setupLayerButton('btnLayerRI', rumahIbadahLayer);
+ setupLayerButton('btnLayerKK', keluargaLayer);
+ setupPanelToggle();
+ initGuides();
+ onClick('btnGoInput', () => { setActiveTab('input'); toast('Klik titik rumah pada peta, lalu lengkapi form.'); });
+ onClick('btnGoMap', () => setActiveTab('peta'));
+}
+
+function setupLayerButton(buttonId, layer) {
+ const button = document.getElementById(buttonId);
+ if (!button) return;
+
+ button.addEventListener('click', () => {
+ if (map.hasLayer(layer)) {
+ map.removeLayer(layer);
+ button.classList.remove('active');
+ } else {
+ layer.addTo(map);
+ button.classList.add('active');
+ }
+ });
+}
+
+function setupPanelToggle() {
+ const body = document.body;
+ const collapse = document.getElementById('btnPanelCollapse');
+ const open = document.getElementById('btnPanelOpen');
+
+ const setCollapsed = (value) => {
+ body.classList.toggle('panel-collapsed', value);
+ setTimeout(() => map && map.invalidateSize(), 260);
+ };
+
+ if (collapse) collapse.addEventListener('click', () => setCollapsed(true));
+ if (open) open.addEventListener('click', () => setCollapsed(false));
+}
+
+function setActiveTab(tabName) {
+ document.querySelectorAll('.tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tabName));
+ document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
+ const panel = document.getElementById(`tab-${tabName}`);
+ if (panel) panel.classList.add('active');
+ setHelpContext(tabName);
+}
+
+function getGuide() {
+ return guideConfig[role] || guideConfig.masyarakat;
+}
+
+function initGuides() {
+ const guide = getGuide();
+ setText('quickGuideTitle', guide.title);
+ setText('quickGuideText', guide.text);
+ setText('dashboardSubtitle', guide.helper);
+
+ const primary = document.getElementById('btnGuidePrimary');
+ if (primary) {
+ primary.textContent = guide.actionLabel;
+ primary.addEventListener('click', () => setActiveTab(guide.actionTab));
+ }
+
+ const roleSteps = document.getElementById('roleSteps');
+ if (roleSteps) {
+ roleSteps.innerHTML = guide.steps.map((step, index) => `
+
+ ${index + 1}
+ ${escapeHtml(step[0])}
+ ${escapeHtml(step[1])}
+
+ `).join('');
+ }
+
+ const openGuide = () => showGuideModal(false);
+ onClick('btnOpenGuide', openGuide);
+ onClick('btnCloseGuide', closeGuideModal);
+ onClick('guideBackdrop', closeGuideModal);
+ onClick('btnGuideLater', closeGuideModal);
+ onClick('btnGuideAction', () => {
+ closeGuideModal();
+ setActiveTab(guide.actionTab);
+ });
+
+ const firstKey = `sipantas-guide-seen-${role}`;
+ if (!localStorage.getItem(firstKey)) {
+ setTimeout(() => showGuideModal(true), 600);
+ localStorage.setItem(firstKey, '1');
+ }
+
+ setHelpContext('dashboard');
+}
+
+function showGuideModal(isFirstTime = false) {
+ const guide = getGuide();
+ setText('guideTitle', guide.title);
+ setText('guideDescription', isFirstTime ? 'Sebelum mulai, lihat alur singkat agar tidak bingung memakai fitur utama.' : guide.text);
+ setText('btnGuideAction', guide.actionLabel);
+
+ const list = document.getElementById('guideList');
+ if (list) {
+ list.innerHTML = guide.steps.map((step, index) => `
+
+
${index + 1}
+
${escapeHtml(step[0])}${escapeHtml(step[1])}
+
+ `).join('');
+ }
+
+ const modal = document.getElementById('guideModal');
+ if (modal) {
+ modal.classList.add('show');
+ modal.setAttribute('aria-hidden', 'false');
+ }
+}
+
+function closeGuideModal() {
+ const modal = document.getElementById('guideModal');
+ if (modal) {
+ modal.classList.remove('show');
+ modal.setAttribute('aria-hidden', 'true');
+ }
+}
+
+function setHelpContext(tabName) {
+ const helper = document.getElementById('mapHelperText');
+ if (!helper) return;
+ const labels = {
+ dashboard: 'Ringkasan ada di panel. Peta tetap bisa digeser dan diperbesar.',
+ peta: 'Klik marker untuk melihat detail. Gunakan filter pada panel untuk mempersempit data.',
+ input: 'Klik titik rumah keluarga di peta. Koordinat dan alamat akan masuk ke form.',
+ 'rumah-ibadah': caps.canCreateRI ? 'Klik lokasi rumah ibadah pada peta, lalu isi radius, kuota, dan dana sosial.' : 'Pilih rumah ibadah dari daftar untuk mengatur radius dan kuota layanan.',
+ validasi: 'Pilih data pada daftar validasi, fokuskan lokasi, lalu ubah status verifikasi.',
+ bantuan: 'Gunakan tombol Rute untuk melihat jalur dan Catat Bantuan untuk menyimpan riwayat.',
+ musibah: 'Pilih keluarga terkait atau klik lokasi kejadian pada peta, lalu kirim laporan musibah.',
+ pemberdayaan: 'Daftar ini berisi anggota keluarga yang masih mampu bekerja dan bisa diarahkan ke program produktif.',
+ laporan: 'Gunakan panel laporan untuk mengunduh atau mencetak ringkasan.',
+ akun: 'Setujui akun resmi yang sudah sesuai, atau tolak jika datanya tidak valid.'
+ };
+ helper.textContent = labels[tabName] || 'Geser atau zoom peta untuk melihat sebaran data.';
+}
+
+async function loadAllData() {
+ try {
+ const requests = [
+ fetch('api/rumah_ibadah.php'),
+ fetch('api/keluarga.php'),
+ fetch('api/musibah.php')
+ ];
+ if (caps.canManagePemberdayaan) requests.push(fetch('api/pemberdayaan.php'));
+ const responses = await Promise.all(requests);
+ const [riJson, kelJson, musibahJson, pemberdayaanJson] = await Promise.all(responses.map(r => r.json()));
+
+ if (!riJson.success) throw new Error(riJson.message || 'Gagal memuat rumah ibadah');
+ if (!kelJson.success) throw new Error(kelJson.message || 'Gagal memuat keluarga');
+ if (!musibahJson.success) throw new Error(musibahJson.message || 'Gagal memuat laporan musibah');
+
+ rumahIbadah = riJson.data || [];
+ keluarga = kelJson.data || [];
+ musibah = musibahJson.data || [];
+ pemberdayaan = pemberdayaanJson && pemberdayaanJson.success ? (pemberdayaanJson.data || []) : [];
+
+ if (role === 'rumah_ibadah' && linkedRumahIbadahId) {
+ const own = rumahIbadah.find(ri => Number(ri.id) === linkedRumahIbadahId);
+ if (own && !getValue('riId')) fillRumahIbadahForm(own.id);
+ }
+
+ if (caps.canManageUsers) {
+ await loadUsers();
+ }
+
+ renderAll();
+ } catch (error) {
+ toast(error.message || 'Gagal memuat data.');
+ }
+}
+
+async function loadUsers() {
+ if (!caps.canManageUsers) return;
+ try {
+ const res = await fetch('api/users.php');
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message || 'Gagal memuat akun');
+ users = json.data || [];
+ } catch (error) {
+ toast(error.message || 'Gagal memuat daftar akun.');
+ }
+}
+
+function renderAll() {
+ renderRumahIbadah();
+ renderKeluarga();
+ updateDashboard();
+ renderListKeluarga();
+ renderListRumahIbadah();
+ renderListValidasi();
+ renderListBantuan();
+ renderListMusibah();
+ renderListPemberdayaan();
+ populateMusibahKeluarga();
+ renderListAkun();
+}
+
+function renderRumahIbadah() {
+ rumahIbadahLayer.clearLayers();
+ radiusLayer.clearLayers();
+
+ rumahIbadah.forEach(ri => {
+ const lat = Number(ri.lat);
+ const lng = Number(ri.lng);
+ const radius = Number(ri.radius || 500);
+ const jumlahTanggungan = Number(ri.jumlah_tanggungan || 0);
+ const sisaKuota = Math.max(0, Number(ri.kuota || 0) - jumlahTanggungan);
+
+ L.circle([lat, lng], {
+ radius,
+ color: '#2563eb',
+ fillColor: '#60a5fa',
+ fillOpacity: 0.12,
+ weight: 1.4
+ }).addTo(radiusLayer);
+
+ const marker = L.circleMarker([lat, lng], {
+ radius: 10,
+ color: '#1d4ed8',
+ fillColor: '#2563eb',
+ fillOpacity: 0.92,
+ weight: 2
+ }).addTo(rumahIbadahLayer);
+
+ const editBtn = canEditRumahIbadah(ri)
+ ? ``
+ : '';
+ const deleteBtn = caps.canCreateRI
+ ? ``
+ : '';
+ const tanggunganBtn = ``;
+
+ marker.bindPopup(`
+
+ Kategori: ${escapeHtml(ri.kategori)}
+ Pengurus: ${escapeHtml(ri.pengurus || '-')}
+ Kontak: ${escapeHtml(ri.kontak || '-')}
+ Dana Sosial: ${rupiah(ri.dana_sosial)}
+ Kuota: ${ri.kuota} KK • Sisa: ${sisaKuota}
+ Radius: ${ri.radius} meter
+ Tanggungan aktif: ${jumlahTanggungan} KK
+ ${escapeHtml(ri.alamat || '')}
+ ${editBtn}${tanggunganBtn}${deleteBtn}
+ `);
+ });
+}
+
+function renderKeluarga() {
+ keluargaLayer.clearLayers();
+ getFilteredKeluarga().forEach(kel => {
+ const color = statusColors[kel.status_bantuan] || '#6b7280';
+ const lat = Number(kel.lat);
+ const lng = Number(kel.lng);
+
+ const marker = L.circleMarker([lat, lng], {
+ radius: 8,
+ color,
+ fillColor: color,
+ fillOpacity: 0.92,
+ weight: 2
+ }).addTo(keluargaLayer);
+
+ const routeBtn = kel.rumah_ibadah_id && kel.ri_lat && kel.ri_lng
+ ? ``
+ : '';
+ const verifyBtns = caps.canVerify
+ ? `
+ `
+ : '';
+ const bantuanBtn = canRecordBantuanFor(kel)
+ ? ``
+ : '';
+ const musibahBtn = caps.canReportMusibah
+ ? ``
+ : '';
+ const deleteBtn = caps.canCreateRI
+ ? ``
+ : '';
+
+ marker.bindPopup(`
+
+ Status Verifikasi: ${escapeHtml(kel.status_verifikasi)}
+ Status Bantuan: ${escapeHtml(kel.status_bantuan)}
+ Penanggung: ${escapeHtml(kel.nama_rumah_ibadah || '-')}
+ Kerentanan: ${escapeHtml(kel.kerentanan)}
+ Kebutuhan: ${escapeHtml(kel.kebutuhan)}
+ Anggota KK: ${Number(kel.jumlah_anggota || 0)} orang • Mampu bekerja: ${Number(kel.jumlah_mampu_bekerja || 0)}
+ ${escapeHtml(kel.alamat || '')}
+ ${routeBtn}${bantuanBtn}${musibahBtn}${verifyBtns}${deleteBtn}
+ `);
+ });
+}
+
+function canEditRumahIbadah(ri) {
+ if (caps.canCreateRI) return true;
+ return role === 'rumah_ibadah' && linkedRumahIbadahId && Number(ri.id) === linkedRumahIbadahId;
+}
+
+function canRecordBantuanFor(kel) {
+ if (!caps.canRecordBantuan || kel.status_bantuan !== 'Ditanggung') return false;
+ if (role === 'rumah_ibadah') return linkedRumahIbadahId && Number(kel.rumah_ibadah_id) === linkedRumahIbadahId;
+ return true;
+}
+
+function getFilteredKeluarga() {
+ const status = getValue('filterStatus') || 'semua';
+ const keyword = (getValue('searchText') || '').toLowerCase();
+
+ return keluarga.filter(k => {
+ const matchStatus = status === 'semua' || k.status_bantuan === status;
+ const text = `${k.nama_kk} ${k.alamat} ${k.kerentanan} ${k.kebutuhan} ${k.nama_rumah_ibadah || ''}`.toLowerCase();
+ const matchKeyword = !keyword || text.includes(keyword);
+ return matchStatus && matchKeyword;
+ });
+}
+
+function updateDashboard() {
+ const visible = keluarga;
+ const total = visible.length;
+ const totalDitanggung = visible.filter(k => k.status_bantuan === 'Ditanggung').length;
+ const totalBlank = visible.filter(k => k.status_bantuan === 'Blank Spot').length;
+ const totalMenunggu = visible.filter(k => k.status_bantuan === 'Menunggu Verifikasi').length;
+ const totalValid = visible.filter(k => k.status_verifikasi === 'Valid').length;
+ const totalMusibahAktif = musibah.filter(m => !['Selesai','Ditolak'].includes(m.status)).length;
+ const totalProduktif = keluarga.reduce((sum, k) => sum + Number(k.jumlah_mampu_bekerja || 0), 0);
+ const cakupan = totalValid > 0 ? Math.round((totalDitanggung / totalValid) * 100) : 0;
+
+ setText('totalRumahIbadah', rumahIbadah.length);
+ setText('totalKeluarga', total);
+ setText('totalDitanggung', totalDitanggung);
+ setText('totalBlankSpot', totalBlank);
+ setText('totalMenunggu', totalMenunggu);
+ setText('totalMusibahAktif', totalMusibahAktif);
+ setText('totalProduktif', totalProduktif);
+ setText('cakupanBantuan', `${cakupan}%`);
+ setText('cakupanLabel', `${cakupan}%`);
+ const bar = document.getElementById('cakupanBar');
+ if (bar) bar.style.width = `${Math.min(100, Math.max(0, cakupan))}%`;
+
+ if (role === 'rumah_ibadah') {
+ const own = rumahIbadah.find(ri => Number(ri.id) === linkedRumahIbadahId);
+ if (own) setText('dashboardTitle', `Cakupan ${own.nama}`);
+ }
+ if (role === 'masyarakat') {
+ setText('dashboardTitle', 'Status Usulan Saya');
+ }
+ if (role === 'relawan') {
+ setText('dashboardTitle', 'Antrian Survei Lapangan');
+ }
+}
+
+function renderListKeluarga() {
+ const box = document.getElementById('listKeluarga');
+ if (!box) return;
+ const rows = getFilteredKeluarga();
+
+ if (!rows.length) {
+ box.innerHTML = emptyState('Belum ada data sesuai filter', 'Coba ubah filter status atau kata kunci pencarian.');
+ return;
+ }
+
+ box.innerHTML = rows.map(k => keluargaCard(k, true)).join('');
+}
+
+function renderListRumahIbadah() {
+ const box = document.getElementById('listRumahIbadah');
+ if (!box) return;
+
+ const rows = role === 'rumah_ibadah' && linkedRumahIbadahId
+ ? rumahIbadah.filter(ri => Number(ri.id) === linkedRumahIbadahId)
+ : rumahIbadah;
+
+ if (!rows.length) {
+ box.innerHTML = emptyState('Rumah ibadah belum tersedia', 'Tambahkan data rumah ibadah agar radius layanan dapat dianalisis.');
+ return;
+ }
+
+ box.innerHTML = rows.map(ri => {
+ const jumlah = Number(ri.jumlah_tanggungan || 0);
+ const sisa = Math.max(0, Number(ri.kuota || 0) - jumlah);
+ const actions = canEditRumahIbadah(ri)
+ ? ``
+ : '';
+ const deleteAction = caps.canCreateRI ? `` : '';
+ return `
+
+
${escapeHtml(ri.nama)}${escapeHtml(ri.kategori)}
+
${escapeHtml(ri.alamat || '-')}
+
Radius ${ri.radius} m • Kuota ${ri.kuota} KK • Sisa ${sisa} KK • Dana ${rupiah(ri.dana_sosial)}
+
${actions}${deleteAction}
+
+ `;
+ }).join('');
+}
+
+function renderListValidasi() {
+ const box = document.getElementById('listValidasi');
+ if (!box) return;
+
+ const rows = keluarga.filter(k => k.status_verifikasi === 'Menunggu Verifikasi' || k.status_verifikasi === 'Perlu Survei Ulang');
+ if (!rows.length) {
+ box.innerHTML = emptyState('Tidak ada antrian verifikasi', 'Usulan baru atau data yang perlu survei ulang akan muncul di sini.');
+ return;
+ }
+
+ box.innerHTML = rows.map(k => keluargaCard(k, false, 'validasi')).join('');
+}
+
+function renderListBantuan() {
+ const box = document.getElementById('listBantuan');
+ if (!box) return;
+
+ let rows = keluarga.filter(k => k.status_bantuan === 'Ditanggung');
+ if (role === 'rumah_ibadah' && linkedRumahIbadahId) {
+ rows = rows.filter(k => Number(k.rumah_ibadah_id) === linkedRumahIbadahId);
+ }
+
+ if (!rows.length) {
+ box.innerHTML = emptyState('Belum ada tanggungan', 'Jalankan analisis setelah data rumah ibadah dan keluarga valid tersedia.');
+ return;
+ }
+
+ box.innerHTML = rows.map(k => keluargaCard(k, false, 'bantuan')).join('');
+}
+
+function renderListMusibah() {
+ const box = document.getElementById('listMusibah');
+ if (!box) return;
+ if (!musibah.length) {
+ box.innerHTML = emptyState('Belum ada laporan musibah', 'Laporan kondisi mendadak akan muncul di sini.');
+ return;
+ }
+ box.innerHTML = musibah.map(m => {
+ const statusClass = m.status === 'Selesai' ? 'badge-green' : (m.tingkat_urgensi === 'Darurat' || m.tingkat_urgensi === 'Tinggi' ? 'badge-red' : 'badge-yellow');
+ const actions = caps.canManageMusibah
+ ? `
+
+ ` : '';
+ return `
+
+
${escapeHtml(m.jenis_musibah)}${escapeHtml(m.tingkat_urgensi)} • ${escapeHtml(m.status)}
+
${escapeHtml(m.nama_kk || 'Lokasi umum')} • ${escapeHtml(m.tanggal_kejadian || '-')}
+
${escapeHtml(m.alamat || '-')}
+
${escapeHtml(m.deskripsi || '')}
+
Kebutuhan: ${escapeHtml(m.kebutuhan_mendesak || '-')}
+ ${m.tindak_lanjut ? `
Tindak lanjut: ${escapeHtml(m.tindak_lanjut)}` : ''}
+
${actions}
+
+ `;
+ }).join('');
+}
+
+function renderListPemberdayaan() {
+ const box = document.getElementById('listPemberdayaan');
+ if (!box) return;
+ if (!pemberdayaan.length) {
+ box.innerHTML = emptyState('Belum ada calon pemberdayaan', 'Isi anggota keluarga yang mampu bekerja pada form keluarga.');
+ return;
+ }
+ box.innerHTML = pemberdayaan.map(a => {
+ const badge = a.status_program ? 'badge-purple' : 'badge-yellow';
+ const status = a.status_program || a.status_pemberdayaan || 'Belum Dinilai';
+ return `
+
+
${escapeHtml(a.nama)}${escapeHtml(status)}
+
${escapeHtml(a.hubungan)} dari ${escapeHtml(a.nama_kk)} • ${escapeHtml(a.kerentanan)}
+
Pekerjaan: ${escapeHtml(a.pekerjaan || '-')} • Penghasilan: ${rupiah(a.penghasilan)}
+
Keahlian/minat: ${escapeHtml(a.keahlian || '-')}
+
Program: ${escapeHtml(a.nama_program || 'Belum ada program')}
+
+
+ `;
+ }).join('');
+}
+
+
+function renderListAkun() {
+ const box = document.getElementById('listAkun');
+ if (!box) return;
+ if (!users.length) {
+ box.innerHTML = emptyState('Belum ada akun terdaftar', 'Pengajuan akun resmi akan tampil di sini untuk diverifikasi.');
+ return;
+ }
+
+ const pending = users.filter(u => u.status_akun === 'Menunggu Verifikasi');
+ const active = users.filter(u => u.status_akun !== 'Menunggu Verifikasi');
+ const rows = [...pending, ...active];
+
+ box.innerHTML = rows.map(u => {
+ const statusClass = u.status_akun === 'Aktif' ? 'badge-green' : (u.status_akun === 'Ditolak' ? 'badge-red' : 'badge-yellow');
+ const roleLabel = getRoleLabel(u.role);
+ const detailInstansi = u.role === 'rumah_ibadah'
+ ? `Rumah ibadah: ${escapeHtml(u.nama_instansi || '-')} • ${escapeHtml(u.kategori_instansi || '-')}`
+ : '';
+ const detailKontak = u.kontak ? `Kontak: ${escapeHtml(u.kontak)}` : '';
+ const detailAlamat = u.alamat ? `${escapeHtml(u.alamat)}` : '';
+ const detailCatatan = u.catatan_pengajuan ? `Catatan: ${escapeHtml(u.catatan_pengajuan)}` : '';
+ const detailKoordinat = u.role === 'rumah_ibadah' && u.lat && u.lng ? `Koordinat: ${escapeHtml(u.lat)}, ${escapeHtml(u.lng)}` : '';
+ const approveReject = u.status_akun === 'Menunggu Verifikasi'
+ ? `
+ `
+ : '';
+ const deleteBtn = Number(u.id) !== Number(user.id)
+ ? ``
+ : '';
+
+ return `
+
+
+ ${escapeHtml(u.nama)}
+ ${escapeHtml(u.status_akun || 'Aktif')}
+
+
${escapeHtml(u.email)} • ${roleLabel}
+ ${detailInstansi}${detailKontak}${detailAlamat}${detailKoordinat}${detailCatatan}
+
${approveReject}${deleteBtn}
+
+ `;
+ }).join('');
+}
+
+function emptyState(title, description) {
+ return `
+
+
⌕
+
${escapeHtml(title)}
+
${escapeHtml(description)}
+
+ `;
+}
+
+function getRoleLabel(value) {
+ const labels = {
+ admin: 'Administrator',
+ dinas: 'Dinas Sosial',
+ rumah_ibadah: 'Pengurus Rumah Ibadah',
+ relawan: 'Relawan Lapangan',
+ masyarakat: 'Masyarakat'
+ };
+ return labels[value] || value;
+}
+
+async function approveUser(id) {
+ if (!caps.canManageUsers) return toast('Akses ditolak.');
+ if (!confirm('Setujui akun ini?')) return;
+ try {
+ const result = await apiPut('api/users.php', { id, action: 'approve' });
+ toast(result.message || 'Akun disetujui.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menyetujui akun.');
+ }
+}
+
+async function rejectUser(id) {
+ if (!caps.canManageUsers) return toast('Akses ditolak.');
+ if (!confirm('Tolak pengajuan akun ini?')) return;
+ try {
+ const result = await apiPut('api/users.php', { id, action: 'reject' });
+ toast(result.message || 'Akun ditolak.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menolak akun.');
+ }
+}
+
+async function deleteUser(id) {
+ if (!caps.canManageUsers) return toast('Akses ditolak.');
+ if (!confirm('Hapus akun ini?')) return;
+ try {
+ const res = await fetch(`api/users.php?id=${id}`, { method: 'DELETE' });
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message || 'Gagal menghapus akun.');
+ toast(json.message || 'Akun dihapus.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menghapus akun.');
+ }
+}
+
+function keluargaCard(k, allowFocus = true, mode = 'data') {
+ const badgeClass = getBadgeClass(k.status_bantuan);
+ const onclick = allowFocus ? `onclick="focusKeluarga(${k.id})"` : '';
+ const route = k.rumah_ibadah_id && k.ri_lat && k.ri_lng
+ ? `` : '';
+ const verify = caps.canVerify && mode !== 'bantuan'
+ ? `
+ ` : '';
+ const bantuan = canRecordBantuanFor(k)
+ ? `` : '';
+ const del = caps.canCreateRI
+ ? `` : '';
+ const musibahBtn = caps.canReportMusibah
+ ? `` : '';
+ const anggotaRingkas = Number(k.jumlah_anggota || 0) > 0
+ ? `Anggota KK: ${Number(k.jumlah_anggota || 0)} orang • Mampu bekerja: ${Number(k.jumlah_mampu_bekerja || 0)}` : '';
+ const anggotaDetail = Array.isArray(k.anggota) && k.anggota.length
+ ? `Lihat biodata anggota
${k.anggota.map(a => `${escapeHtml(a.nama)}${escapeHtml(a.hubungan)} • ${escapeHtml(a.jenis_kelamin)} • ${escapeHtml(a.pekerjaan || '-')} • Mampu bekerja: ${escapeHtml(a.mampu_bekerja || 'Tidak')}
`).join('')} ` : '';
+
+ return `
+
+
+ ${escapeHtml(k.nama_kk)}
+ ${escapeHtml(k.status_bantuan)}
+
+
${escapeHtml(k.alamat || '-')}
+
${escapeHtml(k.kerentanan)} • ${escapeHtml(k.kebutuhan)} • Verifikasi: ${escapeHtml(k.status_verifikasi)}
+
Penanggung: ${escapeHtml(k.nama_rumah_ibadah || '-')}
+ ${anggotaRingkas}${anggotaDetail}
+
${route}${bantuan}${musibahBtn}${verify}${del}
+
+ `;
+}
+
+function getBadgeClass(status) {
+ if (status === 'Ditanggung') return 'badge-green';
+ if (status === 'Blank Spot') return 'badge-red';
+ if (status === 'Menunggu Verifikasi') return 'badge-yellow';
+ if (status === 'Bantuan Pemerintah') return 'badge-purple';
+ return 'badge-gray';
+}
+
+async function simpanKeluarga() {
+ const lat = getValue('lat');
+ const lng = getValue('lng');
+ if (!lat || !lng) {
+ toast('Klik titik rumah keluarga pada peta terlebih dahulu.');
+ return;
+ }
+
+ const payload = {
+ nama_kk: getValue('kelNama'),
+ alamat: getValue('kelAlamat'),
+ no_kk: getValue('kelNoKK'),
+ nik: getValue('kelNik'),
+ kerentanan: getValue('kelKerentanan'),
+ kebutuhan: getValue('kelKebutuhan'),
+ status_verifikasi: getValue('kelStatusVerifikasi') || 'Menunggu Verifikasi',
+ catatan: getValue('kelCatatan'),
+ lat,
+ lng,
+ anggota: collectAnggotaForm()
+ };
+
+ if (!payload.nama_kk || !payload.alamat) {
+ toast('Nama kepala keluarga dan alamat wajib diisi.');
+ return;
+ }
+ if (!payload.anggota.length) {
+ toast('Minimal isi satu anggota keluarga sesuai KK.');
+ return;
+ }
+
+ try {
+ await apiPost('api/keluarga.php', payload);
+ toast(role === 'masyarakat' ? 'Usulan berhasil dikirim dan menunggu verifikasi.' : 'Data keluarga berhasil disimpan.');
+ bersihkanFormKeluarga();
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menyimpan data keluarga.');
+ }
+}
+
+async function simpanRumahIbadah() {
+ if (!caps.canManageRI) {
+ toast('Kamu tidak punya akses manajemen rumah ibadah.');
+ return;
+ }
+
+ const id = Number(getValue('riId') || 0);
+ if (!id && !caps.canCreateRI) {
+ toast('Pengurus hanya dapat memperbarui rumah ibadah yang sudah terhubung dengan akunnya.');
+ return;
+ }
+
+ const payload = {
+ id,
+ nama: getValue('riNama'),
+ alamat: getValue('riAlamat'),
+ kategori: getValue('riKategori'),
+ pengurus: getValue('riPengurus'),
+ kontak: getValue('riKontak'),
+ dana_sosial: Number(getValue('riDana') || 0),
+ kuota: Number(getValue('riKuota') || 0),
+ radius: Number(getValue('riRadius') || 500),
+ lat: getValue('riLat'),
+ lng: getValue('riLng'),
+ status: 'Aktif'
+ };
+
+ if (!payload.nama || !payload.alamat || !payload.lat || !payload.lng) {
+ toast('Lengkapi nama, alamat, dan titik koordinat rumah ibadah.');
+ return;
+ }
+
+ try {
+ if (id > 0) {
+ await apiPut('api/rumah_ibadah.php', payload);
+ toast(caps.canAnalyze ? 'Data rumah ibadah berhasil diperbarui. Jalankan analisis ulang agar radius baru diterapkan.' : 'Perubahan layanan tersimpan. Status tanggungan akan mengikuti analisis terbaru dari pengelola.');
+ } else {
+ await apiPost('api/rumah_ibadah.php', payload);
+ toast('Rumah ibadah baru berhasil disimpan.');
+ resetFormRumahIbadah();
+ }
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menyimpan rumah ibadah.');
+ }
+}
+
+function fillRumahIbadahForm(id) {
+ const ri = rumahIbadah.find(r => Number(r.id) === Number(id));
+ if (!ri) return;
+ if (!canEditRumahIbadah(ri)) {
+ toast('Kamu tidak berwenang mengubah rumah ibadah ini.');
+ return;
+ }
+ setValue('riId', ri.id);
+ setValue('riNama', ri.nama || '');
+ setValue('riAlamat', ri.alamat || '');
+ setValue('riKategori', ri.kategori || 'Masjid');
+ setValue('riPengurus', ri.pengurus || '');
+ setValue('riKontak', ri.kontak || '');
+ setValue('riDana', Number(ri.dana_sosial || 0));
+ setValue('riKuota', Number(ri.kuota || 0));
+ setValue('riRadius', Number(ri.radius || 500));
+ setText('riRadiusLabel', Number(ri.radius || 500));
+ setValue('riLat', ri.lat);
+ setValue('riLng', ri.lng);
+ focusPoint(ri.lat, ri.lng, 16);
+}
+
+function resetFormRumahIbadah() {
+ if (!caps.canCreateRI) {
+ toast('Pengurus tidak dapat membuat rumah ibadah baru.');
+ return;
+ }
+ ['riId','riNama','riAlamat','riPengurus','riKontak','riDana','riKuota','riLat','riLng'].forEach(id => setValue(id, ''));
+ setValue('riKategori', 'Masjid');
+ setValue('riRadius', 500);
+ setText('riRadiusLabel', 500);
+ if (selectedMarker) {
+ map.removeLayer(selectedMarker);
+ selectedMarker = null;
+ }
+ const box = document.getElementById('anggotaContainer');
+ if (box) {
+ box.innerHTML = '';
+ addAnggotaForm({ hubungan: 'Kepala Keluarga' });
+ }
+}
+
+async function jalankanAnalisis() {
+ if (!caps.canAnalyze) {
+ toast('Analisis hanya dapat dijalankan oleh Admin atau Dinas Sosial.');
+ return;
+ }
+ try {
+ const result = await apiPost('api/analisis.php', {});
+ toast(result.message || 'Analisis selesai.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Analisis gagal.');
+ }
+}
+
+async function verifikasiKeluarga(id, status) {
+ if (!caps.canVerify) return toast('Kamu tidak punya akses validasi.');
+ if (!confirm(`Ubah status verifikasi menjadi ${status}?`)) return;
+ const catatan = prompt('Catatan verifikasi, boleh dikosongkan:', '') || '';
+
+ try {
+ const result = await apiPut('api/keluarga.php', { id, status_verifikasi: status, catatan });
+ toast(result.message || 'Status diperbarui.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal memperbarui status.');
+ }
+}
+
+async function catatBantuan(id) {
+ const kel = keluarga.find(k => Number(k.id) === Number(id));
+ if (!kel || !canRecordBantuanFor(kel)) {
+ toast('Kamu tidak berwenang mencatat bantuan untuk data ini.');
+ return;
+ }
+
+ const tanggal = prompt('Tanggal bantuan (YYYY-MM-DD):', new Date().toISOString().slice(0, 10));
+ if (!tanggal) return;
+ const jenis = prompt('Jenis bantuan:', kel.kebutuhan || 'Sembako');
+ if (!jenis) return;
+ const nilai = prompt('Estimasi nilai bantuan (angka):', '0') || '0';
+ const catatan = prompt('Catatan bantuan:', '') || '';
+
+ try {
+ await apiPost('api/bantuan.php', {
+ keluarga_id: id,
+ tanggal_bantuan: tanggal,
+ jenis_bantuan: jenis,
+ nilai_bantuan: Number(nilai),
+ catatan
+ });
+ toast('Riwayat bantuan berhasil dicatat.');
+ } catch (error) {
+ toast(error.message || 'Gagal mencatat bantuan.');
+ }
+}
+
+function buatRute(id) {
+ const kel = keluarga.find(k => Number(k.id) === Number(id));
+ if (!kel || !kel.ri_lat || !kel.ri_lng) {
+ toast('Data rumah ibadah penanggung belum tersedia.');
+ return;
+ }
+
+ if (routingControl) map.removeControl(routingControl);
+ routingControl = L.Routing.control({
+ waypoints: [
+ L.latLng(Number(kel.ri_lat), Number(kel.ri_lng)),
+ L.latLng(Number(kel.lat), Number(kel.lng))
+ ],
+ routeWhileDragging: false,
+ addWaypoints: false,
+ show: false,
+ lineOptions: { styles: [{ color: '#7c3aed', weight: 5, opacity: 0.85 }] }
+ }).addTo(map);
+ map.setView([Number(kel.lat), Number(kel.lng)], 15);
+ toast('Rute distribusi bantuan ditampilkan.');
+}
+
+async function hapusKeluarga(id) {
+ if (!caps.canCreateRI) return toast('Hanya Admin/Dinas yang bisa menghapus data keluarga.');
+ if (!confirm('Hapus data keluarga ini?')) return;
+ try {
+ const res = await fetch(`api/keluarga.php?id=${id}`, { method: 'DELETE' });
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message);
+ toast('Data keluarga dihapus.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menghapus data.');
+ }
+}
+
+async function hapusRumahIbadah(id) {
+ if (!caps.canCreateRI) return toast('Hanya Admin/Dinas yang bisa menghapus rumah ibadah.');
+ if (!confirm('Hapus data rumah ibadah ini? Data keluarga yang ditanggung akan dilepas.')) return;
+ try {
+ const res = await fetch(`api/rumah_ibadah.php?id=${id}`, { method: 'DELETE' });
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message);
+ toast('Data rumah ibadah dihapus.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal menghapus data.');
+ }
+}
+
+function focusKeluarga(id) {
+ const kel = keluarga.find(k => Number(k.id) === Number(id));
+ if (!kel) return;
+ focusPoint(kel.lat, kel.lng, 17);
+}
+
+function focusPoint(lat, lng, zoom = 16) {
+ map.setView([Number(lat), Number(lng)], zoom);
+}
+
+function fitAllMarkers() {
+ const points = [];
+ rumahIbadah.forEach(ri => points.push([Number(ri.lat), Number(ri.lng)]));
+ keluarga.forEach(k => points.push([Number(k.lat), Number(k.lng)]));
+ if (!points.length) return toast('Belum ada titik pada peta.');
+ map.fitBounds(points, { padding: [40, 40] });
+}
+
+function filterTanggunganRI(id) {
+ setValue('filterStatus', 'semua');
+ setValue('searchText', '');
+ setActiveTab('peta');
+ const rows = keluarga.filter(k => Number(k.rumah_ibadah_id) === Number(id));
+ const box = document.getElementById('listKeluarga');
+ if (box) {
+ box.innerHTML = rows.length ? rows.map(k => keluargaCard(k, true)).join('') : 'Belum ada keluarga yang ditanggung rumah ibadah ini.
';
+ }
+ const ri = rumahIbadah.find(r => Number(r.id) === Number(id));
+ if (ri) focusPoint(ri.lat, ri.lng, 15);
+}
+
+function locateMe() {
+ if (!navigator.geolocation) return toast('Browser tidak mendukung geolocation.');
+ navigator.geolocation.getCurrentPosition(
+ pos => {
+ const lat = pos.coords.latitude;
+ const lng = pos.coords.longitude;
+ map.setView([lat, lng], 17);
+ const activeTab = document.querySelector('.tab.active')?.dataset.tab || 'input';
+ if (activeTab === 'rumah-ibadah' && caps.canCreateRI) {
+ setValue('riLat', lat.toFixed(7));
+ setValue('riLng', lng.toFixed(7));
+ } else if (activeTab === 'musibah') {
+ setValue('musibahLat', lat.toFixed(7));
+ setValue('musibahLng', lng.toFixed(7));
+ fillAddress(lat.toFixed(7), lng.toFixed(7), 'musibahAlamat');
+ } else {
+ setValue('lat', lat.toFixed(7));
+ setValue('lng', lng.toFixed(7));
+ }
+ if (selectedMarker) map.removeLayer(selectedMarker);
+ selectedMarker = L.marker([lat, lng]).addTo(map).bindPopup('Lokasi kamu').openPopup();
+ toast('Lokasi berhasil diambil.');
+ },
+ () => toast('Gagal mengambil lokasi. Izinkan akses lokasi di browser.')
+ );
+}
+
+function bersihkanFormKeluarga() {
+ ['kelNama','kelAlamat','kelNoKK','kelNik','kelCatatan','lat','lng'].forEach(id => setValue(id, ''));
+ setValue('kelKerentanan', 'Miskin Ekstrem');
+ setValue('kelKebutuhan', 'Sembako');
+ setValue('kelStatusVerifikasi', 'Menunggu Verifikasi');
+ if (selectedMarker) {
+ map.removeLayer(selectedMarker);
+ selectedMarker = null;
+ }
+}
+
+
+function initAnggotaDefault() {
+ const box = document.getElementById('anggotaContainer');
+ if (box && !box.children.length) addAnggotaForm({ hubungan: 'Kepala Keluarga' });
+}
+
+function addAnggotaForm(defaults = {}) {
+ const box = document.getElementById('anggotaContainer');
+ if (!box) return;
+ const index = box.children.length + 1;
+ const div = document.createElement('div');
+ div.className = 'anggota-card';
+ div.innerHTML = `
+ Anggota ${index}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `;
+ div.querySelector('.btn-remove-anggota').addEventListener('click', () => {
+ if (box.children.length <= 1) return toast('Minimal harus ada satu anggota keluarga.');
+ div.remove();
+ });
+ box.appendChild(div);
+}
+
+function collectAnggotaForm() {
+ return Array.from(document.querySelectorAll('#anggotaContainer .anggota-card')).map(card => {
+ const data = {};
+ card.querySelectorAll('[data-field]').forEach(el => data[el.dataset.field] = String(el.value || '').trim());
+ return data;
+ }).filter(a => a.nama);
+}
+
+function populateMusibahKeluarga() {
+ const select = document.getElementById('musibahKeluarga');
+ if (!select) return;
+ const current = select.value;
+ select.innerHTML = '' + keluarga.map(k => ``).join('');
+ if (current) select.value = current;
+}
+
+function isiLokasiMusibahDariKeluarga() {
+ const id = Number(getValue('musibahKeluarga') || 0);
+ if (!id) return;
+ const kel = keluarga.find(k => Number(k.id) === id);
+ if (!kel) return;
+ setValue('musibahAlamat', kel.alamat || '');
+ setValue('musibahLat', kel.lat || '');
+ setValue('musibahLng', kel.lng || '');
+ focusPoint(kel.lat, kel.lng, 17);
+}
+
+function buatLaporanMusibahUntuk(id) {
+ setActiveTab('musibah');
+ setValue('musibahKeluarga', id);
+ isiLokasiMusibahDariKeluarga();
+ toast('Lengkapi jenis musibah dan kebutuhan mendesak, lalu kirim laporan.');
+}
+
+async function simpanMusibah() {
+ const keluargaId = Number(getValue('musibahKeluarga') || 0);
+ const payload = {
+ keluarga_id: keluargaId || null,
+ jenis_musibah: getValue('musibahJenis'),
+ tingkat_urgensi: getValue('musibahUrgensi'),
+ tanggal_kejadian: getValue('musibahTanggal') || new Date().toISOString().slice(0, 10),
+ alamat: getValue('musibahAlamat'),
+ lat: getValue('musibahLat'),
+ lng: getValue('musibahLng'),
+ deskripsi: getValue('musibahDeskripsi'),
+ kebutuhan_mendesak: getValue('musibahKebutuhan')
+ };
+ if (!payload.deskripsi) return toast('Kronologi singkat wajib diisi.');
+ if (!payload.alamat || !payload.lat || !payload.lng) return toast('Pilih keluarga terkait atau klik lokasi kejadian pada peta.');
+ try {
+ await apiPost('api/musibah.php', payload);
+ toast('Laporan musibah berhasil dikirim.');
+ ['musibahDeskripsi','musibahKebutuhan','musibahAlamat','musibahLat','musibahLng'].forEach(id => setValue(id, ''));
+ setValue('musibahKeluarga', '');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal mengirim laporan musibah.');
+ }
+}
+
+async function updateMusibah(id, status) {
+ const tindak_lanjut = prompt('Catatan tindak lanjut:', '') || '';
+ try {
+ await apiPut('api/musibah.php', { id, status, tindak_lanjut });
+ toast('Status musibah diperbarui.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal memperbarui laporan musibah.');
+ }
+}
+
+async function tambahProgramPemberdayaan(anggotaId) {
+ const anggota = pemberdayaan.find(a => Number(a.id) === Number(anggotaId));
+ if (!anggota) return;
+ const nama_program = prompt('Nama program pemberdayaan:', anggota.keahlian ? `Program ${anggota.keahlian}` : 'Pelatihan kerja dasar');
+ if (!nama_program) return;
+ const jenis_program = prompt('Jenis program (Pelatihan Kerja/Modal Usaha/Pendampingan UMKM/Penempatan Kerja/Bantuan Alat Kerja/Lainnya):', 'Pelatihan Kerja') || 'Pelatihan Kerja';
+ const penyelenggara = prompt('Penyelenggara:', role === 'rumah_ibadah' ? 'Rumah Ibadah' : 'Dinas Sosial') || '';
+ const catatan = prompt('Catatan:', `Keahlian/minat: ${anggota.keahlian || '-'}`) || '';
+ try {
+ await apiPost('api/pemberdayaan.php', {
+ anggota_id: anggotaId,
+ jenis_program,
+ nama_program,
+ penyelenggara,
+ tanggal_mulai: new Date().toISOString().slice(0, 10),
+ status: 'Direkomendasikan',
+ catatan
+ });
+ toast('Program pemberdayaan berhasil dicatat.');
+ await loadAllData();
+ } catch (error) {
+ toast(error.message || 'Gagal mencatat program pemberdayaan.');
+ }
+}
+
+async function apiPost(url, payload) {
+ const res = await fetch(url, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message || 'Request gagal.');
+ return json;
+}
+
+async function apiPut(url, payload) {
+ const res = await fetch(url, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(payload)
+ });
+ const json = await res.json();
+ if (!json.success) throw new Error(json.message || 'Request gagal.');
+ return json;
+}
+
+function onClick(id, handler) {
+ const el = document.getElementById(id);
+ if (el) el.addEventListener('click', handler);
+}
+
+function onInput(id, handler) {
+ const el = document.getElementById(id);
+ if (el) {
+ el.addEventListener('input', handler);
+ el.addEventListener('change', handler);
+ }
+}
+
+function getValue(id) {
+ const el = document.getElementById(id);
+ return el ? String(el.value ?? '').trim() : '';
+}
+
+function setValue(id, value) {
+ const el = document.getElementById(id);
+ if (el) el.value = value;
+}
+
+function setText(id, value) {
+ const el = document.getElementById(id);
+ if (el) el.textContent = value;
+}
+
+function rupiah(value) {
+ const number = Number(value || 0);
+ return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(number);
+}
+
+function escapeHtml(str) {
+ return String(str ?? '')
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''');
+}
+
+function toast(message) {
+ const el = document.getElementById('toast');
+ if (!el) return alert(message);
+ el.textContent = message;
+ el.classList.add('show');
+ setTimeout(() => el.classList.remove('show'), 3200);
+}
+
+window.addEventListener('DOMContentLoaded', async () => {
+ initMap();
+ initUI();
+ await loadAllData();
+});
+
+// Global untuk popup Leaflet.
+window.setActiveTab = setActiveTab;
+window.fillRumahIbadahForm = fillRumahIbadahForm;
+window.buatRute = buatRute;
+window.verifikasiKeluarga = verifikasiKeluarga;
+window.catatBantuan = catatBantuan;
+window.hapusKeluarga = hapusKeluarga;
+window.hapusRumahIbadah = hapusRumahIbadah;
+window.focusKeluarga = focusKeluarga;
+window.focusPoint = focusPoint;
+window.filterTanggunganRI = filterTanggunganRI;
+window.approveUser = approveUser;
+window.rejectUser = rejectUser;
+window.deleteUser = deleteUser;
+window.buatLaporanMusibahUntuk = buatLaporanMusibahUntuk;
+window.updateMusibah = updateMusibah;
+window.tambahProgramPemberdayaan = tambahProgramPemberdayaan;