kpi configuration (still prototype some data still use manual need to update later)

This commit is contained in:
Alberto-Audrix committed 2026-09-09 16:50:07 +07:00
1 parent e4dbfd0252
commit ed84d989b7
12 files changed
+547 -89

No files matched your search

+32 -31
View File
@@ -6,7 +6,6 @@ import type {
ChickenWeight,
Karung,
KPI,
LatestAvg,
ManualInput,
} from '../types/api.ts';
import type { View } from '../types/common.ts';
@@ -20,7 +19,7 @@ import {
formatNumber,
resolveCycleStatus,
} from '../utils/format.ts';
import { isOnOrBeforeVisibleDate, throughVisibleDate } from '../utils/visibility.ts';
import { throughVisibleDate } from '../utils/visibility.ts';
import { getCountingAccuracyWarning } from '../utils/countingAccuracyWarning.ts';
import DashboardReportCtas from './dashboard/DashboardReportCtas.tsx';
import DashboardManualInputForm from './dashboard/DashboardManualInputForm.tsx';
@@ -43,7 +42,6 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
const [manualInputs, setManualInputs] = useState<ManualInput[]>([]);
const [weights, setWeights] = useState<ChickenWeight[]>([]);
const [karung, setKarung] = useState<Karung | null>(null);
const [latestAvg, setLatestAvg] = useState<LatestAvg | null>(null);
const [insights, setInsights] = useState<AIInsight[]>([]);
const [reportOpen, setReportOpen] = useState(false);
const [reportType, setReportType] = useState<DashboardReportType>('daily');
@@ -55,7 +53,6 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
setManualInputs([]);
setWeights([]);
setKarung(null);
setLatestAvg(null);
setInsights([]);
return;
}
@@ -78,19 +75,12 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
} catch (err) {
if (!(err instanceof ApiError && err.status === 404)) throw err;
}
let avg: LatestAvg | null = null;
try {
avg = await api.weights.latestAvg({ cycle_id: cycleId });
} catch (err) {
if (!(err instanceof ApiError && err.status === 404)) throw err;
}
if (cancelled) return;
setKpis(kpiRows);
setCountings(countingRows);
setManualInputs(manualRows);
setWeights(weightRows);
setKarung(latestKarung);
setLatestAvg(avg);
setInsights(insightRows);
} catch (err) {
if (!cancelled) setError(errorMessage(err));
@@ -104,40 +94,51 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
}, [selectedCycle]);
const cards: KpiCardModel[] = useMemo(() => {
// Countings/weights APIs are real-time; KPI cards still honor publish cutoff.
// All cards share the latest complete KPI day (not calendar current_day / live IoT).
const visibleThrough = selectedCycle?.visible_through_date;
const visibleKpis = throughVisibleDate(kpis, visibleThrough);
const visibleCountings = throughVisibleDate(countings, visibleThrough);
const visibleWeights = throughVisibleDate(weights, visibleThrough);
const visibleLatestAvg =
latestAvg && isOnOrBeforeVisibleDate(latestAvg.date, visibleThrough) ? latestAvg : null;
const visibleKarung =
karung && isOnOrBeforeVisibleDate(karung.date, visibleThrough) ? karung : null;
const latest = visibleKpis[0] ?? null;
const anchorDate = latest?.date ?? null;
const kpisThroughAnchor = anchorDate
? visibleKpis.filter((row) => row.date <= anchorDate)
: [];
const weightsThroughAnchor = anchorDate
? throughVisibleDate(weights, visibleThrough).filter((row) => row.date <= anchorDate)
: [];
const weightOnAnchor = anchorDate
? weightsThroughAnchor.find((row) => row.date === anchorDate) ?? weightsThroughAnchor[0]
: null;
const latest = visibleKpis[0];
const startDate = selectedCycle?.start_date;
const trendLabel = (isoDate: string): string => {
const day = startDate ? cycleDayForDate(startDate, isoDate) : 0;
return `Hari ${day} · ${formatDateId(isoDate)}`;
};
const fcrTrend: KpiTrendPoint[] = chronological(visibleKpis).map((row) => ({
const fcrTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({
value: row.fcr,
label: trendLabel(row.date),
}));
const eefTrend: KpiTrendPoint[] = chronological(visibleKpis).map((row) => ({
const eefTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({
value: row.eef,
label: trendLabel(row.date),
}));
const mortalityTrend: KpiTrendPoint[] = chronological(visibleCountings).map((row) => ({
value: row.mortality_count,
const mortalityTrend: KpiTrendPoint[] = chronological(kpisThroughAnchor).map((row) => ({
value: row.mortality,
label: trendLabel(row.date),
}));
const mortalityToday = latest?.mortality ?? visibleCountings[0]?.mortality_count ?? null;
const weightTrend: KpiTrendPoint[] = chronological(visibleWeights).map((row) => ({
const weightTrend: KpiTrendPoint[] = chronological(weightsThroughAnchor).map((row) => ({
value: row.average_weight,
label: trendLabel(row.date),
}));
const age = selectedCycle?.current_day ?? latest?.age ?? null;
const age = latest?.age ?? null;
const mortalityToday = latest?.mortality ?? null;
const feedToday = latest != null ? latest.feed : null;
const weightToday =
latest && latest.iot_weight > 0
? latest.iot_weight
: (weightOnAnchor?.average_weight ?? null);
return [
{
key: 'umur',
@@ -160,20 +161,20 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
{
key: 'karung',
title: 'Karung Dituang',
value: visibleKarung?.feed_use_today ?? visibleKarung?.out_today ?? null,
value: feedToday,
unit: 'karung',
icon: '🛢️',
status: visibleKarung ? 'ok' : 'na',
status: feedToday == null ? 'na' : 'ok',
targetView: 'feedSackCounting',
},
{
key: 'weight',
title: 'Berat AVG',
value: visibleLatestAvg?.average_weight ?? visibleWeights[0]?.average_weight ?? null,
value: weightToday,
unit: 'gram',
icon: '⚖️',
trend: weightTrend,
status: visibleLatestAvg || visibleWeights[0] ? 'ok' : 'na',
status: weightToday == null ? 'na' : 'ok',
targetView: 'weightMonitoring',
},
{
@@ -197,7 +198,7 @@ const Dashboard: React.FC<Props> = ({ setView }) => {
targetView: 'eefDashboard',
},
];
}, [kpis, countings, weights, karung, latestAvg, selectedCycle]);
}, [kpis, weights, selectedCycle]);
const countingAccuracyWarning = useMemo(() => {
if (!selectedCycle) return null;
+2 -2
View File
@@ -230,8 +230,8 @@ const FcrDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
.map((row) => ({
day: row.age,
feedTotal: row.feed_total,
iotWeight: weightByAge.get(row.age) ?? null,
chickenLife: row.chicken_life,
iotWeight: row.iot_weight > 0 ? row.iot_weight : weightByAge.get(row.age) ?? null,
chickenLife: Math.max(row.chicken_life - row.harvest_total, 0),
harvestWeightKg: row.harvest_weight_total / 1000,
fcr: row.fcr > 0 ? row.fcr : null,
})),
@@ -151,7 +151,7 @@ const DashboardManualInputForm: React.FC<Props> = ({ cycleId, currentDay, readOn
<p className="text-sm text-gray-500">
{readOnly
? 'Siklus ini sudah ditutup. Data di bawah hanya untuk ditinjau.'
: 'Isi data harian di sini. Total dihitung otomatis. FCR, EEF, dan rata-rata panen belum dihitung.'}
: 'Isi data harian di sini. Total, FCR, dan EEF dihitung otomatis setelah data sumber lengkap.'}
</p>
{error && <Banner kind="error">{error}</Banner>}
{notice && <Banner kind="success">{notice}</Banner>}