Files
dashboard/components/feedSack/FeedSackComparisonCharts.tsx
T
Alberto-Audrix 32a36cceff
CI / lint-and-test (push) Canceled after 0s
first commit
2026-07-28 08:55:05 +07:00

219 lines
6.9 KiB
TypeScript

import React, { useMemo } from 'react';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import {
FeedSackLogEntry,
ManualFeedSackEntry,
FeedSackColumnConfig,
} from '../../types/counting.ts';
interface FeedSackComparisonChartsProps {
iotData: FeedSackLogEntry[];
manualEntries: ManualFeedSackEntry[];
cycleStartDate: Date;
currentDay: number;
columnConfig: FeedSackColumnConfig[];
}
const FeedSackComparisonCharts: React.FC<FeedSackComparisonChartsProps> = ({
iotData,
manualEntries,
cycleStartDate,
currentDay,
columnConfig,
}) => {
// Aggregate data per camera
const chartDataPerCamera = useMemo(() => {
const cycleStart = new Date(cycleStartDate);
cycleStart.setHours(0, 0, 0, 0);
return columnConfig.map((col) => {
const dailyMap = new Map<string, { date: string; manual: number; iot: number }>();
// Process IOT data for this camera
iotData.forEach((log) => {
if (log.cameraName !== col.cameraName) return;
const logDate = new Date(log.timestamp);
logDate.setHours(0, 0, 0, 0);
const dayNumber = Math.floor(
(logDate.getTime() - cycleStart.getTime()) / (1000 * 60 * 60 * 24)
);
if (dayNumber < 0 || dayNumber > currentDay) return;
const dateKey = logDate.toISOString().split('T')[0];
if (!dailyMap.has(dateKey)) {
dailyMap.set(dateKey, {
date: logDate.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
}),
manual: 0,
iot: 0,
});
}
const entry = dailyMap.get(dateKey)!;
entry.iot += log.count;
});
// Process manual data for this camera
manualEntries.forEach((manual) => {
if (manual.cameraName !== col.cameraName) return;
const manualDate = new Date(manual.date);
if (isNaN(manualDate.getTime())) return;
manualDate.setHours(0, 0, 0, 0);
const dateKey = manualDate.toISOString().split('T')[0];
if (!dailyMap.has(dateKey)) {
dailyMap.set(dateKey, {
date: manualDate.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
}),
manual: 0,
iot: 0,
});
}
const entry = dailyMap.get(dateKey)!;
entry.manual = manual.count;
});
// Fill missing days
const filled = [];
for (let d = 0; d <= currentDay; d++) {
const date = new Date(cycleStart);
date.setDate(cycleStart.getDate() + d);
const dateKey = date.toISOString().split('T')[0];
if (dailyMap.has(dateKey)) {
filled.push(dailyMap.get(dateKey)!);
} else {
filled.push({
date: date.toLocaleDateString('id-ID', {
day: '2-digit',
month: 'short',
}),
manual: 0,
iot: 0,
});
}
}
return {
camera: col,
data: filled,
};
});
}, [iotData, manualEntries, cycleStartDate, currentDay, columnConfig]);
if (columnConfig.length === 0) {
return null;
}
return (
<div className="space-y-6">
<h2 className="text-xl font-bold text-gray-800">Perbandingan Manual vs IOT per Kamera</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{chartDataPerCamera.map(({ camera, data }) => {
const hasData = data.some((d) => d.manual > 0 || d.iot > 0);
return (
<div key={camera.id} className="bg-white p-6 rounded-xl shadow-sm">
<div className="mb-4">
<h3 className="text-lg font-bold" style={{ color: camera.color }}>
{camera.label}
</h3>
<p className="text-sm text-gray-500">{camera.cameraName}</p>
</div>
{!hasData ? (
<div className="h-64 flex items-center justify-center">
<div className="text-center text-gray-400">
<i className="fa-solid fa-chart-line text-4xl mb-2"></i>
<p className="text-sm">Belum ada data untuk kamera ini</p>
</div>
</div>
) : (
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e0e0e0" />
<XAxis dataKey="date" fontSize={12} tick={{ fill: '#6b7280' }} />
<YAxis
fontSize={12}
tick={{ fill: '#6b7280' }}
domain={[
(dataMin: number) => {
if (dataMin === 0) return 0;
return Math.floor(dataMin * 0.9);
},
(dataMax: number) => Math.ceil(dataMax * 1.1),
]}
tickFormatter={(value) => `${value}`}
/>
<Tooltip
contentStyle={{
backgroundColor: 'rgba(255, 255, 255, 0.95)',
border: '1px solid #ccc',
borderRadius: '8px',
padding: '10px',
}}
formatter={(value: number, name: string) => [
`${value} karung`,
name === 'manual' ? 'Manual' : 'IOT',
]}
/>
<Legend
wrapperStyle={{ paddingTop: '10px' }}
formatter={(value) => (value === 'manual' ? 'Manual' : 'IOT')}
/>
<Line
type="monotone"
dataKey="manual"
stroke="#3b82f6"
strokeWidth={2.5}
name="manual"
dot={{ r: 4, fill: '#3b82f6' }}
activeDot={{ r: 6 }}
connectNulls={false}
/>
<Line
type="monotone"
dataKey="iot"
stroke={camera.color}
strokeWidth={2.5}
name="iot"
dot={{ r: 4, fill: camera.color }}
activeDot={{ r: 6 }}
connectNulls={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
)}
</div>
);
})}
</div>
</div>
);
};
export default FeedSackComparisonCharts;