fix ui and add pwa
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 5s
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 5s
This commit is contained in:
207
src/app/telemetry/page.tsx
Normal file
207
src/app/telemetry/page.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo, useState, useCallback, useRef } from 'react'
|
||||
import { api, TelemetryDto } from '@/lib/api'
|
||||
import { Card, DashboardGrid } from '@/components/DashboardCards'
|
||||
import { LineChart, Panel } from '@/components/Charts'
|
||||
import { persianToGregorian, getCurrentPersianDay, getCurrentPersianYear, getCurrentPersianMonth } from '@/lib/persian-date'
|
||||
import { BarChart3, ChevronRight, Calendar as CalendarIcon, RefreshCw } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Loading from '@/components/Loading'
|
||||
|
||||
// زمان بهروزرسانی خودکار (به میلیثانیه) - میتوانید این مقدار را تغییر دهید
|
||||
const AUTO_REFRESH_INTERVAL = 10 * 1000 // 10 ثانیه
|
||||
|
||||
function useQueryParam(name: string) {
|
||||
if (typeof window === 'undefined') return null as string | null
|
||||
return new URLSearchParams(window.location.search).get(name)
|
||||
}
|
||||
|
||||
export default function TelemetryPage() {
|
||||
const [telemetry, setTelemetry] = useState<TelemetryDto[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
|
||||
const deviceId = Number(useQueryParam('deviceId') ?? '1')
|
||||
const dateParam = useQueryParam('date') ?? `${getCurrentPersianYear()}/${getCurrentPersianMonth()}/${getCurrentPersianDay()}`
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const selectedDate = useMemo(() => {
|
||||
if (!dateParam) return null
|
||||
try {
|
||||
const decodedDate = decodeURIComponent(dateParam)
|
||||
return decodedDate
|
||||
} catch (error) {
|
||||
console.error('Error decoding date parameter:', error)
|
||||
return null
|
||||
}
|
||||
}, [dateParam])
|
||||
|
||||
// تابع بارگذاری دادهها
|
||||
const loadData = useCallback(async (showLoading = true) => {
|
||||
if (!selectedDate) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
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 })
|
||||
setTelemetry(result.items)
|
||||
setTotal(result.totalCount)
|
||||
setLastUpdate(new Date())
|
||||
} catch (error) {
|
||||
console.error('Error loading telemetry:', error)
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}, [deviceId, selectedDate])
|
||||
|
||||
// بارگذاری اولیه
|
||||
useEffect(() => {
|
||||
loadData(true)
|
||||
}, [loadData])
|
||||
|
||||
// تنظیم بهروزرسانی خودکار
|
||||
useEffect(() => {
|
||||
if (!selectedDate) return
|
||||
|
||||
// پاک کردن interval قبلی
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
|
||||
// تنظیم interval جدید
|
||||
intervalRef.current = setInterval(() => {
|
||||
loadData(false) // بدون نمایش loading
|
||||
}, AUTO_REFRESH_INTERVAL)
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [selectedDate, loadData])
|
||||
|
||||
const sortedTelemetry = useMemo(() => {
|
||||
return [...telemetry].sort((a, b) => new Date(a.timestampUtc).getTime() - new Date(b.timestampUtc).getTime())
|
||||
}, [telemetry])
|
||||
|
||||
// تبدیل timestamp به ساعت (HH:MM:SS)
|
||||
const labels = useMemo(() => {
|
||||
return sortedTelemetry.map(t => {
|
||||
const date = new Date(t.timestampUtc)
|
||||
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}`
|
||||
})
|
||||
}, [sortedTelemetry])
|
||||
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])
|
||||
|
||||
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>
|
||||
<Link
|
||||
href={`/calendar?deviceId=${deviceId}`}
|
||||
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" />
|
||||
بازگشت به تقویم
|
||||
</Link>
|
||||
</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">
|
||||
<Link
|
||||
href={`/calendar?deviceId=${deviceId}`}
|
||||
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-900 transition-colors mb-4"
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
بازگشت به تقویم
|
||||
</Link>
|
||||
<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-purple-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>
|
||||
{lastUpdate && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
آخرین بهروزرسانی: {lastUpdate.toLocaleTimeString('fa-IR')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => loadData(true)}
|
||||
disabled={loading}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 text-white rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg"
|
||||
title="بهروزرسانی دستی"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span className="text-sm">بهروزرسانی</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardGrid>
|
||||
<Card title="نور (Lux)" icon="💡" value={lux.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...lux)} | حداقل: ${Math.min(0, ...lux)}`} />
|
||||
<Card title="گاز CO (ppm)" icon="🫁" value={gas.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...gas)} | حداقل: ${Math.min(0, ...gas)}`} />
|
||||
<Card title="رطوبت خاک (%)" icon="🌱" value={soil.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...soil)} | حداقل: ${Math.min(0, ...soil)}`} />
|
||||
<Card title="رطوبت (%)" icon="💧" value={hum.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...hum)} | حداقل: ${Math.min(0, ...hum)}`} />
|
||||
<Card title="دما (°C)" icon="🌡️" value={temp.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...temp)} | حداقل: ${Math.min(0, ...temp)}`} />
|
||||
<Card title="تعداد داده" icon="📊" value={total} subtitle="در روز انتخابی" />
|
||||
</DashboardGrid>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<Panel title="رطوبت خاک">
|
||||
<LineChart labels={labels} series={[{ label: 'رطوبت خاک (%)', data: soil, borderColor: '#16a34a', backgroundColor: '#dcfce7', fill: true }]} />
|
||||
</Panel>
|
||||
<Panel title="دما و رطوبت">
|
||||
<LineChart labels={labels} series={[{ label: 'دما (°C)', data: temp, borderColor: '#ef4444' }, { label: 'رطوبت (%)', data: hum, borderColor: '#3b82f6' }]} />
|
||||
</Panel>
|
||||
<Panel title="نور">
|
||||
<LineChart labels={labels} series={[{ label: 'Lux', data: lux, borderColor: '#a855f7' }]} />
|
||||
</Panel>
|
||||
<Panel title="گاز CO">
|
||||
<LineChart labels={labels} series={[{ label: 'CO (ppm)', data: gas, borderColor: '#f59e0b', backgroundColor: '#fef3c7', fill: true }]} />
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user