1997 lines
97 KiB
JavaScript
1997 lines
97 KiB
JavaScript
// 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 & 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 & 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 & 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 & 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()); |