Files
2026-06-10 18:54:25 +07:00

1997 lines
97 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// assets/js/fitur_baru.js
// Modul 3 Fitur Baru WebGIS Bansos
// Integrasi dengan App (app.js) yang sudah ada.
// Cara pakai: <script src="assets/js/fitur_baru.js"></script> SETELAH app.js
const FiturBaru = (() => {
/* ══════════════════════════════════════════════════
SHARED HELPERS
══════════════════════════════════════════════════ */
function svgIcon(paths, w = 14, h = 14) {
return `<svg viewBox="0 0 24 24" width="${w}" height="${h}" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
}
const ICONS = {
swap: `<path d="M7 16V4m0 0L3 8m4-4 4 4"/><path d="M17 8v12m0 0 4-4m-4 4-4-4"/>`,
shield: `<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>`,
trend: `<polyline points="22 7 13.5 15.5 8.5 10.5 2 17"/><polyline points="16 7 22 7 22 13"/>`,
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
check2: `<polyline points="20 6 9 17 4 12"/>`,
alert: `<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
gear: `<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>`,
exit: `<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>`,
chart: `<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>`,
refresh: `<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>`,
};
function formatRp(n) {
return 'Rp ' + parseInt(n || 0).toLocaleString('id-ID');
}
function toast(msg, type = 'info') {
// reuse toast dari app.js jika ada
if (window.App && typeof App.showToast === 'function') {
App.showToast(msg, type);
} else {
const c = document.getElementById('toast-container');
if (!c) return;
const t = document.createElement('div');
t.className = `toast ${type}`;
t.textContent = msg;
c.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 250); }, 3500);
}
}
function showModal(id) { document.getElementById(id)?.classList.add('show'); }
function closeModal(id) { document.getElementById(id)?.classList.remove('show'); }
/* ══════════════════════════════════════════════════
░░ FITUR 1: REDISTRIBUSI BANTUAN ░░
══════════════════════════════════════════════════ */
const Redistribusi = (() => {
let analisisData = null;
let saranData = [];
const BEBAN_CONFIG = {
kosong: { label: 'Kosong', color: '#94a3b8', bg: 'rgba(148,163,184,0.12)' },
ringan: { label: 'Ringan', color: '#10b981', bg: 'rgba(16,185,129,0.12)' },
normal: { label: 'Normal', color: '#6366f1', bg: 'rgba(99,102,241,0.12)' },
padat: { label: 'Padat', color: '#f59e0b', bg: 'rgba(245,158,11,0.12)' },
overload:{ label: 'Overload', color: '#ef4444', bg: 'rgba(239,68,68,0.12)' },
};
const PRIORITAS_CONFIG = {
tinggi: { label: 'Prioritas Tinggi', cls: 'badge-sangat-miskin' },
sedang: { label: 'Prioritas Sedang', cls: 'badge-miskin' },
rendah: { label: 'Prioritas Rendah', cls: 'badge-rentan-miskin' },
};
async function loadAnalisis() {
const container = document.getElementById('redistribusi-analisis');
if (!container) return;
container.innerHTML = `<div class="fb-loading">${svgIcon(ICONS.refresh, 18, 18)}<span>Menganalisis beban distribusi...</span></div>`;
const summaryContainer = document.getElementById('redistribusi-summary-container');
if (summaryContainer) summaryContainer.innerHTML = '';
try {
const res = await fetch('api/redistribusi.php?action=analisis');
const json = await res.json();
if (!json.success) throw new Error(json.message);
analisisData = json.data;
saranData = json.saran;
renderAnalisis(json.data, json.summary);
renderSaran(json.saran);
renderRiwayat();
} catch (e) {
container.innerHTML = `<div class="fb-error">${svgIcon(ICONS.alert,16,16)} Gagal memuat: ${e.message}</div>`;
}
}
let cachedSummaryHtml = '';
function renderAnalisis(data, summary) {
const container = document.getElementById('redistribusi-analisis');
if (!container) return;
if (summary) {
const summaryContainer = document.getElementById('redistribusi-summary-container');
if (summaryContainer) {
summaryContainer.innerHTML = `
<div class="redistribusi-summary">
${Object.entries(summary).filter(([k]) => k !== 'total_ibadah').map(([k, v]) => {
const cfg = BEBAN_CONFIG[k] || { color: '#64748b', bg: 'rgba(100,116,139,0.1)' };
return `<div class="redist-sum-item" style="background:${cfg.bg};border-color:${cfg.color}20">
<span class="redist-sum-val" style="color:${cfg.color}">${v}</span>
<span class="redist-sum-key">${cfg.label || k}</span>
</div>`;
}).join('')}
</div>`;
}
}
// Filter locally based on search
const searchVal = document.getElementById('redistribusi-search')?.value.toLowerCase() || '';
const filtered = data ? data.filter(d => {
return !searchVal ||
d.ibadah.nama.toLowerCase().includes(searchVal) ||
(d.ibadah.kelurahan || '').toLowerCase().includes(searchVal) ||
(d.ibadah.kecamatan || '').toLowerCase().includes(searchVal);
}) : [];
const cardsHtml = filtered.map(d => {
const cfg = BEBAN_CONFIG[d.status_beban] || BEBAN_CONFIG.normal;
const pct = Math.min(d.pct_beban, 100);
return `<div class="redist-card" style="border-left:3px solid ${cfg.color}">
<div class="redist-card-header">
<div>
<div class="redist-card-nama">${d.ibadah.nama}</div>
<div class="redist-card-sub">${d.ibadah.kelurahan || ''} ${d.ibadah.kecamatan ? '· ' + d.ibadah.kecamatan : ''}</div>
</div>
<span class="fb-badge" style="background:${cfg.bg};color:${cfg.color}">${cfg.label}</span>
</div>
<div class="redist-progress-wrap">
<div class="redist-progress-bar">
<div class="redist-progress-fill" style="width:${pct}%;background:${cfg.color}"></div>
</div>
<span class="redist-progress-pct">${d.pct_beban}%</span>
</div>
<div class="redist-stat-row">
<div class="redist-stat-item">
<span class="redist-stat-val" style="color:#f87171">${d.sangat_miskin}</span>
<span class="redist-stat-key">Sangat Miskin</span>
</div>
<div class="redist-stat-item">
<span class="redist-stat-val" style="color:#6366f1">${d.miskin}</span>
<span class="redist-stat-key">Miskin</span>
</div>
<div class="redist-stat-item">
<span class="redist-stat-val" style="color:#fbbf24">${d.rentan_miskin}</span>
<span class="redist-stat-key">Rentan</span>
</div>
<div class="redist-stat-item">
<span class="redist-stat-val">${d.total_warga}/${d.kapasitas}</span>
<span class="redist-stat-key">KK/Kap.</span>
</div>
</div>
<div style="font-size:10px;color:var(--text-muted);margin-top:4px">
Rasio kemiskinan: <b>${d.rasio_miskin}%</b>
${d.surplus_kapasitas > 0 ? `· Surplus: <b style="color:#10b981">+${d.surplus_kapasitas} KK</b>` : ''}
${d.defisit_kapasitas > 0 ? `· Defisit: <b style="color:#ef4444">-${d.defisit_kapasitas} KK</b>` : ''}
</div>
</div>`;
}).join('');
container.innerHTML = cardsHtml;
}
function renderSearch() {
renderAnalisis(analisisData, null);
}
function renderSaran(saran) {
const container = document.getElementById('redistribusi-saran');
if (!container) return;
if (!saran.length) {
container.innerHTML = `<div class="fb-empty">${svgIcon(ICONS.check2,20,20)}<div>Distribusi sudah merata. Tidak ada saran redistribusi saat ini.</div></div>`;
return;
}
container.innerHTML = saran.map((s, i) => {
const priCfg = PRIORITAS_CONFIG[s.prioritas] || PRIORITAS_CONFIG.rendah;
return `<div class="saran-card">
<div class="saran-card-header">
<span class="fb-badge ${priCfg.cls}">${priCfg.label}</span>
<span style="font-size:10px;color:var(--text-muted)">${s.jarak_km} km</span>
</div>
<div class="saran-flow">
<div class="saran-ibadah surplus">${svgIcon(ICONS.swap,12,12)} ${s.pengirim_nama}</div>
<div class="saran-arrow">→ ${s.jumlah_kk} KK →</div>
<div class="saran-ibadah defisit">${s.penerima_nama} ${svgIcon(ICONS.alert,12,12)}</div>
</div>
<div class="saran-alasan">${s.alasan}</div>
<div style="font-size:10px;color:var(--text-muted);margin:4px 0">Est. dana: <b>${formatRp(s.estimasi_dana)}/bulan</b></div>
<div class="saran-actions">
<button class="btn btn-primary btn-xs" onclick="FiturBaru.Redistribusi.setujuiSaran(${i})">
${svgIcon(ICONS.check2,11,11)} Setujui & Simpan
</button>
<button class="btn btn-ghost btn-xs" onclick="FiturBaru.Redistribusi.tolakSaran(${i})">Tolak</button>
</div>
</div>`;
}).join('');
}
async function renderRiwayat() {
const container = document.getElementById('redistribusi-riwayat');
if (!container) return;
try {
const res = await fetch('api/redistribusi.php?action=riwayat&limit=10');
const json = await res.json();
if (!json.data.length) {
container.innerHTML = `<div class="fb-empty" style="padding:20px">Belum ada riwayat redistribusi</div>`;
return;
}
const STATUS_LABEL = { saran_sistem:'Saran Sistem', disetujui:'Disetujui', ditolak:'Ditolak', selesai:'Selesai' };
const STATUS_CLS = { saran_sistem:'badge-belum-terima', disetujui:'badge-sudah-terima', ditolak:'badge-sangat-miskin', selesai:'badge-bantuan-rutin' };
container.innerHTML = json.data.map(r => `
<div class="riwayat-item">
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
<div>
<div style="font-size:11px;font-weight:600;color:var(--text-primary)">${r.pengirim_nama}${r.penerima_nama}</div>
<div style="font-size:10px;color:var(--text-muted);margin-top:2px">${r.jumlah_kk} KK · ${formatRp(r.estimasi_dana)}/bln · ${r.periode_bulan || ''}</div>
</div>
<span class="badge ${STATUS_CLS[r.status] || ''}">${STATUS_LABEL[r.status] || r.status}</span>
</div>
${r.catatan_admin ? `<div style="font-size:10px;color:var(--text-muted);margin-top:4px;border-left:2px solid var(--border);padding-left:6px">${r.catatan_admin}</div>` : ''}
</div>`).join('');
} catch { container.innerHTML = `<div class="fb-error">Gagal memuat riwayat</div>`; }
}
async function setujuiSaran(idx) {
const s = saranData[idx];
if (!s) return;
document.getElementById('sr-penerima-nama').textContent = s.penerima_nama;
document.getElementById('sr-penerima-id').value = s.penerima_id;
document.getElementById('sr-catatan-sistem').value = s.alasan;
// Hitung rasio estimasi dana per KK dari saran (jika ada)
const danaPerKK = s.jumlah_kk > 0 ? (s.estimasi_dana / s.jumlah_kk) : 200000;
document.getElementById('sr-estimasi-dana-per-kk').value = danaPerKK;
// Populate select dengan rumah ibadah yang surplus (kosong/ringan)
const select = document.getElementById('sr-pengirim-id');
const surplusList = analisisData.filter(d => d.status_beban === 'kosong' || d.status_beban === 'ringan');
select.innerHTML = surplusList.map(d => {
const selected = d.ibadah.id == s.pengirim_id ? 'selected' : '';
return `<option value="${d.ibadah.id}" data-surplus="${d.surplus_kapasitas}" ${selected}>${d.ibadah.nama} (Surplus: ${d.surplus_kapasitas} KK)</option>`;
}).join('');
// Jika saran pengirim tidak ada di list surplus (mungkin list kosong), tambahkan manual
if (select.innerHTML === '') {
select.innerHTML = `<option value="${s.pengirim_id}" selected>${s.pengirim_nama}</option>`;
}
// Get eligible warga: dikelola_oleh_ibadah_id == penerima_id or (null and in radius)
const container = document.getElementById('sr-warga-list-container');
container.innerHTML = '<div style="text-align:center; padding:10px; color:#94a3b8">Memuat warga...</div>';
const ibadahData = window.App ? window.App.getIbadahData() : [];
const wargaData = window.App ? window.App.getWargaData() : [];
const haversine = window.App ? window.App.haversine : null;
const penerimaNode = ibadahData.find(i => parseInt(i.id) === parseInt(s.penerima_id));
const pLat = parseFloat(penerimaNode?.latitude || 0);
const pLng = parseFloat(penerimaNode?.longitude || 0);
const pRad = parseFloat(penerimaNode?.radius_primer || 500);
let eligibleWarga = wargaData.filter(w => {
if (!['sangat_miskin', 'miskin', 'rentan_miskin'].includes(w.kategori_kemiskinan) || w.status_kemandirian === 'mandiri') return false;
if (w.dikelola_oleh_ibadah_id && parseInt(w.dikelola_oleh_ibadah_id) === parseInt(s.penerima_id)) return true;
if (!w.dikelola_oleh_ibadah_id && typeof haversine === 'function') {
const dist = haversine(parseFloat(w.latitude), parseFloat(w.longitude), pLat, pLng);
if (dist <= pRad) return true;
}
return false;
});
if (eligibleWarga.length === 0) {
container.innerHTML = '<div style="text-align:center; padding:10px; color:#94a3b8">Tidak ada warga eligible yang dapat dipindahkan.</div>';
} else {
// Render checkboxes
let html = '';
eligibleWarga.forEach((w, idx) => {
const checked = idx < s.jumlah_kk ? 'checked' : '';
html += `
<div style="display:flex; align-items:center; gap:8px; padding:6px 0; border-bottom:1px solid #f1f5f9;">
<input type="checkbox" id="sr-warga-${w.id}" class="sr-warga-cb" value="${w.id}" ${checked} style="cursor:pointer">
<label for="sr-warga-${w.id}" style="cursor:pointer; flex:1; margin:0;">
<div style="font-weight:600; color:#334155">${w.nama_kepala_keluarga}</div>
<div style="color:#64748b; font-size:10px;">${w.alamat || 'Alamat tidak diketahui'}</div>
</label>
</div>
`;
});
container.innerHTML = html;
// Add event listeners to update count
const cbs = container.querySelectorAll('.sr-warga-cb');
cbs.forEach(cb => {
cb.addEventListener('change', () => {
const count = container.querySelectorAll('.sr-warga-cb:checked').length;
const inputJml = document.getElementById('sr-jumlah-kk');
inputJml.value = count;
const total = count * danaPerKK;
document.getElementById('sr-estimasi-dana').value = total;
document.getElementById('sr-estimasi-dana-display').value = formatRp(total);
});
});
}
const inputJml = document.getElementById('sr-jumlah-kk');
const initialChecked = container.querySelectorAll('.sr-warga-cb:checked').length;
inputJml.value = initialChecked;
const total = initialChecked * danaPerKK;
document.getElementById('sr-estimasi-dana').value = total;
document.getElementById('sr-estimasi-dana-display').value = formatRp(total);
document.getElementById('sr-catatan').value = '';
showModal('modal-setujui-redistribusi');
}
async function submitSetujui() {
const pengirimId = document.getElementById('sr-pengirim-id').value;
const penerimaId = document.getElementById('sr-penerima-id').value;
const jumlahKK = document.getElementById('sr-jumlah-kk').value;
const estimasi = document.getElementById('sr-estimasi-dana').value;
const catatan = document.getElementById('sr-catatan').value.trim();
const alasan = document.getElementById('sr-catatan-sistem').value;
if (!pengirimId || !jumlahKK || parseInt(jumlahKK) < 1) {
toast('Lengkapi data redistribusi', 'error');
return;
}
// Kumpulkan warga yang dipilih (sebelum disable button)
const cbWarga = document.querySelectorAll('.sr-warga-cb:checked');
const wargaIds = Array.from(cbWarga).map(cb => cb.value);
const btn = document.querySelector('#modal-setujui-redistribusi .btn-primary');
const oldHtml = btn.innerHTML;
btn.innerHTML = '<div class="spinner"></div> Menyimpan...';
btn.disabled = true;
try {
const res = await fetch('api/redistribusi.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pengirim_id: pengirimId,
penerima_id: penerimaId,
jumlah_kk: jumlahKK,
estimasi_dana: estimasi,
status: 'disetujui',
catatan_sistem: alasan,
catatan_admin: catatan,
periode_bulan: new Date().toLocaleDateString('en-CA').slice(0, 7),
warga_ids: wargaIds,
}),
});
let json;
try { json = await res.json(); } catch(e) {}
if (!res.ok || (json && !json.success)) {
throw new Error(json?.message || 'Gagal menyimpan redistribusi');
}
toast('Redistribusi disetujui dan disimpan', 'success');
closeModal('modal-setujui-redistribusi');
loadAnalisis();
if (window.App && window.App.loadWarga) window.App.loadWarga();
} catch (err) {
toast(err.message || 'Gagal menyimpan keputusan', 'error');
} finally {
btn.innerHTML = oldHtml;
btn.disabled = false;
}
}
async function tolakSaran(idx) {
const s = saranData[idx];
if (!s) return;
const catatan = prompt('Alasan penolakan:') ?? 'Ditolak admin';
try {
await fetch('api/redistribusi.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
pengirim_id: s.pengirim_id,
penerima_id: s.penerima_id,
jumlah_kk: s.jumlah_kk,
estimasi_dana: s.estimasi_dana,
status: 'ditolak',
catatan_sistem:s.alasan,
catatan_admin: catatan,
periode_bulan: new Date().toLocaleDateString('en-CA').slice(0, 7),
}),
});
toast('Saran ditolak dan dicatat', 'info');
loadAnalisis();
} catch { toast('Gagal menyimpan', 'error'); }
}
return { load: loadAnalisis, renderSearch, setujuiSaran, submitSetujui, tolakSaran, open: () => { showModal('modal-premium-redistribusi'); loadAnalisis(); } };
})();
/* ══════════════════════════════════════════════════
░░ FITUR 2: KETERBARUAN DATA ░░
══════════════════════════════════════════════════ */
const Verifikasi = (() => {
let konfig = {};
const STATUS_CFG = {
terverifikasi: { label: 'Terverifikasi', color: '#10b981', bg: 'rgba(16,185,129,0.12)' },
perlu_update: { label: 'Perlu Update', color: '#f59e0b', bg: 'rgba(245,158,11,0.12)' },
kadaluarsa: { label: 'Kadaluarsa', color: '#ef4444', bg: 'rgba(239,68,68,0.12)' },
};
function skorBar(skor) {
const color = skor >= 80 ? '#10b981' : skor >= 40 ? '#f59e0b' : '#ef4444';
return `<div class="skor-bar-wrap">
<div class="skor-bar"><div class="skor-bar-fill" style="width:${skor}%;background:${color}"></div></div>
<span class="skor-pct" style="color:${color}">${skor}%</span>
</div>`;
}
let allData = [];
async function load(statusFilter = '') {
const container = document.getElementById('verifikasi-list');
if (!container) return;
container.innerHTML = `<div class="fb-loading">${svgIcon(ICONS.clock,18,18)}<span>Memuat data verifikasi...</span></div>`;
// Clear search input on load/change
const searchBox = document.getElementById('verifikasi-search');
if (searchBox) searchBox.value = '';
const url = `api/verifikasi.php?action=list${statusFilter ? '&status=' + statusFilter : ''}`;
try {
const res = await fetch(url);
const json = await res.json();
if (!json.success) throw new Error(json.message);
// Update summary count
const s = json.summary || {};
document.getElementById('verif-count-ok')?.setAttribute('data-val', s.terverifikasi || 0);
document.getElementById('verif-count-warn')?.setAttribute('data-val', s.perlu_update || 0);
document.getElementById('verif-count-err')?.setAttribute('data-val', s.kadaluarsa || 0);
['ok','warn','err'].forEach(k => {
const el = document.getElementById(`verif-count-${k}`);
if (el) el.textContent = el.getAttribute('data-val');
});
allData = json.data || [];
renderList();
} catch (e) {
container.innerHTML = `<div class="fb-error">${svgIcon(ICONS.alert,16,16)} ${e.message}</div>`;
}
}
function renderList() {
const container = document.getElementById('verifikasi-list');
if (!container) return;
const searchVal = document.getElementById('verifikasi-search')?.value.toLowerCase() || '';
const filtered = allData.filter(w => {
return !searchVal ||
w.nama_kepala_keluarga.toLowerCase().includes(searchVal) ||
w.no_kk.includes(searchVal) ||
(w.kelurahan || '').toLowerCase().includes(searchVal);
});
if (!filtered.length) {
container.innerHTML = `<div class="fb-empty">${svgIcon(ICONS.check2,20,20)}<div>Tidak ada data warga ditemukan</div></div>`;
return;
}
container.innerHTML = filtered.map(w => {
const stsCfg = STATUS_CFG[w.status_verifikasi] || STATUS_CFG.terverifikasi;
const det = w.detail_skor || {};
return `<div class="verif-item">
<div class="verif-item-header">
<div>
<div class="verif-item-nama">${w.nama_kepala_keluarga}</div>
<div class="verif-item-sub">${w.kelurahan || ''} · No.KK: ${w.no_kk}</div>
</div>
<span class="fb-badge" style="background:${stsCfg.bg};color:${stsCfg.color}">${stsCfg.label}</span>
</div>
${skorBar(w.skor_kepercayaan)}
<div class="verif-meta">
<span>${svgIcon(ICONS.clock,10,10)} Verif: ${w.tanggal_verifikasi ? new Date(w.tanggal_verifikasi).toLocaleDateString('id-ID') : 'Belum pernah'}</span>
${det.hari_sejak_verifikasi != null ? `<span>${det.hari_sejak_verifikasi} hari lalu</span>` : ''}
${det.tanggal_kadaluarsa ? `<span>Exp: ${new Date(det.tanggal_kadaluarsa).toLocaleDateString('id-ID')}</span>` : ''}
</div>
<button class="btn btn-primary btn-xs" style="margin-top:6px;width:100%"
onclick="App.editWarga(${w.id})">
${svgIcon(ICONS.check2,11,11)} Catat Verifikasi &amp; Edit
</button>
</div>`;
}).join('');
}
function openVerifModal(wargaId, namWarga) {
document.getElementById('verif-modal-nama').textContent = namWarga;
document.getElementById('verif-modal-id').value = wargaId;
document.getElementById('verif-modal-tanggal').value = new Date().toLocaleDateString('en-CA');
document.getElementById('verif-modal-petugas').value = '';
document.getElementById('verif-modal-catatan').value = '';
showModal('modal-verifikasi');
}
async function simpanVerifikasi() {
const wargaId = document.getElementById('verif-modal-id').value;
const petugas = document.getElementById('verif-modal-petugas').value.trim();
const tanggal = document.getElementById('verif-modal-tanggal').value;
const catatan = document.getElementById('verif-modal-catatan').value;
try {
const res = await fetch('api/verifikasi.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ warga_id: parseInt(wargaId), petugas, tanggal, catatan }),
});
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
closeModal('modal-verifikasi');
load(document.getElementById('filter-verif-status')?.value || '');
} else toast(json.message, 'error');
} catch { toast('Gagal menyimpan verifikasi', 'error'); }
}
async function loadKonfig() {
try {
const res = await fetch('api/verifikasi.php?action=config');
const json = await res.json();
if (!json.success) return;
const container = document.getElementById('konfig-list');
if (!container) return;
konfig = {};
json.data.forEach(k => { konfig[k.kunci] = k; });
container.innerHTML = json.data.map(k => `
<div class="konfig-item">
<label class="konfig-label">${k.label || k.kunci}</label>
${k.deskripsi ? `<div class="konfig-desc">${k.deskripsi}</div>` : ''}
<input type="${k.tipe === 'integer' || k.tipe === 'decimal' ? 'number' : 'text'}"
class="form-control konfig-input"
id="konfig-${k.kunci}"
value="${k.nilai}"
step="${k.tipe === 'decimal' ? '100000' : '1'}"
data-kunci="${k.kunci}">
</div>`).join('');
} catch { /* silent */ }
}
async function simpanKonfig() {
const inputs = document.querySelectorAll('.konfig-input');
const payload = {};
inputs.forEach(el => { payload[el.dataset.kunci] = el.value; });
try {
const res = await fetch('api/verifikasi.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const json = await res.json();
toast(json.message, json.success ? 'success' : 'error');
if (json.success) load();
} catch { toast('Gagal menyimpan konfigurasi', 'error'); }
}
return { load, renderSearch: renderList, openVerifModal, simpanVerifikasi, loadKonfig, simpanKonfig, open: () => { showModal('modal-premium-verifikasi'); load(); loadKonfig(); } };
})();
/* ══════════════════════════════════════════════════
░░ FITUR 3: POTENSI KEMANDIRIAN ░░
══════════════════════════════════════════════════ */
const Kemandirian = (() => {
const STATUS_CFG = {
sangat_bergantung: { label: 'Sangat Bergantung', color: '#ef4444', bg: 'rgba(239,68,68,0.12)', icon: '🔴' },
bergantung: { label: 'Bergantung', color: '#f87171', bg: 'rgba(248,113,113,0.12)', icon: '🟠' },
berkembang: { label: 'Berkembang', color: '#f59e0b', bg: 'rgba(245,158,11,0.12)', icon: '🟡' },
berpotensi_mandiri: { label: 'Berpotensi Mandiri', color: '#6366f1', bg: 'rgba(99,102,241,0.12)', icon: '🔵' },
mandiri: { label: 'Mandiri / Siap Exit',color: '#10b981', bg: 'rgba(16,185,129,0.12)', icon: '🟢' },
};
function skorGauge(skor, label) {
const deg = Math.round(skor / 100 * 180);
const color = skor >= 80 ? '#10b981' : skor >= 61 ? '#6366f1' : skor >= 41 ? '#f59e0b' : skor >= 21 ? '#f87171' : '#ef4444';
return `<div class="kemandirian-gauge" title="${label}: ${skor}/100">
<svg viewBox="0 0 80 44" width="80" height="44">
<path d="M8,40 A32,32 0 0,1 72,40" fill="none" stroke="var(--border)" stroke-width="7" stroke-linecap="round"/>
<path d="M8,40 A32,32 0 0,1 72,40" fill="none" stroke="${color}" stroke-width="7" stroke-linecap="round"
stroke-dasharray="${deg * 0.558} 200" />
</svg>
<div class="gauge-val" style="color:${color}">${skor}</div>
<div class="gauge-label">${label}</div>
</div>`;
}
let allData = [];
async function load(statusFilter = '') {
const container = document.getElementById('kemandirian-list');
if (!container) return;
container.innerHTML = `<div class="fb-loading">${svgIcon(ICONS.trend,18,18)}<span>Menghitung skor kemandirian...</span></div>`;
// Clear search input on load/change
const searchBox = document.getElementById('kemandirian-search');
if (searchBox) searchBox.value = '';
const url = `api/kemandirian.php?action=list${statusFilter ? '&status=' + statusFilter : ''}`;
try {
const res = await fetch(url);
const json = await res.json();
if (!json.success) throw new Error(json.message);
// Update stat cards
const summaryMap = {};
json.summary.forEach(s => { summaryMap[s.status] = s; });
document.getElementById('kemandirian-siap-exit').textContent = json.siap_exit || 0;
allData = json.data || [];
renderList();
return;
container.innerHTML = json.data.map(w => {
const stsCfg = STATUS_CFG[w.status_kemandirian] || STATUS_CFG.bergantung;
const det = w.detail_skor || {};
const estBulan = det.estimasi_bulan_mandiri;
return `<div class="kemandirian-item" style="border-left:3px solid ${stsCfg.color}">
<div class="kemandirian-item-header">
<div>
<div class="kemandirian-nama">${w.nama_kepala_keluarga}</div>
<div class="kemandirian-sub">${formatRp(w.penghasilan_bulanan)}/bln · ${w.jumlah_tanggungan} tanggungan</div>
</div>
<span class="fb-badge" style="background:${stsCfg.bg};color:${stsCfg.color}">${stsCfg.icon} ${stsCfg.label}</span>
</div>
<div class="kemandirian-gauges">
${skorGauge(w.skor_kemandirian, 'Total')}
${det.skor_penghasilan != null ? skorGauge(det.skor_penghasilan, 'Penghasilan') : ''}
${det.skor_status_kerja != null ? skorGauge(det.skor_status_kerja, 'Kerja') : ''}
${det.skor_tanggungan != null ? skorGauge(det.skor_tanggungan, 'Tanggungan') : ''}
</div>
${estBulan > 0 ? `<div class="kemandirian-est">
${svgIcon(ICONS.clock,11,11)} Est. mandiri: <b>~${estBulan} bulan</b>
</div>` : w.status_kemandirian === 'mandiri' ? `<div class="kemandirian-est" style="color:var(--success)">
${svgIcon(ICONS.check2,11,11)} Siap keluar dari daftar bantuan
</div>` : ''}
${det.rekomendasi?.length ? `<div class="kemandirian-rek">
${det.rekomendasi.map(r => `<div class="kemandirian-rek-item">${svgIcon(ICONS.trend,10,10)} ${r}</div>`).join('')}
</div>` : ''}
${w.tanggal_exit_plan ? `<div style="font-size:10px;margin-top:4px;color:var(--text-muted)">
${svgIcon(ICONS.exit,10,10)} Target exit: <b>${new Date(w.tanggal_exit_plan).toLocaleDateString('id-ID')}</b>
</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:6px;width:100%"
onclick="FiturBaru.Kemandirian.openCatatanModal(${w.id}, '${w.nama_kepala_keluarga.replace(/'/g, "\\'")}', '${w.tanggal_exit_plan || ''}', '${w.catatan_kemandirian?.replace(/'/g, "\\'") || ''}')">
${svgIcon(ICONS.gear,11,11)} Catat Perkembangan
</button>
</div>`;
}).join('');
} catch (e) {
container.innerHTML = `<div class="fb-error">${svgIcon(ICONS.alert,16,16)} ${e.message}</div>`;
}
}
function renderList() {
const container = document.getElementById('kemandirian-list');
if (!container) return;
const searchVal = document.getElementById('kemandirian-search')?.value.toLowerCase() || '';
const filtered = allData.filter(w => {
return !searchVal ||
w.nama_kepala_keluarga.toLowerCase().includes(searchVal) ||
(w.pekerjaan || '').toLowerCase().includes(searchVal);
});
if (!filtered.length) {
container.innerHTML = `<div class="fb-empty">${svgIcon(ICONS.chart,20,20)}<div>Tidak ada data warga ditemukan</div></div>`;
return;
}
container.innerHTML = filtered.map(w => {
const stsCfg = STATUS_CFG[w.status_kemandirian] || STATUS_CFG.bergantung;
const det = w.detail_skor || {};
const estBulan = det.estimasi_bulan_mandiri;
return `<div class="kemandirian-item" style="border-left:3px solid ${stsCfg.color}">
<div class="kemandirian-item-header">
<div>
<div class="kemandirian-nama">${w.nama_kepala_keluarga}</div>
<div class="kemandirian-sub">${formatRp(w.penghasilan_bulanan)}/bln · ${w.jumlah_tanggungan} tanggungan</div>
</div>
<span class="fb-badge" style="background:${stsCfg.bg};color:${stsCfg.color}">${stsCfg.icon} ${stsCfg.label}</span>
</div>
<div class="kemandirian-gauges">
${skorGauge(w.skor_kemandirian, 'Total')}
${det.skor_penghasilan != null ? skorGauge(det.skor_penghasilan, 'Penghasilan') : ''}
${det.skor_status_kerja != null ? skorGauge(det.skor_status_kerja, 'Kerja') : ''}
${det.skor_tanggungan != null ? skorGauge(det.skor_tanggungan, 'Tanggungan') : ''}
</div>
${estBulan > 0 ? `<div class="kemandirian-est">
${svgIcon(ICONS.clock,11,11)} Est. mandiri: <b>~${estBulan} bulan</b>
</div>` : w.status_kemandirian === 'mandiri' ? `<div class="kemandirian-est" style="color:var(--success)">
${svgIcon(ICONS.check2,11,11)} Siap keluar dari daftar bantuan
</div>` : ''}
${det.rekomendasi?.length ? `<div class="kemandirian-rek">
${det.rekomendasi.map(r => `<div class="kemandirian-rek-item">${svgIcon(ICONS.trend,10,10)} ${r}</div>`).join('')}
</div>` : ''}
${w.tanggal_exit_plan ? `<div style="font-size:10px;margin-top:4px;color:var(--text-muted)">
${svgIcon(ICONS.exit,10,10)} Target exit: <b>${new Date(w.tanggal_exit_plan).toLocaleDateString('id-ID')}</b>
</div>` : ''}
<button class="btn btn-ghost btn-xs" style="margin-top:6px;width:100%"
onclick="FiturBaru.Kemandirian.openCatatanModal(${w.id}, '${w.nama_kepala_keluarga.replace(/'/g, "\\'")}', '${w.tanggal_exit_plan || ''}', '${w.catatan_kemandirian?.replace(/'/g, "\\'") || ''}')">
${svgIcon(ICONS.gear,11,11)} Catat Perkembangan
</button>
</div>`;
}).join('');
}
function openCatatanModal(wargaId, nama, exitPlan, catatan) {
document.getElementById('kemandirian-modal-nama').textContent = nama;
document.getElementById('kemandirian-modal-id').value = wargaId;
document.getElementById('kemandirian-modal-exit').value = exitPlan;
document.getElementById('kemandirian-modal-catatan').value = catatan;
showModal('modal-kemandirian');
}
async function simpanCatatan() {
const wargaId = document.getElementById('kemandirian-modal-id').value;
const catatan = document.getElementById('kemandirian-modal-catatan').value;
const exitPlan = document.getElementById('kemandirian-modal-exit').value;
try {
const res = await fetch('api/kemandirian.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
warga_id: parseInt(wargaId),
catatan,
tanggal_exit_plan: exitPlan || null,
}),
});
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
closeModal('modal-kemandirian');
load(document.getElementById('filter-kemandirian-status')?.value || '');
} else toast(json.message, 'error');
} catch { toast('Gagal menyimpan', 'error'); }
}
return { load, renderSearch: renderList, openCatatanModal, simpanCatatan, open: () => { showModal('modal-premium-kemandirian'); load(); } };
})();
/* ══════════════════════════════════════════════════
░░ FITUR 4: KEBUTUHAN & KONTRIBUSI ░░
══════════════════════════════════════════════════ */
const Kebutuhan = (() => {
let kebutuhanData = [];
let activeSubTab = 'daftar'; // 'daftar', 'riwayat', or 'konfirmasi'
async function load() {
const container = document.getElementById('kebutuhan-panel-content');
if (!container) return;
const user = App.getCurrentUser();
const role = user ? user.role : 'guest';
// Enforce default tab based on role transitions
if (activeSubTab === 'riwayat' && role !== 'kontributor') {
activeSubTab = 'daftar';
}
if (activeSubTab === 'konfirmasi' && !['admin', 'koordinator'].includes(role)) {
activeSubTab = 'daftar';
}
if (role === 'guest') {
activeSubTab = 'daftar';
}
let subTabsHtml = '';
if (role === 'kontributor') {
subTabsHtml = `
<div class="fb-sub-tabs" style="margin-bottom: 12px;">
<button class="fb-sub-tab ${activeSubTab === 'daftar' ? 'active' : ''}" onclick="FiturBaru.Kebutuhan.switchTab('daftar')">Kebutuhan Sosial</button>
<button class="fb-sub-tab ${activeSubTab === 'riwayat' ? 'active' : ''}" onclick="FiturBaru.Kebutuhan.switchTab('riwayat')">Riwayat Donasi Saya</button>
</div>
`;
} else if (role === 'koordinator' || role === 'admin') {
subTabsHtml = `
<div class="fb-sub-tabs" style="margin-bottom: 12px;">
<button class="fb-sub-tab ${activeSubTab === 'daftar' ? 'active' : ''}" onclick="FiturBaru.Kebutuhan.switchTab('daftar')">Kelola Kebutuhan</button>
<button class="fb-sub-tab ${activeSubTab === 'konfirmasi' ? 'active' : ''}" onclick="FiturBaru.Kebutuhan.switchTab('konfirmasi')">Verifikasi Donasi</button>
</div>
`;
} else if (role === 'guest') {
subTabsHtml = '';
}
container.innerHTML = `
${subTabsHtml}
<div id="kebutuhan-sub-panel-content">
<div class="fb-loading"><span>Memuat data...</span></div>
</div>
`;
if (activeSubTab === 'daftar') {
await loadKebutuhanList();
} else if (activeSubTab === 'riwayat') {
await loadRiwayatDonasi();
} else if (activeSubTab === 'konfirmasi') {
await loadVerifikasiDonasi();
}
}
// Force refresh Kebutuhan tab if it's currently active
function refreshIfActive() {
const kebutuhanTab = document.getElementById('tab-btn-kebutuhan');
if (kebutuhanTab && kebutuhanTab.classList.contains('active')) {
load();
}
}
async function loadKebutuhanList() {
const subContainer = document.getElementById('kebutuhan-sub-panel-content');
if (!subContainer) return;
try {
const res = await fetch('api/kebutuhan.php');
const json = await res.json();
if (!json.success) throw new Error(json.message);
kebutuhanData = json.data || [];
const user = App.getCurrentUser();
const role = user ? user.role : 'guest';
let addBtnHtml = '';
if (role === 'admin' || role === 'koordinator') {
addBtnHtml = `
<button class="btn btn-primary btn-sm btn-block" style="margin-bottom: 12px; display:flex; align-items:center; justify-content:center; gap: 4px;" onclick="FiturBaru.Kebutuhan.openFormModal()">
${svgIcon(ICONS.refresh, 11, 11)} Tambah Kebutuhan Baru
</button>
`;
}
if (kebutuhanData.length === 0) {
subContainer.innerHTML = `
${addBtnHtml}
<div class="fb-empty">Belum ada kebutuhan bantuan terdaftar.</div>
`;
return;
}
const cardsHtml = kebutuhanData.map(k => {
const gathered = parseFloat(k.jumlah_terkumpul || 0);
const required = parseFloat(k.jumlah_dibutuhkan || 0);
const progress = Math.min(100, Math.round((gathered / required) * 100)) || 0;
const sisa = Math.max(0, required - gathered);
const ibadahNama = k.nama_rumah_ibadah || k.nama_ibadah || 'Rumah Ibadah';
let actionButtons = '';
if (role === 'admin' || (role === 'koordinator' && user.rumah_ibadah_id == k.rumah_ibadah_id)) {
actionButtons = `
<div style="display:flex; gap:6px;">
<button class="btn btn-primary btn-xs" style="padding: 3px 8px" onclick="FiturBaru.Kebutuhan.openFormModal(${k.id})">Edit</button>
<button class="btn btn-danger btn-xs" style="padding: 3px 8px" onclick="FiturBaru.Kebutuhan.deleteKebutuhan(${k.id})">Hapus</button>
</div>
`;
} else {
if (sisa > 0) {
actionButtons = `
<button class="btn btn-success btn-xs" style="padding: 4px 10px; font-weight:700" onclick="FiturBaru.Kebutuhan.triggerDonation(${k.id}, ${sisa}, '${k.jenis_bantuan.replace(/'/g, "\\'")}', '${ibadahNama.replace(/'/g, "\\'")}')">
Beri Donasi
</button>
`;
} else {
actionButtons = `<span class="fb-badge badge-sudah-terima">${svgIcon(ICONS.check2, 10, 10)} Terpenuhi</span>`;
}
}
const categoryIcons = {
barang: '📦',
uang: '💰',
jasa: '🤝'
};
const catIcon = categoryIcons[k.kategori] || '📦';
return `
<div class="kebutuhan-card">
<div class="kebutuhan-header">
<div>
<div class="kebutuhan-ibadah-nama">${ibadahNama}</div>
<div class="kebutuhan-bantuan-jenis">${catIcon} ${k.jenis_bantuan} (${k.kategori})</div>
</div>
</div>
<div class="kebutuhan-progress-section">
<div class="kebutuhan-progress-text">
<span>Terkumpul: ${gathered} / ${required} ${k.satuan ? (k.satuan === 'nominal' ? '(Rp)' : k.satuan) : ''}</span>
<span>${progress}%</span>
</div>
<div class="kebutuhan-progress-bar">
<div class="kebutuhan-progress-fill" style="width: ${progress}%"></div>
</div>
</div>
<div class="kebutuhan-footer">
<div class="kebutuhan-sisa ${sisa === 0 ? 'kebutuhan-penuh' : ''}">
${sisa > 0 ? `Sisa Kebutuhan: <b>${sisa}</b>` : 'Kebutuhan Terpenuhi'}
</div>
${actionButtons}
</div>
</div>
`;
}).join('');
subContainer.innerHTML = addBtnHtml + cardsHtml;
} catch (e) {
subContainer.innerHTML = `<div class="fb-error">${e.message}</div>`;
}
}
async function loadRiwayatDonasi() {
const subContainer = document.getElementById('kebutuhan-sub-panel-content');
if (!subContainer) return;
try {
const res = await fetch('api/kontribusi.php');
const json = await res.json();
if (!json.success) throw new Error(json.message);
const list = json.data || [];
if (list.length === 0) {
subContainer.innerHTML = `<div class="fb-empty">Anda belum pernah mengirimkan donasi.</div>`;
return;
}
subContainer.innerHTML = list.map(c => {
const statusCls = c.status === 'confirmed' ? 'badge-sudah-terima' : c.status === 'rejected' ? 'badge-sangat-miskin' : 'badge-belum-terima';
const statusLabel = c.status === 'confirmed' ? 'Disetujui' : c.status === 'rejected' ? 'Ditolak' : 'Menunggu Konfirmasi';
return `
<div class="kontribusi-item">
<div class="kontribusi-header">
<span style="font-size: 11px; font-weight: 700; color: var(--text-primary);">${c.nama_ibadah}</span>
<span class="fb-badge ${statusCls}">${statusLabel}</span>
</div>
<div style="font-size: 10px; color: var(--text-muted); margin-bottom: 4px;">
Bantuan: <b>${c.jenis_bantuan}</b>
</div>
<div style="font-size: 11px; color: var(--text-secondary); display: flex; justify-content: space-between;">
<span>Jumlah: <b>${c.jumlah}</b> (${c.tipe_kontribusi})</span>
<span>${new Date(c.created_at).toLocaleDateString('id-ID')}</span>
</div>
${c.catatan ? `<div style="font-size: 9.5px; color: var(--text-muted); margin-top: 4px; border-left: 2px solid var(--border); padding-left: 6px;">Catatan: ${c.catatan}</div>` : ''}
</div>
`;
}).join('');
} catch (e) {
subContainer.innerHTML = `<div class="fb-error">${e.message}</div>`;
}
}
async function loadVerifikasiDonasi() {
const subContainer = document.getElementById('kebutuhan-sub-panel-content');
if (!subContainer) return;
try {
const res = await fetch('api/kontribusi.php');
const json = await res.json();
if (!json.success) throw new Error(json.message);
const list = json.data || [];
if (list.length === 0) {
subContainer.innerHTML = `<div class="fb-empty">Tidak ada donasi masuk untuk rumah ibadah Anda.</div>`;
return;
}
subContainer.innerHTML = list.map(c => {
const isPending = c.status === 'pending';
const statusCls = c.status === 'confirmed' ? 'badge-sudah-terima' : c.status === 'rejected' ? 'badge-sangat-miskin' : 'badge-belum-terima';
const statusLabel = c.status === 'confirmed' ? 'Disetujui' : c.status === 'rejected' ? 'Ditolak' : 'Menunggu Konfirmasi';
return `
<div class="kontribusi-item">
<div class="kontribusi-header">
<span style="font-size: 11px; font-weight: 700; color: var(--text-primary);">${c.nama_kontributor}</span>
<span class="fb-badge ${statusCls}">${statusLabel}</span>
</div>
<div style="font-size: 10px; color: var(--text-muted); margin-bottom: 4px;">
Ibadah: <b>${c.nama_ibadah}</b> · Bantuan: <b>${c.jenis_bantuan}</b>
</div>
<div style="font-size: 11px; color: var(--text-secondary); display: flex; justify-content: space-between; align-items: center;">
<span>Jumlah: <b>${c.jumlah}</b> (${c.tipe_kontribusi})</span>
<span>Contact: ${c.kontak_kontributor || ''}</span>
</div>
${c.catatan ? `<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px; font-style: italic;">"${c.catatan}"</div>` : ''}
${isPending ? `
<div style="margin-top: 8px; display: flex; gap: 6px;">
<button class="btn btn-success btn-xs" style="padding: 4px 10px" onclick="FiturBaru.Kebutuhan.verifyDonasi(${c.id}, 'confirmed')">Setujui</button>
<button class="btn btn-danger btn-xs" style="padding: 4px 10px" onclick="FiturBaru.Kebutuhan.verifyDonasi(${c.id}, 'rejected')">Tolak</button>
</div>
` : ''}
</div>
`;
}).join('');
} catch (e) {
subContainer.innerHTML = `<div class="fb-error">${e.message}</div>`;
}
}
function switchTab(tab) {
activeSubTab = tab;
load();
}
function openFormModal(id = null) {
const modal = document.getElementById('modal-kebutuhan-form');
if (!modal) return;
// Prevent non-authorized users from opening the form (defense-in-depth)
const currentUser = App.getCurrentUser();
const role = currentUser ? currentUser.role : 'guest';
if (!['admin', 'koordinator'].includes(role)) {
toast('Hanya Admin atau Koordinator yang dapat menambah / mengedit kebutuhan.', 'error');
return;
}
const titleEl = document.getElementById('modal-kebutuhan-title');
const idInput = document.getElementById('kebutuhan-id');
const ibadahSelect = document.getElementById('kebutuhan-ibadah-id');
const katSelect = document.getElementById('kebutuhan-kategori');
const jenisInput = document.getElementById('kebutuhan-jenis-bantuan');
const jumlahInput = document.getElementById('kebutuhan-jumlah-dibutuhkan');
const ibadahContainer = document.getElementById('kebutuhan-ibadah-select-container');
if (ibadahSelect) {
const list = App.getIbadahData() || [];
ibadahSelect.innerHTML = list.map(i => `<option value="${i.id}">${i.nama}</option>`).join('');
}
const user = App.getCurrentUser();
if (user && user.role === 'koordinator') {
if (ibadahContainer) ibadahContainer.style.display = 'none';
if (ibadahSelect) ibadahSelect.value = user.rumah_ibadah_id;
} else {
if (ibadahContainer) ibadahContainer.style.display = 'block';
}
if (id) {
titleEl.textContent = 'Edit Kebutuhan Bantuan';
const d = kebutuhanData.find(x => x.id == id);
if (d) {
idInput.value = d.id;
if (ibadahSelect) ibadahSelect.value = d.rumah_ibadah_id;
katSelect.value = d.kategori;
jenisInput.value = d.jenis_bantuan;
jumlahInput.value = d.jumlah_dibutuhkan;
const satuanSelect = document.getElementById('kebutuhan-satuan');
if (satuanSelect) satuanSelect.value = d.satuan || (d.kategori === 'uang' ? 'nominal' : (d.kategori === 'jasa' ? 'orang' : 'unit'));
}
} else {
titleEl.textContent = 'Tambah Kebutuhan Bantuan';
idInput.value = '';
jenisInput.value = '';
jumlahInput.value = '';
const satuanSelect = document.getElementById('kebutuhan-satuan');
if (satuanSelect) {
// default based on kategori
const defKat = katSelect ? katSelect.value : 'barang';
satuanSelect.value = defKat === 'uang' ? 'nominal' : (defKat === 'jasa' ? 'orang' : 'unit');
}
}
// Ensure kategori change updates jumlah label/hint
const katSelectEl = document.getElementById('kebutuhan-kategori');
const jumlahGroup = document.getElementById('kebutuhan-jumlah-group');
const jumlahLabel = document.getElementById('kebutuhan-jumlah-label');
const jumlahHint = document.getElementById('kebutuhan-jumlah-hint');
const satuanSelectEl = document.getElementById('kebutuhan-satuan');
function updateJumlahVisibility() {
const val = katSelectEl ? katSelectEl.value : 'barang';
if (val === 'uang') {
// For monetary needs, show amount and hint
if (jumlahGroup) jumlahGroup.style.display = '';
if (jumlahLabel) jumlahLabel.textContent = 'Jumlah Dibutuhkan (nominal dalam satuan lokal) *';
if (jumlahHint) jumlahHint.textContent = 'Masukkan jumlah dana yang dibutuhkan (angka, tanpa pemisah).';
if (satuanSelectEl) satuanSelectEl.value = 'nominal';
} else if (val === 'jasa') {
// For jasa, show count of tenaga/relawan needed
if (jumlahGroup) jumlahGroup.style.display = '';
if (jumlahLabel) jumlahLabel.textContent = 'Jumlah Tenaga / Relawan Dibutuhkan *';
if (jumlahHint) jumlahHint.textContent = 'Masukkan jumlah tenaga/relawan yang diperlukan.';
if (satuanSelectEl) satuanSelectEl.value = 'orang';
} else {
// barang
if (jumlahGroup) jumlahGroup.style.display = '';
if (jumlahLabel) jumlahLabel.textContent = 'Jumlah Dibutuhkan *';
if (jumlahHint) jumlahHint.textContent = 'Untuk barang: jumlah unit yang dibutuhkan.';
if (satuanSelectEl) satuanSelectEl.value = 'unit';
}
}
if (katSelectEl) {
katSelectEl.removeEventListener('change', updateJumlahVisibility);
katSelectEl.addEventListener('change', updateJumlahVisibility);
}
updateJumlahVisibility();
modal.classList.add('show');
}
async function saveKebutuhan() {
const id = document.getElementById('kebutuhan-id').value;
const ibadahId = document.getElementById('kebutuhan-ibadah-id').value;
const kategori = document.getElementById('kebutuhan-kategori').value;
const jenis = document.getElementById('kebutuhan-jenis-bantuan').value.trim();
const jumlah = document.getElementById('kebutuhan-jumlah-dibutuhkan').value;
const satuan = document.getElementById('kebutuhan-satuan') ? document.getElementById('kebutuhan-satuan').value : 'unit';
if (!jenis || jumlah === '' || jumlah === null) {
toast('Semua input wajib diisi!', 'error');
return;
}
// Normalize kategori to allowed DB values
const allowed = ['barang', 'uang', 'jasa'];
const kategoriNorm = allowed.includes(kategori) ? kategori : 'barang';
const method = id ? 'PUT' : 'POST';
const body = { rumah_ibadah_id: parseInt(ibadahId), kategori: kategoriNorm, jenis_bantuan: jenis, jumlah_dibutuhkan: parseFloat(jumlah), satuan };
if (id) body.id = parseInt(id);
try {
const res = await fetch('api/kebutuhan.php', {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
closeModal('modal-kebutuhan-form');
load();
} else {
toast(json.message, 'error');
}
} catch (e) {
toast('Gagal menyimpan kebutuhan', 'error');
}
}
async function deleteKebutuhan(id) {
if (!confirm('Apakah Anda yakin ingin menghapus kebutuhan ini?')) return;
try {
const res = await fetch(`api/kebutuhan.php?id=${id}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
load();
} else {
toast(json.message, 'error');
}
} catch (e) {
toast('Gagal menghapus kebutuhan', 'error');
}
}
function triggerDonation(kebutuhanId, sisa, jenis, ibadahNama) {
App.checkAuth({ type: 'donate', kebutuhan_id: kebutuhanId, sisa, jenis, ibadah_nama: ibadahNama }, () => {
openDonationModal(kebutuhanId, sisa, jenis, ibadahNama);
});
}
function openDonationModal(kebutuhanId, sisa, jenis, ibadahNama) {
const modal = document.getElementById('modal-kontribusi-form');
if (!modal) return;
document.getElementById('kontribusi-kebutuhan-id').value = kebutuhanId;
document.getElementById('kontribusi-target-ibadah').textContent = ibadahNama;
document.getElementById('kontribusi-target-detail').textContent = `Bantuan: ${jenis} (Sisa: ${sisa})`;
const countInput = document.getElementById('kontribusi-jumlah');
countInput.value = '';
countInput.max = sisa;
const maxHint = document.getElementById('kontribusi-max-hint');
if (maxHint) maxHint.textContent = `Batas Maksimal: ${sisa}`;
const nameInput = document.getElementById('kontribusi-nama');
const contactInput = document.getElementById('kontribusi-kontak');
const typeSelect = document.getElementById('kontribusi-tipe');
const user = App.getCurrentUser();
if (user) {
nameInput.value = user.username || '';
nameInput.readOnly = true;
contactInput.value = user.no_telp || '';
contactInput.readOnly = true;
} else {
nameInput.value = '';
nameInput.readOnly = false;
contactInput.value = '';
contactInput.readOnly = false;
}
// Auto-map contribution type based on need category
const d = kebutuhanData.find(x => x.id == kebutuhanId);
let mappedType = 'barang'; // default
if (d) {
if (d.kategori === 'uang') {
mappedType = 'uang';
} else if (['jasa', 'tenaga', 'relawan'].includes(d.kategori) || d.jenis_bantuan.toLowerCase().includes('relawan') || d.jenis_bantuan.toLowerCase().includes('tenaga') || d.jenis_bantuan.toLowerCase().includes('jasa')) {
mappedType = 'tenaga';
}
}
if (typeSelect) {
typeSelect.value = mappedType;
typeSelect.disabled = true; // Lock it to prevent manual modification
}
document.getElementById('kontribusi-catatan').value = '';
modal.classList.add('show');
}
async function saveDonation() {
const kebutuhanId = document.getElementById('kontribusi-kebutuhan-id').value;
const nama = document.getElementById('kontribusi-nama').value.trim();
const kontak = document.getElementById('kontribusi-kontak').value.trim();
const tipe = document.getElementById('kontribusi-tipe').value;
const jumlah = parseFloat(document.getElementById('kontribusi-jumlah').value);
const catatan = document.getElementById('kontribusi-catatan').value.trim();
if (!nama || !kontak || isNaN(jumlah) || jumlah <= 0) {
toast('Nama, Kontak, dan Jumlah Donasi wajib diisi!', 'error');
return;
}
const maxLimit = parseFloat(document.getElementById('kontribusi-jumlah').max);
if (jumlah > maxLimit) {
toast(`Jumlah kontribusi tidak boleh melebihi sisa kebutuhan (${maxLimit})!`, 'error');
return;
}
try {
const res = await fetch('api/kontribusi.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
kebutuhan_id: parseInt(kebutuhanId),
nama_kontributor: nama,
kontak_kontributor: kontak,
tipe_kontribusi: tipe,
jumlah,
catatan
})
});
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
closeModal('modal-kontribusi-form');
load();
} else {
toast(json.message, 'error');
}
} catch (e) {
toast('Gagal mengirimkan donasi', 'error');
}
}
async function verifyDonasi(id, status) {
if (!confirm(`Apakah Anda yakin ingin ${status === 'confirmed' ? 'menyetujui' : 'menolak'} donasi ini?`)) return;
try {
const res = await fetch('api/kontribusi.php', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: parseInt(id), status })
});
const json = await res.json();
if (json.success) {
toast(json.message, 'success');
load();
} else {
toast(json.message, 'error');
}
} catch (e) {
toast('Gagal memverifikasi donasi', 'error');
}
}
return {
load,
switchTab,
openFormModal,
saveKebutuhan,
deleteKebutuhan,
triggerDonation,
openDonationModal,
saveDonation,
verifyDonasi,
refreshIfActive
};
})();
/* ══════════════════════════════════════════════════
INISIALISASI TAB & EVENT LISTENER
══════════════════════════════════════════════════ */
function init() {
// Tambah tab baru ke sidebar jika belum ada
injectSidebarTabs();
injectModals();
bindEvents();
Kebutuhan.load();
}
function injectSidebarTabs() {
// 1. Inject the three feature modals directly to body
document.body.insertAdjacentHTML('beforeend', buildRedistribusiPanel());
document.body.insertAdjacentHTML('beforeend', buildVerifikasiPanel());
document.body.insertAdjacentHTML('beforeend', buildKemandirianPanel());
// 2. Inject the advanced features section inside Overview tab
const overviewTab = document.getElementById('tab-overview');
if (overviewTab) {
const fiturHtml = `
<!-- Fitur Lanjutan -->
<div class="section-title" style="margin-top: 15px;">
${svgIcon(ICONS.gear, 12, 12)}
Fitur Lanjutan
</div>
<div class="ekon-grid" style="margin-bottom: 14px; display: flex; flex-direction: column; gap: 8px;">
<div class="ekon-card"
onmouseover="this.style.borderColor='var(--accent-light)'"
onmouseout="this.style.borderColor='var(--border)'"
style="cursor: pointer; padding: 12px; transition: all 0.2s; border-left: 3px solid var(--accent-light); flex: 1; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center;"
onclick="FiturBaru.Redistribusi.open()">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 11px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 6px;">
${svgIcon(ICONS.swap, 12, 12)}
Redistribusi Bantuan
</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; white-space: normal;">
Analisis beban distribusi & rekomendasi transfer bantuan antar rumah ibadah.
</div>
</div>
</div>
<div class="ekon-card"
onmouseover="this.style.borderColor='var(--success)'"
onmouseout="this.style.borderColor='var(--border)'"
style="cursor: pointer; padding: 12px; transition: all 0.2s; border-left: 3px solid var(--success); flex: 1; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center;"
onclick="FiturBaru.Verifikasi.open()">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 11px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 6px;">
${svgIcon(ICONS.shield, 12, 12)}
Keterbaruan &amp; Verifikasi Data
</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; white-space: normal;">
Pantau waktu verifikasi, skor kepercayaan data, dan rekam kunjungan lapangan.
</div>
</div>
</div>
<div class="ekon-card"
onmouseover="this.style.borderColor='var(--warning)'"
onmouseout="this.style.borderColor='var(--border)'"
style="cursor: pointer; padding: 12px; transition: all 0.2s; border-left: 3px solid var(--warning); flex: 1; border: 1px solid var(--border); border-radius: 8px; display: flex; align-items: center;"
onclick="FiturBaru.Kemandirian.open()">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 11px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 6px;">
${svgIcon(ICONS.trend, 12, 12)}
Potensi Kemandirian Warga
</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 3px; line-height: 1.4; white-space: normal;">
Skor kemandirian warga, grafik gauge interaktif, dan penentuan exit plan.
</div>
</div>
</div>
</div>
`;
const legendaTitle = document.getElementById('section-legenda');
if (legendaTitle) {
legendaTitle.insertAdjacentHTML('beforebegin', fiturHtml);
} else {
overviewTab.insertAdjacentHTML('beforeend', fiturHtml);
}
}
}
/* ─── Premium Overlay Modals HTML Builders ─── */
function buildRedistribusiPanel() {
return `<div class="modal-overlay" id="modal-premium-redistribusi">
<div class="modal" style="width: 520px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header" style="position: sticky; top: 0; background: var(--bg-panel); z-index: 10; border-bottom: 1px solid var(--border);">
<div class="modal-title" style="display: flex; align-items: center; gap: 8px;">
${svgIcon(ICONS.swap, 14, 14)}
<span>Analisis &amp; Redistribusi Bantuan</span>
</div>
<button class="modal-close" onclick="document.getElementById('modal-premium-redistribusi').classList.remove('show')">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body" style="padding: 16px; overflow-y: auto; flex: 1;">
<div class="fb-sub-tabs">
<button class="fb-sub-tab active" data-fbtab="analisis" onclick="switchFbTab(this,'redistribusi')">Analisis Beban</button>
<button class="fb-sub-tab" data-fbtab="saran" onclick="switchFbTab(this,'redistribusi')">Saran Sistem</button>
<button class="fb-sub-tab" data-fbtab="riwayat" onclick="switchFbTab(this,'redistribusi')">Riwayat</button>
</div>
<div class="fb-sub-panel active" id="redistribusi-analisis-wrap">
<div id="redistribusi-summary-container"></div>
<input type="text" id="redistribusi-search" class="form-control" placeholder="Cari rumah ibadah..." style="font-size:11px;padding:6px;margin-top:8px;margin-bottom:8px" oninput="FiturBaru.Redistribusi.renderSearch()">
<div id="redistribusi-analisis">
<div class="fb-empty">Memuat analisis beban...</div>
</div>
</div>
<div class="fb-sub-panel" id="redistribusi-saran-wrap">
<div id="redistribusi-saran"><div class="fb-empty">Memuat saran...</div></div>
</div>
<div class="fb-sub-panel" id="redistribusi-riwayat-wrap">
<div id="redistribusi-riwayat"><div class="fb-empty">Memuat riwayat...</div></div>
</div>
</div>
</div>
</div>`;
}
function buildVerifikasiPanel() {
return `<div class="modal-overlay" id="modal-premium-verifikasi">
<div class="modal" style="width: 520px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header" style="position: sticky; top: 0; background: var(--bg-panel); z-index: 10; border-bottom: 1px solid var(--border);">
<div class="modal-title" style="display: flex; align-items: center; gap: 8px;">
${svgIcon(ICONS.shield, 14, 14)}
<span>Keterbaruan &amp; Verifikasi Data</span>
</div>
<button class="modal-close" onclick="document.getElementById('modal-premium-verifikasi').classList.remove('show'); document.body.classList.remove('side-by-side-modals');">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body" style="padding: 16px; overflow-y: auto; flex: 1;">
<div class="verif-summary-row" style="margin-bottom: 12px; display: flex; gap: 8px;">
<div class="verif-sum-card ok" style="flex: 1; text-align: center; padding: 6px; border-radius: 6px; background: rgba(16,185,129,0.08); border: 1px solid rgba(16,185,129,0.2);"><span id="verif-count-ok" style="font-weight: 700; color: var(--success);"></span><small style="font-size: 9px; color: var(--text-muted);">Terverifikasi</small></div>
<div class="verif-sum-card warn" style="flex: 1; text-align: center; padding: 6px; border-radius: 6px; background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.2);"><span id="verif-count-warn" style="font-weight: 700; color: var(--warning);"></span><small style="font-size: 9px; color: var(--text-muted);">Perlu Update</small></div>
<div class="verif-sum-card err" style="flex: 1; text-align: center; padding: 6px; border-radius: 6px; background: rgba(239,68,68,0.08); border: 1px solid rgba(239,68,68,0.2);"><span id="verif-count-err" style="font-weight: 700; color: var(--danger);"></span><small style="font-size: 9px; color: var(--text-muted);">Kadaluarsa</small></div>
</div>
<div class="fb-sub-tabs">
<button class="fb-sub-tab active" data-fbtab="list" onclick="switchFbTab(this,'verifikasi')">Daftar Warga</button>
<button class="fb-sub-tab" data-fbtab="konfig" onclick="switchFbTab(this,'verifikasi')">Konfigurasi</button>
</div>
<div class="fb-sub-panel active" id="verifikasi-list-wrap">
<div style="display:flex;gap:4px;margin-bottom:8px">
<input type="text" id="verifikasi-search" class="form-control" placeholder="Cari nama, NIK, KK..." style="font-size:11px;padding:6px;flex:1" oninput="FiturBaru.Verifikasi.renderSearch()">
<select id="filter-verif-status" class="form-control" style="font-size:11px;padding:5px;width:120px"
onchange="FiturBaru.Verifikasi.load(this.value)">
<option value="">Perlu Verifikasi</option>
<option value="kadaluarsa">Kadaluarsa</option>
<option value="perlu_update">Perlu Update</option>
<option value="terverifikasi">Terverifikasi</option>
<option value="semua">Semua Warga</option>
</select>
</div>
<div id="verifikasi-list"><div class="fb-empty">Memuat data...</div></div>
</div>
<div class="fb-sub-panel" id="verifikasi-konfig-wrap">
<div class="konfig-intro" style="font-size: 11px; color: var(--text-muted); margin-bottom: 10px;">Atur parameter keterbaruan data dan ambang batas kemandirian.</div>
<div id="konfig-list"><div class="fb-loading">${svgIcon(ICONS.gear,16,16)}<span>Memuat konfigurasi...</span></div></div>
<button class="btn btn-primary btn-block" style="margin-top:10px" onclick="FiturBaru.Verifikasi.simpanKonfig()">
${svgIcon(ICONS.check2,12,12)} Simpan Konfigurasi
</button>
</div>
</div>
</div>
</div>`;
}
function buildKemandirianPanel() {
return `<div class="modal-overlay" id="modal-premium-kemandirian">
<div class="modal" style="width: 520px; max-width: 95vw; max-height: 85vh; display: flex; flex-direction: column;">
<div class="modal-header" style="position: sticky; top: 0; background: var(--bg-panel); z-index: 10; border-bottom: 1px solid var(--border);">
<div class="modal-title" style="display: flex; align-items: center; gap: 8px;">
${svgIcon(ICONS.trend, 14, 14)}
<span>Potensi Kemandirian Warga</span>
<div class="kemandirian-siap-badge" style="margin-left: 8px; font-size: 10px; background: rgba(16,185,129,0.12); color: var(--success); padding: 2px 6px; border-radius: 4px; font-weight: 700;">
<span id="kemandirian-siap-exit"></span> siap exit
</div>
</div>
<button class="modal-close" onclick="document.getElementById('modal-premium-kemandirian').classList.remove('show')">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body" style="padding: 16px; overflow-y: auto; flex: 1;">
<div style="display:flex;gap:4px;margin-bottom:8px">
<input type="text" id="kemandirian-search" class="form-control" placeholder="Cari nama warga..." style="font-size:11px;padding:6px;flex:1" oninput="FiturBaru.Kemandirian.renderSearch()">
<select id="filter-kemandirian-status" class="form-control" style="font-size:11px;padding:5px;width:130px"
onchange="FiturBaru.Kemandirian.load(this.value)">
<option value="">Semua Status</option>
<option value="mandiri">Mandiri / Siap Exit</option>
<option value="berpotensi_mandiri">Berpotensi Mandiri</option>
<option value="berkembang">Berkembang</option>
<option value="bergantung">Bergantung</option>
<option value="sangat_bergantung">Sangat Bergantung</option>
</select>
</div>
<div id="kemandirian-list"><div class="fb-empty">Memuat data...</div></div>
</div>
</div>
</div>`;
}
/* ─── Modal HTML ─── */
function injectModals() {
document.body.insertAdjacentHTML('beforeend', `
<!-- Modal Verifikasi -->
<div class="modal-overlay" id="modal-verifikasi">
<div class="modal" style="width:400px">
<div class="modal-header">
<div class="modal-title">Catat Verifikasi Data</div>
<button class="modal-close" onclick="document.getElementById('modal-verifikasi').classList.remove('show')">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="verif-modal-id">
<div style="font-weight:600;color:var(--text-primary);margin-bottom:12px" id="verif-modal-nama"></div>
<div class="form-group">
<label class="form-label">Tanggal Verifikasi</label>
<input type="date" id="verif-modal-tanggal" class="form-control">
</div>
<div class="form-group">
<label class="form-label">Nama Petugas</label>
<input type="text" id="verif-modal-petugas" class="form-control" placeholder="Nama petugas lapangan">
</div>
<div class="form-group">
<label class="form-label">Catatan</label>
<textarea id="verif-modal-catatan" class="form-control" rows="3" placeholder="Kondisi saat verifikasi..."></textarea>
</div>
<div class="verif-info-box">
${svgIcon(ICONS.shield,13,13)} Menyimpan verifikasi akan mereset skor kepercayaan data ke 100%.
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('modal-verifikasi').classList.remove('show')">Batal</button>
<button class="btn btn-primary" onclick="FiturBaru.Verifikasi.simpanVerifikasi()">
${svgIcon(ICONS.check2,12,12)} Simpan Verifikasi
</button>
</div>
</div>
</div>
<!-- Modal Catatan Kemandirian -->
<div class="modal-overlay" id="modal-kemandirian">
<div class="modal" style="width:420px">
<div class="modal-header">
<div class="modal-title">Catatan Perkembangan Kemandirian</div>
<button class="modal-close" onclick="document.getElementById('modal-kemandirian').classList.remove('show')">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<input type="hidden" id="kemandirian-modal-id">
<div style="font-weight:600;color:var(--text-primary);margin-bottom:12px" id="kemandirian-modal-nama"></div>
<div class="form-group">
<label class="form-label">Target Tanggal Exit Plan</label>
<input type="date" id="kemandirian-modal-exit" class="form-control">
<div class="form-hint">Kapan warga ini ditargetkan keluar dari program bantuan</div>
</div>
<div class="form-group">
<label class="form-label">Catatan Perkembangan</label>
<textarea id="kemandirian-modal-catatan" class="form-control" rows="4"
placeholder="Perkembangan kondisi keluarga, pencapaian, rencana intervensi..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('modal-kemandirian').classList.remove('show')">Batal</button>
<button class="btn btn-primary" onclick="FiturBaru.Kemandirian.simpanCatatan()">
${svgIcon(ICONS.check2,12,12)} Simpan Catatan
</button>
</div>
</div>
</div>
`);
// Modal Setujui Redistribusi
document.body.insertAdjacentHTML('beforeend', `
<div class="modal-overlay" id="modal-setujui-redistribusi">
<div class="modal" style="width:450px">
<div class="modal-header">
<div class="modal-title">Setujui Redistribusi</div>
<button class="modal-close" onclick="document.getElementById('modal-setujui-redistribusi').classList.remove('show')">
<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2.5"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<div class="modal-body">
<div style="font-weight:600;color:var(--text-primary);margin-bottom:4px" id="sr-penerima-nama">Masjid Penerima</div>
<div style="font-size:11px;color:var(--text-muted);margin-bottom:12px">Menerima tambahan beban tanggungan (defisit kapasitas)</div>
<input type="hidden" id="sr-penerima-id">
<input type="hidden" id="sr-estimasi-dana-per-kk">
<input type="hidden" id="sr-catatan-sistem">
<div class="form-group">
<label class="form-label">Alihkan Tanggungan Ke (Rumah Ibadah Surplus)</label>
<select id="sr-pengirim-id" class="form-control"></select>
</div>
<div class="form-row">
<div class="form-group" style="flex:1">
<label class="form-label">Jumlah KK Dipindah</label>
<input type="number" id="sr-jumlah-kk" class="form-control" readonly style="background:#f1f5f9; font-weight:bold;">
<div class="form-hint">Dihitung otomatis dari warga yang dipilih.</div>
</div>
<div class="form-group" style="flex:1">
<label class="form-label">Estimasi Dana</label>
<input type="text" id="sr-estimasi-dana-display" class="form-control" readonly style="background:#f1f5f9">
<input type="hidden" id="sr-estimasi-dana">
</div>
</div>
<div class="form-group">
<label class="form-label">Pilih Warga yang Dipindahkan <span style="font-weight:normal;color:#64748b;font-size:10px">(Otomatis rekomendasi sistem)</span></label>
<div id="sr-warga-list-container" style="max-height:180px; overflow-y:auto; border:1px solid #e2e8f0; border-radius:6px; padding:8px; background:#fafafa; font-size:11px;">
<div style="text-align:center; padding:10px; color:#94a3b8">Memuat warga...</div>
</div>
</div>
<div class="form-group">
<label class="form-label">Catatan Admin (Opsional)</label>
<textarea id="sr-catatan" class="form-control" rows="2" placeholder="Tambahkan catatan keputusan..."></textarea>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-ghost" onclick="document.getElementById('modal-setujui-redistribusi').classList.remove('show')">Batal</button>
<button class="btn btn-primary" onclick="FiturBaru.Redistribusi.submitSetujui()">
${svgIcon(ICONS.check2,12,12)} Simpan Keputusan
</button>
</div>
</div>
</div>
`);
}
function bindEvents() {
// Keyboard close
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
closeModal('modal-verifikasi');
closeModal('modal-kemandirian');
closeModal('modal-setujui-redistribusi');
closeModal('modal-premium-redistribusi');
closeModal('modal-premium-verifikasi');
closeModal('modal-premium-kemandirian');
closeModal('modal-kebutuhan-form');
closeModal('modal-kontribusi-form');
closeModal('modal-laporan-form');
document.body.classList.remove('side-by-side-modals');
}
});
document.getElementById('btn-kebutuhan-save')?.addEventListener('click', () => Kebutuhan.saveKebutuhan());
document.getElementById('btn-kontribusi-save')?.addEventListener('click', () => Kebutuhan.saveDonation());
}
/* ─── Sub-tab switcher (global) ─── */
window.switchFbTab = function(btn, prefix) {
const tabId = btn.dataset.fbtab;
const scope = document.getElementById(`modal-premium-${prefix}`) || document.getElementById(`tab-${prefix}`);
if (!scope) return;
scope.querySelectorAll('.fb-sub-tab').forEach(b => b.classList.toggle('active', b.dataset.fbtab === tabId));
scope.querySelectorAll('.fb-sub-panel').forEach(p => {
p.classList.toggle('active', p.id === `${prefix}-${tabId}-wrap`);
});
};
/* ══════════════════════════════════════════════════
MODUL LAPORAN WARGA
══════════════════════════════════════════════════ */
const Laporan = {
async loadList() {
const currentUser = App.getCurrentUser();
if (!currentUser || currentUser.role === 'guest') return;
const panel = document.getElementById('laporan-panel-content');
if (!panel) return;
const isAdminOrPetugas = currentUser.role === 'admin' || currentUser.role === 'petugas';
try {
const res = await fetch('api/laporan.php?_t=' + Date.now());
const json = await res.json();
if (!json.success) throw new Error(json.message);
let petugasList = [];
if (isAdminOrPetugas && json.petugas) {
petugasList = json.petugas;
}
let html = '';
if (isAdminOrPetugas) {
html += `
<div style="font-size:12px; font-weight:700; margin-bottom:10px; color:var(--text-primary); display:flex; justify-content:space-between; align-items:center;">
<span>Daftar Laporan Warga Masuk <span style="font-size:10px; color:var(--text-muted); font-weight:500">(Total: ${json.total})</span></span>
<select onchange="FiturBaru.Laporan.filterUI(this.value)" style="padding:4px 8px; font-size:11px; border-radius:4px; border:1px solid #d1d5db;">
<option value="all">Semua Status</option>
<option value="pending">Menunggu (${json.summary.pending || 0})</option>
<option value="ditinjau">Sedang Ditinjau</option>
<option value="diproses">Diproses Petugas</option>
<option value="selesai">Selesai</option>
<option value="ditolak">Ditolak</option>
</select>
</div>
`;
} else {
html += `
<div style="font-size:12px; font-weight:700; margin-bottom:10px; color:var(--text-primary);">
Riwayat Laporan Saya
</div>
`;
}
if (!json.data || json.data.length === 0) {
html += `<div class="empty-state">${svgIcon(ICONS.info, 28, 28)}<br>Belum ada laporan.</div>`;
panel.innerHTML = html;
return;
}
let listHtml = '<div class="data-list" id="laporan-data-list">';
json.data.forEach(l => {
const jenisLabel = {
'warga_mampu': 'Warga Mampu',
'data_tidak_sesuai': 'Data Tidak Sesuai',
'warga_pindah': 'Warga Pindah',
'warga_meninggal': 'Warga Meninggal',
'data_ganda': 'Data Ganda',
'lainnya': 'Lainnya'
}[l.jenis_laporan] || l.jenis_laporan;
const st = l.status;
const statusBadge = `<span class="badge badge-laporan-${st}">${st.replace('_', ' ').toUpperCase()}</span>`;
const pr = l.prioritas;
const prioBadge = `<span class="badge badge-prioritas-${pr}">PRIORITAS ${pr.toUpperCase()}</span>`;
const date = new Date(l.created_at).toLocaleDateString('id-ID', {day:'numeric', month:'short', year:'numeric', hour:'2-digit', minute:'2-digit'});
let petugasOpts = '';
petugasList.forEach(p => {
petugasOpts += `<option value="${p}" ${l.ditugaskan_ke === p ? 'selected' : ''}>${p}</option>`;
});
listHtml += `
<div class="laporan-card prioritas-${pr}" data-status="${st}">
<div class="laporan-card-header">
<div style="flex:1; min-width:0;">
<div class="laporan-card-nama">${l.nama_kepala_keluarga}</div>
<div class="laporan-card-meta">${l.kelurahan || '-'}, ${l.kecamatan || '-'}</div>
</div>
${statusBadge}
</div>
<div class="laporan-card-jenis">${jenisLabel}</div>
<div class="laporan-card-desc">" ${l.deskripsi} "</div>
<div class="laporan-card-footer" style="display:flex; justify-content:space-between; align-items:center;">
<div style="font-size:9px; color:var(--text-muted);">${date} • Oleh: ${l.pelapor_nama}</div>
<div style="display:flex; gap:6px; align-items:center;">
${prioBadge}
${(l.ditugaskan_ke && st === 'diproses') ? `<span style="font-size:9px; color:#4f46e5; font-weight:700; background:#e0e7ff; padding:2px 6px; border-radius:12px; border:1px solid rgba(79,70,229,0.3);">👤 ${l.ditugaskan_ke}</span>` : ''}
</div>
</div>
`;
if (l.dokumentasi) {
listHtml += `<div class="laporan-catatan-admin" style="background:#f0fdf4; border-color:#86efac; margin-top:5px;"><b>Link Dokumentasi Lapangan:</b> <a href="${l.dokumentasi}" target="_blank" style="color:#166534; text-decoration:underline; word-break:break-all;">${l.dokumentasi}</a></div>`;
}
if (isAdminOrPetugas) {
listHtml += `
<div class="laporan-admin-actions" style="margin-top:10px; display:flex; gap:8px;">
<button class="btn btn-primary btn-xs" onclick="document.getElementById('lap-form-${l.id}').style.display='block'">Perbarui Status</button>
${currentUser.role === 'admin' ? `<button class="btn btn-danger btn-xs" onclick="FiturBaru.Laporan.hapus(${l.id})">Hapus</button>` : ''}
</div>
<div id="lap-form-${l.id}" style="display:none; margin-top:10px; background:#f9fafb; padding:10px; border-radius:6px; border:1px solid #e5e7eb;">
<label style="font-size:10px; font-weight:600; color:#4b5563;">Status Laporan:</label>
<select id="lap-status-${l.id}" style="width:100%; margin-bottom:8px; padding:6px; border-radius:4px; border:1px solid #d1d5db; font-size:11px;" onchange="if(this.value==='diproses'){document.getElementById('lap-petugas-wrap-${l.id}')?.style.setProperty('display', 'block');}else{document.getElementById('lap-petugas-wrap-${l.id}')?.style.setProperty('display', 'none');}">
<option value="pending" ${st==='pending'?'selected':''}>Menunggu</option>
<option value="ditinjau" ${st==='ditinjau'?'selected':''}>Sedang Ditinjau</option>
<option value="diproses" ${st==='diproses'?'selected':''}>Sedang Diproses (Petugas Lapangan)</option>
<option value="selesai" ${st==='selesai'?'selected':''}>Selesai / Terverifikasi</option>
<option value="ditolak" ${st==='ditolak'?'selected':''}>Ditolak / Tidak Valid</option>
</select>
${currentUser.role === 'admin' ? `
<div id="lap-petugas-wrap-${l.id}" style="display:${st==='diproses'?'block':'none'}; margin-bottom:8px;">
<label style="font-size:10px; font-weight:600; color:#4b5563;">Pilih Petugas:</label>
<select id="lap-petugas-${l.id}" style="width:100%; padding:6px; border-radius:4px; border:1px solid #d1d5db; font-size:11px;">
<option value="">-- Pilih Petugas --</option>
${petugasOpts}
</select>
</div>
` : ''}
<label style="font-size:10px; font-weight:600; color:#4b5563;">Catatan / Tanggapan:</label>
<input type="text" id="lap-catatan-${l.id}" placeholder="Tulis tanggapan atau catatan peninjauan..." value="${l.catatan_admin || ''}" style="width:100%; margin-bottom:8px; padding:6px; border-radius:4px; border:1px solid #d1d5db; font-size:11px;">
<label style="font-size:10px; font-weight:600; color:#4b5563;">Upload Bukti Lapangan (Opsional):</label>
<input type="file" id="lap-dokumentasi-${l.id}" accept="image/*,application/pdf" style="width:100%; margin-bottom:8px; padding:4px; border-radius:4px; border:1px solid #d1d5db; font-size:11px;">
<div style="display:flex; justify-content:flex-end; gap:6px;">
<button class="btn btn-outline btn-xs" onclick="document.getElementById('lap-form-${l.id}').style.display='none'">Batal</button>
<button class="btn btn-success btn-xs" onclick="FiturBaru.Laporan.updateStatus(${l.id})">Simpan</button>
</div>
</div>
`;
}
listHtml += `</div>`;
});
listHtml += '</div>';
html += listHtml;
panel.innerHTML = html;
} catch (err) {
panel.innerHTML = `<div class="empty-state">${svgIcon(ICONS.info, 28, 28)}<br>Gagal memuat laporan.</div>`;
}
},
filterUI(status) {
const cards = document.querySelectorAll('#laporan-data-list .laporan-card');
cards.forEach(c => {
if (status === 'all') c.style.display = 'block';
else if (c.dataset.status === status) c.style.display = 'block';
else c.style.display = 'none';
});
},
bukaModalLaporan(wargaId, namaWarga) {
document.getElementById('laporan-warga-id').value = wargaId;
document.getElementById('laporan-warga-nama-display').textContent = 'Melaporkan Data: ' + namaWarga;
document.getElementById('laporan-jenis').value = 'data_tidak_sesuai';
document.getElementById('laporan-prioritas').value = 'sedang';
document.getElementById('laporan-deskripsi').value = '';
document.getElementById('modal-laporan-form').classList.add('show');
},
async submitLaporan() {
const wargaId = document.getElementById('laporan-warga-id').value;
const jenis = document.getElementById('laporan-jenis').value;
const prioritas = document.getElementById('laporan-prioritas').value;
const deskripsi = document.getElementById('laporan-deskripsi').value.trim();
if (!deskripsi || deskripsi.length < 20) {
showToast('Deskripsi wajib diisi minimal 20 karakter', 'error');
return;
}
const btn = document.getElementById('btn-laporan-submit');
btn.disabled = true;
btn.innerHTML = 'Mengirim...';
try {
const res = await fetch('api/laporan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rumah_warga_id: wargaId,
jenis_laporan: jenis,
prioritas: prioritas,
deskripsi: deskripsi
})
});
const json = await res.json();
if (json.success) {
showToast(json.message, 'success');
closeModal('modal-laporan-form');
// Reload list jika sedang membuka tab laporan
if (document.getElementById('tab-laporan')?.classList.contains('active')) {
this.loadList();
}
} else {
showToast(json.message || 'Gagal mengirim laporan', 'error');
}
} catch (err) {
showToast('Terjadi kesalahan koneksi', 'error');
} finally {
btn.disabled = false;
btn.innerHTML = `<svg viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" fill="none" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> Kirim Laporan`;
}
},
async updateStatus(id) {
const btn = event.currentTarget || event.target;
const originalText = btn.innerHTML;
btn.innerHTML = 'Menyimpan...';
btn.disabled = true;
const status = document.getElementById(`lap-status-${id}`).value;
const catatanEl = document.getElementById(`lap-catatan-${id}`);
const catatan = catatanEl ? catatanEl.value.trim() : '';
const fileInput = document.getElementById(`lap-dokumentasi-${id}`);
let ditugaskan_ke = '';
const petugasSelect = document.getElementById(`lap-petugas-${id}`);
if (petugasSelect && status === 'diproses') {
ditugaskan_ke = petugasSelect.value;
}
const formData = new FormData();
formData.append('_method', 'PUT');
formData.append('id', id);
formData.append('status', status);
formData.append('catatan_admin', catatan);
formData.append('ditugaskan_ke', ditugaskan_ke);
if (fileInput && fileInput.files.length > 0) {
formData.append('dokumentasi_file', fileInput.files[0]);
}
try {
const res = await fetch('api/laporan.php', {
method: 'POST',
body: formData
});
const json = await res.json();
if (json.success) {
if (typeof showToast === 'function') showToast('Data berhasil diperbarui!', 'success');
else alert('Data berhasil diperbarui!');
this.loadList();
} else {
if (typeof showToast === 'function') showToast(json.message || 'Gagal memperbarui', 'error');
else alert(json.message || 'Gagal memperbarui');
}
} catch (err) {
if (typeof showToast === 'function') showToast('Terjadi kesalahan saat mengupdate', 'error');
else alert('Terjadi kesalahan saat mengupdate');
} finally {
if (btn) {
btn.innerHTML = originalText;
btn.disabled = false;
}
}
},
async hapus(id) {
if (!confirm('Apakah Anda yakin ingin menghapus laporan ini?')) return;
try {
const res = await fetch('api/laporan.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
});
const json = await res.json();
if (json.success) {
showToast('Laporan dihapus', 'success');
this.loadList();
} else {
showToast(json.message || 'Gagal menghapus', 'error');
}
} catch (err) {
showToast('Terjadi kesalahan', 'error');
}
}
};
/* ══════════════════════════════════════════════════
PUBLIC API
══════════════════════════════════════════════════ */
return { init, Redistribusi, Verifikasi, Kemandirian, Kebutuhan, Laporan };
})();
// Expose FiturBaru globally
window.FiturBaru = FiturBaru;
document.addEventListener('DOMContentLoaded', () => FiturBaru.init());