new ui and daily report
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 2s

This commit is contained in:
2025-12-17 19:15:28 +03:30
parent c5a69cfbfa
commit 4678207081
26 changed files with 4715 additions and 392 deletions

View File

@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "../../../../GreenHomeBack"
},
{
"path": "../../.."
}
]
}

View File

@@ -1,315 +0,0 @@
"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 ثانیه
type TimeRange = 'today' | '1day' | '1hour' | '2hours' | '6hours' | '10hours'
const TIME_RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
{ value: 'today', label: 'امروز' },
{ value: '1day', label: '۲۴ ساعت اخیر' },
{ value: '10hours', label: '۱۰ ساعت اخیر' },
{ value: '6hours', label: '۶ ساعت اخیر' },
{ value: '2hours', label: 'دو ساعت اخیر' },
{ value: '1hour', label: 'یک ساعت اخیر' },
]
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 [timeRange, setTimeRange] = useState<TimeRange>('1day')
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 {
let startDate: Date
let endDate: Date
if (timeRange === 'today') {
// برای یک روز، از تاریخ انتخابی استفاده می‌کنیم
const [year, month, day] = selectedDate.split('/').map(Number)
startDate = persianToGregorian(year, month, day)
startDate.setHours(0, 0, 0, 0)
endDate = new Date(startDate)
endDate.setHours(23, 59, 59, 999)
} else {
// برای بازه‌های زمانی دیگر، از زمان فعلی به عقب می‌رویم
endDate = new Date()
const now = new Date()
switch (timeRange) {
case '1day':
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
break
case '1hour':
startDate = new Date(now.getTime() - 60 * 60 * 1000)
break
case '2hours':
startDate = new Date(now.getTime() - 2 * 60 * 60 * 1000)
break
case '6hours':
startDate = new Date(now.getTime() - 6 * 60 * 60 * 1000)
break
case '10hours':
startDate = new Date(now.getTime() - 10 * 60 * 60 * 1000)
break
default:
startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000)
}
}
const startUtc = startDate.toISOString();
const endUtc = endDate.toISOString();
const result = await api.listTelemetry({ deviceId, startUtc, endUtc, pageSize: 100000 })
setTelemetry(result.items)
setTotal(result.totalCount)
setLastUpdate(new Date())
} catch (error) {
console.error('Error loading telemetry:', error)
} finally {
if (showLoading) {
setLoading(false)
}
}
}, [deviceId, selectedDate, timeRange])
// بارگذاری اولیه
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) => {
const aTime = a.serverTimestampUtc || a.timestampUtc
const bTime = b.serverTimestampUtc || b.timestampUtc
return new Date(aTime).getTime() - new Date(bTime).getTime()
})
}, [telemetry])
// تبدیل timestamp به ساعت (HH:MM:SS)
const labels = useMemo(() => {
return sortedTelemetry.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}`
})
}, [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])
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]);
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 flex-wrap gap-4">
<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>
<div className="flex items-center gap-3">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value as TimeRange)}
className="px-4 py-2 border border-gray-300 rounded-xl bg-white text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent shadow-md hover:shadow-lg transition-shadow"
>
{TIME_RANGE_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<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>
</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 }]}
yAxisMin={0}
yAxisMax={100}
/>
</Panel>
<Panel title="رطوبت">
<LineChart
labels={labels}
series={[{ label: 'رطوبت (%)', data: hum, borderColor: '#3b82f6', backgroundColor: '#dbeafe', fill: true }]}
yAxisMin={0}
yAxisMax={100}
/>
</Panel>
<Panel title="دما">
<LineChart
labels={labels}
series={[{ label: 'دما (°C)', data: temp, borderColor: '#ef4444', backgroundColor: '#fee2e2', fill: true }]}
yAxisMin={tempMinMax.min}
yAxisMax={tempMinMax.max}
/>
</Panel>
<Panel title="نور">
<LineChart
labels={labels}
series={[{ label: 'Lux', data: lux, borderColor: '#a855f7', backgroundColor: '#f3e8ff', fill: true }]}
yAxisMin={luxMinMax.min}
yAxisMax={luxMinMax.max}
/>
</Panel>
<Panel title="گاز CO">
<LineChart
labels={labels}
series={[{ label: 'CO (ppm)', data: gas, borderColor: '#f59e0b', backgroundColor: '#fef3c7', fill: true }]}
yAxisMin={0}
yAxisMax={100}
/>
</Panel>
</div>
</div>
</div>
)
}