207 lines
5.6 KiB
TypeScript
207 lines
5.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { buildReportSummary, latestValidRatio } from '../reportSummary.ts';
|
|
import type { ReportSummaryMetric, ReportSummaryMetricKey } from '../reportSummary.ts';
|
|
import type { DashboardReportPayload } from '../../types/dashboardReport.ts';
|
|
|
|
function emptyPayload(overrides: Partial<DashboardReportPayload> = {}): DashboardReportPayload {
|
|
return {
|
|
cycleId: '1',
|
|
cycleStart: '2026-05-01',
|
|
cycleEnd: '2026-06-04',
|
|
kandangId: 9,
|
|
kandangName: 'Kandang 1',
|
|
area: 'Area A',
|
|
upToDay: 2,
|
|
generatedAt: '2026-05-03T00:00:00.000Z',
|
|
reportType: 'daily',
|
|
counting: [],
|
|
weight: [],
|
|
feedIn: [],
|
|
feedUse: [],
|
|
feedOut: [],
|
|
fcr: [],
|
|
eef: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('latestValidRatio', () => {
|
|
it('returns the last finite value walking backward', () => {
|
|
expect(
|
|
latestValidRatio(
|
|
[
|
|
{ iotFcr: 1.1 },
|
|
{ iotFcr: null },
|
|
{ iotFcr: 1.4 },
|
|
{ iotFcr: null },
|
|
],
|
|
'iotFcr'
|
|
)
|
|
).toBe(1.4);
|
|
});
|
|
|
|
it('returns null when no finite values exist', () => {
|
|
expect(latestValidRatio([{ iotEef: null }, { iotEef: null }], 'iotEef')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('buildReportSummary', () => {
|
|
it('returns 13 metrics in the agreed order with nulls when sections are empty', () => {
|
|
const summary = buildReportSummary(emptyPayload({ upToDay: 5 }));
|
|
expect(summary.metrics.map((m) => m.key)).toEqual([
|
|
'umur',
|
|
'populasi',
|
|
'mortalitas',
|
|
'panen',
|
|
'beratPanen',
|
|
'karungMasuk',
|
|
'karungDituang',
|
|
'beratPakan',
|
|
'karungKeluar',
|
|
'beratAvg',
|
|
'uniformity',
|
|
'fcr',
|
|
'eef',
|
|
]);
|
|
expect(summary.metrics[0]).toMatchObject({ label: 'Umur', value: 5, unit: 'hari' });
|
|
expect(summary.metrics.slice(1).every((m) => m.value === null)).toBe(true);
|
|
});
|
|
|
|
it('derives totals, latest populasi/weight, and FCR/EEF from payload rows', () => {
|
|
const summary = buildReportSummary(
|
|
emptyPayload({
|
|
upToDay: 2,
|
|
counting: [
|
|
{
|
|
date: '2026-05-01',
|
|
day: 0,
|
|
countBefore: 20000,
|
|
mortality: 10,
|
|
harvest: 0,
|
|
harvestWeight: 0,
|
|
countAfter: 19990,
|
|
iotCount: null,
|
|
accuracy: null,
|
|
},
|
|
{
|
|
date: '2026-05-03',
|
|
day: 2,
|
|
countBefore: 19980,
|
|
mortality: 5,
|
|
harvest: 100,
|
|
harvestWeight: 150000,
|
|
countAfter: 19875,
|
|
iotCount: 19870,
|
|
accuracy: 99.9,
|
|
},
|
|
],
|
|
weight: [
|
|
{
|
|
date: '2026-05-01',
|
|
day: 0,
|
|
avgManual: 45,
|
|
avgIot: 46,
|
|
uniformity: 80,
|
|
difference: 1,
|
|
accuracy: 97,
|
|
},
|
|
{
|
|
date: '2026-05-03',
|
|
day: 2,
|
|
avgManual: 900,
|
|
avgIot: 910,
|
|
uniformity: 88.5,
|
|
difference: 10,
|
|
accuracy: 98,
|
|
},
|
|
],
|
|
feedIn: [
|
|
{ date: '2026-05-01', manualIn: 20, iotIn: 21, accuracy: 95 },
|
|
{ date: '2026-05-03', manualIn: 10, iotIn: 10, accuracy: 100 },
|
|
],
|
|
feedUse: [
|
|
{ date: '2026-05-01', day: 0, manualUse: 8, iotUse: 8, accuracy: 100 },
|
|
{ date: '2026-05-03', day: 2, manualUse: 12, iotUse: 11, accuracy: 91 },
|
|
],
|
|
feedOut: [
|
|
{ date: '2026-05-01', manualOut: 1, iotOut: 1, accuracy: 100 },
|
|
{ date: '2026-05-03', manualOut: 2, iotOut: 2, accuracy: 100 },
|
|
],
|
|
fcr: [
|
|
{
|
|
day: 0,
|
|
iotFcr: 0.5,
|
|
feedTotal: 1,
|
|
iotWeight: 46,
|
|
chickenLife: 19990,
|
|
harvestWeightTotal: 0,
|
|
},
|
|
{
|
|
day: 2,
|
|
iotFcr: 1.25,
|
|
feedTotal: 20,
|
|
iotWeight: 910,
|
|
chickenLife: 19875,
|
|
harvestWeightTotal: 150,
|
|
},
|
|
],
|
|
eef: [
|
|
{
|
|
day: 0,
|
|
iotEef: 100,
|
|
chickenLifePercentage: 99.9,
|
|
iotAverageHarvestBw: null,
|
|
iotAverageDayWeight: 46,
|
|
iotFcr: 0.5,
|
|
},
|
|
{
|
|
day: 2,
|
|
iotEef: 320,
|
|
chickenLifePercentage: 99.4,
|
|
iotAverageHarvestBw: 1500,
|
|
iotAverageDayWeight: 910,
|
|
iotFcr: 1.25,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const byKey = Object.fromEntries(summary.metrics.map((m) => [m.key, m])) as Record<
|
|
ReportSummaryMetricKey,
|
|
ReportSummaryMetric
|
|
>;
|
|
expect(byKey.umur.value).toBe(2);
|
|
expect(byKey.populasi.value).toBe(19875);
|
|
expect(byKey.mortalitas.value).toBe(15);
|
|
expect(byKey.panen.value).toBe(100);
|
|
expect(byKey.beratPanen.value).toBe(150);
|
|
expect(byKey.karungMasuk.value).toBe(30);
|
|
expect(byKey.karungDituang.value).toBe(20);
|
|
expect(byKey.beratPakan.value).toBe(1000);
|
|
expect(byKey.karungKeluar.value).toBe(3);
|
|
expect(byKey.beratAvg.value).toBe(910);
|
|
expect(byKey.uniformity.value).toBe(88.5);
|
|
expect(byKey.fcr.value).toBe(1.25);
|
|
expect(byKey.eef.value).toBe(320);
|
|
});
|
|
|
|
it('falls back to avgManual when avgIot is null on the latest weight row', () => {
|
|
const summary = buildReportSummary(
|
|
emptyPayload({
|
|
weight: [
|
|
{
|
|
date: '2026-05-03',
|
|
day: 2,
|
|
avgManual: 905,
|
|
avgIot: null,
|
|
uniformity: 90,
|
|
difference: null,
|
|
accuracy: null,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
expect(summary.metrics.find((m) => m.key === 'beratAvg')?.value).toBe(905);
|
|
});
|
|
});
|