adjust eef, panel iot, and main dashboard page ai insight shown data
This commit is contained in:
1 parent
73bdb8c739
commit
0d960c8153
5 files changed
+130
-22
No files matched your search
@@ -583,8 +583,11 @@ export const AiInsightCard: React.FC<AiInsightCardProps> = ({
|
|||||||
? { firstDay: 1, lastDay: gradeDay }
|
? { firstDay: 1, lastDay: gradeDay }
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const hasInsight =
|
||||||
|
Boolean(insightText.trim()) || structuredInsight != null || endCycleInsight != null;
|
||||||
|
|
||||||
const renderVisualBlock = () => {
|
const renderVisualBlock = () => {
|
||||||
if (!scopedMetrics || page !== 'dashboard') return null;
|
if (!hasInsight || !scopedMetrics || page !== 'dashboard') return null;
|
||||||
|
|
||||||
const fcrVal = scopedMetrics.fcr;
|
const fcrVal = scopedMetrics.fcr;
|
||||||
const eefVal = scopedMetrics.eef;
|
const eefVal = scopedMetrics.eef;
|
||||||
|
|||||||
@@ -330,6 +330,11 @@ const EefDashboardPage: React.FC<{ onSettings: () => void }> = ({ onSettings })
|
|||||||
agg: 'lastValid',
|
agg: 'lastValid',
|
||||||
replaces: 'bw_panen_gram',
|
replaces: 'bw_panen_gram',
|
||||||
},
|
},
|
||||||
|
umurPanenTerakhir_hari: {
|
||||||
|
field: 'umur_panen_hari',
|
||||||
|
agg: 'lastValid',
|
||||||
|
replaces: 'umur_panen_rata_hari',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ type DaySummary = {
|
|||||||
minExp: number;
|
minExp: number;
|
||||||
maxExp: number;
|
maxExp: number;
|
||||||
humidity: number;
|
humidity: number;
|
||||||
|
minHumidity: number;
|
||||||
|
maxHumidity: number;
|
||||||
windSpeed: number;
|
windSpeed: number;
|
||||||
|
minWind: number;
|
||||||
|
maxWind: number;
|
||||||
waterTotal: number;
|
waterTotal: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -198,7 +202,11 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
|
|||||||
minExp: min('experience_temperature'),
|
minExp: min('experience_temperature'),
|
||||||
maxExp: max('experience_temperature'),
|
maxExp: max('experience_temperature'),
|
||||||
humidity: last.humidity,
|
humidity: last.humidity,
|
||||||
|
minHumidity: min('humidity'),
|
||||||
|
maxHumidity: max('humidity'),
|
||||||
windSpeed: last.wind_speed,
|
windSpeed: last.wind_speed,
|
||||||
|
minWind: min('wind_speed'),
|
||||||
|
maxWind: max('wind_speed'),
|
||||||
waterTotal: last.water_total,
|
waterTotal: last.water_total,
|
||||||
};
|
};
|
||||||
}, [dayRows]);
|
}, [dayRows]);
|
||||||
@@ -248,6 +256,12 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => {
|
|||||||
setpoint_C: getTempStandardByDay(dayAge),
|
setpoint_C: getTempStandardByDay(dayAge),
|
||||||
suhu_min_C: summary?.minTemp ?? null,
|
suhu_min_C: summary?.minTemp ?? null,
|
||||||
suhu_max_C: summary?.maxTemp ?? null,
|
suhu_max_C: summary?.maxTemp ?? null,
|
||||||
|
experience_suhu_min_C: summary?.minExp ?? null,
|
||||||
|
experience_suhu_max_C: summary?.maxExp ?? null,
|
||||||
|
kelembapan_min_persen: summary?.minHumidity ?? null,
|
||||||
|
kelembapan_max_persen: summary?.maxHumidity ?? null,
|
||||||
|
kecepatan_angin_min_mPerDetik: summary?.minWind ?? null,
|
||||||
|
kecepatan_angin_max_mPerDetik: summary?.maxWind ?? null,
|
||||||
waktu_data: selectedDate || null,
|
waktu_data: selectedDate || null,
|
||||||
});
|
});
|
||||||
}, [selectedKandang, selectedCycle, summary, dayAge, selectedDate]);
|
}, [selectedKandang, selectedCycle, summary, dayAge, selectedDate]);
|
||||||
|
|||||||
@@ -74,6 +74,20 @@ const renderValue = (val: unknown, suffix: string = ''): string => {
|
|||||||
return `${val} ${suffix}`.trim();
|
return `${val} ${suffix}`.trim();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Format a day's min–max range for IoT panel insight metrics (no rounding). */
|
||||||
|
const renderDayRange = (minVal: unknown, maxVal: unknown, suffix: string): string => {
|
||||||
|
const lo = typeof minVal === 'number' && Number.isFinite(minVal) ? minVal : null;
|
||||||
|
const hi = typeof maxVal === 'number' && Number.isFinite(maxVal) ? maxVal : null;
|
||||||
|
// Keep full precision — do not toFixed / cap fraction digits.
|
||||||
|
const fmt = (n: number) =>
|
||||||
|
n.toLocaleString('id-ID', { maximumFractionDigits: 20, useGrouping: false });
|
||||||
|
if (lo === null && hi === null) return 'N/A';
|
||||||
|
if (lo === null) return `${fmt(hi!)} ${suffix}`.trim();
|
||||||
|
if (hi === null) return `${fmt(lo)} ${suffix}`.trim();
|
||||||
|
if (lo === hi) return `${fmt(lo)} ${suffix}`.trim();
|
||||||
|
return `${fmt(lo)} – ${fmt(hi)} ${suffix}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
/** Match FeedSaldoCard: masuk IOT + saldo awal. */
|
/** Match FeedSaldoCard: masuk IOT + saldo awal. */
|
||||||
const feedMasukTotal = (ctx: Record<string, unknown>): number | null => {
|
const feedMasukTotal = (ctx: Record<string, unknown>): number | null => {
|
||||||
const masuk =
|
const masuk =
|
||||||
@@ -330,24 +344,40 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (topic === 'iot_panel') {
|
if (topic === 'iot_panel') {
|
||||||
const actual = n(displayContext.suhu_rata_rata_C);
|
const minTemp = n(displayContext.suhu_min_C);
|
||||||
|
const maxTemp = n(displayContext.suhu_max_C);
|
||||||
|
const lastTemp = n(displayContext.suhu_rata_rata_C);
|
||||||
const setpoint = n(displayContext.setpoint_C);
|
const setpoint = n(displayContext.setpoint_C);
|
||||||
if (actual === null) return null;
|
if (minTemp === null && maxTemp === null && lastTemp === null) return null;
|
||||||
const diff = setpoint === null ? null : actual - setpoint;
|
const lo = minTemp ?? lastTemp!;
|
||||||
|
const hi = maxTemp ?? lastTemp!;
|
||||||
|
// Grade against the farther extreme from setpoint so a bad stretch that day surfaces.
|
||||||
|
const worstDiff =
|
||||||
|
setpoint === null
|
||||||
|
? null
|
||||||
|
: Math.max(Math.abs(lo - setpoint), Math.abs(hi - setpoint));
|
||||||
|
const signedWorst =
|
||||||
|
setpoint === null
|
||||||
|
? null
|
||||||
|
: Math.abs(lo - setpoint) >= Math.abs(hi - setpoint)
|
||||||
|
? lo - setpoint
|
||||||
|
: hi - setpoint;
|
||||||
return {
|
return {
|
||||||
label: 'Suhu Rata-rata Kandang',
|
label: 'Suhu Rata-rata Kandang',
|
||||||
value: actual.toLocaleString('id-ID'),
|
value: renderDayRange(lo, hi, ''),
|
||||||
unit: '°C',
|
unit: '°C',
|
||||||
context: setpoint === null ? null : `Setpoint: ${setpoint.toLocaleString('id-ID')} °C`,
|
context: setpoint === null ? null : `Setpoint: ${setpoint.toLocaleString('id-ID')} °C`,
|
||||||
status: (diff === null
|
status: (worstDiff === null
|
||||||
? 'unknown'
|
? 'unknown'
|
||||||
: Math.abs(diff) <= 1
|
: worstDiff <= 1
|
||||||
? 'ok'
|
? 'ok'
|
||||||
: Math.abs(diff) <= 2.5
|
: worstDiff <= 2.5
|
||||||
? 'warning'
|
? 'warning'
|
||||||
: 'critical') as InsightStatus,
|
: 'critical') as InsightStatus,
|
||||||
deltaLabel:
|
deltaLabel:
|
||||||
diff === null ? null : `${diff > 0 ? '+' : ''}${diff.toFixed(1)} °C vs setpoint`,
|
signedWorst === null
|
||||||
|
? null
|
||||||
|
: `${signedWorst > 0 ? '+' : ''}${signedWorst} °C vs setpoint`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,14 +841,6 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
|
|
||||||
{topic === 'eef' && (
|
{topic === 'eef' && (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
|
||||||
<span className="text-sm text-gray-600 font-medium">EEF Aktual</span>
|
|
||||||
<span className="text-sm font-bold text-gray-900">
|
|
||||||
{typeof displayContext.eef_terakhir === 'number'
|
|
||||||
? formatRatio(displayContext.eef_terakhir, 3)
|
|
||||||
: renderValue(displayContext.eef_terakhir)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
<span className="text-sm text-gray-600 font-medium">FCR Aktual</span>
|
<span className="text-sm text-gray-600 font-medium">FCR Aktual</span>
|
||||||
<span className="text-sm font-bold text-gray-900">
|
<span className="text-sm font-bold text-gray-900">
|
||||||
@@ -830,7 +852,31 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
<span className="text-sm text-gray-600 font-medium">Persentase Hidup</span>
|
<span className="text-sm text-gray-600 font-medium">Persentase Hidup</span>
|
||||||
<span className="text-sm font-bold text-gray-900">
|
<span className="text-sm font-bold text-gray-900">
|
||||||
{renderValue(displayContext.persen_hidup, '%')}
|
{typeof displayContext.persen_hidup === 'number'
|
||||||
|
? `${displayContext.persen_hidup.toLocaleString('id-ID', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})} %`
|
||||||
|
: renderValue(displayContext.persen_hidup, '%')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
|
<span className="text-sm text-gray-600 font-medium">BW Rata-rata Panen</span>
|
||||||
|
<span className="text-sm font-bold text-gray-900">
|
||||||
|
{typeof displayContext.bw_panen_gram === 'number'
|
||||||
|
? `${displayContext.bw_panen_gram.toLocaleString('id-ID', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})} g`
|
||||||
|
: renderValue(displayContext.bw_panen_gram, 'g')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
|
<span className="text-sm text-gray-600 font-medium">Umur Rata-rata Panen</span>
|
||||||
|
<span className="text-sm font-bold text-gray-900">
|
||||||
|
{typeof displayContext.umur_panen_rata_hari === 'number'
|
||||||
|
? `${displayContext.umur_panen_rata_hari.toLocaleString('id-ID', {
|
||||||
|
maximumFractionDigits: 2,
|
||||||
|
})} hari`
|
||||||
|
: renderValue(displayContext.umur_panen_rata_hari, 'hari')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -838,16 +884,40 @@ export const PageAiInsight: React.FC<PageAiInsightProps> = ({
|
|||||||
|
|
||||||
{topic === 'iot_panel' && (
|
{topic === 'iot_panel' && (
|
||||||
<>
|
<>
|
||||||
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
|
<span className="text-sm text-gray-600 font-medium">Experience Suhu</span>
|
||||||
|
<span className="text-sm font-bold text-gray-900">
|
||||||
|
{renderDayRange(
|
||||||
|
displayContext.experience_suhu_min_C ??
|
||||||
|
displayContext.experience_suhu_C,
|
||||||
|
displayContext.experience_suhu_max_C ??
|
||||||
|
displayContext.experience_suhu_C,
|
||||||
|
'°C'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
<span className="text-sm text-gray-600 font-medium">Kelembapan</span>
|
<span className="text-sm text-gray-600 font-medium">Kelembapan</span>
|
||||||
<span className="text-sm font-bold text-gray-900">
|
<span className="text-sm font-bold text-gray-900">
|
||||||
{renderValue(displayContext.kelembapan_persen, '%')}
|
{renderDayRange(
|
||||||
|
displayContext.kelembapan_min_persen ??
|
||||||
|
displayContext.kelembapan_persen,
|
||||||
|
displayContext.kelembapan_max_persen ??
|
||||||
|
displayContext.kelembapan_persen,
|
||||||
|
'%'
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
<div className="flex justify-between items-center py-1 border-b border-gray-50">
|
||||||
<span className="text-sm text-gray-600 font-medium">Kecepatan Angin</span>
|
<span className="text-sm text-gray-600 font-medium">Kecepatan Angin</span>
|
||||||
<span className="text-sm font-bold text-gray-900">
|
<span className="text-sm font-bold text-gray-900">
|
||||||
{renderValue(displayContext.kecepatan_angin_mPerDetik, 'm/s')}
|
{renderDayRange(
|
||||||
|
displayContext.kecepatan_angin_min_mPerDetik ??
|
||||||
|
displayContext.kecepatan_angin_mPerDetik,
|
||||||
|
displayContext.kecepatan_angin_max_mPerDetik ??
|
||||||
|
displayContext.kecepatan_angin_mPerDetik,
|
||||||
|
'm/s'
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { getTargetWeightForAge } from './weight.ts';
|
|||||||
const num = (value: unknown): number | null =>
|
const num = (value: unknown): number | null =>
|
||||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
|
||||||
|
/** Round to at most 2 decimal places (avoids float noise like 89.50399999999999). */
|
||||||
|
const round2 = (value: number | null): number | null =>
|
||||||
|
value === null ? null : Math.round(value * 100) / 100;
|
||||||
|
|
||||||
/** Positive finite number only — 0 / negative treated as missing sensor data. */
|
/** Positive finite number only — 0 / negative treated as missing sensor data. */
|
||||||
const positiveNum = (value: unknown): number | null => {
|
const positiveNum = (value: unknown): number | null => {
|
||||||
const n = num(value);
|
const n = num(value);
|
||||||
@@ -162,7 +166,7 @@ export function buildEefInsightContext(args: BuildEefInsightContextArgs): Insigh
|
|||||||
hari,
|
hari,
|
||||||
eef: num(row.eef),
|
eef: num(row.eef),
|
||||||
fcr: num(row.fcr),
|
fcr: num(row.fcr),
|
||||||
persen_hidup: num(row.chickenLifePercentage) ?? num(row.persen_hidup),
|
persen_hidup: round2(num(row.chickenLifePercentage) ?? num(row.persen_hidup)),
|
||||||
bw_panen: num(row.averageHarvestBw) ?? num(row.bw_panen_gram),
|
bw_panen: num(row.averageHarvestBw) ?? num(row.bw_panen_gram),
|
||||||
umur_panen_hari: num(row.averageHarvestDay),
|
umur_panen_hari: num(row.averageHarvestDay),
|
||||||
};
|
};
|
||||||
@@ -180,7 +184,7 @@ export function buildEefInsightContext(args: BuildEefInsightContextArgs): Insigh
|
|||||||
totalDays: args.totalDays ?? null,
|
totalDays: args.totalDays ?? null,
|
||||||
eef_terakhir: args.eef_terakhir ?? latest?.eef ?? null,
|
eef_terakhir: args.eef_terakhir ?? latest?.eef ?? null,
|
||||||
fcr_terakhir: args.fcr_terakhir ?? latest?.fcr ?? null,
|
fcr_terakhir: args.fcr_terakhir ?? latest?.fcr ?? null,
|
||||||
persen_hidup: args.persen_hidup ?? latest?.persen_hidup ?? null,
|
persen_hidup: round2(args.persen_hidup ?? latest?.persen_hidup ?? null),
|
||||||
bw_panen_gram: args.bw_panen_gram ?? latest?.bw_panen ?? null,
|
bw_panen_gram: args.bw_panen_gram ?? latest?.bw_panen ?? null,
|
||||||
umur_panen_rata_hari: args.umur_panen_rata_hari ?? latest?.umur_panen_hari ?? null,
|
umur_panen_rata_hari: args.umur_panen_rata_hari ?? latest?.umur_panen_hari ?? null,
|
||||||
tren_eef_harian: tren,
|
tren_eef_harian: tren,
|
||||||
@@ -273,6 +277,12 @@ export type BuildIotInsightContextArgs = {
|
|||||||
setpoint_C?: number | null;
|
setpoint_C?: number | null;
|
||||||
suhu_min_C?: number | null;
|
suhu_min_C?: number | null;
|
||||||
suhu_max_C?: number | null;
|
suhu_max_C?: number | null;
|
||||||
|
experience_suhu_min_C?: number | null;
|
||||||
|
experience_suhu_max_C?: number | null;
|
||||||
|
kelembapan_min_persen?: number | null;
|
||||||
|
kelembapan_max_persen?: number | null;
|
||||||
|
kecepatan_angin_min_mPerDetik?: number | null;
|
||||||
|
kecepatan_angin_max_mPerDetik?: number | null;
|
||||||
waktu_data?: string | null;
|
waktu_data?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,6 +303,12 @@ export function buildIotInsightContext(args: BuildIotInsightContextArgs): Insigh
|
|||||||
setpoint_C: args.setpoint_C ?? null,
|
setpoint_C: args.setpoint_C ?? null,
|
||||||
suhu_min_C: args.suhu_min_C ?? null,
|
suhu_min_C: args.suhu_min_C ?? null,
|
||||||
suhu_max_C: args.suhu_max_C ?? null,
|
suhu_max_C: args.suhu_max_C ?? null,
|
||||||
|
experience_suhu_min_C: args.experience_suhu_min_C ?? null,
|
||||||
|
experience_suhu_max_C: args.experience_suhu_max_C ?? null,
|
||||||
|
kelembapan_min_persen: args.kelembapan_min_persen ?? null,
|
||||||
|
kelembapan_max_persen: args.kelembapan_max_persen ?? null,
|
||||||
|
kecepatan_angin_min_mPerDetik: args.kecepatan_angin_min_mPerDetik ?? null,
|
||||||
|
kecepatan_angin_max_mPerDetik: args.kecepatan_angin_max_mPerDetik ?? null,
|
||||||
waktu_data: args.waktu_data ?? null,
|
waktu_data: args.waktu_data ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user