Parcourir la source

feat: update subscription cell in user crud

Mohammad Mahdi Salimi il y a 1 semaine
Parent
commit
b6242f560a

+ 7 - 0
src/constants/users/users-constants.ts

@@ -34,3 +34,10 @@ export const activityHistoryOptions = [
   { label: 'دارم', value: true },
   { label: 'ندارم', value: false },
 ]
+
+export const subscriptionStatusLabels: Record<string, string> = {
+  active: 'فعال',
+  inactive: 'غیرفعال',
+  expired: 'منقضی شده',
+  cancelled: 'لغو شده',
+}

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

@@ -51,4 +51,8 @@ export const queryKeys = {
     list: (params: unknown) =>
       [...queryKeys.referralCodes.lists(), params] as const,
   },
+
+  activityFields: {
+    all: ['activityFields'] as const,
+  },
 }

+ 109 - 0
src/features/users/components/user-subscription-cell.tsx

@@ -0,0 +1,109 @@
+import { useMemo } from 'react'
+import {
+  Tooltip,
+  TooltipContent,
+  TooltipTrigger,
+} from '@/components/ui/tooltip'
+import { planDurations } from '@/constants/common-constansts'
+import { subscriptionStatusLabels } from '@/constants/users/users-constants'
+import { useActivityFields } from '@/hooks/activity-fields/useActivityFields'
+import { type Subscription } from '@/types/users.types'
+
+type Props = {
+  subscription: Subscription | null
+  subscriptions?: Subscription[]
+}
+
+function formatDate(value: string | null) {
+  if (!value) return '-'
+  return new Date(value).toLocaleDateString('fa-IR')
+}
+
+function formatPrice(value: string | number | null) {
+  const amount = Number(value)
+  if (Number.isNaN(amount)) return '-'
+  return amount.toLocaleString('fa-IR')
+}
+
+function getSubscriptionLabel(
+  item: Subscription,
+  activityFieldName?: string
+) {
+  const duration = planDurations[item.plan?.duration]
+  const type = item.plan?.type_label || item.plan?.type
+  const planLabel = duration ? `${type} (${duration})` : type
+
+  return activityFieldName ? `${planLabel} - ${activityFieldName}` : planLabel
+}
+
+export function UserSubscriptionCell({ subscription, subscriptions }: Props) {
+  const items = subscriptions?.length
+    ? subscriptions
+    : subscription
+      ? [subscription]
+      : []
+
+  const { data: activityFields = [] } = useActivityFields(items.length > 0)
+
+  const activityFieldNames = useMemo(
+    () => new Map(activityFields.map((field) => [field.id, field.name])),
+    [activityFields]
+  )
+
+  if (items.length === 0) {
+    return <div className='text-nowrap'>رایگان</div>
+  }
+
+  const label = items
+    .map((item) =>
+      getSubscriptionLabel(
+        item,
+        activityFieldNames.get(item.activity_field_id)
+      )
+    )
+    .join('، ')
+
+  return (
+    <Tooltip>
+      <TooltipTrigger asChild>
+        <div className='max-w-44 cursor-default truncate text-nowrap underline decoration-dotted underline-offset-4'>
+          {label}
+        </div>
+      </TooltipTrigger>
+      <TooltipContent
+        side='top'
+        align='start'
+        className='max-w-80 space-y-3 p-3 text-right whitespace-normal'
+      >
+        {items.map((item, index) => {
+          const fieldName = activityFieldNames.get(item.activity_field_id)
+
+          return (
+            <div
+              key={item.id}
+              className={
+                index > 0
+                  ? 'space-y-1.5 border-t border-primary-foreground/20 pt-3'
+                  : 'space-y-1.5'
+              }
+            >
+              <p className='font-semibold'>
+                {getSubscriptionLabel(item, fieldName)}
+              </p>
+              <div className='space-y-0.5 text-xs opacity-90'>
+                {fieldName && <p>رشته: {fieldName}</p>}
+                <p>
+                  وضعیت:{' '}
+                  {subscriptionStatusLabels[item.status] ?? item.status}
+                </p>
+                <p>شروع: {formatDate(item.started_at)}</p>
+                <p>انقضا: {formatDate(item.expires_at)}</p>
+                <p>مبلغ پرداختی: {formatPrice(item.price_paid)}</p>
+              </div>
+            </div>
+          )
+        })}
+      </TooltipContent>
+    </Tooltip>
+  )
+}

+ 5 - 6
src/features/users/components/users-columns.tsx

@@ -11,7 +11,7 @@ import { LongText } from '@/components/long-text'
 import { type User } from '@/types/users.types'
 
 import { DataTableRowActions } from './data-table-row-actions'
-import { planDurations } from '@/constants/common-constansts'
+import { UserSubscriptionCell } from './user-subscription-cell'
 
 export const usersColumns: ColumnDef<User>[] = [
   {
@@ -105,11 +105,10 @@ export const usersColumns: ColumnDef<User>[] = [
       <DataTableColumnHeader column={column} title='اشتراک' />
     ),
     cell: ({ row }) => (
-      <div className='text-nowrap'>
-        {row.original.subscription?.plan
-          ? `${row.original.subscription?.plan?.type} (${planDurations[row.original.subscription?.plan?.duration]})`
-          : 'رایگان'}
-      </div>
+      <UserSubscriptionCell
+        subscription={row.original.subscription}
+        subscriptions={row.original.subscriptions}
+      />
     ),
   },
 

+ 17 - 0
src/hooks/activity-fields/useActivityFields.ts

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query'
+import { queryKeys } from '@/core/react-query/keys'
+import { activityFieldsService } from '@/services/activity-fields.service'
+
+const ONE_DAY_MS = 24 * 60 * 60 * 1000
+
+export function useActivityFields(enabled = true) {
+  return useQuery({
+    queryKey: queryKeys.activityFields.all,
+    queryFn: activityFieldsService.list,
+    enabled,
+    staleTime: Infinity,
+    gcTime: ONE_DAY_MS,
+    refetchOnWindowFocus: false,
+    refetchOnReconnect: false,
+  })
+}

+ 11 - 0
src/services/activity-fields.service.ts

@@ -0,0 +1,11 @@
+import http from '@/core/api/http'
+import { type GetActivityFieldsResponse } from '@/types/activity-fields.types'
+
+export const activityFieldsService = {
+  list: async () => {
+    const res = await http.get<GetActivityFieldsResponse>(
+      '/public/activity-fields'
+    )
+    return res.data.activity_fields
+  },
+}

+ 14 - 0
src/types/activity-fields.types.ts

@@ -0,0 +1,14 @@
+export type ActivityField = {
+  id: number
+  name: string
+  slug: string
+  tag: string
+}
+
+export type GetActivityFieldsResponse = {
+  success: boolean
+  message: string
+  data: {
+    activity_fields: ActivityField[]
+  }
+}

+ 6 - 3
src/types/users.types.ts

@@ -31,22 +31,25 @@ export interface User {
   created_at: string
   updated_at: string
   subscription: Subscription | null
+  subscriptions?: Subscription[]
 }
 
-interface Subscription {
+export interface Subscription {
   id: number
+  activity_field_id: number
   plan: Plan
   status: string
   price_paid: string
   discount_price: string
   started_at: string
   expires_at: string
-  cancelled_at: null
-  payment_reference: null
+  cancelled_at: string | null
+  payment_reference: string | null
 }
 
 interface Plan {
   id: number
+  activity_field_id: number
   name: string
   price: number
   discount_price: number