Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
342 lines
14 KiB
TypeScript
342 lines
14 KiB
TypeScript
"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>
|
||
)
|
||
}
|