manage variant producy

This commit is contained in:
hamid zarghami
2025-09-23 15:44:14 +03:30
parent f8ff05357d
commit a71d521c7c
16 changed files with 1186 additions and 15 deletions
+15
View File
@@ -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)}
+136
View File
@@ -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
+38 -1
View File
@@ -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,
});
};
+31 -1
View File
@@ -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;
};
+175
View File
@@ -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;
};
+35
View File
@@ -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;
}