edit discount

This commit is contained in:
hamid zarghami
2025-12-07 09:32:21 +03:30
parent 2403a314d9
commit 44186d0ceb
7 changed files with 266 additions and 4 deletions
+9 -3
View File
@@ -35,15 +35,21 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
}, [value]); }, [value]);
useEffect(() => { useEffect(() => {
if (props.defaulValue && !value) { if (props.defaulValue) {
const defaultDate = new DateObject({ const defaultDate = new DateObject({
date: props.defaulValue, date: props.defaulValue,
calendar: persian, calendar: persian,
locale: persian_fa, locale: persian_fa,
}); });
setValue(defaultDate); const currentValueStr = value ? `${value.year}-${value.month.number}-${value.day}` : null;
const defaultValueStr = `${defaultDate.year}-${defaultDate.month.number}-${defaultDate.day}`;
if (currentValueStr !== defaultValueStr) {
setValue(defaultDate);
}
} else if (!props.defaulValue && value) {
setValue(null);
} }
}, [props.defaulValue, value]); }, [props.defaulValue]);
return ( return (
<div className="w-full"> <div className="w-full">
+174
View File
@@ -0,0 +1,174 @@
import { type FC, useState, useMemo, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useFormik } from 'formik'
import * as Yup from 'yup'
import { TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import Button from '@/components/Button'
import type { CreateCouponType } from './types/Types'
import { useGetCouponDetails, useUpdateCoupon } 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 UpdateCoupon: FC = () => {
const navigate = useNavigate()
const { id } = useParams<{ id: string }>()
const { data: couponData, isLoading } = useGetCouponDetails(id || '')
const { mutate: updateCoupon, isPending: isUpdating } = useUpdateCoupon()
const { data: categoriesData } = useGetCategories()
const { data: foodsData } = useGetFoods()
const [couponType, setCouponType] = useState<'PERCENTAGE' | 'FIXED'>('PERCENTAGE')
const categories = useMemo(() => categoriesData?.data || [], [categoriesData?.data])
const foods = useMemo(() => foodsData?.data || [], [foodsData?.data])
const formik = useFormik<CreateCouponType>({
initialValues: {
code: '',
name: '',
description: '',
value: 0,
maxDiscount: null,
minOrderAmount: 0,
maxUses: 0,
maxUsesPerUser: null,
startDate: null,
endDate: null,
isActive: true,
foodCategories: [],
foods: [],
userPhone: '',
type: 'PERCENTAGE',
},
validationSchema: Yup.object().shape({
type: Yup.string().required('نوع کوپن الزامی است'),
code: Yup.string().required('کد کوپن الزامی است'),
name: Yup.string().required('نام کوپن الزامی است'),
description: Yup.string(),
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(),
userPhone: Yup.string().nullable(),
}),
enableReinitialize: true,
onSubmit: (values) => {
if (!id) {
toast.error('شناسه کوپن یافت نشد')
return
}
const submitValues = {
...values,
userPhone: values.userPhone?.trim() || undefined,
}
updateCoupon(
{ id, params: submitValues },
{
onSuccess: () => {
toast.success('کوپن با موفقیت به‌روزرسانی شد')
navigate(Pages.coupons.list)
},
onError: (error: ErrorType) => {
toast.error(extractErrorMessage(error))
},
}
)
},
})
useEffect(() => {
if (couponData?.data) {
const coupon = couponData.data
setCouponType(coupon.type)
formik.setValues({
code: coupon.code,
name: coupon.name,
description: coupon.description || '',
value: coupon.value,
maxDiscount: coupon.maxDiscount,
minOrderAmount: coupon.minOrderAmount,
maxUses: coupon.maxUses,
maxUsesPerUser: coupon.maxUsesPerUser,
startDate: coupon.startDate,
endDate: coupon.endDate,
isActive: coupon.isActive,
foodCategories: coupon.foodCategories || [],
foods: coupon.foods || [],
userPhone: coupon.userPhone || '',
type: coupon.type,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [couponData?.data])
if (isLoading) {
return (
<div className='mt-5'>
<div className='text-center'>در حال بارگذاری...</div>
</div>
)
}
return (
<div className='mt-5'>
<div className='flex justify-between items-center mb-6'>
<h1 className='text-lg font-light'>ویرایش کد تخفیف</h1>
</div>
<div className='flex justify-end'>
<Button
className='flex gap-4 w-fit px-5'
isloading={isUpdating}
onClick={() => formik.handleSubmit()}
>
<div className='flex gap-2 items-center'>
<TickCircle color='white' size={20} className='ml-2' />
<span>ویرایش تخفیف</span>
</div>
</Button>
</div>
<div className='flex gap-7 mt-7'>
<div className='flex-1'>
<div className='bg-white rounded-4xl p-8'>
<div className='text-lg font-light mb-6'>ویرایش کد تخفیف</div>
<CouponBasicInfo
formik={formik}
/>
<CouponDiscountSettings
formik={formik}
couponType={couponType}
onCouponTypeChange={setCouponType}
/>
<CouponUsageSettings formik={formik} />
</div>
</div>
<div className='w-80 flex-shrink-0'>
<CouponSidebar
formik={formik}
categories={categories}
foods={foods}
/>
</div>
</div>
</div>
)
}
export default UpdateCoupon
@@ -16,6 +16,12 @@ const CouponUsageSettings: FC<CouponUsageSettingsProps> = ({ formik }) => {
return gregorianDate return gregorianDate
} }
const convertToPersian = (gregorianDate: string | null): string | undefined => {
if (!gregorianDate) return undefined
const persianDate = moment(gregorianDate, 'YYYY-MM-DD').format('jYYYY-jMM-jDD')
return persianDate
}
return ( return (
<> <>
<div className='grid grid-cols-2 gap-4 mb-6'> <div className='grid grid-cols-2 gap-4 mb-6'>
@@ -44,16 +50,20 @@ const CouponUsageSettings: FC<CouponUsageSettingsProps> = ({ formik }) => {
<div className='grid grid-cols-2 gap-4 mb-6'> <div className='grid grid-cols-2 gap-4 mb-6'>
<DatePickerComponent <DatePickerComponent
key={`startDate-${formik.values.startDate || 'empty'}`}
label='تاریخ شروع' label='تاریخ شروع'
placeholder='تاریخ شروع را انتخاب کنید' placeholder='تاریخ شروع را انتخاب کنید'
defaulValue={convertToPersian(formik.values.startDate)}
onChange={(date) => { onChange={(date) => {
const gregorianDate = convertToGregorian(date) const gregorianDate = convertToGregorian(date)
formik.setFieldValue('startDate', gregorianDate) formik.setFieldValue('startDate', gregorianDate)
}} }}
/> />
<DatePickerComponent <DatePickerComponent
key={`endDate-${formik.values.endDate || 'empty'}`}
label='تاریخ پایان' label='تاریخ پایان'
placeholder='تاریخ پایان را انتخاب کنید' placeholder='تاریخ پایان را انتخاب کنید'
defaulValue={convertToPersian(formik.values.endDate)}
onChange={(date) => { onChange={(date) => {
const gregorianDate = convertToGregorian(date) const gregorianDate = convertToGregorian(date)
formik.setFieldValue('endDate', gregorianDate) formik.setFieldValue('endDate', gregorianDate)
+25
View File
@@ -1,5 +1,6 @@
import * as api from "../service/CouponService"; import * as api from "../service/CouponService";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import type { CreateCouponType } from "../types/Types";
export const useGetCoupons = () => { export const useGetCoupons = () => {
return useQuery({ return useQuery({
@@ -37,3 +38,27 @@ export const useGenerateCouponCode = () => {
mutationFn: api.generateCouponCode, mutationFn: api.generateCouponCode,
}); });
}; };
export const useGetCouponDetails = (id: string) => {
return useQuery({
queryKey: ["coupon", id],
queryFn: () => api.getCouponDetails(id),
enabled: !!id,
});
};
export const useUpdateCoupon = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, params }: { id: string; params: CreateCouponType }) =>
api.updateCoupon(id, params),
onSuccess: (_, { id }) => {
queryClient.invalidateQueries({
queryKey: ["coupons"],
});
queryClient.invalidateQueries({
queryKey: ["coupon", id],
});
},
});
};
+19 -1
View File
@@ -1,5 +1,9 @@
import axios from "@/config/axios"; import axios from "@/config/axios";
import type { CreateCouponType, IGetCouponsResponse } from "../types/Types"; import type {
CreateCouponType,
IGetCouponsResponse,
IGetCouponDetailsResponse,
} from "../types/Types";
export const getCoupons = async (): Promise<IGetCouponsResponse> => { export const getCoupons = async (): Promise<IGetCouponsResponse> => {
const { data } = await axios.get<IGetCouponsResponse>("/admin/coupons"); const { data } = await axios.get<IGetCouponsResponse>("/admin/coupons");
@@ -20,3 +24,17 @@ export const generateCouponCode = async () => {
const { data } = await axios.get(`/admin/coupons/generate-code`); const { data } = await axios.get(`/admin/coupons/generate-code`);
return data; return data;
}; };
export const getCouponDetails = async (
id: string
): Promise<IGetCouponDetailsResponse> => {
const { data } = await axios.get<IGetCouponDetailsResponse>(
`/admin/coupons/${id}`
);
return data;
};
export const updateCoupon = async (id: string, params: CreateCouponType) => {
const { data } = await axios.patch(`/admin/coupons/${id}`, params);
return data;
};
+27
View File
@@ -1,4 +1,5 @@
import type { IResponse } from "@/types/response.types"; import type { IResponse } from "@/types/response.types";
import type { Restaurant } from "@/pages/paymentMethods/types/Types";
export type CouponType = "PERCENTAGE" | "FIXED"; export type CouponType = "PERCENTAGE" | "FIXED";
@@ -24,8 +25,34 @@ export interface ICoupon {
[key: string]: unknown; [key: string]: unknown;
} }
export interface ICouponDetails {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
restaurant: Restaurant;
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;
foodCategories: string[];
foods: string[];
userPhone: string | null;
}
export type IGetCouponsResponse = IResponse<ICoupon[]>; export type IGetCouponsResponse = IResponse<ICoupon[]>;
export type IGetCouponDetailsResponse = IResponse<ICouponDetails>;
export type CreateCouponType = { export type CreateCouponType = {
code: string; code: string;
name: string; name: string;
+2
View File
@@ -35,6 +35,7 @@ import CreateShipmentMethod from '@/pages/shipmentMethod/Create'
import UpdateShipmentMethod from '@/pages/shipmentMethod/Update' import UpdateShipmentMethod from '@/pages/shipmentMethod/Update'
import CouponsList from '@/pages/coupon/List' import CouponsList from '@/pages/coupon/List'
import CreateCoupon from '@/pages/coupon/Create' import CreateCoupon from '@/pages/coupon/Create'
import UpdateCoupon from '@/pages/coupon/Update'
const MainRouter: FC = () => { const MainRouter: FC = () => {
const { hasSubMenu } = useSharedStore() const { hasSubMenu } = useSharedStore()
@@ -85,6 +86,7 @@ const MainRouter: FC = () => {
<Route path={Pages.coupons.list} element={<CouponsList />} /> <Route path={Pages.coupons.list} element={<CouponsList />} />
<Route path={Pages.coupons.add} element={<CreateCoupon />} /> <Route path={Pages.coupons.add} element={<CreateCoupon />} />
<Route path={Pages.coupons.update + ':id'} element={<UpdateCoupon />} />
</Routes> </Routes>
</div> </div>
</div> </div>