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,9 +1,11 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { api, errorMessage } from '../services/apiClient.ts';
|
||||
import type { AIInsight, Cycle, Karung, ManualInput } from '../types/api.ts';
|
||||
import type { Cycle, Karung, ManualInput } from '../types/api.ts';
|
||||
import { useFarm } from '../context/FarmContext.tsx';
|
||||
import { Banner, EmptyFarm, Spinner } from './ui/Feedback.tsx';
|
||||
import { addDaysIso, cycleDayForDate, formatDateId, resolveCycleStatus, todayIso } from '../utils/format.ts';
|
||||
import { buildFeedSackInsightContext } from '../utils/insightContextBuilders.ts';
|
||||
import { PageAiInsight } from './shared/PageAiInsight.tsx';
|
||||
import FeedStats from './feedSack/FeedStats.tsx';
|
||||
import FeedSaldoCard from './feedSack/FeedSaldoCard.tsx';
|
||||
import FeedTrendChart from './feedSack/FeedTrendChart.tsx';
|
||||
@@ -15,7 +17,6 @@ const FeedSackCountingPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
||||
useFarm();
|
||||
const [rows, setRows] = useState<Karung[]>([]);
|
||||
const [manualRows, setManualRows] = useState<ManualInput[]>([]);
|
||||
const [insights, setInsights] = useState<AIInsight[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showCyclePicker, setShowCyclePicker] = useState(false);
|
||||
@@ -24,13 +25,11 @@ const FeedSackCountingPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [karungRows, insightRows, manualList] = await Promise.all([
|
||||
const [karungRows, manualList] = await Promise.all([
|
||||
api.karungs.list({ cycle_id: cycleId }),
|
||||
api.insights.list({ cycle_id: cycleId }),
|
||||
api.manualInputs.list({ cycle_id: cycleId }),
|
||||
]);
|
||||
setRows(karungRows);
|
||||
setInsights(insightRows);
|
||||
setManualRows(manualList);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
@@ -42,7 +41,6 @@ const FeedSackCountingPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
||||
useEffect(() => {
|
||||
if (!selectedCycle) {
|
||||
setRows([]);
|
||||
setInsights([]);
|
||||
setManualRows([]);
|
||||
return;
|
||||
}
|
||||
@@ -92,6 +90,36 @@ const FeedSackCountingPage: React.FC<{ onSettings: () => void }> = ({ onSettings
|
||||
})} ${date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' })}`;
|
||||
}, [latest]);
|
||||
|
||||
const insightContext = useMemo(() => {
|
||||
if (!selectedKandang || !selectedCycle) return null;
|
||||
const manualByDate = new Map(sortedManualRows.map((r) => [r.date, r]));
|
||||
const dailyRows = sortedRows.map((r) => {
|
||||
const manual = manualByDate.get(r.date);
|
||||
return {
|
||||
date: r.date,
|
||||
day: cycleDayForDate(selectedCycle.start_date, r.date),
|
||||
masuk: r.in_today,
|
||||
dituang: r.feed_use_today,
|
||||
keluar: r.out_today,
|
||||
masukManual: manual?.feed_in_manual ?? 0,
|
||||
dituangManual: manual?.feed_use_manual ?? 0,
|
||||
};
|
||||
});
|
||||
return buildFeedSackInsightContext({
|
||||
kandangId: selectedKandang.id,
|
||||
kandangName: selectedKandang.kandang_name,
|
||||
cycleId: selectedCycle.id,
|
||||
totalDays: selectedCycle.total_days,
|
||||
dailyRows,
|
||||
saldo_awal_karung: selectedCycle.feed_initial_balance ?? null,
|
||||
total_karung_masuk_iot: latest?.in_total ?? null,
|
||||
total_karung_dituang_iot: latest?.feed_use_total ?? null,
|
||||
total_karung_keluar_iot: latest?.out_total ?? null,
|
||||
total_karung_masuk_manual: latestManual?.feed_in_manual_total ?? null,
|
||||
total_karung_dituang_manual: latestManual?.feed_use_manual_total ?? null,
|
||||
});
|
||||
}, [selectedKandang, selectedCycle, sortedRows, sortedManualRows, latest, latestManual]);
|
||||
|
||||
if (!selectedCycle) return <EmptyFarm onSettings={onSettings} />;
|
||||
|
||||
const cycleStatus = resolveCycleStatus(selectedCycle);
|
||||
@@ -197,40 +225,70 @@ const FeedSackCountingPage: 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">
|
||||
<i className="fa-solid fa-brain text-red-600 mr-2"></i>
|
||||
AI Insight Hitung Karung
|
||||
</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 ? (
|
||||
<PageAiInsight
|
||||
topic="hitung_karung"
|
||||
cacheKey={`pakan-k${selectedKandang.id}-c${selectedCycle.id}-day${currentDay}-rows${sortedRows.length}`}
|
||||
contextData={insightContext}
|
||||
chartSpec={{
|
||||
type: 'trend',
|
||||
historyKey: 'tren_harian',
|
||||
title: 'Karung dituang vs masuk per hari',
|
||||
unit: 'karung',
|
||||
series: [
|
||||
{ field: 'dituang', label: 'Dituang (IOT)', color: '#dc2626' },
|
||||
{ field: 'masuk', label: 'Masuk (IOT)', color: '#3b82f6' },
|
||||
{ field: 'masukManual', label: 'Masuk (Manual)', color: '#0ea5e9' },
|
||||
],
|
||||
}}
|
||||
scalarScope={{
|
||||
historyKey: 'tren_harian',
|
||||
derive: {
|
||||
karungMasuk_karung: {
|
||||
field: 'masuk',
|
||||
agg: 'sum',
|
||||
replaces: 'total_karung_masuk_iot',
|
||||
},
|
||||
karungDituang_karung: {
|
||||
field: 'dituang',
|
||||
agg: 'sum',
|
||||
replaces: 'total_karung_dituang_iot',
|
||||
},
|
||||
karungKeluar_karung: { field: 'keluar', agg: 'sum' },
|
||||
karungMasukManual_karung: {
|
||||
field: 'masukManual',
|
||||
agg: 'sum',
|
||||
replaces: 'total_karung_masuk_manual',
|
||||
},
|
||||
karungDituangManual_karung: {
|
||||
field: 'dituangManual',
|
||||
agg: 'sum',
|
||||
replaces: 'total_karung_dituang_manual',
|
||||
},
|
||||
rataKarungDituangPerHari_karung: {
|
||||
field: 'dituang',
|
||||
agg: 'mean',
|
||||
replaces: 'rata_karung_per_hari',
|
||||
},
|
||||
},
|
||||
drop: [
|
||||
'total_karung_masuk_iot',
|
||||
'total_karung_dituang_iot',
|
||||
'total_karung_masuk_manual',
|
||||
'total_karung_dituang_manual',
|
||||
'rata_karung_per_hari',
|
||||
],
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<section>
|
||||
<h2 className="text-lg font-bold text-gray-800 mb-3">
|
||||
<i className="fa-solid fa-brain text-red-600 mr-2"></i>
|
||||
AI Insight Hitung Karung
|
||||
</h2>
|
||||
<Banner>Pilih kandang untuk melihat AI Insight.</Banner>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Saldo Awal */}
|
||||
<InitialBalanceCard cycle={selectedCycle} />
|
||||
|
||||
Reference in new issue
Block a user