fix doc weight to show real doc instead of forcing 35-51 range and also fixed bug target weight using total days as reference for target weight instead of doc weight

This commit is contained in:
Alberto-Audrix committed 2026-09-07 10:43:54 +07:00
1 parent 4439e3b7b1
commit 75eb218558
8 files changed
+76 -34

No files matched your search

+2 -3
View File
@@ -461,13 +461,12 @@ const SettingsPage: React.FC = () => {
<input <input
type="number" type="number"
className={`${inputClass} mt-1`} className={`${inputClass} mt-1`}
min={35} min={1}
max={51}
value={chickInWeight} value={chickInWeight}
onChange={(e) => { onChange={(e) => {
const value = Number(e.target.value); const value = Number(e.target.value);
if (Number.isNaN(value)) return; if (Number.isNaN(value)) return;
setChickInWeight(Math.min(51, Math.max(35, value))); setChickInWeight(Math.max(1, value));
}} }}
/> />
</label> </label>
+5 -5
View File
@@ -90,7 +90,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
} }
}; };
const totalDays = selectedCycle?.total_days ?? 35; const docWeight = selectedCycle?.chick_in_weight ?? 35;
const selectedRow = useMemo( const selectedRow = useMemo(
() => rows.find((row) => row.date === selectedDate) ?? null, () => rows.find((row) => row.date === selectedDate) ?? null,
@@ -206,13 +206,13 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<div className="space-y-8"> <div className="space-y-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-1"> <div className="lg:col-span-1">
<WeightStats latest={kpiLatest} totalDays={totalDays} /> <WeightStats latest={kpiLatest} docWeight={docWeight} />
</div> </div>
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<DailyWeightProgressionChart rows={chartRows} totalDays={totalDays} /> <DailyWeightProgressionChart rows={chartRows} docWeight={docWeight} />
</div> </div>
</div> </div>
<DailyWeightGainChart rows={chartRows} totalDays={totalDays} /> <DailyWeightGainChart rows={chartRows} docWeight={docWeight} />
</div> </div>
{/* ===== SEPARATOR: DATA PER HARI ===== */} {/* ===== SEPARATOR: DATA PER HARI ===== */}
@@ -243,7 +243,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
</div> </div>
</div> </div>
<DailyDetailPanel row={selectedRow} totalDays={totalDays} /> <DailyDetailPanel row={selectedRow} docWeight={docWeight} />
{/* Riwayat lengkap */} {/* Riwayat lengkap */}
<div className="mt-6"> <div className="mt-6">
+3 -3
View File
@@ -5,7 +5,7 @@ import { formatWeight, getTargetWeightForAge } from '../../utils/weight.ts';
interface DailyDetailPanelProps { interface DailyDetailPanelProps {
row: ChickenWeight | null; row: ChickenWeight | null;
totalDays: number; docWeight: number;
} }
const DetailCard: React.FC<{ const DetailCard: React.FC<{
@@ -29,7 +29,7 @@ const DetailCard: React.FC<{
</div> </div>
); );
const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, totalDays }) => { const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, docWeight }) => {
if (!row) { if (!row) {
return ( return (
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6">
@@ -44,7 +44,7 @@ const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, totalDays }) =
); );
} }
const target = getTargetWeightForAge(row.age, totalDays); const target = getTargetWeightForAge(row.age, docWeight);
const deviation = target != null && row.average_weight > 0 ? row.average_weight - target : null; const deviation = target != null && row.average_weight > 0 ? row.average_weight - target : null;
return ( return (
+4 -4
View File
@@ -17,7 +17,7 @@ import { formatWeight, getTargetGainForAge } from '../../utils/weight.ts';
interface DailyWeightGainChartProps { interface DailyWeightGainChartProps {
rows: ChickenWeight[]; rows: ChickenWeight[];
totalDays: number; docWeight: number;
} }
type GainStatus = 'above' | 'near' | 'below' | 'none'; type GainStatus = 'above' | 'near' | 'below' | 'none';
@@ -30,11 +30,11 @@ type ChartPoint = {
status: GainStatus; status: GainStatus;
}; };
const DailyWeightGainChart: React.FC<DailyWeightGainChartProps> = ({ rows, totalDays }) => { const DailyWeightGainChart: React.FC<DailyWeightGainChartProps> = ({ rows, docWeight }) => {
const chartData = useMemo<ChartPoint[]>( const chartData = useMemo<ChartPoint[]>(
() => () =>
[...rows].reverse().map((row) => { [...rows].reverse().map((row) => {
const targetGain = getTargetGainForAge(row.age, totalDays); const targetGain = getTargetGainForAge(row.age, docWeight);
const gain = row.average_daily_gain; const gain = row.average_daily_gain;
let status: GainStatus = 'none'; let status: GainStatus = 'none';
if (gain != null && targetGain != null) { if (gain != null && targetGain != null) {
@@ -45,7 +45,7 @@ const DailyWeightGainChart: React.FC<DailyWeightGainChartProps> = ({ rows, total
} }
return { day: row.age, date: row.date, gain, targetGain, status }; return { day: row.age, date: row.date, gain, targetGain, status };
}), }),
[rows, totalDays] [rows, docWeight]
); );
const hasNoData = chartData.length === 0; const hasNoData = chartData.length === 0;
@@ -14,7 +14,7 @@ import { formatWeight, getTargetWeightForAge } from '../../utils/weight.ts';
interface DailyWeightProgressionChartProps { interface DailyWeightProgressionChartProps {
rows: ChickenWeight[]; rows: ChickenWeight[];
totalDays: number; docWeight: number;
} }
type ChartPoint = { type ChartPoint = {
@@ -27,7 +27,7 @@ type ChartPoint = {
const DailyWeightProgressionChart: React.FC<DailyWeightProgressionChartProps> = ({ const DailyWeightProgressionChart: React.FC<DailyWeightProgressionChartProps> = ({
rows, rows,
totalDays, docWeight,
}) => { }) => {
const chartData = useMemo<ChartPoint[]>( const chartData = useMemo<ChartPoint[]>(
() => () =>
@@ -35,10 +35,10 @@ const DailyWeightProgressionChart: React.FC<DailyWeightProgressionChartProps> =
day: row.age, day: row.age,
date: row.date, date: row.date,
actual: row.average_weight, actual: row.average_weight,
target: getTargetWeightForAge(row.age, totalDays), target: getTargetWeightForAge(row.age, docWeight),
chickenCount: row.chicken_count, chickenCount: row.chicken_count,
})), })),
[rows, totalDays] [rows, docWeight]
); );
const hasNoData = chartData.length === 0; const hasNoData = chartData.length === 0;
+3 -3
View File
@@ -35,11 +35,11 @@ const StatItem: React.FC<{
interface WeightStatsProps { interface WeightStatsProps {
latest: LatestAvg | null; latest: LatestAvg | null;
totalDays: number; docWeight: number;
} }
const WeightStats: React.FC<WeightStatsProps> = ({ latest, totalDays }) => { const WeightStats: React.FC<WeightStatsProps> = ({ latest, docWeight }) => {
const target = latest ? getTargetWeightForAge(latest.age, totalDays) : null; const target = latest ? getTargetWeightForAge(latest.age, docWeight) : null;
return ( return (
<div className="bg-white p-6 rounded-xl shadow-sm h-full"> <div className="bg-white p-6 rounded-xl shadow-sm h-full">
+43
View File
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import { weightStandardData } from '../../mockData/weightStandardData.ts';
import { getTargetGainForAge, getTargetWeightForAge } from '../weight.ts';
function rowForDoc(docWeight: number) {
return weightStandardData.data.find((row) => row.day === docWeight)!;
}
describe('getTargetWeightForAge', () => {
it('uses the exact DOC-weight row and age column', () => {
const row = rowForDoc(42);
expect(getTargetWeightForAge(1, 42)).toBe(row.values[0]);
expect(getTargetWeightForAge(35, 42)).toBe(row.values[34]);
});
it('clamps below-range DOC weight to the 35 g row', () => {
const row = rowForDoc(35);
expect(getTargetWeightForAge(7, 20)).toBe(row.values[6]);
expect(getTargetWeightForAge(7, 30)).toBe(row.values[6]);
});
it('clamps above-range DOC weight to the 51 g row', () => {
const row = rowForDoc(51);
expect(getTargetWeightForAge(7, 52)).toBe(row.values[6]);
expect(getTargetWeightForAge(7, 60)).toBe(row.values[6]);
});
it('returns null for ages outside 1..35', () => {
expect(getTargetWeightForAge(0, 42)).toBeNull();
expect(getTargetWeightForAge(36, 42)).toBeNull();
});
});
describe('getTargetGainForAge', () => {
it('returns day-over-day delta from the DOC-weight row', () => {
const row = rowForDoc(40);
expect(getTargetGainForAge(2, 40)).toBe(row.values[1]! - row.values[0]!);
});
it('returns null for age 1', () => {
expect(getTargetGainForAge(1, 40)).toBeNull();
});
});
+12 -12
View File
@@ -41,34 +41,34 @@ export function formatWeight(
} }
/** /**
* Picks the standard table whose harvest day is closest to the cycle's * Picks the Ross 308 standard row for a DOC (chick-in) weight.
* `total_days`. Every row carries values for days 1..35, so a 35-, 42- or * Table rows are keyed by DOC weight (g) 35..51; columns are age days 1..35.
* 45-day cycle all map onto a matching table. * Weights outside that range use the closest available row (e.g. 20→35, 60→51).
*/ */
function pickStandardValues(totalDays: number): number[] { function pickStandardValues(docWeight: number): number[] {
const rows = weightStandardData.data; const rows = weightStandardData.data;
if (rows.length === 0) return []; if (rows.length === 0) return [];
const exact = rows.find((row) => row.day === totalDays); const exact = rows.find((row) => row.day === docWeight);
if (exact) return exact.values; if (exact) return exact.values;
let closest = rows[0]!; let closest = rows[0]!;
for (const row of rows) { for (const row of rows) {
if (Math.abs(row.day - totalDays) < Math.abs(closest.day - totalDays)) { if (Math.abs(row.day - docWeight) < Math.abs(closest.day - docWeight)) {
closest = row; closest = row;
} }
} }
return closest.values; return closest.values;
} }
/** Target body weight (grams) for a cycle day from the Ross 308 standard. */ /** Target body weight (grams) for a given age from the Ross 308 DOC-weight table. */
export function getTargetWeightForAge(age: number, totalDays: number): number | null { export function getTargetWeightForAge(age: number, docWeight: number): number | null {
const values = pickStandardValues(totalDays); const values = pickStandardValues(docWeight);
if (age < 1 || age > values.length) return null; if (age < 1 || age > values.length) return null;
return values[age - 1] ?? null; return values[age - 1] ?? null;
} }
/** Target daily weight gain (grams/day) for a cycle day from the standard. */ /** Target daily weight gain (grams/day) for a given age from the standard. */
export function getTargetGainForAge(age: number, totalDays: number): number | null { export function getTargetGainForAge(age: number, docWeight: number): number | null {
const values = pickStandardValues(totalDays); const values = pickStandardValues(docWeight);
if (age < 2 || age > values.length) return null; if (age < 2 || age > values.length) return null;
const current = values[age - 1]; const current = values[age - 1];
const previous = values[age - 2]; const previous = values[age - 2];