first commit
CI / lint-and-test (push) Canceled after 0s

This commit is contained in:
Alberto-Audrix committed 2026-07-28 08:55:05 +07:00
commit 32a36cceff
444 files changed
+67186

No files matched your search

+195
View File
@@ -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;