create discount

This commit is contained in:
hamid zarghami
2025-09-27 11:51:05 +03:30
parent ee89008a9f
commit 0fa7fbf99b
13 changed files with 712 additions and 25 deletions
+197
View File
@@ -0,0 +1,197 @@
import React, { useState, useMemo } from 'react'
import { Check, ChevronsUpDown, X, Search } from 'lucide-react'
import { cn } from '../lib/utils'
import { Button } from './ui/button'
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
} from './ui/command'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from './ui/popover'
export type MultiSelectOption = {
value: string
label: string
}
type MultiSelectSearchableProps = {
options: MultiSelectOption[]
value: string[]
onChange: (value: string[]) => void
onSearch?: (searchTerm: string) => void
onOpenChange?: (open: boolean) => void
placeholder?: string
label?: string
error_text?: string
disabled?: boolean
loading?: boolean
}
const MultiSelectSearchable: React.FC<MultiSelectSearchableProps> = ({
options,
value,
onChange,
onSearch,
onOpenChange,
placeholder = "انتخاب کنید...",
label,
error_text,
disabled = false,
loading = false,
}) => {
const [open, setOpen] = useState(false)
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
onOpenChange?.(newOpen)
}
const selectedLabels = useMemo(() => {
return value
.map(v => options.find(option => option.value === v)?.label)
.filter(Boolean)
}, [value, options])
const handleSelect = (currentValue: string) => {
const newValue = value.includes(currentValue)
? value.filter(v => v !== currentValue)
: [...value, currentValue]
onChange(newValue)
}
const handleRemove = (itemValue: string) => {
onChange(value.filter(v => v !== itemValue))
}
return (
<div className="w-full">
{label && (
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-1">
{label}
</label>
)}
<Popover
open={open}
onOpenChange={handleOpenChange}
>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn(
"w-full justify-between min-h-10 h-auto",
!value.length && "text-muted-foreground",
error_text && "border-red-500"
)}
disabled={disabled || loading}
>
<div className="flex flex-wrap gap-1 flex-1 text-left min-h-[20px]">
{selectedLabels.length > 0 ? (
selectedLabels.map((label, index) => (
<span
key={index}
className="inline-flex items-center gap-1 bg-secondary text-secondary-foreground px-2 py-1 rounded-md text-xs max-w-[200px]"
title={label}
>
<span className="truncate">{label}</span>
<X
color='red'
className="h-3 w-3 cursor-pointer hover:text-destructive shrink-0"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
handleRemove(value[index])
}}
/>
</span>
))
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</div>
<ChevronsUpDown color='gray' className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] max-w-[400px] p-0"
align="start"
onKeyDown={(e) => {
// Prevent event bubbling that might cause navigation
e.stopPropagation()
}}
>
<Command
shouldFilter={false}
onKeyDown={(e) => {
// Prevent any navigation or form submission from command
e.stopPropagation()
}}
>
<div className="flex h-9 items-center gap-2 border-b px-3">
<Search color='gray' className="size-4 shrink-0 opacity-50" />
<input
type="text"
placeholder="جستجو..."
className="placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50"
onChange={(e) => {
e.preventDefault()
onSearch?.(e.target.value)
}}
onKeyDown={(e) => {
// Prevent form submission or other default behaviors
if (e.key === 'Enter') {
e.preventDefault()
e.stopPropagation()
}
}}
/>
</div>
<CommandList>
<CommandEmpty>موردی یافت نشد.</CommandEmpty>
<CommandGroup>
{options.map((option) => {
const isSelected = value.includes(option.value)
return (
<CommandItem
key={option.value}
value={option.value}
onSelect={() => {
handleSelect(option.value)
}}
className="flex items-center"
>
<Check
color='gray'
className={cn(
"mr-2 h-4 w-4 shrink-0",
isSelected ? "opacity-100" : "opacity-0"
)}
/>
<span className="truncate flex-1" title={option.label}>
{option.label}
</span>
</CommandItem>
)
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{error_text && (
<div className="text-red-500 text-xs mt-1">{error_text}</div>
)}
</div>
)
}
export default MultiSelectSearchable
+112
View File
@@ -0,0 +1,112 @@
import React, { useState, useEffect, useMemo } from 'react'
import { useSearchProducts } from '../pages/products/hooks/useProductData'
import { type ProductListType } from '../pages/products/types/Types'
import MultiSelectSearchable, { type MultiSelectOption } from './MultiSelectSearchable'
type ProductMultiSelectProps = {
value: number[]
onChange: (value: number[]) => void
placeholder?: string
label?: string
error_text?: string
disabled?: boolean
}
const ProductMultiSelect: React.FC<ProductMultiSelectProps> = ({
value,
onChange,
placeholder = "انتخاب محصولات...",
label,
error_text,
disabled = false,
}) => {
const [searchTerm, setSearchTerm] = useState('')
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')
const [selectedProducts, setSelectedProducts] = useState<ProductListType[]>([])
const [dropdownOpen, setDropdownOpen] = useState(false)
// Debounce search term
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm)
}, 300)
return () => clearTimeout(timer)
}, [searchTerm])
// Search products (includes initial load and search)
const { data: searchResults, isLoading } = useSearchProducts(debouncedSearchTerm, dropdownOpen)
// Combine selected products with search results
const allProducts = useMemo(() => {
const productsMap = new Map<string, ProductListType>()
// Add selected products
selectedProducts.forEach(product => {
productsMap.set(product._id.toString(), product)
})
// Add search results
if (searchResults?.results?.products) {
searchResults.results.products.forEach((product: ProductListType) => {
productsMap.set(product._id.toString(), product)
})
}
return Array.from(productsMap.values())
}, [selectedProducts, searchResults])
// Convert to options format
const options: MultiSelectOption[] = useMemo(() => {
return allProducts.map(product => ({
value: product._id.toString(),
label: product.title_fa || product.title_en
}))
}, [allProducts])
const handleChange = (newValue: string[]) => {
// Update selected products list
const newSelectedProducts = allProducts.filter(product =>
newValue.includes(product._id.toString())
)
setSelectedProducts(newSelectedProducts)
// Convert back to numbers for the parent component
onChange(newValue.map(v => parseInt(v)))
}
// Custom search handler for MultiSelectSearchable
const handleSearch = (term: string) => {
setSearchTerm(term)
}
return (
<div className="w-full">
{label && (
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 block mb-1">
{label}
</label>
)}
<MultiSelectSearchable
options={options}
value={value.map(v => v.toString())}
onChange={handleChange}
onSearch={handleSearch}
onOpenChange={setDropdownOpen}
placeholder={placeholder}
error_text={error_text}
disabled={disabled}
loading={isLoading}
/>
{dropdownOpen && !isLoading && allProducts.length === 0 && (
<div className="text-xs text-muted-foreground mt-1">
{searchTerm ? 'محصولی یافت نشد.' : 'برای جستجو نام محصول را تایپ کنید.'}
</div>
)}
</div>
)
}
export default ProductMultiSelect
+1
View File
@@ -13,6 +13,7 @@ type Props = {
error_text?: string, error_text?: string,
placeholder?: string, placeholder?: string,
label?: string, label?: string,
value?: any,
} & SelectHTMLAttributes<HTMLSelectElement> } & SelectHTMLAttributes<HTMLSelectElement>
const Select: FC<Props> = (props: Props) => { const Select: FC<Props> = (props: Props) => {
+4 -4
View File
@@ -64,10 +64,10 @@ export const Pages = {
developers: { developers: {
list: "/developers/list", list: "/developers/list",
}, },
discount: { coupon: {
list: "/discounts/list", list: "/coupons/list",
create: "/discounts/create", create: "/coupons/create",
detail: "/discounts/detail/", detail: "/coupons/update/",
}, },
referralCode: { referralCode: {
list: "/referral-code/list", list: "/referral-code/list",
+272
View File
@@ -0,0 +1,272 @@
import { useFormik } from 'formik'
import { type FC } from 'react'
import { type CreateCouponType } from './types/Types'
import * as Yup from 'yup'
import { useCreateCoupon } from './hooks/useCouponData'
import { useNavigate } from 'react-router-dom'
import { useGetCategories, type CategoryTreeNode } from '../category'
import Input from '../../components/Input'
import DatePicker from '../../components/DatePicker'
import Switch from '../../components/Switch'
import Button from '../../components/Button'
import Textarea from '../../components/Textarea'
import MultiSelectSearchable from '../../components/MultiSelectSearchable'
import ProductMultiSelect from '../../components/ProductMultiSelect'
import { TickCircle } from 'iconsax-react'
import { toast } from 'react-toastify'
import { extractErrorMessage } from '@/helpers/utils'
import { Pages } from '../../config/Pages'
const CreateCoupon: FC = () => {
const navigate = useNavigate()
const { data: categoriesData } = useGetCategories()
const createCouponMutation = useCreateCoupon()
const formik = useFormik<CreateCouponType>({
initialValues: {
discountPercentage: 0,
discountAmount: 0,
usageLimit: 1,
userUsageLimit: 1,
minPurchaseAmount: 0,
category: [],
includedProducts: [],
excludedProducts: [],
description: '',
includeSpecialSale: false,
expirationDate: '',
},
validationSchema: Yup.object({
usageLimit: Yup.number().min(1, 'محدودیت استفاده باید حداقل ۱ باشد').required('محدودیت استفاده الزامی است'),
userUsageLimit: Yup.number().min(1, 'محدودیت استفاده کاربر باید حداقل ۱ باشد').required('محدودیت استفاده کاربر الزامی است'),
description: Yup.string().required('توضیحات کوپن الزامی است'),
expirationDate: Yup.string().required('تاریخ انقضا الزامی است'),
discountPercentage: Yup.number().optional(),
discountAmount: Yup.number().optional(),
minPurchaseAmount: Yup.number().min(0, 'حداقل مبلغ خرید نمی‌تواند منفی باشد').optional(),
includedProducts: Yup.array().optional(),
excludedProducts: Yup.array().optional(),
}).test('at-least-one-discount', 'درصد تخفیف یا مبلغ تخفیف الزامی است', function (value) {
const { discountPercentage, discountAmount } = value;
// چک کردن اینکه حداقل یکی از دو فیلد پر شده باشد
if ((!discountPercentage || discountPercentage === 0) && (!discountAmount || discountAmount === 0)) {
return this.createError({
path: 'discountPercentage',
message: 'درصد تخفیف یا مبلغ تخفیف الزامی است',
});
}
// چک کردن اینکه هر دو فیلد همزمان پر نشده باشند
if ((discountPercentage && discountPercentage > 0) && (discountAmount && discountAmount > 0)) {
return this.createError({
path: 'discountPercentage',
message: 'نباید هر دو مقدار مبلغ تخفیف و درصد را وارد کنید',
});
}
if (discountPercentage && (discountPercentage < 1 || discountPercentage > 100)) {
return this.createError({
path: 'discountPercentage',
message: 'درصد تخفیف باید بین ۱ تا ۱۰۰ باشد',
});
}
if (discountAmount && discountAmount < 1) {
return this.createError({
path: 'discountAmount',
message: 'مبلغ تخفیف باید حداقل ۱ باشد',
});
}
return true;
}).test('products-validation', 'محصولات شامل و مستثنی نمی‌توانند همزمان انتخاب شوند', function (value) {
const { includedProducts, excludedProducts } = value;
// چک کردن اینکه هر دو فیلد همزمان پر نشده باشند
if (includedProducts && includedProducts.length > 0 && excludedProducts && excludedProducts.length > 0) {
return this.createError({
path: 'includedProducts',
message: 'نباید هر دو مقدار محصولات شامل و محصولات مستثنی را وارد کنید',
});
}
return true;
}),
onSubmit: async (values) => {
// تبدیل مقادیر به نوع مناسب
// اگر هر دو فیلد تخفیف پر شده باشند، فقط یکی را ارسال می‌کنیم
let discountPercentage: number | undefined
let discountAmount: number | undefined
if (values.discountPercentage && values.discountPercentage > 0) {
discountPercentage = Number(values.discountPercentage)
discountAmount = undefined
} else if (values.discountAmount && values.discountAmount > 0) {
discountAmount = Number(values.discountAmount)
discountPercentage = undefined
} else {
discountPercentage = undefined
discountAmount = undefined
}
const submitData = {
...values,
usageLimit: Number(values.usageLimit),
userUsageLimit: Number(values.userUsageLimit),
discountPercentage,
discountAmount,
minPurchaseAmount: values.minPurchaseAmount ? Number(values.minPurchaseAmount) : undefined,
includedProducts: values.includedProducts?.length ? values.includedProducts : undefined,
excludedProducts: values.excludedProducts?.length ? values.excludedProducts : undefined,
}
createCouponMutation.mutate(submitData, {
onSuccess: () => {
toast.success('کوپن با موفقیت ایجاد شد')
navigate(Pages.coupon.list)
},
onError: (error) => {
toast.error(extractErrorMessage(error))
}
})
}
})
const categoryOptions = categoriesData?.results?.data?.map((category: CategoryTreeNode) => ({
value: category._id,
label: category.title_fa
})) || []
return (
<div className='mt-4'>
<div className='flex justify-between items-center'>
<div className='flex items-center gap-3'>
<h1>ساخت کوپن جدید</h1>
</div>
<div>
<Button
className='px-6'
onClick={() => formik.handleSubmit()}
isLoading={createCouponMutation.isPending}
disabled={!formik.isValid || createCouponMutation.isPending}
>
<div className='flex gap-2 items-center'>
<TickCircle size={16} color='#fff' />
<div>ثبت کوپن</div>
</div>
</Button>
</div>
</div>
<div className='flex gap-6 xl:mt-8 mt-6'>
{/* اطلاعات اصلی */}
<div className='flex-1 text-sm bg-white py-8 xl:px-10 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>اطلاعات اصلی</h2>
<div className='space-y-6'>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='درصد تخفیف'
placeholder='مثال: ۱۰'
type='number'
{...formik.getFieldProps('discountPercentage')}
error_text={(formik.touched.discountPercentage || formik.touched.discountAmount) && formik.errors.discountPercentage ? formik.errors.discountPercentage : ''}
/>
<Input
label='مبلغ تخفیف (تومان)'
placeholder='مثال: ۵۰۰۰'
type='number'
{...formik.getFieldProps('discountAmount')}
error_text={(formik.touched.discountPercentage || formik.touched.discountAmount) && formik.errors.discountAmount ? formik.errors.discountAmount : ''}
/>
</div>
<div className='grid grid-cols-1 md:grid-cols-2 gap-6'>
<Input
label='محدودیت استفاده کل'
placeholder='مثال: ۱۰۰'
type='number'
{...formik.getFieldProps('usageLimit')}
error_text={formik.touched.usageLimit && formik.errors.usageLimit ? formik.errors.usageLimit : ''}
/>
<Input
label='محدودیت استفاده هر کاربر'
placeholder='مثال: ۵'
type='number'
{...formik.getFieldProps('userUsageLimit')}
error_text={formik.touched.userUsageLimit && formik.errors.userUsageLimit ? formik.errors.userUsageLimit : ''}
/>
</div>
<Input
label='حداقل مبلغ خرید (تومان)'
placeholder='مثال: ۱۰۰۰۰'
type='number'
{...formik.getFieldProps('minPurchaseAmount')}
error_text={formik.touched.minPurchaseAmount && formik.errors.minPurchaseAmount ? formik.errors.minPurchaseAmount : ''}
/>
<DatePicker
label='تاریخ انقضا'
placeholder='انتخاب تاریخ'
onChange={(date) => formik.setFieldValue('expirationDate', date)}
error_text={formik.touched.expirationDate && formik.errors.expirationDate ? formik.errors.expirationDate : ''}
/>
<Textarea
label='توضیحات کوپن'
placeholder='توضیحات کامل کوپن را وارد کنید...'
{...formik.getFieldProps('description')}
error_text={formik.touched.description && formik.errors.description ? formik.errors.description : ''}
/>
<div className='flex items-center gap-3'>
<Switch
active={formik.values.includeSpecialSale}
onChange={(value) => formik.setFieldValue('includeSpecialSale', value)}
label='شامل فروش ویژه'
/>
</div>
</div>
</div>
{/* تنظیمات پیشرفته */}
<div className='w-80 space-y-6'>
<div className='text-sm bg-white py-8 px-6 rounded-3xl'>
<h2 className='text-lg font-medium mb-6'>تنظیمات پیشرفته</h2>
<div className='space-y-6'>
<MultiSelectSearchable
label='دسته‌بندی‌های مجاز'
placeholder='انتخاب دسته‌بندی‌ها'
options={categoryOptions}
value={formik.values.category || []}
onChange={(value) => formik.setFieldValue('category', value)}
error_text={formik.touched.category && formik.errors.category ? formik.errors.category : ''}
/>
<ProductMultiSelect
label='محصولات شامل تخفیف'
placeholder='انتخاب محصولات شامل تخفیف'
value={formik.values.includedProducts || []}
onChange={(value) => formik.setFieldValue('includedProducts', value)}
error_text={formik.touched.includedProducts && formik.errors.includedProducts ? formik.errors.includedProducts : ''}
/>
<ProductMultiSelect
label='محصولات مستثنی از تخفیف'
placeholder='انتخاب محصولات مستثنی از تخفیف'
value={formik.values.excludedProducts || []}
onChange={(value) => formik.setFieldValue('excludedProducts', value)}
error_text={formik.touched.excludedProducts && formik.errors.excludedProducts ? formik.errors.excludedProducts : ''}
/>
</div>
</div>
</div>
</div>
</div>
)
}
export default CreateCoupon
+23
View File
@@ -0,0 +1,23 @@
import { type FC } from 'react'
import { useGetCoupons } from './hooks/useCouponData'
import PageLoading from '@/components/PageLoading'
import Error from '@/components/Error'
const CouponList: FC = () => {
const { data, isLoading, error } = useGetCoupons()
if (isLoading) {
return <PageLoading />
}
if (error) {
return <Error errorText="خطا در بارگذاری تخفیفات" />
}
return (
<div>List</div>
)
}
export default CouponList
+21
View File
@@ -0,0 +1,21 @@
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import * as api from "../service/CouponService";
import type { CreateCouponType } from "../types/Types";
export const useGetCoupons = (page: number = 1) => {
return useQuery({
queryKey: ["coupons", page],
queryFn: () => api.getCoupons(page),
});
};
export const useCreateCoupon = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (couponData: CreateCouponType) => api.createCoupon(couponData),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["coupons"] });
},
});
};
+14
View File
@@ -0,0 +1,14 @@
import axios from "../../../config/axios";
import type { CreateCouponType, CouponType } from "../types/Types";
export const getCoupons = async (page: number = 1) => {
const { data } = await axios.get(`/admin/coupon/native?page=${page}`);
return data;
};
export const createCoupon = async (
couponData: CreateCouponType
): Promise<CouponType> => {
const { data } = await axios.post("/admin/coupon", couponData);
return data;
};
+21
View File
@@ -0,0 +1,21 @@
export interface CreateCouponType {
discountPercentage?: number;
discountAmount?: number;
usageLimit: number;
userUsageLimit: number;
minPurchaseAmount?: number;
category?: string[];
includedProducts?: number[];
excludedProducts?: number[];
description: string;
includeSpecialSale: boolean;
expirationDate: string;
}
export interface CouponType extends CreateCouponType {
id: string;
code: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
@@ -87,4 +87,12 @@ export const useUpdateProduct = () => {
return useMutation({ return useMutation({
mutationFn: (variables: UpdateProductRequestType) => api.updateProduct(variables), mutationFn: (variables: UpdateProductRequestType) => api.updateProduct(variables),
}); });
};
export const useSearchProducts = (search: string, enabled: boolean = true) => {
return useQuery({
queryKey: ['products-search', search],
queryFn: () => api.getProducts(1, 50, search),
enabled: enabled,
});
}; };
+30 -17
View File
@@ -9,61 +9,74 @@ import {
type GetProductsResponseType, type GetProductsResponseType,
type GetProductVariantsResponseType, type GetProductVariantsResponseType,
type CreateProductVariantRequestType, type CreateProductVariantRequestType,
type CreateProductVariantResponseType,
type GetProductByIdResponseType, type GetProductByIdResponseType,
type UpdateVariantRequestType, type UpdateVariantRequestType,
type UpdateVariantResponseType,
type GetVariantByIdResponseType, type GetVariantByIdResponseType,
type UpdateProductDetailRequestType, type UpdateProductDetailRequestType,
type UpdateProductDetailResponseType, type UpdateProductDetailResponseType,
type UpdateProductAttributeRequestType, type UpdateProductAttributeRequestType,
type UpdateProductAttributeResponseType, type UpdateProductAttributeResponseType,
type UpdateProductRequestType, type UpdateProductRequestType,
type UpdateProductResponseType, type UpdateProductResponseType
} from "../types/Types"; } from "../types/Types";
export const createProductDetail = async (params: CreateProductDetailRequestType): Promise<CreateProductDetailResponseType> => { export const createProductDetail = async (
const { data } = await axios.post(`/admin/products/creation/detail`, params); variables: CreateProductDetailRequestType
): Promise<CreateProductDetailResponseType> => {
const { data } = await axios.post("/admin/products/create/detail", variables);
return data; return data;
}; };
export const createProductAttribute = async (params: CreateProductAttributeRequestType): Promise<CreateProductAttributeResponseType> => { export const createProductAttribute = async (
const { data } = await axios.post(`/admin/products/creation/attribute`, params); variables: CreateProductAttributeRequestType
): Promise<CreateProductAttributeResponseType> => {
const { data } = await axios.post("/admin/products/create/attribute", variables);
return data; return data;
}; };
export const saveProduct = async (params: SaveProductRequestType): Promise<SaveProductResponseType> => { export const saveProduct = async (
const { data } = await axios.post(`/admin/products/creation/save`, params); variables: SaveProductRequestType
): Promise<SaveProductResponseType> => {
const { data } = await axios.post("/admin/products/create/save", variables);
return data; return data;
}; };
export const getProducts = async (page: number = 1, limit: number = 10): Promise<GetProductsResponseType> => { export const getProducts = async (page: number = 1, limit: number = 10, search?: string): Promise<GetProductsResponseType> => {
const params: Record<string, string | number> = { page, limit };
if (search && search.trim()) {
params.q = search.trim();
}
const { data } = await axios.get(`/admin/products/native`, { const { data } = await axios.get(`/admin/products/native`, {
params: { page, limit } params
}); });
return data; return data;
}; };
export const getProductVariants = async (productId: string): Promise<GetProductVariantsResponseType> => { export const getProductVariants = async (productId: string): Promise<GetProductVariantsResponseType> => {
const { data } = await axios.get(`/admin/products/${productId}/variants`); const { data } = await axios.get(`/admin/products/variants/${productId}`);
return data; return data;
}; };
export const createProductVariant = async (params: CreateProductVariantRequestType)=> { export const createProductVariant = async (variables: CreateProductVariantRequestType): Promise<CreateProductVariantResponseType> => {
const { data } = await axios.post(`/admin/products/${params.productId}/variants`, params); const { data } = await axios.post("/admin/products/variants/create", variables);
return data; return data;
}; };
export const getProductById = async (id: string): Promise<GetProductByIdResponseType> => { export const getProductById = async (id: string): Promise<GetProductByIdResponseType> => {
const { data } = await axios.get(`/admin/products/${id}/details`); const { data } = await axios.get(`/admin/products/get/${id}`);
return data; return data;
}; };
export const updateVariant = async (params: UpdateVariantRequestType) => { export const updateVariant = async (variables: UpdateVariantRequestType): Promise<UpdateVariantResponseType> => {
const { data } = await axios.patch(`/admin/products/${params.productId}/variants`, params); const { data } = await axios.patch(`/admin/products/variants/update`, variables);
return data; return data;
}; };
export const getVariantById = async (productId: string, variantId: string): Promise<GetVariantByIdResponseType> => { export const getVariantById = async (productId: string, variantId: string): Promise<GetVariantByIdResponseType> => {
const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`); const { data } = await axios.get(`/admin/products/variant/${productId}/${variantId}`);
return data; return data;
}; };
@@ -80,4 +93,4 @@ export const updateProductAttribute = async (params: UpdateProductAttributeReque
export const updateProduct = async (params: UpdateProductRequestType): Promise<UpdateProductResponseType> => { export const updateProduct = async (params: UpdateProductRequestType): Promise<UpdateProductResponseType> => {
const { data } = await axios.patch(`/admin/products/update/save`, params); const { data } = await axios.patch(`/admin/products/update/save`, params);
return data; return data;
}; };
+5
View File
@@ -29,6 +29,8 @@ import CreateBlog from '@/pages/blogs/Create'
import UpdateBlog from '@/pages/blogs/Update' import UpdateBlog from '@/pages/blogs/Update'
import ShippingList from '@/pages/shipment/List' import ShippingList from '@/pages/shipment/List'
import CreateShipment from '@/pages/shipment/Create' import CreateShipment from '@/pages/shipment/Create'
import CouponList from '@/pages/Coupon/List'
import CreateCoupon from '@/pages/Coupon/Create'
const MainRouter: FC = () => { const MainRouter: FC = () => {
@@ -77,6 +79,9 @@ const MainRouter: FC = () => {
<Route path={Pages.shipment.list} element={<ShippingList />} /> <Route path={Pages.shipment.list} element={<ShippingList />} />
<Route path={Pages.shipment.create} element={<CreateShipment />} /> <Route path={Pages.shipment.create} element={<CreateShipment />} />
<Route path={Pages.coupon.list} element={<CouponList />} />
<Route path={Pages.coupon.create} element={<CreateCoupon />} />
</Routes> </Routes>
</div> </div>
</div> </div>
+4 -4
View File
@@ -119,11 +119,11 @@ const SideBar: FC = () => {
{/* تخفیف ها */} {/* تخفیف ها */}
<SideBarItem <SideBarItem
icon={<TicketDiscount variant={isActive('discounts') ? 'Bold' : 'Outline'} color={isActive('discounts') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />} icon={<TicketDiscount variant={isActive('coupon') ? 'Bold' : 'Outline'} color={isActive('coupon') ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title="تخفیف ها" title="تخفیف ها"
isActive={isActive('discounts')} isActive={isActive('coupon')}
link="/discounts" link={Pages.coupon.list}
activeName='discounts' activeName='coupon'
/> />
{/* بلاگ */} {/* بلاگ */}