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 { return { cycleId: '1', cycleStart: '2026-05-01', cycleEnd: '2026-06-04', kandangId: 9, kandangName: 'Kandang 1', area: 'Area A', upToDay: 2, docWeight: null, 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 14 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', 'docWeight', '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.docWeight.value).toBeNull(); 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); }); it('places Berat DOC beside Umur from payload.docWeight', () => { const summary = buildReportSummary(emptyPayload({ upToDay: 3, docWeight: 42 })); expect(summary.metrics[0]).toMatchObject({ key: 'umur', value: 3 }); expect(summary.metrics[1]).toMatchObject({ key: 'docWeight', label: 'Berat DOC', value: 42, unit: 'gram', }); }); });