From f3975603125166f058bddaca3d03e8ae60045077 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 6 Dec 2025 14:52:49 +0330 Subject: [PATCH] list of coupons --- src/config/Pages.ts | 5 + src/pages/coupon/List.tsx | 103 +++++++++++++++++ .../coupon/components/CouponFiltersFields.tsx | 35 ++++++ .../coupon/components/CouponTableColumns.tsx | 106 ++++++++++++++++++ src/pages/coupon/hooks/useCouponData.ts | 21 ++++ src/pages/coupon/hooks/useCouponFilters.ts | 29 +++++ src/pages/coupon/service/CouponService.ts | 12 ++ src/pages/coupon/types/Types.ts | 27 +++++ src/router/Main.tsx | 3 + src/shared/components/DiscountSubMenu.tsx | 4 +- 10 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 src/pages/coupon/List.tsx create mode 100644 src/pages/coupon/components/CouponFiltersFields.tsx create mode 100644 src/pages/coupon/components/CouponTableColumns.tsx create mode 100644 src/pages/coupon/hooks/useCouponData.ts create mode 100644 src/pages/coupon/hooks/useCouponFilters.ts create mode 100644 src/pages/coupon/service/CouponService.ts create mode 100644 src/pages/coupon/types/Types.ts diff --git a/src/config/Pages.ts b/src/config/Pages.ts index c6d1fed..7e8166d 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -66,4 +66,9 @@ export const Pages = { add: "/shipment_methods/add", update: "/shipment_methods/update/", }, + coupons: { + list: "/coupons/list", + add: "/coupons/add", + update: "/coupons/update/", + }, }; diff --git a/src/pages/coupon/List.tsx b/src/pages/coupon/List.tsx new file mode 100644 index 0000000..35dc426 --- /dev/null +++ b/src/pages/coupon/List.tsx @@ -0,0 +1,103 @@ +import { type FC, useMemo } from 'react' +import Table from '@/components/Table' +import Filters from '@/components/Filters' +import { useGetCoupons, useDeleteCoupon } from './hooks/useCouponData' +import { useCouponFilters } from './hooks/useCouponFilters' +import { getCouponTableColumns } from './components/CouponTableColumns' +import { useCouponFiltersFields } from './components/CouponFiltersFields' +import { Add } from 'iconsax-react' +import { Link } from 'react-router-dom' +import { Pages } from '@/config/Pages' +import Button from '@/components/Button' +import type { ICoupon } from './types/Types' +import type { RowDataType } from '@/components/types/TableTypes' + +const CouponsList: FC = () => { + const { + filters, + currentPage, + handleFiltersChange, + handlePageChange, + limit, + } = useCouponFilters() + + const { data: couponsData, isLoading } = useGetCoupons() + const { mutate: deleteCoupon, isPending: isDeleting } = useDeleteCoupon() + + const coupons = useMemo(() => couponsData?.data || [], [couponsData]) + + const filteredCoupons = useMemo(() => { + const searchValue = (filters.search || '').toLowerCase().trim() + const statusFilter = filters.status || 'all' + const typeFilter = filters.type || 'all' + + return coupons.filter((coupon: ICoupon) => { + const matchesSearch = + searchValue === '' || + (coupon.code || '').toLowerCase().includes(searchValue) || + (coupon.name || '').toLowerCase().includes(searchValue) + + const matchesStatus = + statusFilter === 'all' || + (statusFilter === 'active' && coupon.isActive) || + (statusFilter === 'inactive' && !coupon.isActive) + + const matchesType = + typeFilter === 'all' || + coupon.type === typeFilter + + return matchesSearch && matchesStatus && matchesType + }) + }, [coupons, filters]) + + const totalPages = Math.max(1, Math.ceil(filteredCoupons.length / limit) || 1) + + const paginatedCoupons = useMemo(() => { + const startIndex = (currentPage - 1) * limit + return filteredCoupons.slice(startIndex, startIndex + limit) + }, [filteredCoupons, currentPage, limit]) + + const columns = getCouponTableColumns({ + onDelete: (id: string) => deleteCoupon(id), + isDeleting + }) + const filterFields = useCouponFiltersFields() + + return ( +
+
+

لیست کوپن‌ها

+ + + +
+ +
+ +
+ + + columns={columns} + data={paginatedCoupons as (ICoupon & RowDataType)[]} + isloading={isLoading} + pagination={{ + currentPage, + totalPages, + onPageChange: handlePageChange, + }} + /> +
+ ) +} + +export default CouponsList \ No newline at end of file diff --git a/src/pages/coupon/components/CouponFiltersFields.tsx b/src/pages/coupon/components/CouponFiltersFields.tsx new file mode 100644 index 0000000..befeafa --- /dev/null +++ b/src/pages/coupon/components/CouponFiltersFields.tsx @@ -0,0 +1,35 @@ +import { useMemo } from 'react' +import type { FieldType } from '@/components/Filters' + +export const useCouponFiltersFields = (): FieldType[] => { + return useMemo(() => [ + { + type: 'input', + name: 'search', + placeholder: 'جستجو (کد یا نام)', + }, + { + type: 'select', + name: 'status', + placeholder: 'انتخاب وضعیت', + defaultValue: 'all', + options: [ + { label: 'همه وضعیت‌ها', value: 'all' }, + { label: 'فعال', value: 'active' }, + { label: 'غیرفعال', value: 'inactive' }, + ], + }, + { + type: 'select', + name: 'type', + placeholder: 'نوع کوپن', + defaultValue: 'all', + options: [ + { label: 'همه انواع', value: 'all' }, + { label: 'درصدی', value: 'PERCENTAGE' }, + { label: 'مقدار ثابت', value: 'FIXED' }, + ], + }, + ], []) +} + diff --git a/src/pages/coupon/components/CouponTableColumns.tsx b/src/pages/coupon/components/CouponTableColumns.tsx new file mode 100644 index 0000000..fd4542e --- /dev/null +++ b/src/pages/coupon/components/CouponTableColumns.tsx @@ -0,0 +1,106 @@ +import type { ColumnType } from '@/components/types/TableTypes' +import type { ICoupon } from '../types/Types' +import TrashWithConfrim from '@/components/TrashWithConfrim' +import { Eye } from 'iconsax-react' +import { Link } from 'react-router-dom' +import { Pages } from '@/config/Pages' +import { formatOptionalDate, formatFaNumber } from '@/helpers/func' +import Status from '@/components/Status' + +interface GetCouponTableColumnsParams { + onDelete?: (id: string) => void + isDeleting?: boolean +} + +export const getCouponTableColumns = ({ onDelete, isDeleting }: GetCouponTableColumnsParams = {}): ColumnType[] => { + return [ + { + key: 'code', + title: 'کد کوپن', + render: (item: ICoupon) => { + return item.code || '-' + } + }, + { + key: 'name', + title: 'نام', + render: (item: ICoupon) => { + return item.name || '-' + } + }, + { + key: 'type', + title: 'نوع', + render: (item: ICoupon) => { + return item.type === 'PERCENTAGE' ? 'درصدی' : 'مقدار ثابت' + } + }, + { + key: 'value', + title: 'مقدار', + render: (item: ICoupon) => { + if (item.type === 'PERCENTAGE') { + return `${formatFaNumber(item.value)}%` + } + return `${formatFaNumber(item.value)} تومان` + } + }, + { + key: 'usedCount', + title: 'استفاده شده', + render: (item: ICoupon) => { + return `${formatFaNumber(item.usedCount)}/${formatFaNumber(item.maxUses)}` + } + }, + { + key: 'startDate', + title: 'تاریخ شروع', + render: (item: ICoupon) => { + return formatOptionalDate(item.startDate) + } + }, + { + key: 'endDate', + title: 'تاریخ پایان', + render: (item: ICoupon) => { + return formatOptionalDate(item.endDate) + } + }, + { + key: 'isActive', + title: 'وضعیت', + render: (item: ICoupon) => { + return ( + + ) + } + }, + { + key: 'actions', + title: '', + render: (item: ICoupon) => { + return ( +
+ + + + {onDelete && ( + onDelete(item.id)} + isloading={isDeleting} + /> + )} +
+ ) + } + }, + ] +} + diff --git a/src/pages/coupon/hooks/useCouponData.ts b/src/pages/coupon/hooks/useCouponData.ts new file mode 100644 index 0000000..6fc340a --- /dev/null +++ b/src/pages/coupon/hooks/useCouponData.ts @@ -0,0 +1,21 @@ +import * as api from "../service/CouponService"; +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; + +export const useGetCoupons = () => { + return useQuery({ + queryKey: ["coupons"], + queryFn: api.getCoupons, + }); +}; + +export const useDeleteCoupon = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: api.deleteCoupon, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: ["coupons"], + }); + }, + }); +}; diff --git a/src/pages/coupon/hooks/useCouponFilters.ts b/src/pages/coupon/hooks/useCouponFilters.ts new file mode 100644 index 0000000..a2c44aa --- /dev/null +++ b/src/pages/coupon/hooks/useCouponFilters.ts @@ -0,0 +1,29 @@ +import { useState, useMemo } from 'react' +import type { FilterValues } from '@/components/Filters' + +const DEFAULT_PAGE = 1 +const DEFAULT_LIMIT = 10 + +export const useCouponFilters = () => { + const [filters, setFilters] = useState({}) + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE) + const [limit] = useState(DEFAULT_LIMIT) + + const handleFiltersChange = (newFilters: FilterValues) => { + setFilters(newFilters) + setCurrentPage(DEFAULT_PAGE) + } + + const handlePageChange = (page: number) => { + setCurrentPage(page) + } + + return { + filters, + currentPage, + handleFiltersChange, + handlePageChange, + limit, + } +} + diff --git a/src/pages/coupon/service/CouponService.ts b/src/pages/coupon/service/CouponService.ts new file mode 100644 index 0000000..1ebc4ee --- /dev/null +++ b/src/pages/coupon/service/CouponService.ts @@ -0,0 +1,12 @@ +import axios from "@/config/axios"; +import type { IGetCouponsResponse } from "../types/Types"; + +export const getCoupons = async (): Promise => { + const { data } = await axios.get("/admin/coupons"); + return data; +}; + +export const deleteCoupon = async (id: string) => { + const { data } = await axios.delete(`/admin/coupons/${id}`); + return data; +}; diff --git a/src/pages/coupon/types/Types.ts b/src/pages/coupon/types/Types.ts new file mode 100644 index 0000000..c250022 --- /dev/null +++ b/src/pages/coupon/types/Types.ts @@ -0,0 +1,27 @@ +import type { IResponse } from "@/types/response.types"; + +export type CouponType = "PERCENTAGE" | "FIXED"; + +export interface ICoupon { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + restaurant: string; + code: string; + name: string; + description: string; + type: CouponType; + value: number; + maxDiscount: number | null; + minOrderAmount: number; + maxUses: number; + usedCount: number; + maxUsesPerUser: number | null; + startDate: string | null; + endDate: string | null; + isActive: boolean; + [key: string]: unknown; +} + +export type IGetCouponsResponse = IResponse; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index e403c68..7c1ee87 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -33,6 +33,7 @@ import UpdateAdmin from '@/pages/admin/Update' import ShipmentMethodList from '@/pages/shipmentMethod/List' import CreateShipmentMethod from '@/pages/shipmentMethod/Create' import UpdateShipmentMethod from '@/pages/shipmentMethod/Update' +import CouponsList from '@/pages/coupon/List' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -80,6 +81,8 @@ const MainRouter: FC = () => { } /> } /> } /> + + } /> diff --git a/src/shared/components/DiscountSubMenu.tsx b/src/shared/components/DiscountSubMenu.tsx index 39142e6..e0ff600 100644 --- a/src/shared/components/DiscountSubMenu.tsx +++ b/src/shared/components/DiscountSubMenu.tsx @@ -31,12 +31,12 @@ const DiscountSubMenu: FC = () => {