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 = () => {