196 lines
6.5 KiB
TypeScript
196 lines
6.5 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import {
|
|
FeedSackLogEntry,
|
|
ManualFeedSackEntry,
|
|
FeedSackColumnConfig,
|
|
} from '../../types/counting.ts';
|
|
import { useAppContext } from '../../context/AppContext.tsx';
|
|
import { formatNumber } from '../../utils/formatters.ts';
|
|
import { getEffectiveDate } from '../../utils/dateFilter.ts';
|
|
|
|
const StatItem: React.FC<{
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
manualValue: string;
|
|
iotValue: string;
|
|
colorOverride?: string;
|
|
}> = ({ icon, label, manualValue, iotValue, colorOverride }) => (
|
|
<div className="border border-gray-200 rounded-lg p-3 sm:p-4 hover:border-gray-300 transition-colors">
|
|
<div className="flex items-start gap-2 mb-3 min-w-0">
|
|
<div className="text-gray-500">{icon}</div>
|
|
<p className="min-w-0 truncate text-xs sm:text-sm font-medium text-gray-700" title={label}>
|
|
{label}
|
|
</p>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
{/* Manual */}
|
|
<div className="rounded-md bg-gray-50/70 p-3">
|
|
<div className="flex items-center gap-1.5 mb-1">
|
|
<i className="fa-solid fa-pen-to-square text-xs text-blue-500"></i>
|
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">Manual</p>
|
|
</div>
|
|
<p className="text-xl sm:text-2xl font-bold text-blue-600 leading-none">{manualValue}</p>
|
|
<p className="text-xs text-gray-400 mt-0.5">karung</p>
|
|
</div>
|
|
|
|
{/* IOT */}
|
|
<div className="rounded-md bg-gray-50/70 p-3">
|
|
<div className="flex items-center gap-1.5 mb-1">
|
|
<i className="fa-solid fa-camera text-xs text-gray-400"></i>
|
|
<p className="text-xs font-medium text-gray-500 uppercase tracking-wide">IOT</p>
|
|
</div>
|
|
<p
|
|
className="text-xl sm:text-2xl font-bold leading-none"
|
|
style={colorOverride ? { color: colorOverride } : { color: '#374151' }}
|
|
>
|
|
{iotValue}
|
|
</p>
|
|
<p className="text-xs text-gray-400 mt-0.5">karung</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
interface FeedStatsProps {
|
|
data: FeedSackLogEntry[];
|
|
manualEntries: ManualFeedSackEntry[];
|
|
columnConfig: FeedSackColumnConfig[];
|
|
effectiveDate?: Date;
|
|
}
|
|
|
|
const FeedStats: React.FC<FeedStatsProps> = ({
|
|
data,
|
|
manualEntries,
|
|
columnConfig,
|
|
effectiveDate,
|
|
}) => {
|
|
const { cycleData } = useAppContext();
|
|
const displayDate = effectiveDate ?? getEffectiveDate();
|
|
|
|
const todayLabel = useMemo(() => {
|
|
return new Intl.DateTimeFormat('id-ID', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
timeZone: 'Asia/Jakarta',
|
|
}).format(displayDate);
|
|
}, [displayDate]);
|
|
|
|
const totalCycleLabel = useMemo(() => {
|
|
const startDate = cycleData.startDate.toLocaleDateString('id-ID', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
});
|
|
const endDate = displayDate.toLocaleDateString('id-ID', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
});
|
|
return `Total (${startDate} - ${endDate})`;
|
|
}, [cycleData.startDate, displayDate]);
|
|
|
|
const columnStats = useMemo(() => {
|
|
return columnConfig.map((col) => {
|
|
const cameraIotData = data.filter((log) => log.cameraName === col.cameraName);
|
|
const cameraManualData = manualEntries.filter((entry) => entry.cameraName === col.cameraName);
|
|
|
|
const calculateIotCount = (dataSet: FeedSackLogEntry[], days: number | 'cycle') => {
|
|
let startDate: Date;
|
|
|
|
if (days === 'cycle') {
|
|
startDate = new Date(cycleData.startDate);
|
|
} else {
|
|
startDate = new Date(displayDate);
|
|
startDate.setDate(displayDate.getDate() - (days - 1));
|
|
}
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
return dataSet
|
|
.filter((log) => log.timestamp >= startDate)
|
|
.reduce((sum, log) => sum + log.count, 0);
|
|
};
|
|
|
|
const calculateManualCount = (dataSet: ManualFeedSackEntry[], days: number | 'cycle') => {
|
|
let startDate: Date;
|
|
|
|
if (days === 'cycle') {
|
|
startDate = new Date(cycleData.startDate);
|
|
} else {
|
|
startDate = new Date(displayDate);
|
|
startDate.setDate(displayDate.getDate() - (days - 1));
|
|
}
|
|
startDate.setHours(0, 0, 0, 0);
|
|
|
|
return dataSet
|
|
.filter((entry) => {
|
|
const entryDate = new Date(entry.date);
|
|
return !isNaN(entryDate.getTime()) && entryDate >= startDate;
|
|
})
|
|
.reduce((sum, entry) => sum + entry.count, 0);
|
|
};
|
|
|
|
return {
|
|
...col,
|
|
todayManual: calculateManualCount(cameraManualData, 1),
|
|
todayIot: calculateIotCount(cameraIotData, 1),
|
|
weekManual: calculateManualCount(cameraManualData, 'cycle'),
|
|
weekIot: calculateIotCount(cameraIotData, 'cycle'),
|
|
};
|
|
});
|
|
}, [data, manualEntries, columnConfig, cycleData, displayDate]);
|
|
|
|
const gridCols =
|
|
columnStats.length <= 2
|
|
? 'md:grid-cols-2'
|
|
: columnStats.length <= 3
|
|
? 'md:grid-cols-3'
|
|
: 'md:grid-cols-2 lg:grid-cols-4';
|
|
|
|
return (
|
|
<div className="bg-white p-4 sm:p-6 rounded-xl shadow-sm">
|
|
<div className={`grid grid-cols-1 ${gridCols} gap-4 sm:gap-6`}>
|
|
{columnStats.map((stat) => (
|
|
<div key={stat.id}>
|
|
<h3
|
|
className="font-semibold text-sm sm:text-base leading-snug mb-2 min-h-[2.75rem] sm:min-h-[3.25rem] min-w-0"
|
|
style={{
|
|
color: stat.color,
|
|
display: '-webkit-box',
|
|
WebkitBoxOrient: 'vertical',
|
|
WebkitLineClamp: 2,
|
|
overflow: 'hidden',
|
|
}}
|
|
title={stat.label}
|
|
>
|
|
{stat.label}
|
|
</h3>
|
|
<p
|
|
className="text-[11px] sm:text-xs text-gray-500 mb-3 truncate"
|
|
title={stat.cameraName}
|
|
>
|
|
{stat.cameraName}
|
|
</p>
|
|
<div className="space-y-3">
|
|
<StatItem
|
|
icon={<i className="fa-solid fa-calendar-day"></i>}
|
|
label={todayLabel}
|
|
manualValue={formatNumber(stat.todayManual)}
|
|
iotValue={formatNumber(stat.todayIot)}
|
|
colorOverride={stat.color}
|
|
/>
|
|
<StatItem
|
|
icon={<i className="fa-solid fa-calendar-week"></i>}
|
|
label={totalCycleLabel}
|
|
manualValue={formatNumber(stat.weekManual)}
|
|
iotValue={formatNumber(stat.weekIot)}
|
|
colorOverride={stat.color}
|
|
/>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FeedStats;
|