This commit is contained in:
2025-10-31 15:05:47 +03:30
parent 37c501890f
commit a96fbfa91c
23 changed files with 5324 additions and 295 deletions

View File

@@ -0,0 +1,68 @@
"use client"
import { useEffect, useMemo, useState } from 'react'
import { api } from '@/lib/api'
import { getCurrentPersianYear, getCurrentPersianMonth } from '@/lib/persian-date'
function useQueryParam(name: string) {
if (typeof window === 'undefined') return null as string | null
return new URLSearchParams(window.location.search).get(name)
}
export default function MonthPage() {
const deviceId = Number(useQueryParam('deviceId') ?? '1')
const year = Number(useQueryParam('year') ?? getCurrentPersianYear().toString())
const month = Number(useQueryParam('month') ?? getCurrentPersianMonth().toString())
const [daysWithCounts, setDaysWithCounts] = useState<{ day: number; count: number }[]>([])
useEffect(() => {
api.monthDays(deviceId, year, month)
.then(list => setDaysWithCounts(list.map(x => ({ day: Number(x.persianDate.split('/')[2]), count: x.count }))))
.catch(console.error)
}, [deviceId, year, month])
const maxDay = useMemo(() => {
const fromApi = Math.max(0, ...daysWithCounts.map(d => d.day))
if (fromApi > 0) return fromApi
if (month <= 6) return 31
if (month <= 11) return 30
return 29
}, [daysWithCounts, month])
const days = useMemo(() => Array.from({ length: maxDay }, (_, i) => i + 1), [maxDay])
const countByDay = useMemo(() => new Map(daysWithCounts.map(d => [d.day, d.count])), [daysWithCounts])
return (
<div className="p-6">
<div className="flex items-center justify-between mb-3">
<a className="border px-3 py-1 rounded-md" href={`/calendar?deviceId=${deviceId}`}>ماه قبل ←</a>
<div className="text-lg font-semibold">تقویم داده‌های گلخانه · {year}/{month}</div>
<a className="border px-3 py-1 rounded-md" href={`/calendar?deviceId=${deviceId}`}>→ ماه بعد</a>
</div>
<div className="rounded-2xl border bg-white shadow-sm overflow-hidden">
<div className="grid grid-cols-7 text-center bg-gray-100 text-sm">
{['شنبه', 'یکشنبه', 'دوشنبه', 'سه‌شنبه', 'چهارشنبه', 'پنجشنبه', 'جمعه'].map(h => (
<div key={h} className="p-2 border-b">{h}</div>
))}
</div>
<div className="grid grid-cols-7 gap-px bg-gray-200">
{days.map(d => {
const c = countByDay.get(d) ?? 0
const hasData = c > 0
return (
<a key={d} href={`/day_details?deviceId=${deviceId}&year=${year}&month=${month}&day=${d}`} className={`min-h-[84px] bg-white p-2 flex flex-col border-l-0 ${hasData ? 'bg-green-50 hover:bg-green-100' : ''}`}>
<div className="text-xs text-gray-500 mb-1">{d}</div>
{hasData && (
<span className="self-start text-[10px] bg-green-600 text-white rounded-full px-1.5 py-0.5">{c}</span>
)}
</a>
)
})}
</div>
</div>
</div>
)
}

96
src/app/calendar/page.tsx Normal file
View 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>
)
}