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