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 from 'react-date-object/calendars/persian';
|
||||||
import persian_fa from 'react-date-object/locales/persian_fa';
|
import persian_fa from 'react-date-object/locales/persian_fa';
|
||||||
import DateObject from 'react-date-object';
|
import DateObject from 'react-date-object';
|
||||||
import CalenderIcon from '../assets/images/calendar.svg'
|
|
||||||
import { clx } from '../helpers/utils';
|
import { clx } from '../helpers/utils';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -14,7 +13,8 @@ type Props = {
|
|||||||
reset?: boolean;
|
reset?: boolean;
|
||||||
isDateTime?: boolean;
|
isDateTime?: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
label?: string
|
label: string,
|
||||||
|
readOnly?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DatePickerComponent: FC<Props> = (props: Props) => {
|
const DatePickerComponent: FC<Props> = (props: Props) => {
|
||||||
@@ -47,13 +47,12 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{props.label &&
|
<div className='text-sm'>
|
||||||
<div className='text-sm'>
|
{props.label}
|
||||||
{props.label}
|
</div>
|
||||||
</div>}
|
|
||||||
<div className={clx(
|
<div className={clx(
|
||||||
'relative ',
|
'relative mt-1.5',
|
||||||
props.label && 'mt-1.5'
|
props.readOnly && 'readOny'
|
||||||
)}>
|
)}>
|
||||||
<DatePicker
|
<DatePicker
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
@@ -62,7 +61,8 @@ const DatePickerComponent: FC<Props> = (props: Props) => {
|
|||||||
calendar={persian}
|
calendar={persian}
|
||||||
locale={persian_fa}
|
locale={persian_fa}
|
||||||
calendarPosition="bottom-right"
|
calendarPosition="bottom-right"
|
||||||
className={`rmdp-mobile ${props.className}`}
|
className={props.className}
|
||||||
|
readOnly={props.readOnly}
|
||||||
/>
|
/>
|
||||||
{props.error_text && props.error_text !== '' && (
|
{props.error_text && props.error_text !== '' && (
|
||||||
<div className="text-xs text-right text-red-600 mt-2 mr-2 font-medium">
|
<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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ const DefaulModal: FC<Props> = (props: Props) => {
|
|||||||
{
|
{
|
||||||
props.open && (
|
props.open && (
|
||||||
<Fragment>
|
<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 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 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'>
|
<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: {
|
products: {
|
||||||
create: "/products/create",
|
create: "/products/create",
|
||||||
list: "/products/list",
|
list: "/products/list",
|
||||||
|
variants: "/products/variants/",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -179,3 +179,20 @@ textarea::placeholder {
|
|||||||
@apply bg-background text-foreground;
|
@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 Td from '../../components/Td';
|
||||||
import TrashWithConfrim from '../../components/TrashWithConfrim';
|
import TrashWithConfrim from '../../components/TrashWithConfrim';
|
||||||
import { Edit, Eye } from 'iconsax-react';
|
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 List: FC = () => {
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
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='تنوع' />
|
||||||
<Td text={'وضعیت'} />
|
<Td text={'وضعیت'} />
|
||||||
<Td text={'قیمت'} />
|
<Td text={'قیمت'} />
|
||||||
<Td text={'موجودی'} />
|
<Td text={'موجودی'} />
|
||||||
@@ -101,6 +105,17 @@ const List: FC = () => {
|
|||||||
</Td>
|
</Td>
|
||||||
<Td text={product.brand_title_fa} />
|
<Td text={product.brand_title_fa} />
|
||||||
<Td text={product.category_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="">
|
<Td text="">
|
||||||
<StatusWithText
|
<StatusWithText
|
||||||
variant={getStatusVariant(product.status)}
|
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 {
|
import {
|
||||||
type CreateProductDetailRequestType,
|
type CreateProductDetailRequestType,
|
||||||
type CreateProductAttributeRequestType,
|
type CreateProductAttributeRequestType,
|
||||||
type SaveProductRequestType
|
type SaveProductRequestType,
|
||||||
|
type UpdateVariantRequestType
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const useCreateProductDetail = () => {
|
export const useCreateProductDetail = () => {
|
||||||
@@ -30,3 +31,39 @@ export const useGetProducts = (page: number = 1, limit: number = 10) => {
|
|||||||
queryFn: () => api.getProducts(page, limit),
|
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 CreateProductAttributeResponseType,
|
||||||
type SaveProductRequestType,
|
type SaveProductRequestType,
|
||||||
type SaveProductResponseType,
|
type SaveProductResponseType,
|
||||||
type GetProductsResponseType
|
type GetProductsResponseType,
|
||||||
|
type GetProductVariantsResponseType,
|
||||||
|
type CreateProductVariantRequestType,
|
||||||
|
type GetProductByIdResponseType,
|
||||||
|
type UpdateVariantRequestType,
|
||||||
|
type GetVariantByIdResponseType,
|
||||||
} from "../types/Types";
|
} from "../types/Types";
|
||||||
|
|
||||||
export const createProductDetail = async (params: CreateProductDetailRequestType): Promise<CreateProductDetailResponseType> => {
|
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;
|
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[];
|
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 Attributes from '../pages/category/Attributes'
|
||||||
import CreateProduct from '../pages/products/Create'
|
import CreateProduct from '../pages/products/Create'
|
||||||
import ListProduct from '../pages/products/List'
|
import ListProduct from '../pages/products/List'
|
||||||
|
import ProductVariant from '@/pages/products/ProductVariant'
|
||||||
|
|
||||||
const MainRouter: FC = () => {
|
const MainRouter: FC = () => {
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ const MainRouter: FC = () => {
|
|||||||
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
<Route path={`${Pages.category.attributes}:id`} element={<Attributes />} />
|
||||||
<Route path={Pages.products.create} element={<CreateProduct />} />
|
<Route path={Pages.products.create} element={<CreateProduct />} />
|
||||||
<Route path={Pages.products.list} element={<ListProduct />} />
|
<Route path={Pages.products.list} element={<ListProduct />} />
|
||||||
|
<Route path={`${Pages.products.variants}:id`} element={<ProductVariant />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const SideBar: FC = () => {
|
|||||||
return (
|
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
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
|
|||||||
Reference in New Issue
Block a user