import ExcelJS from 'exceljs'; import type { DashboardReportPayload } from '../types/dashboardReport.ts'; import { buildPdfFilename, isoDataDates } from '../components/shared/PdfReportHeader.tsx'; import { countingTotals, feedInTotals, feedUseTotals } from './reportTotals.ts'; const dayRangeLabel = (upToDay: number): string => `Hari ke-0 s/d ke-${upToDay}`; const accuracyText = (value: number | null | undefined): string => value == null || !Number.isFinite(value) ? 'N/A' : `${value.toFixed(1)}%`; const numOrNa = (value: number | null | undefined): number | string => value == null || !Number.isFinite(value) ? 'N/A' : value; function writeMetaRow(sheet: ExcelJS.Worksheet, snapshot: DashboardReportPayload): void { sheet.addRow([ `Kandang: ${snapshot.kandangName}`, `Siklus: ${snapshot.cycleId}`, `Periode: ${dayRangeLabel(snapshot.upToDay)}`, `Dibuat: ${snapshot.generatedAt}`, ]); } export async function buildReportWorkbook( snapshot: DashboardReportPayload ): Promise { const workbook = new ExcelJS.Workbook(); workbook.creator = 'Dashboard CPSP'; workbook.created = snapshot.generatedAt ? new Date(snapshot.generatedAt) : new Date(); const counting = workbook.addWorksheet('Counting'); writeMetaRow(counting, snapshot); counting.addRow([ 'Tanggal', 'Umur (hari)', 'Stok awal', 'Mortalitas', 'Panen', 'Berat panen', 'Stok akhir', 'IoT', 'Akurasi %', ]); for (const row of snapshot.counting || []) { counting.addRow([ row.date, row.day, numOrNa(row.countBefore), numOrNa(row.mortality), numOrNa(row.harvest), numOrNa(row.harvestWeight), numOrNa(row.countAfter), numOrNa(row.iotCount), accuracyText(row.accuracy), ]); } const countingTotal = countingTotals(snapshot.counting || []); if (countingTotal) { counting.addRow([ countingTotal.label, '—', numOrNa(countingTotal.countBefore), numOrNa(countingTotal.mortality), numOrNa(countingTotal.harvest), numOrNa(countingTotal.harvestWeight), numOrNa(countingTotal.countAfter), numOrNa(countingTotal.iotCount), accuracyText(countingTotal.accuracy), ]); } const weight = workbook.addWorksheet('Weight'); writeMetaRow(weight, snapshot); weight.addRow([ 'Tanggal', 'Umur (hari)', 'Rata-rata manual', 'Rata-rata IoT', 'Keseragaman', 'Selisih', 'Akurasi %', ]); for (const row of snapshot.weight || []) { weight.addRow([ row.date, row.day, numOrNa(row.avgManual), numOrNa(row.avgIot), accuracyText(row.uniformity), numOrNa(row.difference), accuracyText(row.accuracy), ]); } const feedIn = workbook.addWorksheet('Feed In'); writeMetaRow(feedIn, snapshot); feedIn.addRow(['Tanggal', 'Manual masuk', 'IoT masuk', 'Akurasi %']); for (const row of snapshot.feedIn || []) { feedIn.addRow([row.date, numOrNa(row.manualIn), numOrNa(row.iotIn), accuracyText(row.accuracy)]); } const feedInTotal = feedInTotals(snapshot.feedIn || []); if (feedInTotal) { feedIn.addRow([ feedInTotal.label, numOrNa(feedInTotal.manualIn), numOrNa(feedInTotal.iotIn), accuracyText(feedInTotal.accuracy), ]); } const feedUse = workbook.addWorksheet('Feed Use'); writeMetaRow(feedUse, snapshot); feedUse.addRow(['Tanggal', 'Umur (hari)', 'Manual dituang', 'IoT dituang', 'Akurasi %']); for (const row of snapshot.feedUse || []) { feedUse.addRow([ row.date, row.day, numOrNa(row.manualUse), numOrNa(row.iotUse), accuracyText(row.accuracy), ]); } const feedUseTotal = feedUseTotals(snapshot.feedUse || []); if (feedUseTotal) { feedUse.addRow([ feedUseTotal.label, '—', numOrNa(feedUseTotal.manualUse), numOrNa(feedUseTotal.iotUse), accuracyText(feedUseTotal.accuracy), ]); } if (snapshot.reportType === 'end_cycle') { const harvestKg = (grams: number | null | undefined): number | string => grams == null || !Number.isFinite(grams) ? 'N/A' : grams / 1000; const fcr = workbook.addWorksheet('FCR'); writeMetaRow(fcr, snapshot); fcr.addRow(['Umur', 'Total Pakan', 'Bobot IoT', 'Ayam Hidup', 'Total Bobot Panen', 'FCR']); for (const row of snapshot.fcr || []) { fcr.addRow([ row.day, numOrNa(row.feedTotal), numOrNa(row.iotWeight), numOrNa(row.chickenLife), harvestKg(row.harvestWeightTotal), numOrNa(row.iotFcr), ]); } const eef = workbook.addWorksheet('EEF'); writeMetaRow(eef, snapshot); eef.addRow([ 'Umur', 'Persentase Ayam Hidup', 'BW Rata-rata Panen', 'Umur Rata-rata Panen', 'FCR', 'EEF', ]); for (const row of snapshot.eef || []) { eef.addRow([ row.day, numOrNa(row.chickenLifePercentage), numOrNa(row.iotAverageHarvestBw), numOrNa(row.iotAverageDayWeight), numOrNa(row.iotFcr), numOrNa(row.iotEef), ]); } } return workbook; } export function reportExcelFilename( snapshot: DashboardReportPayload, cycleStartDate?: Date | string | null ): string { const isEndCycle = snapshot.reportType === 'end_cycle'; const pdfName = buildPdfFilename({ prefix: 'Laporan', reportName: isEndCycle ? 'Akhir-Siklus' : 'Harian', area: snapshot.area, kandangLabel: snapshot.kandangName, periodSlug: isEndCycle ? 'Akhir-Siklus' : `Harian-Hari-${snapshot.upToDay}`, isoDates: isoDataDates(cycleStartDate, 0, snapshot.upToDay), }); return pdfName.replace(/\.pdf$/i, '.xlsx'); } export async function downloadReportExcel( snapshot: DashboardReportPayload, cycleStartDate?: Date | string | null ): Promise { const workbook = await buildReportWorkbook(snapshot); const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = reportExcelFilename(snapshot, cycleStartDate); document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); }