update api configuration for panel iot and adjust ai insight
This commit is contained in:
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');
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user