199 lines
8.2 KiB
TypeScript
199 lines
8.2 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import { cobbStandardPerformance } from '../mockData/cobbStandard.ts';
|
|
import { useAppContext } from '../context/AppContext.tsx';
|
|
import { formatNumber, formatRatio } from '../utils/formatters.ts';
|
|
|
|
const CobbStandardPage: React.FC = () => {
|
|
const { cycleData } = useAppContext();
|
|
// Use chickInWeight from active cycle if available, otherwise default to 42
|
|
const [initialBw, setInitialBw] = useState<number>(cycleData.chickInWeight || 42);
|
|
// Use docInCount from active cycle if available, otherwise default to 50000
|
|
const [capacity, setCapacity] = useState<number>(cycleData.docInCount || 50000);
|
|
|
|
// Sync initialBw when cycle's chickInWeight changes
|
|
useEffect(() => {
|
|
if (cycleData.chickInWeight) {
|
|
setInitialBw(cycleData.chickInWeight);
|
|
}
|
|
}, [cycleData.chickInWeight]);
|
|
|
|
// Sync capacity when cycle's docInCount changes
|
|
useEffect(() => {
|
|
if (cycleData.docInCount) {
|
|
setCapacity(cycleData.docInCount);
|
|
}
|
|
}, [cycleData.docInCount]);
|
|
|
|
const calculatedData = useMemo(() => {
|
|
const results = [];
|
|
let lastBw = Number(initialBw);
|
|
let lastCumFeed = 0;
|
|
|
|
for (const standard of cobbStandardPerformance) {
|
|
const currentBw = lastBw * (1 + standard.dailyGainPercent);
|
|
const dailyGainG = currentBw - lastBw;
|
|
const cumFeedG = currentBw * standard.fcr;
|
|
const dailyFeedG = cumFeedG - lastCumFeed;
|
|
const feedKg = (dailyFeedG * capacity) / 1000;
|
|
const feedBag = feedKg / 50;
|
|
|
|
results.push({
|
|
day: standard.day,
|
|
bw: Math.round(currentBw),
|
|
dailyGainG: Math.round(dailyGainG),
|
|
dailyGainPercent: standard.dailyGainPercent,
|
|
dailyFeedG: Math.round(dailyFeedG),
|
|
cumFeedG: Math.round(cumFeedG),
|
|
fcr: standard.fcr,
|
|
feedKg: Math.round(feedKg),
|
|
feedBag: Math.round(feedBag),
|
|
});
|
|
|
|
lastBw = currentBw;
|
|
lastCumFeed = cumFeedG;
|
|
}
|
|
return results;
|
|
}, [initialBw, capacity]);
|
|
|
|
return (
|
|
<main className="px-4 sm:px-6 lg:px-8 pb-8">
|
|
<div className="flex items-center mb-6">
|
|
<i className="fa-solid fa-calculator text-3xl text-red-600 mr-4"></i>
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-800">Kalkulator Standar Performa</h1>
|
|
<p className="mt-1 text-gray-600">
|
|
Hitung proyeksi pertumbuhan dan kebutuhan pakan berdasarkan bobot awal dan kapasitas
|
|
kandang.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
<div className="bg-white p-4 rounded-xl shadow-sm">
|
|
<label htmlFor="initialBw" className="block text-sm font-medium text-gray-700">
|
|
Bobot Awal DOC (g)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="initialBw"
|
|
value={initialBw}
|
|
onChange={(e) => setInitialBw(Number(e.target.value))}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-lg font-semibold"
|
|
/>
|
|
</div>
|
|
<div className="bg-white p-4 rounded-xl shadow-sm">
|
|
<label htmlFor="capacity" className="block text-sm font-medium text-gray-700">
|
|
Kapasitas Kandang (ekor)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
id="capacity"
|
|
value={capacity}
|
|
onChange={(e) => setCapacity(Number(e.target.value))}
|
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 text-lg font-semibold"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-4 rounded-xl shadow-sm overflow-hidden">
|
|
<div className="overflow-auto max-h-[calc(100vh-380px)]">
|
|
<table className="min-w-full border-collapse text-sm">
|
|
<thead className="bg-gray-700 text-white sticky top-0 z-10">
|
|
<tr>
|
|
<th
|
|
rowSpan={2}
|
|
className="p-2 border border-gray-800 font-semibold sticky left-0 bg-gray-700 z-20"
|
|
>
|
|
Umur
|
|
</th>
|
|
<th rowSpan={2} className="p-2 border border-gray-800 font-semibold">
|
|
BW (g)
|
|
</th>
|
|
<th colSpan={2} className="p-2 border border-gray-800 font-semibold">
|
|
Daily Gain
|
|
</th>
|
|
<th colSpan={2} className="p-2 border border-gray-800 font-semibold">
|
|
Feed Consumption
|
|
</th>
|
|
<th rowSpan={2} className="p-2 border border-gray-800 font-semibold">
|
|
FCR
|
|
</th>
|
|
<th colSpan={2} className="p-2 border border-gray-800 font-semibold">
|
|
Kebutuhan Pakan
|
|
</th>
|
|
</tr>
|
|
<tr>
|
|
<th className="p-2 border border-gray-800 font-semibold">(g)</th>
|
|
<th className="p-2 border border-gray-800 font-semibold">(%)</th>
|
|
<th className="p-2 border border-gray-800 font-semibold">Daily (g)</th>
|
|
<th className="p-2 border border-gray-800 font-semibold">Cum (g)</th>
|
|
<th className="p-2 border border-gray-800 font-semibold">(Kg)</th>
|
|
<th className="p-2 border border-gray-800 font-semibold">(Bag)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="font-mono">
|
|
<tr className="bg-yellow-100 font-bold">
|
|
<td className="p-1 border border-gray-200 text-center bg-gray-700 text-white sticky left-0 z-10">
|
|
0
|
|
</td>
|
|
<td className="p-1 border border-gray-200 text-center text-gray-700">
|
|
{initialBw}
|
|
</td>
|
|
<td className="p-1 border border-gray-200 text-center" colSpan={7}></td>
|
|
</tr>
|
|
{calculatedData.map((row) => {
|
|
const isSeventhDay = row.day > 0 && row.day % 7 === 0;
|
|
const rowClass = `hover:bg-red-50 ${isSeventhDay ? 'font-bold bg-blue-100' : 'even:bg-gray-100/50'}`;
|
|
const cellBorderClass = isSeventhDay
|
|
? 'border-b-[3px] border-double border-blue-400'
|
|
: 'border-gray-200';
|
|
|
|
return (
|
|
<tr key={row.day} className={rowClass}>
|
|
<td
|
|
className={`p-1 border ${cellBorderClass} text-center bg-gray-700 text-white font-semibold sticky left-0 z-10`}
|
|
>
|
|
{row.day}
|
|
</td>
|
|
<td
|
|
className={`p-1 border ${cellBorderClass} text-center font-bold text-red-600`}
|
|
>
|
|
{formatNumber(row.bw)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatNumber(row.dailyGainG)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{(row.dailyGainPercent * 100).toFixed(0)}%
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatNumber(row.dailyFeedG)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatNumber(row.cumFeedG)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatRatio(row.fcr)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatNumber(row.feedKg)}
|
|
</td>
|
|
<td className={`p-1 border ${cellBorderClass} text-center text-gray-700`}>
|
|
{formatNumber(row.feedBag)}
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div className="text-right mt-2 text-xs text-gray-500 font-semibold">
|
|
Standar Performa COBB (2018)
|
|
</div>
|
|
</main>
|
|
);
|
|
};
|
|
|
|
export default CobbStandardPage;
|