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;