fix ui and add powerOutage list
Some checks failed
Deploy MyApp on Same Server / build-and-deploy (push) Has been cancelled

This commit is contained in:
2026-06-03 00:46:37 +03:30
parent 9ec7187e07
commit da180e76ab
52 changed files with 746 additions and 280 deletions

View File

@@ -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>
)
}

View File

@@ -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} />

View File

@@ -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;
}
}
}