refactor(alert-settings): integrate AppShell layout and update cache version
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
- Replace custom page layout with AppShell component for consistent UI - Remove unused PageHeader and UpdateAlertConditionDto imports - Update service worker cache names and version.json to new timestamp
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
"use client"
|
||||
import { useEffect, useState, useCallback, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { api, AlertConditionDto, UpdateAlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
|
||||
import { api, AlertConditionDto, CreateAlertConditionDto, CreateAlertRuleRequest } from '@/lib/api'
|
||||
import { Bell, Plus, AlertTriangle } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { PageHeader, EmptyState } from '@/components/common'
|
||||
import { EmptyState } from '@/components/common'
|
||||
import { confirmDialog } from '@/components/utils'
|
||||
import { AlertListItem } from '@/components/alert-settings/AlertListItem'
|
||||
import { AlertFormModal } from '@/components/alert-settings/AlertFormModal'
|
||||
import { AppShell } from '@/components/layout'
|
||||
|
||||
function AlertSettingsContent() {
|
||||
const searchParams = useSearchParams()
|
||||
@@ -102,39 +103,36 @@ function AlertSettingsContent() {
|
||||
if (loading) return <Loading message="در حال بارگذاری تنظیمات هشدار..." />
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6 bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<PageHeader
|
||||
icon={Bell}
|
||||
title="تنظیمات هشدارها"
|
||||
subtitle="مدیریت شرایط و هشدارهای دستگاه"
|
||||
iconGradient="from-orange-500 to-red-600"
|
||||
action={
|
||||
<button onClick={openCreate}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg">
|
||||
<Plus className="w-5 h-5" /> افزودن هشدار
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-bold text-gray-900">لیست هشدارها ({alerts.length})</h2>
|
||||
</div>
|
||||
|
||||
{alerts.length === 0 ? (
|
||||
<EmptyState icon={AlertTriangle} message="هیچ هشداری ثبت نشده است"
|
||||
action={<button onClick={openCreate} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"><Plus className="w-4 h-4" /> افزودن اولین هشدار</button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{alerts.map(alert => (
|
||||
<AlertListItem key={alert.id} alert={alert} onEdit={openEdit} onDelete={handleDelete} onToggleActive={toggleActive} />
|
||||
))}
|
||||
<>
|
||||
<AppShell
|
||||
pageTitle="تنظیمات هشدارها"
|
||||
pageIcon={Bell}
|
||||
iconGradient="from-orange-500 to-red-600"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-2xl shadow-md border border-gray-200 overflow-hidden">
|
||||
<div className="p-6 border-b border-gray-200 flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-900">لیست هشدارها ({alerts.length})</h2>
|
||||
<button onClick={openCreate}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-gradient-to-r from-orange-500 to-red-600 hover:from-orange-600 hover:to-red-700 text-white rounded-xl transition-all duration-200 font-medium shadow-md hover:shadow-lg">
|
||||
<Plus className="w-5 h-5" /> افزودن هشدار
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{alerts.length === 0 ? (
|
||||
<EmptyState icon={AlertTriangle} message="هیچ هشداری ثبت نشده است"
|
||||
action={<button onClick={openCreate} className="inline-flex items-center gap-2 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white rounded-lg transition-colors"><Plus className="w-4 h-4" /> افزودن اولین هشدار</button>}
|
||||
/>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{alerts.map(alert => (
|
||||
<AlertListItem key={alert.id} alert={alert} onEdit={openEdit} onDelete={handleDelete} onToggleActive={toggleActive} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
|
||||
<AlertFormModal
|
||||
isOpen={modal.open}
|
||||
@@ -148,7 +146,7 @@ function AlertSettingsContent() {
|
||||
onRemoveRule={removeRule}
|
||||
onUpdateRule={updateRule}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ import { api } from '@/lib/api'
|
||||
import { getCurrentPersianYear } from '@/lib/date/persian-date'
|
||||
import { Calendar as CalendarIcon, Database, TrendingUp } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { PageHeader, BackLink, Card } from '@/components/common'
|
||||
import { Card } from '@/components/common'
|
||||
import { YearSelector } from '@/components/calendar'
|
||||
import { MonthCard } from '@/components/cards'
|
||||
import { StatsCard } from '@/components/cards'
|
||||
import { AppShell } from '@/components/layout'
|
||||
|
||||
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||||
|
||||
@@ -75,16 +76,12 @@ export default function CalendarPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6">
|
||||
<AppShell
|
||||
pageTitle="انتخاب سال و ماه"
|
||||
pageIcon={CalendarIcon}
|
||||
iconGradient="from-green-500 to-green-600"
|
||||
>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<BackLink href="/" label="بازگشت به صفحه اصلی" />
|
||||
<PageHeader
|
||||
icon={CalendarIcon}
|
||||
title="انتخاب سال و ماه"
|
||||
iconGradient="from-green-500 to-green-600"
|
||||
/>
|
||||
|
||||
{/* Main Card */}
|
||||
<Card padding="lg">
|
||||
{/* Year Selector */}
|
||||
@@ -130,6 +127,6 @@ export default function CalendarPage() {
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
@@ -6,14 +6,14 @@ import { persianToGregorian, getCurrentPersianDay, getCurrentPersianYear, getCur
|
||||
import { formatPersianDate, ensureDateFormat } from '@/lib/format/persian-date'
|
||||
import { TABS, TabType } from '@/features/daily-report'
|
||||
import { detectDataGaps } from '@/features/daily-report/utils'
|
||||
import { BarChart3, Bell, CalendarIcon, ChevronRight } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { BarChart3, CalendarIcon, ChevronRight } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { SummaryTab } from '@/components/daily-report'
|
||||
import { fetchForecastWeather, isToday as checkIsToday, WeatherData } from '@/features/weather'
|
||||
import { Tabs, PageHeader, Button } from '@/components/common'
|
||||
import { Tabs, Button } from '@/components/common'
|
||||
import { DateNavigation } from '@/components/navigation'
|
||||
import { usePullToRefresh } from '@/hooks/usePullToRefresh'
|
||||
import { AppShell } from '@/components/layout'
|
||||
|
||||
// Lazy load heavy components
|
||||
const ChartsTab = lazy(() => import('@/components/daily-report/ChartsTab').then(m => ({ default: m.ChartsTab })))
|
||||
@@ -201,50 +201,43 @@ function DailyReportContent() {
|
||||
|
||||
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>
|
||||
<Button
|
||||
onClick={goToCalendar}
|
||||
variant="outline"
|
||||
icon={ChevronRight}
|
||||
>
|
||||
بازگشت به تقویم
|
||||
</Button>
|
||||
<AppShell
|
||||
pageTitle="گزارش روزانه"
|
||||
pageIcon={BarChart3}
|
||||
iconGradient="from-indigo-500 to-purple-600"
|
||||
>
|
||||
<div className="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>
|
||||
<Button
|
||||
onClick={goToCalendar}
|
||||
variant="outline"
|
||||
icon={ChevronRight}
|
||||
>
|
||||
بازگشت به تقویم
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
icon={BarChart3}
|
||||
title="گزارش روزانه"
|
||||
iconGradient="from-indigo-500 to-purple-600"
|
||||
action={
|
||||
<Link
|
||||
href={`/alert-settings?deviceId=${deviceId}`}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-orange-300 hover:border-orange-500 hover:bg-orange-50 text-orange-700 hover:text-orange-800 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">تنظیمات هشدار</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
<AppShell
|
||||
pageTitle="گزارش روزانه"
|
||||
pageIcon={BarChart3}
|
||||
pageInfo={selectedDate}
|
||||
iconGradient="from-indigo-500 to-purple-600"
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Date Navigation Buttons */}
|
||||
{selectedDate && (
|
||||
<DateNavigation
|
||||
selectedDate={selectedDate}
|
||||
onPrevious={goToPreviousDay}
|
||||
onNext={goToNextDay}
|
||||
onCalendar={goToCalendar}
|
||||
/>
|
||||
)}
|
||||
<DateNavigation
|
||||
selectedDate={selectedDate}
|
||||
onPrevious={goToPreviousDay}
|
||||
onNext={goToNextDay}
|
||||
onCalendar={goToCalendar}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs
|
||||
@@ -273,7 +266,7 @@ function DailyReportContent() {
|
||||
}}
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,10 @@ import { api } from '@/lib/api'
|
||||
import { getCurrentPersianYear, getCurrentPersianMonth, getPersianMonthStartWeekday, getPersianMonthDays } from '@/lib/date/persian-date'
|
||||
import { Calendar as CalendarIcon, Database } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { PageHeader, BackLink, Card } from '@/components/common'
|
||||
import { Card } from '@/components/common'
|
||||
import { WeekdayHeaders } from '@/components/calendar'
|
||||
import { CalendarDayCell, StatsCard } from '@/components/cards'
|
||||
import { AppShell } from '@/components/layout'
|
||||
|
||||
const monthNames = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
|
||||
|
||||
@@ -71,16 +72,12 @@ function DayDetailsContent() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6">
|
||||
<AppShell
|
||||
pageTitle={`${monthNames[month - 1]} ${year}`}
|
||||
pageIcon={CalendarIcon}
|
||||
iconGradient="from-green-500 to-green-600"
|
||||
>
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<BackLink href={`/calendar?deviceId=${deviceId}`} label="بازگشت به تقویم" />
|
||||
<PageHeader
|
||||
icon={CalendarIcon}
|
||||
title={`${monthNames[month - 1]} ${year}`}
|
||||
iconGradient="from-green-500 to-green-600"
|
||||
/>
|
||||
|
||||
{/* Calendar Grid */}
|
||||
<Card className="overflow-hidden" padding="none">
|
||||
{/* Weekday Headers */}
|
||||
@@ -138,7 +135,7 @@ function DayDetailsContent() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { api, DeviceSettingsDto } from '@/lib/api'
|
||||
import { Settings, Thermometer, Wind, Sun, Droplets, Save, Loader2, AlertCircle, Bell } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { Settings, Thermometer, Wind, Sun, Droplets, Save, Loader2, AlertCircle } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { PageHeader, BackLink, ErrorMessage, SuccessMessage, EmptyState, Card } from '@/components/common'
|
||||
import { ErrorMessage, SuccessMessage, EmptyState, Card } from '@/components/common'
|
||||
import { SettingsSection, SettingsInputGroup } from '@/components/settings'
|
||||
import { AppShell } from '@/components/layout'
|
||||
|
||||
function useQueryParam(name: string) {
|
||||
if (typeof window === 'undefined') return null as string | null
|
||||
@@ -95,36 +95,25 @@ export default function DeviceSettingsPage() {
|
||||
|
||||
if (!deviceId) {
|
||||
return (
|
||||
<ErrorMessage
|
||||
message="شناسه دستگاه مشخص نشده است"
|
||||
fullPage
|
||||
action={
|
||||
<BackLink href="/devices" label="بازگشت به انتخاب دستگاه" />
|
||||
}
|
||||
/>
|
||||
<AppShell
|
||||
pageTitle="تنظیمات دستگاه"
|
||||
pageIcon={Settings}
|
||||
iconGradient="from-blue-500 to-blue-600"
|
||||
>
|
||||
<ErrorMessage
|
||||
message="شناسه دستگاه مشخص نشده است"
|
||||
/>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 md:p-6">
|
||||
<AppShell
|
||||
pageTitle={`تنظیمات ${deviceName}`}
|
||||
pageIcon={Settings}
|
||||
iconGradient="from-blue-500 to-blue-600"
|
||||
>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Header */}
|
||||
<BackLink href="/devices" label="بازگشت به انتخاب دستگاه" />
|
||||
<PageHeader
|
||||
icon={Settings}
|
||||
title={`تنظیمات ${deviceName}`}
|
||||
iconGradient="from-blue-500 to-blue-600"
|
||||
action={
|
||||
<Link
|
||||
href={`/alert-settings?deviceId=${deviceId}`}
|
||||
className="flex items-center gap-2 px-4 py-2.5 bg-white border-2 border-orange-300 hover:border-orange-500 hover:bg-orange-50 text-orange-700 hover:text-orange-800 rounded-xl transition-all duration-200 font-medium shadow-sm hover:shadow-md"
|
||||
>
|
||||
<Bell className="w-5 h-5" />
|
||||
<span className="hidden sm:inline">تنظیمات هشدار</span>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Messages */}
|
||||
{error && <ErrorMessage message={error} className="mb-6" />}
|
||||
{success && <SuccessMessage message={success} className="mb-6" />}
|
||||
@@ -252,7 +241,7 @@ export default function DeviceSettingsPage() {
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,15 @@ import { api, DeviceDto, PagedResult } from '@/lib/api'
|
||||
import { Settings, LogOut } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { getCurrentPersianYear, getCurrentPersianMonth, getCurrentPersianDay } from '@/lib/date/persian-date'
|
||||
import { PageHeader, ErrorMessage, EmptyState, BackLink } from '@/components/common'
|
||||
import { ErrorMessage, EmptyState } from '@/components/common'
|
||||
import { SearchInput } from '@/components/forms'
|
||||
import { DeviceCard } from '@/components/cards'
|
||||
import { Pagination } from '@/components/navigation'
|
||||
import { useDevice } from '@/components/layout'
|
||||
|
||||
export default function DevicesPage() {
|
||||
const router = useRouter()
|
||||
const { setDevice } = useDevice()
|
||||
const [pagedResult, setPagedResult] = useState<PagedResult<DeviceDto> | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -22,6 +24,12 @@ export default function DevicesPage() {
|
||||
|
||||
const pageSize = 10
|
||||
|
||||
const handleDeviceSelect = useCallback((device: DeviceDto) => {
|
||||
setDevice(device.id, device.deviceName)
|
||||
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
|
||||
router.push(`/daily-report?deviceId=${device.id}&date=${encodeURIComponent(today)}`)
|
||||
}, [setDevice, router])
|
||||
|
||||
const fetchDevices = useCallback(async (page: number, search?: string) => {
|
||||
if (!user) return
|
||||
|
||||
@@ -39,9 +47,8 @@ export default function DevicesPage() {
|
||||
if (result.items.length === 0 && page === 1) {
|
||||
setError('شما هیچ دستگاهی ندارید. لطفاً با پشتیبانی تماس بگیرید.')
|
||||
} else if (result.items.length === 1 && result.totalCount === 1 && !search) {
|
||||
// Single device - redirect to today's daily report
|
||||
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
|
||||
router.push(`/daily-report?deviceId=${result.items[0].id}&date=${encodeURIComponent(today)}`)
|
||||
// Single device - set context and redirect to today's daily report
|
||||
handleDeviceSelect(result.items[0])
|
||||
return
|
||||
} else {
|
||||
setPagedResult(result)
|
||||
@@ -52,7 +59,7 @@ export default function DevicesPage() {
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [user, router])
|
||||
}, [user, router, handleDeviceSelect])
|
||||
|
||||
useEffect(() => {
|
||||
// Check authentication
|
||||
@@ -120,21 +127,34 @@ export default function DevicesPage() {
|
||||
<div className="min-h-screen p-4 md:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
{/* Header */}
|
||||
<PageHeader
|
||||
icon={Settings}
|
||||
title="انتخاب دستگاه"
|
||||
subtitle={user ? `${user.name} ${user.family} (${user.mobile})` : undefined}
|
||||
action={
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl transition-all duration-200 text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
خروج
|
||||
</button>
|
||||
}
|
||||
iconGradient="from-green-500 to-green-600"
|
||||
/>
|
||||
<div className="mb-6">
|
||||
<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-green-500 to-green-600 rounded-xl shadow-md">
|
||||
<Settings className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-gray-900">
|
||||
انتخاب دستگاه
|
||||
</h1>
|
||||
{user && (
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{user.name} {user.family} ({user.mobile})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl transition-all duration-200 text-sm"
|
||||
>
|
||||
<LogOut className="w-4 h-4" />
|
||||
خروج
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
@@ -174,16 +194,13 @@ export default function DevicesPage() {
|
||||
{pagedResult && pagedResult.items.length > 0 ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{pagedResult.items.map((device) => {
|
||||
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
|
||||
return (
|
||||
<DeviceCard
|
||||
key={device.id}
|
||||
device={device}
|
||||
href={`/daily-report?deviceId=${device.id}&date=${encodeURIComponent(today)}`}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{pagedResult.items.map((device) => (
|
||||
<DeviceCard
|
||||
key={device.id}
|
||||
device={device}
|
||||
onClick={() => handleDeviceSelect(device)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
@@ -203,11 +220,6 @@ export default function DevicesPage() {
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Back to Home */}
|
||||
<div className="mt-6 text-center">
|
||||
<BackLink href="/" label="بازگشت به صفحه اصلی" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -156,4 +156,24 @@ button, a, [role="button"] {
|
||||
);
|
||||
background-size: 2000px 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
/* Bottom Navigation Safe Area */
|
||||
@supports (padding-bottom: env(safe-area-inset-bottom)) {
|
||||
.pb-safe {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.h-safe-bottom {
|
||||
height: env(safe-area-inset-bottom);
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent content from being hidden behind bottom nav */
|
||||
.has-bottom-nav {
|
||||
padding-bottom: 5rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.has-bottom-nav {
|
||||
padding-bottom: 4.5rem;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import ServiceWorkerRegistration from '@/components/ServiceWorkerRegistration'
|
||||
import UpdateNotification from '@/components/UpdateNotification'
|
||||
import { DeviceProvider } from '@/components/layout'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'GreenHome',
|
||||
@@ -42,7 +43,9 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac
|
||||
<body className='persian-number'>
|
||||
<ServiceWorkerRegistration />
|
||||
<UpdateNotification />
|
||||
{children}
|
||||
<DeviceProvider>
|
||||
{children}
|
||||
</DeviceProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Smartphone, ArrowLeft } from 'lucide-react'
|
||||
import Loading from '@/components/Loading'
|
||||
import { MobileInput } from '@/components/forms'
|
||||
import { ErrorMessage } from '@/components/common'
|
||||
import { getCurrentPersianYear, getCurrentPersianMonth, getCurrentPersianDay } from '@/lib/date/persian-date'
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter()
|
||||
@@ -20,9 +21,35 @@ export default function Home() {
|
||||
|
||||
if (token && userStr) {
|
||||
try {
|
||||
JSON.parse(userStr)
|
||||
// Redirect to devices check
|
||||
router.push('/devices')
|
||||
const user = JSON.parse(userStr)
|
||||
|
||||
// Try to use the saved device from localStorage first
|
||||
const savedDeviceId = localStorage.getItem('selectedDeviceId')
|
||||
if (savedDeviceId) {
|
||||
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
|
||||
router.push(`/daily-report?deviceId=${savedDeviceId}&date=${encodeURIComponent(today)}`)
|
||||
return
|
||||
}
|
||||
|
||||
// No saved device — fetch user devices and auto-select the first one
|
||||
api.getUserDevices(user.id)
|
||||
.then((devices) => {
|
||||
if (devices.length > 0) {
|
||||
const firstDevice = devices[0]
|
||||
// Save to localStorage
|
||||
localStorage.setItem('selectedDeviceId', String(firstDevice.id))
|
||||
localStorage.setItem('selectedDeviceName', firstDevice.deviceName)
|
||||
const today = `${getCurrentPersianYear()}/${String(getCurrentPersianMonth()).padStart(2, '0')}/${String(getCurrentPersianDay()).padStart(2, '0')}`
|
||||
router.push(`/daily-report?deviceId=${firstDevice.id}&date=${encodeURIComponent(today)}`)
|
||||
} else {
|
||||
// No devices — go to devices page to show the error
|
||||
router.push('/devices')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// On error, fall back to devices page
|
||||
router.push('/devices')
|
||||
})
|
||||
} catch {
|
||||
// Invalid user data, go to login
|
||||
}
|
||||
|
||||
138
src/app/profile/page.tsx
Normal file
138
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { User, LogOut, Settings, ChevronLeft, Smartphone, Shield, Monitor } from 'lucide-react'
|
||||
import { AppShell, useDevice } from '@/components/layout'
|
||||
import { Card } from '@/components/common'
|
||||
|
||||
type UserData = {
|
||||
id: number
|
||||
mobile: string
|
||||
name: string
|
||||
family: string
|
||||
role: number
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter()
|
||||
const { deviceId } = useDevice()
|
||||
const [user, setUser] = useState<UserData | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const userStr = localStorage.getItem('user')
|
||||
if (userStr) {
|
||||
try {
|
||||
setUser(JSON.parse(userStr))
|
||||
} catch {
|
||||
// Invalid user data
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('authToken')
|
||||
localStorage.removeItem('user')
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
const handleDeviceSettings = () => {
|
||||
router.push(`/device-settings?deviceId=${deviceId}`)
|
||||
}
|
||||
|
||||
const handleSelectDevice = () => {
|
||||
router.push('/devices')
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
pageTitle="پروفایل"
|
||||
pageIcon={User}
|
||||
iconGradient="from-green-500 to-emerald-600"
|
||||
>
|
||||
<div className="max-w-2xl mx-auto space-y-6">
|
||||
{/* User Info Card */}
|
||||
<Card padding="lg">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-2xl flex items-center justify-center shadow-md">
|
||||
<User className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
{user ? `${user.name} ${user.family}` : 'کاربر'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mt-1 flex items-center gap-1.5">
|
||||
<Smartphone className="w-4 h-4" />
|
||||
{user?.mobile || '---'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-100 pt-4 space-y-3">
|
||||
<div className="flex items-center justify-between py-2">
|
||||
<span className="text-sm text-gray-600">نقش کاربری</span>
|
||||
<span className="text-sm font-medium text-gray-900 flex items-center gap-1.5">
|
||||
<Shield className="w-4 h-4 text-green-600" />
|
||||
{user?.role === 1 ? 'مدیر' : 'کاربر'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Actions */}
|
||||
<Card padding="lg">
|
||||
<div className="space-y-2">
|
||||
{/* Select Device — always visible so user can change device */}
|
||||
<button
|
||||
onClick={handleSelectDevice}
|
||||
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-gray-50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Monitor className="w-5 h-5 text-purple-600" />
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900">انتخاب دستگاه</p>
|
||||
<p className="text-xs text-gray-500">تغییر دستگاه فعال</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDeviceSettings}
|
||||
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-gray-50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<Settings className="w-5 h-5 text-blue-600" />
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-gray-900">تنظیمات دستگاه</p>
|
||||
<p className="text-xs text-gray-500">مدیریت محدودههای مجاز سنسورها</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center justify-between p-4 rounded-xl hover:bg-red-50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center group-hover:scale-110 transition-transform">
|
||||
<LogOut className="w-5 h-5 text-red-600" />
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium text-red-600">خروج از حساب</p>
|
||||
<p className="text-xs text-gray-500">خروج از حساب کاربری</p>
|
||||
</div>
|
||||
</div>
|
||||
<ChevronLeft className="w-5 h-5 text-gray-400 group-hover:-translate-x-1 transition-transform" />
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
104
src/app/weather/page.tsx
Normal file
104
src/app/weather/page.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { CloudSun, RefreshCw, MapPin } from 'lucide-react'
|
||||
import { AppShell, useDevice } from '@/components/layout'
|
||||
import { WeatherData, fetchForecastWeather, fetchLocationName } from '@/features/weather'
|
||||
import { QOM_LAT, QOM_LON } from '@/features/weather/helpers'
|
||||
import { TodayWeather } from '@/components/daily-report/weather/TodayWeather'
|
||||
import Loading from '@/components/Loading'
|
||||
import { ErrorMessage } from '@/components/common'
|
||||
import { Button } from '@/components/common/Button'
|
||||
|
||||
export default function WeatherPage() {
|
||||
const { deviceName } = useDevice()
|
||||
const [weatherData, setWeatherData] = useState<WeatherData | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [expandedDayIndex, setExpandedDayIndex] = useState<number | null>(null)
|
||||
const [locationName, setLocationName] = useState<string>('در حال دریافت...')
|
||||
|
||||
const loadWeather = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const [weather, location] = await Promise.all([
|
||||
fetchForecastWeather(),
|
||||
fetchLocationName(QOM_LAT, QOM_LON)
|
||||
])
|
||||
|
||||
setWeatherData(weather)
|
||||
setLocationName(location)
|
||||
} catch (error) {
|
||||
console.error('Error loading weather:', error)
|
||||
setError('خطا در دریافت اطلاعات آب و هوا. لطفاً دوباره تلاش کنید.')
|
||||
setLocationName('کهک قم، ایران')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
loadWeather()
|
||||
}, [loadWeather])
|
||||
|
||||
const handleRefresh = () => {
|
||||
setWeatherData(null)
|
||||
loadWeather()
|
||||
}
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
pageTitle="آب و هوا"
|
||||
pageIcon={CloudSun}
|
||||
pageInfo={deviceName || undefined}
|
||||
iconGradient="from-sky-500 to-cyan-600"
|
||||
>
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Location Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 text-gray-600">
|
||||
<MapPin className="w-5 h-5 text-sky-500" />
|
||||
<span className="font-medium">{locationName}</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
variant="outline"
|
||||
icon={RefreshCw}
|
||||
disabled={loading}
|
||||
>
|
||||
بروزرسانی
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading && (
|
||||
<Loading message="در حال دریافت اطلاعات آب و هوا..." fullScreen={false} />
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<ErrorMessage
|
||||
message={error}
|
||||
action={
|
||||
<Button
|
||||
onClick={handleRefresh}
|
||||
variant="primary"
|
||||
icon={RefreshCw}
|
||||
>
|
||||
تلاش مجدد
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{weatherData && !loading && (
|
||||
<TodayWeather
|
||||
weatherData={weatherData}
|
||||
expandedDayIndex={expandedDayIndex}
|
||||
onDayToggle={setExpandedDayIndex}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user