1358 lines
52 KiB
JavaScript
1358 lines
52 KiB
JavaScript
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) => `
|
|
<div class="guide-step">
|
|
<i>${index + 1}</i>
|
|
<b>${escapeHtml(step[0])}</b>
|
|
<span>${escapeHtml(step[1])}</span>
|
|
</div>
|
|
`).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) => `
|
|
<div class="guide-modal-step">
|
|
<i>${index + 1}</i>
|
|
<div><b>${escapeHtml(step[0])}</b><span>${escapeHtml(step[1])}</span></div>
|
|
</div>
|
|
`).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)
|
|
? `<button class="popup-btn" onclick="fillRumahIbadahForm(${ri.id}); setActiveTab('rumah-ibadah')">Atur Radius</button>`
|
|
: '';
|
|
const deleteBtn = caps.canCreateRI
|
|
? `<button class="popup-btn danger" onclick="hapusRumahIbadah(${ri.id})">Hapus</button>`
|
|
: '';
|
|
const tanggunganBtn = `<button class="popup-btn secondary" onclick="filterTanggunganRI(${ri.id})">Lihat Tanggungan</button>`;
|
|
|
|
marker.bindPopup(`
|
|
<div class="popup-title">${escapeHtml(ri.nama)}</div>
|
|
<b>Kategori:</b> ${escapeHtml(ri.kategori)}<br>
|
|
<b>Pengurus:</b> ${escapeHtml(ri.pengurus || '-')}<br>
|
|
<b>Kontak:</b> ${escapeHtml(ri.kontak || '-')}<br>
|
|
<b>Dana Sosial:</b> ${rupiah(ri.dana_sosial)}<br>
|
|
<b>Kuota:</b> ${ri.kuota} KK • <b>Sisa:</b> ${sisaKuota}<br>
|
|
<b>Radius:</b> ${ri.radius} meter<br>
|
|
<b>Tanggungan aktif:</b> ${jumlahTanggungan} KK<br>
|
|
<small>${escapeHtml(ri.alamat || '')}</small><br>
|
|
${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
|
|
? `<button class="popup-btn secondary" onclick="buatRute(${kel.id})">Rute Distribusi</button>`
|
|
: '';
|
|
const verifyBtns = caps.canVerify
|
|
? `<button class="popup-btn green" onclick="verifikasiKeluarga(${kel.id}, 'Valid')">Valid</button>
|
|
<button class="popup-btn danger" onclick="verifikasiKeluarga(${kel.id}, 'Tidak Valid')">Tidak Valid</button>`
|
|
: '';
|
|
const bantuanBtn = canRecordBantuanFor(kel)
|
|
? `<button class="popup-btn" onclick="catatBantuan(${kel.id})">Catat Bantuan</button>`
|
|
: '';
|
|
const musibahBtn = caps.canReportMusibah
|
|
? `<button class="popup-btn danger" onclick="buatLaporanMusibahUntuk(${kel.id})">Laporkan Musibah</button>`
|
|
: '';
|
|
const deleteBtn = caps.canCreateRI
|
|
? `<button class="popup-btn danger" onclick="hapusKeluarga(${kel.id})">Hapus</button>`
|
|
: '';
|
|
|
|
marker.bindPopup(`
|
|
<div class="popup-title">${escapeHtml(kel.nama_kk)}</div>
|
|
<b>Status Verifikasi:</b> ${escapeHtml(kel.status_verifikasi)}<br>
|
|
<b>Status Bantuan:</b> ${escapeHtml(kel.status_bantuan)}<br>
|
|
<b>Penanggung:</b> ${escapeHtml(kel.nama_rumah_ibadah || '-')}<br>
|
|
<b>Kerentanan:</b> ${escapeHtml(kel.kerentanan)}<br>
|
|
<b>Kebutuhan:</b> ${escapeHtml(kel.kebutuhan)}<br>
|
|
<b>Anggota KK:</b> ${Number(kel.jumlah_anggota || 0)} orang • <b>Mampu bekerja:</b> ${Number(kel.jumlah_mampu_bekerja || 0)}<br>
|
|
<small>${escapeHtml(kel.alamat || '')}</small><br>
|
|
${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)
|
|
? `<button class="mini-btn" onclick="fillRumahIbadahForm(${ri.id}); setActiveTab('rumah-ibadah')">Edit Radius/Kuota</button>`
|
|
: '';
|
|
const deleteAction = caps.canCreateRI ? `<button class="mini-btn red" onclick="hapusRumahIbadah(${ri.id})">Hapus</button>` : '';
|
|
return `
|
|
<div class="list-item" onclick="focusPoint(${ri.lat}, ${ri.lng})">
|
|
<div class="list-title-row"><b>${escapeHtml(ri.nama)}</b><span class="badge badge-blue">${escapeHtml(ri.kategori)}</span></div>
|
|
<span>${escapeHtml(ri.alamat || '-')}</span>
|
|
<span>Radius ${ri.radius} m • Kuota ${ri.kuota} KK • Sisa ${sisa} KK • Dana ${rupiah(ri.dana_sosial)}</span>
|
|
<div class="item-actions">${actions}<button class="mini-btn purple" onclick="event.stopPropagation(); filterTanggunganRI(${ri.id})">Lihat Tanggungan</button>${deleteAction}</div>
|
|
</div>
|
|
`;
|
|
}).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
|
|
? `<button class="mini-btn green" onclick="event.stopPropagation(); updateMusibah(${m.id}, 'Diproses')">Proses</button>
|
|
<button class="mini-btn" onclick="event.stopPropagation(); updateMusibah(${m.id}, 'Ditindaklanjuti')">Tindak Lanjut</button>
|
|
<button class="mini-btn purple" onclick="event.stopPropagation(); updateMusibah(${m.id}, 'Selesai')">Selesai</button>` : '';
|
|
return `
|
|
<div class="list-item" onclick="focusPoint(${m.lat}, ${m.lng}, 17)">
|
|
<div class="list-title-row"><b>${escapeHtml(m.jenis_musibah)}</b><span class="badge ${statusClass}">${escapeHtml(m.tingkat_urgensi)} • ${escapeHtml(m.status)}</span></div>
|
|
<span>${escapeHtml(m.nama_kk || 'Lokasi umum')} • ${escapeHtml(m.tanggal_kejadian || '-')}</span>
|
|
<span>${escapeHtml(m.alamat || '-')}</span>
|
|
<span>${escapeHtml(m.deskripsi || '')}</span>
|
|
<span>Kebutuhan: ${escapeHtml(m.kebutuhan_mendesak || '-')}</span>
|
|
${m.tindak_lanjut ? `<span>Tindak lanjut: ${escapeHtml(m.tindak_lanjut)}</span>` : ''}
|
|
<div class="item-actions"><button class="mini-btn purple" onclick="event.stopPropagation(); focusPoint(${m.lat}, ${m.lng}, 17)">Lihat Lokasi</button>${actions}</div>
|
|
</div>
|
|
`;
|
|
}).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 `
|
|
<div class="list-item" onclick="focusKeluarga(${a.keluarga_id})">
|
|
<div class="list-title-row"><b>${escapeHtml(a.nama)}</b><span class="badge ${badge}">${escapeHtml(status)}</span></div>
|
|
<span>${escapeHtml(a.hubungan)} dari ${escapeHtml(a.nama_kk)} • ${escapeHtml(a.kerentanan)}</span>
|
|
<span>Pekerjaan: ${escapeHtml(a.pekerjaan || '-')} • Penghasilan: ${rupiah(a.penghasilan)}</span>
|
|
<span>Keahlian/minat: ${escapeHtml(a.keahlian || '-')}</span>
|
|
<span>Program: ${escapeHtml(a.nama_program || 'Belum ada program')}</span>
|
|
<div class="item-actions"><button class="mini-btn" onclick="event.stopPropagation(); tambahProgramPemberdayaan(${a.id})">Tambah Program</button></div>
|
|
</div>
|
|
`;
|
|
}).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'
|
|
? `<span>Rumah ibadah: ${escapeHtml(u.nama_instansi || '-')} • ${escapeHtml(u.kategori_instansi || '-')}</span>`
|
|
: '';
|
|
const detailKontak = u.kontak ? `<span>Kontak: ${escapeHtml(u.kontak)}</span>` : '';
|
|
const detailAlamat = u.alamat ? `<span>${escapeHtml(u.alamat)}</span>` : '';
|
|
const detailCatatan = u.catatan_pengajuan ? `<span>Catatan: ${escapeHtml(u.catatan_pengajuan)}</span>` : '';
|
|
const detailKoordinat = u.role === 'rumah_ibadah' && u.lat && u.lng ? `<span>Koordinat: ${escapeHtml(u.lat)}, ${escapeHtml(u.lng)}</span>` : '';
|
|
const approveReject = u.status_akun === 'Menunggu Verifikasi'
|
|
? `<button class="mini-btn green" onclick="event.stopPropagation(); approveUser(${u.id})">Setujui</button>
|
|
<button class="mini-btn red" onclick="event.stopPropagation(); rejectUser(${u.id})">Tolak</button>`
|
|
: '';
|
|
const deleteBtn = Number(u.id) !== Number(user.id)
|
|
? `<button class="mini-btn red" onclick="event.stopPropagation(); deleteUser(${u.id})">Hapus</button>`
|
|
: '';
|
|
|
|
return `
|
|
<div class="list-item">
|
|
<div class="list-title-row">
|
|
<b>${escapeHtml(u.nama)}</b>
|
|
<span class="badge ${statusClass}">${escapeHtml(u.status_akun || 'Aktif')}</span>
|
|
</div>
|
|
<span>${escapeHtml(u.email)} • ${roleLabel}</span>
|
|
${detailInstansi}${detailKontak}${detailAlamat}${detailKoordinat}${detailCatatan}
|
|
<div class="item-actions">${approveReject}${deleteBtn}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function emptyState(title, description) {
|
|
return `
|
|
<div class="empty-state">
|
|
<div class="empty-icon">⌕</div>
|
|
<b>${escapeHtml(title)}</b>
|
|
<span>${escapeHtml(description)}</span>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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
|
|
? `<button class="mini-btn purple" onclick="event.stopPropagation(); buatRute(${k.id})">Lihat Rute</button>` : '';
|
|
const verify = caps.canVerify && mode !== 'bantuan'
|
|
? `<button class="mini-btn green" onclick="event.stopPropagation(); verifikasiKeluarga(${k.id}, 'Valid')">Valid</button>
|
|
<button class="mini-btn red" onclick="event.stopPropagation(); verifikasiKeluarga(${k.id}, 'Tidak Valid')">Tidak Valid</button>` : '';
|
|
const bantuan = canRecordBantuanFor(k)
|
|
? `<button class="mini-btn" onclick="event.stopPropagation(); catatBantuan(${k.id})">Catat Bantuan</button>` : '';
|
|
const del = caps.canCreateRI
|
|
? `<button class="mini-btn red" onclick="event.stopPropagation(); hapusKeluarga(${k.id})">Hapus</button>` : '';
|
|
const musibahBtn = caps.canReportMusibah
|
|
? `<button class="mini-btn red" onclick="event.stopPropagation(); buatLaporanMusibahUntuk(${k.id})">Laporkan Musibah</button>` : '';
|
|
const anggotaRingkas = Number(k.jumlah_anggota || 0) > 0
|
|
? `<span>Anggota KK: ${Number(k.jumlah_anggota || 0)} orang • Mampu bekerja: ${Number(k.jumlah_mampu_bekerja || 0)}</span>` : '';
|
|
const anggotaDetail = Array.isArray(k.anggota) && k.anggota.length
|
|
? `<details class="member-details"><summary>Lihat biodata anggota</summary>${k.anggota.map(a => `<div class="member-row-mini"><b>${escapeHtml(a.nama)}</b><span>${escapeHtml(a.hubungan)} • ${escapeHtml(a.jenis_kelamin)} • ${escapeHtml(a.pekerjaan || '-')} • Mampu bekerja: ${escapeHtml(a.mampu_bekerja || 'Tidak')}</span></div>`).join('')}</details>` : '';
|
|
|
|
return `
|
|
<div class="list-item" ${onclick}>
|
|
<div class="list-title-row">
|
|
<b>${escapeHtml(k.nama_kk)}</b>
|
|
<span class="badge ${badgeClass}">${escapeHtml(k.status_bantuan)}</span>
|
|
</div>
|
|
<span>${escapeHtml(k.alamat || '-')}</span>
|
|
<span>${escapeHtml(k.kerentanan)} • ${escapeHtml(k.kebutuhan)} • Verifikasi: ${escapeHtml(k.status_verifikasi)}</span>
|
|
<span>Penanggung: ${escapeHtml(k.nama_rumah_ibadah || '-')}</span>
|
|
${anggotaRingkas}${anggotaDetail}
|
|
<div class="item-actions">${route}${bantuan}${musibahBtn}${verify}${del}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
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('') : '<div class="info-box">Belum ada keluarga yang ditanggung rumah ibadah ini.</div>';
|
|
}
|
|
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 = `
|
|
<div class="list-title-row anggota-title"><b>Anggota ${index}</b><button type="button" class="mini-btn red btn-remove-anggota">Hapus</button></div>
|
|
<label>Nama Lengkap</label>
|
|
<input data-field="nama" placeholder="Nama sesuai KK" value="${escapeHtml(defaults.nama || '')}">
|
|
<div class="two-col">
|
|
<div><label>NIK</label><input data-field="nik" placeholder="NIK" value="${escapeHtml(defaults.nik || '')}"></div>
|
|
<div><label>Hubungan</label><select data-field="hubungan">
|
|
${['Kepala Keluarga','Istri','Suami','Anak','Orang Tua','Mertua','Famili Lain','Lainnya'].map(v => `<option ${v === (defaults.hubungan || 'Lainnya') ? 'selected' : ''}>${v}</option>`).join('')}
|
|
</select></div>
|
|
</div>
|
|
<div class="two-col">
|
|
<div><label>Jenis Kelamin</label><select data-field="jenis_kelamin"><option>Laki-laki</option><option>Perempuan</option></select></div>
|
|
<div><label>Tanggal Lahir</label><input type="date" data-field="tanggal_lahir" value="${escapeHtml(defaults.tanggal_lahir || '')}"></div>
|
|
</div>
|
|
<div class="two-col">
|
|
<div><label>Tempat Lahir</label><input data-field="tempat_lahir" placeholder="Kota lahir"></div>
|
|
<div><label>Agama</label><input data-field="agama" placeholder="Agama"></div>
|
|
</div>
|
|
<div class="two-col">
|
|
<div><label>Pendidikan</label><input data-field="pendidikan" placeholder="SD/SMP/SMA/dll"></div>
|
|
<div><label>Status Perkawinan</label><input data-field="status_perkawinan" placeholder="Belum kawin/kawin/dll"></div>
|
|
</div>
|
|
<div class="two-col">
|
|
<div><label>Pekerjaan</label><input data-field="pekerjaan" placeholder="Pekerjaan saat ini"></div>
|
|
<div><label>Penghasilan / Bulan</label><input type="number" data-field="penghasilan" placeholder="0"></div>
|
|
</div>
|
|
<label>Masih mampu bekerja?</label>
|
|
<select data-field="mampu_bekerja"><option>Tidak</option><option>Ya</option></select>
|
|
<label>Keahlian / Minat Pemberdayaan</label>
|
|
<textarea data-field="keahlian" placeholder="Contoh: memasak, menjahit, bangunan, servis motor"></textarea>
|
|
<label>Catatan Anggota</label>
|
|
<textarea data-field="catatan" placeholder="Catatan tambahan bila ada"></textarea>
|
|
`;
|
|
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 = '<option value="">Tidak terkait keluarga terdata</option>' + keluarga.map(k => `<option value="${k.id}">${escapeHtml(k.nama_kk)} — ${escapeHtml(k.alamat || '-')}</option>`).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;
|