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
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user