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 { ChickenWeight, 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'; import { buildFcrInsightContext } from '../utils/insightContextBuilders.ts'; import { PageAiInsight } from './shared/PageAiInsight.tsx'; type FcrRow = { day: number; feedTotal: number; iotWeight: number | null; chickenLife: number; harvestWeightKg: number; fcr: number | null; }; type TrendPoint = { day: number; fcr: number | null }; type SupportPoint = { day: number; feedTotal: number; iotWeight: number | null; chickenLife: number; harvestWeightKg: number; }; const formatFcr = (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 FcrTrendChart: React.FC<{ data: TrendPoint[] }> = ({ data }) => (

Grafik FCR

{data.length === 0 ? (
Tidak ada data FCR tersedia.
) : ( [formatFcr(Number(value)), 'FCR']} labelFormatter={(label) => `Hari ${label}`} /> )}
); const SupportingMetricChart: React.FC<{ title: string; data: SupportPoint[]; dataKey: keyof Omit; unit: string; stroke: string; maximumFractionDigits?: number; }> = ({ title, data, dataKey, unit, stroke, maximumFractionDigits = 0 }) => (

{title}

{data.length === 0 ? (
Tidak ada data tersedia.
) : ( formatNum(Number(value), maximumFractionDigits)} width={70} /> [ `${formatNum(Number(value), maximumFractionDigits)} ${unit}`, title, ]} labelFormatter={(label) => `Hari ${label}`} /> )}
); const FcrDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { const { selectedCycle, selectedKandang } = useFarm(); const [rows, setRows] = useState([]); const [weightRows, setWeightRows] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const loadData = useCallback(async () => { if (!selectedCycle) return; setError(null); try { const [kpiRows, weightList] = await Promise.all([ api.kpis.list({ cycle_id: selectedCycle.id }), api.weights.list({ cycle_id: selectedCycle.id }), ]); setRows(kpiRows); setWeightRows(weightList); setLastUpdated(new Date()); } catch (err) { setError(errorMessage(err)); } }, [selectedCycle]); useEffect(() => { if (!selectedCycle) { setRows([]); setWeightRows([]); setLastUpdated(null); return; } let cancelled = false; (async () => { setLoading(true); try { await loadData(); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [selectedCycle, loadData]); const weightByAge = useMemo(() => { const map = new Map(); for (const weight of weightRows) { if (weight.average_weight > 0 && !map.has(weight.age)) { map.set(weight.age, weight.average_weight); } } return map; }, [weightRows]); const fcrRows = useMemo( () => [...rows] .sort((a, b) => a.age - b.age || a.date.localeCompare(b.date)) .map((row) => ({ day: row.age, feedTotal: row.feed_total, iotWeight: weightByAge.get(row.age) ?? null, chickenLife: row.chicken_life, harvestWeightKg: row.harvest_weight_total / 1000, fcr: row.fcr > 0 ? row.fcr : null, })), [rows, weightByAge] ); const trendData = useMemo( () => fcrRows.map((row) => ({ day: row.day, fcr: row.fcr })), [fcrRows] ); const supportData = useMemo( () => fcrRows.map((row) => ({ day: row.day, feedTotal: row.feedTotal, iotWeight: row.iotWeight, chickenLife: row.chickenLife, harvestWeightKg: row.harvestWeightKg, })), [fcrRows] ); const getLatestValid = (key: K): FcrRow[K] | null => { for (let i = fcrRows.length - 1; i >= 0; i--) { const val = fcrRows[i]?.[key]; if (val !== null && val !== undefined && val !== 0 && String(val) !== '') { return val; } } return null; }; const latestFcr = getLatestValid('fcr'); const latestIotWeight = getLatestValid('iotWeight'); const insightContext = useMemo(() => { if (!selectedKandang) return null; return buildFcrInsightContext({ kandangId: selectedKandang.id, kandangName: selectedKandang.kandang_name, cycleId: selectedCycle?.id, totalDays: selectedCycle?.total_days ?? null, fcrRows, }); }, [selectedKandang, selectedCycle, fcrRows]); if (!selectedCycle) return ; return (
} />
{error && (
{error}
)} {selectedKandang && insightContext ? ( ) : (

AI Insight FCR

Pilih kandang untuk melihat AI Insight.
)} {loading ? ( ) : (

Data Pendukung FCR

{lastUpdated && (

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

)}

Grafik Data Pendukung

Tabel Data Pendukung FCR

{fcrRows.map((row) => ( ))} {fcrRows.length === 0 && ( )}
Umur Total Pakan Bobot IoT Ayam Hidup Total Bobot Panen FCR
Hari {row.day} {formatNum(row.feedTotal)} karung {formatNum(row.iotWeight, 2)} {row.iotWeight != null ? ' g' : ''} {formatNum(row.chickenLife)} ekor {formatNum(row.harvestWeightKg, 1)} kg {formatFcr(row.fcr)}
Tidak ada data FCR tersedia.
)}
); }; export default FcrDashboardPage;