158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import {
|
|
CartesianGrid,
|
|
ComposedChart,
|
|
Line,
|
|
ResponsiveContainer,
|
|
Tooltip,
|
|
XAxis,
|
|
YAxis,
|
|
} from 'recharts';
|
|
import type { ChickenWeight } from '../../types/api.ts';
|
|
import { formatDateId } from '../../utils/format.ts';
|
|
import { formatWeight, getTargetWeightForAge } from '../../utils/weight.ts';
|
|
|
|
interface DailyWeightProgressionChartProps {
|
|
rows: ChickenWeight[];
|
|
docWeight: number;
|
|
}
|
|
|
|
type ChartPoint = {
|
|
day: number;
|
|
date: string;
|
|
actual: number | null;
|
|
target: number | null;
|
|
chickenCount: number;
|
|
};
|
|
|
|
const DailyWeightProgressionChart: React.FC<DailyWeightProgressionChartProps> = ({
|
|
rows,
|
|
docWeight,
|
|
}) => {
|
|
const chartData = useMemo<ChartPoint[]>(
|
|
() =>
|
|
[...rows].reverse().map((row) => ({
|
|
day: row.age,
|
|
date: row.date,
|
|
actual: row.average_weight,
|
|
target: getTargetWeightForAge(row.age, docWeight),
|
|
chickenCount: row.chicken_count,
|
|
})),
|
|
[rows, docWeight]
|
|
);
|
|
|
|
const hasNoData = chartData.length === 0;
|
|
|
|
const CustomTooltip = ({
|
|
active,
|
|
payload,
|
|
}: {
|
|
active?: boolean;
|
|
payload?: Array<{ payload: ChartPoint }>;
|
|
}) => {
|
|
if (!active || !payload?.length) return null;
|
|
const point = payload[0]?.payload;
|
|
if (!point || point.actual == null) return null;
|
|
const deviation = point.target != null ? point.actual - point.target : null;
|
|
|
|
return (
|
|
<div className="bg-white p-4 border border-gray-300 rounded shadow-lg">
|
|
<p className="font-bold text-gray-800">Hari ke-{point.day}</p>
|
|
<p className="text-sm text-gray-600">{formatDateId(point.date)}</p>
|
|
<div className="mt-2 space-y-1 text-sm">
|
|
<p>
|
|
<span className="font-semibold text-red-600">Aktual:</span>{' '}
|
|
{formatWeight(point.actual, { stripTrailingZeros: true })}
|
|
</p>
|
|
<p>
|
|
<span className="font-semibold text-blue-600">Target:</span>{' '}
|
|
{formatWeight(point.target, { stripTrailingZeros: true })}
|
|
</p>
|
|
{point.chickenCount > 0 && (
|
|
<p>
|
|
<span className="font-semibold text-gray-600">Ayam:</span> {point.chickenCount} ekor
|
|
</p>
|
|
)}
|
|
{deviation != null && (
|
|
<p>
|
|
<span className="font-semibold">Deviasi:</span>{' '}
|
|
<span className={deviation >= 0 ? 'text-green-600' : 'text-orange-600'}>
|
|
{deviation >= 0 ? '+' : ''}
|
|
{formatWeight(Math.abs(deviation), { stripTrailingZeros: true })}
|
|
</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-sm h-full">
|
|
<h2 className="text-xl font-bold mb-4">Grafik Berat Aktual vs Target</h2>
|
|
<div className="h-80">
|
|
{hasNoData ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="text-center">
|
|
<p className="text-gray-500 text-lg font-medium">Tidak ada data tersedia</p>
|
|
<p className="text-gray-400 text-sm mt-2">
|
|
Data berat belum tersedia untuk periode ini
|
|
</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<ComposedChart data={chartData} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
|
|
<XAxis
|
|
dataKey="day"
|
|
tick={{ fill: '#6b7280', fontSize: 12 }}
|
|
label={{ value: 'Umur (hari)', position: 'insideBottom', offset: -5 }}
|
|
/>
|
|
<YAxis
|
|
tick={{ fill: '#6b7280', fontSize: 12 }}
|
|
tickFormatter={(value) => formatWeight(value, { stripTrailingZeros: true })}
|
|
/>
|
|
<Tooltip content={<CustomTooltip />} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="target"
|
|
stroke="#3b82f6"
|
|
strokeWidth={2}
|
|
strokeDasharray="5 5"
|
|
dot={false}
|
|
connectNulls
|
|
name="Target"
|
|
/>
|
|
<Line
|
|
type="monotone"
|
|
dataKey="actual"
|
|
stroke="#dc2626"
|
|
strokeWidth={3}
|
|
dot={{ r: 4, fill: '#dc2626' }}
|
|
activeDot={{ r: 6 }}
|
|
connectNulls
|
|
name="Berat Aktual"
|
|
/>
|
|
</ComposedChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</div>
|
|
{!hasNoData && (
|
|
<div className="mt-4 flex items-center justify-center space-x-6 text-sm text-gray-600">
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-8 h-0.5 bg-red-600"></div>
|
|
<span>Berat Aktual</span>
|
|
</div>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-8 h-0.5 bg-blue-600 border-dashed border-t-2 border-blue-600"></div>
|
|
<span>Target Standar</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DailyWeightProgressionChart;
|