Files
dashboard/components/counting/ChickenCountingDashboard.tsx
T
Alberto-Audrix 32a36cceff
CI / lint-and-test (push) Canceled after 0s
first commit
2026-07-28 08:55:05 +07:00

366 lines
13 KiB
TypeScript

import React, { useState, useEffect, useMemo, useCallback } from 'react';
import CombinedTrendChart from './CombinedTrendChart.tsx';
import { useAppContext } from '../../context/AppContext.tsx';
import { apiClient } from '../../services/apiService.ts';
import { Kandang, MortalityRecord } from '../../types/counting.ts';
interface ChickenCountDataPoint {
date: string;
count: number | null;
}
interface PopulationDataPoint {
date: string;
population: number;
}
interface KandangPopulationData {
name: string;
data: PopulationDataPoint[];
color: string;
}
interface MortalitySourceRecord {
day: number;
mortalityCount?: number;
afkir?: number;
panen?: number;
population?: number;
chickenCount?: number;
}
const KANDANG_COLORS = [
'#ef4444',
'#3b82f6',
'#10b981',
'#f59e0b',
'#8b5cf6',
'#ec4899',
'#14b8a6',
'#f97316',
];
interface ChickenCountingDashboardProps {
selectedKandangId: number | null;
onKandangChange: (kandangId: number | null) => void;
kandangs: Kandang[];
}
const ChickenCountingDashboard: React.FC<ChickenCountingDashboardProps> = ({
selectedKandangId,
onKandangChange,
kandangs,
}) => {
const { cycleData } = useAppContext();
const [chickenCountHistory, setChickenCountHistory] = useState<ChickenCountDataPoint[]>([]);
const [populationTrendHistory, setPopulationTrendHistory] = useState<PopulationDataPoint[]>([]);
const [_perKandangData, setPerKandangData] = useState<KandangPopulationData[]>([]);
const [mortalityRecords, setMortalityRecords] = useState<MortalityRecord[]>([]);
const [perKandangMortalityRecords, setPerKandangMortalityRecords] = useState<
Map<number, MortalityRecord[]>
>(new Map());
const [iotCurrentPopulation, setIotCurrentPopulation] = useState<number | null>(null);
// Fetch latest IOT Chicken Counting data (for selected kandang or all kandangs)
useEffect(() => {
const fetchLatestChickenCount = async () => {
try {
// Calculate date range for fetching latest data
const today = new Date();
const startDate = new Date(cycleData.startDate);
startDate.setHours(0, 0, 0, 0);
today.setHours(23, 59, 59, 999);
const startDateStr = startDate.toISOString().split('T')[0];
const todayStr = today.toISOString().split('T')[0];
// Fetch chicken counting data for selected kandang or all kandangs
const chickenCounts = await apiClient.getChickenCounting(
startDateStr,
todayStr,
selectedKandangId || undefined // Filter by kandang if one is selected
);
if (chickenCounts && chickenCounts.length > 0) {
// Get the most recent chicken count
const sortedCounts = [...chickenCounts].sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
);
const latestCount = sortedCounts[0].totalCount;
setIotCurrentPopulation(latestCount);
} else {
// No IOT data available
setIotCurrentPopulation(null);
}
} catch (error) {
console.error('Failed to fetch latest chicken counting data:', error);
setIotCurrentPopulation(null);
}
};
fetchLatestChickenCount();
// Refresh IOT data every 2 minutes
const interval = setInterval(fetchLatestChickenCount, 2 * 60 * 1000);
return () => clearInterval(interval);
}, [cycleData.startDate, selectedKandangId, kandangs]);
// Total initial population across all kandangs
const totalInitialPopulation = useMemo(() => {
const total = kandangs.reduce((sum, k) => sum + (k.docInCount || 0), 0);
return total > 0 ? total : cycleData.docInCount || 20000;
}, [kandangs, cycleData]);
// Calculate statistics for the 5 boxes based on selected kandang
const stats = useMemo(() => {
let rawRecords: MortalitySourceRecord[];
let initialPop: number;
if (selectedKandangId === null) {
// Show all kandangs combined
rawRecords = mortalityRecords;
initialPop = totalInitialPopulation;
} else {
// Show specific kandang
rawRecords = perKandangMortalityRecords.get(selectedKandangId) || [];
const selectedKandang = kandangs.find((k) => k.id === selectedKandangId);
initialPop = selectedKandang?.docInCount || 0;
}
// Calculate cumulative mortality and stock from raw records (same logic as DataManagementPage)
let cumulativeMortality = 0;
let cumulativeAfkir = 0;
let cumulativePanen = 0;
rawRecords.forEach((record) => {
cumulativeMortality += record.mortalityCount || 0;
cumulativeAfkir += record.afkir || 0;
cumulativePanen += record.panen || 0;
});
const totalMortality = cumulativeMortality + cumulativeAfkir;
const totalPanen = cumulativePanen;
const mortalityRate =
initialPop > 0 ? ((totalMortality / initialPop) * 100).toFixed(2) : '0.00';
// Use IOT Chicken Counting data for all views (Semua Kandang or individual kandang)
// Fall back to calculated population only when IOT data is unavailable
let currentPopulation: number;
if (iotCurrentPopulation !== null) {
// Use latest IOT chicken count detection
currentPopulation = iotCurrentPopulation;
} else {
// Calculate from mortality records when IOT data unavailable
currentPopulation = initialPop - cumulativeMortality - cumulativeAfkir - cumulativePanen;
}
return { totalMortality, currentPopulation, totalPanen, mortalityRate, initialPop };
}, [
mortalityRecords,
totalInitialPopulation,
selectedKandangId,
perKandangMortalityRecords,
kandangs,
iotCurrentPopulation,
]);
// Helper: build population data from records
const buildPopulationData = useCallback(
(records: MortalitySourceRecord[], initPop: number) => {
const chickenCounts: ChickenCountDataPoint[] = [];
const populations: PopulationDataPoint[] = [];
const sortedRecords = [...records].sort((a, b) => a.day - b.day);
const today = new Date();
const startDate = new Date(cycleData.startDate);
today.setHours(0, 0, 0, 0);
startDate.setHours(0, 0, 0, 0);
const currentDayOfCycle = Math.floor(
(today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
);
const recordMap = new Map<number, MortalitySourceRecord>();
sortedRecords.forEach((record) => {
recordMap.set(record.day, record);
});
let cumulativeMortality = 0;
for (let day = 0; day <= currentDayOfCycle; day++) {
const date = new Date(cycleData.startDate);
date.setDate(date.getDate() + day);
const formattedDate = date.toLocaleDateString('id-ID', { day: '2-digit', month: 'short' });
const record = recordMap.get(day);
if (record && record.chickenCount) {
chickenCounts.push({ date: formattedDate, count: record.chickenCount });
} else {
chickenCounts.push({ date: formattedDate, count: null });
}
if (record) {
cumulativeMortality += (record.mortalityCount || 0) + (record.afkir || 0);
}
const calculatedPopulation = initPop - cumulativeMortality;
const populationValue = record?.population || calculatedPopulation;
populations.push({ date: formattedDate, population: populationValue });
}
return { chickenCounts, populations };
},
[cycleData.startDate]
);
// Fetch all data: total + per-kandang
useEffect(() => {
const fetchData = async () => {
try {
// Total across all kandangs - use getMortalityTotals for aggregated data
const allRecords = await apiClient.getMortalityTotals(cycleData.id);
setMortalityRecords(allRecords); // Store for statistics calculation
const { chickenCounts, populations } = buildPopulationData(
allRecords,
totalInitialPopulation
);
setChickenCountHistory(chickenCounts);
setPopulationTrendHistory(populations);
// Per-kandang data
if (kandangs.length > 0) {
const perKandang: KandangPopulationData[] = [];
const mortalityMap = new Map<number, MortalityRecord[]>();
for (let i = 0; i < kandangs.length; i++) {
const k = kandangs[i];
if (!k) continue;
try {
const kRecords = await apiClient.getMortalityRecords(cycleData.id, k.id);
mortalityMap.set(k.id, kRecords); // Store mortality records for each kandang
const kInitPop = k.docInCount || 0;
const { populations: kPop } = buildPopulationData(kRecords, kInitPop);
perKandang.push({
name: k.name || `Kandang ${i + 1}`,
data: kPop,
color: KANDANG_COLORS[i % KANDANG_COLORS.length] || '#ef4444',
});
} catch {
// Skip if error fetching individual kandang
}
}
setPerKandangData(perKandang);
setPerKandangMortalityRecords(mortalityMap);
} else {
setPerKandangData([]);
setPerKandangMortalityRecords(new Map());
}
} catch (error) {
console.error('Failed to fetch mortality records:', error);
}
};
fetchData();
}, [cycleData, kandangs, totalInitialPopulation, buildPopulationData]);
return (
<div className="bg-white rounded-xl shadow-sm p-6">
{/* Header */}
<div className="flex items-center mb-6">
<i className="fa-solid fa-calculator text-3xl text-red-600 mr-4"></i>
<div>
<h2 className="text-2xl font-bold text-gray-800">Dashboard Penghitungan Ayam</h2>
<p className="text-sm text-gray-600">
Pemantauan populasi dari berbagai kandang secara real-time
</p>
</div>
</div>
{/* Statistics Summary Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
{/* Populasi Awal */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-blue-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Populasi Awal</p>
<p className="text-xl font-bold text-gray-800">{stats.initialPop.toLocaleString()}</p>
</div>
<i className="fa-solid fa-egg text-blue-500 text-2xl"></i>
</div>
</div>
{/* Populasi Saat Ini */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-green-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Populasi Saat Ini</p>
<p className="text-xl font-bold text-green-600">
{stats.currentPopulation.toLocaleString()}
</p>
</div>
<i className="fa-solid fa-drumstick-bite text-green-500 text-2xl"></i>
</div>
</div>
{/* Total Mortalitas */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-red-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Total Mortalitas</p>
<p className="text-xl font-bold text-red-600">
{stats.totalMortality.toLocaleString()}
</p>
</div>
<i className="fa-solid fa-heart-crack text-red-500 text-2xl"></i>
</div>
</div>
{/* Total Panen */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-purple-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Total Panen</p>
<p className="text-xl font-bold text-purple-700">
{stats.totalPanen.toLocaleString()}
</p>
</div>
<i className="fa-solid fa-box text-purple-500 text-2xl"></i>
</div>
</div>
{/* Tingkat Mortalitas */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-orange-500">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-600 mb-1">Tingkat Mortalitas</p>
<p className="text-xl font-bold text-orange-600">{stats.mortalityRate}%</p>
</div>
<i className="fa-solid fa-chart-line text-orange-500 text-2xl"></i>
</div>
</div>
</div>
{/* Chart */}
<CombinedTrendChart
populationData={populationTrendHistory}
chickenCountData={chickenCountHistory}
kandangs={kandangs.map((k) => ({
id: k.id,
name: k.name || '',
docInCount: k.docInCount || 0,
}))}
cycleData={cycleData}
apiClient={apiClient}
selectedKandangId={selectedKandangId}
onKandangChange={onKandangChange}
hideKandangSelector={true}
/>
</div>
);
};
export default ChickenCountingDashboard;