ソースを参照

fix: fix update user info in user crud

Mohammad Mahdi Salimi 1 ヶ月 前
コミット
f1a4dc38f8

+ 1 - 0
package-lock.json

@@ -37,6 +37,7 @@
         "clsx": "^2.1.1",
         "cmdk": "1.1.1",
         "date-fns": "^4.1.0",
+        "date-fns-jalali": "^4.1.0-0",
         "input-otp": "^1.4.2",
         "lucide-react": "^1.8.0",
         "react": "^19.2.5",

+ 1 - 0
package.json

@@ -48,6 +48,7 @@
     "clsx": "^2.1.1",
     "cmdk": "1.1.1",
     "date-fns": "^4.1.0",
+    "date-fns-jalali": "^4.1.0-0",
     "input-otp": "^1.4.2",
     "lucide-react": "^1.8.0",
     "react": "^19.2.5",

+ 63 - 63
src/components/layout/data/sidebar-data.ts

@@ -1,4 +1,4 @@
-import { LayoutDashboard, Users, ShieldCheck, Play } from 'lucide-react'
+import { LayoutDashboard, Users, Play } from 'lucide-react'
 import { type SidebarData } from '../types'
 import Logo from '@/assets/logo.svg'
 
@@ -55,68 +55,68 @@ export const sidebarData: SidebarData = {
         // },
       ],
     },
-    {
-      title: 'صفحات',
-      items: [
-        {
-          title: 'Auth',
-          icon: ShieldCheck,
-          items: [
-            {
-              title: 'Sign In',
-              url: '/sign-in',
-            },
-            // {
-            //   title: 'Sign In (2 Col)',
-            //   url: '/sign-in-2',
-            // },
-            // {
-            //   title: 'Sign Up',
-            //   url: '/sign-up',
-            // },
-            // {
-            //   title: 'Forgot Password',
-            //   url: '/forgot-password',
-            // },
-            // {
-            //   title: 'OTP',
-            //   url: '/otp',
-            // },
-          ],
-        },
-        // {
-        //   title: 'خطاها',
-        //   icon: Bug,
-        //   items: [
-        //     {
-        //       title: 'Unauthorized',
-        //       url: '/errors/unauthorized',
-        //       icon: Lock,
-        //     },
-        //     {
-        //       title: 'Forbidden',
-        //       url: '/errors/forbidden',
-        //       icon: UserX,
-        //     },
-        //     {
-        //       title: 'Not Found',
-        //       url: '/errors/not-found',
-        //       icon: FileX,
-        //     },
-        //     {
-        //       title: 'Internal Server Error',
-        //       url: '/errors/internal-server-error',
-        //       icon: ServerOff,
-        //     },
-        //     {
-        //       title: 'Maintenance Error',
-        //       url: '/errors/maintenance-error',
-        //       icon: Construction,
-        //     },
-        //   ],
-        // },
-      ],
-    },
+    // {
+    //   title: 'صفحات',
+    //   items: [
+    //     {
+    //       title: 'Auth',
+    //       icon: ShieldCheck,
+    //       items: [
+    //         {
+    //           title: 'Sign In',
+    //           url: '/sign-in',
+    //         },
+    //         // {
+    //         //   title: 'Sign In (2 Col)',
+    //         //   url: '/sign-in-2',
+    //         // },
+    //         // {
+    //         //   title: 'Sign Up',
+    //         //   url: '/sign-up',
+    //         // },
+    //         // {
+    //         //   title: 'Forgot Password',
+    //         //   url: '/forgot-password',
+    //         // },
+    //         // {
+    //         //   title: 'OTP',
+    //         //   url: '/otp',
+    //         // },
+    //       ],
+    //     },
+    //     // {
+    //     //   title: 'خطاها',
+    //     //   icon: Bug,
+    //     //   items: [
+    //     //     {
+    //     //       title: 'Unauthorized',
+    //     //       url: '/errors/unauthorized',
+    //     //       icon: Lock,
+    //     //     },
+    //     //     {
+    //     //       title: 'Forbidden',
+    //     //       url: '/errors/forbidden',
+    //     //       icon: UserX,
+    //     //     },
+    //     //     {
+    //     //       title: 'Not Found',
+    //     //       url: '/errors/not-found',
+    //     //       icon: FileX,
+    //     //     },
+    //     //     {
+    //     //       title: 'Internal Server Error',
+    //     //       url: '/errors/internal-server-error',
+    //     //       icon: ServerOff,
+    //     //     },
+    //     //     {
+    //     //       title: 'Maintenance Error',
+    //     //       url: '/errors/maintenance-error',
+    //     //       icon: Construction,
+    //     //     },
+    //     //   ],
+    //     // },
+    //   ],
+    // },
     // {
     //   title: 'دیگر',
     //   items: [

+ 2 - 4
src/components/profile-dropdown.tsx

