edit files
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
const Api = (() => {
|
||||
// Deteksi path API secara otomatis
|
||||
const BASE = (() => {
|
||||
const path = window.location.pathname;
|
||||
const dir = path.substring(0, path.lastIndexOf('/') + 1);
|
||||
return dir + 'api/index.php';
|
||||
})();
|
||||
|
||||
async function req(method, path, body=null) {
|
||||
// path contoh: '/penduduk' → '?r=penduduk' | '/penduduk/123' → '?r=penduduk/123'
|
||||
const cleanPath = path.replace(/^\//, '');
|
||||
const url = `${BASE}?r=${cleanPath}`;
|
||||
const opts = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
};
|
||||
if (body) opts.body = JSON.stringify(body);
|
||||
|
||||
const res = await fetch(url, opts);
|
||||
const text = await res.text();
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
// PHP mengembalikan HTML/teks error bukan JSON
|
||||
const preview = text.substring(0, 200);
|
||||
throw new Error(`Server error ${res.status} — bukan JSON: ${preview}`);
|
||||
}
|
||||
if (!json.success) throw new Error(json.message || 'Terjadi kesalahan');
|
||||
return json.data;
|
||||
}
|
||||
|
||||
return {
|
||||
get: path => req('GET', path),
|
||||
post: (path, b) => req('POST', path, b),
|
||||
put: (path, b) => req('PUT', path, b),
|
||||
delete: path => req('DELETE', path),
|
||||
};
|
||||
})();
|
||||
@@ -0,0 +1,278 @@
|
||||
/* ════════════════════════════════════════
|
||||
GLOBAL HELPERS
|
||||
════════════════════════════════════════ */
|
||||
const ge = id => document.getElementById(id);
|
||||
const gv = id => (document.getElementById(id)?.value ?? '').trim();
|
||||
const sv = (id, val) => { const el = document.getElementById(id); if (el) el.value = val ?? ''; };
|
||||
const rp = n => n > 0 ? 'Rp ' + parseInt(n).toLocaleString('id-ID') : '—';
|
||||
const cap = s => s ? s.charAt(0).toUpperCase() + s.slice(1).replace(/_/g, ' ') : '—';
|
||||
const bdg = (val, _cls) => {
|
||||
const v = val || '';
|
||||
const css = v.replace(/_/g,'-').toLowerCase();
|
||||
return `<span class="bdg bdg-${css}">${v.replace(/_/g,' ').toUpperCase()}</span>`;
|
||||
};
|
||||
|
||||
/* ════════════════════════════════════════
|
||||
ROLE CONFIG
|
||||
1 = Admin, 2 = Pengurus RI, 3 = Pimpinan, 4 = Masyarakat
|
||||
════════════════════════════════════════ */
|
||||
const ROLE_MENUS = {
|
||||
1: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat','log'],
|
||||
2: ['dashboard','peta','penduduk','keluarga','ri','bantuan','prioritas','laporan','chat'],
|
||||
3: ['dashboard','peta','penduduk','keluarga','laporan'],
|
||||
4: ['dashboard','peta'],
|
||||
99: ['dashboard','peta'],
|
||||
};
|
||||
|
||||
const ROLE_LABELS = {
|
||||
1: { icon:'⚙️', label:'Administrator', color:'#e74c3c' },
|
||||
2: { icon:'🕌', label:'Pengurus Rumah Ibadah', color:'#27ae60' },
|
||||
3: { icon:'🏛️', label:'Pimpinan Daerah', color:'#2980b9' },
|
||||
4: { icon:'👤', label:'Masyarakat', color:'#8e44ad' },
|
||||
};
|
||||
|
||||
/* ════════════════════════════════════════
|
||||
APP CONTROLLER
|
||||
════════════════════════════════════════ */
|
||||
const App = (() => {
|
||||
let _user = null, _autoTimer = null;
|
||||
|
||||
const currentUser = () => _user;
|
||||
const userRole = () => _user?.id_role ?? 99;
|
||||
const canEdit = () => [1,2].includes(userRole());
|
||||
|
||||
/* ── Login ── */
|
||||
async function doLogin() {
|
||||
const username = gv('login-user'), password = gv('login-pass');
|
||||
const errEl = ge('login-err');
|
||||
const btnMasuk = ge('btn-masuk-submit');
|
||||
errEl.classList.add('hidden');
|
||||
if (btnMasuk) { btnMasuk.disabled = true; btnMasuk.textContent = 'Memproses...'; }
|
||||
|
||||
try {
|
||||
const res = await fetch('api/index.php?r=auth', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
let json;
|
||||
const text = await res.text();
|
||||
try { json = JSON.parse(text); }
|
||||
catch { throw new Error(`Respons server tidak valid (bukan JSON). Cek error PHP di server.`); }
|
||||
|
||||
if (!json.success) throw new Error(json.message || 'Login gagal');
|
||||
|
||||
_user = json.data;
|
||||
setUserUI(_user);
|
||||
closeModal('modal-login');
|
||||
ge('login-pass').value = '';
|
||||
toast(`Selamat datang, ${_user.nama_lengkap}! 👋`, 'success');
|
||||
|
||||
// Arahkan ke panel default sesuai role
|
||||
const defaultPanel = { 1:'dashboard', 2:'dashboard', 3:'dashboard', 4:'dashboard' };
|
||||
switchPanel(defaultPanel[_user.id_role] || 'dashboard');
|
||||
startAutoRefresh();
|
||||
|
||||
} catch(e) {
|
||||
errEl.textContent = e.message || 'Username atau password salah';
|
||||
errEl.classList.remove('hidden');
|
||||
} finally {
|
||||
if (btnMasuk) { btnMasuk.disabled = false; btnMasuk.textContent = 'Masuk →'; }
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
try {
|
||||
await fetch('api/index.php?r=auth/logout', { method:'DELETE', credentials:'same-origin' });
|
||||
} catch {}
|
||||
stopAutoRefresh();
|
||||
_user = null;
|
||||
setUserUI(null);
|
||||
toast('Anda telah logout', 'info');
|
||||
switchPanel('dashboard');
|
||||
}
|
||||
|
||||
/* ── Apply UI per role ── */
|
||||
function setUserUI(u) {
|
||||
if (u) {
|
||||
const info = ROLE_LABELS[u.id_role] || { icon:'👤', label: u.nama_role, color:'#555' };
|
||||
ge('user-name-el').textContent = u.nama_lengkap;
|
||||
ge('user-avatar-el').textContent = info.icon;
|
||||
ge('user-role-el').textContent = info.label.toUpperCase();
|
||||
ge('user-info').classList.remove('hidden');
|
||||
ge('btn-login').classList.add('hidden');
|
||||
applyRoleUI(u.id_role);
|
||||
|
||||
// Tampilkan role banner
|
||||
showRoleBanner(u.id_role, u.nama_lengkap, info);
|
||||
} else {
|
||||
ge('user-info').classList.add('hidden');
|
||||
ge('btn-login').classList.remove('hidden');
|
||||
applyRoleUI(99);
|
||||
hideRoleBanner();
|
||||
}
|
||||
}
|
||||
|
||||
function showRoleBanner(role, nama, info) {
|
||||
let banner = ge('role-banner');
|
||||
if (!banner) {
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'role-banner';
|
||||
banner.style.cssText = `
|
||||
position:fixed; top:0; left:0; right:0; z-index:9999;
|
||||
padding:6px 16px; font-size:12px; font-weight:600;
|
||||
display:flex; align-items:center; gap:8px;
|
||||
transition: all 0.3s;
|
||||
`;
|
||||
document.body.prepend(banner);
|
||||
}
|
||||
banner.style.background = info.color;
|
||||
banner.style.color = '#fff';
|
||||
banner.innerHTML = `
|
||||
${info.icon} Login sebagai: <strong>${nama}</strong>
|
||||
| Role: <strong>${info.label}</strong>
|
||||
| <span style="opacity:0.8;font-size:11px">${roleDescription(role)}</span>
|
||||
`;
|
||||
// Geser konten ke bawah sedikit
|
||||
document.body.style.paddingTop = '30px';
|
||||
}
|
||||
|
||||
function hideRoleBanner() {
|
||||
const banner = ge('role-banner');
|
||||
if (banner) banner.remove();
|
||||
document.body.style.paddingTop = '';
|
||||
}
|
||||
|
||||
function roleDescription(role) {
|
||||
return {
|
||||
1: 'Akses penuh — kelola semua data & pengguna',
|
||||
2: 'Kelola penduduk, bantuan & verifikasi laporan',
|
||||
3: 'Read-only — monitoring & dashboard',
|
||||
4: 'Hanya bisa melihat peta & kirim laporan',
|
||||
}[role] || '';
|
||||
}
|
||||
|
||||
function applyRoleUI(role) {
|
||||
const allowed = ROLE_MENUS[role] || ROLE_MENUS[99];
|
||||
|
||||
// Sembunyikan/tampilkan nav menu
|
||||
document.querySelectorAll('.nav-btn[data-panel]').forEach(btn => {
|
||||
const panel = btn.dataset.panel;
|
||||
btn.style.display = allowed.includes(panel) ? '' : 'none';
|
||||
});
|
||||
|
||||
// Sembunyikan tombol aksi edit/tambah/salur untuk non-editor
|
||||
const canEditRole = [1,2].includes(role);
|
||||
document.querySelectorAll('[id^="btn-tambah"],[id^="btn-salur"],[id^="btn-edit"]')
|
||||
.forEach(el => el.style.display = canEditRole ? '' : 'none');
|
||||
|
||||
// Khusus Admin: tampilkan menu log
|
||||
const logBtn = document.querySelector('[data-panel="log"]');
|
||||
if (logBtn) logBtn.style.display = role === 1 ? '' : 'none';
|
||||
|
||||
// Kirim Laporan hanya untuk masyarakat & umum
|
||||
const kirimBtn = ge('nav-kirim-laporan');
|
||||
if (kirimBtn) kirimBtn.style.display = [4,99].includes(role) ? '' : 'none';
|
||||
}
|
||||
|
||||
/* ── Panel routing ── */
|
||||
let _cur = 'dashboard';
|
||||
|
||||
function switchPanel(name) {
|
||||
const role = userRole();
|
||||
const allowed = ROLE_MENUS[role] || ROLE_MENUS[99];
|
||||
|
||||
// Jika panel tidak boleh diakses role ini, redirect ke dashboard
|
||||
if (!allowed.includes(name)) {
|
||||
toast('Anda tidak punya akses ke halaman ini', 'error');
|
||||
name = 'dashboard';
|
||||
}
|
||||
|
||||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
|
||||
ge(`panel-${name}`)?.classList.add('active');
|
||||
document.querySelector(`[data-panel="${name}"]`)?.classList.add('active');
|
||||
_cur = name;
|
||||
|
||||
if (name === 'peta') setTimeout(() => MapApp.init(), 80);
|
||||
else loadPanel(name);
|
||||
}
|
||||
|
||||
function loadCurrentPanel() { loadPanel(_cur); }
|
||||
|
||||
function loadPanel(n) {
|
||||
({ dashboard: Dashboard.load, penduduk: Penduduk.load, keluarga: Keluarga.load,
|
||||
bantuan: Bantuan.load, laporan: LaporanPanel.load, ri: RI.load,
|
||||
prioritas: PrioritasPanel.load, chat: Chat.init, log: LogPanel.load })[n]?.();
|
||||
}
|
||||
|
||||
/* ── Auto refresh ── */
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
_autoTimer = setInterval(() => {
|
||||
if (_cur === 'dashboard') Dashboard.load();
|
||||
if (_cur === 'laporan') LaporanPanel.load();
|
||||
updateHeaderStats();
|
||||
}, 30000);
|
||||
}
|
||||
function stopAutoRefresh() { clearInterval(_autoTimer); }
|
||||
|
||||
/* ── Modals ── */
|
||||
function showModal(id) { ge(id)?.classList.remove('hidden'); }
|
||||
function closeModal(id) { ge(id)?.classList.add('hidden'); }
|
||||
|
||||
/* ── Toast ── */
|
||||
function toast(msg, type='info', dur=3800) {
|
||||
const icons = { success:'✅', error:'❌', info:'ℹ️', warning:'⚠️' };
|
||||
const el = document.createElement('div');
|
||||
el.className = `toast-item ${type}`;
|
||||
el.innerHTML = `<span class="toast-icon">${icons[type]||'ℹ️'}</span><span>${msg}</span>`;
|
||||
ge('toast-container').appendChild(el);
|
||||
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 300); }, dur);
|
||||
}
|
||||
|
||||
/* ── Header stats ── */
|
||||
async function updateHeaderStats() {
|
||||
try {
|
||||
const d = await Api.get('/dashboard');
|
||||
const s = d.statistik;
|
||||
ge('hp-miskin-n').textContent = s.total_miskin;
|
||||
ge('hp-rentan-n').textContent = s.total_rentan;
|
||||
ge('hp-mampu-n').textContent = s.total_mampu;
|
||||
ge('hp-lap-n').textContent = s.laporan_pending;
|
||||
const nb = ge('nb-laporan');
|
||||
s.laporan_pending > 0
|
||||
? (nb.textContent = s.laporan_pending, nb.classList.remove('hidden'))
|
||||
: nb.classList.add('hidden');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/* ── Events ── */
|
||||
ge('btn-toggle-sidebar').addEventListener('click', () => {
|
||||
ge('sidebar').classList.toggle('collapsed');
|
||||
});
|
||||
document.addEventListener('click', e => {
|
||||
if (e.target.classList.contains('modal-overlay')) e.target.classList.add('hidden');
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === 'Escape')
|
||||
document.querySelectorAll('.modal-overlay:not(.hidden)').forEach(m => m.classList.add('hidden'));
|
||||
});
|
||||
|
||||
/* ── Init ── */
|
||||
(async function init() {
|
||||
try {
|
||||
const res = await fetch('api/index.php?r=auth/me', { credentials:'same-origin' });
|
||||
const text = await res.text();
|
||||
const json = JSON.parse(text);
|
||||
if (json.success) { _user = json.data; setUserUI(_user); startAutoRefresh(); }
|
||||
} catch {}
|
||||
Dashboard.load();
|
||||
updateHeaderStats();
|
||||
})();
|
||||
|
||||
return { currentUser, userRole, canEdit, doLogin, logout, switchPanel,
|
||||
showModal, closeModal, toast, updateHeaderStats };
|
||||
})();
|
||||
@@ -0,0 +1,132 @@
|
||||
const Bantuan = (() => {
|
||||
let jenisList = [];
|
||||
|
||||
const DEMO_JENIS = [
|
||||
{ id:1, nama_bantuan:'Sembako Ramadan', kategori:'sembako', sumber:'Zakat Masjid', satuan:'paket', aktif:1 },
|
||||
{ id:2, nama_bantuan:'PKH (Prog. Keluarga Harapan)', kategori:'ekonomi', sumber:'APBN', satuan:'Rp', aktif:1 },
|
||||
{ id:3, nama_bantuan:'BPNT / Kartu Sembako', kategori:'sembako', sumber:'APBN', satuan:'bulan', aktif:1 },
|
||||
{ id:4, nama_bantuan:'Beasiswa Pendidikan', kategori:'pendidikan', sumber:'CSR Perusahaan', satuan:'paket/tahun',aktif:1 },
|
||||
{ id:5, nama_bantuan:'BPJS Kesehatan PBI', kategori:'kesehatan', sumber:'APBD', satuan:'bulan', aktif:1 },
|
||||
{ id:6, nama_bantuan:'Bantuan Sembako Gereja', kategori:'sembako', sumber:'Kolekte Gereja', satuan:'paket', aktif:1 },
|
||||
{ id:7, nama_bantuan:'Bantuan Vihara (Kwan Im)',kategori:'sembako', sumber:'Dana Sosial Vihara',satuan:'paket', aktif:1 },
|
||||
{ id:8, nama_bantuan:'Raskin / Beras Miskin', kategori:'sembako', sumber:'Bulog', satuan:'kg', aktif:1 },
|
||||
];
|
||||
|
||||
const DEMO_HISTORI = [
|
||||
{ id:1, nama_lengkap:'Ahmad Suryana', nama_bantuan:'Sembako Ramadan', tanggal:'2025-03-15', jumlah:1, satuan:'paket', nama_ri:"Masjid Jami'", status_penyaluran:'disalurkan' },
|
||||
{ id:2, nama_lengkap:'Ahmad Suryana', nama_bantuan:'BPNT', tanggal:'2025-01-10', jumlah:1, satuan:'bulan', nama_ri:"Masjid Jami'", status_penyaluran:'disalurkan' },
|
||||
{ id:3, nama_lengkap:'Maria Magdalena', nama_bantuan:'Bantuan Sembako Gereja', tanggal:'2025-02-20', jumlah:1, satuan:'paket', nama_ri:'Katedral', status_penyaluran:'disalurkan' },
|
||||
{ id:4, nama_lengkap:'Lim Ah Kow', nama_bantuan:'Bantuan Vihara', tanggal:'2025-04-01', jumlah:1, satuan:'paket', nama_ri:'Vihara', status_penyaluran:'disalurkan' },
|
||||
{ id:5, nama_lengkap:'Tan Eng Huat', nama_bantuan:'Raskin', tanggal:'2025-03-05', jumlah:10, satuan:'kg', nama_ri:'Klenteng', status_penyaluran:'disalurkan' },
|
||||
];
|
||||
|
||||
const KAT_COLOR = { sembako:'#06d6a0', ekonomi:'#4361ee', pendidikan:'#4cc9f0', kesehatan:'#ffd166', lainnya:'#6b7a99' };
|
||||
const KAT_ICON = { sembako:'🛒', ekonomi:'💰', pendidikan:'📚', kesehatan:'🏥', lainnya:'📦' };
|
||||
|
||||
async function load() {
|
||||
try { jenisList = await Api.get('/bantuan'); } catch { jenisList = DEMO_JENIS; }
|
||||
renderJenis();
|
||||
loadHistori();
|
||||
}
|
||||
|
||||
function renderJenis() {
|
||||
const el = ge('bantuan-jenis-list');
|
||||
el.innerHTML = jenisList.map(b => `
|
||||
<div style="display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--bg4);border:1px solid var(--border);border-radius:8px;margin-bottom:6px;border-left:3px solid ${KAT_COLOR[b.kategori]||'#888'}">
|
||||
<span style="font-size:20px">${KAT_ICON[b.kategori]||'📦'}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:700;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${b.nama_bantuan}</div>
|
||||
<div style="font-size:11px;color:var(--muted)">${b.kategori} · ${b.sumber||'—'} · per ${b.satuan||'unit'}</div>
|
||||
</div>
|
||||
${App.canEdit() ? `<button class="btn btn-danger btn-xs" onclick="Bantuan.hapusJenis(${b.id})">🗑</button>` : ''}
|
||||
</div>`).join('') || '<p style="color:var(--muted);font-size:12px;padding:12px">Belum ada data bantuan</p>';
|
||||
}
|
||||
|
||||
async function loadHistori() {
|
||||
let data;
|
||||
try { data = await Api.get('/histori?status=disalurkan'); } catch { data = DEMO_HISTORI; }
|
||||
ge('bantuan-histori-list').innerHTML = data.map(h => `
|
||||
<div style="padding:10px 12px;background:var(--bg4);border:1px solid var(--border);border-radius:8px;margin-bottom:6px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:flex-start">
|
||||
<strong style="font-size:12.5px">${h.nama_lengkap}</strong>
|
||||
<span class="bdg bdg-${h.status_penyaluran||'disalurkan'}" style="font-size:9px">${h.status_penyaluran}</span>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted2);margin-top:3px">${h.nama_bantuan} · ${h.jumlah} ${h.satuan||''}</div>
|
||||
<div style="font-size:10px;color:var(--muted);margin-top:2px">🕌 ${h.nama_ri||'—'} · 📅 ${h.tanggal?.substring(0,10)||'—'}</div>
|
||||
</div>`).join('') || '<p style="color:var(--muted);font-size:12px;padding:12px">Belum ada riwayat</p>';
|
||||
}
|
||||
|
||||
function showFormJenis() {
|
||||
['fjb-nama','fjb-satuan','fjb-sumber','fjb-deskripsi'].forEach(id => sv(id,''));
|
||||
sv('fjb-kategori','sembako');
|
||||
App.showModal('modal-jenis-bantuan');
|
||||
}
|
||||
|
||||
async function saveJenis() {
|
||||
const nama = gv('fjb-nama');
|
||||
if (!nama) { App.toast('Nama bantuan wajib diisi','error'); return; }
|
||||
const body = { nama_bantuan: nama, kategori: gv('fjb-kategori'), sumber: gv('fjb-sumber'), satuan: gv('fjb-satuan'), deskripsi: gv('fjb-deskripsi') };
|
||||
try {
|
||||
await Api.post('/bantuan', body);
|
||||
App.toast('Jenis bantuan ditambahkan ✅','success');
|
||||
App.closeModal('modal-jenis-bantuan');
|
||||
load();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function hapusJenis(id) {
|
||||
if (!confirm('Hapus jenis bantuan ini?')) return;
|
||||
try { await Api.delete(`/bantuan/${id}`); App.toast('Dihapus','success'); load(); }
|
||||
catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function showFormSalur(nikPrefill = '', namaPrefill = '') {
|
||||
sv('fs-bantuan', '');
|
||||
sv('fs-nik', nikPrefill);
|
||||
sv('fs-jumlah', '1');
|
||||
sv('fs-ket', '');
|
||||
ge('fs-nik-info').classList.add('hidden');
|
||||
ge('fs-tgl').value = new Date().toISOString().substring(0,10);
|
||||
// Isi dropdown jenis bantuan
|
||||
if (!jenisList.length) { try { jenisList = await Api.get('/bantuan'); } catch { jenisList = DEMO_JENIS; } }
|
||||
ge('fs-bantuan').innerHTML = jenisList.map(b => `<option value="${b.id}">${KAT_ICON[b.kategori]||'📦'} ${b.nama_bantuan}</option>`).join('');
|
||||
App.showModal('modal-salur');
|
||||
if (nikPrefill) { setTimeout(() => cekPenerima(nikPrefill), 200); }
|
||||
}
|
||||
|
||||
function quickSalur(nik, nama) { showFormSalur(nik, nama); }
|
||||
|
||||
async function cekPenerima(nik) {
|
||||
if (nik.length < 16) { ge('fs-nik-info').classList.add('hidden'); return; }
|
||||
try {
|
||||
const info = await Api.get(`/histori/${nik}/cek`);
|
||||
const box = ge('fs-nik-info');
|
||||
if (info.total > 0) {
|
||||
box.innerHTML = `✅ Sudah dibantu <strong>${info.total}x</strong> · ${info.jenis_bantuan||'—'} · Terakhir: ${info.terakhir_dibantu?.substring(0,10)||'—'}`;
|
||||
box.className = 'alert-ok';
|
||||
} else {
|
||||
box.innerHTML = `⚠️ Belum pernah menerima bantuan`;
|
||||
box.className = 'alert-warn';
|
||||
}
|
||||
box.classList.remove('hidden');
|
||||
} catch {
|
||||
ge('fs-nik-info').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
async function salurkan() {
|
||||
const nik = gv('fs-nik');
|
||||
if (!nik || nik.length !== 16) { App.toast('NIK penerima harus 16 digit','error'); return; }
|
||||
if (!gv('fs-bantuan')) { App.toast('Pilih jenis bantuan','error'); return; }
|
||||
if (!gv('fs-tgl')) { App.toast('Tanggal wajib diisi','error'); return; }
|
||||
const body = { nik, id_bantuan: gv('fs-bantuan'), tanggal: gv('fs-tgl'), jumlah: gv('fs-jumlah'), keterangan: gv('fs-ket') };
|
||||
try {
|
||||
await Api.post('/histori', body);
|
||||
App.toast('Bantuan berhasil dicatat ✅','success');
|
||||
App.closeModal('modal-salur');
|
||||
load();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
return { load, showFormJenis, saveJenis, hapusJenis, showFormSalur, quickSalur, cekPenerima, salurkan };
|
||||
})();
|
||||
@@ -0,0 +1,99 @@
|
||||
const Chat = (() => {
|
||||
let currentRoom = null, pollTimer = null;
|
||||
|
||||
const DEMO_ROOMS = [
|
||||
{ id:1, nama:'# Koordinasi Umum' },
|
||||
{ id:2, nama:'# Bantuan Darurat' },
|
||||
{ id:3, nama:'# Distribusi Sembako' },
|
||||
];
|
||||
|
||||
const DEMO_MSGS = {
|
||||
null: [
|
||||
{ id:1, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Salam rekan-rekan semua. Ada keluarga di Gg. Mawar yang sangat butuh bantuan sembako segera.', created_at:'2025-05-13 08:00:00' },
|
||||
{ id:2, id_user:3, nama_lengkap:'Romo Benediktus',pesan:'Baik. Kami di Katedral juga sedang tangani kasus Ibu Maria yang sakit keras. Mohon dikoordinasikan.', created_at:'2025-05-13 08:05:00' },
|
||||
{ id:3, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Insya Allah kita distribusi bersama hari Jumat. Tolong daftarkan semua ke sistem ya.', created_at:'2025-05-13 08:10:00' },
|
||||
{ id:4, id_user:3, nama_lengkap:'Romo Benediktus',pesan:'Setuju. Saya sudah verifikasi laporan Ibu Maria. Status diproses.', created_at:'2025-05-13 08:15:00' },
|
||||
],
|
||||
1: [{ id:5, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Koordinasi umum aktif. Silakan gunakan channel ini untuk diskusi lintas RI.', created_at:'2025-05-13 09:00:00' }],
|
||||
2: [{ id:6, id_user:3, nama_lengkap:'Romo Benediktus', pesan:'Ada kasus darurat di Akcaya — balita gizi buruk. Butuh respons cepat.', created_at:'2025-05-14 07:00:00' }],
|
||||
3: [{ id:7, id_user:2, nama_lengkap:'Ahmad Fauzi', pesan:'Jadwal distribusi sembako Ramadan: 15 Maret 2025, mulai pukul 08.00 di halaman masjid.', created_at:'2025-05-12 16:00:00' }],
|
||||
};
|
||||
|
||||
async function init() {
|
||||
// Load rooms
|
||||
let rooms = [];
|
||||
try { const d = await Api.get('/chat/rooms'); rooms = d?.length ? d : DEMO_ROOMS; }
|
||||
catch { rooms = DEMO_ROOMS; }
|
||||
|
||||
const container = ge('chat-rooms');
|
||||
container.innerHTML = `<button class="room-btn active" data-room="" onclick="Chat.loadRoom('',this)"># Umum</button>`;
|
||||
rooms.forEach(r => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'room-btn';
|
||||
btn.dataset.room = r.id;
|
||||
btn.textContent = r.nama;
|
||||
btn.onclick = function() { Chat.loadRoom(r.id, this); };
|
||||
container.appendChild(btn);
|
||||
});
|
||||
|
||||
await loadRoom(null, container.firstChild);
|
||||
}
|
||||
|
||||
async function loadRoom(roomId, btn) {
|
||||
currentRoom = roomId || null;
|
||||
clearInterval(pollTimer);
|
||||
document.querySelectorAll('.room-btn').forEach(b => b.classList.remove('active'));
|
||||
if (btn) btn.classList.add('active');
|
||||
await fetchMessages();
|
||||
pollTimer = setInterval(fetchMessages, 5000); // poll tiap 5 detik
|
||||
}
|
||||
|
||||
async function fetchMessages() {
|
||||
let msgs = [];
|
||||
try {
|
||||
const url = currentRoom ? `/chat?room=${currentRoom}` : '/chat';
|
||||
msgs = await Api.get(url);
|
||||
} catch {
|
||||
msgs = DEMO_MSGS[currentRoom] || DEMO_MSGS[null];
|
||||
}
|
||||
renderMessages(msgs);
|
||||
}
|
||||
|
||||
function renderMessages(msgs) {
|
||||
const box = ge('chat-messages');
|
||||
const user = App.currentUser();
|
||||
if (!msgs?.length) {
|
||||
box.innerHTML = '<div class="chat-empty">💬 Belum ada pesan di channel ini</div>';
|
||||
return;
|
||||
}
|
||||
const prevScroll = box.scrollHeight - box.scrollTop;
|
||||
box.innerHTML = msgs.map(m => {
|
||||
const mine = user && m.id_user == user.id;
|
||||
return `<div class="chat-bubble ${mine ? 'mine' : 'other'}">
|
||||
${!mine ? `<div class="chat-sender">${m.nama_lengkap || 'User'}</div>` : ''}
|
||||
<div class="chat-text">${escHtml(m.pesan)}</div>
|
||||
<div class="chat-time">${(m.created_at||'').substring(11,16)}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
// Scroll ke bawah jika sudah di bawah
|
||||
if (prevScroll < 200) box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
|
||||
async function send() {
|
||||
const inp = ge('chat-input');
|
||||
const pesan = inp.value.trim();
|
||||
if (!pesan) return;
|
||||
if (!App.currentUser()) { App.toast('Harus login untuk mengirim pesan','warning'); App.showModal('modal-login'); return; }
|
||||
inp.value = '';
|
||||
try {
|
||||
await Api.post('/chat', { pesan, id_room: currentRoom });
|
||||
await fetchMessages();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
function escHtml(s) {
|
||||
return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/\n/g,'<br>');
|
||||
}
|
||||
|
||||
return { init, loadRoom, send };
|
||||
})();
|
||||
@@ -0,0 +1,69 @@
|
||||
const Dashboard = (() => {
|
||||
async function load() {
|
||||
try {
|
||||
const d = await Api.get('/dashboard');
|
||||
const s = d.statistik;
|
||||
sv2('d-miskin', s.total_miskin); sv2('d-rentan', s.total_rentan);
|
||||
sv2('d-mampu', s.total_mampu); sv2('d-kk', s.total_kk);
|
||||
sv2('d-belum', d.belum_dibantu); sv2('d-darurat', s.laporan_pending);
|
||||
sv2('hp-miskin-n', s.total_miskin); sv2('hp-rentan-n', s.total_rentan);
|
||||
sv2('hp-mampu-n', s.total_mampu); sv2('hp-lap-n', s.laporan_pending);
|
||||
ge('dash-updated').textContent = 'Update: ' + new Date().toLocaleTimeString('id-ID');
|
||||
renderPrio(d.prioritas_darurat);
|
||||
renderBar('d-kec-chart', d.sebaran_kecamatan, r => ({ label: r.kecamatan||'?', val: +r.miskin+ +r.rentan, color:'#ff4d6d', sub:`${r.miskin} miskin` }));
|
||||
renderBar('d-ri-chart', d.beban_per_ri, r => ({ label: r.nama, val: +r.kk_miskin, color:'#4361ee', sub:`${r.jumlah_kk} KK` }));
|
||||
renderBar('d-trend-chart', d.trend_bantuan, r => ({ label: r.bulan, val: +r.jumlah, color:'#06d6a0', sub:`${r.jumlah}x` }));
|
||||
} catch { loadDemo(); }
|
||||
}
|
||||
|
||||
function loadDemo() {
|
||||
sv2('d-miskin',6); sv2('d-rentan',4); sv2('d-mampu',2);
|
||||
sv2('d-kk',8); sv2('d-belum',4); sv2('d-darurat',2);
|
||||
sv2('hp-miskin-n',6); sv2('hp-rentan-n',4); sv2('hp-mampu-n',2); sv2('hp-lap-n',2);
|
||||
ge('dash-updated').textContent = 'Data demo (offline)';
|
||||
renderPrio([
|
||||
{ nama_lengkap:'Maria Magdalena', umur:54, kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Selatan', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI', nik:'6171010101010006' },
|
||||
{ nama_lengkap:'Tan Eng Huat', umur:65, kondisi_kesehatan:'disabilitas', kecamatan:'Pontianak Kota', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI', nik:'6171010101010014' },
|
||||
{ nama_lengkap:'Lim Ah Kow', umur:59, kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Kota', jumlah_dibantu:1, level_prioritas:'TINGGI', nik:'6171010101010010' },
|
||||
]);
|
||||
renderBar('d-kec-chart',[{kecamatan:'Pontianak Timur',miskin:2,rentan:1},{kecamatan:'Pontianak Selatan',miskin:2,rentan:2},{kecamatan:'Pontianak Kota',miskin:2,rentan:1}],
|
||||
r=>({label:r.kecamatan,val:+r.miskin+ +r.rentan,color:'#ff4d6d',sub:`${r.miskin} miskin`}));
|
||||
renderBar('d-ri-chart',[{nama:"Masjid Jami'",jumlah_kk:3,kk_miskin:2},{nama:'Katedral',jumlah_kk:2,kk_miskin:2},{nama:'Mujahidin',jumlah_kk:1,kk_miskin:1}],
|
||||
r=>({label:r.nama,val:+r.kk_miskin,color:'#4361ee',sub:`${r.jumlah_kk} KK`}));
|
||||
renderBar('d-trend-chart',[{bulan:'2025-01',jumlah:2},{bulan:'2025-02',jumlah:1},{bulan:'2025-03',jumlah:3},{bulan:'2025-04',jumlah:1}],
|
||||
r=>({label:r.bulan,val:+r.jumlah,color:'#06d6a0',sub:`${r.jumlah}x`}));
|
||||
}
|
||||
|
||||
function renderPrio(list) {
|
||||
const el = ge('d-prio-list');
|
||||
if (!list?.length) { el.innerHTML = '<div class="empty-state"><div class="es-icon">✅</div><p>Tidak ada kasus darurat</p></div>'; return; }
|
||||
const cls = { 'SANGAT TINGGI':'pb-sangat-tinggi','TINGGI':'pb-tinggi','SEDANG':'pb-sedang','RENDAH':'pb-rendah' };
|
||||
el.innerHTML = list.map(p => `
|
||||
<div class="prio-item" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">
|
||||
<span class="prio-badge ${cls[p.level_prioritas]||'pb-rendah'}">${p.level_prioritas}</span>
|
||||
<div style="flex:1;min-width:0">
|
||||
<div style="font-weight:700;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${p.nama_lengkap}</div>
|
||||
<div style="font-size:11px;color:var(--muted)">${cap(p.kondisi_kesehatan)} · ${p.kecamatan} · dibantu ${p.jumlah_dibantu}x</div>
|
||||
</div>
|
||||
<span style="font-size:18px;flex-shrink:0">→</span>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function renderBar(elId, data, mapper) {
|
||||
const el = ge(elId);
|
||||
if (!data?.length) { el.innerHTML = '<p style="color:var(--muted);font-size:12px;padding:12px">Tidak ada data</p>'; return; }
|
||||
const mapped = data.map(mapper);
|
||||
const max = Math.max(...mapped.map(m => m.val), 1);
|
||||
el.innerHTML = mapped.map(m => {
|
||||
const pct = Math.round(m.val / max * 100);
|
||||
return `<div class="bar-row">
|
||||
<div class="bar-lbl" title="${m.label}">${m.label.slice(0,16)}${m.label.length>16?'…':''}</div>
|
||||
<div class="bar-track"><div class="bar-fill" style="width:${pct}%;background:${m.color}"></div></div>
|
||||
<div class="bar-val">${m.val}<span style="font-size:9px;color:var(--muted);margin-left:3px">${m.sub?.replace(/^\d+ /,'')}</span></div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function sv2(id, val) { const el = ge(id); if (el) el.textContent = val ?? '—'; }
|
||||
return { load };
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
const Keluarga = (() => {
|
||||
let all = [], editId = null;
|
||||
|
||||
const DEMO = [
|
||||
{ id:1, no_kk:'6171010101010001', nama_kepala:'Ahmad Suryana', kecamatan:'Pontianak Timur', kelurahan:'Dalam Bugis', jumlah_anggota:3, total_penghasilan:900000, nama_ri:"Masjid Jami'", status_ekonomi:'miskin', total_bantuan:2 },
|
||||
{ id:2, no_kk:'6171010101010002', nama_kepala:'Samsul Bahri', kecamatan:'Pontianak Kota', kelurahan:'Sungai Bangkong',jumlah_anggota:2,total_penghasilan:1700000, nama_ri:"Masjid Jami'", status_ekonomi:'rentan', total_bantuan:0 },
|
||||
{ id:3, no_kk:'6171010101010003', nama_kepala:'Maria Magdalena', kecamatan:'Pontianak Selatan', kelurahan:'Akcaya', jumlah_anggota:3, total_penghasilan:800000, nama_ri:'Katedral', status_ekonomi:'miskin', total_bantuan:1 },
|
||||
{ id:4, no_kk:'6171010101010004', nama_kepala:'Hendra Wijaya', kecamatan:'Pontianak Selatan', kelurahan:'Parit Tokaya', jumlah_anggota:2, total_penghasilan:1500000, nama_ri:'Mujahidin', status_ekonomi:'rentan', total_bantuan:0 },
|
||||
{ id:5, no_kk:'6171010101010005', nama_kepala:'Lim Ah Kow', kecamatan:'Pontianak Kota', kelurahan:'Darat Sekip', jumlah_anggota:2, total_penghasilan:700000, nama_ri:'Vihara', status_ekonomi:'miskin', total_bantuan:1 },
|
||||
{ id:6, no_kk:'6171010101010006', nama_kepala:'Mulyadi', kecamatan:'Pontianak Timur', kelurahan:'Dalam Bugis', jumlah_anggota:2, total_penghasilan:4500000, nama_ri:"Masjid Jami'", status_ekonomi:'mampu', total_bantuan:0 },
|
||||
{ id:7, no_kk:'6171010101010007', nama_kepala:'Yohanes Kristian',kecamatan:'Pontianak Selatan', kelurahan:'Akcaya', jumlah_anggota:2, total_penghasilan:1100000, nama_ri:'Katedral', status_ekonomi:'rentan', total_bantuan:0 },
|
||||
{ id:8, no_kk:'6171010101010008', nama_kepala:'Tan Eng Huat', kecamatan:'Pontianak Kota', kelurahan:'Pontianak Kota',jumlah_anggota:1,total_penghasilan:600000, nama_ri:'Klenteng', status_ekonomi:'miskin', total_bantuan:1 },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
try { all = await Api.get('/keluarga'); } catch { all = DEMO; }
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
function search(q) { applyFilter(); }
|
||||
|
||||
function applyFilter() {
|
||||
const q = gv('kf-search');
|
||||
const ek = gv('kf-ekonomi');
|
||||
const data = all.filter(k =>
|
||||
(!q || k.nama_kepala?.toLowerCase().includes(q.toLowerCase()) || k.no_kk?.includes(q)) &&
|
||||
(!ek || k.status_ekonomi === ek)
|
||||
);
|
||||
render(data);
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const tb = ge('tbody-keluarga');
|
||||
if (!data.length) { tb.innerHTML = '<tr><td colspan="9"><div class="empty-state"><div class="es-icon">🏠</div><h3>Tidak ada data</h3></div></td></tr>'; return; }
|
||||
tb.innerHTML = data.map(k => `
|
||||
<tr class="row-${k.status_ekonomi}">
|
||||
<td class="mono">${k.no_kk}</td>
|
||||
<td><strong>${k.nama_kepala}</strong><div style="font-size:10px;color:var(--muted)">${k.kelurahan||''}</div></td>
|
||||
<td>${k.kecamatan||'—'}</td>
|
||||
<td style="text-align:center;font-weight:700;font-size:15px">${k.jumlah_anggota||0}</td>
|
||||
<td class="mono" style="font-size:11px">${rp(k.total_penghasilan)}</td>
|
||||
<td>${k.nama_ri||'—'}</td>
|
||||
<td>${bdg(k.status_ekonomi,'')}</td>
|
||||
<td style="text-align:center">
|
||||
${k.total_bantuan > 0
|
||||
? `<span class="bdg bdg-sehat">✅ ${k.total_bantuan}x</span>`
|
||||
: `<span class="bdg bdg-darurat">❌ Belum</span>`}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:4px">
|
||||
<button class="btn btn-secondary btn-xs" onclick="Keluarga.showAnggota(${k.id})" title="Lihat anggota">👥</button>
|
||||
${App.canEdit() ? `
|
||||
<button class="btn btn-secondary btn-xs" onclick="Keluarga.showForm(${k.id})" title="Edit">✏️</button>
|
||||
<button class="btn btn-danger btn-xs" onclick="Keluarga.hapus(${k.id})" title="Hapus">🗑</button>` : ''}
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function showForm(id = null) {
|
||||
editId = id;
|
||||
ge('mk-title').textContent = id ? '✏️ Edit Data Keluarga' : '🏠 Tambah Kartu Keluarga';
|
||||
ge('fk-nokk').readOnly = !!id;
|
||||
// Load RI dropdown
|
||||
try {
|
||||
const riList = await Api.get('/rumah-ibadah');
|
||||
ge('fk-ri').innerHTML = '<option value="">— Pilih Rumah Ibadah —</option>' +
|
||||
riList.map(r => `<option value="${r.id}">${r.nama}</option>`).join('');
|
||||
} catch {
|
||||
ge('fk-ri').innerHTML = '<option value="1">Masjid Jami\' Pontianak</option>';
|
||||
}
|
||||
if (id) {
|
||||
const k = all.find(x => x.id == id) || {};
|
||||
sv('fk-nokk', k.no_kk); sv('fk-kepala', k.nama_kepala);
|
||||
sv('fk-alamat', k.alamat); sv('fk-kecamatan', k.kecamatan);
|
||||
sv('fk-kelurahan', k.kelurahan); sv('fk-ekonomi', k.status_ekonomi || 'rentan');
|
||||
sv('fk-lat', k.lat); sv('fk-lng', k.lng);
|
||||
if (k.id_rumah_ibadah) sv('fk-ri', k.id_rumah_ibadah);
|
||||
} else {
|
||||
['fk-nokk','fk-kepala','fk-alamat','fk-kecamatan','fk-kelurahan','fk-lat','fk-lng'].forEach(i => sv(i,''));
|
||||
sv('fk-ekonomi','rentan');
|
||||
}
|
||||
App.showModal('modal-keluarga');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const nokk = gv('fk-nokk');
|
||||
if (!nokk || nokk.length !== 16) { App.toast('No. KK harus 16 digit','error'); return; }
|
||||
if (!gv('fk-kepala')) { App.toast('Nama kepala wajib diisi','error'); return; }
|
||||
const body = {
|
||||
no_kk: nokk, nama_kepala: gv('fk-kepala'),
|
||||
id_rumah_ibadah: gv('fk-ri') || null,
|
||||
alamat: gv('fk-alamat'), kecamatan: gv('fk-kecamatan'),
|
||||
kelurahan: gv('fk-kelurahan'), status_ekonomi: gv('fk-ekonomi'),
|
||||
lat: gv('fk-lat') || null, lng: gv('fk-lng') || null,
|
||||
};
|
||||
try {
|
||||
if (editId) { await Api.put(`/keluarga/${editId}`, body); App.toast('Data KK diperbarui ✅','success'); }
|
||||
else { await Api.post('/keluarga', body); App.toast('KK ditambahkan ✅','success'); }
|
||||
App.closeModal('modal-keluarga');
|
||||
load();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function hapus(id) {
|
||||
if (!confirm('Hapus data keluarga ini?\nPastikan semua anggota sudah dipindah atau dihapus.')) return;
|
||||
try { await Api.delete(`/keluarga/${id}`); App.toast('Data KK dihapus','success'); load(); }
|
||||
catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function showAnggota(id) {
|
||||
const k = all.find(x => x.id == id);
|
||||
let anggota = [];
|
||||
try {
|
||||
const detail = await Api.get(`/keluarga/${id}`);
|
||||
anggota = detail.anggota || [];
|
||||
} catch {
|
||||
anggota = [];
|
||||
}
|
||||
if (!anggota.length) { App.toast('Tidak ada data anggota (atau belum terhubung ke API)','info'); return; }
|
||||
// Tampilkan dalam alert atau panel detail
|
||||
const info = anggota.map(a => `• ${a.nama_lengkap} (${a.umur}th) — ${cap(a.status_keluarga)} — ${a.status_ekonomi}`).join('\n');
|
||||
alert(`Anggota KK: ${k?.nama_kepala}\n\n${info}`);
|
||||
}
|
||||
|
||||
return { load, search, applyFilter, showForm, save, hapus, showAnggota };
|
||||
})();
|
||||
@@ -0,0 +1,96 @@
|
||||
const LaporanPanel = (() => {
|
||||
const DEMO = [
|
||||
{ id:1, nama_pelapor:'Anonim', deskripsi:'Ibu Maria sakit keras sudah seminggu, tidak bisa berobat karena tidak ada biaya. Butuh bantuan medis segera.', urgensi:'darurat', status:'diverifikasi', alamat_laporan:'Jl. Kemuning No.3, Akcaya', created_at:'2025-05-01T08:00:00', nama_ri:'Gereja Katedral' },
|
||||
{ id:2, nama_pelapor:'Eko Prasetyo', deskripsi:'Ada keluarga baru pindah ke Gg. Melati yang sepertinya sangat membutuhkan bantuan. Anak-anak tidak sekolah.', urgensi:'sedang', status:'pending', alamat_laporan:'Gg. Melati, Dalam Bugis', created_at:'2025-05-10T10:00:00', nama_ri:"Masjid Jami'" },
|
||||
{ id:3, nama_pelapor:'Anonim', deskripsi:'Pak Tan penyandang disabilitas, hidup sebatang kara dan sangat sulit mendapat penghasilan. Perlu perhatian.', urgensi:'tinggi', status:'diproses', alamat_laporan:'Jl. Gusti Situt No.7', created_at:'2025-05-12T14:00:00', nama_ri:'Klenteng Kwan Ti Miau' },
|
||||
{ id:4, nama_pelapor:'Hendra', deskripsi:'Balita 2 tahun kondisi gizi buruk. Orang tua tidak mampu memenuhi kebutuhan gizi anak.', urgensi:'darurat', status:'pending', alamat_laporan:'Gg. Teratai, Parit Tokaya', created_at:'2025-05-15T09:00:00', nama_ri:'Masjid Mujahidin' },
|
||||
];
|
||||
|
||||
const URG = { darurat:'🔴', tinggi:'🟠', sedang:'🟡', rendah:'🟢' };
|
||||
const URG_COLOR = { darurat:'var(--red)', tinggi:'var(--orange)', sedang:'var(--yellow)', rendah:'var(--green)' };
|
||||
const STATUS_LABEL = { pending:'⏳ Pending', diverifikasi:'✅ Diverifikasi', diproses:'🔄 Diproses', selesai:'🏁 Selesai' };
|
||||
|
||||
async function load() {
|
||||
const st = gv('lf-status');
|
||||
const ur = gv('lf-urgensi');
|
||||
let url = '/laporan';
|
||||
const p = []; if(st) p.push(`status=${st}`); if(ur) p.push(`urgensi=${ur}`);
|
||||
if(p.length) url += '?' + p.join('&');
|
||||
let data;
|
||||
try { data = await Api.get(url); } catch { data = DEMO; }
|
||||
if(st) data = data.filter(l => l.status === st);
|
||||
if(ur) data = data.filter(l => l.urgensi === ur);
|
||||
render(data);
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const el = ge('laporan-grid');
|
||||
if (!data.length) {
|
||||
el.innerHTML = '<div class="empty-state" style="grid-column:1/-1"><div class="es-icon">✅</div><h3>Tidak ada laporan</h3><p>Semua laporan sudah ditangani</p></div>';
|
||||
return;
|
||||
}
|
||||
el.innerHTML = data.map(l => `
|
||||
<div class="laporan-card ${l.urgensi}">
|
||||
<div class="lc-hdr">
|
||||
<div>
|
||||
<span class="bdg bdg-${l.urgensi}" style="margin-bottom:6px;display:inline-block">${URG[l.urgensi]||'📋'} ${(l.urgensi||'').toUpperCase()}</span>
|
||||
<div class="lc-title">#${l.id} — ${l.nama_pelapor||'Anonim'}</div>
|
||||
</div>
|
||||
<span class="bdg bdg-${l.status}">${STATUS_LABEL[l.status]||l.status}</span>
|
||||
</div>
|
||||
<div class="lc-desc">${l.deskripsi}</div>
|
||||
<div class="lc-meta">
|
||||
<span>📍 ${l.alamat_laporan||'—'}</span>
|
||||
<span>🕌 ${l.nama_ri||'—'}</span>
|
||||
<span>📅 ${(l.created_at||'').substring(0,10)}</span>
|
||||
</div>
|
||||
${App.canEdit() && l.status !== 'selesai' ? `
|
||||
<div class="lc-actions">
|
||||
${l.status === 'pending' ? `<button class="btn btn-secondary btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'diverifikasi')">✅ Verifikasi</button>` : ''}
|
||||
${l.status === 'diverifikasi' ? `<button class="btn btn-warning btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'diproses')">🔄 Proses</button>` : ''}
|
||||
${l.status !== 'selesai' ? `<button class="btn btn-success btn-sm" onclick="LaporanPanel.updateStatus(${l.id},'selesai')">🏁 Selesai</button>` : ''}
|
||||
<button class="btn btn-danger btn-sm" onclick="LaporanPanel.hapus(${l.id})">🗑</button>
|
||||
</div>` : ''}
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
async function updateStatus(id, status) {
|
||||
try {
|
||||
await Api.put(`/laporan/${id}`, { status });
|
||||
App.toast(`Status laporan diubah: ${STATUS_LABEL[status]||status}`, 'success');
|
||||
load(); App.updateHeaderStats();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function hapus(id) {
|
||||
if (!confirm('Hapus laporan ini?')) return;
|
||||
try { await Api.delete(`/laporan/${id}`); App.toast('Laporan dihapus','success'); load(); }
|
||||
catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
function showKirimModal() {
|
||||
['fl-nama','fl-hp','fl-nik','fl-alamat','fl-deskripsi'].forEach(id => sv(id,''));
|
||||
sv('fl-urgensi','sedang');
|
||||
App.showModal('modal-laporan');
|
||||
}
|
||||
|
||||
async function kirim() {
|
||||
if (!gv('fl-deskripsi')) { App.toast('Deskripsi kondisi wajib diisi','error'); return; }
|
||||
const body = {
|
||||
nama_pelapor: gv('fl-nama') || null,
|
||||
no_hp_pelapor: gv('fl-hp') || null,
|
||||
nik_terdampak: gv('fl-nik') || null,
|
||||
urgensi: gv('fl-urgensi'),
|
||||
alamat_laporan:gv('fl-alamat'),
|
||||
deskripsi: gv('fl-deskripsi'),
|
||||
};
|
||||
try {
|
||||
await Api.post('/laporan', body);
|
||||
App.closeModal('modal-laporan');
|
||||
App.toast('Laporan berhasil dikirim! Terima kasih 🙏','success');
|
||||
load(); App.updateHeaderStats();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
return { load, updateStatus, hapus, showKirimModal, kirim };
|
||||
})();
|
||||
@@ -0,0 +1,40 @@
|
||||
const LogPanel = (() => {
|
||||
const DEMO = [
|
||||
{ created_at:'2025-05-14 10:32:00', nama_lengkap:'Ahmad Fauzi', aksi:'create_histori_bantuan', tabel:'histori_bantuan', id_record:'5', ip_address:'192.168.1.10' },
|
||||
{ created_at:'2025-05-14 09:15:00', nama_lengkap:'Romo Benediktus', aksi:'update_laporan', tabel:'laporan', id_record:'1', ip_address:'192.168.1.20' },
|
||||
{ created_at:'2025-05-13 08:10:00', nama_lengkap:'Ahmad Fauzi', aksi:'update_penduduk', tabel:'penduduk', id_record:'6171010101010006', ip_address:'192.168.1.10' },
|
||||
{ created_at:'2025-05-13 08:00:00', nama_legendkap:'Ahmad Fauzi', aksi:'login', tabel:'user', id_record:'2', ip_address:'192.168.1.10' },
|
||||
{ created_at:'2025-05-12 16:45:00', nama_lengkap:'Administrator', aksi:'create_penduduk', tabel:'penduduk', id_record:'6171010101010014', ip_address:'127.0.0.1' },
|
||||
{ created_at:'2025-05-12 14:00:00', nama_lengkap:'Administrator', aksi:'delete_penduduk', tabel:'penduduk', id_record:'6171999999999999', ip_address:'127.0.0.1' },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
let data;
|
||||
try { data = await Api.get('/log'); } catch { data = DEMO; }
|
||||
render(data);
|
||||
}
|
||||
|
||||
function getAksiClass(aksi) {
|
||||
if (aksi?.startsWith('create')) return 'create';
|
||||
if (aksi?.startsWith('update')) return 'update';
|
||||
if (aksi?.startsWith('delete')) return 'delete';
|
||||
if (aksi === 'login' || aksi === 'logout') return 'login';
|
||||
return '';
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const tb = ge('tbody-log');
|
||||
if (!data.length) { tb.innerHTML = '<tr><td colspan="6"><div class="empty-state"><div class="es-icon">📋</div><h3>Tidak ada log</h3></div></td></tr>'; return; }
|
||||
tb.innerHTML = data.map(l => `
|
||||
<tr>
|
||||
<td class="mono" style="font-size:11px;white-space:nowrap">${(l.created_at||'').substring(0,16)}</td>
|
||||
<td style="font-weight:600">${l.nama_lengkap||'—'}</td>
|
||||
<td><span class="log-badge ${getAksiClass(l.aksi)}">${l.aksi}</span></td>
|
||||
<td style="color:var(--muted2);font-size:11px">${l.tabel||'—'}</td>
|
||||
<td class="mono" style="font-size:10px;color:var(--muted);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${(l.id_record||'').substring(0,20)}</td>
|
||||
<td style="font-size:11px;color:var(--muted)">${l.ip_address||'—'}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
return { load };
|
||||
})();
|
||||
@@ -0,0 +1,199 @@
|
||||
const MapApp = (() => {
|
||||
let map, clusterLayer, riLayer, laporanLayer, radiusLayer;
|
||||
let allPenduduk = [], allRI = [], allLaporan = [];
|
||||
let layerState = { penduduk:true, ri:true, laporan:false, radius:true };
|
||||
let ready = false;
|
||||
|
||||
const COLOR = { miskin:'#ff4d6d', rentan:'#ffd166', mampu:'#06d6a0' };
|
||||
const RI_EMOJI = { Masjid:'🕌', Gereja:'⛪', Vihara:'🛕', Pura:'🛕', Klenteng:'🏮', Lainnya:'🏛️' };
|
||||
|
||||
function mkIcon(color, emoji='') {
|
||||
return L.divIcon({
|
||||
className:'',
|
||||
html:`<div style="width:30px;height:30px;border-radius:50%;background:${color};display:flex;align-items:center;justify-content:center;font-size:13px;box-shadow:0 2px 8px rgba(0,0,0,.5);border:2px solid rgba(255,255,255,.25)">${emoji||'👤'}</div>`,
|
||||
iconSize:[30,30],iconAnchor:[15,15],popupAnchor:[0,-16]
|
||||
});
|
||||
}
|
||||
|
||||
function riIcon(emoji) {
|
||||
return L.divIcon({
|
||||
className:'',
|
||||
html:`<div style="font-size:28px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));line-height:1">${emoji}</div>`,
|
||||
iconSize:[34,34],iconAnchor:[17,17],popupAnchor:[0,-18]
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (ready) { setTimeout(()=>map.invalidateSize(),100); return; }
|
||||
ready = true;
|
||||
|
||||
map = L.map('map',{ center:[-0.028,109.338],zoom:13,zoomControl:true });
|
||||
|
||||
const tiles = {
|
||||
'Dark': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',{attribution:'© CARTO',maxZoom:19}),
|
||||
'Street': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© OSM',maxZoom:19}),
|
||||
'Satellite': L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:19}),
|
||||
};
|
||||
tiles['Dark'].addTo(map);
|
||||
L.control.layers(tiles,{},{collapsed:false,position:'topright'}).addTo(map);
|
||||
|
||||
clusterLayer = L.markerClusterGroup({ maxClusterRadius:60, showCoverageOnHover:false });
|
||||
riLayer = L.layerGroup();
|
||||
laporanLayer = L.layerGroup();
|
||||
radiusLayer = L.layerGroup();
|
||||
|
||||
map.addLayer(clusterLayer);
|
||||
map.addLayer(riLayer);
|
||||
map.addLayer(radiusLayer);
|
||||
|
||||
// Koordinat kursor
|
||||
const coordDiv = L.control({position:'bottomleft'});
|
||||
coordDiv.onAdd = ()=>{
|
||||
const d=L.DomUtil.create('div');
|
||||
d.style.cssText='background:rgba(8,12,20,.85);color:#aaa;padding:4px 12px;border-radius:6px;font-size:11px;font-family:JetBrains Mono,monospace;border:1px solid rgba(255,255,255,.1)';
|
||||
map.on('mousemove',e=>d.innerHTML=`${e.latlng.lat.toFixed(6)}, ${e.latlng.lng.toFixed(6)}`);
|
||||
return d;
|
||||
};
|
||||
coordDiv.addTo(map);
|
||||
|
||||
loadAll();
|
||||
}
|
||||
|
||||
async function loadAll() {
|
||||
try {
|
||||
const [gP, gR, gL] = await Promise.all([
|
||||
Api.get('/peta?layer=penduduk'),
|
||||
Api.get('/peta?layer=rumah_ibadah'),
|
||||
Api.get('/peta?layer=laporan'),
|
||||
]);
|
||||
allPenduduk = gP.features || [];
|
||||
allRI = gR.features || [];
|
||||
allLaporan = gL.features || [];
|
||||
} catch {
|
||||
// Demo data
|
||||
allPenduduk = buildDemoPenduduk();
|
||||
allRI = buildDemoRI();
|
||||
allLaporan = buildDemoLaporan();
|
||||
}
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderPenduduk(allPenduduk);
|
||||
renderRI(allRI);
|
||||
renderLaporan(allLaporan);
|
||||
}
|
||||
|
||||
function renderPenduduk(features) {
|
||||
clusterLayer.clearLayers();
|
||||
features.forEach(f => {
|
||||
const p = f.properties;
|
||||
const [lng,lat] = f.geometry.coordinates;
|
||||
const icon = mkIcon(COLOR[p.status_ekonomi]||COLOR.rentan);
|
||||
L.marker([lat,lng],{icon}).bindPopup(`
|
||||
<div class="map-popup">
|
||||
<div class="mp-title">🏠 ${p.nama_kepala||p.nama_lengkap}</div>
|
||||
<hr class="mp-divider">
|
||||
<div class="mp-row"><span>No. KK</span><strong class="mono">${p.no_kk||p.nik||'—'}</strong></div>
|
||||
<div class="mp-row"><span>Alamat</span><strong>${p.alamat||'—'}</strong></div>
|
||||
<div class="mp-row"><span>Anggota</span><strong>${p.jumlah_anggota||1} jiwa</strong></div>
|
||||
<div class="mp-row"><span>Status</span><strong style="color:${COLOR[p.status_ekonomi]}">${(p.status_ekonomi||'').toUpperCase()}</strong></div>
|
||||
<div class="mp-row"><span>Bantuan</span><strong>${p.total_bantuan||0}x diterima</strong></div>
|
||||
<div class="mp-row"><span>Rumah Ibadah</span><strong>${p.nama_ri||'—'}</strong></div>
|
||||
</div>`).addTo(clusterLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function renderRI(features) {
|
||||
riLayer.clearLayers(); radiusLayer.clearLayers();
|
||||
features.forEach(f => {
|
||||
const r = f.properties;
|
||||
const [lng,lat] = f.geometry.coordinates;
|
||||
const emoji = RI_EMOJI[r.jenis]||'🏛️';
|
||||
L.marker([lat,lng],{icon:riIcon(emoji)}).bindPopup(`
|
||||
<div class="map-popup">
|
||||
<div class="mp-title">${emoji} ${r.nama}</div>
|
||||
<div class="mp-row"><span>Jenis</span><strong>${r.jenis}</strong></div>
|
||||
<div class="mp-row"><span>Alamat</span><strong>${r.alamat||'—'}</strong></div>
|
||||
<div class="mp-row"><span>Kontak</span><strong>${r.kontak||'—'}</strong></div>
|
||||
<div class="mp-row"><span>Radius</span><strong>${r.radius_meter}m</strong></div>
|
||||
</div>`).addTo(riLayer);
|
||||
L.circle([lat,lng],{
|
||||
radius:r.radius_meter,color:'#4361ee',fillColor:'#4361ee',
|
||||
fillOpacity:.06,weight:1.5,dashArray:'6 4'
|
||||
}).addTo(radiusLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function renderLaporan(features) {
|
||||
laporanLayer.clearLayers();
|
||||
const UC = { darurat:'#ff4d6d', tinggi:'#ff8c42', sedang:'#ffd166', rendah:'#06d6a0' };
|
||||
const UE = { darurat:'🚨', tinggi:'⚠️', sedang:'📋', rendah:'📝' };
|
||||
features.forEach(f => {
|
||||
const l = f.properties;
|
||||
const [lng,lat] = f.geometry.coordinates;
|
||||
const icon = mkIcon(UC[l.urgensi]||'#888', UE[l.urgensi]||'📋');
|
||||
L.marker([lat,lng],{icon}).bindPopup(`
|
||||
<div class="map-popup">
|
||||
<div class="mp-title">${UE[l.urgensi]||'📋'} Laporan ${(l.urgensi||'').toUpperCase()}</div>
|
||||
<hr class="mp-divider">
|
||||
<div style="font-size:12px;color:#aaa;margin-bottom:8px">${l.deskripsi||'—'}</div>
|
||||
<div class="mp-row"><span>Alamat</span><strong>${l.alamat_laporan||'—'}</strong></div>
|
||||
<div class="mp-row"><span>Status</span><strong>${l.status||'—'}</strong></div>
|
||||
</div>`).addTo(laporanLayer);
|
||||
});
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
const status = ge('map-status-filter')?.value;
|
||||
const data = status ? allPenduduk.filter(f => f.properties.status_ekonomi === status) : allPenduduk;
|
||||
renderPenduduk(data);
|
||||
}
|
||||
|
||||
function toggleLayer(name, btn) {
|
||||
layerState[name] = !layerState[name];
|
||||
const on = layerState[name];
|
||||
const layerMap = { penduduk:clusterLayer, ri:riLayer, laporan:laporanLayer, radius:radiusLayer };
|
||||
const activeCls = { penduduk:'active-green', ri:'active-blue', laporan:'active-purple', radius:'active-orange' };
|
||||
on ? map.addLayer(layerMap[name]) : map.removeLayer(layerMap[name]);
|
||||
if (btn) {
|
||||
btn.classList.toggle(activeCls[name], on);
|
||||
}
|
||||
}
|
||||
|
||||
// Demo seed data (offline mode)
|
||||
function buildDemoPenduduk() {
|
||||
const seeds = [
|
||||
{ nama_kepala:'Ahmad Suryana', alamat:'Gg. Mawar No.5', lat:-0.027, lng:109.344, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010001', nama_ri:"Masjid Jami'", total_bantuan:2 },
|
||||
{ nama_kepala:'Samsul Bahri', alamat:'Jl. Veteran No.12', lat:-0.0315, lng:109.339, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010002', nama_ri:"Masjid Jami'", total_bantuan:0 },
|
||||
{ nama_kepala:'Maria Magdalena',alamat:'Jl. Kemuning No.3', lat:-0.033, lng:109.331, status_ekonomi:'miskin', jumlah_anggota:3, no_kk:'6171010101010003', nama_ri:'Katedral', total_bantuan:1 },
|
||||
{ nama_kepala:'Hendra Wijaya', alamat:'Jl. Ahmad Yani', lat:-0.049, lng:109.326, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010004', nama_ri:'Mujahidin', total_bantuan:0 },
|
||||
{ nama_kepala:'Lim Ah Kow', alamat:'Jl. Diponegoro', lat:-0.022, lng:109.337, status_ekonomi:'miskin', jumlah_anggota:2, no_kk:'6171010101010005', nama_ri:'Vihara', total_bantuan:1 },
|
||||
{ nama_kepala:'Mulyadi', alamat:'Gg. Teratai No.8', lat:-0.0265, lng:109.346, status_ekonomi:'mampu', jumlah_anggota:2, no_kk:'6171010101010006', nama_ri:"Masjid Jami'", total_bantuan:0 },
|
||||
{ nama_kepala:'Yohanes Kristian',alamat:'Jl. HOS Cokro', lat:-0.034, lng:109.330, status_ekonomi:'rentan', jumlah_anggota:2, no_kk:'6171010101010007', nama_ri:'Katedral', total_bantuan:0 },
|
||||
{ nama_kepala:'Tan Eng Huat', alamat:'Jl. Gusti Situt', lat:-0.020, lng:109.4415,status_ekonomi:'miskin', jumlah_anggota:1, no_kk:'6171010101010008', nama_ri:'Klenteng', total_bantuan:1 },
|
||||
];
|
||||
return seeds.map(s=>({type:'Feature',geometry:{type:'Point',coordinates:[s.lng,s.lat]},properties:s}));
|
||||
}
|
||||
|
||||
function buildDemoRI() {
|
||||
const ri = [
|
||||
{ nama:"Masjid Jami' Pontianak", jenis:'Masjid', lat:-0.02596, lng:109.34307, radius_meter:350, alamat:'Jl. Tanjungpura', kontak:'0561-732001' },
|
||||
{ nama:'Gereja Katedral Pontianak', jenis:'Gereja', lat:-0.03200, lng:109.33200, radius_meter:300, alamat:'Jl. Patimura No.9', kontak:'0561-766000' },
|
||||
{ nama:'Vihara Bodhisattva Karaniya Metta', jenis:'Vihara', lat:-0.02100, lng:109.33800, radius_meter:300, alamat:'Jl. Diponegoro', kontak:'0561-733456' },
|
||||
{ nama:'Klenteng Kwan Ti Miau', jenis:'Klenteng', lat:-0.01900, lng:109.34100, radius_meter:280, alamat:'Jl. Gusti Situt', kontak:'0561-740123' },
|
||||
{ nama:'Masjid Mujahidin Pontianak', jenis:'Masjid', lat:-0.04800, lng:109.32500, radius_meter:400, alamat:'Jl. Ahmad Yani', kontak:'0561-764567' },
|
||||
{ nama:'Gereja GPIB Immanuel', jenis:'Gereja', lat:-0.02800, lng:109.34500, radius_meter:300, alamat:'Jl. Rahadi Usman', kontak:'0561-735890' },
|
||||
];
|
||||
return ri.map(r=>({type:'Feature',geometry:{type:'Point',coordinates:[r.lng,r.lat]},properties:r}));
|
||||
}
|
||||
|
||||
function buildDemoLaporan() {
|
||||
return [
|
||||
{ type:'Feature',geometry:{type:'Point',coordinates:[109.3311,-0.0330]},properties:{ deskripsi:'Ibu Maria sakit keras, tidak bisa berobat', urgensi:'darurat', status:'diverifikasi', alamat_laporan:'Jl. Kemuning No.3' }},
|
||||
{ type:'Feature',geometry:{type:'Point',coordinates:[109.3415,-0.0200]},properties:{ deskripsi:'Pak Tan penyandang disabilitas butuh bantuan', urgensi:'tinggi', status:'diproses', alamat_laporan:'Jl. Gusti Situt No.7' }},
|
||||
];
|
||||
}
|
||||
|
||||
return { init, applyFilter, toggleLayer, loadAll };
|
||||
})();
|
||||
@@ -0,0 +1,217 @@
|
||||
const Penduduk = (() => {
|
||||
let all = [], filtered = [], editNIK = null;
|
||||
|
||||
const DEMO = [
|
||||
{ nik:'6171010101010001', nama_lengkap:'Ahmad Suryana', umur:44, status_keluarga:'kepala', pekerjaan:'Buruh Harian', penghasilan:900000, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||
{ nik:'6171010101010002', nama_lengkap:'Siti Rahma', umur:39, status_keluarga:'istri', pekerjaan:'Ibu RT', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_ringan',pendidikan_terakhir:'SD', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||
{ nik:'6171010101010003', nama_lengkap:'Budi Suryana', umur:12, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMP', no_kk:'6171010101010001', kecamatan:'Pontianak Timur' },
|
||||
{ nik:'6171010101010004', nama_lengkap:'Samsul Bahri', umur:49, status_keluarga:'kepala', pekerjaan:'Pedagang Kaki Lima',penghasilan:1200000,status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
|
||||
{ nik:'6171010101010005', nama_lengkap:'Dewi Kartika', umur:46, status_keluarga:'istri', pekerjaan:'Buruh Cuci', penghasilan:500000, status_ekonomi:'rentan', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SD', no_kk:'6171010101010002', kecamatan:'Pontianak Kota' },
|
||||
{ nik:'6171010101010006', nama_lengkap:'Maria Magdalena', umur:54, status_keluarga:'kepala', pekerjaan:'Penjual Gorengan',penghasilan:800000, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
|
||||
{ nik:'6171010101010007', nama_lengkap:'Petrus Yulius', umur:19, status_keluarga:'anak', pekerjaan:'Pelajar', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', pendidikan_terakhir:'SMA_SMK', no_kk:'6171010101010003', kecamatan:'Pontianak Selatan' },
|
||||
{ nik:'6171010101010010', nama_lengkap:'Lim Ah Kow', umur:59, status_keluarga:'kepala', pekerjaan:'Tidak Bekerja', penghasilan:0, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', pendidikan_terakhir:'SD', no_kk:'6171010101010005', kecamatan:'Pontianak Kota' },
|
||||
{ nik:'6171010101010012', nama_lengkap:'Mulyadi', umur:45, status_keluarga:'kepala', pekerjaan:'PNS Gol. III', penghasilan:4500000, status_ekonomi:'mampu', kondisi_kesehatan:'sehat', pendidikan_terakhir:'S1', no_kk:'6171010101010006', kecamatan:'Pontianak Timur' },
|
||||
{ nik:'6171010101010014', nama_lengkap:'Tan Eng Huat', umur:65, status_keluarga:'kepala', pekerjaan:'Pemulung', penghasilan:600000, status_ekonomi:'miskin', kondisi_kesehatan:'disabilitas', pendidikan_terakhir:'SD', no_kk:'6171010101010008', kecamatan:'Pontianak Kota' },
|
||||
];
|
||||
|
||||
async function load() {
|
||||
try { all = await Api.get('/penduduk'); } catch { all = DEMO; }
|
||||
filtered = [...all];
|
||||
render(filtered);
|
||||
ge('pf-count').textContent = `${all.length} total`;
|
||||
}
|
||||
|
||||
function search(q) { applyFilter(q); }
|
||||
function applyFilter(qOverride) {
|
||||
const q = qOverride ?? gv('pf-search');
|
||||
const ek = gv('pf-ekonomi');
|
||||
const ke = gv('pf-kesehatan');
|
||||
filtered = all.filter(p =>
|
||||
(!q || p.nama_lengkap?.toLowerCase().includes(q.toLowerCase()) || p.nik?.includes(q)) &&
|
||||
(!ek || p.status_ekonomi === ek) &&
|
||||
(!ke || p.kondisi_kesehatan === ke)
|
||||
);
|
||||
render(filtered);
|
||||
ge('pf-count').textContent = `${filtered.length} / ${all.length}`;
|
||||
}
|
||||
|
||||
const pendidikanLabel = { tidak_sekolah:'Tdk Sekolah', SD:'SD', SMP:'SMP', SMA_SMK:'SMA/SMK', D1_D3:'D3', S1:'S1', S2_S3:'S2/S3' };
|
||||
|
||||
function render(data) {
|
||||
const tb = ge('tbody-penduduk');
|
||||
if (!data.length) { tb.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">🔍</div><h3>Tidak ada data</h3><p>Coba ubah filter pencarian</p></div></td></tr>'; return; }
|
||||
tb.innerHTML = data.map(p => `
|
||||
<tr class="row-${p.status_ekonomi}">
|
||||
<td class="mono">${p.nik}</td>
|
||||
<td><strong>${p.nama_lengkap}</strong><div style="font-size:10px;color:var(--muted)">${p.kecamatan||''}</div></td>
|
||||
<td>${p.umur||'?'} th</td>
|
||||
<td>${cap(p.status_keluarga)}</td>
|
||||
<td>${p.pekerjaan||'—'}</td>
|
||||
<td style="white-space:nowrap;font-family:'JetBrains Mono',monospace;font-size:11px">${rp(p.penghasilan)}</td>
|
||||
<td>${bdg(p.status_ekonomi,'')}</td>
|
||||
<td>${bdg(p.kondisi_kesehatan,'')}</td>
|
||||
<td>${pendidikanLabel[p.pendidikan_terakhir]||p.pendidikan_terakhir||'—'}</td>
|
||||
<td>
|
||||
<div style="display:flex;gap:4px">
|
||||
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showDetail('${p.nik}')" title="Detail">👁</button>
|
||||
<button class="btn btn-secondary btn-xs" onclick="Penduduk.showForm('${p.nik}')" title="Edit" ${!App.canEdit()?'style="display:none"':''}>✏️</button>
|
||||
<button class="btn btn-danger btn-xs" onclick="Penduduk.hapus('${p.nik}')" title="Hapus" ${!App.canEdit()?'style="display:none"':''}>🗑</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
async function showForm(nik = null) {
|
||||
editNIK = nik;
|
||||
ge('mp-title').textContent = nik ? `✏️ Edit — ${nik}` : '➕ Tambah Penduduk Baru';
|
||||
ge('fp-nik').readOnly = !!nik;
|
||||
await loadKKDropdown();
|
||||
if (nik) {
|
||||
try {
|
||||
const p = await Api.get(`/penduduk/${nik}`);
|
||||
fillForm(p);
|
||||
} catch {
|
||||
const p = all.find(x => x.nik === nik);
|
||||
if (p) fillForm(p);
|
||||
}
|
||||
} else {
|
||||
['fp-nik','fp-nama','fp-tempat','fp-pekerjaan','fp-ket-sehat'].forEach(id => sv(id,''));
|
||||
sv('fp-penghasilan','0'); sv('fp-umur','');
|
||||
ge('fp-warn').classList.add('hidden');
|
||||
}
|
||||
App.showModal('modal-penduduk');
|
||||
}
|
||||
|
||||
function fillForm(p) {
|
||||
sv('fp-nik', p.nik); sv('fp-nama', p.nama_lengkap);
|
||||
sv('fp-jkel', p.jenis_kelamin); sv('fp-tgl', p.tanggal_lahir?.substring(0,10));
|
||||
sv('fp-umur', p.umur ? `${p.umur} tahun` : '');
|
||||
sv('fp-tempat', p.tempat_lahir); sv('fp-agama', p.agama);
|
||||
sv('fp-status-kk', p.status_keluarga); sv('fp-kawin', p.status_perkawinan);
|
||||
sv('fp-hidup', p.status_hidup); sv('fp-ekonomi', p.status_ekonomi);
|
||||
sv('fp-pekerjaan', p.pekerjaan); sv('fp-penghasilan', p.penghasilan || 0);
|
||||
sv('fp-pendidikan', p.pendidikan_terakhir); sv('fp-stat-pend', p.status_pendidikan);
|
||||
sv('fp-kesehatan', p.kondisi_kesehatan); sv('fp-ket-sehat', p.keterangan_kesehatan);
|
||||
if (p.id_keluarga) sv('fp-kk', p.id_keluarga);
|
||||
}
|
||||
|
||||
async function loadKKDropdown() {
|
||||
const sel = ge('fp-kk');
|
||||
try {
|
||||
const list = await Api.get('/keluarga');
|
||||
sel.innerHTML = list.map(k => `<option value="${k.id}">${k.no_kk} — ${k.nama_kepala}</option>`).join('');
|
||||
} catch {
|
||||
sel.innerHTML = '<option value="1">Demo — Ahmad Suryana</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function hitungUmur() {
|
||||
const tgl = gv('fp-tgl');
|
||||
if (!tgl) return;
|
||||
const umur = Math.floor((Date.now() - new Date(tgl)) / (365.25*24*3600*1000));
|
||||
sv('fp-umur', `${umur} tahun`);
|
||||
// Validasi
|
||||
const pend = gv('fp-pendidikan'), stat = gv('fp-stat-pend');
|
||||
const warns = [];
|
||||
if (umur >= 7 && umur <= 15 && stat === 'tidak_sekolah')
|
||||
warns.push(`⚠️ Usia ${umur} tahun — seharusnya masih wajib belajar (SD/SMP)`);
|
||||
if (umur >= 16 && umur <= 18 && stat === 'tidak_sekolah' && ['tidak_sekolah','SD'].includes(pend))
|
||||
warns.push(`⚠️ Usia ${umur} tahun — disarankan melanjutkan ke SMA/SMK`);
|
||||
const wEl = ge('fp-warn');
|
||||
if (warns.length) { wEl.innerHTML = warns.join('<br>'); wEl.classList.remove('hidden'); }
|
||||
else wEl.classList.add('hidden');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
const nik = gv('fp-nik');
|
||||
if (!nik || nik.length !== 16 || !/^\d+$/.test(nik)) { App.toast('NIK harus 16 digit angka','error'); return; }
|
||||
if (!gv('fp-nama')) { App.toast('Nama lengkap wajib diisi','error'); return; }
|
||||
if (!gv('fp-tgl')) { App.toast('Tanggal lahir wajib diisi','error'); return; }
|
||||
const body = {
|
||||
nik, id_keluarga: gv('fp-kk'),
|
||||
nama_lengkap: gv('fp-nama'), jenis_kelamin: gv('fp-jkel'),
|
||||
tanggal_lahir: gv('fp-tgl'), tempat_lahir: gv('fp-tempat'),
|
||||
agama: gv('fp-agama'), status_keluarga: gv('fp-status-kk'),
|
||||
status_perkawinan: gv('fp-kawin'), status_hidup: gv('fp-hidup'),
|
||||
pekerjaan: gv('fp-pekerjaan'), penghasilan: +gv('fp-penghasilan')||0,
|
||||
status_ekonomi: gv('fp-ekonomi'), pendidikan_terakhir: gv('fp-pendidikan'),
|
||||
status_pendidikan: gv('fp-stat-pend'), kondisi_kesehatan: gv('fp-kesehatan'),
|
||||
keterangan_kesehatan: gv('fp-ket-sehat'),
|
||||
};
|
||||
try {
|
||||
if (editNIK) { await Api.put(`/penduduk/${editNIK}`, body); App.toast('Data diperbarui ✅','success'); }
|
||||
else { await Api.post('/penduduk', body); App.toast('Penduduk ditambahkan ✅','success'); }
|
||||
App.closeModal('modal-penduduk');
|
||||
await load();
|
||||
App.updateHeaderStats();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function hapus(nik) {
|
||||
if (!confirm(`Hapus data penduduk NIK ${nik}?\nTindakan ini tidak dapat dibatalkan.`)) return;
|
||||
try {
|
||||
await Api.delete(`/penduduk/${nik}`);
|
||||
App.toast('Data dihapus','success');
|
||||
load(); App.updateHeaderStats();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function showDetail(nik) {
|
||||
ge('dp-title').textContent = 'Memuat…';
|
||||
ge('detail-panel').classList.add('open');
|
||||
let p;
|
||||
try { p = await Api.get(`/penduduk/${nik}`); }
|
||||
catch { p = all.find(x => x.nik === nik); }
|
||||
if (!p) { App.toast('Gagal memuat detail','error'); return; }
|
||||
ge('dp-title').textContent = p.nama_lengkap;
|
||||
const histori = p.histori_bantuan || [];
|
||||
ge('dp-body').innerHTML = `
|
||||
<div class="dp-section">
|
||||
<div class="dp-section-title">Identitas</div>
|
||||
${field('NIK', `<span class="mono">${p.nik}</span>`)}
|
||||
${field('Jenis Kelamin', p.jenis_kelamin === 'L' ? 'Laki-laki' : 'Perempuan')}
|
||||
${field('Umur', `${p.umur||'?'} tahun`)}
|
||||
${field('Tempat Lahir', p.tempat_lahir||'—')}
|
||||
${field('Agama', p.agama||'—')}
|
||||
${field('Status KK', cap(p.status_keluarga))}
|
||||
${field('Perkawinan', cap(p.status_perkawinan))}
|
||||
</div>
|
||||
<div class="dp-section">
|
||||
<div class="dp-section-title">Sosial Ekonomi</div>
|
||||
${field('Pekerjaan', p.pekerjaan||'—')}
|
||||
${field('Penghasilan', rp(p.penghasilan))}
|
||||
${field('Status Ekonomi', bdg(p.status_ekonomi,''))}
|
||||
${field('Pendidikan', p.pendidikan_terakhir?.replace('_','/')||'—')}
|
||||
${field('Kesehatan', bdg(p.kondisi_kesehatan,''))}
|
||||
</div>
|
||||
<div class="dp-section">
|
||||
<div class="dp-section-title">Lokasi</div>
|
||||
${field('Kelurahan', p.kelurahan||'—')}
|
||||
${field('Kecamatan', p.kecamatan||'—')}
|
||||
${field('Alamat', p.alamat||'—')}
|
||||
${field('Rumah Ibadah', p.nama_ri||'—')}
|
||||
</div>
|
||||
<div class="dp-section">
|
||||
<div class="dp-section-title">Riwayat Bantuan (${histori.length}x)</div>
|
||||
${histori.length ? histori.map(h=>`
|
||||
<div style="background:var(--bg4);border-radius:8px;padding:8px 10px;margin-bottom:6px;font-size:12px">
|
||||
<strong>${h.nama_bantuan}</strong><span style="color:var(--muted);font-size:11px;float:right">${h.tanggal?.substring(0,10)}</span>
|
||||
<div style="color:var(--muted);font-size:11px">${h.nama_ri||''} · ${h.jumlah} ${h.satuan||''}</div>
|
||||
</div>`).join('') : '<p style="color:var(--muted);font-size:12px">Belum pernah menerima bantuan</p>'}
|
||||
</div>
|
||||
${App.canEdit() ? `
|
||||
<div style="display:flex;gap:8px;margin-top:8px">
|
||||
<button class="btn btn-primary" style="flex:1" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">📦 Salurkan Bantuan</button>
|
||||
<button class="btn btn-secondary" onclick="Penduduk.showForm('${p.nik}')">✏️ Edit</button>
|
||||
</div>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function field(lbl, val) {
|
||||
return `<div class="dp-field"><span class="dp-field-lbl">${lbl}</span><span class="dp-field-val">${val}</span></div>`;
|
||||
}
|
||||
|
||||
function closeDetail() { ge('detail-panel').classList.remove('open'); }
|
||||
|
||||
return { load, search, applyFilter, showForm, hitungUmur, save, hapus, showDetail, closeDetail };
|
||||
})();
|
||||
@@ -0,0 +1,59 @@
|
||||
const PrioritasPanel = (() => {
|
||||
let all = [];
|
||||
|
||||
const DEMO = [
|
||||
{ nik:'6171010101010006', nama_lengkap:'Maria Magdalena', umur:54, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Selatan', nama_ri:'Katedral', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||
{ nik:'6171010101010014', nama_lengkap:'Tan Eng Huat', umur:65, status_ekonomi:'miskin', kondisi_kesehatan:'disabilitas', kecamatan:'Pontianak Kota', nama_ri:'Klenteng', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||
{ nik:'6171010101010010', nama_lengkap:'Lim Ah Kow', umur:59, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_berat', kecamatan:'Pontianak Kota', nama_ri:'Vihara', jumlah_dibantu:1, level_prioritas:'SANGAT TINGGI' },
|
||||
{ nik:'6171010101010002', nama_lengkap:'Siti Rahma', umur:39, status_ekonomi:'miskin', kondisi_kesehatan:'sakit_ringan',kecamatan:'Pontianak Timur', nama_ri:"Masjid Jami'", jumlah_dibantu:2, level_prioritas:'TINGGI' },
|
||||
{ nik:'6171010101010001', nama_lengkap:'Ahmad Suryana', umur:44, status_ekonomi:'miskin', kondisi_kesehatan:'sehat', kecamatan:'Pontianak Timur', nama_ri:"Masjid Jami'", jumlah_dibantu:2, level_prioritas:'SEDANG' },
|
||||
{ nik:'6171010101010005', nama_lengkap:'Dewi Kartika', umur:46, status_ekonomi:'rentan', kondisi_kesehatan:'sehat', kecamatan:'Pontianak Kota', nama_ri:"Masjid Jami'", jumlah_dibantu:0, level_prioritas:'SEDANG' },
|
||||
];
|
||||
|
||||
const PRIO_CLS = { 'SANGAT TINGGI':'pb-sangat-tinggi', 'TINGGI':'pb-tinggi', 'SEDANG':'pb-sedang', 'RENDAH':'pb-rendah' };
|
||||
|
||||
async function load() {
|
||||
try { all = await Api.get('/prioritas'); } catch { all = DEMO; }
|
||||
render(all);
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
const tb = ge('tbody-prioritas');
|
||||
if (!data.length) { tb.innerHTML = '<tr><td colspan="10"><div class="empty-state"><div class="es-icon">✅</div><h3>Tidak ada warga prioritas</h3></div></td></tr>'; return; }
|
||||
tb.innerHTML = data.map(p => `
|
||||
<tr>
|
||||
<td class="mono">${p.nik}</td>
|
||||
<td><strong>${p.nama_lengkap}</strong></td>
|
||||
<td>${p.umur} th</td>
|
||||
<td>${bdg(p.status_ekonomi,'')}</td>
|
||||
<td>${bdg(p.kondisi_kesehatan,'')}</td>
|
||||
<td>${p.kecamatan||'—'}</td>
|
||||
<td style="font-size:11px">${p.nama_ri||'—'}</td>
|
||||
<td style="text-align:center">
|
||||
${p.jumlah_dibantu > 0
|
||||
? `<span style="font-weight:700;color:var(--green)">${p.jumlah_dibantu}x</span>`
|
||||
: `<span style="font-weight:700;color:var(--red)">Belum</span>`}
|
||||
</td>
|
||||
<td><span class="prio-badge ${PRIO_CLS[p.level_prioritas]||''}">${p.level_prioritas}</span></td>
|
||||
<td>
|
||||
${App.canEdit() ? `
|
||||
<button class="btn btn-primary btn-xs" onclick="Bantuan.quickSalur('${p.nik}','${p.nama_lengkap}')">📦 Bantu</button>
|
||||
` : '—'}
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
if (!all.length) { App.toast('Tidak ada data untuk diekspor','warning'); return; }
|
||||
const hdr = ['NIK','Nama','Umur','Ekonomi','Kesehatan','Kecamatan','Rumah Ibadah','Dibantu','Prioritas'];
|
||||
const rows = all.map(p => [p.nik,p.nama_lengkap,p.umur,p.status_ekonomi,p.kondisi_kesehatan,p.kecamatan,p.nama_ri,p.jumlah_dibantu,p.level_prioritas]);
|
||||
const csv = '\uFEFF' + [hdr,...rows].map(r => r.map(v => `"${v||''}"`).join(',')).join('\n');
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(new Blob([csv],{type:'text/csv;charset=utf-8'}));
|
||||
a.download = `prioritas-bantuan-${new Date().toISOString().substring(0,10)}.csv`;
|
||||
a.click();
|
||||
App.toast('File CSV berhasil diunduh ✅','success');
|
||||
}
|
||||
|
||||
return { load, exportCSV };
|
||||
})();
|
||||
@@ -0,0 +1,87 @@
|
||||
const RI = (() => {
|
||||
let all = [], editId = null;
|
||||
|
||||
const DEMO = [
|
||||
{ id:1, nama:"Masjid Jami' Pontianak", jenis:'Masjid', kecamatan:'Pontianak Timur', kontak:'0561-732001', radius_meter:350, jumlah_kk:3, jumlah_penduduk:8, lat:-0.02596, lng:109.34307 },
|
||||
{ id:2, nama:'Gereja Katedral Pontianak', jenis:'Gereja', kecamatan:'Pontianak Selatan', kontak:'0561-766000', radius_meter:300, jumlah_kk:2, jumlah_penduduk:5, lat:-0.03200, lng:109.33200 },
|
||||
{ id:3, nama:'Vihara Bodhisattva Karaniya Metta', jenis:'Vihara', kecamatan:'Pontianak Kota', kontak:'0561-733456', radius_meter:300, jumlah_kk:2, jumlah_penduduk:4, lat:-0.02100, lng:109.33800 },
|
||||
{ id:4, nama:'Klenteng Kwan Ti Miau', jenis:'Klenteng', kecamatan:'Pontianak Kota', kontak:'0561-740123', radius_meter:280, jumlah_kk:1, jumlah_penduduk:2, lat:-0.01900, lng:109.34100 },
|
||||
{ id:5, nama:'Masjid Mujahidin Pontianak', jenis:'Masjid', kecamatan:'Pontianak Selatan', kontak:'0561-764567', radius_meter:400, jumlah_kk:1, jumlah_penduduk:2, lat:-0.04800, lng:109.32500 },
|
||||
{ id:6, nama:'Gereja GPIB Immanuel', jenis:'Gereja', kecamatan:'Pontianak Kota', kontak:'0561-735890', radius_meter:300, jumlah_kk:1, jumlah_penduduk:1, lat:-0.02800, lng:109.34500 },
|
||||
];
|
||||
|
||||
const JENIS_EMOJI = { Masjid:'🕌', Gereja:'⛪', Vihara:'🛕', Pura:'🛕', Klenteng:'🏮', Lainnya:'🏛️' };
|
||||
const JENIS_COLOR = { Masjid:'#06d6a0', Gereja:'#4cc9f0', Vihara:'#ffd166', Pura:'#f97316', Klenteng:'#b48eff', Lainnya:'#6b7a99' };
|
||||
|
||||
async function load() {
|
||||
try { all = await Api.get('/rumah-ibadah'); } catch { all = DEMO; }
|
||||
render(all);
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
ge('ri-grid').innerHTML = data.map(r => `
|
||||
<div class="ri-card" style="border-top:3px solid ${JENIS_COLOR[r.jenis]||'#6b7a99'}">
|
||||
<div class="ri-icon-big">${JENIS_EMOJI[r.jenis]||'🏛️'}</div>
|
||||
<div class="ri-name">${r.nama}</div>
|
||||
<div class="ri-type">${r.jenis} · ${r.kecamatan||'—'}</div>
|
||||
<div class="ri-stats">
|
||||
<div class="ri-stat">
|
||||
<div class="ri-stat-num" style="color:${JENIS_COLOR[r.jenis]}">${r.jumlah_kk||0}</div>
|
||||
<div class="ri-stat-lbl">KK Binaan</div>
|
||||
</div>
|
||||
<div class="ri-stat">
|
||||
<div class="ri-stat-num" style="color:var(--blue)">${r.jumlah_penduduk||0}</div>
|
||||
<div class="ri-stat-lbl">Jiwa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="font-size:11px;color:var(--muted);margin-bottom:10px">
|
||||
📞 ${r.kontak||'—'} · ⭕ Radius ${r.radius_meter}m
|
||||
</div>
|
||||
${App.canEdit() ? `
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="btn btn-secondary btn-sm" style="flex:1" onclick="RI.showForm(${r.id})">✏️ Edit</button>
|
||||
<button class="btn btn-danger btn-sm" onclick="RI.hapus(${r.id})">🗑</button>
|
||||
</div>` : ''}
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
async function showForm(id = null) {
|
||||
editId = id;
|
||||
ge('mri-title').textContent = id ? '✏️ Edit Rumah Ibadah' : '🕌 Tambah Rumah Ibadah';
|
||||
if (id) {
|
||||
const r = all.find(x => x.id == id) || {};
|
||||
sv('fri-nama', r.nama); sv('fri-jenis', r.jenis);
|
||||
sv('fri-radius', r.radius_meter||300); sv('fri-lat', r.lat);
|
||||
sv('fri-lng', r.lng); sv('fri-kec', r.kecamatan);
|
||||
sv('fri-kontak', r.kontak); sv('fri-alamat', r.alamat);
|
||||
} else {
|
||||
['fri-nama','fri-kec','fri-kontak','fri-alamat'].forEach(i => sv(i,''));
|
||||
sv('fri-radius','300'); sv('fri-lat',''); sv('fri-lng','');
|
||||
}
|
||||
App.showModal('modal-ri');
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!gv('fri-nama')) { App.toast('Nama wajib diisi','error'); return; }
|
||||
if (!gv('fri-lat') || !gv('fri-lng')) { App.toast('Koordinat (lat/lng) wajib diisi','error'); return; }
|
||||
const body = {
|
||||
nama: gv('fri-nama'), jenis: gv('fri-jenis'),
|
||||
radius_meter: +gv('fri-radius')||300,
|
||||
lat: +gv('fri-lat'), lng: +gv('fri-lng'),
|
||||
kecamatan: gv('fri-kec'), kontak: gv('fri-kontak'), alamat: gv('fri-alamat'),
|
||||
};
|
||||
try {
|
||||
if (editId) { await Api.put(`/rumah-ibadah/${editId}`, body); App.toast('Data diperbarui ✅','success'); }
|
||||
else { await Api.post('/rumah-ibadah', body); App.toast('Rumah ibadah ditambahkan ✅','success'); }
|
||||
App.closeModal('modal-ri'); load();
|
||||
} catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
async function hapus(id) {
|
||||
if (!confirm('Nonaktifkan rumah ibadah ini?')) return;
|
||||
try { await Api.delete(`/rumah-ibadah/${id}`); App.toast('Dinonaktifkan','success'); load(); }
|
||||
catch(e) { App.toast(e.message,'error'); }
|
||||
}
|
||||
|
||||
return { load, showForm, save, hapus, getAll: () => all };
|
||||
})();
|
||||
Reference in New Issue
Block a user