Files

210 lines
6.2 KiB
TypeScript

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<ExcelJS.Workbook> {
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<void> {
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);
}