update applying current ai insight from dashboard-cpsp

This commit is contained in:
Alberto-Audrix committed 2026-10-05 14:25:22 +07:00
1 parent 31272035e9
commit b87f20e3b3
27 files changed
+5300 -343

No files matched your search

+117 -33
View File
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useState } from 'react';
import { api, ApiError, errorMessage } from '../services/apiClient.ts';
import type {
AIInsight,
ChickenCounting,
ChickenWeight,
Karung,
@@ -21,12 +20,20 @@ import {
} from '../utils/format.ts';
import { throughVisibleDate } from '../utils/visibility.ts';
import { getCountingAccuracyWarning } from '../utils/countingAccuracyWarning.ts';
import { getTempStandardByDay } from '../utils/cp707EnvStandards.ts';
import {
buildDashboardInsightContext,
buildDashboardInsightMetrics,
buildIotDayAggregates,
type EndCycleIotDayAggregate,
} from '../utils/insightContextBuilders.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';
import { AiInsightCard } from './AiInsightCard.tsx';
type Props = { setView: (view: View) => void };
@@ -35,7 +42,7 @@ function chronological<T>(rows: T[]): T[] {
}
const Dashboard: React.FC<Props> = ({ setView }) => {
const { selectedCycle, selectedKandang, selectedSite } = useFarm();
const { selectedCycle, selectedKandang, selectedSite, selectedKandangId } = useFarm();
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [kpis, setKpis] = useState<KPI[]>([]);
@@ -43,7 +50,7 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
const [manualInputs, setManualInputs] = useState<ManualInput[]>([]);
const [weights, setWeights] = useState<ChickenWeight[]>([]);
const [karung, setKarung] = useState<Karung | null>(null);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [iotDayAggregates, setIotDayAggregates] = useState<EndCycleIotDayAggregate[]>([]);
const [reportOpen, setReportOpen] = useState(false);
const [reportType, setReportType] = useState<DashboardReportType>('daily');
@@ -54,7 +61,7 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
setManualInputs([]);
setWeights([]);
setKarung(null);
setInsights([]);
setIotDayAggregates([]);
return;
}
const cycleId = selectedCycle.id;
@@ -63,12 +70,11 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
setLoading(true);
setError(null);
try {
const [kpiRows, countingRows, manualRows, weightRows, insightRows] = await Promise.all([
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 }),
api.insights.list({ cycle_id: cycleId }),
]);
let latestKarung: Karung | null = null;
try {
@@ -82,7 +88,31 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
setManualInputs(manualRows);
setWeights(weightRows);
setKarung(latestKarung);
setInsights(insightRows);
// Best-effort IoT aggregates for end-cycle root cause (non-blocking on failure).
try {
const flockId =
kpiRows.find((row) => row.flock_id != null)?.flock_id ??
(selectedKandang
? (await api.flocks.list({ kandang_id: selectedKandang.id }))[0]?.id
: null);
if (flockId != null) {
const panels = await api.panelIot.list({ flock_id: flockId });
if (!cancelled) {
setIotDayAggregates(
buildIotDayAggregates({
rows: panels,
cycleStartDate: selectedCycle.start_date,
getTempTarget: getTempStandardByDay,
})
);
}
} else {
if (!cancelled) setIotDayAggregates([]);
}
} catch {
if (!cancelled) setIotDayAggregates([]);
}
} catch (err) {
if (!cancelled) setError(errorMessage(err));
} finally {
@@ -92,7 +122,72 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
return () => {
cancelled = true;
};
}, [selectedCycle]);
}, [selectedCycle, selectedKandang]);
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]);
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,
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 != null ? Math.max(0, latest.chicken_life - (latest.harvest_total ?? 0)) : 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 ? latest.harvest_weight_total / 1000 : null,
uniformity_persen: weightOnAnchor?.uniformity ?? null,
});
}, [selectedKandangId, selectedKandang, selectedCycle, kpis, weights, karung]);
const cards: KpiCardModel[] = useMemo(() => {
// All cards share the latest complete KPI day (not calendar current_day / live IoT).
@@ -352,32 +447,21 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
</div>
<KandangRankingTop5 setView={setView} />
<section className="mt-8">
<h2 className="text-lg font-bold text-gray-800 mb-3">AI Insight</h2>
{insights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
{selectedKandangId != null ? (
<AiInsightCard
kandangId={selectedKandangId}
context={insightContext ?? undefined}
dashboardMetrics={insightMetrics}
iotDayAggregates={iotDayAggregates}
page="dashboard"
/>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{insights.slice(0, 4).map((item) => (
<article
key={item.id}
className="bg-white rounded-xl shadow-sm p-5 border border-gray-100"
>
<div className="flex items-start justify-between gap-3">
<div>
<p className="text-xs uppercase tracking-wide text-gray-400">
{item.section}
</p>
<h3 className="font-semibold text-gray-800 mt-1">
{item.alert || 'Insight'}
</h3>
</div>
<span className="text-xs text-gray-400">{item.session}</span>
</div>
<p className="text-sm text-gray-600 mt-3 whitespace-pre-wrap">
{item.insight_text}
</p>
</article>
))}
<div className="bg-white border border-gray-100 rounded-xl shadow-sm p-6">
<div className="flex items-center gap-2 mb-2">
<i className="fa-solid fa-brain text-red-600 text-xl" />
<h2 className="text-lg font-bold text-gray-900">AI Insight</h2>
</div>
<Banner>Pilih satu kandang untuk melihat AI Insight.</Banner>
</div>
)}
</section>