fix ui and add powerOutage list
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { Suspense, useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { api } from '@/lib/api'
|
||||
import { getCurrentPersianYear } from '@/lib/date/persian-date'
|
||||
import { Calendar as CalendarIcon, Database, TrendingUp } from 'lucide-react'
|
||||
@@ -13,22 +13,10 @@ import { AppShell } from '@/components/layout'
|
||||
|
||||
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||||
|
||||
function useQueryParam(name: string) {
|
||||
const [value, setValue] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
setValue(params.get(name))
|
||||
}
|
||||
}, [name])
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export default function CalendarPage() {
|
||||
function CalendarContent() {
|
||||
const router = useRouter()
|
||||
const deviceIdParam = useQueryParam('deviceId')
|
||||
const searchParams = useSearchParams()
|
||||
const deviceIdParam = searchParams.get('deviceId')
|
||||
const [deviceId, setDeviceId] = useState<number>(1)
|
||||
const [year, setYear] = useState<number>(getCurrentPersianYear())
|
||||
const [activeMonths, setActiveMonths] = useState<number[]>([])
|
||||
@@ -129,4 +117,12 @@ export default function CalendarPage() {
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default function CalendarPage() {
|
||||
return (
|
||||
<Suspense fallback={<Loading message="در حال بارگذاری تقویم..." />}>
|
||||
<CalendarContent />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo, useState, useCallback, Suspense, lazy } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation'
|
||||
import { api, TelemetryDto } from '@/lib/api'
|
||||
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 { SummaryTab, LiveSummaryCards } from '@/components/daily-report'
|
||||
import { fetchForecastWeather, isToday as checkIsToday, WeatherData } from '@/features/weather'
|
||||
import { Tabs, Button } from '@/components/common'
|
||||
import { DateNavigation } from '@/components/navigation'
|
||||
@@ -29,6 +29,8 @@ function DailyReportContent() {
|
||||
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 => {
|
||||
@@ -131,6 +133,38 @@ function DailyReportContent() {
|
||||
}
|
||||
}, [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)
|
||||
|
||||
@@ -189,6 +223,27 @@ function DailyReportContent() {
|
||||
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)
|
||||
@@ -247,7 +302,16 @@ function DailyReportContent() {
|
||||
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} />,
|
||||
summary: <SummaryTab temperature={temp} humidity={hum} soil={soil} gas={gas} lux={lux} forecastWeather={forecastWeather} forecastWeatherLoading={forecastWeatherLoading} onCardClick={handleCardClick} visibleParams={visibleParams} powerOutageSummary={powerOutageSummary} powerOutageLoading={powerOutageLoading} />,
|
||||
live: <LiveSummaryCards
|
||||
temperature={latestValues.temperature}
|
||||
humidity={latestValues.humidity}
|
||||
soil={latestValues.soil}
|
||||
gas={latestValues.gas}
|
||||
lux={latestValues.lux}
|
||||
lastUpdated={lastUpdated}
|
||||
onCardClick={handleCardClick}
|
||||
/>,
|
||||
charts: (
|
||||
<Suspense fallback={<Loading message="در حال بارگذاری نمودارها..." />}>
|
||||
<ChartsTab sortedTelemetry={sortedTelemetry} dataGaps={dataGaps} visibleParams={visibleParams} />
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
/* --background: #ffffff; */
|
||||
--foreground: #171717;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
/* --color-background: var(--background); */
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
--font-mono: 'Vazirmatn', monospace;
|
||||
@@ -14,7 +12,6 @@
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* --background: #0a0a0a; */
|
||||
--foreground: #ededed;
|
||||
}
|
||||
}
|
||||
@@ -31,12 +28,11 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
/* background: var(--background); */
|
||||
background: linear-gradient(180deg, #eef7ff, #f6fbff);
|
||||
background: #f8fafc;
|
||||
color: var(--foreground);
|
||||
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
|
||||
font-weight: 400;
|
||||
overscroll-behavior-y: contain; /* Prevent pull-to-refresh on Android */
|
||||
overscroll-behavior-y: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
@@ -44,7 +40,7 @@ body {
|
||||
button, a, [role="button"] {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
touch-action: manipulation; /* Disable double-tap zoom */
|
||||
touch-action: manipulation;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
@@ -56,7 +52,7 @@ button, a, [role="button"] {
|
||||
.transition-transform,
|
||||
.transition-opacity {
|
||||
will-change: transform, opacity;
|
||||
transform: translateZ(0); /* Force GPU acceleration */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.persian-number {
|
||||
@@ -176,4 +172,4 @@ button, a, [role="button"] {
|
||||
.has-bottom-nav {
|
||||
padding-bottom: 4.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user