fix charts
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 3s
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Failing after 3s
This commit is contained in:
@@ -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 {
|
||||||
|
let startDate: Date
|
||||||
|
let endDate: Date
|
||||||
|
|
||||||
|
if (timeRange === '1day') {
|
||||||
|
// برای یک روز، از تاریخ انتخابی استفاده میکنیم
|
||||||
const [year, month, day] = selectedDate.split('/').map(Number)
|
const [year, month, day] = selectedDate.split('/').map(Number)
|
||||||
const startDate = persianToGregorian(year, month, day)
|
startDate = persianToGregorian(year, month, day)
|
||||||
startDate.setHours(0, 0, 0, 0)
|
startDate.setHours(0, 0, 0, 0)
|
||||||
const endDate = new Date(startDate)
|
endDate = new Date(startDate)
|
||||||
endDate.setHours(23, 59, 59, 999)
|
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,6 +205,18 @@ export default function TelemetryPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
onClick={() => loadData(true)}
|
onClick={() => loadData(true)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -177,6 +228,7 @@ export default function TelemetryPage() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<DashboardGrid>
|
<DashboardGrid>
|
||||||
<Card title="نور (Lux)" icon="💡" value={lux.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...lux)} | حداقل: ${Math.min(0, ...lux)}`} />
|
<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="گاز CO (ppm)" icon="🫁" value={gas.at(-1) ?? 0} subtitle={`حداکثر: ${Math.max(0, ...gas)} | حداقل: ${Math.min(0, ...gas)}`} />
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user