917 lines
37 KiB
JavaScript
917 lines
37 KiB
JavaScript
// --- Fitur Pemetaan Kemiskinan ---
|
|
|
|
// Emoji per jenis ibadah
|
|
const IBADAH_EMOJI_MAP = {
|
|
'Masjid': '🕌',
|
|
'Gereja': '⛪',
|
|
'Pura': '🛕',
|
|
'Vihara': '🪷',
|
|
'Kelenteng': '🏮',
|
|
};
|
|
|
|
// Emoji Bubble Icon builder
|
|
function makeIbadahIcon(jenis) {
|
|
const emot = IBADAH_EMOJI_MAP[jenis] || '🕌';
|
|
return L.divIcon({
|
|
className: '',
|
|
html: `<div class="emoji-marker"><div class="bubble ibadah"><span>${emot}</span></div></div>`,
|
|
iconSize: [38, 38],
|
|
iconAnchor: [19, 38],
|
|
popupAnchor: [0, -40]
|
|
});
|
|
}
|
|
|
|
function makeMiskinIcon(inRadius) {
|
|
const cls = inRadius ? 'miskin-in' : 'miskin-out';
|
|
return L.divIcon({
|
|
className: '',
|
|
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>🏠</span></div></div>`,
|
|
iconSize: [38, 38],
|
|
iconAnchor: [19, 38],
|
|
popupAnchor: [0, -40]
|
|
});
|
|
}
|
|
|
|
|
|
let ibadahDataList = [];
|
|
let miskinMarkerList = []; // Simpan referensi marker untuk update warna
|
|
|
|
// Helper: cek apakah koordinat (lat, lng) berada dalam radius ibadah pengelola yang login
|
|
function isInMyIbadahRadius(lat, lng) {
|
|
if (!window.currentUser || window.currentUser.role !== 'pengelola') return true; // admin selalu bisa
|
|
const myIbadahId = window.currentUser.ibadah_id;
|
|
if (!myIbadahId) return false;
|
|
const ib = ibadahDataList.find(i => i.id == myIbadahId);
|
|
if (!ib) return false;
|
|
const dist = L.latLng(lat, lng).distanceTo(L.latLng(ib.lat, ib.lng));
|
|
return dist <= ib.radius;
|
|
}
|
|
window.isInMyIbadahRadius = isInMyIbadahRadius;
|
|
|
|
let isResizing = false;
|
|
let resizingCircle = null;
|
|
let resizingIbadah = null;
|
|
|
|
map.on('mousemove', function(e) {
|
|
if (isResizing && resizingCircle && resizingIbadah) {
|
|
const center = resizingCircle.getLatLng();
|
|
const newRadius = center.distanceTo(e.latlng);
|
|
resizingCircle.setRadius(newRadius);
|
|
resizingIbadah.radius = newRadius;
|
|
|
|
// Update data radius di marker juga
|
|
rumahIbadahLayer.eachLayer(function(layer) {
|
|
if (layer.ibadahData && layer.ibadahData.id === resizingIbadah.id && layer instanceof L.Marker) {
|
|
layer.ibadahData.radius = newRadius;
|
|
}
|
|
});
|
|
|
|
updateSemuaWarnaMiskin();
|
|
}
|
|
});
|
|
|
|
map.on('mouseup', function(e) {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
map.dragging.enable();
|
|
if (resizingIbadah) {
|
|
updateIbadah(resizingIbadah.id, resizingIbadah.nama, resizingIbadah.jenis, resizingIbadah.alamat, resizingIbadah.radius, resizingIbadah.lat, resizingIbadah.lng);
|
|
}
|
|
resizingCircle = null;
|
|
resizingIbadah = null;
|
|
}
|
|
});
|
|
|
|
// --- Rumah Ibadah ---
|
|
function loadRumahIbadah() {
|
|
rumahIbadahLayer.clearLayers();
|
|
ibadahDataList = [];
|
|
fetch('api/rumah_ibadah/read.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.data) {
|
|
ibadahDataList = data.data;
|
|
data.data.forEach(item => {
|
|
addIbadahMarker(item);
|
|
});
|
|
updateSemuaWarnaMiskin();
|
|
}
|
|
if (window.refreshActivePanel) window.refreshActivePanel();
|
|
});
|
|
}
|
|
|
|
function addIbadahMarker(item) {
|
|
const lat = parseFloat(item.lat);
|
|
const lng = parseFloat(item.lng);
|
|
const radius = parseFloat(item.radius) || 100; // fallback if invalid
|
|
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
|
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
|
|
|
const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: isAdmin });
|
|
|
|
// Lingkaran radius
|
|
const circle = L.circle([lat, lng], {
|
|
radius: radius,
|
|
color: '#ff7800',
|
|
weight: 2,
|
|
fillColor: '#ff7800',
|
|
fillOpacity: 0.2
|
|
});
|
|
|
|
circle.on('mousemove', function(e) {
|
|
if (!isAdmin || isResizing) return;
|
|
const center = circle.getLatLng();
|
|
const radius = circle.getRadius();
|
|
const dist = center.distanceTo(e.latlng);
|
|
|
|
const metersPerPixel = map.distance(map.containerPointToLatLng([0,0]), map.containerPointToLatLng([0,1]));
|
|
const tolerance = Math.max(radius * 0.1, metersPerPixel * 15); // toleransi 15px atau 10%
|
|
|
|
if (Math.abs(dist - radius) <= tolerance) {
|
|
if (circle._path) circle._path.style.cursor = 'ew-resize';
|
|
circle.nearEdge = true;
|
|
} else {
|
|
if (circle._path) circle._path.style.cursor = 'pointer';
|
|
circle.nearEdge = false;
|
|
}
|
|
});
|
|
|
|
circle.on('mousedown', function(e) {
|
|
if (isAdmin && circle.nearEdge) {
|
|
map.dragging.disable();
|
|
isResizing = true;
|
|
resizingCircle = circle;
|
|
resizingIbadah = item;
|
|
L.DomEvent.stopPropagation(e);
|
|
}
|
|
});
|
|
|
|
marker.ibadahData = item;
|
|
marker.circleLayer = circle;
|
|
|
|
const d = item;
|
|
const emot = IBADAH_EMOJI_MAP[d.jenis] || '🕌';
|
|
let actionButtons = '';
|
|
if (isAdmin) {
|
|
actionButtons = `
|
|
<div style="display:flex; gap:5px;">
|
|
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditIbadahModal(${d.id})">Edit</button>
|
|
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteIbadah(${d.id})">Hapus</button>
|
|
</div>
|
|
`;
|
|
}
|
|
const popupContent = `
|
|
<div style="font-family: Arial, sans-serif; min-width: 160px;">
|
|
<h4 style="margin:0 0 5px 0;">${emot} ${d.nama}</h4>
|
|
<p style="margin: 0 0 3px 0;"><b>Jenis:</b> ${d.jenis || 'Masjid'}</p>
|
|
<p style="margin: 0 0 3px 0;"><b>Alamat:</b> ${d.alamat}</p>
|
|
<p style="margin: 0 0 10px 0;"><b>Radius:</b> ${d.radius} m</p>
|
|
${actionButtons}
|
|
</div>
|
|
`;
|
|
marker.bindPopup(popupContent);
|
|
|
|
marker.on('dragend', function(e) {
|
|
const newPos = marker.getLatLng();
|
|
circle.setLatLng(newPos);
|
|
item.lat = newPos.lat;
|
|
item.lng = newPos.lng;
|
|
// Update ke DB
|
|
updateIbadah(item.id, item.nama, item.jenis, item.alamat, item.radius, newPos.lat, newPos.lng);
|
|
});
|
|
|
|
rumahIbadahLayer.addLayer(circle);
|
|
rumahIbadahLayer.addLayer(marker);
|
|
}
|
|
|
|
// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta)
|
|
map.on('contextmenu', function(e) {
|
|
|
|
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
|
if (!isAdminOrPengelola) return;
|
|
|
|
const canAddHere = isInMyIbadahRadius(e.latlng.lat, e.latlng.lng);
|
|
|
|
if (!canAddHere) {
|
|
// Pengelola: lokasi di luar radius ibadahnya
|
|
const warnContent = `
|
|
<div class="popup-form">
|
|
<h4>⚠️ Di Luar Radius</h4>
|
|
<p style="font-size:12px; color:#e11d48; margin:4px 0 0;">Anda hanya dapat menambah penduduk miskin di dalam radius rumah ibadah yang Anda kelola.</p>
|
|
</div>
|
|
`;
|
|
L.popup().setLatLng(e.latlng).setContent(warnContent).openOn(map);
|
|
return;
|
|
}
|
|
|
|
const popupContent = `
|
|
<div class="popup-form">
|
|
<h4>Tambah Titik</h4>
|
|
<button class="btn-danger" style="margin-top:5px;" onclick="formAddMiskin(${e.latlng.lat}, ${e.latlng.lng})">Tambah Penduduk Miskin</button>
|
|
</div>
|
|
`;
|
|
L.popup().setLatLng(e.latlng).setContent(popupContent).openOn(map);
|
|
});
|
|
|
|
// Map Click -> Form Add Rumah Ibadah
|
|
map.on('click', function(e) {
|
|
if (window.currentAddMode === 'rumah_ibadah') {
|
|
const lat = e.latlng.lat;
|
|
const lng = e.latlng.lng;
|
|
|
|
// Reverse Geocoding
|
|
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
|
|
.then(res => res.json())
|
|
.then(geoData => {
|
|
const alamat = geoData.display_name || 'Alamat tidak ditemukan';
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Nama Rumah Ibadah</label>
|
|
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Jenis</label>
|
|
<select id="modalIbadahJenis">
|
|
<option value="Masjid">🕌 Masjid</option>
|
|
<option value="Gereja">⛪ Gereja</option>
|
|
<option value="Pura">🛕 Pura</option>
|
|
<option value="Vihara">🪷 Vihara</option>
|
|
<option value="Kelenteng">🏮 Kelenteng</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Alamat (Auto)</label>
|
|
<textarea id="modalIbadahAlamat" rows="3">${alamat}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Radius (m)</label>
|
|
<input type="number" id="modalIbadahRadius" value="500">
|
|
</div>
|
|
`;
|
|
|
|
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
|
|
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
|
|
});
|
|
|
|
window.deactivateAddMode();
|
|
})
|
|
.catch(err => {
|
|
// Tetap buka modal meskipun gagal mendapatkan alamat
|
|
const alamat = '';
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Nama Rumah Ibadah</label>
|
|
<input type="text" id="modalIbadahNama" placeholder="Nama Rumah Ibadah">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Jenis</label>
|
|
<select id="modalIbadahJenis">
|
|
<option value="Masjid">🕌 Masjid</option>
|
|
<option value="Gereja">⛪ Gereja</option>
|
|
<option value="Pura">🛕 Pura</option>
|
|
<option value="Vihara">🪷 Vihara</option>
|
|
<option value="Kelenteng">🏮 Kelenteng</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Alamat (Manual)</label>
|
|
<textarea id="modalIbadahAlamat" rows="3" placeholder="Masukkan alamat manual">${alamat}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Radius (m)</label>
|
|
<input type="number" id="modalIbadahRadius" value="500">
|
|
</div>
|
|
`;
|
|
|
|
openModal("Tambah Rumah Ibadah", bodyHTML, function() {
|
|
window.saveNewIbadah(lat, lng, 'modalIbadahNama', 'modalIbadahJenis', 'modalIbadahAlamat', 'modalIbadahRadius');
|
|
});
|
|
|
|
window.deactivateAddMode();
|
|
});
|
|
}
|
|
});
|
|
|
|
window.saveNewIbadah = function(lat, lng, namaId, jenisId, alamatId, radiusId) {
|
|
const nama = document.getElementById(namaId).value;
|
|
const jenis = document.getElementById(jenisId).value;
|
|
const alamat = document.getElementById(alamatId).value;
|
|
const radius = document.getElementById(radiusId).value;
|
|
|
|
if (!nama) { alert("Nama Rumah Ibadah harus diisi!"); return; }
|
|
|
|
fetch('api/rumah_ibadah/create.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ nama, jenis, alamat, radius, lat, lng })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if(data.status === 'success') { closeModal(); loadRumahIbadah(); }
|
|
else { alert(data.message); }
|
|
});
|
|
};
|
|
|
|
window.openEditIbadahModal = function(id) {
|
|
let d = null;
|
|
rumahIbadahLayer.eachLayer(function(layer) {
|
|
if (layer.ibadahData && layer.ibadahData.id == id) d = layer.ibadahData;
|
|
});
|
|
if (!d) return;
|
|
|
|
const jenisOpts = ['Masjid','Gereja','Pura','Vihara','Kelenteng'].map(j =>
|
|
`<option value="${j}" ${d.jenis === j ? 'selected' : ''}>${IBADAH_EMOJI_MAP[j] || ''} ${j}</option>`
|
|
).join('');
|
|
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Nama Rumah Ibadah</label>
|
|
<input type="text" id="editIbadahNama" value="${d.nama}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Jenis</label>
|
|
<select id="editIbadahJenis">${jenisOpts}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Alamat</label>
|
|
<textarea id="editIbadahAlamat" rows="2">${d.alamat}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Radius (m)</label>
|
|
<input type="number" id="editIbadahRadius" value="${d.radius}" oninput="previewRadius(${d.id}, this.value)">
|
|
</div>
|
|
`;
|
|
map.closePopup();
|
|
openModal("Edit Rumah Ibadah", bodyHTML, function() {
|
|
window.saveEditIbadah(d.id, d.lat, d.lng, 'editIbadahNama', 'editIbadahJenis', 'editIbadahAlamat', 'editIbadahRadius');
|
|
});
|
|
};
|
|
|
|
window.previewRadius = function(id, newRadius) {
|
|
rumahIbadahLayer.eachLayer(function(layer) {
|
|
// layer ini bisa marker atau circle, kita cek yang punya circleLayer (marker)
|
|
if (layer.ibadahData && layer.ibadahData.id === id && layer.circleLayer) {
|
|
layer.circleLayer.setRadius(parseFloat(newRadius));
|
|
layer.ibadahData.radius = parseFloat(newRadius);
|
|
updateSemuaWarnaMiskin(); // Update warna miskin real-time
|
|
}
|
|
});
|
|
};
|
|
|
|
window.saveEditIbadah = function(id, lat, lng, namaId, jenisId, alamatId, radiusId) {
|
|
const nama = document.getElementById(namaId).value;
|
|
const jenis = document.getElementById(jenisId).value;
|
|
const alamat = document.getElementById(alamatId).value;
|
|
const radius = document.getElementById(radiusId).value;
|
|
updateIbadah(id, nama, jenis, alamat, radius, lat, lng);
|
|
};
|
|
|
|
function updateIbadah(id, nama, jenis, alamat, radius, lat, lng) {
|
|
fetch('api/rumah_ibadah/update.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id, nama, jenis, alamat, radius, lat, lng })
|
|
}).then(res => res.json()).then(data => {
|
|
if(data.status === 'success') {
|
|
if (typeof closeModal === 'function') closeModal();
|
|
map.closePopup();
|
|
loadRumahIbadah();
|
|
}
|
|
});
|
|
}
|
|
|
|
window.deleteIbadah = function(id) {
|
|
openConfirmModal("Yakin hapus rumah ibadah ini?", function() {
|
|
fetch('api/rumah_ibadah/delete.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
}).then(res => res.json()).then(data => {
|
|
if(data.status === 'success') { map.closePopup(); loadRumahIbadah(); }
|
|
});
|
|
});
|
|
};
|
|
|
|
|
|
// --- Penduduk Miskin ---
|
|
function loadPendudukMiskin() {
|
|
pendudukMiskinLayer.clearLayers();
|
|
miskinMarkerList = [];
|
|
fetch('api/penduduk_miskin/read.php')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'success' && data.data) {
|
|
data.data.forEach(item => {
|
|
addMiskinMarker(item);
|
|
});
|
|
updateSemuaWarnaMiskin();
|
|
}
|
|
if (window.refreshActivePanel) window.refreshActivePanel();
|
|
});
|
|
}
|
|
|
|
function addMiskinMarker(item) {
|
|
const lat = parseFloat(item.lat);
|
|
const lng = parseFloat(item.lng);
|
|
if (isNaN(lat) || isNaN(lng)) return;
|
|
|
|
// Pengelola hanya bisa manage marker yang ada di dalam radius ibadahnya
|
|
const isAdminOrPengelola = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
|
const canManage = isAdminOrPengelola && isInMyIbadahRadius(lat, lng);
|
|
|
|
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: canManage });
|
|
marker.miskinData = item;
|
|
|
|
const d = item;
|
|
let actionButtons = '';
|
|
if (canManage) {
|
|
actionButtons = `
|
|
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditMiskinModal(${d.id})">Edit</button>
|
|
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteMiskin(${d.id})">Hapus</button>
|
|
`;
|
|
}
|
|
|
|
let photoLinksHtml = '';
|
|
if (d.foto_rumah) {
|
|
photoLinksHtml += `
|
|
<div style="margin: 5px 0;">
|
|
<b style="font-size:11px;">Foto Rumah:</b><br>
|
|
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
|
|
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah"
|
|
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
|
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
|
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
|
|
</a>
|
|
</div>`;
|
|
}
|
|
if (d.foto_kk) {
|
|
photoLinksHtml += `
|
|
<div style="margin: 5px 0;">
|
|
<b style="font-size:11px;">Foto KK:</b><br>
|
|
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
|
|
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK"
|
|
style="max-width:150px; max-height:100px; border-radius:4px; margin-top:3px; border:1px solid #ddd; object-fit:cover; cursor:pointer;"
|
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
|
<span style="display:none; font-size:11px; color:#ef4444;">❌ Foto tidak dapat dimuat</span>
|
|
</a>
|
|
</div>`;
|
|
}
|
|
|
|
const popupContent = `
|
|
<div style="font-family: Arial, sans-serif; min-width: 165px;">
|
|
<h4 style="margin:0 0 5px 0;">🏠 ${d.nama}</h4>
|
|
<p style="margin: 0 0 3px 0;"><b>Bantuan:</b> ${d.kategori_bantuan}</p>
|
|
<p style="margin: 0 0 5px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
|
|
${photoLinksHtml}
|
|
<div style="display:flex; gap:5px; flex-wrap:wrap; margin-top:8px;">
|
|
<button style="padding:4px 8px; background:#6366f1; color:white; border:none; border-radius:3px; cursor:pointer; font-size:11px;" onclick="openLogBantuan(${d.id}, '${d.nama.replace(/'/g, "\\'")}');">📋 Log Bantuan</button>
|
|
${actionButtons}
|
|
</div>
|
|
</div>
|
|
`;
|
|
marker.bindPopup(popupContent);
|
|
|
|
marker.on('dragend', function(e) {
|
|
const newPos = marker.getLatLng();
|
|
item.lat = newPos.lat;
|
|
item.lng = newPos.lng;
|
|
updateSemuaWarnaMiskin(); // Update warna segera
|
|
// Simpan ke DB
|
|
fetch('api/penduduk_miskin/update.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(item)
|
|
});
|
|
});
|
|
|
|
miskinMarkerList.push(marker);
|
|
pendudukMiskinLayer.addLayer(marker);
|
|
}
|
|
|
|
window.formAddMiskin = function(lat, lng) {
|
|
map.closePopup();
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}</label>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Nama Kepala Keluarga</label>
|
|
<input type="text" id="miskinNama" placeholder="Nama Kepala Keluarga">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Kategori Bantuan</label>
|
|
<select id="miskinKategori">
|
|
<option value="Makan">Bantuan Makan</option>
|
|
<option value="Pemberdayaan">Pemberdayaan</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Jumlah Jiwa</label>
|
|
<input type="number" id="miskinJiwa" value="1" min="1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Foto Rumah</label>
|
|
<input type="file" id="miskinFotoRumah" accept="image/*">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Foto Kartu Keluarga (KK)</label>
|
|
<input type="file" id="miskinFotoKK" accept="image/*">
|
|
</div>
|
|
`;
|
|
openModal("Tambah Penduduk Miskin", bodyHTML, function() {
|
|
window.saveNewMiskin(lat, lng);
|
|
});
|
|
};
|
|
|
|
window.saveNewMiskin = function(lat, lng) {
|
|
const nama = document.getElementById('miskinNama').value;
|
|
const kategori_bantuan = document.getElementById('miskinKategori').value;
|
|
const jumlah_jiwa = document.getElementById('miskinJiwa').value;
|
|
const fotoRumahInput = document.getElementById('miskinFotoRumah');
|
|
const fotoKKInput = document.getElementById('miskinFotoKK');
|
|
|
|
if (!nama) { alert('Nama kepala keluarga harus diisi!'); return; }
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama', nama);
|
|
formData.append('kategori_bantuan', kategori_bantuan);
|
|
formData.append('jumlah_jiwa', jumlah_jiwa);
|
|
formData.append('lat', lat);
|
|
formData.append('lng', lng);
|
|
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
|
|
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
|
|
|
|
fetch('api/penduduk_miskin/create.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if(data.status === 'success') {
|
|
closeModal();
|
|
loadPendudukMiskin();
|
|
} else { alert(data.message); }
|
|
});
|
|
};
|
|
|
|
window.openEditMiskinModal = function(id) {
|
|
let d = null;
|
|
pendudukMiskinLayer.eachLayer(function(layer) {
|
|
if (layer.miskinData && layer.miskinData.id == id) d = layer.miskinData;
|
|
});
|
|
if (!d) return;
|
|
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Nama Kepala Keluarga</label>
|
|
<input type="text" id="editMiskinNama" value="${d.nama}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Kategori Bantuan</label>
|
|
<select id="editMiskinKategori">
|
|
<option value="Makan" ${d.kategori_bantuan === 'Makan' ? 'selected' : ''}>Bantuan Makan</option>
|
|
<option value="Pemberdayaan" ${d.kategori_bantuan === 'Pemberdayaan' ? 'selected' : ''}>Pemberdayaan</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Jumlah Jiwa</label>
|
|
<input type="number" id="editMiskinJiwa" value="${d.jumlah_jiwa || 1}" min="1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Foto Rumah</label>
|
|
<input type="file" id="editMiskinFotoRumah" accept="image/*">
|
|
${d.foto_rumah ? `
|
|
<div style="margin-top:5px;">
|
|
<a href="/poverty/uploads/${d.foto_rumah}" target="_blank">
|
|
<img src="/poverty/uploads/${d.foto_rumah}" alt="Foto Rumah" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
|
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
|
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
|
|
</a>
|
|
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
|
|
</div>` : ''}
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Foto Kartu Keluarga (KK)</label>
|
|
<input type="file" id="editMiskinFotoKK" accept="image/*">
|
|
${d.foto_kk ? `
|
|
<div style="margin-top:5px;">
|
|
<a href="/poverty/uploads/${d.foto_kk}" target="_blank">
|
|
<img src="/poverty/uploads/${d.foto_kk}" alt="Foto KK" style="max-width:120px; max-height:80px; border-radius:4px; border:1px solid #ddd; object-fit:cover;"
|
|
onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';"/>
|
|
<span style="display:none; font-size:11px; color:#ef4444;">❌ File gambar rusak atau tidak ditemukan</span>
|
|
</a>
|
|
<div style="font-size:11px;color:#888;margin-top:2px;">Upload baru untuk mengganti</div>
|
|
</div>` : ''}
|
|
</div>
|
|
`;
|
|
map.closePopup();
|
|
openModal("Edit Penduduk Miskin", bodyHTML, function() {
|
|
window.saveEditMiskin(d.id, d.lat, d.lng, 'editMiskinNama', 'editMiskinKategori', 'editMiskinJiwa');
|
|
});
|
|
};
|
|
|
|
window.saveEditMiskin = function(id, lat, lng, namaId, kategoriId, jiwaId) {
|
|
const nama = document.getElementById(namaId).value;
|
|
const kategori_bantuan = document.getElementById(kategoriId).value;
|
|
const jumlah_jiwa = jiwaId ? document.getElementById(jiwaId).value : 1;
|
|
const fotoRumahInput = document.getElementById('editMiskinFotoRumah');
|
|
const fotoKKInput = document.getElementById('editMiskinFotoKK');
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('nama', nama);
|
|
formData.append('kategori_bantuan', kategori_bantuan);
|
|
formData.append('jumlah_jiwa', jumlah_jiwa);
|
|
formData.append('lat', lat);
|
|
formData.append('lng', lng);
|
|
if (fotoRumahInput.files[0]) formData.append('foto_rumah', fotoRumahInput.files[0]);
|
|
if (fotoKKInput.files[0]) formData.append('foto_kk', fotoKKInput.files[0]);
|
|
|
|
fetch('api/penduduk_miskin/update.php', {
|
|
method: 'POST',
|
|
body: formData
|
|
}).then(res => res.json()).then(data => {
|
|
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
|
|
});
|
|
};
|
|
|
|
window.deleteMiskin = function(id) {
|
|
openConfirmModal("Yakin hapus penduduk miskin ini?", function() {
|
|
fetch('api/penduduk_miskin/delete.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ id })
|
|
}).then(res => res.json()).then(data => {
|
|
if(data.status === 'success') { map.closePopup(); loadPendudukMiskin(); }
|
|
});
|
|
});
|
|
};
|
|
|
|
// Logika dinamis warna: Merah jika di dalam salah satu radius Rumah Ibadah, Hijau jika di luar
|
|
function updateSemuaWarnaMiskin() {
|
|
miskinMarkerList.forEach(marker => {
|
|
let inRadius = false;
|
|
const pLatlng = L.latLng(marker.miskinData.lat, marker.miskinData.lng);
|
|
|
|
for (let i = 0; i < ibadahDataList.length; i++) {
|
|
const ibadah = ibadahDataList[i];
|
|
const iLatlng = L.latLng(ibadah.lat, ibadah.lng);
|
|
const dist = pLatlng.distanceTo(iLatlng); // meter
|
|
|
|
if (dist <= ibadah.radius) {
|
|
inRadius = true;
|
|
break; // Jika sudah masuk satu radius, langsung merah
|
|
}
|
|
}
|
|
|
|
if (inRadius) {
|
|
marker.setIcon(makeMiskinIcon(true));
|
|
} else {
|
|
marker.setIcon(makeMiskinIcon(false));
|
|
}
|
|
});
|
|
}
|
|
|
|
// ==========================
|
|
// ===== Import Feature =====
|
|
// ==========================
|
|
let parsedImportData = [];
|
|
|
|
window.openImportMiskinModal = function() {
|
|
parsedImportData = []; // Reset data
|
|
|
|
const bodyHTML = `
|
|
<div class="form-group">
|
|
<label>Pilih File (CSV, JSON, atau GeoJSON)</label>
|
|
<input type="file" id="importMiskinFile" accept=".csv,.json,.geojson" style="display:block; margin-bottom:10px; width:100%;">
|
|
<div style="font-size:12px; color:#666; margin-bottom:15px; line-height:1.4;">
|
|
<strong>Format CSV yang didukung:</strong><br>
|
|
Header minimal berisi: <code>nama</code>, <code>kategori_bantuan</code> (atau <code>kategori</code>), <code>jumlah_jiwa</code>, <code>lat</code>, <code>lng</code>.<br>
|
|
Pemisah: Koma (<code>,</code>), Titik Koma (<code>;</code>), atau Tab.
|
|
</div>
|
|
</div>
|
|
<div id="importPreviewContainer" style="display:none; margin-top: 15px;">
|
|
<h4 style="margin:0 0 8px 0; font-size: 13px; color: #333; display: flex; justify-content: space-between;">
|
|
<span>Pratinjau Data (<span id="importPreviewCount">0</span> item)</span>
|
|
<span id="importPreviewStatus" style="font-weight: normal; font-size: 11px;"></span>
|
|
</h4>
|
|
<div style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; background:#f8fafc;">
|
|
<table style="width:100%; border-collapse:collapse; font-size:11px; text-align:left;">
|
|
<thead>
|
|
<tr style="background:#f1f5f9; border-bottom:1px solid #cbd5e1; color:#475569; position: sticky; top: 0;">
|
|
<th style="padding:6px 8px;">Kepala Keluarga</th>
|
|
<th style="padding:6px 8px;">Kategori</th>
|
|
<th style="padding:6px 8px;">Jiwa</th>
|
|
<th style="padding:6px 8px;">Koordinat</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="importPreviewTableBody" style="color:#334155;"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div id="importErrorMsg" style="display:none; margin-top:10px; color:#ef4444; font-size:12px; font-weight: 500;"></div>
|
|
`;
|
|
|
|
map.closePopup();
|
|
|
|
openModal("Impor Penduduk Miskin", bodyHTML, function() {
|
|
if (parsedImportData.length === 0) {
|
|
showToast('Silakan pilih file terlebih dahulu atau pastikan data valid.', 'warning');
|
|
return;
|
|
}
|
|
|
|
// Kirim data ke API bulk_create.php
|
|
fetch('api/penduduk_miskin/bulk_create.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(parsedImportData)
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
closeModal();
|
|
loadPendudukMiskin();
|
|
showToast(data.message, 'success');
|
|
} else {
|
|
showToast(data.message || 'Gagal mengimpor data.', 'error');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
showToast('Terjadi kesalahan saat menghubungi server.', 'error');
|
|
});
|
|
});
|
|
|
|
// Register file input listener
|
|
document.getElementById('importMiskinFile').addEventListener('change', handleImportFileChange);
|
|
};
|
|
|
|
function parseCSV(text) {
|
|
const lines = text.split(/\r?\n/);
|
|
if (lines.length < 2) return [];
|
|
|
|
const firstLine = lines[0];
|
|
let delimiter = ',';
|
|
if (firstLine.includes(';')) delimiter = ';';
|
|
else if (firstLine.includes('\t')) delimiter = '\t';
|
|
|
|
const headers = firstLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, '').toLowerCase());
|
|
|
|
const results = [];
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (!line) continue;
|
|
|
|
let fields = [];
|
|
let currentField = '';
|
|
let insideQuote = false;
|
|
|
|
for (let j = 0; j < line.length; j++) {
|
|
const char = line[j];
|
|
if (char === '"' || char === "'") {
|
|
insideQuote = !insideQuote;
|
|
} else if (char === delimiter && !insideQuote) {
|
|
fields.push(currentField.trim());
|
|
currentField = '';
|
|
} else {
|
|
currentField += char;
|
|
}
|
|
}
|
|
fields.push(currentField.trim());
|
|
|
|
fields = fields.map(f => f.replace(/^["']|["']$/g, ''));
|
|
|
|
const row = {};
|
|
headers.forEach((header, index) => {
|
|
row[header] = fields[index] || '';
|
|
});
|
|
results.push(row);
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function handleImportFileChange(e) {
|
|
const file = e.target.files[0];
|
|
const previewContainer = document.getElementById('importPreviewContainer');
|
|
const previewCount = document.getElementById('importPreviewCount');
|
|
const previewTableBody = document.getElementById('importPreviewTableBody');
|
|
const errorMsg = document.getElementById('importErrorMsg');
|
|
|
|
if (!file) {
|
|
previewContainer.style.display = 'none';
|
|
parsedImportData = [];
|
|
return;
|
|
}
|
|
|
|
errorMsg.style.display = 'none';
|
|
previewContainer.style.display = 'none';
|
|
previewTableBody.innerHTML = '';
|
|
parsedImportData = [];
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
try {
|
|
const text = event.target.result;
|
|
let rawData = [];
|
|
|
|
if (file.name.endsWith('.csv')) {
|
|
rawData = parseCSV(text);
|
|
} else if (file.name.endsWith('.json') || file.name.endsWith('.geojson')) {
|
|
const json = JSON.parse(text);
|
|
if (json.type === 'FeatureCollection' && Array.isArray(json.features)) {
|
|
rawData = json.features.map(f => {
|
|
const props = f.properties || {};
|
|
const coords = f.geometry && f.geometry.coordinates;
|
|
return {
|
|
nama: props.nama || props.name || props.penduduk || '',
|
|
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
|
jumlah_jiwa: props.jumlah_jiwa || props.jumlah || props.jiwa || 1,
|
|
lat: coords ? coords[1] : (props.lat || props.latitude),
|
|
lng: coords ? coords[0] : (props.lng || props.longitude)
|
|
};
|
|
});
|
|
} else if (Array.isArray(json)) {
|
|
rawData = json;
|
|
} else {
|
|
throw new Error("Format JSON tidak didukung.");
|
|
}
|
|
}
|
|
|
|
if (rawData.length === 0) {
|
|
errorMsg.textContent = "File kosong atau format tidak didukung.";
|
|
errorMsg.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
const validRows = [];
|
|
let invalidCount = 0;
|
|
|
|
rawData.forEach(row => {
|
|
const nama = row.nama || row.name || row.penduduk || row['nama lengkap'] || row.fullname || '';
|
|
const kategori_bantuan = row.kategori_bantuan || row.kategori || row.bantuan || row['kategori bantuan'] || 'Makan';
|
|
const jumlah_jiwa = parseInt(row.jumlah_jiwa || row.jumlah || row.jiwa || row['jumlah jiwa'] || 1, 10) || 1;
|
|
const lat = parseFloat(row.lat || row.latitude || row.y);
|
|
const lng = parseFloat(row.lng || row.longitude || row.long || row.x);
|
|
|
|
const isValid = !isNaN(lat) && !isNaN(lng) && lat !== 0 && lng !== 0 && nama.trim() !== '';
|
|
|
|
if (isValid) {
|
|
validRows.push({ nama, kategori_bantuan, jumlah_jiwa, lat, lng });
|
|
} else {
|
|
invalidCount++;
|
|
}
|
|
});
|
|
|
|
parsedImportData = validRows;
|
|
|
|
if (validRows.length === 0) {
|
|
errorMsg.textContent = "Tidak ditemukan data valid (pastikan kolom nama_kk/nama, lat, dan lng terisi).";
|
|
errorMsg.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
validRows.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
tr.style.borderBottom = '1px solid #e2e8f0';
|
|
tr.innerHTML = `
|
|
<td style="padding:6px 8px; font-weight: 500;">${row.nama}</td>
|
|
<td style="padding:6px 8px;">${row.kategori_bantuan}</td>
|
|
<td style="padding:6px 8px;">${row.jumlah_jiwa}</td>
|
|
<td style="padding:6px 8px; color: #64748b; font-family: monospace;">${row.lat.toFixed(5)}, ${row.lng.toFixed(5)}</td>
|
|
`;
|
|
previewTableBody.appendChild(tr);
|
|
});
|
|
|
|
previewCount.textContent = validRows.length;
|
|
previewContainer.style.display = 'block';
|
|
|
|
const statusEl = document.getElementById('importPreviewStatus');
|
|
if (statusEl) {
|
|
if (invalidCount > 0) {
|
|
statusEl.innerHTML = `<span style="color:#e11d48;"><i class="fas fa-exclamation-triangle"></i> ${invalidCount} baris tidak valid diabaikan</span>`;
|
|
} else {
|
|
statusEl.innerHTML = `<span style="color:#16a34a;"><i class="fas fa-check-circle"></i> Semua data valid</span>`;
|
|
}
|
|
}
|
|
|
|
} catch (err) {
|
|
console.error(err);
|
|
errorMsg.textContent = "Gagal memproses file. Pastikan format file sesuai.";
|
|
errorMsg.style.display = 'block';
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// Initial Load
|
|
loadRumahIbadah();
|
|
loadPendudukMiskin();
|