import type { ChickenCounting, Cycle, KPI, ManualInput } from '../types/api.ts'; import { addDaysIso, cycleDayForDate, todayIso } from './format.ts'; import { stockAkhirFromKpi } from './kpiStock.ts'; export const COUNTING_ACCURACY_WARNING_THRESHOLD = 95; export type CountingAccuracyWarning = { day: number; date: string; iotCount: number; stockAkhir: number; accuracy: number; ratioPercent: number; direction: 'over' | 'under'; }; export type CountingAccuracyWarningInput = { cycle: Pick; countings: ChickenCounting[]; kpis: KPI[]; manualInputs: ManualInput[]; }; function round1(value: number): number { return Math.round(value * 10) / 10; } function matchAccuracy(iot: number, stockAkhir: number): number { return round1((Math.min(iot, stockAkhir) / Math.max(iot, stockAkhir)) * 100); } /** Build stock-akhir by date using the same rules as Hitung Ayam. */ function stockAkhirByDate( cycle: CountingAccuracyWarningInput['cycle'], kpis: KPI[], manualInputs: ManualInput[] ): Map { const kpiByDate = new Map(kpis.map((k) => [k.date, k])); const manualByDate = new Map(manualInputs.map((m) => [m.date, m])); const calendarDay = cycleDayForDate(cycle.start_date, todayIso()); const maxDay = cycle.end_date ? Math.min(calendarDay, cycleDayForDate(cycle.start_date, cycle.end_date)) : calendarDay; const result = new Map(); let previousStockAkhir = 0; for (let day = 0; day <= maxDay; day++) { const date = addDaysIso(cycle.start_date, day); const kpi = kpiByDate.get(date); const manual = manualByDate.get(date); const stockAwal = day === 0 ? cycle.doc_in_count || 0 : previousStockAkhir; const mati = manual?.mortality_manual ?? kpi?.mortality ?? 0; const panen = manual?.harvest_manual ?? kpi?.harvest ?? 0; const stockAkhir = stockAkhirFromKpi(kpi) ?? Math.max(0, stockAwal - mati - panen); result.set(date, stockAkhir); previousStockAkhir = stockAkhir; } return result; } function latestIotCounting(countings: ChickenCounting[]): ChickenCounting | null { let best: ChickenCounting | null = null; for (const row of countings) { if (row.total_count == null) continue; if (!best || row.date > best.date) best = row; } return best; } export function getCountingAccuracyWarning( input: CountingAccuracyWarningInput ): CountingAccuracyWarning | null { const latest = latestIotCounting(input.countings); if (!latest) return null; const stockMap = stockAkhirByDate(input.cycle, input.kpis, input.manualInputs); const stockAkhir = stockMap.get(latest.date); if (stockAkhir == null || stockAkhir <= 0) return null; const iotCount = latest.total_count; if (iotCount === stockAkhir) return null; const accuracy = matchAccuracy(iotCount, stockAkhir); if (accuracy > COUNTING_ACCURACY_WARNING_THRESHOLD) return null; return { day: cycleDayForDate(input.cycle.start_date, latest.date), date: latest.date, iotCount, stockAkhir, accuracy, ratioPercent: round1((iotCount / stockAkhir) * 100), direction: iotCount > stockAkhir ? 'over' : 'under', }; }