فهرست منبع

feat: implement subscription filter in user crud

Mohammad Mahdi Salimi 3 روز پیش
والد
کامیت
cd56429c5e

+ 30 - 13
src/components/data-table/faceted-filter.tsx

@@ -28,15 +28,41 @@ type DataTableFacetedFilterProps<TData, TValue> = {
     value: string
     value: string
     icon?: React.ComponentType<{ className?: string }>
     icon?: React.ComponentType<{ className?: string }>
   }[]
   }[]
+  multiple?: boolean
 }
 }
 
 
 export function DataTableFacetedFilter<TData, TValue>({
 export function DataTableFacetedFilter<TData, TValue>({
   column,
   column,
   title,
   title,
   options,
   options,
+  multiple = true,
 }: DataTableFacetedFilterProps<TData, TValue>) {
 }: DataTableFacetedFilterProps<TData, TValue>) {
   const facets = column?.getFacetedUniqueValues()
   const facets = column?.getFacetedUniqueValues()
-  const selectedValues = new Set(column?.getFilterValue() as string[])
+
+  const rawFilterValue = column?.getFilterValue()
+  const selectedValues = new Set<string>(
+    multiple
+      ? (rawFilterValue as string[] | undefined)
+      : rawFilterValue
+        ? [rawFilterValue as string]
+        : []
+  )
+
+  const handleSelect = (value: string) => {
+    if (!multiple) {
+      const isSelected = selectedValues.has(value)
+      column?.setFilterValue(isSelected ? undefined : value)
+      return
+    }
+
+    if (selectedValues.has(value)) {
+      selectedValues.delete(value)
+    } else {
+      selectedValues.add(value)
+    }
+    const filterValues = Array.from(selectedValues)
+    column?.setFilterValue(filterValues.length ? filterValues : undefined)
+  }
 
 
   return (
   return (
     <Popover>
     <Popover>
@@ -90,21 +116,12 @@ export function DataTableFacetedFilter<TData, TValue>({
                 return (
                 return (
                   <CommandItem
                   <CommandItem
                     key={option.value}
                     key={option.value}
-                    onSelect={() => {
-                      if (isSelected) {
-                        selectedValues.delete(option.value)
-                      } else {
-                        selectedValues.add(option.value)
-                      }
-                      const filterValues = Array.from(selectedValues)
-                      column?.setFilterValue(
-                        filterValues.length ? filterValues : undefined
-                      )
-                    }}
+                    onSelect={() => handleSelect(option.value)}
                   >
                   >
                     <div
                     <div
                       className={cn(
                       className={cn(
                         'flex size-4 items-center justify-center rounded-sm border border-primary',
                         'flex size-4 items-center justify-center rounded-sm border border-primary',
+                        multiple ? 'rounded-sm' : 'rounded-full',
                         isSelected
                         isSelected
                           ? 'bg-primary text-primary-foreground'
                           ? 'bg-primary text-primary-foreground'
                           : 'opacity-50 [&_svg]:invisible'
                           : 'opacity-50 [&_svg]:invisible'
@@ -133,7 +150,7 @@ export function DataTableFacetedFilter<TData, TValue>({
                     onSelect={() => column?.setFilterValue(undefined)}
                     onSelect={() => column?.setFilterValue(undefined)}
                     className='justify-center text-center'
                     className='justify-center text-center'
                   >
                   >
-                    Clear filters
+                    پاک کردن فیلتر‌ ها
                   </CommandItem>
                   </CommandItem>
                 </CommandGroup>
                 </CommandGroup>
               </>
               </>

+ 7 - 5
src/components/data-table/toolbar.tsx

@@ -1,7 +1,7 @@
 import { Cross2Icon } from '@radix-ui/react-icons'
 import { Cross2Icon } from '@radix-ui/react-icons'
 import { type Table } from '@tanstack/react-table'
 import { type Table } from '@tanstack/react-table'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
-import { Input } from '@/components/ui/input'
+// import { Input } from '@/components/ui/input'
 import { DataTableFacetedFilter } from './faceted-filter'
 import { DataTableFacetedFilter } from './faceted-filter'
 import { DataTableViewOptions } from './view-options'
 import { DataTableViewOptions } from './view-options'
 
 
@@ -12,6 +12,7 @@ type DataTableToolbarProps<TData> = {
   filters?: {
   filters?: {
     columnId: string
     columnId: string
     title: string
     title: string
+    multiple?: boolean
     options: {
     options: {
       label: string
       label: string
       value: string
       value: string
@@ -22,8 +23,8 @@ type DataTableToolbarProps<TData> = {
 
 
 export function DataTableToolbar<TData>({
 export function DataTableToolbar<TData>({
   table,
   table,
-  searchPlaceholder = 'Filter...',
-  searchKey,
+  // searchPlaceholder = 'Filter...',
+  // searchKey,
   filters = [],
   filters = [],
 }: DataTableToolbarProps<TData>) {
 }: DataTableToolbarProps<TData>) {
   const isFiltered =
   const isFiltered =
@@ -32,7 +33,7 @@ export function DataTableToolbar<TData>({
   return (
   return (
     <div className='flex items-center justify-between'>
     <div className='flex items-center justify-between'>
       <div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
       <div className='flex flex-1 flex-col-reverse items-start gap-y-2 sm:flex-row sm:items-center sm:space-x-2'>
-        {searchKey ? (
+        {/* {searchKey ? (
           <Input
           <Input
             placeholder={searchPlaceholder}
             placeholder={searchPlaceholder}
             value={
             value={
@@ -50,7 +51,7 @@ export function DataTableToolbar<TData>({
             onChange={(event) => table.setGlobalFilter(event.target.value)}
             onChange={(event) => table.setGlobalFilter(event.target.value)}
             className='h-8 w-37.5 lg:w-62.5'
             className='h-8 w-37.5 lg:w-62.5'
           />
           />
-        )}
+        )} */}
         <div className='flex gap-x-2'>
         <div className='flex gap-x-2'>
           {filters.map((filter) => {
           {filters.map((filter) => {
             const column = table.getColumn(filter.columnId)
             const column = table.getColumn(filter.columnId)
@@ -61,6 +62,7 @@ export function DataTableToolbar<TData>({
                 column={column}
                 column={column}
                 title={filter.title}
                 title={filter.title}
                 options={filter.options}
                 options={filter.options}
+                multiple={filter.multiple}
               />
               />
             )
             )
           })}
           })}

+ 15 - 0
src/core/react-query/keys.ts

@@ -28,4 +28,19 @@ export const queryKeys = {
     cities: (provinceId: number | string) =>
     cities: (provinceId: number | string) =>
       ['location', 'cities', provinceId] as const,
       ['location', 'cities', provinceId] as const,
   },
   },
+
+  evaluations: {
+    all: ['evaluations'] as const,
+
+    lists: () => [...queryKeys.evaluations.all, 'list'] as const,
+    list: (params: unknown) =>
+      [...queryKeys.evaluations.lists(), params] as const,
+
+    details: () => [...queryKeys.evaluations.all, 'detail'] as const,
+    detail: (id: number | string) =>
+      [...queryKeys.evaluations.details(), id] as const,
+
+    items: () => [...queryKeys.evaluations.all, 'item'] as const,
+    item: () => [...queryKeys.evaluations.items()] as const,
+  },
 }
 }

+ 12 - 1
src/features/users/components/users-columns.tsx

@@ -86,7 +86,7 @@ export const usersColumns: ColumnDef<User>[] = [
     header: ({ column }) => (
     header: ({ column }) => (
       <DataTableColumnHeader column={column} title='شماره موبایل' />
       <DataTableColumnHeader column={column} title='شماره موبایل' />
     ),
     ),
-    cell: ({ row }) => <div dir='ltr'>{row.original.phone}</div>,
+    cell: ({ row }) => <div>{row.original.phone}</div>,
   },
   },
 
 
   // {
   // {
@@ -137,6 +137,17 @@ export const usersColumns: ColumnDef<User>[] = [
     },
     },
   },
   },
 
 
+  {
+    id: 'subscription_type',
+    accessorFn: (row) => row.subscription?.plan?.type ?? 'free',
+    header: () => null,
+    cell: () => null,
+    filterFn: (row, columnId, filterValue: string) => {
+      if (!filterValue) return true
+      return row.getValue(columnId) === filterValue
+    },
+  },
+
   {
   {
     id: 'actions',
     id: 'actions',
     cell: DataTableRowActions,
     cell: DataTableRowActions,

+ 24 - 5
src/features/users/components/users-table.tsx

@@ -18,7 +18,7 @@ import {
   TableHeader,
   TableHeader,
   TableRow,
   TableRow,
 } from '@/components/ui/table'
 } from '@/components/ui/table'
-import { DataTablePagination } from '@/components/data-table'
+import { DataTablePagination, DataTableToolbar } from '@/components/data-table'
 import { DataTableBulkActions } from './data-table-bulk-actions'
 import { DataTableBulkActions } from './data-table-bulk-actions'
 import { usersColumns as columns } from './users-columns'
 import { usersColumns as columns } from './users-columns'
 import { type User } from '@/types/users.types'
 import { type User } from '@/types/users.types'
@@ -39,7 +39,9 @@ export function UsersTable({
 }: DataTableProps) {
 }: DataTableProps) {
   // Local UI-only states
   // Local UI-only states
   const [rowSelection, setRowSelection] = useState({})
   const [rowSelection, setRowSelection] = useState({})
-  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
+  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({
+    subscription_type: false,
+  })
   const [sorting, setSorting] = useState<SortingState>([])
   const [sorting, setSorting] = useState<SortingState>([])
 
 
   // Local state management for table (uncomment to use local-only state, not synced with URL)
   // Local state management for table (uncomment to use local-only state, not synced with URL)
@@ -63,6 +65,11 @@ export function UsersTable({
       { columnId: 'username', searchKey: 'username', type: 'string' },
       { columnId: 'username', searchKey: 'username', type: 'string' },
       { columnId: 'status', searchKey: 'status', type: 'array' },
       { columnId: 'status', searchKey: 'status', type: 'array' },
       { columnId: 'role', searchKey: 'role', type: 'array' },
       { columnId: 'role', searchKey: 'role', type: 'array' },
+      {
+        columnId: 'subscription_type',
+        searchKey: 'subscription_type',
+        type: 'string',
+      },
     ],
     ],
   })
   })
 
 
@@ -103,12 +110,24 @@ export function UsersTable({
         'flex flex-1 flex-col gap-4'
         'flex flex-1 flex-col gap-4'
       )}
       )}
     >
     >
-      {/* <DataTableToolbar
+      <DataTableToolbar
         table={table}
         table={table}
         searchPlaceholder='فیلتر کاربران...'
         searchPlaceholder='فیلتر کاربران...'
         searchKey='username'
         searchKey='username'
-        filters={[]}
-      /> */}
+        filters={[
+          {
+            columnId: 'subscription_type',
+            title: 'نوع اشتراک',
+            multiple: false,
+            options: [
+              { label: 'رایگان', value: 'no' },
+              { label: 'بیسیک', value: 'basic' },
+              { label: 'پرو', value: 'pro' },
+              { label: 'VIP', value: 'vip' },
+            ],
+          },
+        ]}
+      />
       <div className='overflow-hidden rounded-md border'>
       <div className='overflow-hidden rounded-md border'>
         <Table>
         <Table>
           <TableHeader>
           <TableHeader>

+ 1 - 0
src/features/users/index.tsx

@@ -20,6 +20,7 @@ export function Users() {
   const { data, isLoading } = useUsers({
   const { data, isLoading } = useUsers({
     page: search.page ?? 1,
     page: search.page ?? 1,
     per_page: search.pageSize ?? 10,
     per_page: search.pageSize ?? 10,
+    subscription_type: search.subscription_type,
   })
   })
   const users = data?.data.users
   const users = data?.data.users
   const paginationInfo = data?.data?.pagination
   const paginationInfo = data?.data?.pagination

+ 9 - 0
src/routes/_authenticated/users/index.tsx

@@ -17,6 +17,15 @@ const usersSearchSchema = z.object({
     )
     )
     .optional()
     .optional()
     .catch([]),
     .catch([]),
+  subscription_type: z
+    .union([
+      z.literal('no'),
+      z.literal('basic'),
+      z.literal('pro'),
+      z.literal('vip'),
+    ])
+    .optional()
+    .catch(undefined),
   // Per-column text filter (example for username)
   // Per-column text filter (example for username)
   username: z.string().optional().catch(''),
   username: z.string().optional().catch(''),
 })
 })

+ 2 - 0
src/services/users.service.ts

@@ -14,6 +14,8 @@ export const usersService = {
 
 
     if (params?.page) searchParams.set('page', String(params.page))
     if (params?.page) searchParams.set('page', String(params.page))
     if (params?.per_page) searchParams.set('per_page', String(params.per_page))
     if (params?.per_page) searchParams.set('per_page', String(params.per_page))
+    if (params?.subscription_type)
+      searchParams.set('subscription_type', params.subscription_type)
 
 
     const query = searchParams.toString()
     const query = searchParams.toString()
     const url = query ? `${BASE_URL}?${query}` : BASE_URL
     const url = query ? `${BASE_URL}?${query}` : BASE_URL

+ 1 - 0
src/types/common.types.ts

@@ -1,6 +1,7 @@
 export interface PaginationParams {
 export interface PaginationParams {
   page?: number
   page?: number
   per_page?: number
   per_page?: number
+  subscription_type?: string
 }
 }
 
 
 export interface Links {
 export interface Links {