feat: Plan 5 — Ekspor PDF + Ekspor per Radius + Cetak Laporan
- Add ExportController: allMiskinPDF (all data) + radiusMiskinPDF
(server-side Haversine filtering per ibadah radius)
- Add two table-based PDF Blade views (dompdf-compatible, no flexbox/grid)
- Add export routes (/export/pdf, /export/pdf/radius/{id}/{radius})
with numeric route constraints
- Add "Ekspor PDF" button to data tabular view
- Add CSV + PDF radius export buttons to map analysis panel;
exportRadiusCSV() reuses existing isInside(); PDF href synced on
ibadah click and radius slider change
- 5 new tests: access control, PDF content-type, 404 + param validation
- Mark K-04 and Section 5 (Cetak & Ekspor) complete in missing_features.md
- Update README with export features
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -191,6 +191,14 @@
|
||||
<div class="text-[8px] uppercase tracking-wider font-bold opacity-50">Jiwa</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-base-200 pt-3 mt-3 flex flex-col gap-2">
|
||||
<button onclick="exportRadiusCSV()" class="btn btn-xs btn-outline gap-1 w-full">
|
||||
<i class="fa-solid fa-file-csv text-success"></i> Export CSV Radius
|
||||
</button>
|
||||
<a id="btnExportRadiusPDF" href="#" target="_blank" class="btn btn-xs btn-error btn-outline gap-1 w-full">
|
||||
<i class="fa-solid fa-file-pdf"></i> Export PDF Radius
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,6 +674,10 @@
|
||||
|
||||
const radius = document.getElementById('radiusSlider').value = item.radius || 500;
|
||||
updateRadius(item.latitude, item.longitude, radius);
|
||||
|
||||
// Update PDF export link
|
||||
const pdfBtn = document.getElementById('btnExportRadiusPDF');
|
||||
if (pdfBtn) pdfBtn.href = `{{ url('/export/pdf/radius') }}/${item.id}/${item.radius || 500}`;
|
||||
}
|
||||
|
||||
// ── RADIUS ANALYSIS ────────────────────────────────────────────────
|
||||
@@ -711,6 +723,26 @@
|
||||
document.getElementById('statJiwa').innerText = jiwa;
|
||||
}
|
||||
|
||||
function exportRadiusCSV() {
|
||||
if (!activeIbadah) return;
|
||||
const radius = parseInt(document.getElementById('radiusSlider').value);
|
||||
const inRadius = miskinData.filter(m =>
|
||||
isInside(m, { lat: activeIbadah.latitude, lng: activeIbadah.longitude }, radius)
|
||||
);
|
||||
|
||||
let csv = "data:text/csv;charset=utf-8,ID Rumah,Nama KK,Alamat,KK,Jiwa,Pekerjaan,Pendapatan,Kondisi\n";
|
||||
inRadius.forEach(m => {
|
||||
csv += `"${m.id_rumah}","${m.nama_kk || ''}","${(m.alamat || '').replace(/"/g, '""')}",${m.jumlah_kk},${m.jumlah_orang},"${m.pekerjaan || ''}",${m.pendapatan || 0},"${m.kondisi_rumah || ''}"\n`;
|
||||
});
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodeURI(csv));
|
||||
link.setAttribute('download', `radius-${activeIbadah.nama.replace(/\s+/g, '-')}-${radius}m.csv`);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function isInside(point, center, radius) {
|
||||
// center can be [lat, lng] or {lat, lng}
|
||||
const cLat = center.lat !== undefined ? center.lat : center[0];
|
||||
@@ -752,6 +784,11 @@
|
||||
// Update the local data copy so it persists in memory
|
||||
const localItem = ibadahData.find(i => i.id == activeIbadah.id);
|
||||
if (localItem) localItem.radius = newRadius;
|
||||
// Update PDF export link with new radius
|
||||
const pdfBtn = document.getElementById('btnExportRadiusPDF');
|
||||
if (pdfBtn && activeIbadah) {
|
||||
pdfBtn.href = `{{ url('/export/pdf/radius') }}/${activeIbadah.id}/${newRadius}`;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('Gagal menyimpan radius ke database', 'error');
|
||||
|
||||
Reference in New Issue
Block a user