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

+69 -48
View File
@@ -1,20 +1,21 @@
import React, { useEffect, useMemo, useState } from 'react';
import { api, ApiError, errorMessage } from '../services/apiClient.ts';
import type { AIInsight, ChickenWeight, LatestAvg } from '../types/api.ts';
import type { ChickenWeight, LatestAvg } from '../types/api.ts';
import { useFarm } from '../context/FarmContext.tsx';
import { Banner, EmptyFarm, Spinner } from './ui/Feedback.tsx';
import { formatDateId, formatNumber } from '../utils/format.ts';
import { formatWeight } from '../utils/weight.ts';
import { buildWeightInsightContext } from '../utils/insightContextBuilders.ts';
import { PageAiInsight } from './shared/PageAiInsight.tsx';
import WeightStats from './weight/WeightStats.tsx';
import DailyWeightProgressionChart from './weight/DailyWeightProgressionChart.tsx';
import DailyWeightGainChart from './weight/DailyWeightGainChart.tsx';
import DailyDetailPanel from './weight/DailyDetailPanel.tsx';
const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
const { selectedCycle } = useFarm();
const { selectedCycle, selectedKandang } = useFarm();
const [rows, setRows] = useState<ChickenWeight[]>([]);
const [latest, setLatest] = useState<LatestAvg | null>(null);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [selectedDate, setSelectedDate] = useState('');
@@ -22,12 +23,8 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
const reload = async (cycleId: number) => {
setError(null);
try {
const [list, insightRows] = await Promise.all([
api.weights.list({ cycle_id: cycleId }),
api.insights.list({ cycle_id: cycleId }),
]);
const list = await api.weights.list({ cycle_id: cycleId });
setRows(list);
setInsights(insightRows);
try {
setLatest(await api.weights.latestAvg({ cycle_id: cycleId }));
} catch (err) {
@@ -43,7 +40,6 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
if (!selectedCycle) {
setRows([]);
setLatest(null);
setInsights([]);
setSelectedDate('');
return;
}
@@ -86,14 +82,6 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
const minDate = rows.length > 0 ? (rows[rows.length - 1]?.date ?? '') : '';
const maxDate = rows.length > 0 ? (rows[0]?.date ?? '') : '';
const weightInsights = useMemo(() => {
const matched = insights.filter((item) => {
const section = `${item.section ?? ''} ${item.alert ?? ''}`.toLowerCase();
return section.includes('berat') || section.includes('weight') || section.includes('timbang');
});
return (matched.length > 0 ? matched : insights).slice(0, 4);
}, [insights]);
// Prefer the latest non-zero row for the KPI card so a closed cycle with
// trailing empty days does not read as "0 g".
const kpiLatest = useMemo<LatestAvg | null>(() => {
@@ -112,6 +100,21 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
};
}, [latest, rows]);
const insightContext = useMemo(() => {
if (!selectedKandang) return null;
return buildWeightInsightContext({
kandangId: selectedKandang.id,
kandangName: selectedKandang.kandang_name,
cycleId: selectedCycle?.id,
totalDays: selectedCycle?.total_days ?? null,
docWeight: docWeight,
weightRows: chartRows,
averageWeight: kpiLatest?.average_weight ?? null,
uniformity: kpiLatest?.uniformity ?? null,
adg: kpiLatest?.average_daily_gain ?? null,
});
}, [selectedKandang, selectedCycle, docWeight, chartRows, kpiLatest]);
if (!selectedCycle) return <EmptyFarm onSettings={onSettings} />;
return (
@@ -147,37 +150,55 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<Spinner />
) : (
<div className="space-y-8">
{/* AI Insight */}
<section>
<h2 className="text-lg font-bold text-gray-800 mb-3">AI Insight</h2>
{weightInsights.length === 0 ? (
<Banner>Belum ada insight untuk siklus ini.</Banner>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{weightInsights.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 ? (
<PageAiInsight
topic="berat_ayam"
cacheKey={`wm-k${selectedKandang.id}-c${selectedCycle.id}-day${selectedCycle.current_day}-rows${chartRows.length}`}
contextData={insightContext}
chartSpec={{
type: 'trend',
historyKey: 'tren_harian',
title: 'Bobot aktual vs target',
unit: 'gram',
gap: { actualField: 'bobot_gram', targetField: 'target_gram' },
series: [
{ field: 'bobot_gram', label: 'Bobot aktual', color: '#7c3aed' },
{ field: 'target_gram', label: 'Target', color: '#9ca3af', dashed: true },
],
}}
scalarScope={{
historyKey: 'tren_harian',
derive: {
bobotTerakhir_gram: {
field: 'bobot_gram',
agg: 'lastValid',
replaces: 'averageWeight',
},
targetBobot_gram: {
field: 'target_gram',
agg: 'last',
replaces: 'targetWeight',
emptyAs: 0,
},
pertambahanHarian_gramPerHari: {
field: 'adg_gram_per_hari',
agg: 'last',
replaces: 'adg',
emptyAs: 0,
},
},
drop: ['averageWeight', 'targetWeight', 'adg'],
}}
/>
) : (
<section>
<h2 className="text-lg font-bold text-gray-800 mb-3">
<i className="fa-solid fa-brain text-red-600 mr-2" />
AI Insight Berat Ayam
</h2>
<Banner>Pilih kandang untuk melihat AI Insight.</Banner>
</section>
)}
{/* TOP SECTION: DATA SELURUH SIKLUS */}
<div className="space-y-8">