/* ============================================================ public-reports.js — Admin panel: public report verification Loaded after app.js — requires loadAllData() and loadStats() on window (exported by app.js) ============================================================ */ 'use strict'; // ================================================================ // API wrapper for public reports // ================================================================ const ApiPublicReports = { async list(params = {}) { const qs = new URLSearchParams(params).toString(); return Http.request('api/public/report.php?action=list' + (qs ? '&' + qs : ''), { method: 'GET' }); }, async approve(id, body = {}) { // Menambahkan verified_by ke body untuk audit if (window.currentUserName) { body.verified_by = window.currentUserName; } return Http.post(`api/public/report.php?action=approve&id=${id}`, body); }, async reject(id, body = {}) { if (window.currentUserName) { body.rejected_by = window.currentUserName; } return Http.post(`api/public/report.php?action=reject&id=${id}`, body); }, async delete(id) { return Http.post(`api/public/report.php?action=delete&id=${id}`); }, }; // ================================================================ // Load pending reports into admin panel table // ================================================================ async function loadPendingReports() { const tbody = document.getElementById('pendingTbody'); if (!tbody) return; const status = document.getElementById('pendingStatusFilter')?.value ?? 'pending'; const params = { limit: 100 }; if (status) params.status = status; tbody.innerHTML = 'Memuat...'; const r = await ApiPublicReports.list(params); if (!r.ok || !r.data?.success) { tbody.innerHTML = 'Gagal memuat laporan publik.'; return; } const reports = r.data.data?.reports || []; // Update badge count (always count actual pending) const badge = document.getElementById('pendingBadge'); if (badge) { const pendingCount = reports.filter(rep => rep.status === 'pending').length; badge.textContent = pendingCount > 0 ? pendingCount : ''; } if (!reports.length) { const emptyMsg = status === 'pending' ? 'Tidak ada laporan yang menunggu verifikasi.' : 'Tidak ada laporan.'; tbody.innerHTML = ` ${emptyMsg} `; return; } const statusMap = { pending: { label: 'Menunggu', color: '#d97706', bg: '#fef6e4' }, approved: { label: 'Disetujui', color: '#0b9e73', bg: '#e0faf3' }, rejected: { label: 'Ditolak', color: '#d63230', bg: '#fff0f0' }, }; tbody.innerHTML = reports.map(rep => { const st = statusMap[rep.status] || { label: rep.status, color: '#9ba4b5', bg: '#f5f6f9' }; const canAct = rep.status === 'pending'; const dateStr = formatDateTime(rep.created_at); return ` ${dateStr}
${escapeHtml(rep.head_name || '—')}
${rep.reporter_name ? `
${escapeHtml(rep.reporter_name)}${rep.reporter_phone ? ' · ' + escapeHtml(rep.reporter_phone) : ''}
` : ''} ${escapeHtml(truncate(rep.address || '—', 38))} ${escapeHtml(truncate(rep.description || '—', 60))} ${buildPhotoMiniStrip(rep.proof_photos, 'uploads/reports/')} ${st.label} ${rep.admin_notes ? `
${escapeHtml(truncate(rep.admin_notes, 25))}
` : ''} ${rep.converted_household_id ? `
ID: ${rep.converted_household_id}
` : ''} ${canAct ? ` ` : ''} `; }).join(''); } /** * Renders a tiny inline strip of photo thumbnails in the admin table. * photos: JSON string or array of filenames; baseUrl: path prefix. */ function buildPhotoMiniStrip(photos, baseUrl) { let arr = []; try { arr = typeof photos === 'string' ? JSON.parse(photos) : (photos || []); } catch(e) { console.warn('buildPhotoMiniStrip parse error:', e); return ''; } if (!arr.length) return ''; const imgs = arr.slice(0, 3).map(name => { const safeName = encodeURIComponent(name); const src = baseUrl + safeName; return ``; }).join(''); const extra = arr.length > 3 ? `+${arr.length - 3}` : ''; return `
${imgs}${extra}
`; } /** Safely JSON-encode report object for inline onclick attribute */ function safeJson(obj) { return JSON.stringify(obj) .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); } // ================================================================ // Approve modal // ================================================================ function openApproveModal(reportId, reportData) { document.getElementById('approveReportId').value = reportId; document.getElementById('approveIncome').value = 0; document.getElementById('approveDependents').value = 1; document.getElementById('approveCondition').value = 'tidak_layak'; document.getElementById('approveEducation').value = 'sd'; document.getElementById('approveNotes').value = ''; const preview = document.getElementById('approveReportPreview'); const photoPreview = document.getElementById('approvePhotoPreview'); if (preview && reportData) { const data = typeof reportData === 'string' ? JSON.parse(reportData) : reportData; preview.innerHTML = `
${data.head_name || '—'}
${data.address || '—'}
"${truncate(data.description || '', 100)}"
${data.reporter_name ? `
${data.reporter_name}
` : ''}
`; // ── TAMPILKAN FOTO BUKTI ────────────────────────────── if (photoPreview) { let photosArr = []; try { photosArr = typeof data.proof_photos === 'string' ? JSON.parse(data.proof_photos) : (data.proof_photos || []); } catch(e) { console.warn('Failed to parse proof_photos:', e); } if (photosArr.length) { photoPreview.innerHTML = ''; const rId = reportId; // capture for closure const strip = PhotoUpload.buildSavedStrip( photosArr, 'uploads/reports/', true, // always removable in admin approve-modal (filename, wrap) => deleteReportPhoto(rId, filename, wrap) ); photoPreview.appendChild(strip); } else { photoPreview.innerHTML = '
Tidak ada foto bukti
'; } } } openModal('approveModal'); } document.getElementById('approveForm')?.addEventListener('submit', async (e) => { e.preventDefault(); const id = parseInt(document.getElementById('approveReportId').value); if (!id) return; const body = { income: parseInt(document.getElementById('approveIncome').value) || 0, dependents: parseInt(document.getElementById('approveDependents').value) || 1, house_condition: document.getElementById('approveCondition').value, education: document.getElementById('approveEducation').value, land_ownership: 'numpang', admin_notes: document.getElementById('approveNotes').value.trim() || null, }; showLoading(true); const r = await ApiPublicReports.approve(id, body); showLoading(false); if (r.ok && r.data?.success) { closeModal('approveModal'); showToast('Laporan disetujui. Data rumah ditambahkan ke peta.', 'success', 4000); loadPendingReports(); // Refresh map and stats if (typeof loadAllData === 'function') await loadAllData(); if (typeof loadStats === 'function') await loadStats(); updatePendingBadge(); } else { showToast(r.data?.message || 'Gagal menyetujui laporan.', 'error'); } }); // ================================================================ // Reject modal // ================================================================ function openRejectModal(reportId) { document.getElementById('rejectReportId').value = reportId; document.getElementById('rejectNotes').value = ''; openModal('rejectModal'); } document.getElementById('rejectForm')?.addEventListener('submit', async (e) => { e.preventDefault(); const id = parseInt(document.getElementById('rejectReportId').value); if (!id) return; const body = { admin_notes: document.getElementById('rejectNotes').value.trim() || null, }; showLoading(true); const r = await ApiPublicReports.reject(id, body); showLoading(false); if (r.ok && r.data?.success) { closeModal('rejectModal'); showToast('Laporan ditolak.', 'success'); loadPendingReports(); if (typeof loadStats === 'function') loadStats(); updatePendingBadge(); } else { showToast(r.data?.message || 'Gagal menolak laporan.', 'error'); } }); // ================================================================ // Delete // ================================================================ async function deletePublicReport(id) { if (!confirm('Hapus laporan ini secara permanen? Tindakan ini tidak dapat dibatalkan.')) return; showLoading(true); const r = await ApiPublicReports.delete(id); showLoading(false); if (r.ok && r.data?.success) { showToast('Laporan dihapus.', 'success'); loadPendingReports(); if (typeof loadStats === 'function') loadStats(); updatePendingBadge(); } else { showToast(r.data?.message || 'Gagal menghapus laporan.', 'error'); } } // ================================================================ // Fly to location on map // ================================================================ function flyToPublicReport(lat, lng) { closeModal('adminModal'); flyTo(parseFloat(lat), parseFloat(lng), 17); // Temporary highlight pulse const highlight = L.circleMarker([lat, lng], { radius: 20, color: '#d63230', fillColor: '#d63230', fillOpacity: 0.2, weight: 2.5, }).addTo(MAP); setTimeout(() => { if (MAP.hasLayer(highlight)) MAP.removeLayer(highlight); }, 5000); showToast('Lokasi laporan ditampilkan di peta.', 'success'); } // ================================================================ // Delete a single proof photo from a public report // ================================================================ async function deleteReportPhoto(reportId, filename, thumbWrap) { if (!confirm('Hapus foto bukti ini secara permanen?')) return; if (thumbWrap) thumbWrap.style.opacity = '0.4'; const r = await PhotoUpload.deletePhoto('report', reportId, filename); if (r.ok && r.data?.success) { thumbWrap?.remove(); showToast('Foto bukti dihapus.', 'success'); } else { if (thumbWrap) thumbWrap.style.opacity = '1'; showToast(r.data?.message || 'Gagal menghapus foto.', 'error'); } } // ================================================================ // Expose to global scope // ================================================================ window.loadPendingReports = loadPendingReports; window.openApproveModal = openApproveModal; window.openRejectModal = openRejectModal; window.deletePublicReport = deletePublicReport; window.flyToPublicReport = flyToPublicReport; window.deleteReportPhoto = deleteReportPhoto;