Files
GreenHomeUI/src/app/daily-report/page.tsx
alireza 2b8cc37374
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
remove live tab
2026-06-03 01:07:00 +03:30

342 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client"
import { useEffect, useMemo, useState, useCallback, Suspense, lazy } from 'react'
import { useRouter, useSearchParams } from 'next/navigation'
import { api, TelemetryDto, PowerOutageSummaryDto } from '@/lib/api'
import { persianToGregorian, getCurrentPersianDay, getCurrentPersianYear, getCurrentPersianMonth, getPreviousPersianDay, getNextPersianDay } from '@/lib/date/persian-date'
import { formatPersianDate, ensureDateFormat } from '@/lib/format/persian-date'
import { TABS, TabType } from '@/features/daily-report'
import { detectDataGaps } from '@/features/daily-report/utils'
import { BarChart3, CalendarIcon, ChevronRight } from 'lucide-react'
import Loading from '@/components/Loading'
import { SummaryTab } from '@/components/daily-report'
import { fetchForecastWeather, isToday as checkIsToday, WeatherData } from '@/features/weather'
import { Tabs, Button } from '@/components/common'
import { DateNavigation } from '@/components/navigation'
import { usePullToRefresh } from '@/hooks/usePullToRefresh'
import { AppShell } from '@/components/layout'
// Lazy load heavy components
const ChartsTab = lazy(() => import('@/components/daily-report/ChartsTab').then(m => ({ default: m.ChartsTab })))
const WeatherTab = lazy(() => import('@/components/daily-report/WeatherTab').then(m => ({ default: m.WeatherTab })))
const AnalysisTab = lazy(() => import('@/components/daily-report/AnalysisTab').then(m => ({ default: m.AnalysisTab })))
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 [forecastWeather, setForecastWeather] = useState<WeatherData | null>(null)
const [forecastWeatherLoading, setForecastWeatherLoading] = useState(false)
const [visibleParams, setVisibleParams] = useState<string[] | null>(null)
const [powerOutageSummary, setPowerOutageSummary] = useState<PowerOutageSummaryDto | null>(null)
const [powerOutageLoading, setPowerOutageLoading] = useState(false)
// Map summary param to chart key
const paramToChartKey = useCallback((param: string): string => {
const mapping: Record<string, string> = {
temperature: 'temp',
humidity: 'hum',
gas: 'gas',
soil: 'soil',
lux: 'lux',
}
return mapping[param] || param
}, [])
// Handle card click: switch to charts tab and scroll to chart
const handleCardClick = useCallback((param: string) => {
const chartKey = paramToChartKey(param)
setActiveTab('charts')
// Wait for tab to switch and chart to render, then scroll
setTimeout(() => {
const chartElement = document.getElementById(`chart-${chartKey}`)
if (chartElement) {
chartElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}, 100)
}, [paramToChartKey])
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)
console.log(result.items)
// Load visible params from server (controls which parameters are shown)
try {
const params = await api.getDisplayParameters(deviceId)
setVisibleParams(params ?? null)
} catch (err) {
console.error('Error loading display parameters:', err)
setVisibleParams(['gas', 'temperature', 'humidity', 'lux'])
}
} catch (error) {
console.error('Error loading telemetry:', error)
} finally {
setLoading(false)
}
}, [deviceId, selectedDate])
// Load power outage summary
useEffect(() => {
if (!selectedDate) {
setPowerOutageSummary(null)
return
}
const loadPowerOutage = async () => {
setPowerOutageLoading(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 summary = await api.getPowerOutageSummary(deviceId, startUtc, endUtc)
setPowerOutageSummary(summary)
} catch (error) {
console.error('Error loading power outage summary:', error)
setPowerOutageSummary(null)
} finally {
setPowerOutageLoading(false)
}
}
loadPowerOutage()
}, [deviceId, selectedDate])
// Pull-to-refresh for mobile
usePullToRefresh(loadData)
useEffect(() => {
// Reset states when date or device changes
loadData()
}, [loadData, deviceId, selectedDate])
// Load forecast weather data when selectedDate is today - lazy load after main data is loaded
useEffect(() => {
if (!selectedDate || loading) {
setForecastWeather(null)
setForecastWeatherLoading(false)
return
}
const isTodayDate = checkIsToday(selectedDate)
if (isTodayDate) {
// Delay fetch to ensure page is fully loaded first
setForecastWeatherLoading(true)
setForecastWeather(null)
// Use setTimeout to ensure page is fully rendered before fetching
const timer = setTimeout(() => {
fetchForecastWeather()
.then(setForecastWeather)
.catch((error) => {
console.error('Error loading forecast weather:', error)
setForecastWeather(null)
})
.finally(() => {
setForecastWeatherLoading(false)
})
}, 100) // Small delay to ensure page is rendered
return () => clearTimeout(timer)
} else {
setForecastWeather(null)
setForecastWeatherLoading(false)
}
}, [selectedDate, loading])
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])
// Latest values for live summary
const latestValues = useMemo(() => ({
temperature: temp.length > 0 ? temp.at(-1) ?? 0 : 0,
humidity: hum.length > 0 ? hum.at(-1) ?? 0 : 0,
soil: soil.length > 0 ? soil.at(-1) ?? 0 : 0,
gas: gas.length > 0 ? gas.at(-1) ?? 0 : 0,
lux: lux.length > 0 ? lux.at(-1) ?? 0 : 0,
}), [temp, hum, soil, gas, lux])
// Last updated time from the most recent telemetry entry
const lastUpdated = useMemo(() => {
if (sortedTelemetry.length === 0) return undefined
const lastTimestamp = sortedTelemetry.at(-1)?.serverTimestampUtc || sortedTelemetry.at(-1)?.timestampUtc
if (!lastTimestamp) return undefined
const date = new Date(lastTimestamp)
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}, [sortedTelemetry])
// 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])
if (loading) {
return <Loading message="در حال بارگذاری داده‌ها..." />
}
if (!selectedDate) {
return (
<AppShell
pageTitle="گزارش روزانه"
pageIcon={BarChart3}
iconGradient="from-indigo-500 to-purple-600"
>
<div className="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}
variant="outline"
icon={ChevronRight}
>
بازگشت به تقویم
</Button>
</div>
</div>
</AppShell>
)
}
return (
<AppShell
pageTitle="گزارش روزانه"
pageIcon={BarChart3}
pageInfo={selectedDate}
iconGradient="from-indigo-500 to-purple-600"
>
<div className="space-y-6">
{/* Date Navigation Buttons */}
<DateNavigation
selectedDate={selectedDate}
onPrevious={goToPreviousDay}
onNext={goToNextDay}
onCalendar={goToCalendar}
/>
{/* Tabs */}
<Tabs
tabs={TABS}
activeTab={activeTab}
setActiveTab={setActiveTab}
className="md:mx-0 mx-[-1rem] md:rounded-xl rounded-none"
>
{{
summary: <SummaryTab temperature={temp} humidity={hum} soil={soil} gas={gas} lux={lux} forecastWeather={forecastWeather} forecastWeatherLoading={forecastWeatherLoading} onCardClick={handleCardClick} visibleParams={visibleParams} powerOutageSummary={powerOutageSummary} powerOutageLoading={powerOutageLoading} />,
charts: (
<Suspense fallback={<Loading message="در حال بارگذاری نمودارها..." />}>
<ChartsTab sortedTelemetry={sortedTelemetry} dataGaps={dataGaps} visibleParams={visibleParams} />
</Suspense>
),
weather: selectedDate ? (
<Suspense fallback={<Loading message="در حال بارگذاری اطلاعات آب و هوا..." />}>
<WeatherTab selectedDate={selectedDate} />
</Suspense>
) : null,
analysis: selectedDate ? (
<Suspense fallback={<Loading message="در حال بارگذاری تحلیل..." />}>
<AnalysisTab deviceId={deviceId} selectedDate={selectedDate} />
</Suspense>
) : null,
}}
</Tabs>
</div>
</AppShell>
)
}
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>
)
}