import { weightStandardData } from '../mockData/weightStandardData.ts'; /** * Formats a weight value in grams into a locale-aware string. Values at or * below 999 g stay in grams; larger values are shown in kg. Negative values * (e.g. ADG during close-out) are shown with a minus sign. * * By default every value is shown with 4 decimal places (never rounded). * Callers may override with options: * - `decimals` decimal places to show (default 4) * - `stripTrailingZeros` drop trailing zeros so the exact value is shown * - `truncate` truncate instead of round (default false) */ export function formatWeight( grams: number | null | undefined, options?: { suffix?: string; decimals?: number; stripTrailingZeros?: boolean; truncate?: boolean } ): string { const suffix = options?.suffix ?? ''; const decimals = options?.decimals ?? 4; const minDecimals = options?.stripTrailingZeros ? 0 : decimals; const format = (value: number, unit: string): string => { let display = value; if (options?.truncate) { const factor = 10 ** decimals; display = Math.trunc(display * factor) / factor; } const formatted = display.toLocaleString('id-ID', { minimumFractionDigits: minDecimals, maximumFractionDigits: decimals, }); return `${formatted} ${unit}${suffix}`; }; if (grams == null) return format(0, 'g'); const sign = grams < 0 ? '-' : ''; const abs = Math.abs(grams); const unit = abs <= 999 ? 'g' : 'kg'; const display = abs <= 999 ? abs : abs / 1000; return `${sign}${format(display, unit)}`; } /** * Picks the Ross 308 standard row for a DOC (chick-in) weight. * Table rows are keyed by DOC weight (g) 35..51; columns are age days 1..35. * Weights outside that range use the closest available row (e.g. 20→35, 60→51). */ function pickStandardValues(docWeight: number): number[] { const rows = weightStandardData.data; if (rows.length === 0) return []; const exact = rows.find((row) => row.day === docWeight); if (exact) return exact.values; let closest = rows[0]!; for (const row of rows) { if (Math.abs(row.day - docWeight) < Math.abs(closest.day - docWeight)) { closest = row; } } return closest.values; } /** Target body weight (grams) for a given age from the Ross 308 DOC-weight table. */ export function getTargetWeightForAge(age: number, docWeight: number): number | null { if (age === 0) return docWeight > 0 ? docWeight : null; const values = pickStandardValues(docWeight); if (age < 1 || age > values.length) return null; return values[age - 1] ?? null; } /** Target daily weight gain (grams/day) for a given age from the standard. */ export function getTargetGainForAge(age: number, docWeight: number): number | null { const values = pickStandardValues(docWeight); if (age < 1 || age > values.length) return null; if (age === 1) { const current = values[0]; if (current == null || docWeight <= 0) return null; return current - docWeight; } const current = values[age - 1]; const previous = values[age - 2]; if (current == null || previous == null) return null; return current - previous; } export type WeightStatus = 'ok' | 'warning' | 'critical'; /** ADG threshold used by the old dashboard: >=60 ok, >=40 warning, else critical. */ export function adgStatus(adg: number | null | undefined): WeightStatus { if (adg == null || adg < 0) return 'critical'; if (adg >= 60) return 'ok'; if (adg >= 40) return 'warning'; return 'critical'; } /** Uniformity threshold (CP 707): >=80 ok, >=70 warning, else critical. */ export function uniformityStatus(uniformity: number | null | undefined): WeightStatus { if (uniformity == null || uniformity < 0) return 'critical'; if (uniformity >= 80) return 'ok'; if (uniformity >= 70) return 'warning'; return 'critical'; } /** Average-weight status from the deviation against the standard target. */ export function weightDeviationStatus( actual: number | null | undefined, target: number | null | undefined ): WeightStatus { if (actual == null || target == null || target <= 0) return 'ok'; const deviation = ((actual - target) / target) * 100; if (deviation < -20) return 'critical'; if (deviation < -10) return 'warning'; return 'ok'; }