From 6447177b0529e2d6b0b2f63b6949d8fa4cd17e17 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 6 Dec 2025 16:38:41 +0330 Subject: [PATCH] create coupon --- src/App.tsx | 2 +- src/components/DatePicker.tsx | 2 +- src/pages/coupon/Create.tsx | 137 ++++++++++++++++++ .../coupon/components/CategorySelector.tsx | 86 +++++++++++ .../coupon/components/CouponBasicInfo.tsx | 89 ++++++++++++ .../components/CouponDiscountSettings.tsx | 82 +++++++++++ src/pages/coupon/components/CouponSidebar.tsx | 43 ++++++ .../coupon/components/CouponUsageSettings.tsx | 68 +++++++++ src/pages/coupon/components/FoodSelector.tsx | 86 +++++++++++ src/pages/coupon/hooks/useCouponData.ts | 12 ++ src/pages/coupon/service/CouponService.ts | 7 +- src/pages/coupon/types/Types.ts | 17 +++ src/router/Main.tsx | 2 + 13 files changed, 630 insertions(+), 3 deletions(-) create mode 100644 src/pages/coupon/Create.tsx create mode 100644 src/pages/coupon/components/CategorySelector.tsx create mode 100644 src/pages/coupon/components/CouponBasicInfo.tsx create mode 100644 src/pages/coupon/components/CouponDiscountSettings.tsx create mode 100644 src/pages/coupon/components/CouponSidebar.tsx create mode 100644 src/pages/coupon/components/CouponUsageSettings.tsx create mode 100644 src/pages/coupon/components/FoodSelector.tsx diff --git a/src/App.tsx b/src/App.tsx index 05cb4ef..c9ba256 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import { type FC, useEffect, useState } from 'react' import { BrowserRouter } from 'react-router-dom' import 'swiper/swiper-bundle.css'; // import 'rc-rate/assets/index.css'; -// import "react-multi-date-picker/styles/layouts/mobile.css" +import "react-multi-date-picker/styles/layouts/mobile.css" import 'leaflet/dist/leaflet.css'; import './assets/fonts/irancell/style.css' import i18next from 'i18next' diff --git a/src/components/DatePicker.tsx b/src/components/DatePicker.tsx index 555d4f8..7e5d06a 100644 --- a/src/components/DatePicker.tsx +++ b/src/components/DatePicker.tsx @@ -28,7 +28,7 @@ const DatePickerComponent: FC = (props: Props) => { useEffect(() => { if (value) { - const formattedDate = `${value.year}/${value.month.number}/${value.day}`; + const formattedDate = `${value.year}-${value.month.number}-${value.day}`; props.onChange(formattedDate); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/pages/coupon/Create.tsx b/src/pages/coupon/Create.tsx new file mode 100644 index 0000000..f86b584 --- /dev/null +++ b/src/pages/coupon/Create.tsx @@ -0,0 +1,137 @@ +import { type FC, useState, useMemo } from 'react' +import { useFormik } from 'formik' +import * as Yup from 'yup' +import { TickCircle } from 'iconsax-react' +import { toast } from 'react-toastify' +import { useNavigate } from 'react-router-dom' +import Button from '@/components/Button' +import type { CreateCouponType } from './types/Types' +import { useCreateCoupon } from './hooks/useCouponData' +import { useGetCategories } from '@/pages/food/hooks/useFoodData' +import { useGetFoods } from '@/pages/food/hooks/useFoodData' +import { Pages } from '@/config/Pages' +import type { ErrorType } from '@/helpers/types' +import { extractErrorMessage } from '@/config/func' +import CouponBasicInfo from './components/CouponBasicInfo' +import CouponDiscountSettings from './components/CouponDiscountSettings' +import CouponUsageSettings from './components/CouponUsageSettings' +import CouponSidebar from './components/CouponSidebar' + +const CreateCoupon: FC = () => { + const navigate = useNavigate() + const { mutate: createCoupon, isPending: isCreating } = useCreateCoupon() + const { data: categoriesData } = useGetCategories() + const { data: foodsData } = useGetFoods() + + const [discountType, setDiscountType] = useState<'DIRECT' | 'CODE'>('CODE') + const [couponType, setCouponType] = useState<'PERCENTAGE' | 'FIXED'>('PERCENTAGE') + + const categories = useMemo(() => categoriesData?.data || [], [categoriesData?.data]) + const foods = useMemo(() => foodsData?.data || [], [foodsData?.data]) + + const formik = useFormik({ + initialValues: { + code: '', + name: '', + description: '', + type: 'PERCENTAGE', + value: 0, + maxDiscount: null, + minOrderAmount: 0, + maxUses: 0, + maxUsesPerUser: null, + startDate: null, + endDate: null, + isActive: true, + categoryIds: [], + foodIds: [], + }, + validationSchema: Yup.object().shape({ + code: Yup.string().required('کد کوپن الزامی است'), + name: Yup.string().required('نام کوپن الزامی است'), + description: Yup.string(), + type: Yup.string().required('نوع کوپن الزامی است'), + value: Yup.number().min(0, 'مقدار باید بیشتر از صفر باشد').required('مقدار کوپن الزامی است'), + maxDiscount: Yup.number().nullable().min(0, 'حداکثر تخفیف باید بیشتر از صفر باشد'), + minOrderAmount: Yup.number().min(0, 'حداقل مبلغ سفارش باید بیشتر از صفر باشد').required('حداقل مبلغ سفارش الزامی است'), + maxUses: Yup.number().min(0, 'حداکثر استفاده باید بیشتر از صفر باشد').required('حداکثر استفاده الزامی است'), + maxUsesPerUser: Yup.number().nullable().min(0, 'حداکثر استفاده برای هر کاربر باید بیشتر از صفر باشد'), + startDate: Yup.string().nullable(), + endDate: Yup.string().nullable(), + }), + onSubmit: (values) => { + createCoupon(values, { + onSuccess: () => { + toast.success('کوپن با موفقیت ایجاد شد') + navigate(Pages.coupons.list) + }, + onError: (error: ErrorType) => { + toast.error(extractErrorMessage(error)) + }, + }) + }, + }) + + const generateRandomCode = () => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + let code = '' + for (let i = 0; i < 8; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)) + } + formik.setFieldValue('code', code) + } + + return ( +
+
+

