manage variant producy
This commit is contained in:
@@ -3,7 +3,6 @@ import DatePicker from 'react-multi-date-picker';
|
||||
import persian from 'react-date-object/calendars/persian';
|
||||
import persian_fa from 'react-date-object/locales/persian_fa';
|
||||
import DateObject from 'react-date-object';
|
||||
import CalenderIcon from '../assets/images/calendar.svg'
|
||||
import { clx } from '../helpers/utils';
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +13,8 @@ type Props = {
|
||||
reset?: boolean;
|
||||
isDateTime?: boolean;
|
||||
className?: string;
|
||||
label?: string
|
||||
label: string,
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
@@ -47,13 +47,12 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{props.label &&
|
||||
<div className='text-sm'>
|
||||
{props.label}
|
||||
</div>}
|
||||
<div className='text-sm'>
|
||||
{props.label}
|
||||
</div>
|
||||
<div className={clx(
|
||||
'relative ',
|
||||
props.label && 'mt-1.5'
|
||||
'relative mt-1.5',
|
||||
props.readOnly && 'readOny'
|
||||
)}>
|
||||
<DatePicker
|
||||
placeholder={props.placeholder}
|
||||
@@ -62,7 +61,8 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
calendar={persian}
|
||||
locale={persian_fa}
|
||||
calendarPosition="bottom-right"
|
||||
className={`rmdp-mobile ${props.className}`}
|
||||
className={props.className}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
{props.error_text && props.error_text !== '' && (
|
||||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
||||
@@ -70,7 +70,7 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<img src={CalenderIcon} className='absolute top-0 bottom-0 my-auto left-2' />
|
||||
{/* <img src={CalenderIcon} className='absolute top-0 bottom-0 my-auto left-2' /> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -26,8 +26,8 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
||||
{
|
||||
props.open && (
|
||||
<Fragment>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-hidden fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full h-[80%] bottom-0 left-0 flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div style={{ maxWidth: props.width }} className='xl:justify-center xl:items-center items-end flex overflow-x-hidden overflow-hidden fixed inset-0 z-[60] h-auto top-0 bottom-0 m-auto outline-none focus:outline-none xl:max-w-xl mx-auto'>
|
||||
<div className='relative xl:h-full h-[80%] bottom-0 left-0 max-h-[80%] top-0 m-auto overflow-hidden flex xl:items-center sm:h-auto w-full xl:my-6 xl:p-2'>
|
||||
<div className='border-0 h-auto p-5 lg:min-w-full overflow-y-auto rounded-3xl rounded-b-none xl:rounded-b-3xl relative flex flex-col w-full bg-white outline-none focus:outline-none'>
|
||||
|
||||
|
||||
|
||||
@@ -127,5 +127,6 @@ export const Pages = {
|
||||
products: {
|
||||
create: "/products/create",
|
||||
list: "/products/list",
|
||||
variants: "/products/variants/",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -179,3 +179,20 @@ textarea::placeholder {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.rmdp-input {
|
||||
min-height: 40px;
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid #d0d0d0 !important;
|
||||
font-size: 12px !important;
|
||||
width: 100% !important;
|
||||
margin: 0px;
|
||||
padding-right: 16px !important;
|
||||
}
|
||||
.readOny .rmdp-input {
|
||||
background-color: #f5f5f5 !important;
|
||||
border: none !important;
|
||||
}
|
||||
.rmdp-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import StatusWithText from '../../components/StatusWithText';
|
||||
import Td from '../../components/Td';
|
||||
import TrashWithConfrim from '../../components/TrashWithConfrim';
|
||||
import { Edit, Eye } from 'iconsax-react';
|
||||
import Button from '@/components/Button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Pages } from '@/config/Pages';
|
||||
|
||||
const List: FC = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@@ -64,6 +67,7 @@ const List: FC = () => {
|
||||
<Td text={'عنوان'} />
|
||||
<Td text={'برند'} />
|
||||
<Td text={'دستهبندی'} />
|
||||
<Td text='تنوع' />
|
||||
<Td text={'وضعیت'} />
|
||||
<Td text={'قیمت'} />
|
||||
<Td text={'موجودی'} />
|
||||
@@ -101,6 +105,17 @@ const List: FC = () => {
|
||||
</Td>
|
||||
<Td text={product.brand_title_fa} />
|
||||
<Td text={product.category_title_fa} />
|
||||
<Td text=''>
|
||||
<Link
|
||||
to={`${Pages.products.variants}${product._id}`}
|
||||
>
|
||||
<Button
|
||||
className='h-8 text-xs'
|
||||
>
|
||||
مدیریت تنوع
|
||||
</Button>
|
||||
</Link>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<StatusWithText
|
||||
variant={getStatusVariant(product.status)}
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { type FC } from 'react'
|
||||
import { useGetProductVariants, useGetProductById } from './hooks/useProductData'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import PageLoading from '@/components/PageLoading'
|
||||
import type { ProductVariantDetailType } from './types/Types'
|
||||
import Td from '@/components/Td'
|
||||
import StatusWithText from '@/components/StatusWithText'
|
||||
import CreateVariant from './components/CreateVariant'
|
||||
import UpdateVariant from './components/UpdateVariant'
|
||||
|
||||
const ProductVariant: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
const { data, isLoading, refetch } = useGetProductVariants(id!)
|
||||
const { data: productData } = useGetProductById(id!)
|
||||
|
||||
const categoryTheme = productData?.results?.product?.category?.theme
|
||||
|
||||
const getVariantColumnHeader = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return 'سایز'
|
||||
case 'Color': return 'رنگ'
|
||||
case 'Meterage': return 'متریاژ'
|
||||
default: return 'تنوع'
|
||||
}
|
||||
}
|
||||
|
||||
const getVariantValue = (variant: ProductVariantDetailType) => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return variant.size?.value || '-'
|
||||
case 'Color': return 'رنگ' // TODO: باید نام رنگ را نمایش دهیم
|
||||
case 'Meterage': return '-' // TODO: باید فیلد meterage اضافه شود
|
||||
default: return '-'
|
||||
}
|
||||
}
|
||||
|
||||
const getMarketStatusVariant = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Marketable': return 'success';
|
||||
case 'out_of_stock': return 'error';
|
||||
case 'stopProduction': return 'warning';
|
||||
default: return 'warning';
|
||||
}
|
||||
};
|
||||
|
||||
const getMarketStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'Marketable': return 'قابل فروش';
|
||||
case 'out_of_stock': return 'ناموجود';
|
||||
case 'stopProduction': return 'توقف تولید';
|
||||
default: return status;
|
||||
}
|
||||
};
|
||||
|
||||
const renderVariantRow = (variant: ProductVariantDetailType) => (
|
||||
<tr key={variant._id} className='tr'>
|
||||
<Td text="">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">
|
||||
{variant.title_fa}
|
||||
</div>
|
||||
<div className="text-gray-500 text-xs">
|
||||
{variant.title_en}
|
||||
</div>
|
||||
</div>
|
||||
</Td>
|
||||
<Td text={variant.productCode} />
|
||||
<Td text="">
|
||||
<StatusWithText
|
||||
variant={getMarketStatusVariant(variant.market_status)}
|
||||
text={getMarketStatusText(variant.market_status)}
|
||||
/>
|
||||
</Td>
|
||||
<Td text={variant.stock.toString()} />
|
||||
<Td text={`${variant.price.selling_price.toLocaleString('fa-IR')} تومان`} />
|
||||
<Td text={getVariantValue(variant)} />
|
||||
<Td text="">
|
||||
<span className={`inline-block px-2 py-1 text-xs rounded-full ${variant.is_variant_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}>
|
||||
{variant.is_variant_active ? 'فعال' : 'غیرفعال'}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<span className={`inline-block px-2 py-1 text-xs rounded-full ${variant.isFreeShip ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{variant.isFreeShip ? 'بله' : 'خیر'}
|
||||
</span>
|
||||
</Td>
|
||||
<Td text="">
|
||||
<UpdateVariant variant={variant} onUpdate={refetch} />
|
||||
</Td>
|
||||
</tr>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CreateVariant />
|
||||
<div className='relative overflow-x-auto rounded-3xl mt-5 w-full'>
|
||||
<table className='w-full text-sm'>
|
||||
<thead className='thead'>
|
||||
<tr>
|
||||
<Td text={'عنوان'} />
|
||||
<Td text={'کد محصول'} />
|
||||
<Td text={'وضعیت بازار'} />
|
||||
<Td text={'موجودی'} />
|
||||
<Td text={'قیمت فروش'} />
|
||||
<Td text={getVariantColumnHeader()} />
|
||||
<Td text={'وضعیت'} />
|
||||
<Td text={'ارسال رایگان'} />
|
||||
<Td text={'اکشن'} />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{isLoading ? (
|
||||
<tr className='tr'>
|
||||
<td colSpan={9} className="text-center py-8">
|
||||
<PageLoading />
|
||||
</td>
|
||||
</tr>
|
||||
) : data?.results?.productVariants && data.results.productVariants.length > 0 ? (
|
||||
data.results.productVariants.map((variant) => renderVariantRow(variant))
|
||||
) : (
|
||||
<tr className='tr'>
|
||||
<td colSpan={9} className="text-center py-8 text-gray-500">
|
||||
هیچ variant ای یافت نشد
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductVariant
|
||||
@@ -0,0 +1,382 @@
|
||||
import Button from '@/components/Button'
|
||||
import DefaulModal from '@/components/DefaulModal'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import { useState, type FC } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useCreateProductVariant, useGetProductById } from '../hooks/useProductData'
|
||||
import { useGetCategoryVariants } from '@/pages/category'
|
||||
import type { CreateProductVariantRequestType } from '../types/Types'
|
||||
import { useGetWarranties } from '@/pages/warranty/hooks/useWarrantyData'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik'
|
||||
import type { FieldInputProps } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
interface FormikFieldProps {
|
||||
field: FieldInputProps<string | number>
|
||||
}
|
||||
|
||||
const CreateVariant: FC = () => {
|
||||
|
||||
const { id } = useParams()
|
||||
|
||||
const { data: productData } = useGetProductById(id!)
|
||||
const { data: categoryData } = useGetCategoryVariants(productData?.results?.product?.category?._id ?? '')
|
||||
const createProductVariant = useCreateProductVariant()
|
||||
const { data: warrantiesData } = useGetWarranties()
|
||||
|
||||
const [showModal, setShowModal] = useState<boolean>(false)
|
||||
|
||||
const categoryTheme = productData?.results?.product?.category?.theme
|
||||
const categoryVariants = categoryData?.results?.data?.category?.categoryVariants
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
variantId: Yup.string().required('انتخاب تنوع الزامی است'),
|
||||
warrantyId: Yup.string().required('انتخاب گارانتی الزامی است'),
|
||||
order_limit: Yup.number().required('محدودیت سفارش الزامی است').min(1, 'حداقل ۱ عدد'),
|
||||
sellerSpecialCode: Yup.string().required('کد ویژه فروشنده الزامی است'),
|
||||
retail_price: Yup.number().required('قیمت فروش الزامی است').min(0, 'قیمت نمیتواند منفی باشد'),
|
||||
package_weight: Yup.number().required('وزن بسته الزامی است').min(0.1, 'وزن باید حداقل ۰.۱ کیلوگرم باشد'),
|
||||
package_height: Yup.number().required('ارتفاع بسته الزامی است').min(1, 'ارتفاع باید حداقل ۱ سانتیمتر باشد'),
|
||||
package_length: Yup.number().required('طول بسته الزامی است').min(1, 'طول باید حداقل ۱ سانتیمتر باشد'),
|
||||
package_width: Yup.number().required('عرض بسته الزامی است').min(1, 'عرض باید حداقل ۱ سانتیمتر باشد'),
|
||||
postingTime: Yup.number().required('زمان ارسال الزامی است').min(1, 'زمان ارسال باید حداقل ۱ روز باشد'),
|
||||
stock: Yup.number().required('موجودی انبار الزامی است').min(0, 'موجودی نمیتواند منفی باشد')
|
||||
})
|
||||
|
||||
const initialValues = {
|
||||
variantId: '',
|
||||
warrantyId: '',
|
||||
order_limit: '',
|
||||
sellerSpecialCode: '',
|
||||
retail_price: '',
|
||||
package_weight: '',
|
||||
package_height: '',
|
||||
package_length: '',
|
||||
package_width: '',
|
||||
postingTime: '',
|
||||
stock: ''
|
||||
}
|
||||
|
||||
const getVariantOptions = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size':
|
||||
return categoryVariants?.sizes?.map(size => ({
|
||||
value: size._id.toString(),
|
||||
label: size.value
|
||||
})) || []
|
||||
case 'Color':
|
||||
return categoryVariants?.colors?.map((color, index) => ({
|
||||
value: index.toString(), // استفاده از index به عنوان ID موقت
|
||||
label: color.name
|
||||
})) || []
|
||||
case 'Meterage':
|
||||
return categoryVariants?.meterages?.map(meterage => ({
|
||||
value: meterage._id.toString(),
|
||||
label: meterage.value
|
||||
})) || []
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const getVariantLabel = () => {
|
||||
switch (categoryTheme) {
|
||||
case 'Size': return 'سایز'
|
||||
case 'Color': return 'رنگ'
|
||||
case 'Meterage': return 'متریاژ'
|
||||
default: return 'تنوع'
|
||||
}
|
||||
}
|
||||
|
||||
const getWarrantyOptions = () => {
|
||||
return warrantiesData?.results?.warranties?.map(warranty => ({
|
||||
value: warranty._id.toString(),
|
||||
label: `${warranty.name} (${warranty.duration})`
|
||||
})) || []
|
||||
}
|
||||
|
||||
const handleSubmit = (values: typeof initialValues) => {
|
||||
const baseData: CreateProductVariantRequestType = {
|
||||
productId: id!,
|
||||
warrantyId: parseInt(values.warrantyId),
|
||||
order_limit: parseInt(values.order_limit),
|
||||
sellerSpecialCode: values.sellerSpecialCode,
|
||||
retail_price: parseFloat(values.retail_price),
|
||||
package_weight: parseFloat(values.package_weight),
|
||||
package_height: parseFloat(values.package_height),
|
||||
package_length: parseFloat(values.package_length),
|
||||
package_width: parseFloat(values.package_width),
|
||||
postingTime: parseInt(values.postingTime),
|
||||
stock: parseInt(values.stock)
|
||||
}
|
||||
|
||||
// اضافه کردن variant بر اساس theme
|
||||
let variantData: CreateProductVariantRequestType
|
||||
|
||||
if (categoryTheme === 'Size') {
|
||||
variantData = { ...baseData, sizeId: parseInt(values.variantId) }
|
||||
} else if (categoryTheme === 'Color') {
|
||||
// برای رنگها از index به عنوان ID استفاده میکنیم
|
||||
variantData = { ...baseData, colorId: parseInt(values.variantId) + 1 } // +1 برای اینکه ID از 1 شروع شود
|
||||
} else if (categoryTheme === 'Meterage') {
|
||||
variantData = { ...baseData, meterageId: parseInt(values.variantId) }
|
||||
} else {
|
||||
variantData = baseData
|
||||
}
|
||||
|
||||
createProductVariant.mutate(variantData, {
|
||||
onSuccess: () => {
|
||||
setShowModal(false)
|
||||
toast.success('تنوع جدید با موفقیت ایجاد شد')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex justify-end mt-5'>
|
||||
<Button className='w-fit' label='ساخت تنوع جدید' onClick={() => setShowModal(true)} />
|
||||
|
||||
<DefaulModal
|
||||
open={showModal}
|
||||
close={() => setShowModal(false)}
|
||||
isHeader={true}
|
||||
title_header='ساخت تنوع جدید'
|
||||
width={600}
|
||||
>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ setFieldValue }) => (
|
||||
<Form className="space-y-4">
|
||||
{/* انتخاب variant */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 mb-1 block">
|
||||
{getVariantLabel()}
|
||||
</label>
|
||||
<Field name="variantId">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Select
|
||||
{...field}
|
||||
placeholder={`انتخاب ${getVariantLabel().toLowerCase()}`}
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFieldValue('variantId', e.target.value)}
|
||||
items={getVariantOptions()}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="variantId" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
{/* اطلاعات پایه */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium text-gray-700 mb-1 block">
|
||||
گارانتی
|
||||
</label>
|
||||
<Field name="warrantyId">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Select
|
||||
{...field}
|
||||
placeholder="انتخاب گارانتی"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFieldValue('warrantyId', e.target.value)}
|
||||
items={getWarrantyOptions()}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="warrantyId" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="order_limit">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="محدودیت سفارش"
|
||||
placeholder="مثال: 10"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('order_limit', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="order_limit" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="sellerSpecialCode">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="کد ویژه فروشنده"
|
||||
placeholder="کد ویژه فروشنده را وارد کنید"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('sellerSpecialCode', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="sellerSpecialCode" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="retail_price">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="قیمت فروش (تومان)"
|
||||
placeholder="مثال: 100000"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('retail_price', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="retail_price" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
{/* اطلاعات بستهبندی */}
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-medium mb-4">اطلاعات بستهبندی</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Field name="package_weight">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="وزن بسته (کیلوگرم)"
|
||||
placeholder="مثال: 1.5"
|
||||
type="number"
|
||||
step="0.1"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('package_weight', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="package_weight" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="package_height">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="ارتفاع بسته (سانتی متر)"
|
||||
placeholder="مثال: 20"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('package_height', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="package_height" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="package_length">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="طول بسته (سانتی متر)"
|
||||
placeholder="مثال: 30"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('package_length', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="package_length" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="package_width">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="عرض بسته (سانتی متر)"
|
||||
placeholder="مثال: 15"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('package_width', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="package_width" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* اطلاعات انبارداری */}
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-medium mb-4">اطلاعات انبارداری</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Field name="postingTime">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="زمان ارسال (روز)"
|
||||
placeholder="مثال: 2"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('postingTime', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="postingTime" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="stock">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="موجودی انبار"
|
||||
placeholder="مثال: 50"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('stock', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="stock" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* دکمهها */}
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
disabled={createProductVariant.isPending}
|
||||
>
|
||||
{createProductVariant.isPending ? 'در حال ایجاد...' : 'ایجاد تنوع'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</DefaulModal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreateVariant
|
||||
@@ -0,0 +1,322 @@
|
||||
import Button from '@/components/Button'
|
||||
import DefaulModal from '@/components/DefaulModal'
|
||||
import Input from '@/components/Input'
|
||||
import Select from '@/components/Select'
|
||||
import DatePickerComponent from '@/components/DatePicker'
|
||||
import { useState, type FC, useMemo } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useGetVariantById, useUpdateVariant } from '../hooks/useProductData'
|
||||
import type { ProductVariantDetailType, UpdateVariantRequestType } from '../types/Types'
|
||||
import { toast } from 'react-toastify'
|
||||
import { extractErrorMessage } from '@/helpers/utils'
|
||||
import { Formik, Form, Field, ErrorMessage } from 'formik'
|
||||
import type { FieldInputProps } from 'formik'
|
||||
import * as Yup from 'yup'
|
||||
|
||||
interface FormikFieldProps {
|
||||
field: FieldInputProps<string | number>
|
||||
form: { values: { discount_type: string } }
|
||||
}
|
||||
|
||||
interface UpdateVariantProps {
|
||||
variant: ProductVariantDetailType
|
||||
onUpdate?: () => void
|
||||
}
|
||||
|
||||
const UpdateVariant: FC<UpdateVariantProps> = ({ variant, onUpdate }) => {
|
||||
|
||||
const { id } = useParams()
|
||||
const { data: variantData, isLoading, error } = useGetVariantById(id!, variant._id)
|
||||
|
||||
const updateVariant = useUpdateVariant()
|
||||
|
||||
const [showModal, setShowModal] = useState<boolean>(false)
|
||||
|
||||
const discountTypeOptions = [
|
||||
{ value: 'percent', label: 'درصدی (%)' },
|
||||
{ value: 'fixed', label: 'مبلغ ثابت (تومان)' }
|
||||
]
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
retailPrice: Yup.number().required('قیمت فروش الزامی است').min(0, 'قیمت نمیتواند منفی باشد'),
|
||||
order_limit: Yup.number().required('محدودیت سفارش الزامی است').min(1, 'حداقل ۱ عدد'),
|
||||
sellerSpecialCode: Yup.string().required('کد ویژه فروشنده الزامی است'),
|
||||
stock: Yup.number().required('موجودی انبار الزامی است').min(0, 'موجودی نمیتواند منفی باشد'),
|
||||
discount_type: Yup.string().oneOf(['fixed', 'percent'], 'نوع تخفیف باید fixed یا percent باشد').required('نوع تخفیف الزامی است'),
|
||||
discount_value: Yup.number().when('discount_type', {
|
||||
is: 'fixed',
|
||||
then: (schema) => schema.min(0, 'مقدار تخفیف نمیتواند منفی باشد'),
|
||||
otherwise: (schema) => schema.min(0, 'تخفیف نمیتواند منفی باشد').max(100, 'تخفیف نمیتواند بیش از ۱۰۰٪ باشد')
|
||||
}),
|
||||
specialSale_order_limit: Yup.number().min(0, 'محدودیت فروش ویژه نمیتواند منفی باشد'),
|
||||
specialSale_quantity: Yup.number().min(0, 'تعداد فروش ویژه نمیتواند منفی باشد'),
|
||||
specialSale_endDate: Yup.string()
|
||||
})
|
||||
|
||||
const initialValues = useMemo(() => ({
|
||||
retailPrice: variantData?.results?.productVariant?.price?.retailPrice?.toString() || '',
|
||||
order_limit: variantData?.results?.productVariant?.price?.order_limit?.toString() || '',
|
||||
sellerSpecialCode: variantData?.results?.productVariant?.sellerSpecialCode,
|
||||
stock: variantData?.results?.productVariant?.stock?.toString() || '',
|
||||
discount_type: 'percent',
|
||||
discount_value: variantData?.results?.productVariant?.price?.discount_percent?.toString() || '0',
|
||||
specialSale_order_limit: variantData?.results?.productVariant?.price?.specialSale_order_limit?.toString() || '',
|
||||
specialSale_quantity: variantData?.results?.productVariant?.price?.specialSale_quantity?.toString() || '',
|
||||
specialSale_endDate: variantData?.results?.productVariant?.price?.specialSale_endDate || ''
|
||||
}), [variantData])
|
||||
|
||||
const handleSubmit = (values: typeof initialValues) => {
|
||||
const discountValue = parseFloat(values.discount_value as string) || 0
|
||||
|
||||
const baseData = {
|
||||
productId: id!,
|
||||
variantId: variant._id,
|
||||
retailPrice: parseFloat(values.retailPrice as string),
|
||||
order_limit: parseInt(values.order_limit as string),
|
||||
sellerSpecialCode: values.sellerSpecialCode!,
|
||||
stock: parseInt(values.stock as string),
|
||||
saleFormat: { wholeSale: [] }, // فعلاً خالی میگذاریم
|
||||
discount_type: values.discount_type as "fixed" | "percent",
|
||||
...(values.discount_type === 'fixed' ? {
|
||||
discount_value: discountValue
|
||||
} : {
|
||||
discount_percent: discountValue
|
||||
}),
|
||||
specialSale_order_limit: parseInt(values.specialSale_order_limit as string) || 0,
|
||||
specialSale_quantity: parseInt(values.specialSale_quantity as string) || 0,
|
||||
specialSale_endDate: values.specialSale_endDate || ''
|
||||
} as UpdateVariantRequestType
|
||||
|
||||
updateVariant.mutate(baseData, {
|
||||
onSuccess: () => {
|
||||
setShowModal(false)
|
||||
toast.success('تنوع با موفقیت بروزرسانی شد')
|
||||
onUpdate?.()
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(extractErrorMessage(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="text-center py-4">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto"></div>
|
||||
<p className="mt-2 text-sm text-gray-600">در حال بارگذاری...</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-4">
|
||||
<p className="text-red-500 text-sm">خطا در بارگذاری اطلاعات تنوع</p>
|
||||
<p className="text-gray-500 text-xs mt-1">{extractErrorMessage(error)}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className='text-xs px-3 py-1'
|
||||
variant="outline"
|
||||
label='ویرایش'
|
||||
onClick={() => setShowModal(true)}
|
||||
disabled={!variantData?.results?.productVariant}
|
||||
/>
|
||||
|
||||
{variantData?.results?.productVariant && (
|
||||
<DefaulModal
|
||||
open={showModal}
|
||||
close={() => setShowModal(false)}
|
||||
isHeader={true}
|
||||
title_header='ویرایش تنوع'
|
||||
width={600}
|
||||
>
|
||||
<Formik
|
||||
key={variantData?.results?.productVariant?._id} // برای force re-render وقتی variant تغییر کند
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ setFieldValue }) => (
|
||||
<Form className="space-y-4">
|
||||
{/* اطلاعات قیمت و موجودی */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Field name="retailPrice">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="قیمت فروش (تومان)"
|
||||
placeholder="مثال: 100000"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('retailPrice', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="retailPrice" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="order_limit">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="محدودیت سفارش"
|
||||
placeholder="مثال: 10"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('order_limit', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="order_limit" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="sellerSpecialCode">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="کد ویژه فروشنده"
|
||||
placeholder="کد ویژه فروشنده را وارد کنید"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('sellerSpecialCode', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="sellerSpecialCode" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Field name="stock">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="موجودی انبار"
|
||||
placeholder="مثال: 50"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('stock', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="stock" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="discount_type">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Select
|
||||
{...field}
|
||||
label="نوع تخفیف"
|
||||
placeholder="انتخاب کنید"
|
||||
items={discountTypeOptions}
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setFieldValue('discount_type', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="discount_type" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="discount_value">
|
||||
{({ field, form }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label={form.values.discount_type === 'percent' ? 'درصد تخفیف (%)' : 'مقدار تخفیف (تومان)'}
|
||||
placeholder={form.values.discount_type === 'percent' ? 'مثال: 10' : 'مثال: 50000'}
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('discount_value', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="discount_value" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
{/* اطلاعات فروش ویژه */}
|
||||
<div className="border-t pt-4">
|
||||
<h3 className="text-lg font-medium mb-4">فروش ویژه (اختیاری)</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Field name="specialSale_order_limit">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="محدودیت سفارش فروش ویژه"
|
||||
placeholder="مثال: 5"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('specialSale_order_limit', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="specialSale_order_limit" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Field name="specialSale_quantity">
|
||||
{({ field }: FormikFieldProps) => (
|
||||
<Input
|
||||
{...field}
|
||||
label="تعداد فروش ویژه"
|
||||
placeholder="مثال: 100"
|
||||
type="number"
|
||||
value={field.value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFieldValue('specialSale_quantity', e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<ErrorMessage name="specialSale_quantity" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<DatePickerComponent
|
||||
label="تاریخ پایان فروش ویژه"
|
||||
placeholder="انتخاب تاریخ"
|
||||
defaulValue={initialValues.specialSale_endDate}
|
||||
onChange={(date: string) => setFieldValue('specialSale_endDate', date)}
|
||||
/>
|
||||
<ErrorMessage name="specialSale_endDate" component="div" className="text-red-500 text-sm mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* دکمهها */}
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => setShowModal(false)}
|
||||
>
|
||||
انصراف
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
className="flex-1"
|
||||
disabled={updateVariant.isPending}
|
||||
>
|
||||
{updateVariant.isPending ? 'در حال بروزرسانی...' : 'بروزرسانی'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</DefaulModal>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateVariant
|
||||
@@ -3,7 +3,8 @@ import * as api from "../service/ProductService";
|
||||
import {
|
||||
type CreateProductDetailRequestType,
|
||||
type CreateProductAttributeRequestType,
|
||||
type SaveProductRequestType
|
||||
type SaveProductRequestType,
|
||||
type UpdateVariantRequestType
|
||||
} from "../types/Types";
|
||||
|
||||
export const useCreateProductDetail = () => {
|
||||
@@ -30,3 +31,39 @@ export const useGetProducts = (page: number = 1, limit: number = 10) => {
|
||||
queryFn: () => api.getProducts(page, limit),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProductVariants = (productId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['product-variants', productId],
|
||||
queryFn: () => api.getProductVariants(productId),
|
||||
enabled: !!productId,
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateProductVariant = () => {
|
||||
return useMutation({
|
||||
mutationFn: api.createProductVariant,
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetProductById = (id: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['product-by-id', id],
|
||||
queryFn: () => api.getProductById(id),
|
||||
enabled: !!id,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateVariant = () => {
|
||||
return useMutation({
|
||||
mutationFn: (variables: UpdateVariantRequestType) => api.updateVariant(variables),
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetVariantById = (productId: string, variantId: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['variant-by-id', productId, variantId],
|
||||
queryFn: () => api.getVariantById(productId, variantId),
|
||||
enabled: !!productId && !!variantId,
|
||||
});
|
||||
};
|
||||
@@ -6,7 +6,12 @@ import {
|
||||
type CreateProductAttributeResponseType,
|
||||
type SaveProductRequestType,
|
||||
type SaveProductResponseType,
|
||||
type GetProductsResponseType
|
||||
type GetProductsResponseType,
|
||||
type GetProductVariantsResponseType,
|
||||
type CreateProductVariantRequestType,
|
||||
type GetProductByIdResponseType,
|
||||
type UpdateVariantRequestType,
|
||||
type GetVariantByIdResponseType,
|
||||
} from "../types/Types";
|
||||
|
||||
export const createProductDetail = async (params: CreateProductDetailRequestType): Promise<CreateProductDetailResponseType> => {
|
||||
@@ -30,3 +35,28 @@ export const getProducts = async (page: number = 1, limit: number = 10): Promise
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getProductVariants = async (productId: string): Promise<GetProductVariantsResponseType> => {
|
||||
const { data } = await axios.get(`/admin/products/${productId}/variants`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createProductVariant = async (params: CreateProductVariantRequestType)=> {
|
||||
const { data } = await axios.post(`/admin/products/${params.productId}/variants`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getProductById = async (id: string): Promise<GetProductByIdResponseType> => {
|
||||
const { data } = await axios.get(`/admin/products/${id}/details`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateVariant = async (params: UpdateVariantRequestType) => {
|
||||
const { data } = await axios.patch(`/admin/products/${params.productId}/variants`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getVariantById = async (productId: string, variantId: string): Promise<GetVariantByIdResponseType> => {
|
||||
const { data } = await axios.get(`/admin/products/${productId}/variants/${variantId}`);
|
||||
return data;
|
||||
};
|
||||
@@ -197,3 +197,178 @@ export type GetProductsResponseType = {
|
||||
categories: CategoryListType[];
|
||||
};
|
||||
};
|
||||
|
||||
// تایپهای مربوط به variant های محصول
|
||||
export type ShipmentMethodType = {
|
||||
_id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
deliveryTime: number;
|
||||
deliveryType: string;
|
||||
};
|
||||
|
||||
export type SizeType = {
|
||||
_id: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type ProductVariantDetailType = {
|
||||
_id: string;
|
||||
product_id: number;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
model: string;
|
||||
market_status: string;
|
||||
productCode: string;
|
||||
category_title_fa: string;
|
||||
is_variant_active: boolean;
|
||||
coverImage: string;
|
||||
price: ProductPriceType;
|
||||
stock: number;
|
||||
sellerSpecialCode?: string;
|
||||
shipmentMethod: ShipmentMethodType[];
|
||||
product_status: string;
|
||||
isFreeShip: boolean;
|
||||
isWholeSale: boolean;
|
||||
size: SizeType;
|
||||
};
|
||||
|
||||
export type GetProductVariantsResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
count: number;
|
||||
productVariants: ProductVariantDetailType[];
|
||||
};
|
||||
};
|
||||
|
||||
export type GetVariantByIdResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
productVariant: ProductVariantDetailType;
|
||||
};
|
||||
};
|
||||
|
||||
export type CreateProductVariantRequestType = {
|
||||
productId: string;
|
||||
colorId?: number;
|
||||
sizeId?: number;
|
||||
meterageId?: number;
|
||||
warrantyId: number;
|
||||
order_limit: number;
|
||||
sellerSpecialCode: string;
|
||||
retail_price: number;
|
||||
package_weight: number;
|
||||
package_height: number;
|
||||
package_length: number;
|
||||
package_width: number;
|
||||
postingTime: number;
|
||||
stock: number;
|
||||
};
|
||||
|
||||
// تایپهای مربوط به جزئیات محصول
|
||||
export type ProductSpecificationType = {
|
||||
title: string;
|
||||
values: string[];
|
||||
};
|
||||
|
||||
export type ProductCategoryType = {
|
||||
_id: string;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
icon: string;
|
||||
imageUrl: string;
|
||||
description: string;
|
||||
variants: number[];
|
||||
hierarchy: string[];
|
||||
leaf: boolean;
|
||||
parent: string;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
theme: string;
|
||||
url: string;
|
||||
children: ProductCategoryType[];
|
||||
};
|
||||
|
||||
export type ProductBrandType = {
|
||||
_id: string;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
category: string | null;
|
||||
status: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
images: string[];
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type ProductDetailsType = {
|
||||
_id: number;
|
||||
title_fa: string;
|
||||
title_en: string;
|
||||
seoTitle: string | null;
|
||||
seoDescription: string | null;
|
||||
model: string;
|
||||
source: string;
|
||||
description: string;
|
||||
metaDescription: string;
|
||||
tags: string[];
|
||||
advantages: string[];
|
||||
disAdvantages: string[];
|
||||
totalRate: number;
|
||||
category: ProductCategoryType;
|
||||
shop: string;
|
||||
brand: ProductBrandType;
|
||||
status: string;
|
||||
adminComments: string | null;
|
||||
step: number;
|
||||
isFake: boolean;
|
||||
variants: string[];
|
||||
voice: string | null;
|
||||
deleted: boolean;
|
||||
specifications: ProductSpecificationType[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
imagesUrl: ProductImagesType;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export type GetProductByIdResponseType = {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: {
|
||||
product: ProductDetailsType;
|
||||
};
|
||||
};
|
||||
|
||||
// تایپهای مربوط به آپدیت variant
|
||||
export type WholeSaleItemType = {
|
||||
from: number;
|
||||
to: number;
|
||||
price: number;
|
||||
};
|
||||
|
||||
export type SaleFormatType = {
|
||||
wholeSale: WholeSaleItemType[];
|
||||
};
|
||||
|
||||
export type UpdateVariantRequestType = {
|
||||
productId: string;
|
||||
variantId: string;
|
||||
retailPrice: number;
|
||||
order_limit: number;
|
||||
sellerSpecialCode: string;
|
||||
stock: number;
|
||||
saleFormat: SaleFormatType;
|
||||
discount_type: "fixed" | "percent";
|
||||
discount_value?: number;
|
||||
discount_percent?: number;
|
||||
specialSale_order_limit: number;
|
||||
specialSale_quantity: number;
|
||||
specialSale_endDate: string;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import * as api from "../service/WarrantyService";
|
||||
import type { WarrantyResponse } from "../types/Types";
|
||||
|
||||
export const useGetWarranties = () => {
|
||||
return useQuery<WarrantyResponse>({
|
||||
queryKey: ["warranties"],
|
||||
queryFn: () => api.getWarranties(),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import axios from "../../../config/axios";
|
||||
import type { GetWarrantiesParams, WarrantyResponse } from "../types/Types";
|
||||
|
||||
export const getWarranties = async (
|
||||
params?: GetWarrantiesParams
|
||||
): Promise<WarrantyResponse> => {
|
||||
const { data } = await axios.get("/warranty", { params });
|
||||
return data;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface Warranty {
|
||||
_id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
duration: string;
|
||||
logoUrl: string;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Pager {
|
||||
page: number;
|
||||
limit: number;
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
prevPage: boolean;
|
||||
nextPage: boolean;
|
||||
}
|
||||
|
||||
export interface WarrantyResults {
|
||||
warranties: Warranty[];
|
||||
pager: Pager;
|
||||
}
|
||||
|
||||
export interface WarrantyResponse {
|
||||
status: number;
|
||||
success: boolean;
|
||||
results: WarrantyResults;
|
||||
}
|
||||
|
||||
export interface GetWarrantiesParams {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import Variant from '../pages/category/Variant'
|
||||
import Attributes from '../pages/category/Attributes'
|
||||
import CreateProduct from '../pages/products/Create'
|
||||
import ListProduct from '../pages/products/List'
|
||||
import ProductVariant from '@/pages/products/ProductVariant'
|
||||
|
||||
const MainRouter: FC = () => {
|
||||
|
||||
@@ -38,6 +39,7 @@ const MainRouter: FC = () => {
|
||||
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
||||
<Route path={Pages.products.create} element={<CreateProduct />} />
|
||||
<Route path={Pages.products.list} element={<ListProduct />} />
|
||||
<Route path={`${Pages.products.variants}:id`} element={<ProductVariant />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -62,7 +62,7 @@ const SideBar: FC = () => {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
openSidebar && <div className='fixed top-0 left-0 right-0 bottom-0 bg-black bg-opacity-50 z-10' onClick={() => setOpenSidebar(false)} />
|
||||
openSidebar && <div className='fixed top-0 left-0 right-0 bottom-0 bg-black/50 bg-opacity-50 z-10' onClick={() => setOpenSidebar(false)} />
|
||||
}
|
||||
<div
|
||||
className={clx(
|
||||
|
||||
Reference in New Issue
Block a user