@@ -1,11 +1,9 @@
-import { Link } from '@tanstack/react-router'
 import useDialogState from '@/hooks/use-dialog-state'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import { Button } from '@/components/ui/button'
 import {
   DropdownMenu,
   DropdownMenuContent,
-  DropdownMenuGroup,
   DropdownMenuItem,
   DropdownMenuLabel,
   DropdownMenuSeparator,
@@ -41,7 +39,7 @@ export function ProfileDropdown() {
               </p>
             </div>
           </DropdownMenuLabel>
-          <DropdownMenuSeparator />
+          {/* <DropdownMenuSeparator />
           <DropdownMenuGroup>
             <DropdownMenuItem asChild>
               <Link to='/settings'>
@@ -49,7 +47,7 @@ export function ProfileDropdown() {
                 <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
               </Link>
             </DropdownMenuItem>
-          </DropdownMenuGroup>
+          </DropdownMenuGroup> */}
           <DropdownMenuSeparator />
           <DropdownMenuItem variant='destructive' onClick={() => setOpen(true)}>
             خروج

+ 11 - 5
src/components/ui/calendar.tsx

@@ -4,7 +4,8 @@ import {
   ChevronLeftIcon,
   ChevronRightIcon,
 } from 'lucide-react'
-import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
+import { DayButton, getDefaultClassNames, type Locale } from 'react-day-picker'
+import { DayPicker } from 'react-day-picker/jalali'
 import { cn } from '@/lib/utils'
 import { Button, buttonVariants } from '@/components/ui/button'
 
@@ -14,6 +15,7 @@ function Calendar({
   showOutsideDays = true,
   captionLayout = 'label',
   buttonVariant = 'ghost',
+  locale,
   formatters,
   components,
   ...props
@@ -31,10 +33,11 @@ function Calendar({
         String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
         className
       )}
+      locale={locale}
       captionLayout={captionLayout}
       formatters={{
         formatMonthDropdown: (date) =>
-          date.toLocaleString('default', { month: 'short' }),
+          date.toLocaleString(locale?.code, { month: 'short' }),
         ...formatters,
       }}
       classNames={{
@@ -152,7 +155,9 @@ function Calendar({
             <ChevronDownIcon className={cn('size-4', className)} {...props} />
           )
         },
-        DayButton: CalendarDayButton,
+        DayButton: ({ ...props }) => (
+          <CalendarDayButton locale={locale} {...props} />
+        ),
         WeekNumber: ({ children, ...props }) => {
           return (
             <td {...props}>
@@ -173,8 +178,9 @@ function CalendarDayButton({
   className,
   day,
   modifiers,
+  locale,
   ...props
-}: React.ComponentProps<typeof DayButton>) {
+}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
   const defaultClassNames = getDefaultClassNames()
 
   const ref = React.useRef<HTMLButtonElement>(null)
@@ -187,7 +193,7 @@ function CalendarDayButton({
       ref={ref}
       variant='ghost'
       size='icon'
-      data-day={day.date.toLocaleDateString()}
+      data-day={day.date.toLocaleDateString(locale?.code)}
       data-selected-single={
         modifiers.selected &&
         !modifiers.range_start &&

+ 250 - 2
src/features/dashboard/index.tsx

@@ -4,6 +4,16 @@ import { Main } from '@/components/layout/main'
 import { ProfileDropdown } from '@/components/profile-dropdown'
 import { Search } from '@/components/search'
 import { ThemeSwitch } from '@/components/theme-switch'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import {
+  Video,
+  Users,
+  Eye,
+  TrendingUp,
+  Play,
+  Clock,
+  CheckCircle,
+} from 'lucide-react'
 
 export function Dashboard() {
   return (
@@ -16,9 +26,247 @@ export function Dashboard() {
       </Header>
 
       <Main>
-        <div className='mb-2 flex items-center justify-between space-y-2'>
-          <h1 className='text-2xl font-bold tracking-tight'>داشبورد</h1>
+        <div className='mb-6 flex items-center justify-between'>
+          <div>
+            <h2 className='text-2xl font-bold tracking-tight'>داشبورد</h2>
+            <p className='text-muted-foreground'>
+              به پنل ادمین تلنت‌یار خوش آمدید.
+            </p>
+          </div>
         </div>
+
+        {/* کارت‌های آماری */}
+        <div className='mb-6 grid gap-4 md:grid-cols-2 lg:grid-cols-4'>
+          <Card>
+            <CardHeader className='flex flex-row items-center justify-between pb-2'>
+              <CardTitle className='text-sm font-medium'>کل ویدیوها</CardTitle>
+              <Video className='h-4 w-4 text-muted-foreground' />
+            </CardHeader>
+            <CardContent>
+              <div className='text-2xl font-bold'>2,847</div>
+              <p className='mt-1 text-xs text-muted-foreground'>+156 این ماه</p>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className='flex flex-row items-center justify-between pb-2'>
+              <CardTitle className='text-sm font-medium'>کل بازدیدها</CardTitle>
+              <Eye className='h-4 w-4 text-muted-foreground' />
+            </CardHeader>
+            <CardContent>
+              <div className='text-2xl font-bold'>45,231</div>
+              <p className='mt-1 text-xs text-muted-foreground'>
+                +18% نسبت به ماه قبل
+              </p>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className='flex flex-row items-center justify-between pb-2'>
+              <CardTitle className='text-sm font-medium'>
+                ورزشکاران فعال
+              </CardTitle>
+              <Users className='h-4 w-4 text-muted-foreground' />
+            </CardHeader>
+            <CardContent>
+              <div className='text-2xl font-bold'>1,234</div>
+              <p className='mt-1 text-xs text-muted-foreground'>
+                892 نفر این ماه فعال بودند
+              </p>
+            </CardContent>
+          </Card>
+
+          <Card>
+            <CardHeader className='flex flex-row items-center justify-between pb-2'>
+              <CardTitle className='text-sm font-medium'>
+                ویدیوهای امروز
+              </CardTitle>
+              <TrendingUp className='h-4 w-4 text-muted-foreground' />
+            </CardHeader>
+            <CardContent>
+              <div className='text-2xl font-bold'>23</div>
+              <p className='mt-1 text-xs text-muted-foreground'>
+                ویدیوی جدید آپلود شده
+              </p>
+            </CardContent>
+          </Card>
+        </div>
+
+        <div className='grid gap-6 md:grid-cols-2'>
+          {/* ویدیوهای اخیر */}
+          <Card>
+            <CardHeader>
+              <CardTitle className='flex items-center gap-2'>
+                <Play className='h-5 w-5' />
+                ویدیوهای اخیر
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <div className='space-y-4'>
+                {[
+                  {
+                    user: 'علی احمدی',
+                    sport: 'فوتبال',
+                    time: '5 دقیقه پیش',
+                    views: 124,
+                    status: 'pending',
+                  },
+                  {
+                    user: 'مریم رضایی',
+                    sport: 'والیبال',
+                    time: '15 دقیقه پیش',
+                    views: 89,
+                    status: 'approved',
+                  },
+                  {
+                    user: 'حسین محمدی',
+                    sport: 'بسکتبال',
+                    time: '30 دقیقه پیش',
+                    views: 256,
+                    status: 'approved',
+                  },
+                  {
+                    user: 'فاطمه کریمی',
+                    sport: 'تنیس',
+                    time: '1 ساعت پیش',
+                    views: 178,
+                    status: 'pending',
+                  },
+                  {
+                    user: 'رضا نوری',
+                    sport: 'شنا',
+                    time: '2 ساعت پیش',
+                    views: 312,
+                    status: 'approved',
+                  },
+                ].map((video, i) => (
+                  <div
+                    key={i}
+                    className='flex items-center justify-between border-b pb-3 last:border-0 last:pb-0'
+                  >
+                    <div className='flex-1'>
+                      <div className='flex items-center gap-2'>
+                        <p className='font-medium'>{video.user}</p>
+                        {video.status === 'approved' ? (
+                          <CheckCircle className='h-4 w-4 text-green-500' />
+                        ) : (
+                          <Clock className='h-4 w-4 text-yellow-500' />
+                        )}
+                      </div>
+                      <p className='text-sm text-muted-foreground'>
+                        {video.sport}
+                      </p>
+                    </div>
+                    <div className='text-left'>
+                      <p className='flex items-center gap-1 text-sm font-medium'>
+                        <Eye className='h-3 w-3' />
+                        {video.views}
+                      </p>
+                      <span className='text-xs text-muted-foreground'>
+                        {video.time}
+                      </span>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </CardContent>
+          </Card>
+
+          {/* ویدیوهای در انتظار تأیید */}
+          <Card>
+            <CardHeader>
+              <CardTitle className='flex items-center gap-2'>
+                <Clock className='h-5 w-5' />
+                در انتظار بررسی
+              </CardTitle>
+            </CardHeader>
+            <CardContent>
+              <div className='space-y-4'>
+                {[
+                  {
+                    user: 'محمد حسینی',
+                    sport: 'کاراته',
+                    time: '10 دقیقه پیش',
+                    duration: '2:34',
+                  },
+                  {
+                    user: 'زهرا اکبری',
+                    sport: 'ژیمناستیک',
+                    time: '25 دقیقه پیش',
+                    duration: '3:12',
+                  },
+                  {
+                    user: 'امیر رستمی',
+                    sport: 'دوومیدانی',
+                    time: '45 دقیقه پیش',
+                    duration: '1:45',
+                  },
+                  {
+                    user: 'سارا محمودی',
+                    sport: 'بدمینتون',
+                    time: '1 ساعت پیش',
+                    duration: '2:58',
+                  },
+                  {
+                    user: 'پویا کریمی',
+                    sport: 'کشتی',
+                    time: '2 ساعت پیش',
+                    duration: '4:21',
+                  },
+                ].map((video, i) => (
+                  <div
+                    key={i}
+                    className='flex items-center justify-between border-b pb-3 last:border-0 last:pb-0'
+                  >
+                    <div className='flex-1'>
+                      <p className='font-medium'>{video.user}</p>
+                      <p className='text-sm text-muted-foreground'>
+                        {video.sport}
+                      </p>
+                    </div>
+                    <div className='text-left'>
+                      <p className='text-sm font-medium'>{video.duration}</p>
+                      <span className='text-xs text-muted-foreground'>
+                        {video.time}
+                      </span>
+                    </div>
+                  </div>
+                ))}
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+
+        {/* محبوب‌ترین رشته‌ها */}
+        <Card className='mt-6'>
+          <CardHeader>
+            <CardTitle>محبوب‌ترین رشته‌های ورزشی</CardTitle>
+          </CardHeader>
+          <CardContent>
+            <div className='grid gap-4 md:grid-cols-3 lg:grid-cols-5'>
+              {[
+                { sport: 'فوتبال', count: 487, color: 'bg-secondary' },
+                { sport: 'والیبال', count: 356, color: 'bg-secondary' },
+                { sport: 'بسکتبال', count: 298, color: 'bg-secondary' },
+                { sport: 'کاراته', count: 234, color: 'bg-secondary' },
+                { sport: 'شنا', count: 189, color: 'bg-secondary' },
+              ].map((item, i) => (
+                <div
+                  key={i}
+                  className='flex flex-col items-center gap-2 rounded-lg border p-4'
+                >
+                  <div
+                    className={`h-12 w-12 rounded-full ${item.color} flex items-center justify-center font-bold`}
+                  >
+                    {item.count}
+                  </div>
+                  <p className='font-medium'>{item.sport}</p>
+                  <p className='text-xs text-muted-foreground'>ویدیو</p>
+                </div>
+              ))}
+            </div>
+          </CardContent>
+        </Card>
       </Main>
     </>
   )

+ 16 - 5
src/features/posts/components/posts-columns.tsx

@@ -74,17 +74,21 @@ export const postsColumns: ColumnDef<Post>[] = [
   {
     accessorKey: 'likes_count',
     header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='تعداد لایک' />
+      <DataTableColumnHeader column={column} title='لایک' />
+    ),
+    cell: ({ row }) => (
+      <div className='max-w-36 ps-3'>{row.original.likes_count}</div>
     ),
-    cell: ({ row }) => <div dir='ltr'>{row.original.likes_count}</div>,
   },
 
   {
     accessorKey: 'comment_count',
     header: ({ column }) => (
-      <DataTableColumnHeader column={column} title='تعداد کامنت' />
+      <DataTableColumnHeader column={column} title='کامنت' />
+    ),
+    cell: ({ row }) => (
+      <div className='max-w-36 ps-3'>{row.original.comment_count}</div>
     ),
-    cell: ({ row }) => <div dir='ltr'>{row.original.comment_count}</div>,
   },
 
   {
@@ -92,7 +96,14 @@ export const postsColumns: ColumnDef<Post>[] = [
     header: ({ column }) => (
       <DataTableColumnHeader column={column} title='وضعیت نمایش' />
     ),
-    cell: ({ row }) => <Badge variant='secondary'>{row.original.state}</Badge>,
+    cell: ({ row }) => {
+      const state = Number(row.original.state)
+      const stateText =
+        state === 1 ? 'در حال نمایش' : state === 2 ? 'مخفی' : 'نامشخص'
+      const variant = state === 1 ? 'secondary' : 'destructive'
+
+      return <Badge variant={variant}>{stateText}</Badge>
+    },
   },
 
   {

+ 370 - 110
src/features/users/components/users-action-dialog.tsx

@@ -1,4 +1,8 @@
-import { useForm } from 'react-hook-form'
+import { useEffect } from 'react'
+import { z } from 'zod'
+import { useForm, useWatch } from 'react-hook-form'
+import { zodResolver } from '@hookform/resolvers/zod'
+import { format, parse } from 'date-fns-jalali'
 
 import {
   Dialog,
@@ -18,6 +22,7 @@ import {
 
 import { Input } from '@/components/ui/input'
 import { Textarea } from '@/components/ui/textarea'
+
 import {
   Select,
   SelectContent,
@@ -28,9 +33,12 @@ import {
 
 import { Button } from '@/components/ui/button'
 
+import { useUsers } from './users-provider'
+
 import { useCreateUser } from '@/hooks/users/useCreateUser'
 import { useUpdateUser } from '@/hooks/users/useUpdateUser'
 import { useUserDetail } from '@/hooks/users/useUserDetail'
+
 import { useProvinces } from '@/hooks/location/useProvinces'
 import { useCities } from '@/hooks/location/useCities'
 
@@ -41,22 +49,57 @@ import {
   skillLevelOptions,
   activityHistoryOptions,
 } from '@/constants/users/users-constants'
-
-import { useUsers } from './users-provider'
-import type { CreateUserPayload } from '@/types/users.types'
-import { useEffect } from 'react'
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from '@/components/ui/popover'
+import { Calendar } from '@/components/ui/calendar'
+import { faIR } from 'react-day-picker/persian'
+import { toast } from 'sonner'
+import { useQueryClient } from '@tanstack/react-query'
+import { queryKeys } from '@/core/react-query/keys'
+
+const schema = z.object({
+  username: z.string().min(1, 'نام کاربری الزامی است'),
+  phone: z.string().min(1, 'شماره موبایل الزامی است'),
+  first_name: z.string().min(1, 'نام الزامی است'),
+  last_name: z.string().min(1, 'نام خانوادگی الزامی است'),
+  email: z.string().optional(),
+  province_id: z.number().optional(),
+  city_id: z.number().optional(),
+  gender: z.number(),
+  birth_date: z.date().optional(),
+  weight: z.number().optional(),
+  height: z.number().optional(),
+  foot_specialization: z.string().optional(),
+  post_skill: z.string().optional(),
+  skill_level: z.string().optional(),
+  activity_history: z.boolean().optional(),
+  team_name: z.string().optional(),
+  favorite_iranian_team: z.string().optional(),
+  favorite_foreign_team: z.string().optional(),
+  shirt_number: z.number().optional(),
+  bio: z.string().optional(),
+})
+
+type FormValues = z.infer<typeof schema>
 
 export function UsersActionDialog() {
+  const queryClient = useQueryClient()
   const { open, setOpen, currentRow } = useUsers()
+
   const isAdd = open === 'add'
   const isEdit = open === 'edit'
   const isView = open === 'view'
+
   const opened = isAdd || isEdit || isView
+
   const readOnly = isView
 
-  const { data: user, isLoading } = useUserDetail(currentRow?.id as number)
+  const form = useForm<FormValues>({
+    resolver: zodResolver(schema),
 
-  const form = useForm<CreateUserPayload>({
     defaultValues: {
       username: '',
       phone: '',
@@ -65,10 +108,10 @@ export function UsersActionDialog() {
       email: '',
       province_id: undefined,
       city_id: undefined,
-      gender: undefined,
-      birth_date: '',
-      height: undefined,
+      gender: 0,
+      birth_date: undefined,
       weight: undefined,
+      height: undefined,
       foot_specialization: '',
       post_skill: '',
       skill_level: '',
@@ -81,85 +124,143 @@ export function UsersActionDialog() {
     },
   })
 
-  useEffect(() => {
-    if (user && (isEdit || isView)) {
-      form.reset({
-        username: user.username ?? '',
-        phone: user.phone ?? '',
-        first_name: user.first_name ?? '',
-        last_name: user.last_name ?? '',
-        email: user.email ?? '',
-        province_id: user.province_id ?? undefined,
-        city_id: user.city_id ?? undefined,
-        gender: user.gender ?? undefined,
-        birth_date: user.birth_date ?? '',
-        height: user.height ?? undefined,
-        weight: user.weight ?? undefined,
-        foot_specialization: user.foot_specialization ?? '',
-        post_skill: user.post_skill ?? '',
-        skill_level: user.skill_level ?? '',
-        activity_history: Boolean(user.activity_history),
-        team_name: user.team_name ?? '',
-        favorite_iranian_team: user.favorite_iranian_team ?? '',
-        favorite_foreign_team: user.favorite_foreign_team ?? '',
-        shirt_number: user.shirt_number ?? undefined,
-        bio: user.bio ?? '',
-      })
-    } else if (isAdd) {
-      form.reset({
-        username: '',
-        phone: '',
-        first_name: '',
-        last_name: '',
-        email: '',
-        province_id: undefined,
-        city_id: undefined,
-        gender: undefined,
-        birth_date: '',
-        height: undefined,
-        weight: undefined,
-        foot_specialization: '',
-        post_skill: '',
-        skill_level: '',
-        activity_history: false,
-        team_name: '',
-        favorite_iranian_team: '',
-        favorite_foreign_team: '',
-        shirt_number: undefined,
-        bio: '',
-      })
-    }
-  }, [user, isEdit, isView, isAdd, form])
+  const [provinceId, activity_history] = useWatch({
+    control: form.control,
+    name: ['province_id', 'activity_history'],
+  })
 
-  const provinceId = form.watch('province_id')
+  const { data: provinces = [] } = useProvinces(opened)
 
-  const { data: provinces = [] } = useProvinces()
   const { data: cities = [] } = useCities(provinceId as number)
 
+  const { data: user, isLoading } = useUserDetail(currentRow?.id as number)
+
   const createMutation = useCreateUser()
+
   const updateMutation = useUpdateUser()
 
-  const onSubmit = (values: CreateUserPayload) => {
+  const handleClose = () => {
+    setOpen(null)
+    form.reset({
+      username: '',
+      phone: '',
+      first_name: '',
+      last_name: '',
+      email: '',
+      province_id: undefined,
+      city_id: undefined,
+      gender: 0,
+      birth_date: undefined,
+      weight: undefined,
+      height: undefined,
+      foot_specialization: '',
+      post_skill: '',
+      skill_level: '',
+      activity_history: false,
+      team_name: '',
+      favorite_iranian_team: '',
+      favorite_foreign_team: '',
+      shirt_number: undefined,
+      bio: '',
+    })
+  }
+
+  useEffect(() => {
+    if ((isEdit || isView) && user && !isLoading) {
+      const timer = setTimeout(() => {
+        form.reset({
+          username: user.username || '',
+          phone: user.phone || '',
+          first_name: user.first_name || '',
+          last_name: user.last_name || '',
+          email: user.email || '',
+          province_id: user.province_id || undefined,
+          gender: user.gender ?? 0,
+          birth_date: user.birth_date
+            ? parse(user.birth_date, 'yyyy-MM-dd', new Date())
+            : undefined,
+          weight: user.weight || undefined,
+          height: user.height || undefined,
+          foot_specialization: user.foot_specialization || '',
+          post_skill: user.post_skill || '',
+          skill_level: user.skill_level || '',
+          activity_history: !!user.activity_history,
+          team_name: user.team_name || '',
+          favorite_iranian_team: user.favorite_iranian_team || '',
+          favorite_foreign_team: user.favorite_foreign_team || '',
+          shirt_number: user.shirt_number || undefined,
+          bio: user.bio || '',
+        })
+      }, 100)
+
+      return () => clearTimeout(timer)
+    }
+  }, [user, isLoading, isEdit, isView, form])
+
+  useEffect(() => {
+    if (user && (isEdit || isView) && provinceId && user.city_id) {
+      const timer = setTimeout(() => {
+        form.setValue('city_id', user.city_id)
+      }, 250)
+
+      return () => clearTimeout(timer)
+    }
+  }, [user, isEdit, isView, provinceId, form])
+
+  const onSubmit = (values: FormValues) => {
     if (isView) return
 
+    const payload = {
+      ...values,
+      activity_history: values.activity_history ? 1 : 0,
+      email: values.email || undefined,
+      province_id: values.province_id || undefined,
+      city_id: values.city_id || undefined,
+      weight: values.weight || undefined,
+      height: values.height || undefined,
+      foot_specialization: values.foot_specialization || undefined,
+      post_skill: values.post_skill || undefined,
+      skill_level: values.skill_level || undefined,
+      team_name: values.team_name || undefined,
+      favorite_iranian_team: values.favorite_iranian_team || undefined,
+      favorite_foreign_team: values.favorite_foreign_team || undefined,
+      shirt_number: values.shirt_number || undefined,
+      bio: values.bio || undefined,
+      birth_date: values.birth_date
+        ? format(values.birth_date, 'yyyy-MM-dd')
+        : undefined,
+    }
+
     if (isAdd) {
-      createMutation.mutate(values, {
-        onSuccess: () => setOpen(null),
+      createMutation.mutate(payload, {
+        onSuccess: () => {
+          toast.success('افزودن کاربر با موفقیت انجام شد!')
+          handleClose()
+        },
       })
     }
 
     if (isEdit && currentRow) {
       updateMutation.mutate(
-        { id: currentRow.id, payload: values },
         {
-          onSuccess: () => setOpen(null),
+          id: currentRow.id,
+          payload,
+        },
+        {
+          onSuccess: () => {
+            toast.success('ویرایش کاربر با موفقیت انجام شد!')
+            queryClient.invalidateQueries({
+              queryKey: queryKeys.users.detail(currentRow.id),
+            })
+            handleClose()
+          },
         }
       )
     }
   }
 
   return (
-    <Dialog open={opened} modal={true} onOpenChange={() => setOpen(null)}>
+    <Dialog open={opened} onOpenChange={handleClose}>
       <DialogContent className='max-h-[90vh] max-w-5xl overflow-y-auto'>
         <DialogHeader>
           <DialogTitle>
@@ -169,84 +270,88 @@ export function UsersActionDialog() {
           </DialogTitle>
         </DialogHeader>
 
-        {isLoading ? (
+        {isLoading && (isEdit || isView) ? (
           <div className='py-10 text-center'>در حال دریافت اطلاعات...</div>
         ) : (
           <Form {...form}>
             <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
               <div className='grid grid-cols-1 gap-4 md:grid-cols-2'>
+                {/* first name */}
                 <FormField
                   control={form.control}
                   name='first_name'
-                  rules={{ required: 'نام الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>نام</FormLabel>
+
                       <FormControl>
                         <Input {...field} disabled={readOnly} />
                       </FormControl>
+
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* last name */}
                 <FormField
                   control={form.control}
                   name='last_name'
-                  rules={{ required: 'نام خانوادگی الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>نام خانوادگی</FormLabel>
+
                       <FormControl>
                         <Input {...field} disabled={readOnly} />
                       </FormControl>
+
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* username */}
                 <FormField
                   control={form.control}
                   name='username'
-                  rules={{ required: 'نام کاربری الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>نام کاربری</FormLabel>
+
                       <FormControl>
                         <Input {...field} disabled={readOnly} />
                       </FormControl>
+
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* phone */}
                 <FormField
                   control={form.control}
                   name='phone'
-                  rules={{
-                    required: 'شماره موبایل الزامی است',
-                    minLength: {
-                      value: 10,
-                      message: 'شماره موبایل نامعتبر است',
-                    },
-                  }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>موبایل</FormLabel>
+
                       <FormControl>
                         <Input {...field} disabled={readOnly} />
                       </FormControl>
+
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* email */}
                 <FormField
                   control={form.control}
                   name='email'
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>ایمیل</FormLabel>
+
                       <FormControl>
                         <Input
                           {...field}
@@ -254,37 +359,71 @@ export function UsersActionDialog() {
                           value={field.value ?? ''}
                         />
                       </FormControl>
+
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* birth date */}
+
                 <FormField
                   control={form.control}
                   name='birth_date'
-                  rules={{ required: 'تاریخ تولد الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>تاریخ تولد</FormLabel>
                       <FormControl>
-                        <Input type='date' {...field} disabled={readOnly} />
+                        <Popover>
+                          <PopoverTrigger asChild>
+                            <Button
+                              variant='outline'
+                              disabled={readOnly}
+                              className={`${field.value ? '' : 'text-muted-foreground'} w-full justify-start font-normal`}
+                              size={'lg'}
+                            >
+                              {field.value
+                                ? format(field.value, 'yyyy/MM/dd')
+                                : '****/**/**'}
+                            </Button>
+                          </PopoverTrigger>
+                          <PopoverContent
+                            className='w-auto overflow-hidden p-0'
+                            align='start'
+                          >
+                            <Calendar
+                              mode='single'
+                              dir='rtl'
+                              locale={faIR}
+                              selected={field.value as Date | undefined}
+                              defaultMonth={field.value as Date | undefined}
+                              captionLayout='dropdown'
+                              onSelect={(date) => field.onChange(date)}
+                              disabled={readOnly}
+                            />
+                          </PopoverContent>
+                        </Popover>
                       </FormControl>
                       <FormMessage />
                     </FormItem>
                   )}
                 />
 
+                {/* gender */}
                 <FormField
                   control={form.control}
                   name='gender'
-                  rules={{ required: 'جنسیت الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>جنسیت</FormLabel>
 
                       <Select
                         disabled={readOnly}
-                        value={field.value?.toString() ?? ''}
+                        value={
+                          field.value !== undefined && field.value !== null
+                            ? String(field.value)
+                            : undefined
+                        }
                         onValueChange={(value) => field.onChange(Number(value))}
                       >
                         <FormControl>
@@ -310,18 +449,23 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* province */}
                 <FormField
                   control={form.control}
                   name='province_id'
-                  rules={{ required: 'انتخاب استان الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>استان</FormLabel>
+
                       <Select
                         disabled={readOnly}
-                        value={field.value ? field.value.toString() : ''}
+                        value={
+                          field.value !== undefined && field.value !== null
+                            ? String(field.value)
+                            : undefined
+                        }
                         onValueChange={(value) => {
-                          field.onChange(value ? Number(value) : undefined)
+                          field.onChange(Number(value))
                           form.setValue('city_id', undefined)
                         }}
                       >
@@ -348,17 +492,21 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* city */}
                 <FormField
                   control={form.control}
                   name='city_id'
-                  rules={{ required: 'انتخاب شهر الزامی است' }}
                   render={({ field }) => (
                     <FormItem>
                       <FormLabel>شهر</FormLabel>
 
                       <Select
                         disabled={readOnly || !provinceId}
-                        value={field.value ? String(field.value) : ''}
+                        value={
+                          field.value !== undefined && field.value !== null
+                            ? String(field.value)
+                            : undefined
+                        }
                         onValueChange={(value) => field.onChange(Number(value))}
                       >
                         <FormControl>
@@ -381,6 +529,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* height */}
                 <FormField
                   control={form.control}
                   name='height'
@@ -391,15 +540,15 @@ export function UsersActionDialog() {
                       <FormControl>
                         <Input
                           type='number'
-                          value={field.value ?? ''}
+                          {...field}
                           disabled={readOnly}
-                          onChange={(e) =>
+                          value={field.value ?? ''}
+                          onChange={(e) => {
+                            const value = e.target.value
                             field.onChange(
-                              e.target.value === ''
-                                ? undefined
-                                : Number(e.target.value)
+                              value === '' ? undefined : Number(value)
                             )
-                          }
+                          }}
                         />
                       </FormControl>
 
@@ -408,6 +557,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* weight */}
                 <FormField
                   control={form.control}
                   name='weight'
@@ -418,15 +568,15 @@ export function UsersActionDialog() {
                       <FormControl>
                         <Input
                           type='number'
-                          value={field.value ?? ''}
+                          {...field}
                           disabled={readOnly}
-                          onChange={(e) =>
+                          value={field.value ?? ''}
+                          onChange={(e) => {
+                            const value = e.target.value
                             field.onChange(
-                              e.target.value === ''
-                                ? undefined
-                                : Number(e.target.value)
+                              value === '' ? undefined : Number(value)
                             )
-                          }
+                          }}
                         />
                       </FormControl>
 
@@ -435,6 +585,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* foot */}
                 <FormField
                   control={form.control}
                   name='foot_specialization'
@@ -444,7 +595,7 @@ export function UsersActionDialog() {
 
                       <Select
                         disabled={readOnly}
-                        value={field.value ?? ''}
+                        value={field.value || undefined}
                         onValueChange={field.onChange}
                       >
                         <FormControl>
@@ -467,6 +618,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* position */}
                 <FormField
                   control={form.control}
                   name='post_skill'
@@ -476,7 +628,7 @@ export function UsersActionDialog() {
 
                       <Select
                         disabled={readOnly}
-                        value={field.value ?? ''}
+                        value={field.value || undefined}
                         onValueChange={field.onChange}
                       >
                         <FormControl>
@@ -499,6 +651,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* skill level */}
                 <FormField
                   control={form.control}
                   name='skill_level'
@@ -508,7 +661,7 @@ export function UsersActionDialog() {
 
                       <Select
                         disabled={readOnly}
-                        value={field.value ?? ''}
+                        value={field.value || undefined}
                         onValueChange={field.onChange}
                       >
                         <FormControl>
@@ -531,6 +684,7 @@ export function UsersActionDialog() {
                   )}
                 />
 
+                {/* activity history */}
                 <FormField
                   control={form.control}
                   name='activity_history'
@@ -541,7 +695,9 @@ export function UsersActionDialog() {
                       <Select
                         disabled={readOnly}
                         value={String(field.value)}
-                        onValueChange={(v) => field.onChange(v === 'true')}
+                        onValueChange={(value) =>
+                          field.onChange(value === 'true')
+                        }
                       >
                         <FormControl>
                           <SelectTrigger>
@@ -565,8 +721,102 @@ export function UsersActionDialog() {
                     </FormItem>
                   )}
                 />
+
+                {/* shirt number */}
+                <FormField
+                  control={form.control}
+                  name='shirt_number'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>شماره پیراهن</FormLabel>
+
+                      <FormControl>
+                        <Input
+                          type='number'
+                          {...field}
+                          disabled={readOnly}
+                          value={field.value ?? ''}
+                          onChange={(e) => {
+                            const value = e.target.value
+                            field.onChange(
+                              value === '' ? undefined : Number(value)
+                            )
+                          }}
+                        />
+                      </FormControl>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                {/* team name */}
+                {activity_history && (
+                  <FormField
+                    control={form.control}
+                    name='team_name'
+                    render={({ field }) => (
+                      <FormItem>
+                        <FormLabel>نام تیم</FormLabel>
+
+                        <FormControl>
+                          <Input
+                            {...field}
+                            disabled={readOnly}
+                            value={field.value ?? ''}
+                          />
+                        </FormControl>
+
+                        <FormMessage />
+                      </FormItem>
+                    )}
+                  />
+                )}
+
+                {/* favorite iranian team */}
+                <FormField
+                  control={form.control}
+                  name='favorite_iranian_team'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>تیم محبوب ایرانی</FormLabel>
+
+                      <FormControl>
+                        <Input
+                          {...field}
+                          disabled={readOnly}
+                          value={field.value ?? ''}
+                        />
+                      </FormControl>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
+
+                {/* favorite foreign team */}
+                <FormField
+                  control={form.control}
+                  name='favorite_foreign_team'
+                  render={({ field }) => (
+                    <FormItem>
+                      <FormLabel>تیم محبوب خارجی</FormLabel>
+
+                      <FormControl>
+                        <Input
+                          {...field}
+                          disabled={readOnly}
+                          value={field.value ?? ''}
+                        />
+                      </FormControl>
+
+                      <FormMessage />
+                    </FormItem>
+                  )}
+                />
               </div>
 
+              {/* bio */}
               <FormField
                 control={form.control}
                 name='bio'
@@ -579,6 +829,7 @@ export function UsersActionDialog() {
                         {...field}
                         disabled={readOnly}
                         value={field.value ?? ''}
+                        rows={4}
                       />
                     </FormControl>
 
@@ -587,13 +838,10 @@ export function UsersActionDialog() {
                 )}
               />
 
+              {/* buttons */}
               {!readOnly && (
-                <div className='flex justify-end gap-2'>
-                  <Button
-                    type='button'
-                    variant='outline'
-                    onClick={() => setOpen(null)}
-                  >
+                <div className='flex justify-end gap-3'>
+                  <Button type='button' variant='outline' onClick={handleClose}>
                     انصراف
                   </Button>
 
@@ -603,7 +851,19 @@ export function UsersActionDialog() {
                       createMutation.isPending || updateMutation.isPending
                     }
                   >
-                    {isAdd ? 'ایجاد کاربر' : 'ذخیره تغییرات'}
+                    {createMutation.isPending || updateMutation.isPending
+                      ? 'در حال ارسال...'
+                      : isAdd
+                        ? 'افزودن'
+                        : 'ویرایش'}
+                  </Button>
+                </div>
+              )}
+
+              {readOnly && (
+                <div className='flex justify-end'>
+                  <Button type='button' variant='outline' onClick={handleClose}>
+                    بستن
                   </Button>
                 </div>
               )}

+ 0 - 16
src/features/users/components/users-dialogs.tsx

@@ -1,9 +1,7 @@
 import { useUsers } from './users-provider'
 
-// import { UsersInviteDialog } from './users-invite-dialog'
 import { UsersDeleteDialog } from './users-delete-dialog'
 import { UsersActionDialog } from './users-action-dialog'
-// import { UsersMultiDeleteDialog } from './users-multi-delete-dialog'
 
 export function UsersDialogs() {
   const { open, setOpen, currentRow, setCurrentRow } = useUsers()
@@ -17,13 +15,6 @@ export function UsersDialogs() {
     <>
       <UsersActionDialog />
 
-      {/* <UsersInviteDialog
-        open={open === 'invite'}
-        onOpenChange={(state) => {
-          if (!state) handleClose()
-        }}
-      /> */}
-
       <UsersDeleteDialog
         open={open === 'delete'}
         onOpenChange={(state) => {
@@ -31,13 +22,6 @@ export function UsersDialogs() {
         }}
         currentRow={currentRow}
       />
-
-      {/* <UsersMultiDeleteDialog
-        open={open === 'multi-delete'}
-        onOpenChange={(state) => {
-          if (!state) handleClose()
-        }}
-      /> */}
     </>
   )
 }

+ 0 - 7
src/features/users/components/users-primary-buttons.tsx

@@ -6,13 +6,6 @@ export function UsersPrimaryButtons() {
   const { setOpen } = useUsers()
   return (
     <div className='flex gap-2'>
-      {/* <Button
-        variant='outline'
-        className='space-x-1'
-        onClick={() => setOpen('invite')}
-      >
-        <span>دعوت کاربر</span> <MailPlus size={18} />
-      </Button> */}
       <Button className='space-x-1' onClick={() => setOpen('add')}>
         <span>افزودن کاربر</span> <UserPlus size={18} />
       </Button>

+ 2 - 1
src/hooks/location/useProvinces.ts

@@ -2,9 +2,10 @@ import { useQuery } from '@tanstack/react-query'
 import { locationService } from '@/services/location.service'
 import { queryKeys } from '@/core/react-query/keys'
 
-export function useProvinces() {
+export function useProvinces(enabled: boolean) {
   return useQuery({
     queryKey: queryKeys.location.provinces,
     queryFn: locationService.provinces,
+    enabled,
   })
 }

+ 5 - 1
src/hooks/users/useUpdateUser.tsx

@@ -15,7 +15,11 @@ export function useUpdateUser() {
     mutationFn: ({ id, payload }: UpdateUserVariables) =>
       usersService.update(id, payload),
 
-    onSuccess: () => {
+    onSuccess: (_, variables) => {
+      queryClient.invalidateQueries({
+        queryKey: queryKeys.users.detail(variables.id),
+      })
+
       queryClient.invalidateQueries({
         queryKey: queryKeys.users.lists(),
       })

+ 2 - 2
src/types/users.types.ts

@@ -89,13 +89,13 @@ export interface CreateUserPayload {
   province_id?: number
   city_id?: number
   gender: number
-  birth_date: string
+  birth_date: string | undefined
   weight?: number
   height?: number
   foot_specialization?: string
   post_skill?: string
   skill_level?: string
-  activity_history?: boolean
+  activity_history?: boolean | number
   team_name?: string
   favorite_iranian_team?: string
   favorite_foreign_team?: string