کد تخفیف جدید

+
+ +
+ +
+ +
+
+
+
کد تخفیف جدید
+ + + + + + +
+
+ +
+ +
+
+
+ ) +} + +export default CreateCoupon diff --git a/src/pages/coupon/components/CategorySelector.tsx b/src/pages/coupon/components/CategorySelector.tsx new file mode 100644 index 0000000..fcb9380 --- /dev/null +++ b/src/pages/coupon/components/CategorySelector.tsx @@ -0,0 +1,86 @@ +import { type FC, useState, useMemo } from 'react' +import type { FormikProps } from 'formik' +import Input from '@/components/Input' +import { Checkbox } from '@/components/ui/checkbox' +import type { CreateCouponType } from '../types/Types' + +interface Category { + id: string + title: string +} + +interface CategorySelectorProps { + formik: FormikProps + categories: Category[] +} + +const CategorySelector: FC = ({ formik, categories }) => { + const [search, setSearch] = useState('') + + const filteredCategories = useMemo(() => { + if (!search) return categories + return categories.filter((cat) => + cat.title.toLowerCase().includes(search.toLowerCase()) + ) + }, [categories, search]) + + const handleToggle = (categoryId: string, checked: boolean) => { + const currentIds = formik.values.categoryIds || [] + if (checked) { + formik.setFieldValue('categoryIds', [...currentIds, categoryId]) + } else { + formik.setFieldValue('categoryIds', currentIds.filter((id) => id !== categoryId)) + } + } + + const handleSelectAll = (checked: boolean) => { + if (checked) { + formik.setFieldValue('categoryIds', filteredCategories.map((cat) => cat.id)) + } else { + formik.setFieldValue('categoryIds', []) + } + } + + const isAllSelected = useMemo(() => { + return filteredCategories.length > 0 && + filteredCategories.every((cat) => formik.values.categoryIds?.includes(cat.id)) + }, [filteredCategories, formik.values.categoryIds]) + + return ( +
+
دسته بندیهایی که شامل تخفیف میشوند
+
+ setSearch(e.target.value)} + /> +
+
+ + +
+ {filteredCategories.map((category) => ( +
+ + handleToggle(category.id, checked as boolean) + } + /> + +
+ ))} +
+
+
+ ) +} + +export default CategorySelector + diff --git a/src/pages/coupon/components/CouponBasicInfo.tsx b/src/pages/coupon/components/CouponBasicInfo.tsx new file mode 100644 index 0000000..cf88d62 --- /dev/null +++ b/src/pages/coupon/components/CouponBasicInfo.tsx @@ -0,0 +1,89 @@ +import { type FC } from 'react' +import { Refresh } from 'iconsax-react' +import type { FormikProps } from 'formik' +import Input from '@/components/Input' +import Textarea from '@/components/Textarea' +import RadioGroup from '@/components/RadioGroup' +import Button from '@/components/Button' +import type { CreateCouponType } from '../types/Types' + +interface CouponBasicInfoProps { + formik: FormikProps + discountType: 'DIRECT' | 'CODE' + onDiscountTypeChange: (value: 'DIRECT' | 'CODE') => void + onGenerateCode?: () => void +} + +const CouponBasicInfo: FC = ({ + formik, + discountType, + onDiscountTypeChange, + onGenerateCode, +}) => { + return ( + <> +
+
نوع تخفیف
+ onDiscountTypeChange(value as 'DIRECT' | 'CODE')} + /> +
+ +
+
+
+ +
+
+
+ +
+