mengubah sistem menjadi lebih terfokus ke poverty
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
let allData = [];
|
||||
let allRI = [];
|
||||
let filteredData = [];
|
||||
let sortField = 'nama';
|
||||
let sortDir = 'asc';
|
||||
const PAGE_SIZE = 20;
|
||||
let currentPage = 1;
|
||||
|
||||
document.addEventListener('auth-ready', function (e) {
|
||||
const user = e.detail;
|
||||
document.body.classList.add('auth-loaded', 'role-' + user.role);
|
||||
|
||||
document.getElementById('user-display-name').textContent = user.username;
|
||||
const roleMap = { 'admin': 'Admin', 'petugas_lapangan': 'Rumah Ibadah', 'guest': 'Guest' };
|
||||
document.getElementById('user-display-role').textContent = roleMap[user.role] || user.role;
|
||||
|
||||
// Hide admin-nav for non-admin
|
||||
document.querySelectorAll('.admin-nav').forEach(el => {
|
||||
if (user.role !== 'admin') el.style.display = 'none';
|
||||
});
|
||||
|
||||
loadData(user);
|
||||
});
|
||||
|
||||
async function loadData(user) {
|
||||
try {
|
||||
const [pmRes, riRes] = await Promise.all([
|
||||
fetch('api/penduduk_miskin.php'),
|
||||
fetch('api/rumah_ibadah.php')
|
||||
]);
|
||||
const [pmData, riData] = await Promise.all([pmRes.json(), riRes.json()]);
|
||||
|
||||
allData = pmData.data || [];
|
||||
allRI = riData.data || [];
|
||||
|
||||
// For petugas_lapangan: filter to only show residents in their radius
|
||||
if (user && user.role === 'petugas_lapangan') {
|
||||
const myRiId = String(user.rumah_ibadah_id);
|
||||
allData = allData.filter(p => p.status_proximity === 'dalam_radius' && String(p.rumah_ibadah_id) === myRiId);
|
||||
}
|
||||
|
||||
// Populate RI filter
|
||||
const riFilter = document.getElementById('filter-ri');
|
||||
allRI.forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.nama;
|
||||
riFilter.appendChild(opt);
|
||||
});
|
||||
|
||||
updateSummary();
|
||||
applyFilters();
|
||||
} catch (err) {
|
||||
document.getElementById('pm-tbody').innerHTML = `<tr><td colspan="7" style="text-align:center;padding:40px;color:#ef4444">Gagal memuat data: ${esc(err.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateSummary() {
|
||||
const total = allData.length;
|
||||
const jiwa = allData.reduce((s, p) => s + (parseInt(p.jumlah_anggota, 10) || 0), 0);
|
||||
const dalam = allData.filter(p => p.status_proximity === 'dalam_radius').length;
|
||||
const luar = allData.filter(p => p.status_proximity !== 'dalam_radius').length;
|
||||
const withBantuan = allData.filter(p => p.total_bantuan > 0).length;
|
||||
document.getElementById('sum-total').textContent = total;
|
||||
document.getElementById('sum-jiwa').textContent = jiwa;
|
||||
document.getElementById('sum-dalam').textContent = dalam;
|
||||
document.getElementById('sum-luar').textContent = luar;
|
||||
document.getElementById('sum-bantuan').textContent = withBantuan;
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
const query = document.getElementById('search-input').value.toLowerCase().trim();
|
||||
const proximity = document.getElementById('filter-proximity').value;
|
||||
const riId = document.getElementById('filter-ri').value;
|
||||
const bantuan = document.getElementById('filter-bantuan').value;
|
||||
|
||||
filteredData = allData.filter(p => {
|
||||
const matchQ = !query || p.nama.toLowerCase().includes(query) || (p.nik && p.nik.includes(query)) || (p.alamat && p.alamat.toLowerCase().includes(query)) || (p.nama_rumah_ibadah && p.nama_rumah_ibadah.toLowerCase().includes(query));
|
||||
const matchProx = !proximity || p.status_proximity === proximity;
|
||||
const matchRI = !riId || String(p.rumah_ibadah_id) === riId;
|
||||
const matchBantuan = !bantuan || (bantuan === 'pernah' ? p.total_bantuan > 0 : p.total_bantuan === 0);
|
||||
return matchQ && matchProx && matchRI && matchBantuan;
|
||||
});
|
||||
|
||||
// Sort
|
||||
filteredData.sort((a, b) => {
|
||||
let va = a[sortField], vb = b[sortField];
|
||||
if (typeof va === 'string') va = va.toLowerCase();
|
||||
if (typeof vb === 'string') vb = vb.toLowerCase();
|
||||
if (va < vb) return sortDir === 'asc' ? -1 : 1;
|
||||
if (va > vb) return sortDir === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentPage = 1;
|
||||
document.getElementById('filter-count').textContent = `${filteredData.length} data`;
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function sortBy(field) {
|
||||
if (sortField === field) {
|
||||
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortField = field;
|
||||
sortDir = 'asc';
|
||||
}
|
||||
// Update sort icons
|
||||
document.querySelectorAll('.pm-table thead th').forEach(th => {
|
||||
th.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
const ths = document.querySelectorAll('.pm-table thead th');
|
||||
const fieldIdx = { 'nama': 0, 'nik': 1, 'jumlah_anggota': 2, 'total_bantuan': 5 };
|
||||
if (fieldIdx[field] !== undefined) {
|
||||
ths[fieldIdx[field]].classList.add(sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
}
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const tbody = document.getElementById('pm-tbody');
|
||||
const total = filteredData.length;
|
||||
|
||||
if (total === 0) {
|
||||
tbody.innerHTML = `
|
||||
<tr><td colspan="7">
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🔍</div>
|
||||
<h3>Tidak ada data</h3>
|
||||
<p>Coba ubah kriteria pencarian atau filter.</p>
|
||||
</div>
|
||||
</td></tr>`;
|
||||
document.getElementById('pagination').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / PAGE_SIZE);
|
||||
if (currentPage > totalPages) currentPage = totalPages;
|
||||
const start = (currentPage - 1) * PAGE_SIZE;
|
||||
const slice = filteredData.slice(start, start + PAGE_SIZE);
|
||||
|
||||
tbody.innerHTML = slice.map((p, idx) => {
|
||||
const inR = p.status_proximity === 'dalam_radius';
|
||||
const hasPemb = p.bantuan_pemberdayaan > 0;
|
||||
const hasKons = p.bantuan_konsumtif > 0;
|
||||
return `
|
||||
<tr onclick="openDrawer(${p.id})">
|
||||
<td>
|
||||
<div class="cell-name">${esc(p.nama)}</div>
|
||||
${p.alamat ? `<div class="cell-sub">📍 ${esc(p.alamat.substring(0, 50))}${p.alamat.length > 50 ? '…' : ''}</div>` : ''}
|
||||
</td>
|
||||
<td><span style="font-family:monospace;font-size:11px">${p.nik ? esc(p.nik) : '<span style="color:#c4c4c4">—</span>'}</span></td>
|
||||
<td><strong>${p.jumlah_anggota}</strong> jiwa</td>
|
||||
<td><span class="badge ${inR ? 'badge-dalam' : 'badge-luar'}">${inR ? '✅ Dalam' : '❌ Luar'}</span></td>
|
||||
<td>
|
||||
${p.nama_rumah_ibadah ? `<span style="font-size:12px">⛪ ${esc(p.nama_rumah_ibadah)}</span>` : '<span style="color:#c4c4c4;font-size:11px">—</span>'}
|
||||
</td>
|
||||
<td>
|
||||
<div class="aid-pills">
|
||||
${p.total_bantuan === 0 ? '<span style="color:#c4c4c4;font-size:11px">Belum ada</span>' : ''}
|
||||
${hasPemb ? `<span class="badge badge-pemberdayaan">📚 ${p.bantuan_pemberdayaan}</span>` : ''}
|
||||
${hasKons ? `<span class="badge badge-konsumtif">🛍️ ${p.bantuan_konsumtif}</span>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
<td onclick="event.stopPropagation()">
|
||||
<div style="display:flex;gap:4px">
|
||||
<button class="btn btn-ghost btn-sm" style="padding:5px 8px;font-size:11px" onclick="openDrawer(${p.id})">👁 Detail</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Pagination
|
||||
const pag = document.getElementById('pagination');
|
||||
const pagInfo = document.getElementById('pagination-info');
|
||||
const pagBtns = document.getElementById('pagination-btns');
|
||||
pag.style.display = 'flex';
|
||||
pagInfo.textContent = `Menampilkan ${start + 1}–${Math.min(start + PAGE_SIZE, total)} dari ${total} data`;
|
||||
|
||||
// Build page buttons
|
||||
let btns = '';
|
||||
btns += `<button class="pg-btn" onclick="goPage(${currentPage - 1})" ${currentPage === 1 ? 'disabled' : ''}>‹</button>`;
|
||||
const rangeStart = Math.max(1, currentPage - 2);
|
||||
const rangeEnd = Math.min(totalPages, currentPage + 2);
|
||||
if (rangeStart > 1) btns += `<button class="pg-btn" onclick="goPage(1)">1</button>${rangeStart > 2 ? '<span style="padding:0 4px;color:#94a3b8">…</span>' : ''}`;
|
||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||
btns += `<button class="pg-btn ${i === currentPage ? 'active' : ''}" onclick="goPage(${i})">${i}</button>`;
|
||||
}
|
||||
if (rangeEnd < totalPages) btns += `${rangeEnd < totalPages - 1 ? '<span style="padding:0 4px;color:#94a3b8">…</span>' : ''}<button class="pg-btn" onclick="goPage(${totalPages})">${totalPages}</button>`;
|
||||
btns += `<button class="pg-btn" onclick="goPage(${currentPage + 1})" ${currentPage === totalPages ? 'disabled' : ''}>›</button>`;
|
||||
pagBtns.innerHTML = btns;
|
||||
}
|
||||
|
||||
function goPage(page) {
|
||||
const totalPages = Math.ceil(filteredData.length / PAGE_SIZE);
|
||||
if (page < 1 || page > totalPages) return;
|
||||
currentPage = page;
|
||||
renderTable();
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
async function openDrawer(id) {
|
||||
document.getElementById('detail-overlay').classList.add('open');
|
||||
document.getElementById('drawer-body').innerHTML = `
|
||||
<div class="loading-spinner-wrap">
|
||||
<span class="spinner" style="width:20px;height:20px;border-width:3px;"></span>
|
||||
<p style="margin-top:12px">Memuat detail…</p>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const [pmRes, logRes] = await Promise.all([
|
||||
fetch(`api/penduduk_miskin.php?id=${id}`),
|
||||
fetch(`api/log_bantuan.php?penduduk_miskin_id=${id}`)
|
||||
]);
|
||||
const [pmData, logData] = await Promise.all([pmRes.json(), logRes.json()]);
|
||||
if (pmData.status !== 'success') throw new Error(pmData.message);
|
||||
renderDrawer(pmData.data, logData.data || []);
|
||||
} catch (err) {
|
||||
document.getElementById('drawer-body').innerHTML = `<p style="color:#ef4444">Gagal memuat: ${esc(err.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDrawer(p, logs) {
|
||||
document.getElementById('drawer-title').textContent = p.nama;
|
||||
const inR = p.status_proximity === 'dalam_radius';
|
||||
|
||||
const isAdmin = window.currentUser && window.currentUser.role === 'admin';
|
||||
const isPetugas = window.currentUser && window.currentUser.role === 'petugas_lapangan';
|
||||
const isResidentInMyRadius = isPetugas && inR && String(p.rumah_ibadah_id) === String(window.currentUser.rumah_ibadah_id);
|
||||
const canEdit = isAdmin || isResidentInMyRadius;
|
||||
|
||||
document.getElementById('drawer-body').innerHTML = `
|
||||
<!-- Proximity status -->
|
||||
<div class="proximity-card ${inR ? 'dalam' : 'luar'}">
|
||||
${inR ? `✅ Dalam radius jangkauan <strong>${esc(p.nama_rumah_ibadah || 'rumah ibadah')}</strong>` : '❌ Di luar radius semua rumah ibadah'}
|
||||
</div>
|
||||
|
||||
<!-- Basic info -->
|
||||
<div class="section-heading">Informasi Pribadi</div>
|
||||
<div>
|
||||
<div class="info-row"><span class="info-label">Nama Lengkap</span><span class="info-val">${esc(p.nama)}</span></div>
|
||||
<div class="info-row"><span class="info-label">NIK</span><span class="info-val" style="font-family:monospace">${p.nik ? esc(p.nik) : '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">No. WhatsApp</span><span class="info-val">${p.no_wa ? esc(p.no_wa) : '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">Jumlah Anggota</span><span class="info-val">${p.jumlah_anggota} jiwa</span></div>
|
||||
<div class="info-row"><span class="info-label">Alamat</span><span class="info-val">${p.alamat ? esc(p.alamat) : '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">Koordinat</span><span class="info-val" style="font-family:monospace;font-size:11px">${p.latitude.toFixed(6)}, ${p.longitude.toFixed(6)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Terdata Sejak</span><span class="info-val">${new Date(p.created_at).toLocaleDateString('id-ID', { day: 'numeric', month: 'long', year: 'numeric' })}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Bantuan summary -->
|
||||
<div class="section-heading">Ringkasan Bantuan</div>
|
||||
<div>
|
||||
${p.ringkasan_bantuan && p.ringkasan_bantuan.length ? p.ringkasan_bantuan.map(rb => `
|
||||
<div class="info-row">
|
||||
<span class="info-label">${rb.tipe_bantuan === 'Pemberdayaan' ? '📚 Pemberdayaan' : '🛍️ Konsumtif'}</span>
|
||||
<span class="info-val">${rb.jumlah} kali</span>
|
||||
</div>
|
||||
`).join('') : '<div style="color:#94a3b8;font-size:13px;padding:8px 0">Belum pernah menerima bantuan.</div>'}
|
||||
</div>
|
||||
|
||||
<!-- Action buttons -->
|
||||
${canEdit ? `
|
||||
<div style="margin-top:16px;display:flex;gap:8px">
|
||||
<button class="btn btn-primary btn-sm" onclick="openLogBantuanFromDrawer(${p.id}, '${esc(p.nama)}')">📋 Catat Bantuan</button>
|
||||
<button class="btn btn-ghost btn-sm" onclick="window.location.href='?page=map'">✏️ Edit di Peta</button>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Log history -->
|
||||
${logs.length ? `
|
||||
<div class="section-heading">Riwayat Bantuan (${logs.length})</div>
|
||||
<div class="log-list">
|
||||
${logs.map(l => `
|
||||
<div class="log-entry">
|
||||
<div class="log-type-dot ${l.tipe_bantuan === 'Pemberdayaan' ? 'pem' : 'kon'}"></div>
|
||||
<div class="log-entry-info">
|
||||
<div class="log-entry-name">${l.tipe_bantuan === 'Pemberdayaan' ? '📚' : '🛍️'} ${esc(l.tipe_bantuan)}${l.sub_kategori ? ' — ' + esc(l.sub_kategori) : ''}</div>
|
||||
<div class="log-entry-detail">Dari: ${esc(l.nama_rumah_ibadah)}${l.nilai_bantuan ? ' · Rp' + new Intl.NumberFormat('id-ID').format(l.nilai_bantuan) : ''}${l.keterangan ? ' · ' + esc(l.keterangan) : ''}</div>
|
||||
</div>
|
||||
<div class="log-entry-date">${l.tanggal_bantuan}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function closeDrawer(e) {
|
||||
if (e && e.target !== document.getElementById('detail-overlay')) return;
|
||||
document.getElementById('detail-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
function openLogBantuanFromDrawer(pmId, pmNama) {
|
||||
// Redirect to peta with log bantuan opened
|
||||
window.location.href = '?page=map';
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
if (!filteredData.length) return toast('Tidak ada data untuk diekspor', 'error');
|
||||
const headers = ['Nama', 'NIK', 'No WA', 'Alamat', 'Jumlah Anggota', 'Status Proximity', 'Rumah Ibadah', 'Total Bantuan', 'Bantuan Pemberdayaan', 'Bantuan Konsumtif', 'Latitude', 'Longitude'];
|
||||
const rows = filteredData.map(p => [
|
||||
p.nama, p.nik || '', p.no_wa || '', p.alamat || '',
|
||||
p.jumlah_anggota, p.status_proximity, p.nama_rumah_ibadah || '',
|
||||
p.total_bantuan, p.bantuan_pemberdayaan, p.bantuan_konsumtif,
|
||||
p.latitude, p.longitude
|
||||
]);
|
||||
const csv = [headers, ...rows].map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n');
|
||||
const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `penduduk_miskin_${new Date().toISOString().slice(0, 10)}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast(`${filteredData.length} data berhasil diekspor`, 'success');
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function toast(msg, type = 'info') {
|
||||
const t = document.createElement('div');
|
||||
t.className = `toast ${type}`;
|
||||
t.textContent = msg;
|
||||
document.getElementById('toast-container').appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
if (!confirm('Apakah Anda yakin ingin keluar?')) return;
|
||||
try {
|
||||
const response = await fetch('api/logout.php');
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') window.location.href = '?page=login';
|
||||
} catch { alert('Gagal logout.'); }
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
document.addEventListener('auth-ready', function (e) {
|
||||
const user = e.detail;
|
||||
document.body.classList.add('auth-loaded');
|
||||
|
||||
// Update User Profile in Sidebar
|
||||
document.getElementById('user-display-name').textContent = user.username;
|
||||
|
||||
// Format role name nicely
|
||||
const roleMap = {
|
||||
'admin': 'Admin',
|
||||
'petugas_lapangan': 'Rumah Ibadah',
|
||||
'guest': 'Guest'
|
||||
};
|
||||
document.getElementById('user-display-role').textContent = roleMap[user.role] || user.role;
|
||||
|
||||
// Show admin-only nav items
|
||||
if (user.role === 'admin') {
|
||||
document.getElementById('nav-akun').style.display = 'flex';
|
||||
}
|
||||
|
||||
// Update Greeting
|
||||
document.getElementById('greeting-title').textContent = `Selamat Datang, ${user.username}!`;
|
||||
|
||||
// Load stats data
|
||||
loadDashboardStats();
|
||||
});
|
||||
|
||||
// Format current date nicely in Indonesian
|
||||
const dateOpts = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
|
||||
document.getElementById('current-date').textContent = new Date().toLocaleDateString('id-ID', dateOpts);
|
||||
|
||||
async function handleLogout() {
|
||||
if (!confirm('Apakah Anda yakin ingin keluar?')) return;
|
||||
try {
|
||||
const response = await fetch('api/logout.php');
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
window.location.href = '?page=login';
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Gagal melakukan logout.');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDashboardStats() {
|
||||
try {
|
||||
const response = await fetch('api/dashboard_stats.php');
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
const stats = result.data;
|
||||
renderStatsCards(stats);
|
||||
renderProximityChart(stats.proximity);
|
||||
renderBantuanChart(stats.tipe_bantuan);
|
||||
renderBantuanTable(stats.bantuan_terbaru);
|
||||
renderRiTable(stats.ri_teraktif);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Gagal memuat statistik dashboard:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatsCards(stats) {
|
||||
// 1. Penduduk Miskin
|
||||
document.getElementById('pm-total-kk').textContent = `${stats.penduduk_miskin.total_kk} KK`;
|
||||
document.getElementById('pm-total-jiwa').textContent = `${stats.penduduk_miskin.total_jiwa} Jiwa terdata`;
|
||||
|
||||
// 2. Rumah Ibadah
|
||||
document.getElementById('ri-total').textContent = stats.rumah_ibadah.total;
|
||||
|
||||
|
||||
// 5. Log Bantuan
|
||||
document.getElementById('lb-total-count').textContent = `${stats.log_bantuan.total_count} kali`;
|
||||
const rpBantuan = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(stats.log_bantuan.total_nilai || 0);
|
||||
document.getElementById('lb-total-nilai').textContent = `Total ${rpBantuan}`;
|
||||
}
|
||||
|
||||
let proximityChart = null;
|
||||
function renderProximityChart(proximity) {
|
||||
const ctx = document.getElementById('chart-proximity').getContext('2d');
|
||||
if (proximityChart) proximityChart.destroy();
|
||||
|
||||
proximityChart = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Dalam Radius (Dalam Jangkauan)', 'Luar Radius (Di Luar Jangkauan)'],
|
||||
datasets: [{
|
||||
data: [proximity.dalam_radius, proximity.luar_radius],
|
||||
backgroundColor: ['#10b981', '#f43f5e'],
|
||||
borderWidth: 4,
|
||||
borderColor: '#ffffff',
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: {
|
||||
boxWidth: 12,
|
||||
font: { family: 'Inter', size: 11, weight: '600' },
|
||||
color: '#475569'
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let bantuanChart = null;
|
||||
function renderBantuanChart(tipeBantuan) {
|
||||
const ctx = document.getElementById('chart-bantuan-tipe').getContext('2d');
|
||||
if (bantuanChart) bantuanChart.destroy();
|
||||
|
||||
bantuanChart = new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ['📚 Pemberdayaan (Pelatihan)', '🛍️ Konsumtif (Makan/Barang)'],
|
||||
datasets: [{
|
||||
label: 'Penyaluran Bantuan',
|
||||
data: [tipeBantuan.Pemberdayaan, tipeBantuan.Konsumtif],
|
||||
backgroundColor: ['#2563eb', '#f59e0b'],
|
||||
borderRadius: 8,
|
||||
barThickness: 36
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { stepSize: 1, color: '#64748b', font: { family: 'Inter' } },
|
||||
grid: { color: '#f1f5f9' }
|
||||
},
|
||||
x: {
|
||||
ticks: { color: '#475569', font: { family: 'Inter', weight: '600' } },
|
||||
grid: { display: false }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderBantuanTable(bantuanList) {
|
||||
const tbody = document.getElementById('table-bantuan-body');
|
||||
if (!bantuanList || bantuanList.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="6" class="loading">Belum ada riwayat bantuan</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = bantuanList.map(b => {
|
||||
const rp = b.nilai_bantuan ? new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(b.nilai_bantuan) : '—';
|
||||
const badgeClass = b.tipe_bantuan === 'Pemberdayaan' ? 'pemberdayaan' : 'konsumtif';
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${b.tanggal_bantuan}</strong></td>
|
||||
<td>${escapeHTML(b.nama_penduduk)}</td>
|
||||
<td><span class="db-badge ${badgeClass}">${b.tipe_bantuan}</span></td>
|
||||
<td>${escapeHTML(b.sub_kategori || '—')}</td>
|
||||
<td><strong>${rp}</strong></td>
|
||||
<td>${escapeHTML(b.nama_rumah_ibadah)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderRiTable(riList) {
|
||||
const tbody = document.getElementById('table-ri-body');
|
||||
if (!riList || riList.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="3" class="loading">Belum ada data penyaluran</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = riList.map(r => {
|
||||
const rp = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(r.nilai);
|
||||
return `
|
||||
<tr>
|
||||
<td><strong>${escapeHTML(r.nama)}</strong></td>
|
||||
<td>${r.count} kali</td>
|
||||
<td><strong>${rp}</strong></td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
if (str == null) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
let allData = [];
|
||||
|
||||
document.addEventListener('auth-ready', function(e) {
|
||||
const user = e.detail;
|
||||
document.body.classList.add('auth-loaded', 'role-admin');
|
||||
document.getElementById('user-display-name').textContent = user.username;
|
||||
document.getElementById('user-display-role').textContent = 'Admin';
|
||||
loadData();
|
||||
});
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const res = await fetch('api/manajemen_akun.php');
|
||||
const data = await res.json();
|
||||
if (data.status !== 'success') throw new Error(data.message);
|
||||
allData = data.data || [];
|
||||
renderCards(allData);
|
||||
} catch (err) {
|
||||
document.getElementById('ri-grid').innerHTML = `
|
||||
<div class="empty-state" style="grid-column:1/-1">
|
||||
<div class="empty-state-icon">⚠️</div>
|
||||
<h3>Gagal memuat data</h3>
|
||||
<p>${esc(err.message)}</p>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCards(data) {
|
||||
const grid = document.getElementById('ri-grid');
|
||||
document.getElementById('filter-count').textContent = `${data.length} rumah ibadah`;
|
||||
|
||||
if (!data.length) {
|
||||
grid.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">🏛️</div>
|
||||
<h3>Belum ada rumah ibadah</h3>
|
||||
<p>Tambahkan rumah ibadah melalui halaman Peta Interaktif.</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = data.map(r => {
|
||||
const hasAccount = !!r.username;
|
||||
const rpFormatted = new Intl.NumberFormat('id-ID', { notation: 'compact', maximumFractionDigits: 1 }).format(r.total_nilai_bantuan);
|
||||
return `
|
||||
<div class="ri-card" onclick="openDrawer(${r.id})" data-jenis="${esc(r.jenis)}" data-account="${hasAccount ? 'active' : 'inactive'}" data-nama="${esc(r.nama.toLowerCase())}">
|
||||
<span class="ri-account-status ${hasAccount ? 'active' : 'inactive'}">${hasAccount ? '● Akun Aktif' : '○ Tanpa Akun'}</span>
|
||||
<div class="ri-card-header">
|
||||
<div class="ri-avatar">${riEmoji(r.jenis)}</div>
|
||||
<div class="ri-card-meta">
|
||||
<div class="ri-card-name">${esc(r.nama)}</div>
|
||||
<span class="ri-card-type">${esc(r.jenis)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ri-card-stats">
|
||||
<div class="stat-mini">
|
||||
<div class="stat-mini-val">${r.pm_dalam_radius}</div>
|
||||
<div class="stat-mini-label">Warga Binaan</div>
|
||||
</div>
|
||||
<div class="stat-mini">
|
||||
<div class="stat-mini-val">${r.total_penyaluran}</div>
|
||||
<div class="stat-mini-label">Penyaluran</div>
|
||||
</div>
|
||||
<div class="stat-mini">
|
||||
<div class="stat-mini-val">${r.total_nilai_bantuan > 0 ? rpFormatted : '—'}</div>
|
||||
<div class="stat-mini-label">Total Nilai</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ri-card-account">
|
||||
${hasAccount
|
||||
? `<span>👤</span> <span class="username">${esc(r.username)}</span>`
|
||||
: `<span class="no-account">Belum ada akun terdaftar</span>`}
|
||||
</div>
|
||||
|
||||
<div class="ri-card-footer">
|
||||
<div class="ri-card-alamat">📍 ${r.alamat ? esc(r.alamat) : 'Alamat belum diisi'}</div>
|
||||
<div class="ri-card-actions" onclick="event.stopPropagation()">
|
||||
<button class="btn-icon" title="Edit Rumah Ibadah" onclick="goToEdit(${r.id})">✏️</button>
|
||||
${!hasAccount
|
||||
? `<button class="btn-icon" title="Buat Akun" onclick="openBuatAkun(${r.id}, '${esc(r.nama)}')">🔑</button>`
|
||||
: `<button class="btn-icon" title="Reset Password" onclick="openResetPassword(${r.id}, '${esc(r.username)}')">🔐</button>`}
|
||||
${hasAccount ? `<button class="btn-icon danger" title="Hapus Akun" onclick="hapusAkun(${r.id}, '${esc(r.nama)}')">🗑️</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function filterCards() {
|
||||
const query = document.getElementById('search-input').value.toLowerCase().trim();
|
||||
const jenis = document.getElementById('filter-jenis').value;
|
||||
const akun = document.getElementById('filter-akun').value;
|
||||
|
||||
const filtered = allData.filter(r => {
|
||||
const matchQuery = !query || r.nama.toLowerCase().includes(query) || r.jenis.toLowerCase().includes(query) || (r.alamat && r.alamat.toLowerCase().includes(query));
|
||||
const matchJenis = !jenis || r.jenis === jenis;
|
||||
const matchAkun = !akun || (akun === 'active' ? !!r.username : !r.username);
|
||||
return matchQuery && matchJenis && matchAkun;
|
||||
});
|
||||
renderCards(filtered);
|
||||
}
|
||||
|
||||
async function openDrawer(id) {
|
||||
pwVisible = false; // Reset password visibility on each open
|
||||
document.getElementById('detail-overlay').classList.add('open');
|
||||
document.getElementById('drawer-body').innerHTML = `
|
||||
<div class="loading-spinner-wrap">
|
||||
<span class="spinner" style="width:20px;height:20px;border-width:3px;"></span>
|
||||
<p style="margin-top:12px">Memuat detail…</p>
|
||||
</div>`;
|
||||
|
||||
try {
|
||||
const res = await fetch(`api/manajemen_akun.php?id=${id}`);
|
||||
const data = await res.json();
|
||||
if (data.status !== 'success') throw new Error(data.message);
|
||||
renderDrawer(data.data);
|
||||
} catch (err) {
|
||||
document.getElementById('drawer-body').innerHTML = `<p style="color:#ef4444">Gagal memuat: ${esc(err.message)}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderDrawer(r) {
|
||||
document.getElementById('drawer-title').textContent = r.nama;
|
||||
const rp = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(r.total_nilai_bantuan);
|
||||
|
||||
document.getElementById('drawer-body').innerHTML = `
|
||||
<!-- KPI boxes -->
|
||||
<div class="kpi-row">
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-val green">${r.pm_dalam_radius}</div>
|
||||
<div class="kpi-label">Warga Binaan</div>
|
||||
</div>
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-val accent">${r.total_penyaluran}</div>
|
||||
<div class="kpi-label">Total Penyaluran</div>
|
||||
</div>
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-val">${r.jumlah_penerima_unik}</div>
|
||||
<div class="kpi-label">Penerima Unik</div>
|
||||
</div>
|
||||
<div class="kpi-box">
|
||||
<div class="kpi-val" style="font-size:15px">${rp}</div>
|
||||
<div class="kpi-label">Total Nilai</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info dasar -->
|
||||
<div class="section-heading">Informasi Rumah Ibadah</div>
|
||||
<div>
|
||||
<div class="info-row"><span class="info-label">Jenis</span><span class="info-val">${riEmoji(r.jenis)} ${esc(r.jenis)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Radius Jangkauan</span><span class="info-val">${r.radius_meter} meter</span></div>
|
||||
<div class="info-row"><span class="info-label">Alamat</span><span class="info-val" style="max-width:260px">${r.alamat ? esc(r.alamat) : '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">No. WhatsApp</span><span class="info-val">${r.no_wa ? esc(r.no_wa) : '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">Koordinat</span><span class="info-val" style="font-family:monospace;font-size:11px">${r.latitude.toFixed(6)}, ${r.longitude.toFixed(6)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Bantuan Pemberdayaan</span><span class="info-val">📚 ${r.bantuan_pemberdayaan} kali</span></div>
|
||||
<div class="info-row"><span class="info-label">Bantuan Konsumtif</span><span class="info-val">🛍️ ${r.bantuan_konsumtif} kali</span></div>
|
||||
<div class="info-row"><span class="info-label">Penyaluran Terakhir</span><span class="info-val">${r.tanggal_bantuan_terakhir || '—'}</span></div>
|
||||
<div class="info-row"><span class="info-label">Terdaftar Sejak</span><span class="info-val">${new Date(r.created_at).toLocaleDateString('id-ID', {day:'numeric',month:'long',year:'numeric'})}</span></div>
|
||||
</div>
|
||||
|
||||
<!-- Info akun -->
|
||||
<div class="section-heading">Informasi Akun</div>
|
||||
<div>
|
||||
${r.username ? `
|
||||
<div class="info-row"><span class="info-label">Username</span><span class="info-val" style="font-family:monospace">${esc(r.username)}</span></div>
|
||||
<div class="info-row"><span class="info-label">Password</span>
|
||||
<span class="info-val" style="display:flex;align-items:center;gap:8px">
|
||||
<span id="pw-display" style="font-family:monospace;letter-spacing:.1em">${r.password_plain ? '••••••••' : '<em style=color:#94a3b8>Tidak diketahui</em>'}</span>
|
||||
${r.password_plain ? `<button onclick="togglePw('${esc(r.password_plain)}')" id="pw-toggle-btn" style="font-size:11px;padding:3px 8px;border:1px solid #e2e8f0;border-radius:6px;background:#f8fafc;cursor:pointer;color:#475569;transition:all .15s" title="Tampilkan/Sembunyikan Password">👁 Lihat</button>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div class="info-row"><span class="info-label">Status</span><span class="info-val" style="color:#15803d">● Aktif</span></div>
|
||||
<div class="info-row"><span class="info-label">Akun Dibuat</span><span class="info-val">${r.akun_dibuat ? new Date(r.akun_dibuat).toLocaleDateString('id-ID',{day:'numeric',month:'long',year:'numeric'}) : '—'}</span></div>
|
||||
<div style="margin-top:12px;display:flex;gap:8px">
|
||||
<button class="btn btn-ghost btn-sm" onclick="openResetPassword(${r.id}, '${esc(r.username)}')">🔐 Reset Password</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="hapusAkun(${r.id}, '${esc(r.nama)}')">🗑️ Hapus Akun</button>
|
||||
</div>
|
||||
` : `
|
||||
<div style="padding:16px;background:#f8fafc;border-radius:12px;text-align:center;color:#94a3b8;font-size:13px">
|
||||
Belum ada akun terdaftar untuk rumah ibadah ini.
|
||||
</div>
|
||||
<div style="margin-top:12px">
|
||||
<button class="btn btn-primary btn-sm" onclick="openBuatAkun(${r.id}, '${esc(r.nama)}')">🔑 Buat Akun</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<!-- Riwayat bantuan terbaru -->
|
||||
${r.riwayat_bantuan && r.riwayat_bantuan.length ? `
|
||||
<div class="section-heading">Riwayat Penyaluran Terbaru</div>
|
||||
<div class="log-list">
|
||||
${r.riwayat_bantuan.map(l => `
|
||||
<div class="log-entry">
|
||||
<div class="log-type-dot ${l.tipe_bantuan === 'Pemberdayaan' ? 'pem' : 'kon'}"></div>
|
||||
<div class="log-entry-info">
|
||||
<div class="log-entry-name">${esc(l.nama_penerima)}</div>
|
||||
<div class="log-entry-detail">${l.tipe_bantuan === 'Pemberdayaan' ? '📚' : '🛍️'} ${esc(l.tipe_bantuan)}${l.sub_kategori ? ' · ' + esc(l.sub_kategori) : ''}${l.nilai_bantuan ? ' · Rp' + new Intl.NumberFormat('id-ID').format(l.nilai_bantuan) : ''}</div>
|
||||
</div>
|
||||
<div class="log-entry-date">${l.tanggal_bantuan}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function closeDrawer(e) {
|
||||
if (e && e.target !== document.getElementById('detail-overlay')) return;
|
||||
document.getElementById('detail-overlay').classList.remove('open');
|
||||
}
|
||||
|
||||
let pwVisible = false;
|
||||
function togglePw(plainPassword) {
|
||||
const display = document.getElementById('pw-display');
|
||||
const btn = document.getElementById('pw-toggle-btn');
|
||||
if (!display || !btn) return;
|
||||
pwVisible = !pwVisible;
|
||||
if (pwVisible) {
|
||||
display.textContent = plainPassword;
|
||||
display.style.color = '#0f172a';
|
||||
btn.textContent = '🙈 Sembunyikan';
|
||||
btn.style.background = '#f0fdf4';
|
||||
btn.style.borderColor = '#bbf7d0';
|
||||
btn.style.color = '#15803d';
|
||||
} else {
|
||||
display.textContent = '••••••••';
|
||||
display.style.color = '';
|
||||
btn.textContent = '👁 Lihat';
|
||||
btn.style.background = '#f8fafc';
|
||||
btn.style.borderColor = '#e2e8f0';
|
||||
btn.style.color = '#475569';
|
||||
}
|
||||
}
|
||||
|
||||
function openResetPassword(id, username) {
|
||||
openModal('Reset Password Akun', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username</label>
|
||||
<input class="form-input" value="${esc(username)}" readonly style="background:#f8fafc;color:#64748b">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password Baru *</label>
|
||||
<input class="form-input" id="new-password" type="password" placeholder="Min. 6 karakter" autocomplete="new-password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Konfirmasi Password *</label>
|
||||
<input class="form-input" id="confirm-password" type="password" placeholder="Ulangi password baru" autocomplete="new-password">
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Batal</button>
|
||||
<button class="btn btn-primary" onclick="doResetPassword(${id})">🔐 Simpan Password</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function doResetPassword(riId) {
|
||||
const pw = document.getElementById('new-password').value;
|
||||
const conf = document.getElementById('confirm-password').value;
|
||||
if (!pw || pw.length < 6) return toast('Password minimal 6 karakter', 'error');
|
||||
if (pw !== conf) return toast('Konfirmasi password tidak cocok', 'error');
|
||||
|
||||
// Find the data — cast ID to number to avoid string vs number mismatch
|
||||
const ri = allData.find(r => Number(r.id) === Number(riId));
|
||||
if (!ri) {
|
||||
toast('Data rumah ibadah tidak ditemukan di cache, coba refresh halaman', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('#modal-footer .btn-primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Menyimpan…'; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`api/rumah_ibadah.php?id=${riId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
nama: ri.nama, jenis: ri.jenis, alamat: ri.alamat,
|
||||
no_wa: ri.no_wa, latitude: ri.latitude, longitude: ri.longitude,
|
||||
radius_meter: ri.radius_meter, username: ri.username, password: pw
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
toast('Password berhasil direset!', 'success');
|
||||
closeModal();
|
||||
await loadData(); // Refresh allData
|
||||
openDrawer(riId); // Reopen drawer with fresh data
|
||||
} else {
|
||||
toast(data.message || 'Gagal reset password', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('Terjadi kesalahan: ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔐 Simpan Password'; }
|
||||
}
|
||||
}
|
||||
|
||||
function openBuatAkun(id, namaRI) {
|
||||
const sanitized = namaRI.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
||||
openModal('Buat Akun Rumah Ibadah', `
|
||||
<div class="form-group">
|
||||
<label class="form-label">Nama Rumah Ibadah</label>
|
||||
<input class="form-input" value="${esc(namaRI)}" readonly style="background:#f8fafc;color:#64748b">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Username (otomatis)</label>
|
||||
<input class="form-input" id="buat-username" value="${esc(sanitized)}" readonly style="background:#f1f5f9;color:#64748b;font-family:monospace">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Password *</label>
|
||||
<input class="form-input" id="buat-password" type="password" placeholder="Min. 6 karakter" autocomplete="new-password">
|
||||
</div>
|
||||
`, `
|
||||
<button class="btn btn-ghost" onclick="closeModal()">Batal</button>
|
||||
<button class="btn btn-primary" onclick="doBuatAkun(${id})">🔑 Buat Akun</button>
|
||||
`);
|
||||
}
|
||||
|
||||
async function doBuatAkun(riId) {
|
||||
const username = document.getElementById('buat-username').value;
|
||||
const password = document.getElementById('buat-password').value;
|
||||
if (!password || password.length < 6) return toast('Password minimal 6 karakter', 'error');
|
||||
|
||||
// Cast ID to number to avoid string vs number mismatch from PHP JSON
|
||||
const ri = allData.find(r => Number(r.id) === Number(riId));
|
||||
if (!ri) {
|
||||
toast('Data rumah ibadah tidak ditemukan di cache, coba refresh halaman', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('#modal-footer .btn-primary');
|
||||
if (btn) { btn.disabled = true; btn.textContent = 'Membuat…'; }
|
||||
|
||||
try {
|
||||
const res = await fetch(`api/rumah_ibadah.php?id=${riId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
nama: ri.nama, jenis: ri.jenis, alamat: ri.alamat,
|
||||
no_wa: ri.no_wa, latitude: ri.latitude, longitude: ri.longitude,
|
||||
radius_meter: ri.radius_meter, username, password
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
toast('Akun berhasil dibuat!', 'success');
|
||||
closeModal();
|
||||
await loadData(); // Refresh allData
|
||||
openDrawer(riId); // Reopen drawer with new account info
|
||||
} else {
|
||||
toast(data.message || 'Gagal membuat akun', 'error');
|
||||
}
|
||||
} catch (err) {
|
||||
toast('Terjadi kesalahan: ' + err.message, 'error');
|
||||
} finally {
|
||||
if (btn) { btn.disabled = false; btn.textContent = '🔑 Buat Akun'; }
|
||||
}
|
||||
}
|
||||
|
||||
async function hapusAkun(id, nama) {
|
||||
if (!confirm(`Hapus akun login untuk "${nama}"?\nRumah ibadah tidak akan dihapus, hanya akun loginnya.`)) return;
|
||||
const res = await fetch(`api/manajemen_akun.php?id=${id}`, { method: 'DELETE' });
|
||||
const data = await res.json();
|
||||
if (data.status === 'success') {
|
||||
toast(data.message, 'success');
|
||||
document.getElementById('detail-overlay').classList.remove('open');
|
||||
await loadData();
|
||||
} else {
|
||||
toast(data.message || 'Gagal menghapus akun', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function goToEdit(id) {
|
||||
window.location.href = `?page=map`;
|
||||
}
|
||||
|
||||
function riEmoji(jenis) {
|
||||
const map = { 'Masjid': '🕌', 'Gereja': '⛪', 'Pura': '🛕', 'Vihara': '🏯', 'Klenteng': '🏮', 'Lainnya': '🏛️' };
|
||||
return map[jenis] || '🏛️';
|
||||
}
|
||||
|
||||
function esc(s) {
|
||||
if (s == null) return '';
|
||||
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function toast(msg, type = 'info') {
|
||||
const t = document.createElement('div');
|
||||
t.className = `toast ${type}`;
|
||||
t.textContent = msg;
|
||||
document.getElementById('toast-container').appendChild(t);
|
||||
setTimeout(() => t.remove(), 3500);
|
||||
}
|
||||
|
||||
function openModal(title, bodyHTML, footerHTML) {
|
||||
document.getElementById('modal-title').textContent = title;
|
||||
document.getElementById('modal-body').innerHTML = bodyHTML;
|
||||
document.getElementById('modal-footer').innerHTML = footerHTML;
|
||||
document.getElementById('modal-overlay').classList.add('open');
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('modal-overlay').classList.remove('open');
|
||||
}
|
||||
document.getElementById('modal-overlay').addEventListener('click', e => {
|
||||
if (e.target.id === 'modal-overlay') closeModal();
|
||||
});
|
||||
|
||||
async function handleLogout() {
|
||||
if (!confirm('Apakah Anda yakin ingin keluar?')) return;
|
||||
try {
|
||||
const response = await fetch('api/logout.php');
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') window.location.href = '?page=login';
|
||||
} catch { alert('Gagal logout.'); }
|
||||
}
|
||||
Reference in New Issue
Block a user