Files

350 lines
15 KiB
JavaScript

/* ============================================================
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 = '<tr><td colspan="6" class="text-center" style="color:#9ba4b5;padding:18px;">Memuat...</td></tr>';
const r = await ApiPublicReports.list(params);
if (!r.ok || !r.data?.success) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center" style="color:var(--danger);padding:18px;">Gagal memuat laporan publik.</td></tr>';
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 = `<tr><td colspan="6" class="text-center" style="color:#9ba4b5;padding:24px;">
<i class="fas fa-check-circle" style="font-size:20px;display:block;margin-bottom:8px;opacity:0.3;"></i>
${emptyMsg}
</td></tr>`;
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 `
<tr style="vertical-align:top;">
<td style="font-size:9.5px;color:#9ba4b5;white-space:nowrap;padding-top:10px;">${dateStr}</td>
<td style="padding-top:8px;">
<div style="font-size:12px;font-weight:700;color:#0f1623;">${escapeHtml(rep.head_name || '—')}</div>
${rep.reporter_name
? `<div style="font-size:10px;color:#9ba4b5;margin-top:2px;"><i class="fas fa-user" style="font-size:9px;"></i> ${escapeHtml(rep.reporter_name)}${rep.reporter_phone ? ' · ' + escapeHtml(rep.reporter_phone) : ''}</div>`
: ''}
</td>
<td style="font-size:10.5px;color:#5a6478;max-width:130px;padding-top:10px;">${escapeHtml(truncate(rep.address || '—', 38))}</td>
<td style="font-size:10.5px;color:#5a6478;max-width:160px;padding-top:10px;">
${escapeHtml(truncate(rep.description || '—', 60))}
${buildPhotoMiniStrip(rep.proof_photos, 'uploads/reports/')}
</td>
<td style="padding-top:10px;">
<span style="padding:3px 9px;border-radius:20px;font-size:9.5px;font-weight:700;
background:${st.bg};color:${st.color};white-space:nowrap;">${st.label}</span>
${rep.admin_notes
? `<div style="font-size:9px;color:#9ba4b5;margin-top:3px;">${escapeHtml(truncate(rep.admin_notes, 25))}</div>`
: ''}
${rep.converted_household_id
? `<div style="font-size:9px;color:#0b9e73;margin-top:2px;"><i class="fas fa-home"></i> ID: ${rep.converted_household_id}</div>`
: ''}
</td>
<td style="white-space:nowrap;padding-top:8px;">
${canAct ? `
<button class="action-btn" onclick="openApproveModal(${rep.id}, ${safeJson(rep)})"
style="color:#0b9e73;border-color:#a8e8d4;" title="Setujui & tambah ke peta">
<i class="fas fa-check"></i>
</button>
<button class="action-btn" onclick="openRejectModal(${rep.id})"
style="color:var(--danger);border-color:#fcc;" title="Tolak laporan">
<i class="fas fa-times"></i>
</button>
` : ''}
<button class="action-btn" onclick="flyToPublicReport(${rep.latitude}, ${rep.longitude})"
title="Lihat di peta" style="color:var(--accent);border-color:#c8d0f5;">
<i class="fas fa-map-marker-alt"></i>
</button>
<button class="action-btn danger" onclick="deletePublicReport(${rep.id})" title="Hapus permanen">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>`;
}).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 `<img src="${src}"
style="width:36px;height:36px;object-fit:cover;border-radius:5px;border:1.5px solid #e2e6ef;cursor:zoom-in;margin-right:3px;"
onclick="PhotoUpload.lightbox('${src}')"
title="${escapeHtml(name)}">`;
}).join('');
const extra = arr.length > 3 ? `<span style="font-size:10px;color:#9ba4b5;">+${arr.length - 3}</span>` : '';
return `<div style="display:flex;align-items:center;flex-wrap:wrap;gap:3px;margin-top:5px;">${imgs}${extra}</div>`;
}
/** Safely JSON-encode report object for inline onclick attribute */
function safeJson(obj) {
return JSON.stringify(obj)
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// ================================================================
// 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 = `
<div style="display:flex;align-items:flex-start;gap:10px;">
<div style="width:32px;height:32px;border-radius:8px;background:#fff0f0;display:flex;align-items:center;justify-content:center;flex-shrink:0;">
<i class="fas fa-flag" style="color:var(--danger);font-size:14px;"></i>
</div>
<div style="flex:1;min-width:0;">
<div style="font-size:13px;font-weight:700;color:#0f1623;">${data.head_name || '—'}</div>
<div style="font-size:11px;color:#5a6478;margin-top:3px;">${data.address || '—'}</div>
<div style="font-size:11px;color:#5a6478;margin-top:5px;font-style:italic;border-left:2px solid #e2e6ef;padding-left:8px;">"${truncate(data.description || '', 100)}"</div>
${data.reporter_name ? `<div style="font-size:10.5px;color:#9ba4b5;margin-top:4px;"><i class="fas fa-user" style="font-size:9px;"></i> ${data.reporter_name}</div>` : ''}
</div>
</div>`;
// ── 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 = '<div style="font-size:11px;color:#9ba4b5;padding:8px 0;"><i class="fas fa-image"></i> Tidak ada foto bukti</div>';
}
}
}
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;