From 2e2d5b35695ef3d45e835bd2748320f084f3c786 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 6 Jan 2026 10:46:43 +0330 Subject: [PATCH] subscription list --- src/config/Pages.ts | 3 + src/langs/fa.json | 17 ++- src/pages/subscriptions/List.tsx | 137 ++++++++++++++++++ .../hooks/useSucscriptionData.ts | 9 ++ .../service/SubscriptionService.ts | 8 + .../subscriptions/types/SubscriptionTypes.ts | 71 +++++++++ src/router/Main.tsx | 3 + src/shared/SideBar.tsx | 10 +- 8 files changed, 256 insertions(+), 2 deletions(-) create mode 100644 src/pages/subscriptions/List.tsx create mode 100644 src/pages/subscriptions/hooks/useSucscriptionData.ts create mode 100644 src/pages/subscriptions/service/SubscriptionService.ts create mode 100644 src/pages/subscriptions/types/SubscriptionTypes.ts diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 987ea28..b3dfb67 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -136,4 +136,7 @@ export const Pages = { smsCountList: "/dmenu/sms-count/list", }, }, + subscriptions: { + list: "/subscriptions/list", + }, }; diff --git a/src/langs/fa.json b/src/langs/fa.json index 357a82f..803fcce 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -79,7 +79,8 @@ "groups_icon": "گروه های آیکون", "reports": "گزارش ها", "restaurants": "رستوران ها", - "sms_count": "تعداد پیامک" + "sms_count": "تعداد پیامک", + "subscriptions": "اشتراک ها" }, "slider": { "new_slider": "اسلایدر جدید", @@ -894,5 +895,19 @@ "subscription_end_date": "تاریخ پایان اشتراک", "days_remaining": "چند روز مانده" }, + "subscription": { + "list_subscriptions": "لیست اشتراک‌ها", + "service": "سرویس", + "plan": "پلن", + "subscription_start_date": "تاریخ شروع اشتراک", + "subscription_end_date": "تاریخ پایان اشتراک", + "days_remaining": "چند روز مانده", + "expired": "منقضی شده", + "cancelled": "لغو شده", + "active": "فعال", + "inactive": "غیرفعال", + "days": "روز", + "id": "شناسه" + }, "cancel": "لغو" } diff --git a/src/pages/subscriptions/List.tsx b/src/pages/subscriptions/List.tsx new file mode 100644 index 0000000..bfdfe76 --- /dev/null +++ b/src/pages/subscriptions/List.tsx @@ -0,0 +1,137 @@ +import { FC, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useGetUserSubscriptions } from './hooks/useSucscriptionData' +import PageLoading from '../../components/PageLoading' +import Td from '../../components/Td' +import Pagination from '../../components/Pagination' +import { UserSubscriptionsResponse, UserSubscription } from './types/SubscriptionTypes' +import moment from 'moment-jalaali' + +const SubscriptionsList: FC = () => { + const { t } = useTranslation('global') + const [page, setPage] = useState(1) + const { data: subscriptionsResponse, isLoading } = useGetUserSubscriptions(page) + + const subscriptionsData = (subscriptionsResponse as UserSubscriptionsResponse | undefined)?.data + const subscriptionsList = subscriptionsData?.userSubscriptions || [] + const totalPages = subscriptionsData?.pager?.totalPages || 1 + + const formatJalaliDate = (date: string): string => { + return moment(date).format('jYYYY-jMM-jDD') + } + + const calculateDaysRemaining = (endDate: string): string => { + const end = moment(endDate) + const now = moment() + const diff = end.diff(now, 'days') + if (diff < 0) { + return t('subscription.expired') + } + return `${diff} ${t('subscription.days')}` + } + + const getStatusBadgeClass = (status: string): string => { + switch (status) { + case 'ACTIVE': + return 'bg-green-100 text-green-700' + case 'EXPIRED': + return 'bg-red-100 text-red-700' + case 'CANCELLED': + return 'bg-gray-100 text-gray-700' + case 'INACTIVE': + return 'bg-yellow-100 text-yellow-700' + default: + return 'bg-blue-100 text-blue-700' + } + } + + const getStatusText = (status: string): string => { + switch (status) { + case 'ACTIVE': + return t('subscription.active') + case 'EXPIRED': + return t('subscription.expired') + case 'CANCELLED': + return t('subscription.cancelled') + case 'INACTIVE': + return t('subscription.inactive') + default: + return status + } + } + + return ( +
+
+
+ {t('subscription.list_subscriptions')} +
+
+ + {isLoading ? ( + + ) : ( +
+ + + + + + + {subscriptionsList.map((item: UserSubscription) => ( + + + + + + + ))} + +
+ + + + + + + + + +
+ + + + + + +
+ {formatJalaliDate(item.startDate)} +
+
+
+ {formatJalaliDate(item.endDate)} +
+
+
+ {calculateDaysRemaining(item.endDate)} +
+
+
+ {getStatusText(item.status)} +
+
+ + {totalPages > 1 && ( + + )} +
+ )} +
+ ) +} + +export default SubscriptionsList \ No newline at end of file diff --git a/src/pages/subscriptions/hooks/useSucscriptionData.ts b/src/pages/subscriptions/hooks/useSucscriptionData.ts new file mode 100644 index 0000000..9736393 --- /dev/null +++ b/src/pages/subscriptions/hooks/useSucscriptionData.ts @@ -0,0 +1,9 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/SubscriptionService"; + +export const useGetUserSubscriptions = (page: number) => { + return useQuery({ + queryKey: ["user-subscriptions", page], + queryFn: () => api.getUserSubscriptions(page), + }); +}; diff --git a/src/pages/subscriptions/service/SubscriptionService.ts b/src/pages/subscriptions/service/SubscriptionService.ts new file mode 100644 index 0000000..f6452b8 --- /dev/null +++ b/src/pages/subscriptions/service/SubscriptionService.ts @@ -0,0 +1,8 @@ +import axios from "../../../config/axios"; + +export const getUserSubscriptions = async (page: number) => { + const { data } = await axios.get( + `/subscriptions/admin/user-subscriptions/all?page=${page}` + ); + return data; +}; diff --git a/src/pages/subscriptions/types/SubscriptionTypes.ts b/src/pages/subscriptions/types/SubscriptionTypes.ts new file mode 100644 index 0000000..b5c9bfb --- /dev/null +++ b/src/pages/subscriptions/types/SubscriptionTypes.ts @@ -0,0 +1,71 @@ +export interface SubscriptionUser { + id: string; + createdAt: string; + updatedAt: string; + email: string | null; + phone: string; + userName: string; + firstName: string; + lastName: string; + birthDate: string | null; + nationalCode: string | null; + profilePic: string | null; + emailVerified: boolean; + financialType: string | null; + deletedAt: string | null; +} + +export interface SubscriptionService { + name: string; + title: string; +} + +export interface SubscriptionPlan { + id: string; + createdAt: string; + updatedAt: string; + name: string; + duration: number; + isFree: boolean; + price: number; + originalPrice: number; + isActive: boolean; + service: SubscriptionService; + deletedAt: string | null; +} + +export interface UserSubscription { + id: string; + createdAt: string; + updatedAt: string; + user: SubscriptionUser; + plan: SubscriptionPlan; + startDate: string; + endDate: string; + businessName: string; + businessPhone: string; + description: string | null; + slug: string; + status: string; +} + +export interface SubscriptionPager { + page: number; + limit: number; + totalItems: number; + totalPages: number; + prevPage: boolean | string; + nextPage: string | null; +} + +export interface UserSubscriptionsData { + pager: SubscriptionPager; + userSubscriptions: UserSubscription[]; + pagination: boolean; +} + +export interface UserSubscriptionsResponse { + statusCode: number; + success: boolean; + data: UserSubscriptionsData; +} diff --git a/src/router/Main.tsx b/src/router/Main.tsx index fc9c5f8..b9d1140 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -83,6 +83,7 @@ import WarningsList from '../pages/dmenu/report/List.tsx' import RestaurantsList from '../pages/dmenu/restaurant/List.tsx' import SmsCountList from '../pages/dmenu/restaurant/SmsCountList.tsx' import UpdateRestaurant from '../pages/dmenu/restaurant/Update.tsx' +import SubscriptionsList from '../pages/subscriptions/List.tsx' // import WarningsList from '../pages/dmenu/reports/List' // TODO: Create this component const MainRouter: FC = () => { @@ -179,6 +180,8 @@ const MainRouter: FC = () => { } /> } /> } /> + + } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 60a2c96..04e7424 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -2,7 +2,7 @@ import { FC, useEffect } from 'react' import LogoImage from '../assets/images/logo.svg' import LogoSmall from '../assets/images/logo-small.svg' import { useTranslation } from 'react-i18next' -import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Message, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare, Activity, Icon, Category, Warning2, Building, Sms } from 'iconsax-react' +import { Card, Code, CodeCircle, DocumentLike, DocumentText, Element3, Gallery, Headphone, Home2, Logout, Message, Messages3, Money3, NotificationStatus, People, Profile, Receipt21, Setting2, SmsTracking, Teacher, TicketDiscount, UserSquare, Activity, Icon, Category, Warning2, Building, Sms, BagTick } from 'iconsax-react' import SideBarItem from './SideBarItem' import { useLocation } from 'react-router-dom' import { Pages } from '../config/Pages' @@ -187,6 +187,14 @@ const SideBar: FC = () => { activeName='services' /> + } + title={t('sidebar.subscriptions')} + isActive={isActive('subscriptions')} + link={Pages.subscriptions.list} + activeName='payments' + /> + {