push
This commit is contained in:
96
src/app/calendar/page.tsx
Normal file
96
src/app/calendar/page.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { api } from '@/lib/api'
|
||||
import { getCurrentPersianYear } from '@/lib/persian-date'
|
||||
|
||||
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() {
|
||||
const deviceIdParam = useQueryParam('deviceId')
|
||||
const [deviceId, setDeviceId] = useState<number>(1)
|
||||
const [year, setYear] = useState<number>(getCurrentPersianYear())
|
||||
const [activeMonths, setActiveMonths] = useState<number[]>([])
|
||||
const [monthDays, setMonthDays] = useState<Record<number, { days: number; records: number }>>({})
|
||||
const years = useMemo(() => Array.from({ length: 10 }, (_, i) => getCurrentPersianYear() - 2 + i), [])
|
||||
|
||||
// وقتی deviceIdParam تغییر کرد، deviceId را به روز کن
|
||||
useEffect(() => {
|
||||
if (deviceIdParam) {
|
||||
setDeviceId(Number(deviceIdParam))
|
||||
}
|
||||
}, [deviceIdParam])
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
setMonthDays({})
|
||||
setActiveMonths([])
|
||||
|
||||
api.activeMonths(deviceId, year)
|
||||
.then(async (months) => {
|
||||
if (!mounted) return
|
||||
setActiveMonths(months)
|
||||
const entries = await Promise.all(months.map(async m => {
|
||||
const days = await api.monthDays(deviceId, year, m)
|
||||
const records = days.reduce((s, d) => s + d.count, 0)
|
||||
return [m, { days: days.length, records }] as const
|
||||
}))
|
||||
if (!mounted) return
|
||||
setMonthDays(Object.fromEntries(entries))
|
||||
})
|
||||
.catch(console.error)
|
||||
return () => { mounted = false }
|
||||
}, [deviceId, year])
|
||||
|
||||
const totalDays = useMemo(() => Object.values(monthDays).reduce((s, v) => s + (v?.days ?? 0), 0), [monthDays])
|
||||
const totalRecords = useMemo(() => Object.values(monthDays).reduce((s, v) => s + (v?.records ?? 0), 0), [monthDays])
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="rounded-2xl border bg-white shadow-sm p-6">
|
||||
<div className="text-center text-xl font-semibold mb-4">انتخاب سال و ماه 📅</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 mb-4">
|
||||
<label className="text-sm text-gray-600">انتخاب سال:</label>
|
||||
<select className="border rounded-md px-2 py-1" value={year} onChange={e => setYear(Number(e.target.value))}>
|
||||
{years.map(y => <option key={y} value={y}>{y}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-gray-600 mb-5">
|
||||
خلاصه {year}: {totalDays} روز دارای دیتا · {totalRecords} رکورد
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3 md:grid-cols-3 xl:grid-cols-4">
|
||||
{monthNames.map((name, idx) => {
|
||||
const m = idx + 1
|
||||
const isActive = activeMonths.includes(m)
|
||||
const stats = monthDays[m]
|
||||
return (
|
||||
<a key={m} href={`/day_details?deviceId=${deviceId}&year=${year}&month=${m}`} className={`rounded-xl border p-4 text-center transition ${isActive ? 'bg-white hover:bg-gray-50' : 'opacity-50'}`}>
|
||||
<div className="text-sm">{name}</div>
|
||||
{isActive && stats && (
|
||||
<div className="inline-block mt-3 text-xs bg-green-600 text-white rounded-full px-2 py-1">
|
||||
{stats.days} روز · {stats.records} رکورد
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user