import React, { useEffect, useMemo, useState } from 'react'; import { api, ApiError, errorMessage } from '../services/apiClient.ts'; import type { AIInsight, ChickenCounting, ChickenWeight, Karung, KPI, LatestAvg, 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 { isOnOrBeforeVisibleDate, throughVisibleDate } from '../utils/visibility.ts'; import { getCountingAccuracyWarning } from '../utils/countingAccuracyWarning.ts'; import DashboardReportCtas from './dashboard/DashboardReportCtas.tsx'; import ReportPreviewOverlay from './dashboard/ReportPreviewOverlay.tsx'; import CountingAccuracyWarningBanner from './counting/CountingAccuracyWarningBanner.tsx'; import PendingCloseQueue from './PendingCloseQueue.tsx'; import KandangRankingTop5 from './dashboard/KandangRankingTop5.tsx'; import type { DashboardReportType } from '../types/dashboardReport.ts'; type Props = { setView: (view: View) => void }; function chronological(rows: T[]): T[] { return [...rows].reverse(); } const Dashboard: React.FC = ({ setView }) => { const { selectedCycle, selectedKandang, selectedSite } = 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 [latestAvg, setLatestAvg] = useState(null); const [insights, setInsights] = useState([]); const [reportOpen, setReportOpen] = useState(false); const [reportType, setReportType] = useState('daily'); useEffect(() => { if (!selectedCycle) { setKpis([]); setCountings([]); setManualInputs([]); setWeights([]); setKarung(null); setLatestAvg(null); setInsights([]); return; } const cycleId = selectedCycle.id; let cancelled = false; (async () => { setLoading(true); setError(null); try { const [kpiRows, countingRows, manualRows, weightRows, insightRows] = 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 }), api.insights.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; } let avg: LatestAvg | null = null; try { avg = await api.weights.latestAvg({ 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); setLatestAvg(avg); setInsights(insightRows); } catch (err) { if (!cancelled) setError(errorMessage(err)); } finally { if (!cancelled) setLoading(false); } })(); return () => { cancelled = true; }; }, [selectedCycle]); const cards: KpiCardModel[] = useMemo(() => { // Countings/weights APIs are real-time; KPI cards still honor publish cutoff. const visibleThrough = selectedCycle?.visible_through_date; const visibleKpis = throughVisibleDate(kpis, visibleThrough); const visibleCountings = throughVisibleDate(countings, visibleThrough); const visibleWeights = throughVisibleDate(weights, visibleThrough); const visibleLatestAvg = latestAvg && isOnOrBeforeVisibleDate(latestAvg.date, visibleThrough) ? latestAvg : null; const visibleKarung = karung && isOnOrBeforeVisibleDate(karung.date, visibleThrough) ? karung : null; const latest = visibleKpis[0]; 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(visibleKpis).map((row) => ({ value: row.fcr, label: trendLabel(row.date), })); const eefTrend: KpiTrendPoint[] = chronological(visibleKpis).map((row) => ({ value: row.eef, label: trendLabel(row.date), })); const mortalityTrend: KpiTrendPoint[] = chronological(visibleCountings).map((row) => ({ value: row.mortality_count, label: trendLabel(row.date), })); const mortalityToday = latest?.mortality ?? visibleCountings[0]?.mortality_count ?? null; const weightTrend: KpiTrendPoint[] = chronological(visibleWeights).map((row) => ({ value: row.average_weight, label: trendLabel(row.date), })); const age = selectedCycle?.current_day ?? latest?.age ?? 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: visibleKarung?.feed_use_today ?? visibleKarung?.out_today ?? null, unit: 'karung', icon: '๐Ÿ›ข๏ธ', status: visibleKarung ? 'ok' : 'na', targetView: 'feedSackCounting', }, { key: 'weight', title: 'Berat AVG', value: visibleLatestAvg?.average_weight ?? visibleWeights[0]?.average_weight ?? null, unit: 'gram', icon: 'โš–๏ธ', trend: weightTrend, status: visibleLatestAvg || visibleWeights[0] ? 'ok' : 'na', 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, countings, weights, karung, latestAvg, selectedCycle]); const countingAccuracyWarning = useMemo(() => { if (!selectedCycle) return null; return getCountingAccuracyWarning({ cycle: selectedCycle, countings, kpis, manualInputs, }); }, [selectedCycle, countings, kpis, manualInputs]); 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

AI Insight

{insights.length === 0 ? ( Belum ada insight untuk siklus ini. ) : (
{insights.slice(0, 4).map((item) => (

{item.section}

{item.alert || 'Insight'}

{item.session}

{item.insight_text}

))}
)}
)} setReportOpen(false)} />
); }; export default Dashboard;