// 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 ``;
}
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.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 `
${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 ``;
}
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 `
${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 ``;
}
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}
`;
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 `
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 `
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 `
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 `
–Terverifikasi
–Perlu Update
–Kadaluarsa
Atur parameter keterbaruan data dan ambang batas kemandirian.
${svgIcon(ICONS.gear,16,16)}Memuat konfigurasi...
`;
}
function buildKemandirianPanel() {
return ``;
}
/* ─── Modal HTML ─── */
function injectModals() {
document.body.insertAdjacentHTML('beforeend', `
`);
// Modal Setujui Redistribusi
document.body.insertAdjacentHTML('beforeend', `
Masjid Penerima
Menerima tambahan beban tanggungan (defisit kapasitas)
`);
}
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 += `
${jenisLabel}
" ${l.deskripsi} "
`;
if (l.dokumentasi) {
listHtml += `
`;
}
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());