122 lines
4.0 KiB
TypeScript
122 lines
4.0 KiB
TypeScript
import React, { useMemo, useEffect, useState } from 'react';
|
|
import { LiveCount } from '../../types.ts';
|
|
import { CalculatorIcon } from '../icons/Icons.tsx';
|
|
import { useAppContext } from '../../context/AppContext.tsx';
|
|
import { apiClient } from '../../services/apiService.ts';
|
|
import { formatNumber } from '../../utils/formatters.ts';
|
|
|
|
const StatItem: React.FC<{
|
|
icon: React.ReactNode;
|
|
label: string;
|
|
value: string;
|
|
valueColor?: string;
|
|
}> = ({ icon, label, value, valueColor = 'text-gray-900' }) => (
|
|
<div className="flex items-center">
|
|
<div className="text-xl text-gray-500 w-8">{icon}</div>
|
|
<div>
|
|
<p className="text-sm text-gray-600">{label}</p>
|
|
<p className={`text-lg font-bold ${valueColor}`}>{value}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
interface CountingStatsProps {
|
|
liveCounts: LiveCount[];
|
|
kandangId?: number | null;
|
|
initialPop?: number;
|
|
}
|
|
|
|
const CountingStats: React.FC<CountingStatsProps> = ({ liveCounts, kandangId, initialPop }) => {
|
|
const { cycleData, apiMode } = useAppContext();
|
|
const [currentPopulation, setCurrentPopulation] = useState<number | null>(null);
|
|
|
|
// Fetch current population from mortality data
|
|
useEffect(() => {
|
|
const fetchPopulation = async () => {
|
|
try {
|
|
const dbRecords = await apiClient.getMortalityRecords(cycleData.id, kandangId ?? undefined);
|
|
const initPop = initialPop ?? cycleData.docInCount ?? 20000;
|
|
|
|
// Calculate total mortality
|
|
const totalMortality = dbRecords.reduce(
|
|
(sum: number, record: { mortalityCount?: number; afkir?: number }) =>
|
|
sum + (record.mortalityCount || 0) + (record.afkir || 0),
|
|
0
|
|
);
|
|
const population = initPop - totalMortality;
|
|
|
|
setCurrentPopulation(population);
|
|
} catch (error) {
|
|
console.error('Failed to fetch population:', error);
|
|
setCurrentPopulation(null);
|
|
}
|
|
};
|
|
|
|
if (apiMode === 'LIVE') {
|
|
fetchPopulation();
|
|
}
|
|
}, [cycleData, apiMode, kandangId, initialPop]);
|
|
|
|
const { totalCount, activeCameras } = useMemo(() => {
|
|
const onlineCams = liveCounts.filter((cam) => cam.status === 'online');
|
|
const total = onlineCams.reduce((sum, cam) => sum + cam.count, 0);
|
|
return {
|
|
totalCount: total,
|
|
activeCameras: onlineCams.length,
|
|
};
|
|
}, [liveCounts]);
|
|
|
|
const totalCameras = liveCounts.length;
|
|
|
|
// In LIVE mode, use real population data and show N/A for unimplemented features
|
|
const displayTotalCount =
|
|
apiMode === 'LIVE' && currentPopulation !== null
|
|
? formatNumber(currentPopulation)
|
|
: apiMode === 'DEMO'
|
|
? formatNumber(totalCount)
|
|
: 'N/A';
|
|
|
|
const displayCameras = apiMode === 'DEMO' ? `${activeCameras} / ${totalCameras}` : 'N/A';
|
|
|
|
const displayAccuracy = apiMode === 'DEMO' ? '98.2%' : 'N/A';
|
|
const displayLastUpdate = apiMode === 'DEMO' ? 'Baru saja' : 'N/A';
|
|
|
|
return (
|
|
<div className="bg-white p-6 rounded-xl shadow-sm">
|
|
<h2 className="text-xl font-bold mb-4">Statistik Agregat</h2>
|
|
<div className="space-y-4">
|
|
<StatItem
|
|
icon={<CalculatorIcon />}
|
|
label="Total Populasi Terhitung"
|
|
value={`${displayTotalCount} ekor`}
|
|
valueColor={apiMode === 'LIVE' ? 'text-red-600' : 'text-red-600'}
|
|
/>
|
|
<StatItem
|
|
icon={<i className="fa-solid fa-video"></i>}
|
|
label="Kamera Aktif (Menghitung)"
|
|
value={displayCameras}
|
|
valueColor={
|
|
apiMode === 'DEMO' && activeCameras === totalCameras
|
|
? 'text-green-600'
|
|
: 'text-gray-500'
|
|
}
|
|
/>
|
|
<StatItem
|
|
icon={<i className="fa-solid fa-crosshairs"></i>}
|
|
label="Akurasi Model AI (Avg)"
|
|
value={displayAccuracy}
|
|
valueColor={apiMode === 'DEMO' ? 'text-green-600' : 'text-gray-500'}
|
|
/>
|
|
<StatItem
|
|
icon={<i className="fa-solid fa-clock"></i>}
|
|
label="Pembaruan Terakhir"
|
|
value={displayLastUpdate}
|
|
valueColor="text-gray-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CountingStats;
|