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

+96 -39
View File
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { api, errorMessage } from '../../services/apiClient.ts';
import type { AIInsight, ChickenCounting, KPI, ManualInput } from '../../types/api.ts';
import type { ChickenCounting, KPI, ManualInput } from '../../types/api.ts';
import { useFarm } from '../../context/FarmContext.tsx';
import { Banner, EmptyFarm, PageHeader, Spinner } from '../ui/Feedback.tsx';
import {
@@ -12,6 +12,8 @@ import {
resolveCycleStatus,
todayIso,
} from '../../utils/format.ts';
import { buildCountingInsightContext } from '../../utils/insightContextBuilders.ts';
import { PageAiInsight } from '../shared/PageAiInsight.tsx';
import PopulationTrendChart from './PopulationTrendChart.tsx';
import CountingAccuracyWarningBanner from './CountingAccuracyWarningBanner.tsx';
import { getCountingAccuracyWarning } from '../../utils/countingAccuracyWarning.ts';
@@ -49,7 +51,6 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
const [kpis, setKpis] = useState<KPI[]>([]);
const [countings, setCountings] = useState<ChickenCounting[]>([]);
const [manualRows, setManualRows] = useState<ManualInput[]>([]);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchDay, setSearchDay] = useState<string>('');
@@ -61,16 +62,14 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
setLoading(true);
setError(null);
try {
const [k, c, m, i] = await Promise.all([
const [k, c, m] = await Promise.all([
api.kpis.list({ cycle_id: cycleId }),
api.countings.list({ cycle_id: cycleId }),
api.manualInputs.list({ cycle_id: cycleId }),
api.insights.list({ cycle_id: cycleId }),
]);
setKpis(k);
setCountings(c);
setManualRows(m);
setInsights(i);
} catch (err) {
setError(errorMessage(err));
} finally {
@@ -83,7 +82,6 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
setKpis([]);
setCountings([]);
setManualRows([]);
setInsights([]);
return;
}
void loadData(selectedCycle.id);
@@ -175,9 +173,7 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
const totalPanen = latest?.harvestTotal ?? 0;
const totalBeratPanen = latest?.harvestWeightTotal ?? 0;
const currentPopulation = latest?.stockAkhir ?? initialPopulation;
const mortalityRate = initialPopulation
? ((totalMortality / initialPopulation) * 100).toFixed(2)
: '0.00';
const mortalityRate = initialPopulation > 0 ? (totalMortality / initialPopulation) * 100 : null;
return {
initialPopulation,
@@ -189,6 +185,33 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
};
}, [rows, selectedCycle]);
const insightContext = useMemo(() => {
if (!selectedKandang || !selectedCycle) return null;
const lastKpiAge = kpis.length > 0 ? Math.max(...kpis.map((k) => k.age)) : null;
const lastCountingDay =
countings.length > 0
? Math.max(...countings.map((c) => cycleDayForDate(selectedCycle.start_date, c.date)))
: null;
const lastDataDay =
lastKpiAge != null || lastCountingDay != null
? Math.max(lastKpiAge ?? 0, lastCountingDay ?? 0)
: null;
const insightRows = lastDataDay != null ? rows.filter((row) => row.day <= lastDataDay) : rows;
return buildCountingInsightContext({
kandangId: selectedKandang.id,
kandangName: selectedKandang.kandang_name,
cycleId: selectedCycle.id,
totalDays: selectedCycle.total_days ?? null,
dailyRows: insightRows,
populasi_awal_ekor: stats.initialPopulation > 0 ? stats.initialPopulation : null,
populasi_kini_ekor: stats.currentPopulation,
mortalitas_kumulatif_ekor: stats.totalMortality,
mortalitas_persen: stats.mortalityRate,
panen_kumulatif_ekor: stats.totalPanen,
berat_panen_kumulatif_kg: stats.totalBeratPanen,
});
}, [selectedKandang, selectedCycle, rows, stats, kpis, countings]);
const chartData = useMemo(
() =>
rows.map((r) => ({
@@ -269,36 +292,70 @@ const DataManagementPage: React.FC<{ onSettings: () => void }> = ({ onSettings }
<CountingAccuracyWarningBanner warning={countingAccuracyWarning} />
)}
{/* AI Insight */}
<section className="mb-6">
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-wand-magic-sparkles text-purple-500 mr-2"></i>
AI Insight
</h2>
{insights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
) : (
<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>
)}
</section>
{selectedKandang && insightContext ? (
<div className="mb-6">
<PageAiInsight
topic="hitung_ayam"
cacheKey={`hitung-k${selectedKandang.id}-c${selectedCycle.id}-day${selectedCycle.current_day}-rows${rows.length}`}
contextData={insightContext}
chartSpec={{
type: 'trend',
historyKey: 'tren_populasi_harian',
title: 'Tren populasi',
unit: 'ekor',
series: [{ field: 'populasi', label: 'Populasi', color: '#7c3aed' }],
}}
scalarScope={{
historyKey: 'tren_populasi_harian',
derive: {
populasiKini_ekor: {
field: 'populasi',
agg: 'lastValid',
replaces: 'populasi_kini_ekor',
},
mortalitasKumulatif_ekor: {
field: 'mortalitas_kumulatif_ekor',
agg: 'last',
replaces: 'mortalitas_kumulatif_ekor',
emptyAs: 0,
},
panenKumulatif_ekor: {
field: 'panen_kumulatif_ekor',
agg: 'last',
replaces: 'panen_kumulatif_ekor',
emptyAs: 0,
},
beratPanenKumulatif_kg: {
field: 'berat_panen_kumulatif_kg',
agg: 'last',
replaces: 'berat_panen_kumulatif_kg',
emptyAs: 0,
},
mortalitasPersen: {
field: 'mortalitas_persen',
agg: 'last',
replaces: 'mortalitas_persen',
},
},
drop: [
'populasi_kini_ekor',
'mortalitas_kumulatif_ekor',
'panen_kumulatif_ekor',
'berat_panen_kumulatif_kg',
'mortalitas_persen',
],
}}
/>
</div>
) : (
<section className="mb-6">
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-wand-magic-sparkles text-purple-500 mr-2"></i>
AI Insight
</h2>
<Banner>Pilih kandang untuk melihat AI Insight.</Banner>
</section>
)}
{/* Population Chart */}
<div className="mb-6">