Add daily report day picker and cumulative insights for mortality data. Updated AiInsightCard and related components to support daily and weekly reporting, ensuring accurate data representation. Introduced new utility functions for water consumption standards and enhanced context management for insights.
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-08-12 13:50:30 +07:00
1 parent 172ea3a148
commit 427cac54a1
18 files changed
+1055 -205

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import { describe, it, expect } from 'vitest';
import {
calculateCp707Uniformity,
getCp707UniformityStatus,
CP707_UNIFORMITY_TARGET,
} from '../weightUtils.ts';
/**
* Keseragaman dihitung ulang dari `avg` + `Standard_deviasi` menurut CP 707
* (persen ayam dalam ±10% dari rata-rata), bukan diambil dari field
* `uniformity` milik API `distribusi_bobot` yang memakai band ±5%.
*
* Nilai acuan di bawah berasal dari respons API sungguhan dan dicocokkan
* dengan perhitungan referensi `math.erf` di Python.
*/
describe('calculateCp707Uniformity', () => {
it.each([
// tanggal, avg (g), Standard_deviasi (g), harapan (%)
['2026-08-10', 1123.53, 161.3, 51.4],
['2026-08-05', 806.79, 26.87, 99.7],
['2026-08-04', 667.31, 139.71, 36.7],
['2026-06-18', 1324.8, 106.5, 78.6],
])('cocok dengan perhitungan referensi untuk %s', (_tanggal, avg, sd, harapan) => {
expect(calculateCp707Uniformity(avg, sd)).toBeCloseTo(harapan as number, 0);
});
it('tidak terpengaruh lebar bucket 50 g di awal siklus', () => {
// Bobot 75 g: band ±10% hanya 15 g, jauh lebih sempit dari bucket 50 g milik
// API. Perhitungan berbasis bucket gagal di sini; berbasis SD tidak.
expect(calculateCp707Uniformity(75.4, 3.3)).toBeCloseTo(97.8, 0);
});
it('mengembalikan 100 saat tidak ada variasi bobot', () => {
expect(calculateCp707Uniformity(1000, 0)).toBe(100);
});
it('hasilnya selalu berada di rentang 0-100', () => {
for (const [avg, sd] of [
[1000, 1],
[1000, 5000],
[50, 0.1],
]) {
const v = calculateCp707Uniformity(avg, sd);
expect(v).not.toBeNull();
expect(v as number).toBeGreaterThanOrEqual(0);
expect(v as number).toBeLessThanOrEqual(100);
}
});
it('mengembalikan null, bukan angka karangan, saat data tidak memadai', () => {
expect(calculateCp707Uniformity(null, 10)).toBeNull();
expect(calculateCp707Uniformity(1000, null)).toBeNull();
expect(calculateCp707Uniformity(undefined, undefined)).toBeNull();
expect(calculateCp707Uniformity(0, 10)).toBeNull();
expect(calculateCp707Uniformity(-100, 10)).toBeNull();
expect(calculateCp707Uniformity(1000, -5)).toBeNull();
expect(calculateCp707Uniformity(NaN, 10)).toBeNull();
expect(calculateCp707Uniformity(1000, Infinity)).toBeNull();
});
it('naik saat sebaran menyempit', () => {
const ketat = calculateCp707Uniformity(1000, 30) as number;
const longgar = calculateCp707Uniformity(1000, 150) as number;
expect(ketat).toBeGreaterThan(longgar);
});
});
describe('getCp707UniformityStatus', () => {
it('memakai ambang CP 707 (>80%), bukan ambang lama 85/75', () => {
expect(getCp707UniformityStatus(CP707_UNIFORMITY_TARGET)).toBe('ok');
expect(getCp707UniformityStatus(82)).toBe('ok');
// 82 dulu berstatus "warning" dengan ambang lama 85.
expect(getCp707UniformityStatus(75)).toBe('warning');
expect(getCp707UniformityStatus(70)).toBe('warning');
expect(getCp707UniformityStatus(69.9)).toBe('critical');
});
it('memperlakukan data tidak tersedia sebagai critical', () => {
expect(getCp707UniformityStatus(null)).toBe('critical');
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, it, expect } from 'vitest';
import { scopeContextToTimeframe, describeTimeframe } from '../insightScope.ts';
/**
* Picking a day in the AI Insight day picker means "the cycle so far, up to
* that day" — day 0 through day N — not that day in isolation. A single row
* shows no trend and cannot support a cumulative figure such as
* mortality-to-date, which is what these cards lead with.
*/
const ctx = {
riwayat: [
{ hari: 0, nilai: 5 },
{ hari: 1, nilai: 10 },
{ hari: 5, nilai: 50 },
{ hari: 12, nilai: 120 },
{ hari: 30, nilai: 300 },
],
// Snapshot list: no time dimension, must survive slicing intact.
perangkat: [{ nama: 'B1' }, { nama: 'B2' }],
bobot: 1234,
};
const daysIn = (scoped: typeof ctx) => scoped.riwayat.map((row) => row.hari);
describe('scopeContextToTimeframe — jendela kumulatif hari 0..N', () => {
it('menyertakan seluruh hari sampai hari yang dipilih', () => {
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 5))).toEqual([0, 1, 5]);
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 12))).toEqual([0, 1, 5, 12]);
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 30))).toEqual([0, 1, 5, 12, 30]);
});
it('tidak menyertakan hari setelah hari yang dipilih', () => {
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 5))).not.toContain(12);
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 5))).not.toContain(30);
});
it('menyertakan hari 0 sebagai awal siklus', () => {
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 1))).toEqual([0, 1]);
});
it('hari tanpa data sendiri tetap memuat hari-hari sebelumnya', () => {
// Hari 7 tidak ada barisnya, tapi jendela 0..7 tetap berisi hari 0, 1, 5.
expect(daysIn(scopeContextToTimeframe(ctx, 'harian', 7))).toEqual([0, 1, 5]);
});
it('tetap memakai hari terakhir saja bila tidak ada hari yang diminta', () => {
// Perilaku lama, dipakai saat pengguna belum menyentuh dropdown.
expect(daysIn(scopeContextToTimeframe(ctx, 'harian'))).toEqual([30]);
});
it('tidak memotong daftar snapshot dan tidak menyentuh skalar', () => {
const scoped = scopeContextToTimeframe(ctx, 'harian', 5);
expect(scoped.perangkat).toHaveLength(2);
expect(scoped.bobot).toBe(1234);
});
it('tidak mengubah perilaku mingguan', () => {
// Jendela 7 hari tetap dihitung dari hari terakhir, apa pun hari yang dipilih.
expect(daysIn(scopeContextToTimeframe(ctx, 'mingguan', 5))).toEqual([30]);
});
it('mengenali penamaan field "day" maupun "hari"', () => {
const inggris = {
riwayat: [
{ day: 3, nilai: 1 },
{ day: 9, nilai: 2 },
],
};
expect(scopeContextToTimeframe(inggris, 'harian', 3).riwayat.map((r) => r.day)).toEqual([3]);
});
});
describe('describeTimeframe — label periode', () => {
it('menyatakan rentang kumulatif, bukan satu hari', () => {
// Menyebut "hari ke-30" saja membuat model melaporkan angka kumulatif
// 31 hari seolah-olah itu capaian satu hari.
expect(describeTimeframe('harian', 48, 30)).toContain('hari ke-0 sampai hari ke-30');
expect(describeTimeframe('harian', 48, 30)).toContain('kumulatif');
});
it('menandai bahwa periode berakhir bukan di hari ini', () => {
expect(describeTimeframe('harian', 48, 30)).toContain('BUKAN hari ini');
});
it('menandai bahwa periode berakhir di hari ini bila memang begitu', () => {
expect(describeTimeframe('harian', 30, 30)).toContain('berakhir hari ini');
expect(describeTimeframe('harian', 30, 30)).not.toContain('BUKAN hari ini');
});
it('mempertahankan perilaku lama saat tidak ada hari dipilih', () => {
expect(describeTimeframe('harian', 30)).toBe('Hari ini saja (hari ke-30)');
expect(describeTimeframe('mingguan', 30)).toContain('7 hari terakhir');
});
});