Files
dashboard/components/Dashboard.tsx
T
Alberto-Audrix a8b49374f1
CI / lint-and-test (push) Canceled after 0s
update ai insight pake qwenn
2026-07-31 08:52:30 +07:00

253 lines
7.9 KiB
TypeScript

import React, { useLayoutEffect, useMemo, useRef, useState, useEffect } from 'react';
import KpiCard from './KpiCard.tsx';
import ChickenCountingDashboard from './counting/ChickenCountingDashboard.tsx';
import DailyWeightGainChart from './weight/DailyWeightGainChart.tsx';
import { AiInsightCard } from './AiInsightCard.tsx';
import FeedSaldoCard from './feedSack/FeedSaldoCard.tsx';
import KarungDituangTodayCard from './feedSack/KarungDituangTodayCard.tsx';
import { Kpi } from '../types.ts';
import { View } from '../types/common.ts';
import {
Kandang,
FeedSackColumnConfig,
DEFAULT_FEED_SACK_COLUMNS,
CycleFeedInitialBalance,
} from '../types/counting.ts';
import { performanceService } from '../services/performanceService.ts';
import { useAppContext } from '../context/AppContext.tsx';
import { apiClient } from '../services/apiService.ts';
import {
getUsageCameraNamesForKandang,
normalizeFeedSackColumnConfigs,
} from '../utils/feedSackMapping.ts';
interface DashboardProps {
kpis: Record<string, Kpi>;
setView: (view: View) => void;
selectedArea: string;
onAreaChange: (area: string) => void;
selectedKandangId: number | null;
onKandangChange: (id: number | null) => void;
kandangs: Kandang[];
}
const Dashboard: React.FC<DashboardProps> = ({
kpis,
setView,
selectedArea: _selectedArea,
onAreaChange: _onAreaChange,
selectedKandangId,
onKandangChange,
kandangs,
}) => {
const mountStart = useRef(performance.now());
const selectedKandangName = useMemo(
() =>
selectedKandangId ? kandangs.find((k) => k.id === selectedKandangId)?.name || null : null,
[kandangs, selectedKandangId]
);
const {
dailyWeightGainData,
cycleData,
isRefreshingWeightKpi,
feedSackHistory,
effectiveDate,
isShowingPreviousDayData,
} = useAppContext();
// Column config state for FeedSaldoCard (loaded from DB)
const [columnConfig, setColumnConfig] =
useState<FeedSackColumnConfig[]>(DEFAULT_FEED_SACK_COLUMNS);
const [initialBalance, setInitialBalance] = useState<CycleFeedInitialBalance | null>(null);
const feedSaldoEffectiveDate = useMemo(() => {
const cappedEffectiveDate = new Date(effectiveDate);
cappedEffectiveDate.setHours(0, 0, 0, 0);
if (!cycleData.startDate) {
return cappedEffectiveDate;
}
const cycleEndDate = cycleData.endDate
? new Date(cycleData.endDate)
: (() => {
const derivedEndDate = new Date(cycleData.startDate);
derivedEndDate.setDate(
derivedEndDate.getDate() + Math.max(0, (cycleData.totalDays || 1) - 1)
);
return derivedEndDate;
})();
cycleEndDate.setHours(0, 0, 0, 0);
return cappedEffectiveDate <= cycleEndDate ? cappedEffectiveDate : cycleEndDate;
}, [cycleData.endDate, cycleData.startDate, cycleData.totalDays, effectiveDate]);
const displayedKpis = useMemo(() => {
const karungKpi = kpis.karungDituang;
const reportedDay = Math.max(0, Math.round(kpis.umur?.value || 0));
if (!cycleData.startDate || !selectedKandangName || reportedDay <= 0) {
return kpis;
}
const usageCameraNames = getUsageCameraNamesForKandang(
columnConfig,
selectedKandangId,
selectedKandangName
);
if (usageCameraNames.length === 0) {
return kpis;
}
const cycleStart = new Date(cycleData.startDate);
cycleStart.setHours(0, 0, 0, 0);
const getLocalDateString = (d: Date) => {
const pad = (n: number) => n.toString().padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
};
const dailyTotals = new Map<string, number>();
for (const entry of feedSackHistory) {
if (!usageCameraNames.includes(entry.cameraName)) continue;
const entryDate = new Date(entry.timestamp);
entryDate.setHours(0, 0, 0, 0);
if (entryDate < cycleStart) continue;
const dateKey = getLocalDateString(entryDate);
if (!dateKey) continue;
dailyTotals.set(dateKey, (dailyTotals.get(dateKey) || 0) + (entry.count || 0));
}
const history: number[] = [];
for (let dayIndex = 0; dayIndex <= reportedDay; dayIndex += 1) {
const date = new Date(cycleStart);
date.setDate(date.getDate() + dayIndex);
const dateKey = getLocalDateString(date);
if (!dateKey) {
history.push(0);
continue;
}
history.push(dailyTotals.get(dateKey) || 0);
}
const targetDate = new Date(cycleStart);
targetDate.setDate(targetDate.getDate() + reportedDay);
const targetDateKey = getLocalDateString(targetDate);
const totalKarung = dailyTotals.get(targetDateKey) || 0;
return {
...kpis,
karungDituang: {
title: karungKpi?.title || 'Karung Dituang',
value: totalKarung,
unit: 'karung',
icon: karungKpi?.icon || '🛢️',
trend: history,
status: 'ok' as const,
targetView: 'feedSackCounting' as const,
},
};
}, [
cycleData.startDate,
columnConfig,
feedSackHistory,
kpis,
selectedKandangId,
selectedKandangName,
]);
// Load column config from database on mount
useEffect(() => {
apiClient
.getFeedSackColumnConfig()
.then((config: FeedSackColumnConfig[]) => {
if (config && config.length > 0) {
const normalizedConfig: FeedSackColumnConfig[] = normalizeFeedSackColumnConfigs(config);
setColumnConfig(normalizedConfig);
}
})
.catch((error: unknown) => {
console.error('Failed to load column config:', error);
});
}, []);
// Load initial balance for active cycle
useEffect(() => {
const fetchInitialBalance = async () => {
try {
const balance = await apiClient.getCycleFeedInitialBalance(cycleData.id);
setInitialBalance(balance);
} catch (error) {
console.error('Failed to fetch initial balance:', error);
setInitialBalance(null);
}
};
if (cycleData.id) {
fetchInitialBalance();
}
}, [cycleData.id]);
useLayoutEffect(() => {
const duration = performance.now() - mountStart.current;
performanceService.recordComponentTiming('Dashboard', duration);
}, []);
return (
<main className="px-4 sm:px-6 lg:px-8 pb-8 space-y-8">
{/* KPI Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-6 gap-6">
{/* FIX: Explicitly type 'kpi' as Kpi to resolve type errors. */}
{Object.entries(displayedKpis).map(([key, kpi]: [string, Kpi]) => {
// Show loading state for weight-related KPIs (use keys for type safety)
const isWeightRelated = ['weight', 'fcr', 'eef'].includes(key);
return (
<KpiCard
key={kpi.title}
{...kpi}
onClick={kpi.targetView ? () => setView(kpi.targetView!) : undefined}
isLoading={isWeightRelated && isRefreshingWeightKpi}
effectiveDate={effectiveDate}
showDateDisclaimer={isShowingPreviousDayData}
/>
);
})}
</div>
{/* AI Insight Card */}
<AiInsightCard kandangId={selectedKandangId ? selectedKandangId.toString() : null} />
{/* Karung Dituang Hari Ini Card */}
<KarungDituangTodayCard
karungKpi={displayedKpis.karungDituang}
effectiveDate={effectiveDate}
/>
{/* Saldo Karung Pakan Card */}
<FeedSaldoCard
iotData={feedSackHistory}
columnConfig={columnConfig}
startDate={new Date(cycleData.startDate)}
saldoAwal={initialBalance?.saldoAwal || 0}
effectiveDate={feedSaldoEffectiveDate}
/>
{/* Chicken Counting Dashboard */}
<ChickenCountingDashboard
selectedKandangId={selectedKandangId}
onKandangChange={onKandangChange}
kandangs={kandangs}
/>
{/* Daily Weight Gain Chart */}
<DailyWeightGainChart data={dailyWeightGainData} startDate={cycleData.startDate} />
</main>
);
};
export default Dashboard;