Files
Webgis-Poverty/js/feature-kemiskinan.js
T
Abimanyu Ridho 35e1a93aeb Initial commit
2026-06-06 00:22:16 +07:00

452 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
function renderKemiskinanPanel(pb) {
return Promise.all([callAPI('rumah_ibadah.php'), callAPI('penduduk_miskin.php')]).then(([riRes, pmRes]) => {
const ri = riRes.data || [];
const pm = pmRes.data || [];
pb.innerHTML = `
<div class="layer-section">
<div class="layer-section-title">Visibilitas Layer</div>
${mkLayerItem('rumahIbadah', '#f59e0b', 'Rumah Ibadah', ri.length)}
${mkLayerItem('ibadahRadius', '#fbbf24', 'Radius Jangkauan', ri.length, true)}
${mkLayerItem('pendudukMiskin', '#16a34a', 'Penduduk Miskin', pm.length)}
</div>
<div class="layer-section">
<div class="layer-section-title">Tambah Data</div>
<button class="btn-add" id="btn-add-ri" onclick="startAdd('ri')"> Tambah Rumah Ibadah</button>
<button class="btn-add" id="btn-add-pm" onclick="startAdd('pm')" style="margin-top:5px"> Tambah Penduduk Miskin</button>
</div>
<div class="layer-section">
<div class="layer-section-title">Rumah Ibadah (${ri.length})</div>
<div class="data-list">
${ri.length ? ri.map(r => `
<div class="data-item" onclick="map.flyTo([${r.latitude},${r.longitude}],17)">
<div class="item-icon">⛪</div>
<div class="item-info">
<div class="item-name">${esc(r.nama)}</div>
<div class="item-sub">${r.jenis} · R=${r.radius_meter}m</div>
</div>
<div style="display:flex;gap:4px">
<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();editRI(${r.id})">✏</button>
<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deleteRI(${r.id})">✕</button>
</div>
</div>`).join('') : '<div class="loading">Belum ada data</div>'}
</div>
</div>
<div class="layer-section">
<div class="layer-section-title">Penduduk Miskin (${pm.length})</div>
<div class="data-list">
${pm.length ? pm.map(p => `
<div class="data-item" onclick="map.flyTo([${p.latitude},${p.longitude}],17)">
<div class="item-icon">${pmEmoji(p)}</div>
<div class="item-info">
<div class="item-name">${esc(p.nama)}</div>
<div class="item-sub">${p.total_bantuan} bantuan · ${p.jumlah_anggota} jiwa</div>
</div>
<div class="item-badge ${p.status_proximity === 'dalam_radius' ? 'badge-green' : 'badge-red'}">
${p.status_proximity === 'dalam_radius' ? 'DALAM' : 'LUAR'}
</div>
</div>`).join('') : '<div class="loading">Belum ada data</div>'}
</div>
</div>`;
loadRILayer(ri);
loadPMLayer(pm);
registerSearchEntries('ri', ri.map(r => ({
key: `ri:${r.id}`,
label: r.nama,
subtitle: `Rumah Ibadah · ${r.jenis}${r.alamat ? ` · ${r.alamat}` : ''}`,
typeLabel: 'Rumah Ibadah',
icon: '⛪',
searchText: normalizeSearchText(`${r.nama} ${r.jenis} rumah ibadah`),
focus: () => focusMarkerById('rumahIbadah', r.id)
})));
registerSearchEntries('pm', pm.map(p => ({
key: `pm:${p.id}`,
label: p.nama,
subtitle: `Penduduk Miskin · ${p.jumlah_anggota} jiwa${p.alamat ? ` · ${p.alamat}` : ''}`,
typeLabel: 'Penduduk Miskin',
icon: '👤',
searchText: normalizeSearchText(`${p.nama} ${p.nik || ''} penduduk miskin`),
focus: () => focusMarkerById('pendudukMiskin', p.id)
})));
updateStatusCount();
});
}
function mkLayerItem(key, color, label, count, isRing = false) {
const dot = isRing
? `<div class="layer-dot" style="background:transparent;border:2px solid ${color}"></div>`
: `<div class="layer-dot" style="background:${color}"></div>`;
return `<div class="layer-item ${layerVisible[key] ? 'checked' : ''}" onclick="toggleLayer('${key}',this)">
<div class="layer-check">${layerVisible[key] ? '✓' : ''}</div>${dot}
<div class="layer-label">${label}</div>
<div class="layer-count">${count}</div>
</div>`;
}
function pmEmoji(p) {
if (p.bantuan_pemberdayaan > 0 && p.bantuan_konsumtif > 0) return '🔗';
if (p.bantuan_pemberdayaan > 0) return '📚';
if (p.bantuan_konsumtif > 0) return '🛍';
return '👤';
}
function loadRILayer(data) {
layers.rumahIbadah.clearLayers();
layers.ibadahRadius.clearLayers();
data.forEach(r => {
const circle = L.circle([r.latitude, r.longitude], {
radius: r.radius_meter,
color: '#f59e0b', fillColor: '#fef3c7', fillOpacity: 0.18,
weight: 1.5, dashArray: '6,4'
});
layers.ibadahRadius.addLayer(circle);
const icon = L.divIcon({
className: '',
html: `<div style="width:32px;height:32px;background:#f59e0b;border:3px solid #fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:15px;box-shadow:0 3px 10px rgba(0,0,0,.2);cursor:grab;">⛪</div>`,
iconSize: [32, 32], iconAnchor: [16, 16]
});
const marker = L.marker([r.latitude, r.longitude], { icon, draggable: true }).bindPopup(riPopup(r));
marker._dataId = r.id;
marker.on('dragend', async function (ev) {
const { lat, lng } = ev.target.getLatLng();
circle.setLatLng([lat, lng]);
const res = await callAPI(`rumah_ibadah.php?id=${r.id}`, 'PUT', {
nama: r.nama, jenis: r.jenis, alamat: r.alamat,
no_wa: r.no_wa, latitude: lat, longitude: lng,
radius_meter: r.radius_meter
});
if (res.status === 'success') {
r.latitude = lat;
r.longitude = lng;
toast(`📍 ${r.nama} dipindahkan & koordinat diperbarui`, 'success');
await recalcAllProximity();
} else {
toast('Gagal update lokasi: ' + res.message, 'error');
marker.setLatLng([r.latitude, r.longitude]);
}
});
layers.rumahIbadah.addLayer(marker);
});
}
function riPopup(r) {
return `<h4>⛪ ${esc(r.nama)}</h4>
<p><b>Jenis:</b> ${r.jenis}</p>
<p><b>Radius:</b> ${r.radius_meter} m</p>
${r.alamat ? `<p><b>Alamat:</b> ${esc(r.alamat)}</p>` : ''}
${r.no_wa ? `<p><b>WA:</b> ${r.no_wa}</p>` : ''}
<div class="popup-drag-hint">↔ Seret marker untuk pindahkan lokasi — auto-save</div>
<div class="popup-actions">
<button class="btn btn-ghost btn-sm" onclick="editRI(${r.id})">✏ Edit</button>
<button class="btn btn-danger btn-sm" onclick="deleteRI(${r.id})">✕ Hapus</button>
</div>`;
}
function loadPMLayer(data) {
layers.pendudukMiskin.clearLayers();
data.forEach(p => {
const inR = p.status_proximity === 'dalam_radius';
const color = inR ? '#16a34a' : '#dc2626';
const hasBoth = p.bantuan_pemberdayaan > 0 && p.bantuan_konsumtif > 0;
const hasPemb = p.bantuan_pemberdayaan > 0;
const hasKons = p.bantuan_konsumtif > 0;
let dot = '';
if (hasBoth || hasPemb) dot = `<div style="position:absolute;top:-3px;right:-3px;width:9px;height:9px;background:#2563eb;border:1.5px solid #fff;border-radius:50%"></div>`;
if (hasKons && !hasPemb) dot = `<div style="position:absolute;top:-3px;right:-3px;width:9px;height:9px;background:#f59e0b;border:1.5px solid #fff;border-radius:50%"></div>`;
const icon = L.divIcon({
className: '',
html: `<div style="position:relative"><div style="width:26px;height:26px;background:${color};border:3px solid #fff;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;box-shadow:0 3px 10px ${color}66;cursor:grab;">👤</div>${dot}</div>`,
iconSize: [26, 26], iconAnchor: [13, 13]
});
const marker = L.marker([p.latitude, p.longitude], { icon, draggable: true }).bindPopup(pmPopup(p));
marker._dataId = p.id;
marker.on('dragend', async function (ev) {
const { lat, lng } = ev.target.getLatLng();
const res = await callAPI(`penduduk_miskin.php?id=${p.id}`, 'PUT', {
nama: p.nama, no_wa: p.no_wa, nik: p.nik,
alamat: p.alamat, jumlah_anggota: p.jumlah_anggota,
latitude: lat, longitude: lng
});
if (res.status === 'success') {
const prox = res.data?.proximity;
const statusMsg = prox?.status === 'dalam_radius' ? '✅ Dalam radius' : '❌ Luar radius';
toast(`📍 ${p.nama} dipindahkan — ${statusMsg}`, 'success');
switchTab('kemiskinan');
} else {
toast('Gagal update: ' + res.message, 'error');
marker.setLatLng([p.latitude, p.longitude]);
}
});
layers.pendudukMiskin.addLayer(marker);
});
}
function pmPopup(p) {
const inR = p.status_proximity === 'dalam_radius';
return `<h4>👤 ${esc(p.nama)}</h4>
<div class="proximity-info ${inR ? 'dalam' : 'luar'}">
${inR ? `✅ Dalam radius — ${esc(p.nama_rumah_ibadah || '')}` : '❌ Di luar radius rumah ibadah'}
</div>
${p.no_wa ? `<p><b>WA:</b> ${p.no_wa}</p>` : ''}
${p.nik ? `<p><b>NIK:</b> ${p.nik}</p>` : ''}
${p.alamat ? `<p><b>Alamat:</b> ${esc(p.alamat)}</p>` : ''}
<p><b>Keluarga:</b> ${p.jumlah_anggota} jiwa</p>
<p><b>Bantuan:</b> 📚${p.bantuan_pemberdayaan} · 🛍${p.bantuan_konsumtif}</p>
<div class="popup-drag-hint">↔ Seret marker untuk pindahkan lokasi — auto-save</div>
<div class="popup-actions">
<button class="btn btn-primary btn-sm" onclick="openLogBantuan(${p.id},'${esc(p.nama)}')">📋 Log Bantuan</button>
<button class="btn btn-ghost btn-sm" onclick="editPM(${p.id})">✏ Edit</button>
<button class="btn btn-danger btn-sm" onclick="deletePM(${p.id})">✕</button>
</div>`;
}
function startAdd(type) {
cancelMapClick();
const btnId = type === 'ri' ? 'btn-add-ri' : 'btn-add-pm';
const label = type === 'ri' ? 'lokasi Rumah Ibadah' : 'lokasi Penduduk Miskin';
document.getElementById(btnId).classList.add('active');
showInstruction(`Klik di peta untuk menentukan ${label}`);
onceMapClick((lat, lng) => {
document.getElementById(btnId).classList.remove('active');
hideInstruction();
if (type === 'ri') openRIForm(null, lat, lng);
else openPMForm(null, lat, lng);
});
}
function openRIForm(data, lat, lng) {
const isEdit = !!data;
const fLat = isEdit ? data.latitude : lat;
const fLng = isEdit ? data.longitude : lng;
openModal(isEdit ? 'Edit Rumah Ibadah' : 'Tambah Rumah Ibadah', `
<div class="form-coord">📍 Koordinat: ${fLat.toFixed(7)}, ${fLng.toFixed(7)}</div>
<div class="form-group">
<label class="form-label">Nama Rumah Ibadah *</label>
<input class="form-input" id="ri-nama" value="${esc(isEdit ? data.nama : '')}" placeholder="Masjid Al-Ikhlas…"/>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Jenis *</label>
<select class="form-select" id="ri-jenis">
${['Masjid', 'Gereja', 'Pura', 'Vihara', 'Klenteng', 'Lainnya'].map(j =>
`<option value="${j}"${isEdit && data.jenis === j ? ' selected' : ''}>${j}</option>`
).join('')}
</select>
</div>
<div class="form-group">
<label class="form-label">Radius (meter) *</label>
<input class="form-input" id="ri-radius" type="number" value="${isEdit ? data.radius_meter : 500}" min="50" max="10000"/>
</div>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<input class="form-input" id="ri-alamat" value="${esc(isEdit ? data.alamat || '' : '')}" placeholder="Jl. …"/>
</div>
<div class="form-group">
<label class="form-label">No. WhatsApp</label>
<input class="form-input" id="ri-nowa" value="${esc(isEdit ? data.no_wa || '' : '')}" placeholder="081234…"/>
</div>`,
`<button class="btn btn-ghost" onclick="closeModal()">Batal</button>
<button class="btn btn-primary" onclick="saveRI(${isEdit ? data.id : 'null'},${fLat},${fLng})">💾 Simpan</button>`);
}
async function saveRI(id, lat, lng) {
const body = {
nama: document.getElementById('ri-nama').value.trim(),
jenis: document.getElementById('ri-jenis').value,
alamat: document.getElementById('ri-alamat').value.trim(),
no_wa: document.getElementById('ri-nowa').value.trim(),
latitude: lat,
longitude: lng,
radius_meter: parseInt(document.getElementById('ri-radius').value)
};
if (!body.nama) return toast('Nama wajib diisi', 'error');
const res = await callAPI(id ? `rumah_ibadah.php?id=${id}` : 'rumah_ibadah.php', id ? 'PUT' : 'POST', body);
if (res.status === 'success') {
toast(res.message, 'success');
closeModal();
await recalcAllProximity();
switchTab('kemiskinan');
} else {
toast(res.message, 'error');
}
}
function openPMForm(data, lat, lng) {
const isEdit = !!data;
const fLat = isEdit ? data.latitude : lat;
const fLng = isEdit ? data.longitude : lng;
openModal(isEdit ? 'Edit Penduduk Miskin' : 'Tambah Penduduk Miskin', `
<div class="form-coord">📍 Koordinat: ${fLat.toFixed(7)}, ${fLng.toFixed(7)}</div>
<div class="form-group">
<label class="form-label">Nama Lengkap *</label>
<input class="form-input" id="pm-nama" value="${esc(isEdit ? data.nama : '')}" placeholder="Ahmad Subarjo…"/>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">No. WhatsApp</label>
<input class="form-input" id="pm-nowa" value="${esc(isEdit ? data.no_wa || '' : '')}" placeholder="081234…"/>
</div>
<div class="form-group">
<label class="form-label">NIK</label>
<input class="form-input" id="pm-nik" value="${esc(isEdit ? data.nik || '' : '')}" placeholder="617101…"/>
</div>
</div>
<div class="form-group">
<label class="form-label">Alamat</label>
<input class="form-input" id="pm-alamat" value="${esc(isEdit ? data.alamat || '' : '')}" placeholder="Jl. …"/>
</div>
<div class="form-group">
<label class="form-label">Jumlah Anggota Keluarga</label>
<input class="form-input" id="pm-jml" type="number" value="${isEdit ? data.jumlah_anggota : 1}" min="1" max="20"/>
</div>`,
`<button class="btn btn-ghost" onclick="closeModal()">Batal</button>
<button class="btn btn-primary" onclick="savePM(${isEdit ? data.id : 'null'},${fLat},${fLng})">💾 Simpan</button>`);
}
async function savePM(id, lat, lng) {
const body = {
nama: document.getElementById('pm-nama').value.trim(),
no_wa: document.getElementById('pm-nowa').value.trim(),
nik: document.getElementById('pm-nik').value.trim(),
alamat: document.getElementById('pm-alamat').value.trim(),
jumlah_anggota: parseInt(document.getElementById('pm-jml').value) || 1,
latitude: lat,
longitude: lng
};
if (!body.nama) return toast('Nama wajib diisi', 'error');
const res = await callAPI(id ? `penduduk_miskin.php?id=${id}` : 'penduduk_miskin.php', id ? 'PUT' : 'POST', body);
if (res.status === 'success') {
toast(res.message, 'success');
closeModal();
switchTab('kemiskinan');
} else {
toast(res.message, 'error');
}
}
async function editRI(id) {
map.closePopup();
const res = await callAPI(`rumah_ibadah.php?id=${id}`);
if (res.data) openRIForm(res.data, res.data.latitude, res.data.longitude);
}
async function deleteRI(id) {
if (!confirm('Hapus rumah ibadah ini?')) return;
const res = await callAPI(`rumah_ibadah.php?id=${id}`, 'DELETE');
if (res.status === 'success') {
toast('Dihapus', 'success');
map.closePopup();
await recalcAllProximity();
switchTab('kemiskinan');
}
}
async function editPM(id) {
map.closePopup();
const res = await callAPI(`penduduk_miskin.php?id=${id}`);
if (res.data) openPMForm(res.data, res.data.latitude, res.data.longitude);
}
async function deletePM(id) {
if (!confirm('Hapus data ini?')) return;
const res = await callAPI(`penduduk_miskin.php?id=${id}`, 'DELETE');
if (res.status === 'success') {
toast('Dihapus', 'success');
map.closePopup();
await recalcAllProximity();
switchTab('kemiskinan');
}
}
async function openLogBantuan(pmId, pmNama) {
map.closePopup();
const [logRes, riRes] = await Promise.all([
callAPI(`log_bantuan.php?penduduk_miskin_id=${pmId}`),
callAPI('rumah_ibadah.php')
]);
const logs = logRes.data || [];
const ris = riRes.data || [];
openModal(`Log Bantuan — ${pmNama}`, `
<div style="margin-bottom:16px">
<div class="layer-section-title">Riwayat Bantuan</div>
${logs.length ? logs.map(l => `
<div class="log-item ${l.tipe_bantuan === 'Pemberdayaan' ? 'pemberdayaan' : 'konsumtif'}">
<div class="log-type">${l.tipe_bantuan === 'Pemberdayaan' ? '📚 Pemberdayaan' : '🛍 Konsumtif'}</div>
<div class="log-detail">${esc(l.sub_kategori) || ''} ${l.keterangan ? '— ' + esc(l.keterangan) : ''}</div>
<div class="log-date">📅 ${l.tanggal_bantuan} · ${esc(l.nama_rumah_ibadah)}</div>
</div>`).join('') : '<div class="loading">Belum ada riwayat bantuan</div>'}
</div>
<div class="layer-section-title">Tambah Bantuan Baru</div>
<div class="form-group">
<label class="form-label">Dari Rumah Ibadah *</label>
<select class="form-select" id="lb-ri">
${ris.map(r => `<option value="${r.id}">${esc(r.nama)} (${r.jenis})</option>`).join('')}
</select>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Tipe Bantuan *</label>
<select class="form-select" id="lb-tipe">
<option value="Pemberdayaan">📚 Pemberdayaan (Pelatihan)</option>
<option value="Konsumtif">🛍 Konsumtif (Makan/Barang)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Tanggal *</label>
<input class="form-input" id="lb-tgl" type="date" value="${new Date().toISOString().slice(0, 10)}"/>
</div>
</div>
<div class="form-group">
<label class="form-label">Sub Kategori</label>
<input class="form-input" id="lb-sub" placeholder="Pelatihan menjahit / Sembako…"/>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Keterangan</label>
<input class="form-input" id="lb-ket" placeholder="Opsional"/>
</div>
<div class="form-group">
<label class="form-label">Nilai (Rp)</label>
<input class="form-input" id="lb-nilai" type="number" placeholder="0"/>
</div>
</div>`,
`<button class="btn btn-ghost" onclick="closeModal()">Tutup</button>
<button class="btn btn-primary" onclick="saveLog(${pmId})"> Tambah Log</button>`);
}
async function saveLog(pmId) {
const body = {
penduduk_miskin_id: pmId,
rumah_ibadah_id: parseInt(document.getElementById('lb-ri').value),
tipe_bantuan: document.getElementById('lb-tipe').value,
tanggal_bantuan: document.getElementById('lb-tgl').value,
sub_kategori: document.getElementById('lb-sub').value,
keterangan: document.getElementById('lb-ket').value,
nilai_bantuan: document.getElementById('lb-nilai').value || null
};
const res = await callAPI('log_bantuan.php', 'POST', body);
if (res.status === 'success') {
toast('Log dicatat!', 'success');
closeModal();
switchTab('kemiskinan');
} else {
toast(res.message, 'error');
}
}
async function recalcAllProximity() {
await callAPI('penduduk_miskin.php?action=recalc_all', 'POST');
}