import React, { useEffect, useMemo, useState } from 'react'; import { api, ApiError, errorMessage } from '../services/apiClient.ts'; import type { ChickenCounting, ChickenWeight, Karung, KPI, ManualInput, } from '../types/api.ts'; import type { View } from '../types/common.ts'; import { useFarm } from '../context/FarmContext.tsx'; import KpiCard, { type KpiCardModel, type KpiTrendPoint } from './KpiCard.tsx'; import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx'; import { cycleDayForDate, formatDateId, formatDateRange, formatNumber, resolveCycleStatus, } from '../utils/format.ts'; import { throughVisibleDate } from '../utils/visibility.ts'; import { getCountingAccuracyWarning } from '../utils/countingAccuracyWarning.ts'; import { buildDashboardInsightContext, buildDashboardInsightMetrics, } from '../utils/insightContextBuilders.ts'; import DashboardReportCtas from './dashboard/DashboardReportCtas.tsx'; import DashboardCloseCycleControls from './dashboard/DashboardCloseCycleControls.tsx'; import DashboardManualInputForm from './dashboard/DashboardManualInputForm.tsx'; import ReportPreviewOverlay from './dashboard/ReportPreviewOverlay.tsx'; import CountingAccuracyWarningBanner from './counting/CountingAccuracyWarningBanner.tsx'; import type { DashboardReportType } from '../types/dashboardReport.ts'; import { AiInsightCard } from './AiInsightCard.tsx'; type Props = { setView: (view: View) => void }; function chronological(rows: T[]): T[] { return [...rows].reverse(); } const Dashboard: React.FC = ({ setView }) => { const { selectedCycle, selectedKandang, selectedSite, selectedKandangId } = useFarm(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [kpis, setKpis] = useState([]); const [countings, setCountings] = useState([]); const [manualInputs, setManualInputs] = useState([]); const [weights, setWeights] = useState([]); const [karung, setKarung] = useState(null); const [reportOpen, setReportOpen] = useState(false); const [reportType, setReportType] = useState('daily'); useEffect(() => { if (!selectedCycle) { setKpis([]); setCountings([]); setManualInputs([]); setWeights([]); setKarung(null); return; } const cycleId = selectedCycle.id; let cancelled = false; (async () => { setLoading(true); setError(null); try { const [kpiRows, countingRows, manualRows, weightRows] = await Promise.all([ api.kpis.list({ cycle_id: cycleId }), api.countings.list({ cycle_id: cycleId }), api.manualInputs.list({ cycle_id: cycleId }), api.weights.list({ cycle_id: cycleId }), ]); let latestKarung: Karung | null = null; try { latestKarung = await api.karungs.latest({ cycle_id: cycleId }); } catch (err) { if (!(err instanceof ApiError && err.status === 404)) throw err; } if (cancelled) return; setKpis(kpiRows); setCountings(countingRows); setManualInputs(manualRows); setWeights(weightRows); setKarung(latestKarung); } catch (err) { if (!cancelled) setError(errorMessage(err)); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [selectedCycle]); const cards: KpiCardModel[] = useMemo(() => { // All cards share the latest complete KPI day (not calendar current_day / live IoT). const visibleThrough = selectedCycle?.visible_through_date; const visibleKpis = throughVisibleDate(kpis, visibleThrough); const latest = visibleKpis[0] ?? null; const anchorDate = latest?.date ?? null; const kpisThroughAnchor = anchorDate ? visibleKpis.filter((row) => row.date <= anchorDate) : []; const weightsThroughAnchor = anchorDate ? throughVisibleDate(weights, visibleThrough).filter((row) => row.date <= anchorDate) : []; const weightOnAnchor = anchorDate ? weightsThroughAnchor.find((row) => row.date === anchorDate) ?? weightsThroughAnchor[0] : null; const startDate = selectedCycle?.start_date; const trendLabel = (isoDate: string): string => { const day = startDate ? cycleDayForDate(startDate, isoDate) : 0; return `Hari ${day} ยท ${formatDateId(isoDate)}`; }; const fcrTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({ value: row.fcr, label: trendLabel(row.date), })); const eefTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({ value: row.eef, label: trendLabel(row.date), })); const mortalityTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({ value: row.mortality, label: trendLabel(row.date), })); const weightTrend: KpiTrendPoint[] = chronological(weightsThroughAnchor).map((row) => ({ value: row.average_weight, label: trendLabel(row.date), })); const age = latest?.age ?? null; const mortalityToday = latest?.mortality ?? null; const feedToday = latest != null ? latest.feed : null; const weightToday = latest && latest.iot_weight > 0 ? latest.iot_weight : (weightOnAnchor?.average_weight ?? null); return [ { key: 'umur', title: 'Umur', value: age, unit: 'hari', icon: '๐Ÿ“…', status: age == null ? 'na' : 'ok', }, { key: 'mortality', title: 'Mortalitas', value: mortalityToday, unit: 'ekor', icon: '๐Ÿ“‰', trend: mortalityTrend, status: mortalityToday == null ? 'na' : 'ok', targetView: 'dataManagement', }, { key: 'karung', title: 'Karung Dituang', value: feedToday, unit: 'karung', icon: '๐Ÿ›ข๏ธ', status: feedToday == null ? 'na' : 'ok', targetView: 'feedSackCounting', }, { key: 'weight', title: 'Berat AVG', value: weightToday, unit: 'gram', icon: 'โš–๏ธ', trend: weightTrend, status: weightToday == null ? 'na' : 'ok', targetView: 'weightMonitoring', }, { key: 'fcr', title: 'FCR', value: latest?.fcr ?? null, unit: '', icon: '๐Ÿ“ˆ', trend: fcrTrend, status: latest ? 'ok' : 'na', targetView: 'fcrDashboard', }, { key: 'eef', title: 'EEF', value: latest?.eef ?? null, unit: '', icon: '๐ŸŽฏ', trend: eefTrend, status: latest ? 'ok' : 'na', targetView: 'eefDashboard', }, ]; }, [kpis, weights, selectedCycle]); const countingAccuracyWarning = useMemo(() => { if (!selectedCycle) return null; return getCountingAccuracyWarning({ cycle: selectedCycle, countings, kpis, manualInputs, }); }, [selectedCycle, countings, kpis, manualInputs]); const insightContext = useMemo(() => { if (selectedKandangId == null) return null; const visibleThrough = selectedCycle?.visible_through_date; const visibleKpis = throughVisibleDate(kpis, visibleThrough); const latest = visibleKpis[0] ?? null; const anchorDate = latest?.date ?? null; const weightsThroughAnchor = anchorDate ? throughVisibleDate(weights, visibleThrough).filter((row) => row.date <= anchorDate) : []; const weightOnAnchor = anchorDate ? (weightsThroughAnchor.find((row) => row.date === anchorDate) ?? weightsThroughAnchor[0]) : null; const bobotAvg = latest && latest.iot_weight > 0 ? latest.iot_weight : (weightOnAnchor?.average_weight ?? null); return buildDashboardInsightContext({ kandangId: selectedKandangId, kandangName: selectedKandang?.kandang_name, cycleId: selectedCycle?.id, // Prefer latest KPI age โ€” cycle.current_day can be ahead of available data. hari_ke: latest?.age ?? selectedCycle?.current_day ?? null, totalDays: selectedCycle?.total_days ?? null, fcr_terakhir: latest?.fcr ?? null, eef_terakhir: latest?.eef ?? null, bobot_avg_gram: bobotAvg, mortalitas_hari_ini_ekor: latest?.mortality ?? null, mortalitas_kumulatif_persen: latest?.chicken_life_percentage == null ? null : Math.round((100 - latest.chicken_life_percentage) * 100) / 100, karung_dituang_hari_ini: latest != null ? latest.feed : (karung?.feed_use_today ?? karung?.out_today ?? null), pakan_kumulatif_karung: karung?.feed_use_total ?? latest?.feed_total ?? null, ayam_hidup_ekor: latest?.chicken_life ?? null, persen_hidup: latest?.chicken_life_percentage ?? null, doc_in_ekor: selectedCycle?.doc_in_count ?? null, kematian_kumulatif_ekor: latest?.mortality_total ?? null, panen_kumulatif_ekor: latest?.harvest_total ?? null, tonase_panen_kg: latest?.harvest_weight_total ?? null, uniformity_persen: weightOnAnchor?.uniformity ?? null, }); }, [ selectedKandangId, selectedKandang, selectedCycle, kpis, weights, karung, ]); const insightMetrics = useMemo(() => { if (selectedKandangId == null) return null; const visibleThrough = selectedCycle?.visible_through_date; const visibleKpis = throughVisibleDate(kpis, visibleThrough); const latest = visibleKpis[0] ?? null; const anchorDate = latest?.date ?? null; const weightsThroughAnchor = anchorDate ? throughVisibleDate(weights, visibleThrough).filter((row) => row.date <= anchorDate) : throughVisibleDate(weights, visibleThrough); return buildDashboardInsightMetrics({ kpis: visibleKpis, weights: weightsThroughAnchor, currentDay: latest?.age ?? selectedCycle?.current_day ?? null, docInCount: selectedCycle?.doc_in_count ?? null, pakanKumulatifFallback: karung?.feed_use_total ?? latest?.feed_total ?? null, }); }, [selectedKandangId, selectedCycle, kpis, weights, karung]); if (!selectedCycle) return setView('kandangSettings')} />; const feedTotal = karung?.feed_use_total ?? kpis[0]?.feed_total ?? 0; const feedWeightKg = feedTotal * 50; const feedInitialBalance = selectedCycle?.feed_initial_balance ?? 0; const feedInTotal = (karung?.in_total ?? 0) + feedInitialBalance; const feedOutTotal = karung?.out_total ?? 0; const feedSaldo = feedInTotal - feedTotal - feedOutTotal; const feedSaldoColor = feedSaldo > 0 ? 'text-green-600' : feedSaldo < 0 ? 'text-red-600' : 'text-gray-600'; const cycleStatus = resolveCycleStatus(selectedCycle); return (
{ setReportType('daily'); setReportOpen(true); }} onOpenEndCycle={() => { setReportType('end_cycle'); setReportOpen(true); }} />
{error && (
{error}
)} {countingAccuracyWarning && ( )} {loading ? ( ) : ( <>
{cards.map((card) => ( setView(card.targetView as View) : undefined} /> ))}

Total Pakan

Kumulatif karung pakan sejak awal siklus

Total Karung Tuang

{formatNumber(feedTotal)}

karung

Total Karung Masuk

{formatNumber(feedInTotal)}

karung

Total Karung Keluar

{formatNumber(feedOutTotal)}

karung

Saldo Awal

{formatNumber(feedInitialBalance)}

karung

Total Berat Pakan

{formatNumber(feedWeightKg)}

kg

Saldo Karung Pakan

{formatNumber(feedSaldo)}

karung

{selectedKandangId != null ? ( ) : (
Pilih satu kandang untuk menghasilkan AI Insight. Generate untuk semua kandang tidak didukung.
)}
)} setReportOpen(false)} />
); }; export default Dashboard;