commit
32a36cceff
444 files changed
+67186
No files matched your search
@@ -0,0 +1,218 @@
|
||||
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;
|
||||
Reference in new issue
Block a user