114 lines
4.2 KiB
TypeScript
114 lines
4.2 KiB
TypeScript
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';
|
|
}
|