mengubah sistem menjadi lebih terfokus ke poverty
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
function haversineDistance(lat1, lng1, lat2, lng2) {
|
||||
const R = 6371000;
|
||||
const toRad = d => d * Math.PI / 180;
|
||||
const dLat = toRad(lat2 - lat1);
|
||||
const dLng = toRad(lng2 - lng1);
|
||||
const a = Math.sin(dLat/2)**2 +
|
||||
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng/2)**2;
|
||||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
}
|
||||
|
||||
async function openLogBantuan(pmId, pmNama, pmLat, pmLng) {
|
||||
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 || [];
|
||||
|
||||
// --- Determine best default RI ---
|
||||
let defaultRiId = ris.length > 0 ? ris[0].id : null;
|
||||
|
||||
if (pmLat != null && pmLng != null && ris.length > 0) {
|
||||
// Find the RI whose circle the resident falls inside (if any), prefer that
|
||||
const insideRis = ris.filter(r => {
|
||||
const dist = haversineDistance(pmLat, pmLng, r.latitude, r.longitude);
|
||||
return dist <= (r.radius_meter || 500);
|
||||
});
|
||||
|
||||
if (insideRis.length > 0) {
|
||||
// Among RIs covering this resident, pick the closest center
|
||||
insideRis.sort((a, b) =>
|
||||
haversineDistance(pmLat, pmLng, a.latitude, a.longitude) -
|
||||
haversineDistance(pmLat, pmLng, b.latitude, b.longitude)
|
||||
);
|
||||
defaultRiId = insideRis[0].id;
|
||||
} else {
|
||||
// Not within any radius — pick nearest RI geometrically
|
||||
const sorted = [...ris].sort((a, b) =>
|
||||
haversineDistance(pmLat, pmLng, a.latitude, a.longitude) -
|
||||
haversineDistance(pmLat, pmLng, b.latitude, b.longitude)
|
||||
);
|
||||
defaultRiId = sorted[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
// Build RI options with distance label (admin only) or locked (petugas)
|
||||
let riSelectHTML;
|
||||
if (window.currentUser && window.currentUser.role === 'petugas_lapangan') {
|
||||
// Petugas can only see their own RI
|
||||
riSelectHTML = `<select class="form-select" id="lb-ri" disabled>
|
||||
${ris.filter(r => String(r.id) === String(window.currentUser.rumah_ibadah_id))
|
||||
.map(r => `<option value="${r.id}" selected>${esc(r.nama)} (${r.jenis})</option>`).join('')}
|
||||
</select>`;
|
||||
} else {
|
||||
// Admin — show all, sorted by distance, with distance label if coords available
|
||||
const sorted = (pmLat != null && pmLng != null)
|
||||
? [...ris].sort((a, b) =>
|
||||
haversineDistance(pmLat, pmLng, a.latitude, a.longitude) -
|
||||
haversineDistance(pmLat, pmLng, b.latitude, b.longitude))
|
||||
: ris;
|
||||
|
||||
riSelectHTML = `<select class="form-select" id="lb-ri">
|
||||
${sorted.map(r => {
|
||||
const dist = (pmLat != null && pmLng != null)
|
||||
? Math.round(haversineDistance(pmLat, pmLng, r.latitude, r.longitude))
|
||||
: null;
|
||||
const distLabel = dist != null ? ` — ${dist < 1000 ? dist + ' m' : (dist/1000).toFixed(1) + ' km'}` : '';
|
||||
const isInside = dist != null && dist <= (r.radius_meter || 500);
|
||||
const label = `${esc(r.nama)} (${r.jenis})${distLabel}${isInside ? ' ✅' : ''}`;
|
||||
const selected = String(r.id) === String(defaultRiId) ? 'selected' : '';
|
||||
return `<option value="${r.id}" ${selected}>${label}</option>`;
|
||||
}).join('')}
|
||||
</select>
|
||||
${pmLat != null ? `<div style="font-size:11px;color:#64748b;margin-top:4px">✅ = dalam radius · Diurutkan berdasarkan jarak terdekat</div>` : ''}`;
|
||||
}
|
||||
|
||||
openModal(`Log Bantuan — ${pmNama}`, `
|
||||
<div style="margin-bottom:16px">
|
||||
<div class="layer-section-title">Riwayat Bantuan</div>
|
||||
${logs.length ? `
|
||||
${logs.slice(0, 2).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('')}
|
||||
${logs.length > 2 ? `<div style="font-size:11px;color:#64748b;text-align:center;padding:6px 0;border-top:1px solid #f1f5f9;margin-top:4px">+${logs.length - 2} bantuan lainnya · Lihat detail lengkap di halaman Daftar Penduduk Miskin</div>` : ''}
|
||||
` : '<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>
|
||||
${riSelectHTML}
|
||||
</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('penduduk_miskin');
|
||||
} else {
|
||||
toast(res.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function recalcAllProximity() {
|
||||
await callAPI('penduduk_miskin.php?action=recalc_all', 'POST');
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
const accountFieldsHTML = isAdmin ? `
|
||||
<div style="margin-top:16px; border-top: 1px solid var(--line); padding-top:14px;">
|
||||
<div class="layer-section-title">Akun Rumah Ibadah</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username Rumah Ibadah</label>
|
||||
<input class="form-input" id="ri-username" value="${esc(isEdit ? data.username || '' : '')}" placeholder="username_rumah_ibadah" autocomplete="off" readonly style="background-color:#f1f5f9;cursor:not-allowed;color:#64748b;"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password Rumah Ibadah ${isEdit ? '(kosongkan jika tidak diubah)' : '*'}</label>
|
||||
<input class="form-input" id="ri-password" type="password" placeholder="••••••••" autocomplete="new-password"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
` : '';
|
||||
|
||||
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="${isEdit ? esc(data.alamat || '') : 'Memuat alamat otomatis...'}" 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>
|
||||
${accountFieldsHTML}`,
|
||||
`<button class="btn btn-ghost" onclick="closeModal()">Batal</button>
|
||||
<button class="btn btn-primary" onclick="saveRI(${isEdit ? data.id : 'null'},${fLat},${fLng})">💾 Simpan</button>`);
|
||||
|
||||
// Attach dynamic username locking listener
|
||||
if (isAdmin) {
|
||||
const namaEl = document.getElementById('ri-nama');
|
||||
const usernameEl = document.getElementById('ri-username');
|
||||
if (namaEl && usernameEl) {
|
||||
namaEl.addEventListener('input', () => {
|
||||
usernameEl.value = namaEl.value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_+|_+$/g, '');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Geocode address asynchronously
|
||||
if (!isEdit) {
|
||||
getAddressFromCoords(fLat, fLng).then(addr => {
|
||||
const el = document.getElementById('ri-alamat');
|
||||
if (el && el.value === 'Memuat alamat otomatis...') {
|
||||
el.value = addr || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRI(id, lat, lng) {
|
||||
const usernameEl = document.getElementById('ri-username');
|
||||
const passwordEl = document.getElementById('ri-password');
|
||||
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),
|
||||
username: usernameEl ? usernameEl.value.trim() : '',
|
||||
password: passwordEl ? passwordEl.value : ''
|
||||
};
|
||||
if (!body.nama) return toast('Nama wajib diisi', 'error');
|
||||
|
||||
// Validate account password for new Rumah Ibadah
|
||||
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
if (isAdmin && !id) {
|
||||
if (!body.username) return toast('Username Rumah Ibadah wajib terisi', 'error');
|
||||
if (!body.password) return toast('Password Rumah Ibadah 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('rumah_ibadah');
|
||||
} 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="${isEdit ? esc(data.alamat || '') : 'Memuat alamat otomatis...'}" 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>`);
|
||||
|
||||
// Geocode address asynchronously
|
||||
if (!isEdit) {
|
||||
getAddressFromCoords(fLat, fLng).then(addr => {
|
||||
const el = document.getElementById('pm-alamat');
|
||||
if (el && el.value === 'Memuat alamat otomatis...') {
|
||||
el.value = addr || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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('penduduk_miskin');
|
||||
} 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('rumah_ibadah');
|
||||
}
|
||||
}
|
||||
|
||||
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('penduduk_miskin');
|
||||
}
|
||||
}
|
||||
|
||||
// Haversine formula — returns distance in meters between two lat/lng points
|
||||
@@ -0,0 +1,138 @@
|
||||
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: (window.currentUser && window.currentUser.role === 'admin')
|
||||
}).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();
|
||||
switchTab(currentTab);
|
||||
} else {
|
||||
toast('Gagal update lokasi: ' + res.message, 'error');
|
||||
marker.setLatLng([r.latitude, r.longitude]);
|
||||
}
|
||||
});
|
||||
|
||||
layers.rumahIbadah.addLayer(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function riPopup(r) {
|
||||
const canEdit = window.currentUser && (window.currentUser.role === 'admin' || (window.currentUser.role === 'petugas_lapangan' && String(window.currentUser.rumah_ibadah_id) === String(r.id)));
|
||||
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
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-actions">
|
||||
${canEdit ? `<button class="btn btn-ghost btn-sm" onclick="editRI(${r.id})">✏ Edit</button>` : ''}
|
||||
${isAdmin ? `<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 isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
const isPetugas = window.currentUser && window.currentUser.role === 'petugas_lapangan';
|
||||
const isResidentInRadiusOfPetugas = isPetugas && inR && String(p.rumah_ibadah_id) === String(window.currentUser.rumah_ibadah_id);
|
||||
const canDrag = isAdmin || isResidentInRadiusOfPetugas;
|
||||
|
||||
const marker = L.marker([p.latitude, p.longitude], {
|
||||
icon,
|
||||
draggable: canDrag
|
||||
}).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('penduduk_miskin');
|
||||
} 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';
|
||||
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
const isPetugas = window.currentUser && window.currentUser.role === 'petugas_lapangan';
|
||||
const isResidentInRadiusOfPetugas = isPetugas && inR && String(p.rumah_ibadah_id) === String(window.currentUser.rumah_ibadah_id);
|
||||
const canEditOrLog = isAdmin || isResidentInRadiusOfPetugas;
|
||||
|
||||
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-actions">
|
||||
${canEditOrLog ? `<button class="btn btn-primary btn-sm" onclick="openLogBantuan(${p.id},'${esc(p.nama)}',${p.latitude},${p.longitude})">📋 Log Bantuan</button>` : ''}
|
||||
${canEditOrLog ? `<button class="btn btn-ghost btn-sm" onclick="editPM(${p.id})">✏ Edit</button>` : ''}
|
||||
${isAdmin ? `<button class="btn btn-danger btn-sm" onclick="deletePM(${p.id})">✕</button>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
function renderRumahIbadahPanel(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 admin-only" id="btn-add-ri" onclick="startAdd('ri')">+ Tambah Rumah Ibadah</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">
|
||||
${(window.currentUser && (window.currentUser.role === 'admin' || (window.currentUser.role === 'petugas_lapangan' && String(window.currentUser.rumah_ibadah_id) === String(r.id)))) ? `<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();editRI(${r.id})">✏</button>` : ''}
|
||||
${(window.currentUser && window.currentUser.role === 'admin') ? `<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>`;
|
||||
|
||||
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 renderPendudukMiskinPanel(pb) {
|
||||
return Promise.all([callAPI('rumah_ibadah.php'), callAPI('penduduk_miskin.php')]).then(([riRes, pmRes]) => {
|
||||
const ri = riRes.data || [];
|
||||
const pm = pmRes.data || [];
|
||||
|
||||
// Filter Penduduk Miskin for petugas_lapangan to only show those in their radius
|
||||
let filteredPm = pm;
|
||||
if (window.currentUser && window.currentUser.role === 'petugas_lapangan') {
|
||||
const userRiId = String(window.currentUser.rumah_ibadah_id);
|
||||
filteredPm = pm.filter(p => p.status_proximity === 'dalam_radius' && String(p.rumah_ibadah_id) === userRiId);
|
||||
}
|
||||
|
||||
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 write-only" id="btn-add-pm" onclick="startAdd('pm')">+ Tambah Penduduk Miskin</button>
|
||||
</div>
|
||||
<div class="layer-section">
|
||||
<div class="layer-section-title">Penduduk Miskin (${filteredPm.length})</div>
|
||||
<div class="data-list">
|
||||
${filteredPm.length ? filteredPm.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 style="display:flex; align-items:center; gap:4px">
|
||||
<div class="item-badge ${p.status_proximity === 'dalam_radius' ? 'badge-green' : 'badge-red'}" style="margin-right:4px">
|
||||
${p.status_proximity === 'dalam_radius' ? 'DALAM' : 'LUAR'}
|
||||
</div>
|
||||
${(window.currentUser && (window.currentUser.role === 'admin' || (window.currentUser.role === 'petugas_lapangan' && p.status_proximity === 'dalam_radius' && String(p.rumah_ibadah_id) === String(window.currentUser.rumah_ibadah_id)))) ? `
|
||||
<button class="btn btn-primary btn-sm" onclick="event.stopPropagation();openLogBantuan(${p.id},'${esc(p.nama)}',${p.latitude},${p.longitude})">📋</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="event.stopPropagation();editPM(${p.id})">✏</button>
|
||||
` : ''}
|
||||
${(window.currentUser && window.currentUser.role === 'admin') ? `<button class="btn btn-danger btn-sm" onclick="event.stopPropagation();deletePM(${p.id})">✕</button>` : ''}
|
||||
</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 '👤';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user