Initial commit
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
// --- 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;
|
||||
|
||||
const dashboardBtn = document.getElementById('menuDashboard');
|
||||
|
||||
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 Dashboard button for all logged-in users
|
||||
if (dashboardBtn) dashboardBtn.style.display = 'flex';
|
||||
|
||||
// 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';
|
||||
if (dashboardBtn) dashboardBtn.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();
|
||||
});
|
||||
}
|
||||
|
||||
// Dashboard Button
|
||||
const menuDashboardBtn = document.getElementById('menuDashboard');
|
||||
if (menuDashboardBtn) {
|
||||
menuDashboardBtn.addEventListener('click', function() {
|
||||
if (typeof window.openDashboard === 'function') window.openDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
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, '"')})">
|
||||
<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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// 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();
|
||||
})();
|
||||
@@ -0,0 +1,468 @@
|
||||
// ===== Dashboard Feature (Terintegrasi) =====
|
||||
// Menghubungkan: webgis-poverty-pontianak, jalan, penduduk, spbu
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── State ──────────────────────────────────────────────────────────────
|
||||
let dashboardData = null;
|
||||
let activeTab = 'kemiskinan';
|
||||
|
||||
// ── Modal Element ──────────────────────────────────────────────────────
|
||||
const modal = document.getElementById('dashboardModal');
|
||||
if (!modal) return;
|
||||
|
||||
// ── Open / Close ───────────────────────────────────────────────────────
|
||||
window.openDashboard = function () {
|
||||
modal.classList.add('show');
|
||||
if (!dashboardData) loadDashboard();
|
||||
else renderTab(activeTab);
|
||||
};
|
||||
|
||||
window.closeDashboard = function () {
|
||||
modal.classList.remove('show');
|
||||
};
|
||||
|
||||
document.getElementById('closeDashboardModal')?.addEventListener('click', window.closeDashboard);
|
||||
modal.addEventListener('click', function (e) {
|
||||
if (e.target === modal) window.closeDashboard();
|
||||
});
|
||||
|
||||
// ── Load Data ──────────────────────────────────────────────────────────
|
||||
function loadDashboard() {
|
||||
const body = document.getElementById('dashboardBody');
|
||||
body.innerHTML = `
|
||||
<div class="db-loading">
|
||||
<div class="db-spinner"></div>
|
||||
<span>Memuat data dashboard terintegrasi…</span>
|
||||
</div>`;
|
||||
|
||||
fetch('api/dashboard/stats_extended.php')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status !== 'success') throw new Error(data.message || 'Gagal memuat');
|
||||
dashboardData = data;
|
||||
renderShell();
|
||||
renderTab(activeTab);
|
||||
})
|
||||
.catch(err => {
|
||||
body.innerHTML = `<div class="db-error">❌ Gagal memuat data: ${err.message}</div>`;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Shell (tab bar + container) ────────────────────────────────────────
|
||||
function renderShell() {
|
||||
const body = document.getElementById('dashboardBody');
|
||||
const tabs = [
|
||||
{ id: 'kemiskinan', label: '🏠 Kemiskinan' },
|
||||
{ id: 'jalan', label: '🛣️ Jalan' },
|
||||
{ id: 'spbu', label: '⛽ SPBU' },
|
||||
{ id: 'penduduk', label: '👥 Penduduk' },
|
||||
];
|
||||
|
||||
body.innerHTML = `
|
||||
<div class="db-tab-bar" id="dbTabBar">
|
||||
${tabs.map(t => `
|
||||
<button class="db-tab-btn${t.id === activeTab ? ' active' : ''}"
|
||||
data-tab="${t.id}">${t.label}</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div id="dbTabContent" class="db-tab-content"></div>`;
|
||||
|
||||
document.getElementById('dbTabBar').addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('[data-tab]');
|
||||
if (!btn) return;
|
||||
activeTab = btn.dataset.tab;
|
||||
document.querySelectorAll('.db-tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === activeTab));
|
||||
renderTab(activeTab);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Route to tab ───────────────────────────────────────────────────────
|
||||
function renderTab(tab) {
|
||||
const content = document.getElementById('dbTabContent');
|
||||
if (!content || !dashboardData) return;
|
||||
if (tab === 'kemiskinan') renderKemiskinan(content, dashboardData.poverty);
|
||||
else if (tab === 'jalan') renderJalan(content, dashboardData.jalan);
|
||||
else if (tab === 'spbu') renderSpbu(content, dashboardData.spbu);
|
||||
else if (tab === 'penduduk') renderPenduduk(content, dashboardData.penduduk);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 1: Kemiskinan
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderKemiskinan(el, d) {
|
||||
const pctColor = d.pct_terurus >= 70 ? '#22c55e' : d.pct_terurus >= 40 ? '#f59e0b' : '#ef4444';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card kpi-ibadah">
|
||||
<div class="db-kpi-icon">🕌</div>
|
||||
<div class="db-kpi-val">${d.total_ibadah}</div>
|
||||
<div class="db-kpi-label">Rumah Ibadah</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-miskin">
|
||||
<div class="db-kpi-icon">🏠</div>
|
||||
<div class="db-kpi-val">${d.total_miskin}</div>
|
||||
<div class="db-kpi-label">KK Miskin</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-jiwa">
|
||||
<div class="db-kpi-icon">👥</div>
|
||||
<div class="db-kpi-val">${d.total_jiwa}</div>
|
||||
<div class="db-kpi-label">Total Jiwa</div>
|
||||
</div>
|
||||
<div class="db-kpi-card kpi-log">
|
||||
<div class="db-kpi-icon">📋</div>
|
||||
<div class="db-kpi-val">${d.total_log}</div>
|
||||
<div class="db-kpi-label">Log Bantuan</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${d.total_miskin > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">📊 Cakupan Penanganan</div>
|
||||
<div class="db-coverage-row">
|
||||
<div class="db-coverage-labels">
|
||||
<span style="color:#22c55e">✅ Terurus: <strong>${d.terurus}</strong></span>
|
||||
<span style="color:#ef4444">⚠️ Tidak Terurus: <strong>${d.tidak_terurus}</strong></span>
|
||||
<span style="color:${pctColor}; font-weight:700">${d.pct_terurus}%</span>
|
||||
</div>
|
||||
<div class="db-progress-bar">
|
||||
<div class="db-progress-fill" style="width:${d.pct_terurus}%; background:${pctColor};"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>` : ''}
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">📦 Kategori Bantuan</div>
|
||||
<canvas id="chartKategori" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">🎁 Tipe Bantuan</div>
|
||||
<canvas id="chartTipe" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card db-chart-wide">
|
||||
<div class="db-chart-title">📅 Aktivitas Bantuan (6 Bulan)</div>
|
||||
<canvas id="chartBulan" height="140"></canvas>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartKategori', d.distribusi.kategori, ['#6366f1','#f59e0b','#22c55e','#ef4444','#06b6d4']);
|
||||
drawPieChart('chartTipe', d.distribusi.tipe_bantuan, ['#4f46e5','#16a34a','#dc2626','#d97706','#0891b2','#7c3aed']);
|
||||
drawBarChart('chartBulan', d.distribusi.per_bulan);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 2: Jalan
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderJalan(el, d) {
|
||||
const panjangKm = (d.total_panjang_m / 1000).toFixed(2);
|
||||
const luasHa = (d.total_luas_m2 / 10000).toFixed(2);
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#3b82f6">
|
||||
<div class="db-kpi-icon">🛣️</div>
|
||||
<div class="db-kpi-val">${d.total_jalan}</div>
|
||||
<div class="db-kpi-label">Ruas Jalan</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#0ea5e9">
|
||||
<div class="db-kpi-icon">📏</div>
|
||||
<div class="db-kpi-val">${panjangKm} km</div>
|
||||
<div class="db-kpi-label">Total Panjang</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#8b5cf6">
|
||||
<div class="db-kpi-icon">🗂️</div>
|
||||
<div class="db-kpi-val">${d.total_parsil}</div>
|
||||
<div class="db-kpi-label">Persil Tanah</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#6d28d9">
|
||||
<div class="db-kpi-icon">📐</div>
|
||||
<div class="db-kpi-val">${luasHa} ha</div>
|
||||
<div class="db-kpi-label">Total Luas Persil</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">🛣️ Jalan per Status</div>
|
||||
<canvas id="chartStatusJalan" height="180"></canvas>
|
||||
</div>
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">📋 Persil per Kepemilikan</div>
|
||||
<canvas id="chartKepemilikan" height="180"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${Object.keys(d.per_status).length > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">🛣️ Rincian Status Jalan</div>
|
||||
<div class="db-table-wrap">
|
||||
<table class="db-table">
|
||||
<thead><tr><th>Status</th><th>Jumlah Ruas</th></tr></thead>
|
||||
<tbody>
|
||||
${Object.entries(d.per_status).map(([k,v]) => `
|
||||
<tr><td>${esc(k)}</td><td><span class="db-badge">${v}</span></td></tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : '<div class="db-empty">Belum ada data jalan tersimpan di database.</div>'}`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartStatusJalan', d.per_status, ['#3b82f6','#0ea5e9','#38bdf8']);
|
||||
drawPieChart('chartKepemilikan', d.per_kepemilikan, ['#8b5cf6','#a78bfa','#c4b5fd','#7c3aed']);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 3: SPBU
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderSpbu(el, d) {
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#f97316">
|
||||
<div class="db-kpi-icon">⛽</div>
|
||||
<div class="db-kpi-val">${d.total_spbu}</div>
|
||||
<div class="db-kpi-label">Total SPBU</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#22c55e">
|
||||
<div class="db-kpi-icon">🕐</div>
|
||||
<div class="db-kpi-val">${d.buka_24_jam}</div>
|
||||
<div class="db-kpi-label">Buka 24 Jam</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#ef4444">
|
||||
<div class="db-kpi-icon">🕙</div>
|
||||
<div class="db-kpi-val">${d.tidak_24_jam}</div>
|
||||
<div class="db-kpi-label">Tidak 24 Jam</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card">
|
||||
<div class="db-chart-title">⏰ Status Operasional</div>
|
||||
<canvas id="chartSpbuOps" height="180"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${d.list.length > 0 ? `
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">📍 Daftar SPBU</div>
|
||||
<div class="db-table-wrap">
|
||||
<table class="db-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Nama SPBU</th>
|
||||
<th>Nomor WA</th>
|
||||
<th>Operasional</th>
|
||||
<th>Koordinat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${d.list.map((s, i) => `
|
||||
<tr>
|
||||
<td>${i+1}</td>
|
||||
<td><strong>${esc(s.nama_spbu)}</strong></td>
|
||||
<td>${esc(s.nomor_wa)}</td>
|
||||
<td>${s.buka_24_jam == 1
|
||||
? '<span class="db-badge" style="background:#22c55e">24 Jam</span>'
|
||||
: '<span class="db-badge" style="background:#f59e0b">Terbatas</span>'}</td>
|
||||
<td style="font-size:11px;color:#888">${parseFloat(s.latitude).toFixed(5)}, ${parseFloat(s.longitude).toFixed(5)}</td>
|
||||
</tr>`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>` : '<div class="db-empty">Belum ada data SPBU tersimpan di database.</div>'}`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawPieChart('chartSpbuOps', { 'Buka 24 Jam': d.buka_24_jam, 'Tidak 24 Jam': d.tidak_24_jam }, ['#22c55e','#ef4444']);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Tab 4: Penduduk (Choropleth Kecamatan)
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function renderPenduduk(el, d) {
|
||||
const maxDensity = Math.max(...d.kecamatan.map(k => k.density));
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="db-kpi-grid">
|
||||
<div class="db-kpi-card" style="--kpi-color:#0891b2">
|
||||
<div class="db-kpi-icon">🗺️</div>
|
||||
<div class="db-kpi-val">${d.total_kecamatan}</div>
|
||||
<div class="db-kpi-label">Kecamatan</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#06b6d4">
|
||||
<div class="db-kpi-icon">📊</div>
|
||||
<div class="db-kpi-val">${d.rata_density.toLocaleString('id-ID')}</div>
|
||||
<div class="db-kpi-label">Rata-rata Kepadatan (jiwa/km²)</div>
|
||||
</div>
|
||||
<div class="db-kpi-card" style="--kpi-color:#0e7490">
|
||||
<div class="db-kpi-icon">🏆</div>
|
||||
<div class="db-kpi-val" style="font-size:14px">${esc(d.terpadat)}</div>
|
||||
<div class="db-kpi-label">Kecamatan Terpadat</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-section">
|
||||
<div class="db-section-title">👥 Kepadatan Penduduk per Kecamatan (jiwa/km²)</div>
|
||||
${d.kecamatan.map(k => {
|
||||
const pct = ((k.density / maxDensity) * 100).toFixed(1);
|
||||
const color = k.density >= 8 ? '#ef4444' : k.density >= 5 ? '#f59e0b' : '#22c55e';
|
||||
return `
|
||||
<div style="margin-bottom:10px">
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; margin-bottom:3px">
|
||||
<span>${esc(k.nama)}</span>
|
||||
<strong style="color:${color}">${k.density.toLocaleString('id-ID')} jiwa/km²</strong>
|
||||
</div>
|
||||
<div class="db-progress-bar">
|
||||
<div class="db-progress-fill" style="width:${pct}%; background:${color};"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
<div style="margin-top:8px; font-size:11px; color:#888; display:flex; gap:16px">
|
||||
<span>🟢 <5 Rendah</span>
|
||||
<span>🟡 5–8 Sedang</span>
|
||||
<span>🔴 ≥8 Tinggi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="db-charts-grid">
|
||||
<div class="db-chart-card db-chart-wide">
|
||||
<div class="db-chart-title">📊 Kepadatan per Kecamatan</div>
|
||||
<canvas id="chartDensity" height="160"></canvas>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
setTimeout(() => {
|
||||
drawDensityBar('chartDensity', d.kecamatan);
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
// Chart Helpers
|
||||
// ══════════════════════════════════════════════════════════════════════
|
||||
function drawPieChart(canvasId, dataObj, colors) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const entries = Object.entries(dataObj || {}).filter(([,v]) => v > 0);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (entries.length === 0) {
|
||||
ctx.fillStyle = '#aaa'; ctx.font = '13px Inter'; ctx.textAlign = 'center';
|
||||
ctx.fillText('Belum ada data', canvas.width/2, canvas.height/2);
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = entries.map(([k]) => k);
|
||||
const values = entries.map(([,v]) => v);
|
||||
const total = values.reduce((a,b) => a+b, 0);
|
||||
const cx = canvas.width/2, cy = canvas.height * 0.42;
|
||||
const r = Math.min(cx, cy) * 0.72;
|
||||
|
||||
let startAngle = -Math.PI/2;
|
||||
values.forEach((v,i) => {
|
||||
const slice = (v/total)*2*Math.PI;
|
||||
ctx.beginPath(); ctx.moveTo(cx, cy);
|
||||
ctx.arc(cx, cy, r, startAngle, startAngle+slice);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = colors[i % colors.length];
|
||||
ctx.fill(); ctx.strokeStyle='#fff'; ctx.lineWidth=2; ctx.stroke();
|
||||
startAngle += slice;
|
||||
});
|
||||
|
||||
ctx.fillStyle='#333'; ctx.font='bold 15px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText(total, cx, cy+5);
|
||||
|
||||
const legY = cy+r+14, legItemW = 92, perRow = Math.floor(canvas.width/legItemW);
|
||||
labels.forEach((lbl,i) => {
|
||||
const row=Math.floor(i/perRow), col=i%perRow;
|
||||
const lx=(canvas.width - Math.min(labels.length,perRow)*legItemW)/2 + col*legItemW;
|
||||
const ly=legY+row*17;
|
||||
ctx.fillStyle=colors[i%colors.length]; ctx.fillRect(lx,ly,10,10);
|
||||
ctx.fillStyle='#555'; ctx.font='10px Inter'; ctx.textAlign='left';
|
||||
const shortLbl = lbl.length>11 ? lbl.slice(0,10)+'…' : lbl;
|
||||
ctx.fillText(`${shortLbl} (${values[i]})`, lx+13, ly+9);
|
||||
});
|
||||
}
|
||||
|
||||
function drawBarChart(canvasId, perBulan) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
if (!perBulan || perBulan.length === 0) {
|
||||
ctx.fillStyle='#aaa'; ctx.font='13px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText('Belum ada data bantuan', canvas.width/2, canvas.height/2);
|
||||
return;
|
||||
}
|
||||
const padL=36, padR=12, padT=12, padB=28;
|
||||
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
|
||||
const values=perBulan.map(d=>parseInt(d.cnt)), maxVal=Math.max(...values,1);
|
||||
const barW=W/perBulan.length*0.6, gap=W/perBulan.length;
|
||||
ctx.strokeStyle='#e5e7eb'; ctx.lineWidth=1;
|
||||
for (let i=0;i<=4;i++){
|
||||
const y=padT+H-(i/4)*H;
|
||||
ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(padL+W,y); ctx.stroke();
|
||||
ctx.fillStyle='#9ca3af'; ctx.font='9px Inter'; ctx.textAlign='right';
|
||||
ctx.fillText(Math.round((i/4)*maxVal), padL-4, y+3);
|
||||
}
|
||||
perBulan.forEach((d,i)=>{
|
||||
const bh=(parseInt(d.cnt)/maxVal)*H, bx=padL+i*gap+(gap-barW)/2, by=padT+H-bh;
|
||||
const grad=ctx.createLinearGradient(0,by,0,padT+H);
|
||||
grad.addColorStop(0,'#6366f1'); grad.addColorStop(1,'#a5b4fc');
|
||||
ctx.fillStyle=grad;
|
||||
ctx.beginPath();
|
||||
if(ctx.roundRect) ctx.roundRect(bx,by,barW,bh,[4,4,0,0]); else ctx.rect(bx,by,barW,bh);
|
||||
ctx.fill();
|
||||
ctx.fillStyle='#4f46e5'; ctx.font='bold 10px Inter'; ctx.textAlign='center';
|
||||
ctx.fillText(d.cnt, bx+barW/2, by-4);
|
||||
const ml=d.bulan ? d.bulan.slice(5)+'/'+d.bulan.slice(2,4) : '';
|
||||
ctx.fillStyle='#6b7280'; ctx.font='9px Inter';
|
||||
ctx.fillText(ml, bx+barW/2, padT+H+14);
|
||||
});
|
||||
}
|
||||
|
||||
function drawDensityBar(canvasId, kecamatan) {
|
||||
const canvas = document.getElementById(canvasId);
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const padL=130, padR=50, padT=12, padB=12;
|
||||
const W=canvas.width-padL-padR, H=canvas.height-padT-padB;
|
||||
const maxVal=Math.max(...kecamatan.map(k=>k.density),1);
|
||||
const barH=Math.floor(H/kecamatan.length)-6;
|
||||
|
||||
kecamatan.forEach((k,i) => {
|
||||
const bw=(k.density/maxVal)*W;
|
||||
const by=padT+i*(barH+6);
|
||||
const color=k.density>=8?'#ef4444':k.density>=5?'#f59e0b':'#22c55e';
|
||||
// Label kecamatan
|
||||
ctx.fillStyle='#374151'; ctx.font='11px Inter'; ctx.textAlign='right';
|
||||
ctx.fillText(k.nama, padL-6, by+barH/2+4);
|
||||
// Bar
|
||||
ctx.fillStyle=color+'33';
|
||||
ctx.fillRect(padL, by, W, barH);
|
||||
const grad=ctx.createLinearGradient(padL,0,padL+bw,0);
|
||||
grad.addColorStop(0,color); grad.addColorStop(1,color+'99');
|
||||
ctx.fillStyle=grad;
|
||||
if(ctx.roundRect) ctx.roundRect(padL,by,bw,barH,[0,4,4,0]); else ctx.fillRect(padL,by,bw,barH);
|
||||
ctx.fill();
|
||||
// Value
|
||||
ctx.fillStyle=color; ctx.font='bold 11px Inter'; ctx.textAlign='left';
|
||||
ctx.fillText(k.density.toFixed(3), padL+bw+5, by+barH/2+4);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Utility ───────────────────────────────────────────────────────────
|
||||
function esc(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,181 @@
|
||||
// --- Fitur Import GeoJSON ---
|
||||
|
||||
const fileGeoJson = document.getElementById('fileGeoJson');
|
||||
let geoJsonLayers = {};
|
||||
let geoJsonCounter = 0;
|
||||
|
||||
window.toggleGeoJsonMenu = function() {
|
||||
const list = document.getElementById('geoJsonFileList');
|
||||
const icon = document.getElementById('geoJsonToggleIcon');
|
||||
if (list.style.display === 'none') {
|
||||
list.style.display = 'block';
|
||||
icon.className = 'fas fa-minus';
|
||||
} else {
|
||||
list.style.display = 'none';
|
||||
icon.className = 'fas fa-plus';
|
||||
}
|
||||
};
|
||||
|
||||
// Cek apakah fitur GeoJSON ini terlihat seperti data penduduk miskin
|
||||
function isMiskinFeature(feature) {
|
||||
if (!feature || !feature.geometry || feature.geometry.type !== 'Point') return false;
|
||||
const props = feature.properties || {};
|
||||
const keys = Object.keys(props).map(k => k.toLowerCase());
|
||||
// Dianggap data miskin jika punya properti nama (dan bukan ibadah atau spbu)
|
||||
const hasNama = keys.some(k => ['nama', 'name', 'penduduk'].includes(k));
|
||||
const isIbadah = keys.some(k => ['jenis', 'radius', 'alamat'].includes(k));
|
||||
const isSpbu = keys.some(k => ['alamat_spbu', 'is_24_jam'].includes(k));
|
||||
return hasNama && !isIbadah && !isSpbu;
|
||||
}
|
||||
|
||||
fileGeoJson.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(event) {
|
||||
try {
|
||||
const geoJsonData = JSON.parse(event.target.result);
|
||||
const fileName = file.name;
|
||||
|
||||
// Pisahkan fitur menjadi dua kelompok: miskin dan non-miskin
|
||||
const features = geoJsonData.type === 'FeatureCollection'
|
||||
? geoJsonData.features
|
||||
: [geoJsonData];
|
||||
|
||||
const miskinFeatures = features.filter(f => isMiskinFeature(f));
|
||||
const otherFeatures = features.filter(f => !isMiskinFeature(f));
|
||||
|
||||
// ---- Proses fitur miskin: Simpan ke DB, lalu tampilkan seperti penduduk miskin ----
|
||||
if (miskinFeatures.length > 0) {
|
||||
const bulkData = miskinFeatures.map(f => {
|
||||
const props = f.properties || {};
|
||||
const coords = f.geometry.coordinates;
|
||||
return {
|
||||
nama: props.nama || props.name || props.penduduk || 'Data Impor',
|
||||
kategori_bantuan: props.kategori_bantuan || props.kategori || props.bantuan || 'Makan',
|
||||
jumlah_jiwa: parseInt(props.jumlah_jiwa || props.jumlah || props.jiwa || 1, 10) || 1,
|
||||
lat: parseFloat(coords[1]),
|
||||
lng: parseFloat(coords[0])
|
||||
};
|
||||
});
|
||||
|
||||
fetch('api/penduduk_miskin/bulk_create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(bulkData)
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// Reload layer penduduk miskin agar marker tampil dengan edit/hapus/log bantuan
|
||||
if (typeof loadPendudukMiskin === 'function') {
|
||||
loadPendudukMiskin();
|
||||
}
|
||||
if (typeof window.refreshActivePanel === 'function') {
|
||||
window.refreshActivePanel();
|
||||
}
|
||||
showToast(data.message || 'Data berhasil diimpor ke layer Penduduk Miskin.', 'success');
|
||||
} else {
|
||||
showToast(data.message || 'Gagal menyimpan data miskin.', 'error');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
showToast('Gagal terhubung ke server saat menyimpan data miskin.', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Proses fitur non-miskin: Render sebagai layer GeoJSON biasa ----
|
||||
if (otherFeatures.length > 0) {
|
||||
const layerId = 'gj_' + (++geoJsonCounter);
|
||||
const individualLayer = L.featureGroup();
|
||||
|
||||
const otherGeoJson = {
|
||||
type: 'FeatureCollection',
|
||||
features: otherFeatures
|
||||
};
|
||||
|
||||
L.geoJSON(otherGeoJson, {
|
||||
pointToLayer: function(feature, latlng) {
|
||||
const props = feature.properties || {};
|
||||
let emoji = props.emoji;
|
||||
|
||||
if (!emoji) {
|
||||
const text = JSON.stringify(props).toLowerCase();
|
||||
if (text.includes('spbu')) emoji = '⛽';
|
||||
else if (text.includes('masjid')) emoji = '🕌';
|
||||
else if (text.includes('gereja')) emoji = '⛪';
|
||||
else if (text.includes('vihara')) emoji = '🪷';
|
||||
else if (text.includes('pura')) emoji = '🛕';
|
||||
else if (text.includes('kelenteng')) emoji = '🏮';
|
||||
else emoji = '📍';
|
||||
}
|
||||
|
||||
let cls = 'miskin-out';
|
||||
if (emoji === '⛽') cls = 'spbu-24';
|
||||
else if (['🕌','⛪','🛕','🪷','🏮'].includes(emoji)) cls = 'ibadah';
|
||||
|
||||
const icon = L.divIcon({
|
||||
className: '',
|
||||
html: `<div class="emoji-marker"><div class="bubble ${cls}"><span>${emoji}</span></div></div>`,
|
||||
iconSize: [38, 38],
|
||||
iconAnchor: [19, 38],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
return L.marker(latlng, { icon: icon });
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
if (feature.properties) {
|
||||
let popupContent = '<h4>Informasi Feature</h4><ul style="list-style:none; padding:0; font-size:12px; margin: 0;">';
|
||||
for (let key in feature.properties) {
|
||||
popupContent += `<li><strong>${key}:</strong> ${feature.properties[key]}</li>`;
|
||||
}
|
||||
popupContent += '</ul>';
|
||||
layer.bindPopup(popupContent);
|
||||
}
|
||||
},
|
||||
style: function(feature) {
|
||||
return {
|
||||
color: (feature.properties && feature.properties.color) || '#3388ff',
|
||||
weight: 2,
|
||||
fillOpacity: 0.4
|
||||
};
|
||||
}
|
||||
}).addTo(individualLayer);
|
||||
|
||||
individualLayer.addTo(map);
|
||||
geoJsonLayers[layerId] = individualLayer;
|
||||
|
||||
// Tambahkan checkbox ke UI
|
||||
const container = document.getElementById('geoJsonLayersContainer');
|
||||
const label = document.createElement('label');
|
||||
label.className = 'layer-option';
|
||||
label.innerHTML = `<input type="checkbox" id="chk_${layerId}" checked> ${fileName}`;
|
||||
|
||||
label.querySelector('input').addEventListener('change', function(e) {
|
||||
if (e.target.checked) {
|
||||
map.addLayer(geoJsonLayers[layerId]);
|
||||
} else {
|
||||
map.removeLayer(geoJsonLayers[layerId]);
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
|
||||
if (miskinFeatures.length === 0) {
|
||||
showToast('File GeoJSON berhasil dimuat!', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
// Reset input file agar bisa import file yang sama
|
||||
fileGeoJson.value = '';
|
||||
|
||||
} catch (error) {
|
||||
showToast('Gagal memproses file GeoJSON. Pastikan format valid.', 'error');
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
// --- Fitur Geolocation ---
|
||||
|
||||
// Tambahkan tombol di dalam wadah zoom control
|
||||
const geoBtn = document.createElement('button');
|
||||
geoBtn.className = 'custom-layer-btn';
|
||||
geoBtn.style.position = 'relative';
|
||||
geoBtn.style.top = '0';
|
||||
geoBtn.style.left = '0';
|
||||
geoBtn.style.right = 'auto';
|
||||
geoBtn.innerHTML = '<i class="fas fa-crosshairs fa-lg"></i>';
|
||||
geoBtn.title = 'Lokasi Saya';
|
||||
document.querySelector('.custom-zoom-control').appendChild(geoBtn);
|
||||
|
||||
let userMarker = null;
|
||||
|
||||
geoBtn.addEventListener('click', function() {
|
||||
map.locate({setView: true, maxZoom: 16});
|
||||
});
|
||||
|
||||
map.on('locationfound', function(e) {
|
||||
if (userMarker) {
|
||||
map.removeLayer(userMarker);
|
||||
}
|
||||
userMarker = L.marker(e.latlng).addTo(map)
|
||||
.bindPopup("Anda berada di sini!").openPopup();
|
||||
});
|
||||
|
||||
map.on('locationerror', function(e) {
|
||||
alert("Gagal mendapatkan lokasi Anda.");
|
||||
});
|
||||
@@ -0,0 +1,916 @@
|
||||
// --- 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();
|
||||
@@ -0,0 +1,242 @@
|
||||
// Inisialisasi Peta
|
||||
// Koordinat awal: [-0.0263, 109.3425] (Pontianak)
|
||||
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
|
||||
|
||||
// ===== Toast Notification =====
|
||||
window.showToast = function(message, type = 'success', duration = 3500) {
|
||||
const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
|
||||
const container = document.getElementById('toastContainer');
|
||||
if (!container) return;
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.innerHTML = `<span class="toast-icon">${icons[type] || 'ℹ️'}</span><span>${message}</span>`;
|
||||
container.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.add('hiding');
|
||||
toast.addEventListener('animationend', () => toast.remove());
|
||||
}, duration);
|
||||
};
|
||||
|
||||
// Custom Zoom Control Logic
|
||||
document.getElementById('zoomInBtn').addEventListener('click', function() { map.zoomIn(); });
|
||||
document.getElementById('zoomOutBtn').addEventListener('click', function() { map.zoomOut(); });
|
||||
|
||||
// Base Map dari OpenStreetMap
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
// Inisialisasi FeatureGroups untuk masing-masing layer
|
||||
const rumahIbadahLayer = L.featureGroup().addTo(map);
|
||||
const pendudukMiskinLayer = L.featureGroup().addTo(map);
|
||||
const geoJsonLayer = L.featureGroup().addTo(map);
|
||||
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchClear = document.getElementById('searchClear');
|
||||
|
||||
searchInput.addEventListener('focus', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
});
|
||||
|
||||
searchInput.addEventListener('input', function() {
|
||||
const val = this.value.toLowerCase();
|
||||
if (val.length > 0) {
|
||||
searchClear.style.visibility = 'visible';
|
||||
window.showSearchResults(val);
|
||||
} else {
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
searchClear.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
searchClear.style.visibility = 'hidden';
|
||||
document.getElementById('searchResults').style.display = 'none';
|
||||
searchInput.focus();
|
||||
});
|
||||
|
||||
window.showSearchResults = function(query) {
|
||||
const resultsContainer = document.getElementById('searchResults');
|
||||
resultsContainer.innerHTML = '';
|
||||
let results = [];
|
||||
|
||||
|
||||
|
||||
if (typeof rumahIbadahLayer !== 'undefined') {
|
||||
rumahIbadahLayer.eachLayer(layer => {
|
||||
if (layer.ibadahData && layer.ibadahData.nama && layer.ibadahData.nama.toLowerCase().includes(query)) {
|
||||
if(layer instanceof L.Marker) {
|
||||
results.push({ type: 'Rumah Ibadah', nama: layer.ibadahData.nama, layer: layer });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px 15px; color: #666; font-size: 14px;">Tidak ada hasil</div>';
|
||||
} else {
|
||||
results.forEach(res => {
|
||||
const item = document.createElement('div');
|
||||
item.style.padding = '10px 15px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid #eee';
|
||||
item.style.fontSize = '14px';
|
||||
item.innerHTML = `<strong>${res.type}:</strong> ${res.nama}`;
|
||||
item.addEventListener('mouseenter', () => item.style.backgroundColor = '#f8f9fa');
|
||||
item.addEventListener('mouseleave', () => item.style.backgroundColor = 'white');
|
||||
item.addEventListener('click', () => {
|
||||
if (res.layer instanceof L.Marker) {
|
||||
map.setView(res.layer.getLatLng(), 17);
|
||||
} else if (res.layer.getBounds) {
|
||||
map.fitBounds(res.layer.getBounds());
|
||||
}
|
||||
res.layer.openPopup();
|
||||
resultsContainer.style.display = 'none';
|
||||
});
|
||||
resultsContainer.appendChild(item);
|
||||
});
|
||||
}
|
||||
resultsContainer.style.display = 'block';
|
||||
};
|
||||
|
||||
// UI Logic: Custom Layer Control Toggle
|
||||
const layerBtn = document.getElementById('layerBtn');
|
||||
const layerPanel = document.getElementById('layerPanel');
|
||||
|
||||
layerBtn.addEventListener('click', function() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
layerPanel.style.display = layerPanel.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
|
||||
// Sembunyikan layer panel saat klik di peta
|
||||
map.on('click', function() {
|
||||
layerPanel.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
// Logic untuk Toggle Layer Visibility
|
||||
document.getElementById('layerRumahIbadah').addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(rumahIbadahLayer) : map.removeLayer(rumahIbadahLayer);
|
||||
});
|
||||
document.getElementById('layerMiskin').addEventListener('change', function(e) {
|
||||
e.target.checked ? map.addLayer(pendudukMiskinLayer) : map.removeLayer(pendudukMiskinLayer);
|
||||
});
|
||||
|
||||
// --- Sub-layer Toggle (expand/collapse) ---
|
||||
window.toggleSubLayer = function(subId, iconEl) {
|
||||
const sub = document.getElementById(subId);
|
||||
if (!sub) return;
|
||||
const isHidden = sub.style.display === 'none';
|
||||
sub.style.display = isHidden ? '' : 'none';
|
||||
iconEl.classList.toggle('collapsed', !isHidden);
|
||||
};
|
||||
|
||||
// --- Sub-layer Filter ---
|
||||
window.applySubFilter = function(type) {
|
||||
|
||||
if (type === 'miskin') {
|
||||
const checked = [...document.querySelectorAll('.sub-miskin:checked')].map(el => el.value);
|
||||
pendudukMiskinLayer.eachLayer(layer => {
|
||||
if (!layer.miskinData) return;
|
||||
if (checked.includes(layer.miskinData.kategori_bantuan)) {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = '');
|
||||
} else {
|
||||
layer.getElement && layer.getElement() && (layer.getElement().style.display = 'none');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- Modal Logic ---
|
||||
const unifiedModal = document.getElementById('unifiedModal');
|
||||
const modalTitle = document.getElementById('modalTitle');
|
||||
const modalBody = document.getElementById('modalBody');
|
||||
|
||||
window.openModal = function(title, bodyHTML, saveCallback) {
|
||||
modalTitle.textContent = title;
|
||||
modalBody.innerHTML = bodyHTML;
|
||||
unifiedModal.classList.add('show');
|
||||
|
||||
// Ambil elemen tombol save terbaru dari DOM
|
||||
const currentSaveBtn = document.getElementById('modalSaveBtn');
|
||||
|
||||
// Clone untuk menghapus semua event listener lama
|
||||
const newSaveBtn = currentSaveBtn.cloneNode(true);
|
||||
currentSaveBtn.parentNode.replaceChild(newSaveBtn, currentSaveBtn);
|
||||
|
||||
newSaveBtn.addEventListener('click', saveCallback);
|
||||
};
|
||||
|
||||
window.closeModal = function() {
|
||||
unifiedModal.classList.remove('show');
|
||||
};
|
||||
|
||||
const confirmModal = document.getElementById('confirmModal');
|
||||
const confirmMessage = document.getElementById('confirmMessage');
|
||||
|
||||
window.openConfirmModal = function(msg, confirmCallback) {
|
||||
confirmMessage.textContent = msg;
|
||||
confirmModal.classList.add('show');
|
||||
|
||||
const currentYesBtn = document.getElementById('confirmYesBtn');
|
||||
const newYesBtn = currentYesBtn.cloneNode(true);
|
||||
currentYesBtn.parentNode.replaceChild(newYesBtn, currentYesBtn);
|
||||
|
||||
newYesBtn.addEventListener('click', function() {
|
||||
confirmModal.classList.remove('show');
|
||||
confirmCallback();
|
||||
});
|
||||
};
|
||||
|
||||
window.closeConfirmModal = function() {
|
||||
confirmModal.classList.remove('show');
|
||||
};
|
||||
|
||||
// --- Action Menu Logic (replaced by left sidebar) ---
|
||||
|
||||
window.currentAddMode = null; // 'rumah_ibadah' atau 'miskin_click'
|
||||
|
||||
window.activateAddMode = function(mode) {
|
||||
// Toggle: jika mode yang sama diklik lagi, batalkan
|
||||
if (window.currentAddMode === mode) {
|
||||
window.deactivateAddMode();
|
||||
return;
|
||||
}
|
||||
|
||||
window.currentAddMode = mode;
|
||||
|
||||
// Tooltip
|
||||
const tooltips = {
|
||||
'rumah_ibadah': 'Klik untuk Tambah Rumah Ibadah'
|
||||
};
|
||||
if (window.cursorTooltip && tooltips[mode]) window.cursorTooltip.textContent = tooltips[mode];
|
||||
|
||||
// Ubah kursor map menjadi crosshair
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
};
|
||||
|
||||
window.deactivateAddMode = function() {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
|
||||
};
|
||||
|
||||
// --- Custom Tooltip Cursor ---
|
||||
window.cursorTooltip = document.createElement('div');
|
||||
window.cursorTooltip.className = 'custom-cursor-tooltip';
|
||||
document.body.appendChild(window.cursorTooltip);
|
||||
|
||||
// Gunakan document-level mousemove agar selalu terpicu
|
||||
// bahkan saat Leaflet Draw overlay aktif menangkap event map
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
if (window.currentAddMode) {
|
||||
window.cursorTooltip.style.display = 'block';
|
||||
window.cursorTooltip.style.left = e.pageX + 15 + 'px';
|
||||
window.cursorTooltip.style.top = e.pageY + 15 + 'px';
|
||||
} else {
|
||||
window.cursorTooltip.style.display = 'none';
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,465 @@
|
||||
// ===== Panel Management =====
|
||||
// Mengelola Left Sidebar Toggle + Right Panel + Layer Control
|
||||
|
||||
(function () {
|
||||
// ---- Konstanta Jenis Ibadah ----
|
||||
const IBADAH_EMOJI = {
|
||||
'Masjid': '🕌',
|
||||
'Gereja': '⛪',
|
||||
'Pura': '🛕',
|
||||
'Vihara': '🪷',
|
||||
'Kelenteng': '🏮',
|
||||
};
|
||||
window.IBADAH_EMOJI = IBADAH_EMOJI;
|
||||
|
||||
// ---- State ----
|
||||
let activePanel = null;
|
||||
|
||||
// ---- Elements ----
|
||||
const sidebarToggleBtn = document.getElementById('sidebarToggleBtn');
|
||||
const sidebarToggleIcon = document.getElementById('sidebarToggleIcon');
|
||||
const leftSidebar = document.getElementById('leftSidebar');
|
||||
const rightPanel = document.getElementById('rightPanel');
|
||||
const rightPanelTitle = document.getElementById('rightPanelTitle');
|
||||
const rightPanelActions = document.getElementById('rightPanelActions');
|
||||
const rightPanelBody = document.getElementById('rightPanelBody');
|
||||
const rightPanelClose = document.getElementById('rightPanelClose');
|
||||
|
||||
// ---- Sidebar Toggle ----
|
||||
sidebarToggleBtn.addEventListener('click', function () {
|
||||
const isOpen = leftSidebar.style.display !== 'none';
|
||||
if (isOpen) {
|
||||
leftSidebar.style.display = 'none';
|
||||
sidebarToggleBtn.classList.remove('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-right';
|
||||
} else {
|
||||
leftSidebar.style.display = 'flex';
|
||||
sidebarToggleBtn.classList.add('open');
|
||||
sidebarToggleIcon.className = 'fas fa-chevron-left';
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Sidebar Buttons ----
|
||||
document.querySelectorAll('.sidebar-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
const panel = this.dataset.panel;
|
||||
if (activePanel === panel) {
|
||||
closePanel();
|
||||
} else {
|
||||
openPanel(panel);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
rightPanelClose.addEventListener('click', closePanel);
|
||||
|
||||
// ---- Open / Close Panel ----
|
||||
function openPanel(panel) {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = panel;
|
||||
rightPanel.classList.add('open');
|
||||
document.body.classList.add('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
const activeBtn = document.querySelector(`.sidebar-btn[data-panel="${panel}"]`);
|
||||
if (activeBtn) activeBtn.classList.add('active');
|
||||
renderPanel(panel);
|
||||
}
|
||||
|
||||
function closePanel() {
|
||||
if (typeof window.deactivateAddMode === 'function') window.deactivateAddMode();
|
||||
activePanel = null;
|
||||
rightPanel.classList.remove('open');
|
||||
document.body.classList.remove('right-panel-open');
|
||||
document.querySelectorAll('.sidebar-btn').forEach(b => b.classList.remove('active'));
|
||||
}
|
||||
|
||||
window.refreshActivePanel = function () {
|
||||
if (activePanel && activePanel !== 'layer') renderPanel(activePanel);
|
||||
};
|
||||
|
||||
// ---- Render Panel by Type ----
|
||||
function renderPanel(type) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Memuat data...</div>';
|
||||
rightPanelActions.innerHTML = '';
|
||||
|
||||
switch (type) {
|
||||
case 'ibadah': renderIbadahPanel(); break;
|
||||
case 'miskin': renderMiskinPanel(); break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==========================
|
||||
// ===== Ibadah Panel =======
|
||||
// ==========================
|
||||
function renderIbadahPanel() {
|
||||
rightPanelTitle.textContent = '🕌 Rumah Ibadah';
|
||||
const isAdmin = !!(window.currentUser && window.currentUser.role === 'admin');
|
||||
if (isAdmin) {
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelAddIbadah">
|
||||
<i class="fas fa-plus"></i> Tambah Ibadah
|
||||
</button>`;
|
||||
document.getElementById('btnPanelAddIbadah').addEventListener('click', () => {
|
||||
window.activateAddMode('rumah_ibadah');
|
||||
});
|
||||
} else {
|
||||
rightPanelActions.innerHTML = '';
|
||||
}
|
||||
|
||||
const items = [];
|
||||
rumahIbadahLayer.eachLayer(l => {
|
||||
if (l.ibadahData && l instanceof L.Marker) items.push(l.ibadahData);
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data rumah ibadah</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
items.forEach(d => {
|
||||
const emoji = IBADAH_EMOJI[d.jenis] || '🕌';
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
let actionButtons = '';
|
||||
if (isAdmin) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon card-icon-ibadah">${emoji}</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">${escHtml(d.jenis || 'Masjid')} • 📡 ${d.radius} m</div>
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>`;
|
||||
if (isAdmin) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditIbadahModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteIbadah(d.id); });
|
||||
}
|
||||
card.addEventListener('click', () => flyToIbadah(d.id));
|
||||
rightPanelBody.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function flyToIbadah(id) {
|
||||
rumahIbadahLayer.eachLayer(l => {
|
||||
if (l.ibadahData && l.ibadahData.id == id && l instanceof L.Marker) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================
|
||||
// ===== Miskin Panel =======
|
||||
// ==========================
|
||||
function renderMiskinPanel() {
|
||||
rightPanelTitle.textContent = '🏠 Penduduk Miskin';
|
||||
const canManage = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
if (canManage) {
|
||||
const isPengelola = window.currentUser.role === 'pengelola';
|
||||
const addLabel = isPengelola
|
||||
? '<i class="fas fa-plus"></i> Tambah <span style="font-size:10px;opacity:0.8;">(dalam radius)</span>'
|
||||
: '<i class="fas fa-plus"></i> Tambah';
|
||||
rightPanelActions.innerHTML = `
|
||||
<button class="btn-panel-add" id="btnPanelTambahMiskin">
|
||||
${addLabel}
|
||||
</button>
|
||||
<button class="btn-panel-import" id="btnPanelImportMiskin">
|
||||
<i class="fas fa-file-import"></i> Impor
|
||||
</button>`;
|
||||
|
||||
document.getElementById('btnPanelTambahMiskin').addEventListener('click', () => {
|
||||
window.currentAddMode = 'miskin_click';
|
||||
document.getElementById('map').style.cursor = 'crosshair';
|
||||
if (window.cursorTooltip) {
|
||||
const tipText = isPengelola
|
||||
? 'Klik di dalam radius ibadah Anda untuk tambah penduduk'
|
||||
: 'Klik di peta untuk tentukan lokasi penduduk miskin';
|
||||
window.cursorTooltip.textContent = tipText;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('btnPanelImportMiskin').addEventListener('click', () => {
|
||||
if (typeof window.openImportMiskinModal === 'function') {
|
||||
window.openImportMiskinModal();
|
||||
} else {
|
||||
alert('Fitur impor belum siap.');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
rightPanelActions.innerHTML = '';
|
||||
}
|
||||
|
||||
const all = [];
|
||||
pendudukMiskinLayer.eachLayer(l => { if (l.miskinData) all.push(l.miskinData); });
|
||||
|
||||
if (!all.length) {
|
||||
rightPanelBody.innerHTML = '<div class="panel-empty">Belum ada data penduduk miskin</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const inRadius = (d) => {
|
||||
if (typeof ibadahDataList === 'undefined') return false;
|
||||
for (const ib of ibadahDataList) {
|
||||
const dist = L.latLng(d.lat, d.lng).distanceTo(L.latLng(ib.lat, ib.lng));
|
||||
if (dist <= ib.radius) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
rightPanelBody.innerHTML = '';
|
||||
|
||||
const terurusItems = all.filter(d => inRadius(d));
|
||||
const tidakItems = all.filter(d => !inRadius(d));
|
||||
|
||||
if (terurusItems.length) {
|
||||
const h1 = document.createElement('div');
|
||||
h1.className = 'panel-section-title';
|
||||
h1.innerHTML = `✅ Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${terurusItems.length})</span>`;
|
||||
rightPanelBody.appendChild(h1);
|
||||
terurusItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, true)));
|
||||
}
|
||||
|
||||
if (tidakItems.length) {
|
||||
const h2 = document.createElement('div');
|
||||
h2.className = 'panel-section-title';
|
||||
h2.innerHTML = `⚠️ Tidak Terurus <span style="font-weight:400;text-transform:none;font-size:10px;">(${tidakItems.length})</span>`;
|
||||
rightPanelBody.appendChild(h2);
|
||||
tidakItems.forEach(d => rightPanelBody.appendChild(buildMiskinCard(d, false)));
|
||||
}
|
||||
}
|
||||
|
||||
function buildMiskinCard(d, terurus) {
|
||||
// Pengelola hanya boleh edit/hapus penduduk yang dalam radius ibadahnya
|
||||
const isPengelolaOrAdmin = !!(window.currentUser && (window.currentUser.role === 'admin' || window.currentUser.role === 'pengelola'));
|
||||
const canManage = isPengelolaOrAdmin && (typeof window.isInMyIbadahRadius === 'function'
|
||||
? window.isInMyIbadahRadius(d.lat, d.lng)
|
||||
: true);
|
||||
const iconCls = terurus ? 'card-icon-miskin-in' : 'card-icon-miskin-out';
|
||||
const badge = d.kategori_bantuan === 'Makan'
|
||||
? '<span class="data-card-badge badge-orange">Bantuan Makan</span>'
|
||||
: '<span class="data-card-badge badge-blue">Pemberdayaan</span>';
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'data-card';
|
||||
card.dataset.miskinId = d.id;
|
||||
|
||||
let actionButtons = '';
|
||||
if (canManage) {
|
||||
actionButtons = `
|
||||
<div class="data-card-actions">
|
||||
<button class="btn-card-action btn-card-edit" title="Edit"><i class="fas fa-pen"></i></button>
|
||||
<button class="btn-card-action btn-card-delete" title="Hapus"><i class="fas fa-trash"></i></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
let fotoHtml = '';
|
||||
if (d.foto_rumah || d.foto_kk) {
|
||||
fotoHtml += `<div class="expand-row-photos" style="display:flex; gap:10px; margin-top:8px; justify-content: space-around;">`;
|
||||
if (d.foto_rumah) {
|
||||
fotoHtml += `
|
||||
<div style="text-align:center;">
|
||||
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto Rumah</span>
|
||||
<a href="uploads/${d.foto_rumah}" target="_blank">
|
||||
<img src="uploads/${d.foto_rumah}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
if (d.foto_kk) {
|
||||
fotoHtml += `
|
||||
<div style="text-align:center;">
|
||||
<span style="font-size:10px; display:block; font-weight:500; color:#555;">Foto KK</span>
|
||||
<a href="uploads/${d.foto_kk}" target="_blank">
|
||||
<img src="uploads/${d.foto_kk}" style="width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid #ddd; margin-top:3px;" />
|
||||
</a>
|
||||
</div>`;
|
||||
}
|
||||
fotoHtml += `</div>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="data-card-main">
|
||||
<div class="data-card-icon ${iconCls}">🏠</div>
|
||||
<div class="data-card-info">
|
||||
<div class="data-card-name">${escHtml(d.nama)}</div>
|
||||
<div class="data-card-sub">👨👩👧 ${d.jumlah_jiwa || '-'} jiwa</div>
|
||||
${badge}
|
||||
</div>
|
||||
${actionButtons}
|
||||
</div>
|
||||
<div class="data-card-expand">
|
||||
<div class="expand-row"><span>Kepala Keluarga</span><span>${escHtml(d.nama)}</span></div>
|
||||
<div class="expand-row"><span>Kategori Bantuan</span><span>${escHtml(d.kategori_bantuan)}</span></div>
|
||||
<div class="expand-row"><span>Jumlah Jiwa</span><span>${d.jumlah_jiwa || '-'}</span></div>
|
||||
<div class="expand-row"><span>Status</span><span>${terurus ? 'Terurus ✅' : 'Tidak Terurus ⚠️'}</span></div>
|
||||
${fotoHtml}
|
||||
<button class="btn-log-bantuan" data-id="${d.id}" data-nama="${escHtml(d.nama)}">
|
||||
<i class="fas fa-history"></i> Lihat Log Bantuan
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
if (canManage) {
|
||||
card.querySelector('.btn-card-edit').addEventListener('click', (e) => { e.stopPropagation(); openEditMiskinModal(d.id); });
|
||||
card.querySelector('.btn-card-delete').addEventListener('click', (e) => { e.stopPropagation(); deleteMiskin(d.id); });
|
||||
}
|
||||
card.querySelector('.btn-log-bantuan').addEventListener('click', (e) => { e.stopPropagation(); openLogBantuan(d.id, d.nama); });
|
||||
|
||||
card.querySelector('.data-card-main').addEventListener('click', () => {
|
||||
card.classList.toggle('expanded');
|
||||
flyToMiskin(d.id);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function flyToMiskin(id) {
|
||||
pendudukMiskinLayer.eachLayer(l => {
|
||||
if (l.miskinData && l.miskinData.id == id) {
|
||||
map.setView(l.getLatLng(), 17); l.openPopup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---- Hook: klik peta saat mode miskin_click ----
|
||||
// Ditangkap oleh kemiskinan.js via contextmenu — mode ini pakai klik kiri
|
||||
map.on('click', function(e) {
|
||||
if (window.currentAddMode === 'miskin_click') {
|
||||
window.currentAddMode = null;
|
||||
document.getElementById('map').style.cursor = '';
|
||||
if (window.cursorTooltip) window.cursorTooltip.style.display = 'none';
|
||||
// Buka form tambah miskin
|
||||
if (typeof window.formAddMiskin === 'function') {
|
||||
window.formAddMiskin(e.latlng.lat, e.latlng.lng);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ==========================
|
||||
// ===== Log Bantuan Modal ==
|
||||
// ==========================
|
||||
const logModal = document.getElementById('logBantuanModal');
|
||||
const logTitle = document.getElementById('logBantuanTitle');
|
||||
const logList = document.getElementById('logBantuanList');
|
||||
const logSaveBtn = document.getElementById('logSaveBtn');
|
||||
const logIbadahSel = document.getElementById('logIbadahId');
|
||||
let _logMiskinId = null;
|
||||
|
||||
window.openLogBantuan = function (miskinId, nama) {
|
||||
_logMiskinId = miskinId;
|
||||
logTitle.textContent = `Log Bantuan — ${nama}`;
|
||||
logModal.classList.add('show');
|
||||
|
||||
const isAuth = !!window.currentUser;
|
||||
const formContainer = document.querySelector('#logBantuanModal .modal-body > div:last-child');
|
||||
const modalFooter = document.querySelector('#logBantuanModal .modal-footer');
|
||||
|
||||
if (isAuth) {
|
||||
if (formContainer) formContainer.style.display = 'block';
|
||||
if (modalFooter) modalFooter.style.display = 'flex';
|
||||
} else {
|
||||
if (formContainer) formContainer.style.display = 'none';
|
||||
if (modalFooter) modalFooter.style.display = 'none';
|
||||
}
|
||||
|
||||
// Isi dropdown ibadah
|
||||
logIbadahSel.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 = `${IBADAH_EMOJI[ib.jenis] || '🕌'} ${ib.nama}`;
|
||||
logIbadahSel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
if (window.currentUser && window.currentUser.role === 'pengelola') {
|
||||
logIbadahSel.value = window.currentUser.ibadah_id;
|
||||
logIbadahSel.disabled = true;
|
||||
} else {
|
||||
logIbadahSel.disabled = false;
|
||||
}
|
||||
|
||||
// Set tanggal default hari ini
|
||||
document.getElementById('logTanggal').valueAsDate = new Date();
|
||||
fetchLogBantuan(miskinId);
|
||||
};
|
||||
|
||||
window.closeLogBantuanModal = function () {
|
||||
logModal.classList.remove('show');
|
||||
_logMiskinId = null;
|
||||
};
|
||||
|
||||
function fetchLogBantuan(miskinId) {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Memuat...</div>';
|
||||
fetch(`api/log_bantuan/read.php?miskin_id=${miskinId}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (!data.data || !data.data.length) {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px;">Belum ada riwayat bantuan</div>';
|
||||
return;
|
||||
}
|
||||
logList.innerHTML = '';
|
||||
data.data.forEach(log => {
|
||||
const tgl = new Date(log.tanggal).toLocaleDateString('id-ID', { day:'2-digit', month:'short', year:'numeric' });
|
||||
const item = document.createElement('div');
|
||||
item.className = 'log-item';
|
||||
item.innerHTML = `
|
||||
<div class="log-dot"></div>
|
||||
<div class="log-info">
|
||||
<div class="log-type">${escHtml(log.tipe_bantuan)}</div>
|
||||
<div class="log-meta">${IBADAH_EMOJI[log.jenis_ibadah] || '🕌'} ${escHtml(log.nama_ibadah)} • ${tgl}</div>
|
||||
${log.keterangan ? `<div class="log-keterangan">${escHtml(log.keterangan)}</div>` : ''}
|
||||
</div>`;
|
||||
logList.appendChild(item);
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
logList.innerHTML = '<div class="panel-empty" style="padding:15px; color:#ef4444;">Gagal memuat data</div>';
|
||||
});
|
||||
}
|
||||
|
||||
logSaveBtn.addEventListener('click', function () {
|
||||
const ibadah_id = logIbadahSel.value;
|
||||
const tipe_bantuan = document.getElementById('logTipeBantuan').value;
|
||||
const tanggal = document.getElementById('logTanggal').value;
|
||||
const keterangan = document.getElementById('logKeterangan').value;
|
||||
|
||||
if (!ibadah_id) { alert('Pilih rumah ibadah terlebih dahulu!'); return; }
|
||||
if (!tanggal) { alert('Tanggal harus diisi!'); return; }
|
||||
|
||||
fetch('api/log_bantuan/create.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ miskin_id: _logMiskinId, ibadah_id, tipe_bantuan, tanggal, keterangan })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('logKeterangan').value = '';
|
||||
fetchLogBantuan(_logMiskinId);
|
||||
} else {
|
||||
alert(data.message || 'Gagal menyimpan');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Utility ----
|
||||
function escHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
// ---- Expose ----
|
||||
window.openRightPanel = openPanel;
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user