update applying current ai insight from dashboard-cpsp
This commit is contained in:
1 parent
31272035e9
commit
b87f20e3b3
27 files changed
+5300
-343
No files matched your search
+117
-33
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user