update api configuration for panel iot and adjust ai insight

This commit is contained in:
Alberto-Audrix committed 2026-09-18 16:06:41 +07:00
1 parent a2b5b10977
commit 20d04acb3e
20 files changed
+814 -177

No files matched your search

@@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest';
import { buildCountingInsightContext } from '../insightContextBuilders.ts';
import { scopeContextToTimeframe, scopeScalarsToTimeframe } from '../insightScope.ts';
const countingScalarScope = {
historyKey: 'tren_populasi_harian',
derive: {
populasiKini_ekor: {
field: 'populasi',
agg: 'lastValid' as const,
replaces: 'populasi_kini_ekor',
},
mortalitasKumulatif_ekor: {
field: 'mortalitas_kumulatif_ekor',
agg: 'last' as const,
replaces: 'mortalitas_kumulatif_ekor',
emptyAs: 0 as const,
},
panenKumulatif_ekor: {
field: 'panen_kumulatif_ekor',
agg: 'last' as const,
replaces: 'panen_kumulatif_ekor',
emptyAs: 0 as const,
},
beratPanenKumulatif_kg: {
field: 'berat_panen_kumulatif_kg',
agg: 'last' as const,
replaces: 'berat_panen_kumulatif_kg',
emptyAs: 0 as const,
},
mortalitasPersen: {
field: 'mortalitas_persen',
agg: 'last' as const,
replaces: 'mortalitas_persen',
},
},
drop: [
'populasi_kini_ekor',
'mortalitas_kumulatif_ekor',
'panen_kumulatif_ekor',
'berat_panen_kumulatif_kg',
'mortalitas_persen',
],
};
describe('hitung ayam insight day scope', () => {
it('does not keep full-cycle mortality/panen when day is capped', () => {
const ctx = buildCountingInsightContext({
kandangId: 1,
populasi_awal_ekor: 45_000,
// Stale full-cycle scalars — must be replaced by day-29 window.
populasi_kini_ekor: 100,
mortalitas_kumulatif_ekor: 4269,
mortalitas_persen: 9.49,
panen_kumulatif_ekor: 40_700,
berat_panen_kumulatif_kg: 57_469.4,
dailyRows: [
{ day: 0, stockAkhir: 44_800, mati: 200, panen: 0, beratPanen: 0 },
{ day: 29, stockAkhir: 42_000, mati: 100, panen: 500, beratPanen: 700 },
{ day: 48, stockAkhir: 100, mati: 3969, panen: 40_200, beratPanen: 56_769.4 },
],
});
const sliced = scopeContextToTimeframe(ctx, 'harian', 29);
const scoped = scopeScalarsToTimeframe(
sliced as Record<string, unknown>,
'harian',
countingScalarScope,
29
);
expect(scoped.populasiKini_ekor).toBe(42_000);
// Days 0+29 only: 200+100 mati, 0+500 panen, 0+700 kg
expect(scoped.mortalitasKumulatif_ekor).toBe(300);
expect(scoped.panenKumulatif_ekor).toBe(500);
expect(scoped.beratPanenKumulatif_kg).toBe(700);
expect(scoped.mortalitasPersen).toBe(0.67);
expect(scoped.periode).toBe('hari ke-0 s/d ke-29');
});
});
+50 -16
View File
@@ -397,6 +397,11 @@ export type CountingInsightRow = {
populasi?: number | null;
mati?: number | null;
panen?: number | null;
beratPanen?: number | null;
/** Running totals when the page already computed them (preferred). */
mortalityTotal?: number | null;
harvestTotal?: number | null;
harvestWeightTotal?: number | null;
chickenCount?: number | null;
[key: string]: unknown;
};
@@ -421,30 +426,58 @@ export function buildCountingInsightContext(args: BuildCountingInsightContextArg
const kandangId = requireKandangId(args.kandangId, 'hitung ayam');
const rows = Array.isArray(args.dailyRows) ? args.dailyRows : [];
const tren = rows
const populasi_awal = args.populasi_awal_ekor ?? null;
// Sort first, then accumulate — unsorted input would corrupt running totals.
const ordered = [...rows]
.map((row) => {
const hari = dayOf(row);
if (hari === null) return null;
return {
hari,
populasi: num(row.stockAkhir) ?? num(row.populasi),
mati_ekor: num(row.mati),
panen_ekor: num(row.panen),
chicken_count_iot_ekor: num(row.chickenCount),
};
return { row, hari };
})
.filter((row): row is NonNullable<typeof row> => row !== null)
.filter((item): item is { row: CountingInsightRow; hari: number } => item !== null)
.sort((a, b) => a.hari - b.hari);
let runMort = 0;
let runPanen = 0;
let runBerat = 0;
const tren = ordered.map(({ row, hari }) => {
const mati = num(row.mati) ?? 0;
const panen = num(row.panen) ?? 0;
const berat = num(row.beratPanen) ?? 0;
runMort += mati;
runPanen += panen;
runBerat += berat;
const mortCum = num(row.mortalityTotal) ?? runMort;
const panenCum = num(row.harvestTotal) ?? runPanen;
const beratCum = num(row.harvestWeightTotal) ?? runBerat;
const mortPct =
populasi_awal != null && populasi_awal > 0
? Math.round((mortCum / populasi_awal) * 10000) / 100
: null;
return {
hari,
populasi: num(row.stockAkhir) ?? num(row.populasi),
mati_ekor: num(row.mati),
panen_ekor: num(row.panen),
chicken_count_iot_ekor: num(row.chickenCount),
mortalitas_kumulatif_ekor: mortCum,
panen_kumulatif_ekor: panenCum,
berat_panen_kumulatif_kg: Math.round(beratCum * 100) / 100,
mortalitas_persen: mortPct,
};
});
const latest = tren[tren.length - 1];
const populasi_awal = args.populasi_awal_ekor ?? null;
const mortalitas_ekor = args.mortalitas_kumulatif_ekor ?? null;
const mortalitas_ekor =
args.mortalitas_kumulatif_ekor ?? latest?.mortalitas_kumulatif_ekor ?? null;
const mortalitas_persen =
args.mortalitas_persen !== undefined
? args.mortalitas_persen
: populasi_awal != null && populasi_awal > 0 && mortalitas_ekor != null
? (mortalitas_ekor / populasi_awal) * 100
: null;
: latest?.mortalitas_persen ??
(populasi_awal != null && populasi_awal > 0 && mortalitas_ekor != null
? Math.round((mortalitas_ekor / populasi_awal) * 10000) / 100
: null);
return {
kandangId,
@@ -456,8 +489,9 @@ export function buildCountingInsightContext(args: BuildCountingInsightContextArg
populasi_kini_ekor: args.populasi_kini_ekor ?? latest?.populasi ?? null,
mortalitas_kumulatif_ekor: mortalitas_ekor,
mortalitas_persen,
panen_kumulatif_ekor: args.panen_kumulatif_ekor ?? null,
berat_panen_kumulatif_kg: args.berat_panen_kumulatif_kg ?? null,
panen_kumulatif_ekor: args.panen_kumulatif_ekor ?? latest?.panen_kumulatif_ekor ?? null,
berat_panen_kumulatif_kg:
args.berat_panen_kumulatif_kg ?? latest?.berat_panen_kumulatif_kg ?? null,
tren_populasi_harian: tren,
};
}