feat: implement full authentication system and CRUD APIs for users, rumah ibadah, and penduduk miskin with database updates.

This commit is contained in:
Syariffullah
2026-06-11 08:10:42 +07:00
parent 5585c34c5c
commit b77d4f6869
48 changed files with 1439 additions and 1442 deletions
+369
View File
@@ -0,0 +1,369 @@
// --- Fitur Autentikasi dan Manajemen User (RBAC) ---
(function() {
window.currentUser = null;
// Elements
const authWidget = document.getElementById('authWidget');
const loginModal = document.getElementById('loginModal');
const loginUsernameInput = document.getElementById('loginUsername');
const loginPasswordInput = document.getElementById('loginPassword');
const loginSubmitBtn = document.getElementById('loginSubmitBtn');
const loginErrorMsg = document.getElementById('loginErrorMsg');
const closeLoginModal = document.getElementById('closeLoginModal');
// User Management Modal Elements
const userManagementModal = document.getElementById('userManagementModal');
const closeUserManagementModal = document.getElementById('closeUserManagementModal');
const menuUsersBtn = document.getElementById('menuUsers');
const userTableBody = document.getElementById('userTableBody');
const btnSaveUser = document.getElementById('btnSaveUser');
const btnCancelUserEdit = document.getElementById('btnCancelUserEdit');
const userFormTitle = document.getElementById('userFormTitle');
const manageUserId = document.getElementById('manageUserId');
const manageUsername = document.getElementById('manageUsername');
const managePassword = document.getElementById('managePassword');
const manageRole = document.getElementById('manageRole');
const manageIbadahId = document.getElementById('manageIbadahId');
const manageIbadahGroup = document.getElementById('manageIbadahGroup');
// Startup Session Check
function checkSession() {
fetch('api/check_session.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.isLoggedIn) {
window.currentUser = data.data;
} else {
window.currentUser = null;
}
updateAuthUI();
refreshAllLayers();
})
.catch(err => {
console.error("Session check failed:", err);
window.currentUser = null;
updateAuthUI();
});
}
// Update UI based on logged-in state
function updateAuthUI() {
if (!authWidget) return;
if (window.currentUser) {
// Logged In state
const roleLabel = window.currentUser.role === 'admin' ? 'Admin' : 'Pengelola';
const extraLabel = window.currentUser.nama_ibadah ? ` - ${window.currentUser.nama_ibadah}` : '';
authWidget.innerHTML = `
<div class="user-profile-pill">
<div class="user-profile-info">
<div class="user-profile-name">👤 ${escHtml(window.currentUser.username)}</div>
<div class="user-profile-role">${roleLabel}${escHtml(extraLabel)}</div>
</div>
<button class="btn-profile-logout" id="authLogoutBtn" title="Logout">
<i class="fas fa-sign-out-alt"></i>
</button>
</div>
`;
document.getElementById('authLogoutBtn').addEventListener('click', logout);
// Show user management button in sidebar for Admin
const adminDivider = document.getElementById('sidebarAdminDivider');
if (window.currentUser.role === 'admin') {
if (menuUsersBtn) menuUsersBtn.style.display = 'flex';
if (adminDivider) adminDivider.style.display = 'block';
} else {
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
if (adminDivider) adminDivider.style.display = 'none';
}
} else {
// Logged Out state
authWidget.innerHTML = `
<button class="btn-login" id="authLoginBtn"><i class="fas fa-sign-in-alt"></i> Login</button>
`;
document.getElementById('authLoginBtn').addEventListener('click', showLoginModal);
if (menuUsersBtn) menuUsersBtn.style.display = 'none';
const adminDivider = document.getElementById('sidebarAdminDivider');
if (adminDivider) adminDivider.style.display = 'none';
}
}
function refreshAllLayers() {
if (typeof loadSpbu === 'function') loadSpbu();
if (typeof loadJalan === 'function') loadJalan();
if (typeof loadParsil === 'function') loadParsil();
if (typeof loadRumahIbadah === 'function') loadRumahIbadah();
if (typeof loadPendudukMiskin === 'function') loadPendudukMiskin();
if (window.refreshActivePanel) window.refreshActivePanel();
}
// Login Modals logic
function showLoginModal() {
loginUsernameInput.value = '';
loginPasswordInput.value = '';
loginErrorMsg.style.display = 'none';
loginModal.classList.add('show');
}
function hideLoginModal() {
loginModal.classList.remove('show');
}
if (closeLoginModal) {
closeLoginModal.addEventListener('click', hideLoginModal);
}
loginSubmitBtn.addEventListener('click', function() {
const username = loginUsernameInput.value.trim();
const password = loginPasswordInput.value;
if (!username || !password) {
loginErrorMsg.textContent = 'Username dan password wajib diisi';
loginErrorMsg.style.display = 'block';
return;
}
fetch('api/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
window.currentUser = data.data;
hideLoginModal();
updateAuthUI();
refreshAllLayers();
showToast('Login berhasil', 'success');
} else {
loginErrorMsg.textContent = data.message || 'Login gagal';
loginErrorMsg.style.display = 'block';
}
})
.catch(err => {
console.error(err);
loginErrorMsg.textContent = 'Koneksi ke server terputus';
loginErrorMsg.style.display = 'block';
});
});
function logout() {
fetch('api/logout.php')
.then(res => res.json())
.then(data => {
window.currentUser = null;
updateAuthUI();
refreshAllLayers();
showToast('Logout berhasil', 'success');
})
.catch(err => {
console.error(err);
showToast('Gagal logout', 'error');
});
}
// User Management Modal Logic
if (menuUsersBtn) {
menuUsersBtn.addEventListener('click', function() {
userManagementModal.classList.add('show');
loadUsers();
populateIbadahDropdown();
resetUserForm();
});
}
if (closeUserManagementModal) {
closeUserManagementModal.addEventListener('click', function() {
userManagementModal.classList.remove('show');
});
}
// Show/hide ibadah dropdown based on selected role in form
if (manageRole) {
manageRole.addEventListener('change', function() {
if (this.value === 'admin') {
manageIbadahGroup.style.display = 'none';
manageIbadahId.value = '';
} else {
manageIbadahGroup.style.display = 'flex';
}
});
}
function populateIbadahDropdown() {
if (!manageIbadahId) return;
manageIbadahId.innerHTML = '<option value="">-- Pilih Rumah Ibadah --</option>';
if (typeof ibadahDataList !== 'undefined') {
ibadahDataList.forEach(ib => {
const opt = document.createElement('option');
opt.value = ib.id;
opt.textContent = `${ib.jenis || '🕌'} ${ib.nama}`;
manageIbadahId.appendChild(opt);
});
}
}
function loadUsers() {
if (!userTableBody) return;
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Memuat user...</td></tr>';
fetch('api/users/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
userTableBody.innerHTML = '';
if (data.data.length === 0) {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#aaa;">Belum ada user</td></tr>';
return;
}
data.data.forEach(user => {
const tr = document.createElement('tr');
tr.style.borderBottom = '1px solid #e2e8f0';
const roleBadge = user.role === 'admin'
? '<span class="data-card-badge badge-red">Admin</span>'
: '<span class="data-card-badge badge-blue">Pengelola</span>';
tr.innerHTML = `
<td style="padding:10px 12px; font-weight:600;">${escHtml(user.username)}</td>
<td style="padding:10px 12px;">${roleBadge}</td>
<td style="padding:10px 12px; color:#64748b;">${escHtml(user.nama_ibadah)}</td>
<td style="padding:10px 12px; text-align:center; display:flex; gap:6px; justify-content:center;">
<button class="btn-card-action btn-card-edit" title="Edit User" onclick="editUser(${JSON.stringify(user).replace(/"/g, '&quot;')})">
<i class="fas fa-pen"></i>
</button>
<button class="btn-card-action btn-card-delete" title="Hapus User" onclick="deleteUser(${user.id})">
<i class="fas fa-trash"></i>
</button>
</td>
`;
userTableBody.appendChild(tr);
});
}
})
.catch(err => {
userTableBody.innerHTML = '<tr><td colspan="4" style="text-align:center; padding:15px; color:#ef4444;">Gagal memuat user</td></tr>';
});
}
window.editUser = function(user) {
userFormTitle.textContent = 'Edit Pengguna';
manageUserId.value = user.id;
manageUsername.value = user.username;
managePassword.value = ''; // Password cleared in edit
manageRole.value = user.role;
if (user.role === 'admin') {
manageIbadahGroup.style.display = 'none';
manageIbadahId.value = '';
} else {
manageIbadahGroup.style.display = 'flex';
manageIbadahId.value = user.ibadah_id || '';
}
btnCancelUserEdit.style.display = 'inline-block';
};
window.deleteUser = function(id) {
if (confirm('Apakah Anda yakin ingin menghapus user ini?')) {
fetch('api/users/delete.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
showToast(data.message, 'success');
loadUsers();
resetUserForm();
} else {
showToast(data.message || 'Gagal menghapus user', 'error');
}
})
.catch(err => {
showToast('Koneksi terputus', 'error');
});
}
};
btnSaveUser.addEventListener('click', function() {
const id = manageUserId.value;
const username = manageUsername.value.trim();
const password = managePassword.value;
const role = manageRole.value;
const ibadah_id = manageIbadahId.value;
if (!username) {
alert('Username wajib diisi');
return;
}
if (!id && !password) {
alert('Password wajib diisi untuk user baru');
return;
}
const payload = { username, role, ibadah_id };
if (id) payload.id = id;
if (password) payload.password = password;
const endpoint = id ? 'api/users/update.php' : 'api/users/create.php';
fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.status === 'success') {
showToast(data.message, 'success');
loadUsers();
resetUserForm();
} else {
alert(data.message || 'Gagal menyimpan user');
}
})
.catch(err => {
alert('Koneksi terputus');
});
});
btnCancelUserEdit.addEventListener('click', resetUserForm);
function resetUserForm() {
userFormTitle.textContent = 'Tambah User Baru';
manageUserId.value = '';
manageUsername.value = '';
managePassword.value = '';
manageRole.value = 'pengelola';
manageIbadahGroup.style.display = 'flex';
manageIbadahId.value = '';
btnCancelUserEdit.style.display = 'none';
}
// Helper functions
function escHtml(str) {
if (!str) return '';
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// Toast Notification helper
function showToast(message, type = 'success') {
if (typeof window.showToast === 'function') {
window.showToast(message, type);
return;
}
// Fallback alert
alert(message);
}
// Run session check on initialization
checkSession();
})();
-138
View File
@@ -1,138 +0,0 @@
// --- Setup Leaflet Draw ---
// Layer terpisah untuk hasil gambar sementara sebelum disimpan
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
draw: {
polygon: {
allowIntersection: false,
showArea: true
},
polyline: {
metric: true
},
circle: false,
circlemarker: false,
marker: false, // Marker default dimatikan, kita pakai klik peta untuk SPBU
rectangle: false
},
edit: {
featureGroup: drawnItems, // Kita tidak mengedit di drawnItems, karena data aslinya di jalanLayer/parsilLayer
edit: false,
remove: false
}
});
// Hapus map.addControl(drawControl); agar tidak tampil UI bawaan LeafletJS
// Flag untuk menghindari munculnya form SPBU saat sedang menggambar
map.on(L.Draw.Event.DRAWSTART, function (e) {
isDrawingMode = true;
});
map.on(L.Draw.Event.DRAWSTOP, function (e) {
setTimeout(() => {
isDrawingMode = false;
window.currentDrawMode = null;
window.activeDrawHandler = null;
window.deactivateAddMode();
}, 200);
});
window.activateDraw = function(type) {
console.log("-> activateDraw dipanggil untuk tipe:", type);
// Toggle: jika mode yang sama ditekan lagi, batalkan
if (window.currentDrawMode === type) {
console.log("Mode", type, "sudah aktif, membatalkan...");
window.deactivateAddMode();
return;
}
window.deactivateAddMode();
window.currentDrawMode = type;
console.log("Mengaktifkan mode menggambar:", type);
if (type === 'polyline') {
const handler = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
handler.enable();
window.activeDrawHandler = handler;
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Jalan';
} else if (type === 'polygon') {
const handler = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
handler.enable();
window.activeDrawHandler = handler;
if (window.cursorTooltip) window.cursorTooltip.textContent = 'Klik untuk menggambar Parsil';
}
};
map.on(L.Draw.Event.CREATED, function (e) {
const type = e.layerType;
const layer = e.layer;
// Convert layer to GeoJSON geometry
const geoJson = layer.toGeoJSON().geometry;
const geoJsonStr = JSON.stringify(geoJson);
if (type === 'polyline') {
let length = 0;
const latlngs = layer.getLatLngs();
for (let i = 0; i < latlngs.length - 1; i++) {
length += latlngs[i].distanceTo(latlngs[i + 1]);
}
// Gunakan Modal alih-alih prompt
const bodyHTML = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="modalJalanNama" placeholder="Nama Jalan">
</div>
<div class="form-group">
<label>Status</label>
<select id="modalJalanStatus">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten" selected>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${length.toFixed(2)} m</label>
</div>
`;
openModal("Tambah Jalan Baru", bodyHTML, function() {
window.saveNewJalan(geoJsonStr, length, 'modalJalanNama', 'modalJalanStatus');
});
}
else if (type === 'polygon') {
const latlngs = layer.getLatLngs()[0];
let area = 0;
if (L.GeometryUtil && L.GeometryUtil.geodesicArea) {
area = L.GeometryUtil.geodesicArea(latlngs);
}
const bodyHTML = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="modalParsilNama" placeholder="Contoh: Budi atau Sawah A">
</div>
<div class="form-group">
<label>Status Tanah</label>
<select id="modalParsilStatus">
<option value="SHM" selected>SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${area.toFixed(2)} m²</label>
</div>
`;
openModal("Tambah Parsil Tanah", bodyHTML, function() {
window.saveNewParsil(geoJsonStr, area, 'modalParsilNama', 'modalParsilStatus');
});
}
});
-164
View File
@@ -1,164 +0,0 @@
// --- Fitur Jalan ---
const jalanColors = {
'Nasional': '#ff0000', // Merah
'Provinsi': '#0000ff', // Biru
'Kabupaten': '#00ff00' // Hijau
};
function loadJalan() {
jalanLayer.clearLayers();
fetch('api/jalan/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.data) {
data.data.forEach(item => {
addJalanToMap(item);
});
}
if (window.refreshActivePanel) window.refreshActivePanel();
});
}
function addJalanToMap(item) {
// geom adalah GeoJSON {type: "LineString", coordinates: [[lng, lat], ...]}
const latlngs = item.geom.coordinates.map(coord => [coord[1], coord[0]]);
const polyline = L.polyline(latlngs, {
color: jalanColors[item.status] || '#3388ff',
weight: 5
});
polyline.jalanData = item;
// Tampilkan label nama jalan sejajar dengan garis (diagonal)
polyline.setText(item.nama, {
center: true,
offset: 0,
attributes: {
fill: '#000000',
'font-weight': 'bold',
'font-size': '14px',
'dy': '7'
}
});
// Hitung popupContent sekali saat jalan dibuat
const d = item;
const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Jalan ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Panjang:</b> ${d.panjang.toFixed(2)} m</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditJalanModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteJalan(${d.id})">Hapus</button>
</div>
</div>
`;
polyline.bindPopup(popupContent);
jalanLayer.addLayer(polyline);
}
window.openEditJalanModal = function(id) {
let d = null;
jalanLayer.eachLayer(function(layer) {
if (layer.jalanData && layer.jalanData.id == id) d = layer.jalanData;
});
if (!d) return;
const bodyHTML = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="editJalanNama" value="${d.nama}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editJalanStatus">
<option value="Nasional" ${d.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${d.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${d.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang: ${d.panjang.toFixed(2)} m</label>
</div>
`;
map.closePopup();
openModal("Edit Jalan", bodyHTML, function() {
window.saveEditJalan(d.id, 'editJalanNama', 'editJalanStatus');
});
};
window.saveEditJalan = function(id, namaId, statusId) {
const nama = document.getElementById(namaId).value;
const status = document.getElementById(statusId).value;
fetch('api/jalan/update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, status })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
map.closePopup();
closeModal();
loadJalan();
} else {
alert(data.message);
}
});
};
window.deleteJalan = function(id) {
openConfirmModal("Yakin hapus jalan ini?", function() {
fetch('api/jalan/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();
loadJalan();
} else {
alert(data.message);
}
});
});
};
window.saveNewJalan = function(geoJsonStr, panjang, namaId, statusId) {
const nama = document.getElementById(namaId).value;
if (!nama) {
alert("Nama jalan harus diisi!");
return false;
}
const status = document.getElementById(statusId).value;
const geom = JSON.parse(geoJsonStr);
fetch('api/jalan/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, status, panjang, geom })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
closeModal();
loadJalan();
} else {
alert(data.message);
}
});
return true;
};
// Initial Load
loadJalan();
+119 -19
View File
@@ -95,7 +95,9 @@ function addIbadahMarker(item) {
if (isNaN(lat) || isNaN(lng)) return;
const marker = L.marker([lat, lng], { icon: makeIbadahIcon(item.jenis), draggable: true });
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], {
@@ -107,7 +109,7 @@ function addIbadahMarker(item) {
});
circle.on('mousemove', function(e) {
if (isResizing) return;
if (!isAdmin || isResizing) return;
const center = circle.getLatLng();
const radius = circle.getRadius();
const dist = center.distanceTo(e.latlng);
@@ -125,7 +127,7 @@ function addIbadahMarker(item) {
});
circle.on('mousedown', function(e) {
if (circle.nearEdge) {
if (isAdmin && circle.nearEdge) {
map.dragging.disable();
isResizing = true;
resizingCircle = circle;
@@ -139,16 +141,22 @@ function addIbadahMarker(item) {
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>
<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>
${actionButtons}
</div>
`;
marker.bindPopup(popupContent);
@@ -168,7 +176,9 @@ function addIbadahMarker(item) {
// Konteks Menu untuk Tambah Penduduk Miskin (klik kanan peta)
map.on('contextmenu', function(e) {
if (isDrawingMode) return;
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
if (!canManage) return;
const popupContent = `
<div class="popup-form">
@@ -181,8 +191,6 @@ map.on('contextmenu', function(e) {
// Map Click -> Form Add Rumah Ibadah
map.on('click', function(e) {
if (isDrawingMode) return;
if (window.currentAddMode === 'rumah_ibadah') {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
@@ -389,19 +397,55 @@ function addMiskinMarker(item) {
const lng = parseFloat(item.lng);
if (isNaN(lat) || isNaN(lng)) return;
const marker = L.marker([lat, lng], { icon: makeMiskinIcon(false), draggable: true });
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
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 10px 0;"><b>Jiwa:</b> ${d.jumlah_jiwa || '-'}</p>
<div style="display:flex; gap:5px; flex-wrap:wrap;">
<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>
<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>
${actionButtons}
</div>
</div>
`;
@@ -445,6 +489,14 @@ window.formAddMiskin = function(lat, lng) {
<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);
@@ -455,13 +507,23 @@ 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 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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, kategori_bantuan, jumlah_jiwa, lat, lng })
body: formData
})
.then(res => res.json())
.then(data => {
@@ -495,6 +557,32 @@ window.openEditMiskinModal = function(id) {
<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() {
@@ -506,10 +594,22 @@ 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',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, kategori_bantuan, jumlah_jiwa, lat, lng })
body: formData
}).then(res => res.json()).then(data => {
if(data.status === 'success') { closeModal(); loadPendudukMiskin(); }
});
-151
View File
@@ -1,151 +0,0 @@
// --- Fitur Parsil ---
const parsilColors = {
'SHM': '#28a745', // Hijau
'HGB': '#17a2b8', // Biru Muda
'HGU': '#ffc107', // Kuning
'HP': '#fd7e14' // Oranye
};
function loadParsil() {
parsilLayer.clearLayers();
fetch('api/parsil/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.data) {
data.data.forEach(item => {
addParsilToMap(item);
});
}
if (window.refreshActivePanel) window.refreshActivePanel();
});
}
function addParsilToMap(item) {
// geom adalah GeoJSON Polygon
// coordinates pada Polygon formatnya: [[[lng, lat], [lng, lat], ...]]
// Leaflet Polygon butuh array of [lat, lng]
const latlngs = item.geom.coordinates[0].map(coord => [coord[1], coord[0]]);
const polygon = L.polygon(latlngs, {
color: parsilColors[item.status] || '#3388ff',
fillColor: parsilColors[item.status] || '#3388ff',
fillOpacity: 0.5,
weight: 2
});
polygon.parsilData = item;
const d = item;
const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">Parsil ${d.nama || ''}</h4>
<p style="margin: 0 0 5px 0;"><b>Status:</b> ${d.status}</p>
<p style="margin: 0 0 10px 0;"><b>Luas:</b> ${d.luas.toFixed(2)} m²</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditParsilModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteParsil(${d.id})">Hapus</button>
</div>
</div>
`;
polygon.bindPopup(popupContent);
parsilLayer.addLayer(polygon);
}
window.openEditParsilModal = function(id) {
let d = null;
parsilLayer.eachLayer(function(layer) {
if (layer.parsilData && layer.parsilData.id == id) d = layer.parsilData;
});
if (!d) return;
const bodyHTML = `
<div class="form-group">
<label>Nama Pemilik/Area</label>
<input type="text" id="editParsilNama" value="${d.nama || ''}">
</div>
<div class="form-group">
<label>Status</label>
<select id="editParsilStatus">
<option value="SHM" ${d.status === 'SHM' ? 'selected' : ''}>SHM</option>
<option value="HGB" ${d.status === 'HGB' ? 'selected' : ''}>HGB</option>
<option value="HGU" ${d.status === 'HGU' ? 'selected' : ''}>HGU</option>
<option value="HP" ${d.status === 'HP' ? 'selected' : ''}>HP</option>
</select>
</div>
<div class="form-group">
<label>Luas: ${d.luas.toFixed(2)} m²</label>
</div>
`;
map.closePopup();
openModal("Edit Parsil Tanah", bodyHTML, function() {
window.saveEditParsil(d.id, 'editParsilNama', 'editParsilStatus');
});
};
window.saveEditParsil = function(id, namaId, statusId) {
const nama = document.getElementById(namaId).value;
const status = document.getElementById(statusId).value;
fetch('api/parsil/update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, status })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
map.closePopup();
closeModal();
loadParsil();
} else {
alert(data.message);
}
});
};
window.deleteParsil = function(id) {
openConfirmModal("Yakin hapus parsil ini?", function() {
fetch('api/parsil/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();
loadParsil();
} else {
alert(data.message);
}
});
});
};
window.saveNewParsil = function(geoJsonStr, luas, namaId, statusId) {
const nama = document.getElementById(namaId).value;
const status = document.getElementById(statusId).value;
const geom = JSON.parse(geoJsonStr);
fetch('api/parsil/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, status, luas, geom })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
closeModal();
loadParsil();
} else {
alert(data.message);
}
});
return true;
};
// Initial Load
loadParsil();
-206
View File
@@ -1,206 +0,0 @@
// --- Fitur SPBU ---
// Emoji Bubble Icon builder
function makeSpbuIcon(is24) {
const cls = is24 ? 'spbu-24' : 'spbu-not24';
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]
});
}
const spbuGreenIcon = makeSpbuIcon(true);
const spbuRedIcon = makeSpbuIcon(false);
let isDrawingMode = false; // Akan diset true saat draw mode aktif (jalan/parsil)
// Fetch SPBU
function loadSpbu() {
spbuLayer.clearLayers();
fetch('api/spbu/read.php')
.then(res => res.json())
.then(data => {
if (data.status === 'success' && data.data) {
data.data.forEach(item => {
addSpbuMarker(item);
});
}
if (window.refreshActivePanel) window.refreshActivePanel();
});
}
function addSpbuMarker(item) {
const icon = makeSpbuIcon(item.is_24_jam);
const marker = L.marker([item.lat, item.lng], {
icon: icon,
draggable: true
});
marker.spbuData = item; // Simpan data di objek marker
// Hitung popupContent sekali saat marker dibuat
const d = item;
const popupContent = `
<div style="font-family: Arial, sans-serif; min-width: 150px;">
<h4 style="margin:0 0 5px 0;">SPBU ${d.nama}</h4>
<p style="margin: 0 0 5px 0;"><b>No. WA:</b> ${d.no_wa}</p>
<p style="margin: 0 0 10px 0;"><b>Buka 24 Jam:</b> ${d.is_24_jam ? 'Ya' : 'Tidak'}</p>
<div style="display:flex; gap:5px;">
<button style="padding:4px 8px; background:#007bff; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="openEditSpbuModal(${d.id})">Edit</button>
<button style="padding:4px 8px; background:#dc3545; color:white; border:none; border-radius:3px; cursor:pointer;" onclick="deleteSpbu(${d.id})">Hapus</button>
</div>
</div>
`;
marker.bindPopup(popupContent);
// Event drag untuk update koordinat
marker.on('dragend', function(e) {
const newPos = marker.getLatLng();
updateSpbu(item.id, item.nama, item.no_wa, item.is_24_jam, newPos.lat, newPos.lng);
});
spbuLayer.addLayer(marker);
}
// Map Click -> Form Add
map.on('click', function(e) {
if (isDrawingMode) return; // Jangan muncul form jika sedang gambar garis/polygon
// Hanya proses jika mode tambah SPBU aktif
if (window.currentAddMode === 'spbu') {
const bodyHTML = `
<div class="form-group">
<label>Lat: ${e.latlng.lat.toFixed(6)}, Lng: ${e.latlng.lng.toFixed(6)}</label>
</div>
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="modalSpbuNama" placeholder="Nama SPBU">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="modalSpbuWa" placeholder="No. WA">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="modalSpbu24" value="1"> Ya</label>
<label><input type="radio" name="modalSpbu24" value="0" checked> Tidak</label>
</div>
</div>
`;
openModal("Tambah SPBU Baru", bodyHTML, function() {
window.saveNewSpbu(e.latlng.lat, e.latlng.lng, 'modalSpbuNama', 'modalSpbuWa', 'modalSpbu24');
});
// Nonaktifkan mode tambah setelah modal muncul
window.deactivateAddMode();
}
});
window.saveNewSpbu = function(lat, lng, namaId, waId, radioName) {
const nama = document.getElementById(namaId).value;
if (!nama) {
alert("Nama SPBU harus diisi!");
return;
}
const no_wa = document.getElementById(waId).value;
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
fetch('api/spbu/create.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ nama, no_wa, is_24_jam, lat, lng })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
closeModal();
loadSpbu();
} else {
alert(data.message);
}
});
};
window.openEditSpbuModal = function(id) {
let d = null;
spbuLayer.eachLayer(function(layer) {
if (layer.spbuData && layer.spbuData.id == id) d = layer.spbuData;
});
if (!d) return;
const bodyHTML = `
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="editSpbuNama" value="${d.nama}">
</div>
<div class="form-group">
<label>No. WA</label>
<input type="text" id="editSpbuWa" value="${d.no_wa}">
</div>
<div class="form-group">
<label>Buka 24 Jam?</label>
<div class="radio-group" style="margin-top:5px;">
<label><input type="radio" name="editSpbu24" value="1" ${d.is_24_jam ? 'checked' : ''}> Ya</label>
<label><input type="radio" name="editSpbu24" value="0" ${!d.is_24_jam ? 'checked' : ''}> Tidak</label>
</div>
</div>
`;
map.closePopup();
openModal("Edit SPBU", bodyHTML, function() {
window.saveEditSpbu(d.id, d.lat, d.lng, 'editSpbuNama', 'editSpbuWa', 'editSpbu24');
});
};
window.saveEditSpbu = function(id, lat, lng, namaId, waId, radioName) {
const nama = document.getElementById(namaId).value;
const no_wa = document.getElementById(waId).value;
const is_24_jam = document.querySelector(`input[name="${radioName}"]:checked`).value;
updateSpbu(id, nama, no_wa, is_24_jam, lat, lng);
};
function updateSpbu(id, nama, no_wa, is_24_jam, lat, lng) {
fetch('api/spbu/update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, nama, no_wa, is_24_jam, lat, lng })
})
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
map.closePopup();
closeModal();
loadSpbu();
} else {
alert(data.message);
}
});
}
window.deleteSpbu = function(id) {
openConfirmModal("Yakin hapus SPBU ini?", function() {
fetch('api/spbu/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();
loadSpbu();
} else {
alert(data.message);
}
});
});
};
// Initial Load
loadSpbu();