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:
1 parent
4439e3b7b1
commit
75eb218558
8 files changed
+76
-34
No files matched your search
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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];
|
||||||
|
|||||||
Reference in new issue
Block a user