diff --git a/components/IotPanelPage.tsx b/components/IotPanelPage.tsx index fd90640..bbdd5f7 100644 --- a/components/IotPanelPage.tsx +++ b/components/IotPanelPage.tsx @@ -12,7 +12,14 @@ import { api, errorMessage } from '../services/apiClient.ts'; import type { Flock, PanelIoT } from '../types/api.ts'; import { useFarm } from '../context/FarmContext.tsx'; import { Banner, EmptyFarm, PageHeader, Spinner } from './ui/Feedback.tsx'; -import { cycleDayForDate, formatDateId, formatDateRange, formatNumber } from '../utils/format.ts'; +import { + cycleDayForDate, + formatDateId, + formatDateRange, + formatNumber, + resolveCycleBoundDate, + todayIso, +} from '../utils/format.ts'; import { getTempStandardByDay, getWaterStandardByDay, @@ -53,7 +60,11 @@ type DaySummary = { minExp: number; maxExp: number; humidity: number; + minHumidity: number; + maxHumidity: number; windSpeed: number; + minWind: number; + maxWind: number; waterTotal: number; }; @@ -126,11 +137,19 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { if (cancelled) return; const sortedDates = [...dateList].sort(); setDates(sortedDates); - setSelectedDate((previous) => - previous && sortedDates.includes(previous) - ? previous - : (sortedDates[sortedDates.length - 1] ?? '') - ); + const cycleMin = selectedCycle.start_date; + const cycleMax = resolveCycleBoundDate(selectedCycle); + const fallbackDate = + todayIso() > cycleMax ? cycleMax : todayIso() < cycleMin ? cycleMin : todayIso(); + setSelectedDate((previous) => { + if (sortedDates.length > 0) { + return previous && sortedDates.includes(previous) + ? previous + : (sortedDates[sortedDates.length - 1] ?? fallbackDate); + } + if (previous && previous >= cycleMin && previous <= cycleMax) return previous; + return fallbackDate; + }); } catch (err) { if (!cancelled) setError(errorMessage(err)); } finally { @@ -191,7 +210,11 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { minExp: min('experience_temperature'), maxExp: max('experience_temperature'), humidity: last.humidity, + minHumidity: min('humidity'), + maxHumidity: max('humidity'), windSpeed: last.wind_speed, + minWind: min('wind_speed'), + maxWind: max('wind_speed'), waterTotal: last.water_total, }; }, [dayRows]); @@ -243,8 +266,19 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { 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, }); - }, [selectedKandang, selectedCycle, dayAge, summary]); + }, [selectedKandang, selectedCycle, summary, dayAge, selectedDate]); + + const datePickerMin = selectedCycle?.start_date ?? undefined; + // IoT panel is real-time — not subject to the publish cutoff that + // resolveCycleBoundDate() applies via soft_end_date → visible_through_date(). + // Closed cycles use end_date; open cycles go up to today. + const datePickerMax = selectedCycle?.end_date ?? todayIso(); if (!selectedCycle) return ; @@ -261,9 +295,9 @@ const IotPanelPage: React.FC<{ onSettings: () => void }> = ({ onSettings }) => { setSelectedFlockId(event.target.value ? Number(event.target.value) : null) } className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700" - aria-label="Pilih flock" + aria-label="Pilih lantai" > - {flocks.length === 0 && } + {flocks.length === 0 && } {flocks.map((flock) => (