fix charts
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 3s

This commit is contained in:
2025-11-27 16:46:46 +03:30
parent 9ad6dc3a79
commit 0e812a45c5
4 changed files with 111 additions and 26 deletions

View File

@@ -11,6 +11,16 @@ import Loading from '@/components/Loading'
// زمان به‌روزرسانی خودکار (به میلی‌ثانیه) - می‌توانید این مقدار را تغییر دهید // زمان به‌روزرسانی خودکار (به میلی‌ثانیه) - می‌توانید این مقدار را تغییر دهید
const AUTO_REFRESH_INTERVAL = 10 * 1000 // 10 ثانیه const AUTO_REFRESH_INTERVAL = 10 * 1000 // 10 ثانیه
type TimeRange = '1day' | '1hour' | '2hours' | '6hours' | '10hours'
const TIME_RANGE_OPTIONS: { value: TimeRange; label: string }[] = [
{ value: '1day', label: 'یک روز' },
{ value: '1hour', label: 'یک ساعت اخیر' },
{ value: '2hours', label: 'دو ساعت اخیر' },
{ value: '6hours', label: '۶ ساعت اخیر' },
{ value: '10hours', label: '۱۰ ساعت اخیر' }
]
function useQueryParam(name: string) { function useQueryParam(name: string) {
if (typeof window === 'undefined') return null as string | null if (typeof window === 'undefined') return null as string | null
return new URLSearchParams(window.location.search).get(name) return new URLSearchParams(window.location.search).get(name)
@@ -21,6 +31,7 @@ export default function TelemetryPage() {
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [lastUpdate, setLastUpdate] = useState<Date | null>(null) const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
const [timeRange, setTimeRange] = useState<TimeRange>('1day')
const deviceId = Number(useQueryParam('deviceId') ?? '1') const deviceId = Number(useQueryParam('deviceId') ?? '1')
const dateParam = useQueryParam('date') ?? `${getCurrentPersianYear()}/${getCurrentPersianMonth()}/${getCurrentPersianDay()}` const dateParam = useQueryParam('date') ?? `${getCurrentPersianYear()}/${getCurrentPersianMonth()}/${getCurrentPersianDay()}`
const intervalRef = useRef<NodeJS.Timeout | null>(null) const intervalRef = useRef<NodeJS.Timeout | null>(null)
@@ -48,15 +59,43 @@ export default function TelemetryPage() {
} }
try { try {
const [year, month, day] = selectedDate.split('/').map(Number) let startDate: Date
const startDate = persianToGregorian(year, month, day) let endDate: Date
startDate.setHours(0, 0, 0, 0)
const endDate = new Date(startDate) if (timeRange === '1day') {
endDate.setHours(23, 59, 59, 999) // برای یک روز، از تاریخ انتخابی استفاده می‌کنیم
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 '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 startUtc = startDate.toISOString()
const endUtc = endDate.toISOString() const endUtc = endDate.toISOString()
const result = await api.listTelemetry({ deviceId, startUtc, endUtc }) const result = await api.listTelemetry({ deviceId, startUtc, endUtc, pageSize: 100000 })
setTelemetry(result.items) setTelemetry(result.items)
setTotal(result.totalCount) setTotal(result.totalCount)
setLastUpdate(new Date()) setLastUpdate(new Date())
@@ -67,7 +106,7 @@ export default function TelemetryPage() {
setLoading(false) setLoading(false)
} }
} }
}, [deviceId, selectedDate]) }, [deviceId, selectedDate, timeRange])
// بارگذاری اولیه // بارگذاری اولیه
useEffect(() => { useEffect(() => {
@@ -97,13 +136,13 @@ export default function TelemetryPage() {
}, [selectedDate, loadData]) }, [selectedDate, loadData])
const sortedTelemetry = useMemo(() => { const sortedTelemetry = useMemo(() => {
return [...telemetry].sort((a, b) => new Date(a.timestampUtc).getTime() - new Date(b.timestampUtc).getTime()) return [...telemetry].sort((a, b) => new Date(a.serverTimestampUtc).getTime() - new Date(b.serverTimestampUtc).getTime())
}, [telemetry]) }, [telemetry])
// تبدیل timestamp به ساعت (HH:MM:SS) // تبدیل timestamp به ساعت (HH:MM:SS)
const labels = useMemo(() => { const labels = useMemo(() => {
return sortedTelemetry.map(t => { return sortedTelemetry.map(t => {
const date = new Date(t.timestampUtc) const date = new Date(t.serverTimestampUtc)
const hours = date.getHours().toString().padStart(2, '0') const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0') const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0') const seconds = date.getSeconds().toString().padStart(2, '0')
@@ -150,7 +189,7 @@ export default function TelemetryPage() {
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />
بازگشت به تقویم بازگشت به تقویم
</Link> </Link>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between flex-wrap gap-4">
<div className="flex items-center gap-3"> <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"> <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" /> <BarChart3 className="w-6 h-6 text-white" />
@@ -166,15 +205,28 @@ export default function TelemetryPage() {
)} )}
</div> </div>
</div> </div>
<button <div className="flex items-center gap-3">
onClick={() => loadData(true)} <select
disabled={loading} value={timeRange}
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" onChange={(e) => setTimeRange(e.target.value as TimeRange)}
title="به‌روزرسانی دستی" 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"
> >
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> {TIME_RANGE_OPTIONS.map(option => (
<span className="text-sm">بهروزرسانی</span> <option key={option.value} value={option.value}>
</button> {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>
</div> </div>
<DashboardGrid> <DashboardGrid>
@@ -188,16 +240,44 @@ export default function TelemetryPage() {
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<Panel title="رطوبت خاک"> <Panel title="رطوبت خاک">
<LineChart labels={labels} series={[{ label: 'رطوبت خاک (%)', data: soil, borderColor: '#16a34a', backgroundColor: '#dcfce7', fill: true }]} /> <LineChart
labels={labels}
series={[{ label: 'رطوبت خاک (%)', data: soil, borderColor: '#16a34a', backgroundColor: '#dcfce7', fill: true }]}
yAxisMin={0}
yAxisMax={100}
/>
</Panel> </Panel>
<Panel title="دما و رطوبت"> <Panel title="رطوبت">
<LineChart labels={labels} series={[{ label: 'دما (°C)', data: temp, borderColor: '#ef4444' }, { label: 'رطوبت (%)', data: hum, borderColor: '#3b82f6' }]} /> <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={-10}
yAxisMax={80}
/>
</Panel> </Panel>
<Panel title="نور"> <Panel title="نور">
<LineChart labels={labels} series={[{ label: 'Lux', data: lux, borderColor: '#a855f7' }]} /> <LineChart
labels={labels}
series={[{ label: 'Lux', data: lux, borderColor: '#a855f7', backgroundColor: '#f3e8ff', fill: true }]}
yAxisMin={0}
yAxisMax={50000}
/>
</Panel> </Panel>
<Panel title="گاز CO"> <Panel title="گاز CO">
<LineChart labels={labels} series={[{ label: 'CO (ppm)', data: gas, borderColor: '#f59e0b', backgroundColor: '#fef3c7', fill: true }]} /> <LineChart
labels={labels}
series={[{ label: 'CO (ppm)', data: gas, borderColor: '#f59e0b', backgroundColor: '#fef3c7', fill: true }]}
yAxisMin={0}
yAxisMax={100}
/>
</Panel> </Panel>
</div> </div>
</div> </div>

View File

@@ -25,6 +25,8 @@ type Props = {
labels: string[] labels: string[]
series: Series[] series: Series[]
title?: string title?: string
yAxisMin?: number
yAxisMax?: number
} }
export function Panel({ title, children }: { title: string; children: React.ReactNode }) { export function Panel({ title, children }: { title: string; children: React.ReactNode }) {
@@ -40,7 +42,7 @@ export function Panel({ title, children }: { title: string; children: React.Reac
) )
} }
export function LineChart({ labels, series }: Props) { export function LineChart({ labels, series, yAxisMin, yAxisMax }: Props) {
return ( return (
<div className="persian-number"> <div className="persian-number">
<Line <Line
@@ -111,6 +113,8 @@ export function LineChart({ labels, series }: Props) {
}, },
y: { y: {
display: true, display: true,
min: yAxisMin !== undefined ? yAxisMin : undefined,
max: yAxisMax !== undefined ? yAxisMax : undefined,
ticks: { ticks: {
font: { font: {
family: "'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif", family: "'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif",

View File

@@ -65,7 +65,7 @@ function detectColor(title: string, icon?: string): 'light' | 'gas' | 'soil' | '
} }
export function DashboardGrid({ children }: { children: React.ReactNode }) { export function DashboardGrid({ children }: { children: React.ReactNode }) {
return <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">{children}</div> return <div className="grid gap-4 grid-cols-3 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-6">{children}</div>
} }
export function Card({ title, value, subtitle, icon, color }: CardProps) { export function Card({ title, value, subtitle, icon, color }: CardProps) {

View File

@@ -24,6 +24,7 @@ export type TelemetryDto = {
persianMonth: number persianMonth: number
persianDate: string persianDate: string
deviceName?: string deviceName?: string
serverTimestampUtc?: string
} }
export type DeviceSettingsDto = { export type DeviceSettingsDto = {