import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, } from 'recharts'; import { api, errorMessage } from '../services/apiClient.ts'; import type { AIInsight, KPI } from '../types/api.ts'; import { useFarm } from '../context/FarmContext.tsx'; import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx'; import { formatNumber, formatRatio } from '../utils/format.ts'; type EefRow = { day: number; chickenLifePercentage: number | null; averageHarvestBw: number | null; averageHarvestDay: number | null; fcr: number | null; eef: number | null; }; type TrendPoint = { day: number; eef: number | null }; type SupportPoint = { day: number; chickenLifePercentage: number | null; averageHarvestBw: number | null; averageHarvestDay: number | null; fcr: number | null; }; const formatThreeDecimal = (value: number | null): string => value == null ? 'N/A' : formatRatio(value, 3); const formatNum = (value: number | null | undefined, maxDecimals = 0): string => value == null || !Number.isFinite(Number(value)) ? 'N/A' : formatNumber(Number(value), maxDecimals, 0, true); const StatCard: React.FC<{ title: string; value: string; unit: string }> = ({ title, value, unit, }) => (

{title}

{value} {unit}
); const EefTrendChart: React.FC<{ data: TrendPoint[] }> = ({ data }) => (

Grafik EEF

{data.length === 0 ? (
Tidak ada data EEF tersedia.
) : ( formatNum(Number(value), 0)} width={60} domain={[0, 'dataMax + 20']} label={{ value: 'EEF', angle: -90, position: 'insideLeft' }} /> [formatThreeDecimal(Number(value)), 'EEF']} labelFormatter={(label) => `Hari ${label}`} /> )}
); const SupportingMetricChart: React.FC<{ title: string; data: SupportPoint[]; dataKey: keyof Omit; unit: string; stroke: string; maximumFractionDigits?: number; valueFormatter?: (value: number) => string; }> = ({ title, data, dataKey, unit, stroke, maximumFractionDigits = 0, valueFormatter }) => (

{title}

{data.length === 0 ? (
Tidak ada data tersedia.
) : ( valueFormatter ? valueFormatter(Number(value)) : formatNum(Number(value), maximumFractionDigits) } width={70} /> { const formatted = valueFormatter ? valueFormatter(Number(value)) : formatNum(Number(value), maximumFractionDigits); return [`${formatted}${unit ? ` ${unit}` : ''}`, title]; }} labelFormatter={(label) => `Hari ${label}`} /> )}
); const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { const { selectedCycle } = useFarm(); const [rows, setRows] = useState([]); const [insights, setInsights] = useState([]); const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const loadData = useCallback(async () => { if (!selectedCycle) return; setError(null); try { const [kpiRows, insightRows] = await Promise.all([ api.kpis.list({ cycle_id: selectedCycle.id }), api.insights.list({ cycle_id: selectedCycle.id }), ]); setRows(kpiRows); setInsights(insightRows); setLastUpdated(new Date()); } catch (err) { setError(errorMessage(err)); } }, [selectedCycle]); useEffect(() => { if (!selectedCycle) { setRows([]); setInsights([]); setLastUpdated(null); return; } let cancelled = false; (async () => { setLoading(true); try { await loadData(); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [selectedCycle, loadData]); const handleRefresh = async () => { setRefreshing(true); try { await loadData(); } finally { setRefreshing(false); } }; const eefRows = useMemo( () => [...rows] .sort((a, b) => a.age - b.age || a.date.localeCompare(b.date)) .map((row) => ({ day: row.age, chickenLifePercentage: row.chicken_life_percentage, averageHarvestBw: row.average_harvest_weight > 0 ? row.average_harvest_weight : null, averageHarvestDay: row.average_harvest_day > 0 ? row.average_harvest_day : null, fcr: row.fcr > 0 ? row.fcr : null, eef: row.eef > 0 ? row.eef : null, })), [rows] ); const trendData = useMemo( () => eefRows.map((row) => ({ day: row.day, eef: row.eef })), [eefRows] ); const supportData = useMemo( () => eefRows.map((row) => ({ day: row.day, chickenLifePercentage: row.chickenLifePercentage, averageHarvestBw: row.averageHarvestBw, averageHarvestDay: row.averageHarvestDay, fcr: row.fcr, })), [eefRows] ); const getLatestValid = (key: K): EefRow[K] | null => { for (let i = eefRows.length - 1; i >= 0; i--) { const val = eefRows[i]?.[key]; if (val !== null && val !== undefined && val !== 0 && String(val) !== '') { return val; } } return null; }; const latestEef = getLatestValid('eef'); const eefInsights = useMemo(() => { const matched = insights.filter((item) => { const text = `${item.section ?? ''} ${item.alert ?? ''} ${item.insight_text ?? ''}`.toLowerCase(); return ( text.includes('eef') || text.includes('fcr') || text.includes('hidup') || text.includes('live') || text.includes('panen') || text.includes('bw') ); }); return (matched.length > 0 ? matched : insights).slice(0, 4); }, [insights]); if (!selectedCycle) return ; return (
} />
{error && (
{error}
)}

AI Insight EEF

{loading ? ( ) : eefInsights.length === 0 ? ( Belum ada insight untuk siklus ini. ) : (
{eefInsights.map((item) => (

{item.section}

{item.alert || 'Insight'}

{item.session}

{item.insight_text}

))}
)}
{loading ? ( ) : (

Data Pendukung EEF

{lastUpdated && (

Update:{' '} {lastUpdated.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}

)}

Grafik Data Pendukung

formatThreeDecimal(value)} />

Tabel Data Pendukung EEF

{eefRows.map((row) => ( ))} {eefRows.length === 0 && ( )}
Umur Persentase Ayam Hidup BW Rata-rata Panen Umur Rata-rata Panen FCR EEF
Hari {row.day} {formatNum(row.chickenLifePercentage, 3)} {row.chickenLifePercentage != null ? '%' : ''} {formatNum(row.averageHarvestBw, 2)} {row.averageHarvestBw != null ? ' g' : ''} {formatNum(row.averageHarvestDay, 2)} {formatThreeDecimal(row.fcr)} {formatThreeDecimal(row.eef)}
Tidak ada data EEF tersedia.
)}
); }; export default EefDashboardPage;