/* ============================================================
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;