// assets/js/fitur_baru.js // Modul 3 Fitur Baru WebGIS Bansos // Integrasi dengan App (app.js) yang sudah ada. // Cara pakai: SETELAH app.js const FiturBaru = (() => { /* ══════════════════════════════════════════════════ SHARED HELPERS ══════════════════════════════════════════════════ */ function svgIcon(paths, w = 14, h = 14) { return `${paths}`; } const ICONS = { swap: ``, shield: ``, trend: ``, clock: ``, check2: ``, alert: ``, gear: ``, exit: ``, chart: ``, refresh: ``, }; 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 = `
${svgIcon(ICONS.refresh, 18, 18)}Menganalisis beban distribusi...
`; 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 = `
${svgIcon(ICONS.alert,16,16)} Gagal memuat: ${e.message}
`; } } 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 = `
${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 `
${v} ${cfg.label || k}
`; }).join('')}
`; } } // 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 `
${d.ibadah.nama}
${d.ibadah.kelurahan || ''} ${d.ibadah.kecamatan ? '· ' + d.ibadah.kecamatan : ''}
${cfg.label}
${d.pct_beban}%
${d.sangat_miskin} Sangat Miskin
${d.miskin} Miskin
${d.rentan_miskin} Rentan
${d.total_warga}/${d.kapasitas} KK/Kap.
Rasio kemiskinan: ${d.rasio_miskin}% ${d.surplus_kapasitas > 0 ? `· Surplus: +${d.surplus_kapasitas} KK` : ''} ${d.defisit_kapasitas > 0 ? `· Defisit: -${d.defisit_kapasitas} KK` : ''}
`; }).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 = `
${svgIcon(ICONS.check2,20,20)}
Distribusi sudah merata. Tidak ada saran redistribusi saat ini.
`; return; } container.innerHTML = saran.map((s, i) => { const priCfg = PRIORITAS_CONFIG[s.prioritas] || PRIORITAS_CONFIG.rendah; return `
${priCfg.label} ${s.jarak_km} km
${svgIcon(ICONS.swap,12,12)} ${s.pengirim_nama}
→ ${s.jumlah_kk} KK →
${s.penerima_nama} ${svgIcon(ICONS.alert,12,12)}
${s.alasan}
Est. dana: ${formatRp(s.estimasi_dana)}/bulan
`; }).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 = `
Belum ada riwayat redistribusi
`; 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 => `
${r.pengirim_nama} → ${r.penerima_nama}
${r.jumlah_kk} KK · ${formatRp(r.estimasi_dana)}/bln · ${r.periode_bulan || ''}
${STATUS_LABEL[r.status] || r.status}
${r.catatan_admin ? `
${r.catatan_admin}
` : ''}
`).join(''); } catch { container.innerHTML = `
Gagal memuat riwayat
`; } } 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 ``; }).join(''); // Jika saran pengirim tidak ada di list surplus (mungkin list kosong), tambahkan manual if (select.innerHTML === '') { select.innerHTML = ``; } // Get eligible warga: dikelola_oleh_ibadah_id == penerima_id or (null and in radius) const container = document.getElementById('sr-warga-list-container'); container.innerHTML = '
Memuat warga...
'; 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 = '
Tidak ada warga eligible yang dapat dipindahkan.
'; } else { // Render checkboxes let html = ''; eligibleWarga.forEach((w, idx) => { const checked = idx < s.jumlah_kk ? 'checked' : ''; html += `
`; }); 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 = '
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 `
${skor}%
`; } let allData = []; async function load(statusFilter = '') { const container = document.getElementById('verifikasi-list'); if (!container) return; container.innerHTML = `
${svgIcon(ICONS.clock,18,18)}Memuat data verifikasi...
`; // 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 = `
${svgIcon(ICONS.alert,16,16)} ${e.message}
`; } } 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 = `
${svgIcon(ICONS.check2,20,20)}
Tidak ada data warga ditemukan
`; return; } container.innerHTML = filtered.map(w => { const stsCfg = STATUS_CFG[w.status_verifikasi] || STATUS_CFG.terverifikasi; const det = w.detail_skor || {}; return `
${w.nama_kepala_keluarga}
${w.kelurahan || ''} · No.KK: ${w.no_kk}
${stsCfg.label}
${skorBar(w.skor_kepercayaan)}
${svgIcon(ICONS.clock,10,10)} Verif: ${w.tanggal_verifikasi ? new Date(w.tanggal_verifikasi).toLocaleDateString('id-ID') : 'Belum pernah'} ${det.hari_sejak_verifikasi != null ? `${det.hari_sejak_verifikasi} hari lalu` : ''} ${det.tanggal_kadaluarsa ? `Exp: ${new Date(det.tanggal_kadaluarsa).toLocaleDateString('id-ID')}` : ''}
`; }).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 => `
${k.deskripsi ? `
${k.deskripsi}
` : ''}
`).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 `
${skor}
${label}
`; } let allData = []; async function load(statusFilter = '') { const container = document.getElementById('kemandirian-list'); if (!container) return; container.innerHTML = `
${svgIcon(ICONS.trend,18,18)}Menghitung skor kemandirian...
`; // 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 `
${w.nama_kepala_keluarga}
${formatRp(w.penghasilan_bulanan)}/bln · ${w.jumlah_tanggungan} tanggungan
${stsCfg.icon} ${stsCfg.label}
${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') : ''}
${estBulan > 0 ? `
${svgIcon(ICONS.clock,11,11)} Est. mandiri: ~${estBulan} bulan
` : w.status_kemandirian === 'mandiri' ? `
${svgIcon(ICONS.check2,11,11)} Siap keluar dari daftar bantuan
` : ''} ${det.rekomendasi?.length ? `
${det.rekomendasi.map(r => `
${svgIcon(ICONS.trend,10,10)} ${r}
`).join('')}
` : ''} ${w.tanggal_exit_plan ? `
${svgIcon(ICONS.exit,10,10)} Target exit: ${new Date(w.tanggal_exit_plan).toLocaleDateString('id-ID')}
` : ''}
`; }).join(''); } catch (e) { container.innerHTML = `
${svgIcon(ICONS.alert,16,16)} ${e.message}
`; } } 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 = `
${svgIcon(ICONS.chart,20,20)}
Tidak ada data warga ditemukan
`; 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 `
${w.nama_kepala_keluarga}
${formatRp(w.penghasilan_bulanan)}/bln · ${w.jumlah_tanggungan} tanggungan
${stsCfg.icon} ${stsCfg.label}
${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') : ''}
${estBulan > 0 ? `
${svgIcon(ICONS.clock,11,11)} Est. mandiri: ~${estBulan} bulan
` : w.status_kemandirian === 'mandiri' ? `
${svgIcon(ICONS.check2,11,11)} Siap keluar dari daftar bantuan
` : ''} ${det.rekomendasi?.length ? `
${det.rekomendasi.map(r => `
${svgIcon(ICONS.trend,10,10)} ${r}
`).join('')}
` : ''} ${w.tanggal_exit_plan ? `
${svgIcon(ICONS.exit,10,10)} Target exit: ${new Date(w.tanggal_exit_plan).toLocaleDateString('id-ID')}
` : ''}
`; }).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 = `
`; } else if (role === 'koordinator' || role === 'admin') { subTabsHtml = `
`; } else if (role === 'guest') { subTabsHtml = ''; } container.innerHTML = ` ${subTabsHtml}
Memuat data...
`; 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 = ` `; } if (kebutuhanData.length === 0) { subContainer.innerHTML = ` ${addBtnHtml}
Belum ada kebutuhan bantuan terdaftar.
`; 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 = `
`; } else { if (sisa > 0) { actionButtons = ` `; } else { actionButtons = `${svgIcon(ICONS.check2, 10, 10)} Terpenuhi`; } } const categoryIcons = { barang: '📦', uang: '💰', jasa: '🤝' }; const catIcon = categoryIcons[k.kategori] || '📦'; return `
${ibadahNama}
${catIcon} ${k.jenis_bantuan} (${k.kategori})
Terkumpul: ${gathered} / ${required} ${k.satuan ? (k.satuan === 'nominal' ? '(Rp)' : k.satuan) : ''} ${progress}%
`; }).join(''); subContainer.innerHTML = addBtnHtml + cardsHtml; } catch (e) { subContainer.innerHTML = `
${e.message}
`; } } 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 = `
Anda belum pernah mengirimkan donasi.
`; 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 `
${c.nama_ibadah} ${statusLabel}
Bantuan: ${c.jenis_bantuan}
Jumlah: ${c.jumlah} (${c.tipe_kontribusi}) ${new Date(c.created_at).toLocaleDateString('id-ID')}
${c.catatan ? `
Catatan: ${c.catatan}
` : ''}
`; }).join(''); } catch (e) { subContainer.innerHTML = `
${e.message}
`; } } 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 = `
Tidak ada donasi masuk untuk rumah ibadah Anda.
`; 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 `
${c.nama_kontributor} ${statusLabel}
Ibadah: ${c.nama_ibadah} · Bantuan: ${c.jenis_bantuan}
Jumlah: ${c.jumlah} (${c.tipe_kontribusi}) Contact: ${c.kontak_kontributor || '–'}
${c.catatan ? `
"${c.catatan}"
` : ''} ${isPending ? `
` : ''}
`; }).join(''); } catch (e) { subContainer.innerHTML = `
${e.message}
`; } } 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 => ``).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 = `
${svgIcon(ICONS.gear, 12, 12)} Fitur Lanjutan
${svgIcon(ICONS.swap, 12, 12)} Redistribusi Bantuan
Analisis beban distribusi & rekomendasi transfer bantuan antar rumah ibadah.
${svgIcon(ICONS.shield, 12, 12)} Keterbaruan & Verifikasi Data
Pantau waktu verifikasi, skor kepercayaan data, dan rekam kunjungan lapangan.
${svgIcon(ICONS.trend, 12, 12)} Potensi Kemandirian Warga
Skor kemandirian warga, grafik gauge interaktif, dan penentuan exit plan.
`; 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 ``; } function buildVerifikasiPanel() { return ``; } function buildKemandirianPanel() { return ``; } /* ─── Modal HTML ─── */ function injectModals() { document.body.insertAdjacentHTML('beforeend', ` `); // Modal Setujui Redistribusi document.body.insertAdjacentHTML('beforeend', ` `); } 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 += `
Daftar Laporan Warga Masuk (Total: ${json.total})
`; } else { html += `
Riwayat Laporan Saya
`; } if (!json.data || json.data.length === 0) { html += `
${svgIcon(ICONS.info, 28, 28)}
Belum ada laporan.
`; panel.innerHTML = html; return; } let listHtml = '
'; 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 = `${st.replace('_', ' ').toUpperCase()}`; const pr = l.prioritas; const prioBadge = `PRIORITAS ${pr.toUpperCase()}`; 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 += ``; }); listHtml += `
${l.nama_kepala_keluarga}
${l.kelurahan || '-'}, ${l.kecamatan || '-'}
${statusBadge}
${jenisLabel}
" ${l.deskripsi} "
`; if (l.dokumentasi) { listHtml += `
Link Dokumentasi Lapangan: ${l.dokumentasi}
`; } if (isAdminOrPetugas) { listHtml += `
${currentUser.role === 'admin' ? `` : ''}
`; } listHtml += `
`; }); listHtml += '
'; html += listHtml; panel.innerHTML = html; } catch (err) { panel.innerHTML = `
${svgIcon(ICONS.info, 28, 28)}
Gagal memuat laporan.
`; } }, 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 = ` 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());