Files
dashboard-cpsp-executive/components/weight/DailyWeightProgressionChart.tsx
T
2026-09-08 15:21:23 +07:00

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;