first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,96 @@
|
||||
import type { ChickenCounting, Cycle, KPI, ManualInput } from '../types/api.ts';
|
||||
import { addDaysIso, cycleDayForDate } from './format.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<Cycle, 'start_date' | 'end_date' | 'total_days' | 'doc_in_count'>;
|
||||
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<string, number> {
|
||||
const kpiByDate = new Map(kpis.map((k) => [k.date, k]));
|
||||
const manualByDate = new Map(manualInputs.map((m) => [m.date, m]));
|
||||
|
||||
const lastCycleDay = Math.max(0, (cycle.total_days || 1) - 1);
|
||||
const endDay = cycleDayForDate(cycle.start_date, cycle.end_date);
|
||||
const maxDay = Math.min(lastCycleDay, endDay);
|
||||
|
||||
const result = new Map<string, number>();
|
||||
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 = kpi?.chicken_life ?? 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',
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user