Files
dashboard/components/ChickenCountingPage.tsx
T

271 lines
9.5 KiB
TypeScript

import React, { useState, useEffect, useMemo, useCallback } from 'react';
import CombinedTrendChart from './counting/CombinedTrendChart.tsx';
import ChickenCountingAiInsight from './counting/ChickenCountingAiInsight.tsx';
import { CalculatorIcon } from './icons/Icons.tsx';
import { useAppContext } from '../context/AppContext.tsx';
import { apiClient } from '../services/apiService.ts';
import { Kandang } 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;
mortality?: number;
}
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',
];
const ChickenCountingPage: React.FC = () => {
const { cycleData } = useAppContext();
const [chickenCountHistory, setChickenCountHistory] = useState<ChickenCountDataPoint[]>([]);
const [populationTrendHistory, setPopulationTrendHistory] = useState<PopulationDataPoint[]>([]);
const [kandangs, setKandangs] = useState<Kandang[]>([]);
const [perKandangData, setPerKandangData] = useState<KandangPopulationData[]>([]);
// Load kandangs
useEffect(() => {
const loadKandangs = async () => {
try {
const data = await apiClient.getKandangsByCycle(cycleData.id);
setKandangs(data);
} catch (error) {
console.error('Failed to load kandangs:', error);
}
};
loadKandangs();
}, [cycleData]);
// 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]);
// 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 lastCycleDay = Math.max(0, (cycleData.totalDays || 1) - 1);
const currentDayOfCycle = Math.min(
Math.floor((today.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)),
lastCycleDay
);
const recordMap = new Map<number, MortalitySourceRecord>();
sortedRecords.forEach((record) => {
recordMap.set(record.day, record);
});
let cumulativeMortality = 0;
let cumulativePanen = 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);
cumulativePanen += record.panen || 0;
}
const calculatedPopulation = initPop - cumulativeMortality - cumulativePanen;
const populationValue = record?.population || calculatedPopulation;
populations.push({ date: formattedDate, population: populationValue });
}
return { chickenCounts, populations };
},
[cycleData.startDate, cycleData.totalDays]
);
// Fetch all data: total + per-kandang
useEffect(() => {
const fetchData = async () => {
try {
// Total across all kandangs
const allRecords = await apiClient.getMortalityTotals(cycleData.id);
const { chickenCounts, populations } = buildPopulationData(
allRecords,
totalInitialPopulation
);
setChickenCountHistory(chickenCounts);
setPopulationTrendHistory(populations);
// Per-kandang data
if (kandangs.length > 0) {
const perKandang: KandangPopulationData[] = [];
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);
const kInitPop = k.docInCount || 0;
const { populations: kPop } = buildPopulationData(kRecords, kInitPop);
const mortality = kRecords.reduce(
(sum: number, record: { mortalityCount?: number; afkir?: number }) =>
sum + (record.mortalityCount || 0) + (record.afkir || 0),
0
);
perKandang.push({
name: k.name,
data: kPop,
color: KANDANG_COLORS[i % KANDANG_COLORS.length] ?? '#6b7280',
mortality,
});
} catch {
// Skip if error fetching individual kandang
}
}
setPerKandangData(perKandang);
} else {
setPerKandangData([]);
}
} catch (error) {
console.error('Failed to fetch mortality records:', error);
}
};
fetchData();
}, [cycleData, kandangs, totalInitialPopulation, buildPopulationData]);
// Removed unused kandangInsights and currentDay hooks
return (
<main className="px-4 sm:px-6 lg:px-8 pb-8">
<div className="max-w-7xl mx-auto">
<div className="flex items-center mb-6">
<CalculatorIcon className="text-3xl text-red-600 mr-4" />
<div>
<h1 className="text-3xl font-bold text-gray-800">Dashboard Penghitungan Ayam</h1>
<p className="mt-1 text-gray-600">
Pemantauan populasi dari berbagai kamera secara real-time.
</p>
</div>
</div>
{/* AI Insight */}
<div className="mb-8">
<ChickenCountingAiInsight
cycleId={cycleData.id}
currentDay={cycleData.currentDay}
totalDays={cycleData.totalDays || 1}
totalInitialPopulation={totalInitialPopulation}
populationTrend={populationTrendHistory as never}
kandangInsights={[]}
/>
</div>
{kandangs.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{/* Total card */}
<div className="bg-white rounded-xl shadow-sm p-4 border-l-4 border-l-red-600">
<div className="flex items-center gap-2 mb-1">
<i className="fa-solid fa-layer-group text-red-600"></i>
<p className="text-sm font-semibold text-gray-700">Total Semua Kandang</p>
</div>
<p className="text-2xl font-bold text-gray-800">
{(
populationTrendHistory[populationTrendHistory.length - 1]?.population ??
totalInitialPopulation
).toLocaleString()}
<span className="text-sm font-normal text-gray-500 ml-1">ekor</span>
</p>
<p className="text-xs text-gray-500">
DOC: {totalInitialPopulation.toLocaleString()}
</p>
</div>
{/* Individual kandang cards */}
{kandangs.map((k, i) => {
const kData = perKandangData.find((d) => d.name === k.name);
const lastPop = kData?.data[kData.data.length - 1]?.population;
const mortality = kData?.mortality || 0;
return (
<div
key={k.id}
className="bg-white rounded-xl shadow-sm p-4 border-l-4"
style={{ borderLeftColor: KANDANG_COLORS[i % KANDANG_COLORS.length] }}
>
<div className="flex items-center gap-2 mb-1">
<i
className="fa-solid fa-warehouse"
style={{ color: KANDANG_COLORS[i % KANDANG_COLORS.length] }}
></i>
<p className="text-sm font-semibold text-gray-700">{k.name}</p>
</div>
<p className="text-2xl font-bold text-gray-800">
{lastPop?.toLocaleString() || '-'}
<span className="text-sm font-normal text-gray-500 ml-1">ekor</span>
</p>
<div className="flex justify-between text-xs text-gray-500 mt-1">
<span>DOC: {(k.docInCount || 0).toLocaleString()}</span>
{mortality > 0 && (
<span className="text-red-500">Mortalitas: {mortality.toLocaleString()}</span>
)}
</div>
</div>
);
})}
</div>
)}
<div className="space-y-8">
<CombinedTrendChart
populationData={populationTrendHistory}
chickenCountData={chickenCountHistory}
kandangs={kandangs}
cycleData={cycleData}
apiClient={apiClient}
/>
</div>
</div>
</main>
);
};
export default ChickenCountingPage;