350 lines
15 KiB
JavaScript
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, '&')
|
|
.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 = `
|
|
<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; |