Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 1s
629 lines
28 KiB
TypeScript
629 lines
28 KiB
TypeScript
"use client"
|
||
import { useEffect, useMemo, useState, useCallback, Suspense } from 'react'
|
||
import { useRouter, useSearchParams } from 'next/navigation'
|
||
import { api, TelemetryDto, DailyReportDto } from '@/lib/api'
|
||
import { persianToGregorian, getCurrentPersianDay, getCurrentPersianYear, getCurrentPersianMonth, getPreviousPersianDay, getNextPersianDay } from '@/lib/persian-date'
|
||
import { BarChart3, ChevronRight, ChevronLeft, Calendar as CalendarIcon, Bell } from 'lucide-react'
|
||
import Link from 'next/link'
|
||
import Loading from '@/components/Loading'
|
||
import {
|
||
SummaryTab,
|
||
ChartsTab,
|
||
WeatherTab,
|
||
AnalysisTab,
|
||
TABS,
|
||
TabType,
|
||
WeatherData,
|
||
ensureDateFormat,
|
||
formatPersianDate,
|
||
QOM_LAT,
|
||
QOM_LON,
|
||
detectDataGaps,
|
||
DataGap
|
||
} from '@/components/daily-report'
|
||
|
||
function DailyReportContent() {
|
||
const router = useRouter()
|
||
const searchParams = useSearchParams()
|
||
const [telemetry, setTelemetry] = useState<TelemetryDto[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [activeTab, setActiveTab] = useState<TabType>('summary')
|
||
const [dailyReport, setDailyReport] = useState<DailyReportDto | null>(null)
|
||
const [analysisLoading, setAnalysisLoading] = useState(false)
|
||
const [analysisError, setAnalysisError] = useState<string | null>(null)
|
||
const [weatherData, setWeatherData] = useState<WeatherData | null>(null)
|
||
const [weatherLoading, setWeatherLoading] = useState(false)
|
||
const [weatherError, setWeatherError] = useState<string | null>(null)
|
||
const [expandedDayIndex, setExpandedDayIndex] = useState<number | null>(null)
|
||
const [chartStartMinute, setChartStartMinute] = useState(0) // 00:00
|
||
const [chartEndMinute, setChartEndMinute] = useState(1439) // 23:59
|
||
|
||
const deviceId = Number(searchParams.get('deviceId') ?? '1')
|
||
const dateParam = searchParams.get('date') ?? formatPersianDate(getCurrentPersianYear(), getCurrentPersianMonth(), getCurrentPersianDay())
|
||
|
||
const selectedDate = useMemo(() => {
|
||
if (!dateParam) return null
|
||
try {
|
||
const decodedDate = decodeURIComponent(dateParam)
|
||
// Ensure date is in yyyy/MM/dd format
|
||
return ensureDateFormat(decodedDate)
|
||
} catch (error) {
|
||
console.error('Error decoding date parameter:', error)
|
||
return null
|
||
}
|
||
}, [dateParam])
|
||
|
||
// Navigate to previous day
|
||
const goToPreviousDay = useCallback(() => {
|
||
if (!selectedDate) return
|
||
const prevDay = getPreviousPersianDay(selectedDate)
|
||
if (prevDay) {
|
||
router.push(`/daily-report?deviceId=${deviceId}&date=${encodeURIComponent(prevDay)}`)
|
||
}
|
||
}, [selectedDate, deviceId, router])
|
||
|
||
// Navigate to next day
|
||
const goToNextDay = useCallback(() => {
|
||
if (!selectedDate) return
|
||
const nextDay = getNextPersianDay(selectedDate)
|
||
if (nextDay) {
|
||
router.push(`/daily-report?deviceId=${deviceId}&date=${encodeURIComponent(nextDay)}`)
|
||
}
|
||
}, [selectedDate, deviceId, router])
|
||
|
||
// Navigate to calendar to select a date
|
||
const goToCalendar = useCallback(() => {
|
||
router.push(`/calendar?deviceId=${deviceId}`)
|
||
}, [deviceId, router])
|
||
|
||
const loadData = useCallback(async () => {
|
||
if (!selectedDate) {
|
||
setLoading(false)
|
||
return
|
||
}
|
||
|
||
setLoading(true)
|
||
|
||
try {
|
||
const [year, month, day] = selectedDate.split('/').map(Number)
|
||
const startDate = persianToGregorian(year, month, day)
|
||
startDate.setHours(0, 0, 0, 0)
|
||
const endDate = new Date(startDate)
|
||
endDate.setHours(23, 59, 59, 999)
|
||
|
||
const startUtc = startDate.toISOString()
|
||
const endUtc = endDate.toISOString()
|
||
|
||
const result = await api.listTelemetry({ deviceId, startUtc, endUtc, pageSize: 100000 })
|
||
setTelemetry(result.items)
|
||
} catch (error) {
|
||
console.error('Error loading telemetry:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [deviceId, selectedDate])
|
||
|
||
const loadAnalysis = useCallback(async () => {
|
||
if (!selectedDate || dailyReport) return
|
||
|
||
setAnalysisLoading(true)
|
||
setAnalysisError(null)
|
||
|
||
try {
|
||
const report = await api.getDailyReport(deviceId, selectedDate)
|
||
setDailyReport(report)
|
||
} catch (error) {
|
||
console.error('Error loading analysis:', error)
|
||
setAnalysisError('خطا در دریافت تحلیل. لطفاً دوباره تلاش کنید.')
|
||
} finally {
|
||
setAnalysisLoading(false)
|
||
}
|
||
}, [deviceId, selectedDate, dailyReport])
|
||
|
||
const loadWeather = useCallback(async () => {
|
||
if (weatherData) return
|
||
|
||
setWeatherLoading(true)
|
||
setWeatherError(null)
|
||
|
||
try {
|
||
if (!selectedDate) {
|
||
setWeatherError('تاریخ انتخاب نشده است')
|
||
return
|
||
}
|
||
|
||
// تبدیل تاریخ شمسی به میلادی
|
||
const [year, month, day] = selectedDate.split('/').map(Number)
|
||
const gregorianDate = persianToGregorian(year, month, day)
|
||
|
||
// بررسی اینکه تاریخ امروز است یا گذشته
|
||
const today = new Date()
|
||
today.setHours(0, 0, 0, 0)
|
||
gregorianDate.setHours(0, 0, 0, 0)
|
||
|
||
const isPast = gregorianDate.getTime() < today.getTime()
|
||
|
||
let weather: WeatherData
|
||
|
||
if (isPast) {
|
||
// استفاده از Historical API برای روزهای گذشته
|
||
const dateStr = gregorianDate.toISOString().split('T')[0] // YYYY-MM-DD
|
||
const response = await fetch(
|
||
`https://archive-api.open-meteo.com/v1/archive?latitude=${QOM_LAT}&longitude=${QOM_LON}&start_date=${dateStr}&end_date=${dateStr}&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max,sunshine_duration&timezone=Asia/Tehran`
|
||
)
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch historical weather data')
|
||
}
|
||
|
||
const data = await response.json()
|
||
|
||
// ساختار داده برای روزهای گذشته (بدون current و hourly)
|
||
weather = {
|
||
current: {
|
||
temperature: data.daily.temperature_2m_max?.[0] || 0,
|
||
humidity: 0, // Historical API رطوبت ندارد
|
||
windSpeed: data.daily.wind_speed_10m_max?.[0] || 0,
|
||
weatherCode: data.daily.weather_code?.[0] || 0,
|
||
},
|
||
hourly: [], // برای گذشته hourly نداریم
|
||
daily: [{
|
||
date: data.daily.time?.[0] || dateStr,
|
||
tempMax: data.daily.temperature_2m_max?.[0] || 0,
|
||
tempMin: data.daily.temperature_2m_min?.[0] || 0,
|
||
weatherCode: data.daily.weather_code?.[0] || 0,
|
||
precipitation: data.daily.precipitation_sum?.[0] || 0,
|
||
precipitationProbability: 0,
|
||
uvIndexMax: 0,
|
||
sunshineDuration: data.daily.sunshine_duration?.[0] || 0,
|
||
windSpeedMax: data.daily.wind_speed_10m_max?.[0] || 0,
|
||
}]
|
||
}
|
||
} else {
|
||
// استفاده از Forecast API برای امروز و آینده
|
||
const response = await fetch(
|
||
`https://api.open-meteo.com/v1/forecast?latitude=${QOM_LAT}&longitude=${QOM_LON}¤t=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,weather_code,precipitation&daily=weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,uv_index_max,sunshine_duration,wind_speed_10m_max&timezone=Asia/Tehran&forecast_days=7`
|
||
)
|
||
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch weather data')
|
||
}
|
||
|
||
const data = await response.json()
|
||
|
||
// Get only today's hourly data (first 24 hours)
|
||
const todayHourly = data.hourly.time.slice(0, 24).map((time: string, i: number) => ({
|
||
time,
|
||
temperature: data.hourly.temperature_2m[i],
|
||
humidity: data.hourly.relative_humidity_2m[i],
|
||
weatherCode: data.hourly.weather_code[i],
|
||
precipitation: data.hourly.precipitation[i],
|
||
}))
|
||
|
||
weather = {
|
||
current: {
|
||
temperature: data.current.temperature_2m,
|
||
humidity: data.current.relative_humidity_2m,
|
||
windSpeed: data.current.wind_speed_10m,
|
||
weatherCode: data.current.weather_code,
|
||
},
|
||
hourly: todayHourly,
|
||
daily: data.daily.time.map((date: string, i: number) => ({
|
||
date,
|
||
tempMax: data.daily.temperature_2m_max[i],
|
||
tempMin: data.daily.temperature_2m_min[i],
|
||
weatherCode: data.daily.weather_code[i],
|
||
precipitation: data.daily.precipitation_sum[i],
|
||
precipitationProbability: data.daily.precipitation_probability_max[i],
|
||
uvIndexMax: data.daily.uv_index_max[i],
|
||
sunshineDuration: data.daily.sunshine_duration[i],
|
||
windSpeedMax: data.daily.wind_speed_10m_max[i],
|
||
}))
|
||
}
|
||
}
|
||
|
||
setWeatherData(weather)
|
||
} catch (error) {
|
||
console.error('Error loading weather:', error)
|
||
setWeatherError('خطا در دریافت اطلاعات آب و هوا. لطفاً دوباره تلاش کنید.')
|
||
} finally {
|
||
setWeatherLoading(false)
|
||
}
|
||
}, [weatherData, selectedDate])
|
||
|
||
useEffect(() => {
|
||
// Reset states when date or device changes
|
||
setDailyReport(null)
|
||
setWeatherData(null)
|
||
setAnalysisError(null)
|
||
setWeatherError(null)
|
||
loadData()
|
||
}, [loadData, deviceId, selectedDate])
|
||
|
||
// Load analysis when switching to analysis tab
|
||
useEffect(() => {
|
||
if (activeTab === 'analysis') {
|
||
loadAnalysis()
|
||
}
|
||
}, [activeTab, loadAnalysis])
|
||
|
||
// Load weather when switching to weather tab
|
||
useEffect(() => {
|
||
if (activeTab === 'weather') {
|
||
loadWeather()
|
||
}
|
||
}, [activeTab, loadWeather])
|
||
|
||
const sortedTelemetry = useMemo(() => {
|
||
return [...telemetry].sort((a, b) => {
|
||
const aTime = a.serverTimestampUtc || a.timestampUtc
|
||
const bTime = b.serverTimestampUtc || b.timestampUtc
|
||
return new Date(aTime).getTime() - new Date(bTime).getTime()
|
||
})
|
||
}, [telemetry])
|
||
|
||
// Data arrays
|
||
const soil = useMemo(() => sortedTelemetry.map(t => Number(t.soilPercent ?? 0)), [sortedTelemetry])
|
||
const temp = useMemo(() => sortedTelemetry.map(t => Number(t.temperatureC ?? 0)), [sortedTelemetry])
|
||
const hum = useMemo(() => sortedTelemetry.map(t => Number(t.humidityPercent ?? 0)), [sortedTelemetry])
|
||
const gas = useMemo(() => sortedTelemetry.map(t => Number(t.gasPPM ?? 0)), [sortedTelemetry])
|
||
const lux = useMemo(() => sortedTelemetry.map(t => Number(t.lux ?? 0)), [sortedTelemetry])
|
||
|
||
// Min/Max calculations (not currently used but kept for potential future use)
|
||
// const tempMinMax = useMemo(() => {
|
||
// const min = Math.min(...temp)
|
||
// const max = Math.max(...temp)
|
||
// return {
|
||
// min: min < 0 ? Math.floor(min / 10) * 10 : 0,
|
||
// max: max > 40 ? Math.floor(max / 10) * 10 : 40
|
||
// }
|
||
// }, [temp])
|
||
|
||
// const luxMinMax = useMemo(() => {
|
||
// const max = Math.max(...lux)
|
||
// return {
|
||
// min: 0,
|
||
// max: max > 2000 ? Math.floor(max / 1000) * 1000 : 2000
|
||
// }
|
||
// }, [lux])
|
||
|
||
// Detect data gaps in the full day data
|
||
const dataGaps = useMemo(() => {
|
||
const timestamps = sortedTelemetry.map(t => t.serverTimestampUtc || t.timestampUtc)
|
||
return detectDataGaps(timestamps, 30) // 30 minutes threshold
|
||
}, [sortedTelemetry])
|
||
|
||
// Filtered telemetry for charts based on minute range
|
||
const filteredTelemetryForCharts = useMemo(() => {
|
||
return sortedTelemetry.filter(t => {
|
||
const timestamp = t.serverTimestampUtc || t.timestampUtc
|
||
const date = new Date(timestamp)
|
||
const minuteOfDay = date.getHours() * 60 + date.getMinutes()
|
||
return minuteOfDay >= chartStartMinute && minuteOfDay <= chartEndMinute
|
||
})
|
||
}, [sortedTelemetry, chartStartMinute, chartEndMinute])
|
||
|
||
// Detect gaps in filtered data
|
||
const filteredDataGaps = useMemo(() => {
|
||
const timestamps = filteredTelemetryForCharts.map(t => t.serverTimestampUtc || t.timestampUtc)
|
||
return detectDataGaps(timestamps, 30)
|
||
}, [filteredTelemetryForCharts])
|
||
|
||
// Filtered chart labels
|
||
const chartLabels = useMemo(() => {
|
||
return filteredTelemetryForCharts.map(t => {
|
||
const timestamp = t.serverTimestampUtc || t.timestampUtc
|
||
const date = new Date(timestamp)
|
||
const hours = date.getHours().toString().padStart(2, '0')
|
||
const minutes = date.getMinutes().toString().padStart(2, '0')
|
||
const seconds = date.getSeconds().toString().padStart(2, '0')
|
||
return `${hours}:${minutes}:${seconds}`
|
||
})
|
||
}, [filteredTelemetryForCharts])
|
||
|
||
// Helper function to insert nulls for gaps
|
||
const insertGapsInData = (data: number[], timestamps: string[], gaps: DataGap[]): (number | null)[] => {
|
||
if (gaps.length === 0 || data.length < 2) return data
|
||
|
||
const result: (number | null)[] = []
|
||
|
||
for (let i = 0; i < data.length; i++) {
|
||
result.push(data[i])
|
||
|
||
// Check if there's a gap after this point
|
||
if (i < data.length - 1) {
|
||
const currentTime = new Date(timestamps[i])
|
||
const nextTime = new Date(timestamps[i + 1])
|
||
const currentMinute = currentTime.getHours() * 60 + currentTime.getMinutes()
|
||
const nextMinute = nextTime.getHours() * 60 + nextTime.getMinutes()
|
||
|
||
// Find if any gap exists between current and next
|
||
const hasGap = gaps.some(gap =>
|
||
currentMinute <= gap.startMinute && nextMinute >= gap.endMinute
|
||
)
|
||
|
||
if (hasGap) {
|
||
result.push(null) // Insert null to break the line
|
||
}
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
// Filtered data arrays for charts (with gaps as null)
|
||
const filteredTimestamps = useMemo(() =>
|
||
filteredTelemetryForCharts.map(t => t.serverTimestampUtc || t.timestampUtc),
|
||
[filteredTelemetryForCharts]
|
||
)
|
||
|
||
const chartSoil = useMemo(() => {
|
||
const data = filteredTelemetryForCharts.map(t => Number(t.soilPercent ?? 0))
|
||
return insertGapsInData(data, filteredTimestamps, filteredDataGaps)
|
||
}, [filteredTelemetryForCharts, filteredTimestamps, filteredDataGaps])
|
||
|
||
const chartTemp = useMemo(() => {
|
||
const data = filteredTelemetryForCharts.map(t => Number(t.temperatureC ?? 0))
|
||
return insertGapsInData(data, filteredTimestamps, filteredDataGaps)
|
||
}, [filteredTelemetryForCharts, filteredTimestamps, filteredDataGaps])
|
||
|
||
const chartHum = useMemo(() => {
|
||
const data = filteredTelemetryForCharts.map(t => Number(t.humidityPercent ?? 0))
|
||
return insertGapsInData(data, filteredTimestamps, filteredDataGaps)
|
||
}, [filteredTelemetryForCharts, filteredTimestamps, filteredDataGaps])
|
||
|
||
const chartGas = useMemo(() => {
|
||
const data = filteredTelemetryForCharts.map(t => Number(t.gasPPM ?? 0))
|
||
return insertGapsInData(data, filteredTimestamps, filteredDataGaps)
|
||
}, [filteredTelemetryForCharts, filteredTimestamps, filteredDataGaps])
|
||
|
||
const chartLux = useMemo(() => {
|
||
const data = filteredTelemetryForCharts.map(t => Number(t.lux ?? 0))
|
||
return insertGapsInData(data, filteredTimestamps, filteredDataGaps)
|
||
}, [filteredTelemetryForCharts, filteredTimestamps, filteredDataGaps])
|
||
|
||
// Min/Max calculations for filtered charts (filter out nulls)
|
||
const chartTempMinMax = useMemo(() => {
|
||
const validTemps = chartTemp.filter((t): t is number => t !== null)
|
||
if (validTemps.length === 0) return { min: 0, max: 40 }
|
||
const min = Math.min(...validTemps)
|
||
const max = Math.max(...validTemps)
|
||
return {
|
||
min: min < 0 ? Math.floor(min / 10) * 10 : 0,
|
||
max: max > 40 ? Math.floor(max / 10) * 10 : 40
|
||
}
|
||
}, [chartTemp])
|
||
|
||
const chartLuxMinMax = useMemo(() => {
|
||
const validLux = chartLux.filter((l): l is number => l !== null)
|
||
if (validLux.length === 0) return { min: 0, max: 2000 }
|
||
const max = Math.max(...validLux)
|
||
return {
|
||
min: 0,
|
||
max: max > 2000 ? Math.floor(max / 1000) * 1000 : 2000
|
||
}
|
||
}, [chartLux])
|
||
|
||
if (loading) {
|
||
return <Loading message="در حال بارگذاری دادهها..." />
|
||
}
|
||
|
||
if (!selectedDate) {
|
||
return (
|
||
<div className="min-h-screen flex items-center justify-center p-4">
|
||
<div className="text-center">
|
||
<CalendarIcon className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
||
<div className="text-lg text-red-600 mb-4">تاریخ انتخاب نشده است</div>
|
||
<button
|
||
onClick={goToCalendar}
|
||
className="inline-flex items-center gap-2 border border-gray-300 px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||
>
|
||
<ChevronRight className="w-4 h-4" />
|
||
بازگشت به تقویم
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen p-4 md:p-6">
|
||
<div className="max-w-7xl mx-auto space-y-6">
|
||
{/* Header */}
|
||
<div className="mb-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-3">
|
||
<div className="inline-flex items-center justify-center w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl shadow-md">
|
||
<BarChart3 className="w-6 h-6 text-white" />
|
||
</div>
|
||
<div>
|
||
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">
|
||
گزارش روزانه {selectedDate}
|
||
</h1>
|
||
<p className="text-sm text-gray-500 mt-1">
|
||
مشاهده خلاصه و نمودارهای روز
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<Link
|
||
href={`/alert-settings?deviceId=${deviceId}`}
|
||
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-orange-300 hover:border-orange-500 hover:bg-orange-50 text-orange-700 hover:text-orange-800 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
|
||
>
|
||
<Bell className="w-5 h-5" />
|
||
<span className="hidden sm:inline">تنظیمات هشدار</span>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Date Navigation Buttons */}
|
||
<div className="flex items-center justify-center gap-3 mb-4">
|
||
<button
|
||
onClick={goToPreviousDay}
|
||
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-gray-200 hover:border-indigo-500 hover:bg-indigo-50 text-gray-700 hover:text-indigo-600 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
|
||
>
|
||
<ChevronRight className="w-5 h-5" />
|
||
روز قبل
|
||
</button>
|
||
<button
|
||
onClick={goToCalendar}
|
||
className="flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg"
|
||
>
|
||
<CalendarIcon className="w-5 h-5" />
|
||
انتخاب تاریخ
|
||
</button>
|
||
<button
|
||
onClick={goToNextDay}
|
||
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-gray-200 hover:border-indigo-500 hover:bg-indigo-50 text-gray-700 hover:text-indigo-600 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
|
||
>
|
||
روز بعد
|
||
<ChevronLeft className="w-5 h-5" />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Tabs */}
|
||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
||
{/* Segmented Control for Mobile */}
|
||
<div className="p-3 md:p-6 md:pb-0">
|
||
<div className="bg-gray-100 rounded-xl p-1 flex md:hidden">
|
||
{TABS.map(tab => (
|
||
<button
|
||
key={tab.value}
|
||
onClick={() => setActiveTab(tab.value)}
|
||
className={`flex-1 px-2 py-2.5 text-xs font-medium rounded-lg transition-all duration-200 ${
|
||
activeTab === tab.value
|
||
? 'bg-white text-indigo-600 shadow-sm'
|
||
: 'text-gray-600 hover:text-gray-900'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* Desktop Tabs */}
|
||
<div className="hidden md:flex border-b border-gray-200 -mx-6 -mt-6 mb-6">
|
||
{TABS.map(tab => (
|
||
<button
|
||
key={tab.value}
|
||
onClick={() => setActiveTab(tab.value)}
|
||
className={`flex-1 px-6 py-4 text-sm font-medium transition-all duration-200 whitespace-nowrap ${
|
||
activeTab === tab.value
|
||
? 'text-indigo-600 border-b-2 border-indigo-600 bg-indigo-50/50'
|
||
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50'
|
||
}`}
|
||
>
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div className="p-4 md:p-6 md:pt-0">
|
||
{/* Summary Tab */}
|
||
{activeTab === 'summary' && (
|
||
<SummaryTab
|
||
temperature={{
|
||
current: temp.at(-1) ?? 0,
|
||
min: Math.min(...temp),
|
||
max: Math.max(...temp),
|
||
data: temp
|
||
}}
|
||
humidity={{
|
||
current: hum.at(-1) ?? 0,
|
||
min: Math.min(...hum),
|
||
max: Math.max(...hum),
|
||
data: hum
|
||
}}
|
||
soil={{
|
||
current: soil.at(-1) ?? 0,
|
||
min: Math.min(...soil),
|
||
max: Math.max(...soil),
|
||
data: soil
|
||
}}
|
||
gas={{
|
||
current: gas.at(-1) ?? 0,
|
||
min: Math.min(...gas),
|
||
max: Math.max(...gas),
|
||
data: gas
|
||
}}
|
||
lux={{
|
||
current: lux.at(-1) ?? 0,
|
||
min: Math.min(...lux),
|
||
max: Math.max(...lux),
|
||
data: lux
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{/* Charts Tab */}
|
||
{activeTab === 'charts' && (
|
||
<ChartsTab
|
||
chartStartMinute={chartStartMinute}
|
||
chartEndMinute={chartEndMinute}
|
||
onStartMinuteChange={setChartStartMinute}
|
||
onEndMinuteChange={setChartEndMinute}
|
||
labels={chartLabels}
|
||
soil={chartSoil}
|
||
humidity={chartHum}
|
||
temperature={chartTemp}
|
||
lux={chartLux}
|
||
gas={chartGas}
|
||
tempMinMax={chartTempMinMax}
|
||
luxMinMax={chartLuxMinMax}
|
||
totalRecords={filteredTelemetryForCharts.length}
|
||
dataGaps={dataGaps}
|
||
/>
|
||
)}
|
||
|
||
{/* Weather Tab */}
|
||
{activeTab === 'weather' && (
|
||
<WeatherTab
|
||
loading={weatherLoading}
|
||
error={weatherError}
|
||
weatherData={weatherData}
|
||
onRetry={() => {
|
||
setWeatherData(null)
|
||
setWeatherError(null)
|
||
loadWeather()
|
||
}}
|
||
expandedDayIndex={expandedDayIndex}
|
||
onDayToggle={setExpandedDayIndex}
|
||
selectedDate={selectedDate}
|
||
/>
|
||
)}
|
||
|
||
{/* Analysis Tab */}
|
||
{activeTab === 'analysis' && (
|
||
<AnalysisTab
|
||
loading={analysisLoading}
|
||
error={analysisError}
|
||
dailyReport={dailyReport}
|
||
onRetry={() => {
|
||
setDailyReport(null)
|
||
setAnalysisError(null)
|
||
loadAnalysis()
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default function DailyReportPage() {
|
||
return (
|
||
<Suspense fallback={
|
||
<div className="min-h-screen flex items-center justify-center p-4">
|
||
<div className="text-center">
|
||
<div className="w-16 h-16 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
|
||
<p className="text-gray-600">در حال بارگذاری گزارش...</p>
|
||
</div>
|
||
</div>
|
||
}>
|
||
<DailyReportContent />
|
||
</Suspense>
|
||
)
|
||
}
|