first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,220 @@
|
||||
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,
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user