adjust excel report format for more cleaner report rather than dumping raw data
This commit is contained in:
1 parent
41905078df
commit
5b74e0842c
2 files changed
+384
-83
No files matched your search
@@ -324,7 +324,9 @@ class Command(BaseCommand):
|
|||||||
mort_today = MORTALITY[i]
|
mort_today = MORTALITY[i]
|
||||||
mort_total_after = mort_total + mort_today
|
mort_total_after = mort_total + mort_today
|
||||||
life = DOC_IN_COUNT - mort_total_after - harvest_total
|
life = DOC_IN_COUNT - mort_total_after - harvest_total
|
||||||
life_pct = life / DOC_IN_COUNT * 100.0 if DOC_IN_COUNT else 0.0
|
# Liveability for EEF: birds still accounted for (DOC minus mortality),
|
||||||
|
# not remaining in-house stock (which drops during harvest).
|
||||||
|
life_pct = (DOC_IN_COUNT - mort_total_after) / DOC_IN_COUNT * 100.0 if DOC_IN_COUNT else 0.0
|
||||||
# Running averages: flock weight before the first harvest, then the
|
# Running averages: flock weight before the first harvest, then the
|
||||||
# harvest-weighted running mean carried forward on idle days.
|
# harvest-weighted running mean carried forward on idle days.
|
||||||
if harvest_today:
|
if harvest_today:
|
||||||
|
|||||||
+381
-82
@@ -1,108 +1,407 @@
|
|||||||
import ExcelJS from 'exceljs';
|
import ExcelJS from 'exceljs';
|
||||||
import type { DashboardReportPayload } from '../types/dashboardReport.ts';
|
import type { DashboardReportPayload } from '../types/dashboardReport.ts';
|
||||||
|
import {
|
||||||
|
countingTotals,
|
||||||
|
feedInTotals,
|
||||||
|
feedOutTotals,
|
||||||
|
feedUseTotals,
|
||||||
|
} from './reportTotals.ts';
|
||||||
|
import { formatDateId, formatDateRange, formatNumber, formatRatio } from './format.ts';
|
||||||
|
|
||||||
const na = (value: number | null | undefined): number | string =>
|
const COLORS = {
|
||||||
value == null || !Number.isFinite(value) ? 'N/A' : value;
|
headerFill: 'FFF8FAFC',
|
||||||
|
headerFont: 'FF475569',
|
||||||
|
accentFill: 'FF6D28D0',
|
||||||
|
accentFont: 'FFFFFFFF',
|
||||||
|
totalFill: 'FFF1F5F9',
|
||||||
|
zebraFill: 'FFFAFAFA',
|
||||||
|
border: 'FFE2E8F0',
|
||||||
|
titleFont: 'FF111827',
|
||||||
|
metaFont: 'FF374151',
|
||||||
|
};
|
||||||
|
|
||||||
|
const thinBorder: Partial<ExcelJS.Borders> = {
|
||||||
|
top: { style: 'thin', color: { argb: COLORS.border } },
|
||||||
|
left: { style: 'thin', color: { argb: COLORS.border } },
|
||||||
|
bottom: { style: 'thin', color: { argb: COLORS.border } },
|
||||||
|
right: { style: 'thin', color: { argb: COLORS.border } },
|
||||||
|
};
|
||||||
|
|
||||||
|
type CellValue = string | number | null;
|
||||||
|
|
||||||
|
const isPresent = (value: number | null | undefined): value is number =>
|
||||||
|
typeof value === 'number' && Number.isFinite(value);
|
||||||
|
|
||||||
|
const dash = (value: number | null | undefined): CellValue =>
|
||||||
|
isPresent(value) ? value : '-';
|
||||||
|
|
||||||
|
const na = (value: number | null | undefined): CellValue =>
|
||||||
|
isPresent(value) ? value : 'N/A';
|
||||||
|
|
||||||
|
const pct = (value: number | null | undefined): CellValue =>
|
||||||
|
isPresent(value) ? `${value.toFixed(1)}%` : 'N/A';
|
||||||
|
|
||||||
|
const qty = (value: number | null | undefined, maxDecimals = 0, suffix = ''): CellValue => {
|
||||||
|
if (!isPresent(value)) return 'N/A';
|
||||||
|
return `${formatNumber(value, maxDecimals, 0, true)}${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ratio = (value: number | null | undefined): string =>
|
||||||
|
isPresent(value) ? formatRatio(value, 3) : 'N/A';
|
||||||
|
|
||||||
|
const harvestKg = (grams: number | null | undefined): CellValue => {
|
||||||
|
if (!isPresent(grams)) return 'N/A';
|
||||||
|
return `${formatNumber(grams / 1000, 1)} kg`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const latestValidRatio = (
|
||||||
|
rows: Array<{ iotFcr?: number | null; iotEef?: number | null }>,
|
||||||
|
key: 'iotFcr' | 'iotEef'
|
||||||
|
): number | null => {
|
||||||
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
||||||
|
const value = rows[i]?.[key];
|
||||||
|
if (isPresent(value)) return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function styleHeaderRow(row: ExcelJS.Row, colCount: number): void {
|
||||||
|
row.height = 22;
|
||||||
|
for (let c = 1; c <= colCount; c += 1) {
|
||||||
|
const cell = row.getCell(c);
|
||||||
|
cell.font = { bold: true, color: { argb: COLORS.headerFont }, size: 11 };
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS.headerFill } };
|
||||||
|
cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true };
|
||||||
|
cell.border = thinBorder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleDataRow(row: ExcelJS.Row, colCount: number, stripe: boolean): void {
|
||||||
|
for (let c = 1; c <= colCount; c += 1) {
|
||||||
|
const cell = row.getCell(c);
|
||||||
|
cell.font = { size: 11, color: { argb: COLORS.titleFont } };
|
||||||
|
cell.alignment = {
|
||||||
|
vertical: 'middle',
|
||||||
|
horizontal: c === 1 ? 'left' : 'right',
|
||||||
|
};
|
||||||
|
if (stripe) {
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS.zebraFill } };
|
||||||
|
}
|
||||||
|
cell.border = thinBorder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function styleTotalRow(row: ExcelJS.Row, colCount: number): void {
|
||||||
|
row.height = 22;
|
||||||
|
for (let c = 1; c <= colCount; c += 1) {
|
||||||
|
const cell = row.getCell(c);
|
||||||
|
cell.font = { bold: true, size: 11, color: { argb: COLORS.titleFont } };
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS.totalFill } };
|
||||||
|
cell.alignment = {
|
||||||
|
vertical: 'middle',
|
||||||
|
horizontal: c === 1 ? 'left' : 'right',
|
||||||
|
};
|
||||||
|
cell.border = {
|
||||||
|
...thinBorder,
|
||||||
|
top: { style: 'medium', color: { argb: COLORS.border } },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoFitColumns(sheet: ExcelJS.Worksheet, min = 10, max = 42): void {
|
||||||
|
sheet.columns.forEach((column) => {
|
||||||
|
let maxLen = min;
|
||||||
|
column.eachCell?.({ includeEmpty: false }, (cell) => {
|
||||||
|
const text =
|
||||||
|
cell.value instanceof Date
|
||||||
|
? cell.value.toLocaleDateString('id-ID')
|
||||||
|
: String(cell.value ?? '');
|
||||||
|
maxLen = Math.max(maxLen, text.length);
|
||||||
|
});
|
||||||
|
column.width = Math.min(max, Math.max(min, maxLen + 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSheetTitle(sheet: ExcelJS.Worksheet, title: string, colCount: number): void {
|
||||||
|
sheet.mergeCells(1, 1, 1, colCount);
|
||||||
|
const cell = sheet.getCell(1, 1);
|
||||||
|
cell.value = title;
|
||||||
|
cell.font = { bold: true, size: 13, color: { argb: COLORS.titleFont } };
|
||||||
|
cell.alignment = { vertical: 'middle', horizontal: 'left' };
|
||||||
|
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: COLORS.headerFill } };
|
||||||
|
cell.border = {
|
||||||
|
...thinBorder,
|
||||||
|
bottom: { style: 'medium', color: { argb: COLORS.accentFill } },
|
||||||
|
};
|
||||||
|
sheet.getRow(1).height = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTable(
|
||||||
|
sheet: ExcelJS.Worksheet,
|
||||||
|
title: string,
|
||||||
|
headers: string[],
|
||||||
|
rows: CellValue[][],
|
||||||
|
totalRow?: CellValue[] | null
|
||||||
|
): void {
|
||||||
|
const colCount = headers.length;
|
||||||
|
addSheetTitle(sheet, title, colCount);
|
||||||
|
|
||||||
|
const headerRow = sheet.addRow(headers);
|
||||||
|
styleHeaderRow(headerRow, colCount);
|
||||||
|
|
||||||
|
rows.forEach((values, index) => {
|
||||||
|
const row = sheet.addRow(values);
|
||||||
|
styleDataRow(row, colCount, index % 2 === 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (totalRow) {
|
||||||
|
const row = sheet.addRow(totalRow);
|
||||||
|
styleTotalRow(row, colCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.views = [{ state: 'frozen', ySplit: 2, activeCell: 'A3' }];
|
||||||
|
autoFitColumns(sheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSummarySheet(workbook: ExcelJS.Workbook, payload: DashboardReportPayload): void {
|
||||||
|
const isEndCycle = payload.reportType === 'end_cycle';
|
||||||
|
const title = isEndCycle ? 'Laporan Akhir Siklus' : 'Laporan Harian';
|
||||||
|
const lastDate = payload.counting[payload.counting.length - 1]?.date ?? payload.cycleStart;
|
||||||
|
const printedDate = new Date(payload.generatedAt).toLocaleDateString('id-ID', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheet = workbook.addWorksheet('Ringkasan');
|
||||||
|
sheet.getColumn(1).width = 22;
|
||||||
|
sheet.getColumn(2).width = 48;
|
||||||
|
|
||||||
|
const addMeta = (label: string, value: string | number, bold = false): void => {
|
||||||
|
const row = sheet.addRow([label, value]);
|
||||||
|
row.getCell(1).font = { bold: true, size: 11, color: { argb: COLORS.metaFont } };
|
||||||
|
row.getCell(2).font = {
|
||||||
|
bold,
|
||||||
|
size: bold ? 14 : 11,
|
||||||
|
color: { argb: COLORS.titleFont },
|
||||||
|
};
|
||||||
|
row.height = bold ? 24 : 20;
|
||||||
|
};
|
||||||
|
|
||||||
|
sheet.getCell('A1').value = 'Dashboard CPSP';
|
||||||
|
sheet.getCell('A1').font = { bold: true, size: 10, color: { argb: COLORS.accentFill } };
|
||||||
|
sheet.addRow([]);
|
||||||
|
|
||||||
|
addMeta('Judul', title, true);
|
||||||
|
addMeta('Lokasi', `${payload.area} — ${payload.kandangName}`);
|
||||||
|
addMeta(
|
||||||
|
'Periode',
|
||||||
|
`Hari ke-0 s/d ke-${payload.upToDay} (${formatDateId(payload.cycleStart)} – ${formatDateId(lastDate)})`
|
||||||
|
);
|
||||||
|
addMeta('Siklus', formatDateRange(payload.cycleStart, payload.cycleEnd));
|
||||||
|
addMeta('Dicetak', printedDate);
|
||||||
|
|
||||||
|
if (isEndCycle) {
|
||||||
|
const finalFcr = latestValidRatio(payload.fcr, 'iotFcr');
|
||||||
|
const finalEef = latestValidRatio(payload.eef, 'iotEef');
|
||||||
|
sheet.addRow([]);
|
||||||
|
addMeta('FCR akhir', ratio(finalFcr), true);
|
||||||
|
addMeta('EEF akhir', ratio(finalEef), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
sheet.addRow([]);
|
||||||
|
const navHeader = sheet.addRow(['Sheet', 'Isi']);
|
||||||
|
styleHeaderRow(navHeader, 2);
|
||||||
|
|
||||||
|
const navRows: Array<[string, string]> = [
|
||||||
|
['Hitung Ayam', 'Stok, mortalitas, panen, dan akurasi hitung harian'],
|
||||||
|
['Berat Ayam', 'Rata-rata berat manual vs IoT dan akurasi'],
|
||||||
|
['Karung Masuk', 'Pakan masuk manual vs IoT'],
|
||||||
|
['Karung Dituang', 'Pakan dituang manual vs IoT'],
|
||||||
|
['Karung Keluar', 'Pakan keluar manual vs IoT'],
|
||||||
|
];
|
||||||
|
if (isEndCycle) {
|
||||||
|
navRows.push(['FCR', 'Data pendukung FCR per umur']);
|
||||||
|
navRows.push(['EEF', 'Data pendukung EEF per umur']);
|
||||||
|
}
|
||||||
|
|
||||||
|
navRows.forEach(([name, desc], index) => {
|
||||||
|
const row = sheet.addRow([name, desc]);
|
||||||
|
styleDataRow(row, 2, index % 2 === 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadReportExcel(payload: DashboardReportPayload): Promise<void> {
|
export async function downloadReportExcel(payload: DashboardReportPayload): Promise<void> {
|
||||||
const workbook = new ExcelJS.Workbook();
|
const workbook = new ExcelJS.Workbook();
|
||||||
workbook.creator = 'Dashboard CPSP';
|
workbook.creator = 'Dashboard CPSP';
|
||||||
|
workbook.created = new Date();
|
||||||
|
|
||||||
const counting = workbook.addWorksheet('Chicken Counting');
|
addSummarySheet(workbook, payload);
|
||||||
counting.addRow([
|
|
||||||
'Tanggal',
|
const countingTotal = countingTotals(payload.counting);
|
||||||
'Hari',
|
addTable(
|
||||||
'Stok awal',
|
workbook.addWorksheet('Hitung Ayam'),
|
||||||
'Mortalitas',
|
'Hitung Ayam harian',
|
||||||
'Panen',
|
[
|
||||||
'Stok akhir',
|
'Tanggal',
|
||||||
'IoT',
|
'Umur (hari)',
|
||||||
'Akurasi %',
|
'Stok awal',
|
||||||
]);
|
'Mortalitas',
|
||||||
for (const row of payload.counting) {
|
'Panen',
|
||||||
counting.addRow([
|
'Berat panen',
|
||||||
row.date,
|
'Stok akhir',
|
||||||
|
'IoT',
|
||||||
|
'Akurasi %',
|
||||||
|
],
|
||||||
|
payload.counting.map((row) => [
|
||||||
|
formatDateId(row.date),
|
||||||
row.day,
|
row.day,
|
||||||
na(row.countBefore),
|
na(row.countBefore),
|
||||||
na(row.mortality),
|
na(row.mortality),
|
||||||
na(row.harvest),
|
na(row.harvest),
|
||||||
|
na(row.harvestWeight),
|
||||||
na(row.countAfter),
|
na(row.countAfter),
|
||||||
na(row.iotCount),
|
na(row.iotCount),
|
||||||
na(row.accuracy),
|
pct(row.accuracy),
|
||||||
]);
|
]),
|
||||||
}
|
countingTotal
|
||||||
|
? [
|
||||||
|
countingTotal.label,
|
||||||
|
'—',
|
||||||
|
dash(countingTotal.countBefore),
|
||||||
|
na(countingTotal.mortality),
|
||||||
|
na(countingTotal.harvest),
|
||||||
|
na(countingTotal.harvestWeight),
|
||||||
|
dash(countingTotal.countAfter),
|
||||||
|
dash(countingTotal.iotCount),
|
||||||
|
countingTotal.accuracy == null ? '-' : pct(countingTotal.accuracy),
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const weight = workbook.addWorksheet('Weight');
|
addTable(
|
||||||
weight.addRow([
|
workbook.addWorksheet('Berat Ayam'),
|
||||||
'Tanggal',
|
'Berat Ayam harian',
|
||||||
'Hari',
|
[
|
||||||
'AVG manual',
|
'Tanggal',
|
||||||
'AVG IoT',
|
'Umur (hari)',
|
||||||
'Keseragaman',
|
'Rata-rata manual',
|
||||||
'Selisih',
|
'Rata-rata IoT',
|
||||||
'Akurasi %',
|
'Keseragaman',
|
||||||
]);
|
'Selisih',
|
||||||
for (const row of payload.weight) {
|
'Akurasi %',
|
||||||
weight.addRow([
|
],
|
||||||
row.date,
|
payload.weight.map((row) => [
|
||||||
|
formatDateId(row.date),
|
||||||
row.day,
|
row.day,
|
||||||
na(row.avgManual),
|
na(row.avgManual),
|
||||||
na(row.avgIot),
|
na(row.avgIot),
|
||||||
na(row.uniformity),
|
pct(row.uniformity),
|
||||||
na(row.difference),
|
na(row.difference),
|
||||||
na(row.accuracy),
|
pct(row.accuracy),
|
||||||
]);
|
])
|
||||||
}
|
);
|
||||||
|
|
||||||
const feedInSheet = workbook.addWorksheet('Feed In');
|
const feedInTotal = feedInTotals(payload.feedIn);
|
||||||
feedInSheet.addRow(['Tanggal', 'Manual in', 'IoT in', 'Akurasi in %']);
|
addTable(
|
||||||
for (const row of payload.feedIn) {
|
workbook.addWorksheet('Karung Masuk'),
|
||||||
feedInSheet.addRow([row.date, na(row.manualIn), na(row.iotIn), na(row.accuracy)]);
|
'Karung masuk',
|
||||||
}
|
['Tanggal', 'Manual masuk', 'IoT masuk', 'Akurasi %'],
|
||||||
|
payload.feedIn.map((row) => [
|
||||||
|
formatDateId(row.date),
|
||||||
|
na(row.manualIn),
|
||||||
|
na(row.iotIn),
|
||||||
|
pct(row.accuracy),
|
||||||
|
]),
|
||||||
|
feedInTotal
|
||||||
|
? [
|
||||||
|
feedInTotal.label,
|
||||||
|
na(feedInTotal.manualIn),
|
||||||
|
na(feedInTotal.iotIn),
|
||||||
|
pct(feedInTotal.accuracy),
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const feedUseSheet = workbook.addWorksheet('Feed Use');
|
const feedUseTotal = feedUseTotals(payload.feedUse);
|
||||||
feedUseSheet.addRow(['Tanggal', 'Hari', 'Manual use', 'IoT use', 'Akurasi use %']);
|
addTable(
|
||||||
for (const row of payload.feedUse) {
|
workbook.addWorksheet('Karung Dituang'),
|
||||||
feedUseSheet.addRow([row.date, row.day, na(row.manualUse), na(row.iotUse), na(row.accuracy)]);
|
'Karung dituang',
|
||||||
}
|
['Tanggal', 'Umur (hari)', 'Manual dituang', 'IoT dituang', 'Akurasi %'],
|
||||||
|
payload.feedUse.map((row) => [
|
||||||
const feedOutSheet = workbook.addWorksheet('Feed Out');
|
formatDateId(row.date),
|
||||||
feedOutSheet.addRow(['Tanggal', 'Manual out', 'IoT out', 'Akurasi out %']);
|
|
||||||
for (const row of payload.feedOut) {
|
|
||||||
feedOutSheet.addRow([row.date, na(row.manualOut), na(row.iotOut), na(row.accuracy)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fcrSheet = workbook.addWorksheet('FCR');
|
|
||||||
fcrSheet.addRow(['Hari', 'FCR', 'Total Pakan', 'Bobot IoT', 'Ayam Hidup', 'Total Bobot Panen']);
|
|
||||||
for (const row of payload.fcr) {
|
|
||||||
fcrSheet.addRow([
|
|
||||||
row.day,
|
row.day,
|
||||||
na(row.iotFcr),
|
na(row.manualUse),
|
||||||
na(row.feedTotal),
|
na(row.iotUse),
|
||||||
na(row.iotWeight),
|
pct(row.accuracy),
|
||||||
na(row.chickenLife),
|
]),
|
||||||
na(row.harvestWeightTotal),
|
feedUseTotal
|
||||||
]);
|
? [
|
||||||
}
|
feedUseTotal.label,
|
||||||
|
'—',
|
||||||
|
na(feedUseTotal.manualUse),
|
||||||
|
na(feedUseTotal.iotUse),
|
||||||
|
pct(feedUseTotal.accuracy),
|
||||||
|
]
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
const eefSheet = workbook.addWorksheet('EEF');
|
const feedOutTotal = feedOutTotals(payload.feedOut);
|
||||||
eefSheet.addRow([
|
addTable(
|
||||||
'Hari',
|
workbook.addWorksheet('Karung Keluar'),
|
||||||
'EEF',
|
'Karung keluar',
|
||||||
'Persentase Ayam Hidup',
|
['Tanggal', 'Manual keluar', 'IoT keluar', 'Akurasi %'],
|
||||||
'BW Rata-rata Panen',
|
payload.feedOut.map((row) => [
|
||||||
'Umur Rata-rata Panen',
|
formatDateId(row.date),
|
||||||
'FCR',
|
na(row.manualOut),
|
||||||
]);
|
na(row.iotOut),
|
||||||
for (const row of payload.eef) {
|
pct(row.accuracy),
|
||||||
eefSheet.addRow([
|
]),
|
||||||
row.day,
|
feedOutTotal
|
||||||
na(row.iotEef),
|
? [
|
||||||
na(row.chickenLifePercentage),
|
feedOutTotal.label,
|
||||||
na(row.iotAverageHarvestBw),
|
na(feedOutTotal.manualOut),
|
||||||
na(row.iotAverageDayWeight),
|
na(feedOutTotal.iotOut),
|
||||||
na(row.iotFcr),
|
pct(feedOutTotal.accuracy),
|
||||||
]);
|
]
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (payload.reportType === 'end_cycle') {
|
||||||
|
addTable(
|
||||||
|
workbook.addWorksheet('FCR'),
|
||||||
|
'Data Pendukung FCR',
|
||||||
|
['Umur', 'Total Pakan', 'Bobot IoT', 'Ayam Hidup', 'Total Bobot Panen', 'FCR'],
|
||||||
|
payload.fcr.map((row) => [
|
||||||
|
`Hari ${row.day}`,
|
||||||
|
qty(row.feedTotal, 0, ' karung'),
|
||||||
|
qty(row.iotWeight, 2, row.iotWeight != null ? ' g' : ''),
|
||||||
|
qty(row.chickenLife, 0, ' ekor'),
|
||||||
|
harvestKg(row.harvestWeightTotal),
|
||||||
|
ratio(row.iotFcr),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
addTable(
|
||||||
|
workbook.addWorksheet('EEF'),
|
||||||
|
'Data Pendukung EEF',
|
||||||
|
[
|
||||||
|
'Umur',
|
||||||
|
'Persentase Ayam Hidup',
|
||||||
|
'BW Rata-rata Panen',
|
||||||
|
'Umur Rata-rata Panen',
|
||||||
|
'FCR',
|
||||||
|
'EEF',
|
||||||
|
],
|
||||||
|
payload.eef.map((row) => [
|
||||||
|
`Hari ${row.day}`,
|
||||||
|
qty(row.chickenLifePercentage, 3, '%'),
|
||||||
|
qty(row.iotAverageHarvestBw, 2, row.iotAverageHarvestBw != null ? ' g' : ''),
|
||||||
|
qty(row.iotAverageDayWeight, 2),
|
||||||
|
ratio(row.iotFcr),
|
||||||
|
ratio(row.iotEef),
|
||||||
|
])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
|||||||
Reference in new issue
Block a user