794 lines
31 KiB
JavaScript
794 lines
31 KiB
JavaScript
// Globals
|
|
let map;
|
|
let layerRI, layerPM, layerRadius;
|
|
let riData = [];
|
|
let pmData = [];
|
|
let pickingMode = null; // 'ri' or 'pm'
|
|
let tempMarker = null;
|
|
let currentView = 'dashboard';
|
|
let searchTerm = '';
|
|
|
|
const rumahIbadahIconConfig = {
|
|
Masjid: { icon: 'fa-mosque', className: 'mosque' },
|
|
Gereja: { icon: 'fa-church', className: 'church' },
|
|
Pura: { icon: 'fa-gopuram', className: 'temple' },
|
|
Vihara: { icon: 'fa-vihara', className: 'vihara' },
|
|
Klenteng: { icon: 'fa-torii-gate', className: 'klenteng' },
|
|
Lainnya: { icon: 'fa-place-of-worship', className: 'other' }
|
|
};
|
|
|
|
function getRumahIbadahIcon(jenis) {
|
|
const config = rumahIbadahIconConfig[jenis] || rumahIbadahIconConfig.Lainnya;
|
|
|
|
return L.divIcon({
|
|
className: 'ri-div-icon',
|
|
html: `<div class="ri-marker ri-${config.className}" title="${jenis || 'Rumah Ibadah'}"><i class="fas ${config.icon}"></i></div>`,
|
|
iconSize: [36, 46],
|
|
iconAnchor: [18, 43],
|
|
popupAnchor: [0, -43]
|
|
});
|
|
}
|
|
|
|
// PM Icons are now circleMarkers generated inline in renderPM
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
await initMap();
|
|
initUI();
|
|
loadData();
|
|
});
|
|
|
|
async function initMap() {
|
|
map = L.map('map', {
|
|
zoomControl: false,
|
|
preferCanvas: true
|
|
}).setView([-0.060547, 109.344902], 13);
|
|
window.map = map;
|
|
|
|
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
|
|
|
const tileOptions = {
|
|
keepBuffer: 6,
|
|
updateWhenIdle: false,
|
|
updateWhenZooming: false,
|
|
updateInterval: 100
|
|
};
|
|
|
|
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
...tileOptions,
|
|
attribution: '© OpenStreetMap contributors'
|
|
});
|
|
|
|
const satelit = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
|
...tileOptions,
|
|
attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
|
|
});
|
|
|
|
const darkMap = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
...tileOptions,
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
subdomains: 'abcd',
|
|
maxZoom: 20
|
|
});
|
|
|
|
osm.addTo(map);
|
|
|
|
const baseMaps = {
|
|
"OSM Standard": osm,
|
|
"Citra Satelit": satelit,
|
|
"Dark Mode": darkMap
|
|
};
|
|
|
|
layerRadius = L.featureGroup().addTo(map);
|
|
layerRI = L.featureGroup().addTo(map);
|
|
layerPM = L.featureGroup().addTo(map);
|
|
|
|
const overlays = {
|
|
"Radius Layanan": layerRadius,
|
|
"Rumah Ibadah": layerRI,
|
|
"Penduduk Miskin": layerPM
|
|
};
|
|
|
|
L.control.layers(baseMaps, overlays, { position: 'topright' }).addTo(map);
|
|
|
|
// Map Click for Picking Location
|
|
map.on('click', async (e) => {
|
|
if (!pickingMode) return;
|
|
|
|
const lat = e.latlng.lat;
|
|
const lng = e.latlng.lng;
|
|
|
|
if (tempMarker) map.removeLayer(tempMarker);
|
|
tempMarker = L.marker([lat, lng]).addTo(map);
|
|
|
|
if (pickingMode === 'ri') {
|
|
document.getElementById('ri-lat').value = lat.toFixed(6);
|
|
document.getElementById('ri-lng').value = lng.toFixed(6);
|
|
document.getElementById('ri-alamat').value = 'Mencari alamat...';
|
|
document.getElementById('ri-alamat').value = await reverseGeocode(lat, lng);
|
|
document.getElementById('modal-ri').classList.add('show');
|
|
} else if (pickingMode === 'pm') {
|
|
document.getElementById('pm-lat').value = lat.toFixed(6);
|
|
document.getElementById('pm-lng').value = lng.toFixed(6);
|
|
document.getElementById('pm-alamat').value = 'Mencari alamat...';
|
|
document.getElementById('pm-alamat').value = await reverseGeocode(lat, lng);
|
|
document.getElementById('modal-pm').classList.add('show');
|
|
}
|
|
|
|
pickingMode = null;
|
|
if(tempMarker) map.removeLayer(tempMarker);
|
|
});
|
|
}
|
|
|
|
function initUI() {
|
|
// Sidebar Toggle
|
|
document.getElementById('toggle-sidebar').addEventListener('click', () => {
|
|
const sidebar = document.getElementById('sidebar');
|
|
if (window.innerWidth <= 768) {
|
|
sidebar.classList.toggle('open');
|
|
} else {
|
|
sidebar.classList.toggle('collapsed');
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
|
item.addEventListener('click', () => switchView(item.dataset.view));
|
|
});
|
|
|
|
// Menus
|
|
const openAddRI = () => {
|
|
switchView('map');
|
|
document.getElementById('form-ri').reset();
|
|
document.getElementById('ri-id').value = '';
|
|
document.getElementById('modal-ri-title').innerText = 'Tambah Rumah Ibadah';
|
|
document.getElementById('modal-ri').classList.add('show');
|
|
};
|
|
document.getElementById('menu-add-ri').addEventListener('click', openAddRI);
|
|
document.getElementById('fab-add-ri')?.addEventListener('click', openAddRI);
|
|
|
|
const openAddPM = () => {
|
|
switchView('map');
|
|
document.getElementById('form-pm').reset();
|
|
document.getElementById('pm-id').value = '';
|
|
document.getElementById('anggota-container').innerHTML = '';
|
|
document.getElementById('bantuan-fields').style.display = 'none';
|
|
document.getElementById('modal-pm-title').innerText = 'Tambah Penduduk Miskin';
|
|
document.getElementById('modal-pm').classList.add('show');
|
|
};
|
|
document.getElementById('menu-add-pm').addEventListener('click', openAddPM);
|
|
document.getElementById('fab-add-pm')?.addEventListener('click', openAddPM);
|
|
document.getElementById('dashboard-add-ri')?.addEventListener('click', openAddRI);
|
|
document.getElementById('dashboard-add-pm')?.addEventListener('click', openAddPM);
|
|
document.getElementById('dashboard-open-map')?.addEventListener('click', () => switchView('map'));
|
|
|
|
// Filters
|
|
document.getElementById('filter-status').addEventListener('change', renderMapLayers);
|
|
document.getElementById('filter-jenis').addEventListener('change', renderMapLayers);
|
|
document.getElementById('filter-kebutuhan').addEventListener('change', renderPM);
|
|
|
|
// Search
|
|
document.getElementById('search-input').addEventListener('input', () => {
|
|
searchTerm = getSearchTerm();
|
|
renderMapLayers();
|
|
});
|
|
document.getElementById('search-input').addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
applyMapSearch(true);
|
|
}
|
|
});
|
|
document.getElementById('search-button').addEventListener('click', () => applyMapSearch(true));
|
|
|
|
// Location
|
|
document.getElementById('btn-my-location').addEventListener('click', () => {
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
const lat = position.coords.latitude;
|
|
const lng = position.coords.longitude;
|
|
map.flyTo([lat, lng], 16);
|
|
L.marker([lat, lng]).addTo(map).bindPopup('Lokasi Anda').openPopup();
|
|
},
|
|
(err) => alert('Gagal mendapatkan lokasi: ' + err.message)
|
|
);
|
|
} else {
|
|
alert('Geolocation tidak didukung browser ini.');
|
|
}
|
|
});
|
|
|
|
switchView(currentView);
|
|
}
|
|
|
|
function switchView(view) {
|
|
currentView = view;
|
|
const mainContent = document.querySelector('.main-content');
|
|
|
|
document.querySelectorAll('.view-panel').forEach(panel => {
|
|
panel.classList.toggle('active', panel.id === `${view}-view`);
|
|
});
|
|
|
|
document.querySelectorAll('.menu-item[data-view]').forEach(item => {
|
|
item.classList.toggle('active', item.dataset.view === view);
|
|
});
|
|
|
|
mainContent?.classList.toggle('map-mode', view === 'map');
|
|
mainContent?.classList.toggle('dashboard-mode', view !== 'map');
|
|
document.querySelector('.search-bar')?.classList.toggle('hidden', view !== 'map');
|
|
document.querySelector('.map-header-controls')?.classList.toggle('hidden', view !== 'map');
|
|
|
|
if (window.innerWidth <= 768) {
|
|
document.getElementById('sidebar').classList.remove('open');
|
|
}
|
|
|
|
if (view === 'map' && map) {
|
|
refreshMapSize();
|
|
}
|
|
}
|
|
|
|
function refreshMapSize() {
|
|
[0, 100, 300].forEach(delay => {
|
|
setTimeout(() => map.invalidateSize({ pan: false }), delay);
|
|
});
|
|
}
|
|
|
|
function getSearchTerm() {
|
|
return document.getElementById('search-input').value.trim().toLowerCase();
|
|
}
|
|
|
|
function textIncludes(value, term) {
|
|
return String(value || '').toLowerCase().includes(term);
|
|
}
|
|
|
|
function matchesRI(ri) {
|
|
if (!searchTerm) return true;
|
|
|
|
return [
|
|
ri.nama,
|
|
ri.jenis,
|
|
ri.kontak,
|
|
ri.alamat
|
|
].some(value => textIncludes(value, searchTerm));
|
|
}
|
|
|
|
function matchesPM(pm) {
|
|
if (!searchTerm) return true;
|
|
|
|
const anggotaMatch = Array.isArray(pm.anggota) && pm.anggota.some(anggota => [
|
|
anggota.nama,
|
|
anggota.hubungan,
|
|
anggota.pekerjaan
|
|
].some(value => textIncludes(value, searchTerm)));
|
|
|
|
return anggotaMatch || [
|
|
pm.kk_nama,
|
|
pm.kk_nik,
|
|
pm.alamat,
|
|
pm.status_bantuan,
|
|
pm.jenis_bantuan,
|
|
pm.kategori_kebutuhan,
|
|
pm.deskripsi_kebutuhan,
|
|
pm.tindak_lanjut,
|
|
pm.kondisi_rumah,
|
|
pm.rumah_ibadah_nama
|
|
].some(value => textIncludes(value, searchTerm));
|
|
}
|
|
|
|
function getFilteredRI() {
|
|
const filterJenis = document.getElementById('filter-jenis').value;
|
|
|
|
return riData.filter(ri => {
|
|
if (filterJenis !== 'all' && ri.jenis !== filterJenis) return false;
|
|
return matchesRI(ri);
|
|
});
|
|
}
|
|
|
|
function getFilteredPM() {
|
|
const filterStatus = document.getElementById('filter-status').value;
|
|
const filterKebutuhan = document.getElementById('filter-kebutuhan').value;
|
|
|
|
return pmData.filter(pm => {
|
|
if (filterStatus !== 'all' && pm.status_bantuan !== filterStatus) return false;
|
|
if (filterKebutuhan !== 'all' && pm.kategori_kebutuhan !== filterKebutuhan) return false;
|
|
return matchesPM(pm);
|
|
});
|
|
}
|
|
|
|
function renderMapLayers() {
|
|
renderRI();
|
|
renderPM();
|
|
}
|
|
|
|
function applyMapSearch(focusResult = false) {
|
|
searchTerm = getSearchTerm();
|
|
renderMapLayers();
|
|
|
|
if (focusResult) {
|
|
switchView('map');
|
|
focusSearchResults();
|
|
}
|
|
}
|
|
|
|
function focusSearchResults() {
|
|
const results = [
|
|
...getFilteredRI().map(item => [parseFloat(item.lat), parseFloat(item.lng)]),
|
|
...getFilteredPM().map(item => [parseFloat(item.lat), parseFloat(item.lng)])
|
|
].filter(([lat, lng]) => Number.isFinite(lat) && Number.isFinite(lng));
|
|
|
|
if (!searchTerm || results.length === 0) {
|
|
alert(searchTerm ? 'Data tidak ditemukan di peta.' : 'Masukkan kata kunci pencarian.');
|
|
return;
|
|
}
|
|
|
|
if (results.length === 1) {
|
|
map.flyTo(results[0], 16);
|
|
return;
|
|
}
|
|
|
|
map.fitBounds(L.latLngBounds(results), { padding: [40, 40], maxZoom: 16 });
|
|
}
|
|
|
|
function pickLocation(mode) {
|
|
switchView('map');
|
|
pickingMode = mode;
|
|
closeModal('modal-' + mode);
|
|
}
|
|
|
|
function closeModal(id) {
|
|
document.getElementById(id).classList.remove('show');
|
|
}
|
|
|
|
async function reverseGeocode(lat, lng) {
|
|
try {
|
|
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`);
|
|
const data = await res.json();
|
|
return data.display_name || 'Alamat tidak ditemukan';
|
|
} catch (e) {
|
|
return 'Gagal memuat alamat';
|
|
}
|
|
}
|
|
|
|
function toggleBantuanFields() {
|
|
const status = document.getElementById('pm-status').value;
|
|
document.getElementById('bantuan-fields').style.display = status === 'Sudah dibantu' ? 'block' : 'none';
|
|
}
|
|
|
|
function addAnggotaField(data = null) {
|
|
const container = document.getElementById('anggota-container');
|
|
const id = Date.now() + Math.floor(Math.random() * 100);
|
|
const html = `
|
|
<div class="dynamic-item" id="anggota-${id}">
|
|
<button type="button" class="btn-remove" onclick="document.getElementById('anggota-${id}').remove()">X</button>
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<div class="form-group">
|
|
<label>Nama</label>
|
|
<input type="text" class="form-control a-nama" value="${data ? data.nama : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Hubungan</label>
|
|
<input type="text" class="form-control a-hubungan" value="${data ? data.hubungan : ''}" placeholder="Anak/Istri/dll" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Umur</label>
|
|
<input type="number" class="form-control a-umur" value="${data ? data.umur : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Pekerjaan</label>
|
|
<input type="text" class="form-control a-pekerjaan" value="${data && data.pekerjaan ? data.pekerjaan : ''}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
container.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
// Data Fetching and Rendering
|
|
async function loadData() {
|
|
await loadStats();
|
|
await fetchRI();
|
|
await fetchPM();
|
|
}
|
|
|
|
async function loadStats() {
|
|
try {
|
|
const res = await fetch('api/dashboard_stats.php');
|
|
const json = await res.json();
|
|
if(json.success) {
|
|
document.getElementById('stat-ri').innerText = json.data.total_rumah_ibadah;
|
|
document.getElementById('stat-pm').innerText = json.data.total_penduduk;
|
|
document.getElementById('stat-sudah').innerText = json.data.sudah_dibantu;
|
|
document.getElementById('stat-belum').innerText = json.data.belum_dibantu;
|
|
document.getElementById('stat-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
|
document.getElementById('stat-prioritas').innerText = json.data.prioritas_tinggi || 0;
|
|
document.getElementById('stat-luar-radius').innerText = json.data.luar_radius || 0;
|
|
document.getElementById('summary-sudah').innerText = json.data.sudah_dibantu;
|
|
document.getElementById('summary-belum').innerText = json.data.belum_dibantu;
|
|
document.getElementById('summary-menunggu').innerText = json.data.menunggu_verifikasi || 0;
|
|
renderDashboardLists(json.data);
|
|
}
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
|
|
function renderDashboardLists(stats) {
|
|
const priorityList = document.getElementById('priority-list');
|
|
const needList = document.getElementById('need-list');
|
|
|
|
if (priorityList) {
|
|
const items = stats.top_prioritas || [];
|
|
priorityList.innerHTML = items.length ? items.map(item => `
|
|
<div class="priority-item">
|
|
<div class="priority-score">${item.skor_prioritas || 0}</div>
|
|
<div>
|
|
<strong>${item.kk_nama}</strong>
|
|
<p>${item.kategori_kebutuhan || 'Kebutuhan belum diisi'} - ${item.tindak_lanjut || 'Belum disurvei'}</p>
|
|
</div>
|
|
</div>
|
|
`).join('') : '<div class="need-item"><p>Belum ada data prioritas.</p></div>';
|
|
}
|
|
|
|
if (needList) {
|
|
const items = stats.kebutuhan_terbanyak || [];
|
|
needList.innerHTML = items.length ? items.map(item => `
|
|
<div class="need-item">
|
|
<strong>${item.kategori}</strong>
|
|
<p>${item.total} keluarga membutuhkan kategori ini</p>
|
|
</div>
|
|
`).join('') : '<div class="need-item"><p>Belum ada data kebutuhan.</p></div>';
|
|
}
|
|
}
|
|
|
|
async function fetchRI() {
|
|
try {
|
|
const res = await fetch('api/rumah_ibadah_read.php');
|
|
const json = await res.json();
|
|
if(json.success) {
|
|
riData = json.data;
|
|
renderRI();
|
|
}
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
|
|
async function fetchPM() {
|
|
try {
|
|
const res = await fetch('api/penduduk_read.php');
|
|
const json = await res.json();
|
|
if(json.success) {
|
|
pmData = json.data;
|
|
renderPM();
|
|
}
|
|
} catch(e) { console.error(e); }
|
|
}
|
|
|
|
function renderRI() {
|
|
layerRI.clearLayers();
|
|
layerRadius.clearLayers();
|
|
|
|
getFilteredRI().forEach(ri => {
|
|
const lat = parseFloat(ri.lat);
|
|
const lng = parseFloat(ri.lng);
|
|
const rad = parseInt(ri.radius) || 500;
|
|
|
|
// Circle
|
|
L.circle([lat, lng], {
|
|
color: '#4f46e5',
|
|
fillColor: '#4f46e5',
|
|
fillOpacity: 0.1,
|
|
radius: rad,
|
|
interactive: false
|
|
}).addTo(layerRadius);
|
|
|
|
// Marker
|
|
const marker = L.marker([lat, lng], { icon: getRumahIbadahIcon(ri.jenis) }).addTo(layerRI);
|
|
marker.feature = ri;
|
|
|
|
let popup = `
|
|
<div class="popup-header">
|
|
<h3>${ri.nama}</h3>
|
|
<p>${ri.jenis}</p>
|
|
</div>
|
|
<div class="popup-body">
|
|
<div class="popup-row"><div class="label">Kontak</div><div class="value">${ri.kontak || '-'}</div></div>
|
|
<div class="popup-row"><div class="label">Radius</div><div class="value">${rad} m</div></div>
|
|
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${ri.alamat}</div></div>
|
|
<div class="popup-actions">
|
|
<button class="btn-edit" onclick='editRI(${JSON.stringify(ri)})'>Edit</button>
|
|
<button class="btn-delete" onclick='deleteRI(${ri.id})'>Hapus</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
marker.bindPopup(popup);
|
|
});
|
|
|
|
layerRadius.eachLayer(circle => circle.bringToBack());
|
|
}
|
|
|
|
function renderPM() {
|
|
layerPM.clearLayers();
|
|
|
|
getFilteredPM().forEach(pm => {
|
|
const lat = parseFloat(pm.lat);
|
|
const lng = parseFloat(pm.lng);
|
|
const anggotaCount = pm.anggota ? pm.anggota.length : 0;
|
|
|
|
let badgeClass = 'badge-danger';
|
|
if(pm.status_bantuan === 'Sudah dibantu') badgeClass = 'badge-success';
|
|
else if(pm.status_bantuan === 'Menunggu verifikasi') badgeClass = 'badge-warning';
|
|
|
|
const isBlankSpot = pm.is_blank_spot == 1;
|
|
const pmColor = isBlankSpot ? '#ef4444' : '#10b981'; // Merah (Blank Spot), Hijau (Terjangkau)
|
|
const riwayatHtml = Array.isArray(pm.riwayat_bantuan) && pm.riwayat_bantuan.length
|
|
? pm.riwayat_bantuan.slice(0, 3).map(r => `${r.tanggal_bantuan}: ${r.jenis_bantuan}`).join('<br>')
|
|
: '-';
|
|
const buktiHtml = pm.bukti_file
|
|
? `<a href="${pm.bukti_file}" target="_blank">Lihat bukti</a>`
|
|
: '-';
|
|
|
|
const marker = L.circleMarker([lat, lng], {
|
|
radius: 7,
|
|
fillColor: pmColor,
|
|
color: '#ffffff',
|
|
weight: 1.5,
|
|
opacity: 1,
|
|
fillOpacity: 0.9
|
|
}).addTo(layerPM);
|
|
marker.feature = pm;
|
|
|
|
let popup = `
|
|
<div class="popup-header ${pm.status_bantuan === 'Sudah dibantu' ? 'success' : pm.status_bantuan === 'Menunggu verifikasi' ? 'warning' : 'danger'}">
|
|
<h3>Keluarga ${pm.kk_nama}</h3>
|
|
<span class="badge ${badgeClass}">${pm.status_bantuan}</span>
|
|
</div>
|
|
<div class="popup-body">
|
|
<div class="popup-row"><div class="label">Jumlah Anggota</div><div class="value">${anggotaCount} orang</div></div>
|
|
<div class="popup-row"><div class="label">Rumah Ibadah Terdekat</div><div class="value">${pm.rumah_ibadah_nama || '-'}</div></div>
|
|
<div class="popup-row"><div class="label">Prioritas</div><div class="value">${pm.skor_prioritas || 0} (${pm.label_prioritas || '-'})</div></div>
|
|
<div class="popup-row"><div class="label">Kebutuhan</div><div class="value">${pm.kategori_kebutuhan || '-'}</div></div>
|
|
<div class="popup-row"><div class="label">Tindak Lanjut</div><div class="value">${pm.tindak_lanjut || 'Belum disurvei'}</div></div>
|
|
<div class="popup-row"><div class="label">Jarak Lurus</div><div class="value">${pm.jarak_m ? pm.jarak_m + ' meter' : '-'} ${isBlankSpot ? '<i style="color:red">(Di luar radius)</i>' : '<i style="color:green">(Terjangkau)</i>'}</div></div>
|
|
<div class="popup-row"><div class="label">Jarak Rute</div><div class="value">${pm.jarak_rute_m ? pm.jarak_rute_m + ' meter' : '-'}</div></div>
|
|
${pm.status_bantuan === 'Sudah dibantu' ? `<div class="popup-row"><div class="label">Bantuan</div><div class="value">${pm.jenis_bantuan} (${pm.tanggal_bantuan})</div></div>` : ''}
|
|
<div class="popup-row"><div class="label">Riwayat</div><div class="value">${riwayatHtml}</div></div>
|
|
<div class="popup-row"><div class="label">Bukti</div><div class="value">${buktiHtml}</div></div>
|
|
<div class="popup-row" style="flex-direction: column;"><div class="label" style="width:100%">Alamat</div><div class="value" style="font-size:0.8rem; font-weight:400">${pm.alamat}</div></div>
|
|
<div class="popup-actions">
|
|
<button class="btn-edit" onclick='editPM(${JSON.stringify(pm).replace(/'/g, "\\'")})'>Edit</button>
|
|
<button class="btn-edit" onclick='openBantuanModal(${pm.id})'>Bantuan</button>
|
|
<button class="btn-delete" onclick='deletePM(${pm.id})'>Hapus</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
marker.bindPopup(popup);
|
|
});
|
|
|
|
layerPM.bringToFront();
|
|
}
|
|
|
|
// CRUD Operations
|
|
async function saveRI() {
|
|
const id = document.getElementById('ri-id').value;
|
|
const data = {
|
|
nama: document.getElementById('ri-nama').value,
|
|
jenis: document.getElementById('ri-jenis').value,
|
|
kontak: document.getElementById('ri-kontak').value,
|
|
lat: document.getElementById('ri-lat').value,
|
|
lng: document.getElementById('ri-lng').value,
|
|
alamat: document.getElementById('ri-alamat').value,
|
|
radius: document.getElementById('ri-radius').value
|
|
};
|
|
|
|
if(!data.nama || !data.lat) { alert("Isi form dengan lengkap"); return; }
|
|
|
|
const url = id ? 'api/rumah_ibadah_update.php' : 'api/rumah_ibadah_create.php';
|
|
if(id) data.id = id;
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(data)
|
|
});
|
|
const json = await res.json();
|
|
if(json.success) {
|
|
closeModal('modal-ri');
|
|
await fetchRI();
|
|
await fetchPM();
|
|
await loadStats();
|
|
} else {
|
|
alert(json.message);
|
|
}
|
|
} catch(e) { alert('Error saving data'); }
|
|
}
|
|
|
|
async function savePM() {
|
|
const id = document.getElementById('pm-id').value;
|
|
const data = new FormData();
|
|
const anggota = [];
|
|
|
|
data.append('kk_nama', document.getElementById('pm-nama').value);
|
|
data.append('kk_nik', document.getElementById('pm-nik').value);
|
|
data.append('lat', document.getElementById('pm-lat').value);
|
|
data.append('lng', document.getElementById('pm-lng').value);
|
|
data.append('alamat', document.getElementById('pm-alamat').value);
|
|
data.append('kategori_kebutuhan', document.getElementById('pm-kategori-kebutuhan').value);
|
|
data.append('deskripsi_kebutuhan', document.getElementById('pm-deskripsi-kebutuhan').value);
|
|
data.append('kondisi_rumah', document.getElementById('pm-kondisi-rumah').value);
|
|
data.append('penghasilan', document.getElementById('pm-penghasilan').value);
|
|
data.append('status_bantuan', document.getElementById('pm-status').value);
|
|
data.append('jenis_bantuan', document.getElementById('pm-jenis-bantuan').value);
|
|
data.append('tanggal_bantuan', document.getElementById('pm-tanggal-bantuan').value);
|
|
data.append('tindak_lanjut', document.getElementById('pm-tindak-lanjut').value);
|
|
data.append('catatan_survei', document.getElementById('pm-catatan-survei').value);
|
|
data.append('existing_bukti_file', document.getElementById('pm-existing-bukti-file').value);
|
|
|
|
const buktiFile = document.getElementById('pm-bukti-file').files[0];
|
|
if (buktiFile) data.append('bukti_file', buktiFile);
|
|
|
|
if(!data.get('kk_nama') || !data.get('lat')) { alert("Isi form dengan lengkap"); return; }
|
|
if (data.get('penghasilan') !== '' && Number(data.get('penghasilan')) < 0) {
|
|
alert('Penghasilan tidak boleh bernilai negatif.');
|
|
document.getElementById('pm-penghasilan').focus();
|
|
return;
|
|
}
|
|
|
|
const items = document.querySelectorAll('.dynamic-item');
|
|
items.forEach(el => {
|
|
anggota.push({
|
|
nama: el.querySelector('.a-nama').value,
|
|
hubungan: el.querySelector('.a-hubungan').value,
|
|
umur: el.querySelector('.a-umur').value,
|
|
pekerjaan: el.querySelector('.a-pekerjaan').value
|
|
});
|
|
});
|
|
data.append('anggota', JSON.stringify(anggota));
|
|
|
|
const url = id ? 'api/penduduk_update.php' : 'api/penduduk_create.php';
|
|
if(id) data.append('id', id);
|
|
|
|
try {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
body: data
|
|
});
|
|
const json = await res.json();
|
|
if(json.success) {
|
|
closeModal('modal-pm');
|
|
await fetchPM();
|
|
await loadStats();
|
|
if(json.ibadah_id) {
|
|
const ri = riData.find(r => r.id == json.ibadah_id);
|
|
if(ri) alert(`Warga otomatis dimasukkan ke binaan: ${ri.nama}`);
|
|
}
|
|
} else {
|
|
alert(json.message);
|
|
}
|
|
} catch(e) { alert('Error saving data'); }
|
|
}
|
|
|
|
function editRI(data) {
|
|
document.getElementById('ri-id').value = data.id;
|
|
document.getElementById('ri-nama').value = data.nama;
|
|
document.getElementById('ri-jenis').value = data.jenis;
|
|
document.getElementById('ri-kontak').value = data.kontak;
|
|
document.getElementById('ri-lat').value = data.lat;
|
|
document.getElementById('ri-lng').value = data.lng;
|
|
document.getElementById('ri-alamat').value = data.alamat;
|
|
document.getElementById('ri-radius').value = data.radius;
|
|
document.getElementById('modal-ri-title').innerText = 'Edit Rumah Ibadah';
|
|
document.getElementById('modal-ri').classList.add('show');
|
|
}
|
|
|
|
function editPM(data) {
|
|
document.getElementById('pm-id').value = data.id;
|
|
document.getElementById('pm-nama').value = data.kk_nama;
|
|
document.getElementById('pm-nik').value = data.kk_nik;
|
|
document.getElementById('pm-lat').value = data.lat;
|
|
document.getElementById('pm-lng').value = data.lng;
|
|
document.getElementById('pm-alamat').value = data.alamat;
|
|
document.getElementById('pm-kategori-kebutuhan').value = data.kategori_kebutuhan || '';
|
|
document.getElementById('pm-deskripsi-kebutuhan').value = data.deskripsi_kebutuhan || '';
|
|
document.getElementById('pm-kondisi-rumah').value = data.kondisi_rumah || '';
|
|
document.getElementById('pm-penghasilan').value = data.penghasilan || '';
|
|
document.getElementById('pm-status').value = data.status_bantuan;
|
|
document.getElementById('pm-jenis-bantuan').value = data.jenis_bantuan;
|
|
document.getElementById('pm-tanggal-bantuan').value = data.tanggal_bantuan;
|
|
document.getElementById('pm-tindak-lanjut').value = data.tindak_lanjut || 'Belum disurvei';
|
|
document.getElementById('pm-catatan-survei').value = data.catatan_survei || '';
|
|
document.getElementById('pm-existing-bukti-file').value = data.bukti_file || '';
|
|
document.getElementById('pm-bukti-file').value = '';
|
|
|
|
toggleBantuanFields();
|
|
|
|
document.getElementById('anggota-container').innerHTML = '';
|
|
if(data.anggota && data.anggota.length > 0) {
|
|
data.anggota.forEach(a => addAnggotaField(a));
|
|
}
|
|
|
|
document.getElementById('modal-pm-title').innerText = 'Edit Penduduk Miskin';
|
|
document.getElementById('modal-pm').classList.add('show');
|
|
}
|
|
|
|
function setBantuanAlert(message = '', type = 'error') {
|
|
const alertBox = document.getElementById('bantuan-alert');
|
|
if (!alertBox) return;
|
|
|
|
alertBox.textContent = message;
|
|
alertBox.hidden = !message;
|
|
alertBox.classList.toggle('success', type === 'success');
|
|
}
|
|
|
|
function openBantuanModal(id) {
|
|
const penerima = pmData.find(pm => String(pm.id) === String(id));
|
|
const form = document.getElementById('form-bantuan');
|
|
|
|
form.reset();
|
|
setBantuanAlert();
|
|
document.getElementById('bantuan-penduduk-id').value = id;
|
|
document.getElementById('bantuan-penerima').value = penerima ? `Keluarga ${penerima.kk_nama}` : `ID ${id}`;
|
|
document.getElementById('bantuan-jenis').value = penerima?.kategori_kebutuhan || '';
|
|
document.getElementById('bantuan-tanggal').value = new Date().toISOString().slice(0, 10);
|
|
document.getElementById('bantuan-pemberi').value = 'Admin';
|
|
document.getElementById('bantuan-bukti-file').value = '';
|
|
document.getElementById('btn-save-bantuan').disabled = false;
|
|
document.getElementById('modal-bantuan').classList.add('show');
|
|
}
|
|
|
|
async function saveBantuan() {
|
|
const id = document.getElementById('bantuan-penduduk-id').value;
|
|
const jenis = document.getElementById('bantuan-jenis').value.trim();
|
|
const tanggal = document.getElementById('bantuan-tanggal').value;
|
|
const nilai = document.getElementById('bantuan-nilai').value;
|
|
const pemberi = document.getElementById('bantuan-pemberi').value.trim() || 'Admin';
|
|
const catatan = document.getElementById('bantuan-catatan').value.trim();
|
|
const buktiFile = document.getElementById('bantuan-bukti-file').files[0];
|
|
const saveButton = document.getElementById('btn-save-bantuan');
|
|
|
|
if (!id || !jenis || !tanggal) {
|
|
setBantuanAlert('Keluarga penerima, jenis bantuan, dan tanggal wajib diisi.');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('penduduk_id', id);
|
|
formData.append('jenis_bantuan', jenis);
|
|
formData.append('tanggal_bantuan', tanggal);
|
|
formData.append('nilai_bantuan', nilai || '');
|
|
formData.append('pemberi_bantuan', pemberi);
|
|
formData.append('catatan', catatan || '');
|
|
if (buktiFile) formData.append('bukti_file', buktiFile);
|
|
|
|
try {
|
|
saveButton.disabled = true;
|
|
saveButton.innerText = 'Menyimpan...';
|
|
const res = await fetch('api/bantuan_create.php', { method: 'POST', body: formData });
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
closeModal('modal-bantuan');
|
|
await fetchPM();
|
|
await loadStats();
|
|
renderMapLayers();
|
|
} else {
|
|
setBantuanAlert(json.message || 'Gagal menambah bantuan.');
|
|
}
|
|
} catch(e) {
|
|
setBantuanAlert('Terjadi kesalahan saat menyimpan bantuan.');
|
|
} finally {
|
|
saveButton.disabled = false;
|
|
saveButton.innerText = 'Simpan Bantuan';
|
|
}
|
|
}
|
|
|
|
async function deleteRI(id) {
|
|
if(!confirm('Hapus rumah ibadah ini?')) return;
|
|
try {
|
|
const res = await fetch('api/rumah_ibadah_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
|
const json = await res.json();
|
|
if(json.success) { fetchRI(); fetchPM(); loadStats(); }
|
|
} catch(e) { alert('Error'); }
|
|
}
|
|
|
|
async function deletePM(id) {
|
|
if(!confirm('Hapus penduduk miskin ini?')) return;
|
|
try {
|
|
const res = await fetch('api/penduduk_delete.php', { method:'POST', body:JSON.stringify({id}) });
|
|
const json = await res.json();
|
|
if(json.success) { fetchPM(); loadStats(); }
|
|
} catch(e) { alert('Error'); }
|
|
}
|