Add variable pricing and purchase options to product forms.
Support fixed vs variable pricing, payment type, purchase units, and decimal quantity inputs in create/update flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+111
-17
@@ -14,21 +14,30 @@ type Props = {
|
|||||||
onEnter?: () => void;
|
onEnter?: () => void;
|
||||||
unit?: string;
|
unit?: string;
|
||||||
seprator?: boolean;
|
seprator?: boolean;
|
||||||
|
allowDecimal?: boolean;
|
||||||
isNotRequired?: boolean;
|
isNotRequired?: boolean;
|
||||||
onChangeSearchFinal?: (value: string) => void;
|
onChangeSearchFinal?: (value: string) => void;
|
||||||
} & InputHTMLAttributes<HTMLInputElement>
|
} & InputHTMLAttributes<HTMLInputElement>
|
||||||
|
|
||||||
const formatNumber = (value: string | number): string => {
|
const formatNumber = (value: string | number): string => {
|
||||||
if (!value) return '';
|
if (value === '' || value === null || value === undefined) return '';
|
||||||
const inputValue = String(value).replace(/,/g, '');
|
const inputValue = String(value).replace(/,/g, '');
|
||||||
return inputValue.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
if (inputValue === '') return '';
|
||||||
|
return inputValue.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DECIMAL_PATTERN = /^\d*\.?\d*$/
|
||||||
|
|
||||||
const Input: FC<Props> = (props: Props) => {
|
const Input: FC<Props> = (props: Props) => {
|
||||||
|
const { allowDecimal, seprator, unit, variant, error_text, onChangeSearchFinal, ...inputProps } = props
|
||||||
|
|
||||||
const [formattedValue, setFormattedValue] = useState<string>(
|
const [formattedValue, setFormattedValue] = useState<string>(
|
||||||
props.value ? formatNumber(props.value as string) : ''
|
props.value !== undefined && props.value !== null && props.value !== '' ? formatNumber(props.value as string) : ''
|
||||||
);
|
);
|
||||||
|
const [decimalValue, setDecimalValue] = useState<string>(
|
||||||
|
props.value !== undefined && props.value !== null && props.value !== '' ? String(props.value) : ''
|
||||||
|
)
|
||||||
|
const [isDecimalFocused, setIsDecimalFocused] = useState(false)
|
||||||
|
|
||||||
const [showPassword, setShowPassword] = useState<boolean>(false)
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
@@ -41,7 +50,23 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (props.seprator) {
|
if (allowDecimal) {
|
||||||
|
const raw = event.target.value
|
||||||
|
if (raw !== '' && !DECIMAL_PATTERN.test(raw)) return
|
||||||
|
|
||||||
|
setDecimalValue(raw)
|
||||||
|
props.onChange?.({
|
||||||
|
...event,
|
||||||
|
target: {
|
||||||
|
...event.target,
|
||||||
|
name: event.target.name,
|
||||||
|
value: raw,
|
||||||
|
},
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seprator) {
|
||||||
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
|
const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها
|
||||||
const formatted = formatNumber(inputValue);
|
const formatted = formatNumber(inputValue);
|
||||||
|
|
||||||
@@ -65,22 +90,54 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.value) {
|
if (allowDecimal) {
|
||||||
|
if (!isDecimalFocused) {
|
||||||
|
if (props.value !== undefined && props.value !== null && props.value !== '') {
|
||||||
|
setDecimalValue(String(props.value))
|
||||||
|
} else {
|
||||||
|
setDecimalValue('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.value !== undefined && props.value !== null && props.value !== '') {
|
||||||
setFormattedValue(formatNumber(props.value as string));
|
setFormattedValue(formatNumber(props.value as string));
|
||||||
} else {
|
} else {
|
||||||
setFormattedValue('');
|
setFormattedValue('');
|
||||||
}
|
}
|
||||||
}, [props.value])
|
}, [props.value, allowDecimal, isDecimalFocused])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.variant === 'search' && props.onChangeSearchFinal) {
|
if (variant === 'search' && onChangeSearchFinal) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
props.onChangeSearchFinal?.(search)
|
onChangeSearchFinal?.(search)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return () => clearTimeout(timeout)
|
return () => clearTimeout(timeout)
|
||||||
}
|
}
|
||||||
}, [search, props])
|
}, [search, variant, onChangeSearchFinal])
|
||||||
|
|
||||||
|
const inputType = allowDecimal
|
||||||
|
? 'text'
|
||||||
|
: seprator
|
||||||
|
? 'text'
|
||||||
|
: inputProps.type === 'password' && showPassword
|
||||||
|
? 'text'
|
||||||
|
: inputProps.type === 'password'
|
||||||
|
? 'password'
|
||||||
|
: inputProps.type
|
||||||
|
|
||||||
|
const inputMode = allowDecimal
|
||||||
|
? 'decimal'
|
||||||
|
: seprator
|
||||||
|
? 'numeric'
|
||||||
|
: inputProps.inputMode
|
||||||
|
|
||||||
|
const inputValue = allowDecimal
|
||||||
|
? decimalValue
|
||||||
|
: seprator
|
||||||
|
? formattedValue
|
||||||
|
: inputProps.value
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
@@ -89,25 +146,62 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className='w-full relative mt-1'>
|
<div className='w-full relative mt-1'>
|
||||||
<input {...props} onChange={(e) => {
|
<input
|
||||||
setSearch(e.target.value)
|
{...inputProps}
|
||||||
handleInputChange(e)
|
type={inputType}
|
||||||
}} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} />
|
inputMode={inputMode}
|
||||||
|
onFocus={(e) => {
|
||||||
|
if (allowDecimal) setIsDecimalFocused(true)
|
||||||
|
inputProps.onFocus?.(e)
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (allowDecimal) {
|
||||||
|
setIsDecimalFocused(false)
|
||||||
|
let normalized = decimalValue
|
||||||
|
if (normalized === '.' || normalized.endsWith('.')) {
|
||||||
|
normalized = normalized.replace(/\.$/, '')
|
||||||
|
setDecimalValue(normalized)
|
||||||
|
}
|
||||||
|
props.onChange?.({
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
name: e.target.name,
|
||||||
|
value: normalized,
|
||||||
|
},
|
||||||
|
} as React.ChangeEvent<HTMLInputElement>)
|
||||||
|
}
|
||||||
|
inputProps.onBlur?.(e)
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearch(e.target.value)
|
||||||
|
handleInputChange(e)
|
||||||
|
}}
|
||||||
|
value={inputValue}
|
||||||
|
className={clx(inputClass, unit && 'pl-14')}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
props.type === 'password' &&
|
unit &&
|
||||||
|
<span className='absolute top-0 bottom-0 left-3 my-auto text-xs text-gray-500 flex items-center pointer-events-none'>
|
||||||
|
{unit}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
inputProps.type === 'password' &&
|
||||||
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
<img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.variant === 'search' &&
|
variant === 'search' &&
|
||||||
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
<SearchNormal size={20} color='#8C90A3' className='absolute top-0 w-5 bottom-0 my-auto right-3' />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
props.error_text &&
|
error_text &&
|
||||||
<Error
|
<Error
|
||||||
errorText={props.error_text}
|
errorText={error_text}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useCreateProduct, useGetCategories } from './hooks/useProductData'
|
|||||||
import { useMultipleUpload } from '../uploader/hooks/useUploaderData'
|
import { useMultipleUpload } from '../uploader/hooks/useUploaderData'
|
||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import type { ErrorType } from '@/helpers/types'
|
import type { ErrorType } from '@/helpers/types'
|
||||||
|
import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +40,13 @@ const CreateProduct: FC = () => {
|
|||||||
isSpecialOffer: false,
|
isSpecialOffer: false,
|
||||||
order: 0,
|
order: 0,
|
||||||
stock: undefined,
|
stock: undefined,
|
||||||
maxPurchaseQuantity: undefined
|
maxPurchaseQuantity: undefined,
|
||||||
|
minPurchaseQuantity: undefined,
|
||||||
|
purchasePitch: undefined,
|
||||||
|
pricingType: PricingType.FIXED,
|
||||||
|
paymentType: PaymentType.INSTANT,
|
||||||
|
purchaseUnit: PurchaseUnit.NUMBER,
|
||||||
|
needAdminAcceptance: false,
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object().shape({
|
validationSchema: Yup.object().shape({
|
||||||
title: Yup.string().required('نام کالا الزامی است'),
|
title: Yup.string().required('نام کالا الزامی است'),
|
||||||
@@ -47,6 +54,17 @@ const CreateProduct: FC = () => {
|
|||||||
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
||||||
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'),
|
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'),
|
||||||
attribute: Yup.string(),
|
attribute: Yup.string(),
|
||||||
|
pricingType: Yup.string().required(),
|
||||||
|
paymentType: Yup.string().required(),
|
||||||
|
minPurchaseQuantity: Yup.number().when('pricingType', {
|
||||||
|
is: PricingType.VARIABLE,
|
||||||
|
then: (schema) => schema.min(0, 'حداقل مقدار باید مثبت باشد'),
|
||||||
|
}),
|
||||||
|
maxPurchaseQuantity: Yup.number().min(0, 'حداکثر مقدار باید مثبت باشد'),
|
||||||
|
purchasePitch: Yup.number().when('pricingType', {
|
||||||
|
is: PricingType.VARIABLE,
|
||||||
|
then: (schema) => schema.min(0, 'گام تغییر باید مثبت باشد'),
|
||||||
|
}),
|
||||||
variants: Yup.array().of(
|
variants: Yup.array().of(
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
id: Yup.string(),
|
id: Yup.string(),
|
||||||
@@ -66,7 +84,13 @@ const CreateProduct: FC = () => {
|
|||||||
variants,
|
variants,
|
||||||
isActive,
|
isActive,
|
||||||
isSpecialOffer: isSpecial,
|
isSpecialOffer: isSpecial,
|
||||||
images: imageUrls
|
images: imageUrls,
|
||||||
|
needAdminAcceptance: values.paymentType === PaymentType.AFTER_PREPARATION,
|
||||||
|
...(values.pricingType === PricingType.FIXED && {
|
||||||
|
purchaseUnit: values.purchaseUnit ?? PurchaseUnit.NUMBER,
|
||||||
|
minPurchaseQuantity: values.minPurchaseQuantity ?? 1,
|
||||||
|
purchasePitch: undefined,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
createProduct(productData, {
|
createProduct(productData, {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { useMultipleUpload } from '../uploader/hooks/useUploaderData'
|
|||||||
import { Pages } from '@/config/Pages'
|
import { Pages } from '@/config/Pages'
|
||||||
import type { ErrorType } from '@/helpers/types'
|
import type { ErrorType } from '@/helpers/types'
|
||||||
import { extractErrorMessage } from '@/config/func'
|
import { extractErrorMessage } from '@/config/func'
|
||||||
|
import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum'
|
||||||
|
|
||||||
const UpdateProduct: FC = () => {
|
const UpdateProduct: FC = () => {
|
||||||
const { id } = useParams<{ id: string }>()
|
const { id } = useParams<{ id: string }>()
|
||||||
@@ -45,7 +46,13 @@ const UpdateProduct: FC = () => {
|
|||||||
isSpecialOffer: false,
|
isSpecialOffer: false,
|
||||||
order: 0,
|
order: 0,
|
||||||
stock: undefined,
|
stock: undefined,
|
||||||
maxPurchaseQuantity: undefined
|
maxPurchaseQuantity: undefined,
|
||||||
|
minPurchaseQuantity: undefined,
|
||||||
|
purchasePitch: undefined,
|
||||||
|
pricingType: PricingType.FIXED,
|
||||||
|
paymentType: PaymentType.INSTANT,
|
||||||
|
purchaseUnit: PurchaseUnit.NUMBER,
|
||||||
|
needAdminAcceptance: false,
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object().shape({
|
validationSchema: Yup.object().shape({
|
||||||
title: Yup.string().required('نام کالا الزامی است'),
|
title: Yup.string().required('نام کالا الزامی است'),
|
||||||
@@ -53,6 +60,17 @@ const UpdateProduct: FC = () => {
|
|||||||
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'),
|
||||||
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'),
|
discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'),
|
||||||
attribute: Yup.string(),
|
attribute: Yup.string(),
|
||||||
|
pricingType: Yup.string().required(),
|
||||||
|
paymentType: Yup.string().required(),
|
||||||
|
minPurchaseQuantity: Yup.number().when('pricingType', {
|
||||||
|
is: PricingType.VARIABLE,
|
||||||
|
then: (schema) => schema.min(0, 'حداقل مقدار باید مثبت باشد'),
|
||||||
|
}),
|
||||||
|
maxPurchaseQuantity: Yup.number().min(0, 'حداکثر مقدار باید مثبت باشد'),
|
||||||
|
purchasePitch: Yup.number().when('pricingType', {
|
||||||
|
is: PricingType.VARIABLE,
|
||||||
|
then: (schema) => schema.min(0, 'گام تغییر باید مثبت باشد'),
|
||||||
|
}),
|
||||||
variants: Yup.array().of(
|
variants: Yup.array().of(
|
||||||
Yup.object().shape({
|
Yup.object().shape({
|
||||||
id: Yup.string(),
|
id: Yup.string(),
|
||||||
@@ -83,7 +101,13 @@ const UpdateProduct: FC = () => {
|
|||||||
variants,
|
variants,
|
||||||
isActive,
|
isActive,
|
||||||
isSpecialOffer: isSpecial,
|
isSpecialOffer: isSpecial,
|
||||||
images: allImages
|
images: allImages,
|
||||||
|
needAdminAcceptance: values.paymentType === PaymentType.AFTER_PREPARATION,
|
||||||
|
...(values.pricingType === PricingType.FIXED && {
|
||||||
|
purchaseUnit: values.purchaseUnit ?? PurchaseUnit.NUMBER,
|
||||||
|
minPurchaseQuantity: values.minPurchaseQuantity ?? 1,
|
||||||
|
purchasePitch: undefined,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
updateProduct({ id, params: productData }, {
|
updateProduct({ id, params: productData }, {
|
||||||
@@ -132,7 +156,13 @@ const UpdateProduct: FC = () => {
|
|||||||
isSpecialOffer: product.isSpecialOffer || false,
|
isSpecialOffer: product.isSpecialOffer || false,
|
||||||
order: product.order ?? 0,
|
order: product.order ?? 0,
|
||||||
stock: product.variants?.[0]?.stock,
|
stock: product.variants?.[0]?.stock,
|
||||||
maxPurchaseQuantity: product.maxPurchaseQuantity
|
maxPurchaseQuantity: product.maxPurchaseQuantity,
|
||||||
|
minPurchaseQuantity: product.minPurchaseQuantity,
|
||||||
|
purchasePitch: product.purchasePitch,
|
||||||
|
pricingType: product.pricingType ?? PricingType.FIXED,
|
||||||
|
paymentType: product.paymentType ?? (product.needAdminAcceptance ? PaymentType.AFTER_PREPARATION : PaymentType.INSTANT),
|
||||||
|
purchaseUnit: product.purchaseUnit ?? PurchaseUnit.NUMBER,
|
||||||
|
needAdminAcceptance: product.needAdminAcceptance ?? false,
|
||||||
})
|
})
|
||||||
setIsActive(product.isActive || false)
|
setIsActive(product.isActive || false)
|
||||||
setIsSpecial(product.isSpecialOffer || false)
|
setIsSpecial(product.isSpecialOffer || false)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import Textarea from '@/components/Textarea'
|
|||||||
import Button from '@/components/Button'
|
import Button from '@/components/Button'
|
||||||
import RadioGroup from '@/components/RadioGroup'
|
import RadioGroup from '@/components/RadioGroup'
|
||||||
import type { CreateProductType, ProductVariant, Category } from '../types/Types'
|
import type { CreateProductType, ProductVariant, Category } from '../types/Types'
|
||||||
|
import { PaymentType, PricingType, PurchaseUnit, PURCHASE_UNIT_LABELS } from '../enum/Enum'
|
||||||
import { Add, Trash } from 'iconsax-react'
|
import { Add, Trash } from 'iconsax-react'
|
||||||
|
|
||||||
type BasicInfoSectionProps = {
|
type BasicInfoSectionProps = {
|
||||||
@@ -17,11 +18,20 @@ type BasicInfoSectionProps = {
|
|||||||
|
|
||||||
const newVariant = (): ProductVariant => ({ value: '', price: 0 })
|
const newVariant = (): ProductVariant => ({ value: '', price: 0 })
|
||||||
|
|
||||||
|
const purchaseUnitOptions = Object.values(PurchaseUnit).map((unit) => ({
|
||||||
|
value: unit,
|
||||||
|
label: PURCHASE_UNIT_LABELS[unit],
|
||||||
|
}))
|
||||||
|
|
||||||
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initialHasVariants }) => {
|
const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initialHasVariants }) => {
|
||||||
const [hasVariants, setHasVariants] = useState<boolean>(initialHasVariants ?? false)
|
const [hasVariants, setHasVariants] = useState<boolean>(initialHasVariants ?? false)
|
||||||
/** دسته اصلی انتخابشده برای نمایش زیردستهها */
|
/** دسته اصلی انتخابشده برای نمایش زیردستهها */
|
||||||
const [parentCategoryId, setParentCategoryId] = useState<string>('')
|
const [parentCategoryId, setParentCategoryId] = useState<string>('')
|
||||||
|
|
||||||
|
const isVariablePricing = formik.values.pricingType === PricingType.VARIABLE
|
||||||
|
const purchaseUnit = formik.values.purchaseUnit ?? PurchaseUnit.NUMBER
|
||||||
|
const unitLabel = PURCHASE_UNIT_LABELS[purchaseUnit]
|
||||||
|
|
||||||
const mainCategories = useMemo(
|
const mainCategories = useMemo(
|
||||||
() => categories.filter((c) => !c.parent).map((c) => ({ label: c.title, value: c.id })),
|
() => categories.filter((c) => !c.parent).map((c) => ({ label: c.title, value: c.id })),
|
||||||
[categories]
|
[categories]
|
||||||
@@ -68,6 +78,22 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePricingTypeChange = (value: string | boolean) => {
|
||||||
|
const next = value as PricingType
|
||||||
|
formik.setFieldValue('pricingType', next)
|
||||||
|
if (next === PricingType.VARIABLE) {
|
||||||
|
if (!formik.values.purchaseUnit) {
|
||||||
|
formik.setFieldValue('purchaseUnit', PurchaseUnit.NUMBER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaymentTypeChange = (value: string | boolean) => {
|
||||||
|
const next = value as PaymentType
|
||||||
|
formik.setFieldValue('paymentType', next)
|
||||||
|
formik.setFieldValue('needAdminAcceptance', next === PaymentType.AFTER_PREPARATION)
|
||||||
|
}
|
||||||
|
|
||||||
const addVariant = () => {
|
const addVariant = () => {
|
||||||
formik.setFieldValue('variants', [...formik.values.variants, newVariant()])
|
formik.setFieldValue('variants', [...formik.values.variants, newVariant()])
|
||||||
}
|
}
|
||||||
@@ -85,6 +111,21 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setOptionalNumberField = (name: keyof CreateProductType, raw: string) => {
|
||||||
|
formik.setFieldValue(name, raw === '' ? undefined : Number(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOptionalDecimalField = (name: keyof CreateProductType, raw: string) => {
|
||||||
|
if (raw === '' || raw === '.') {
|
||||||
|
formik.setFieldValue(name, undefined)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const num = parseFloat(raw)
|
||||||
|
if (!Number.isNaN(num)) {
|
||||||
|
formik.setFieldValue(name, num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
@@ -135,21 +176,20 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
|
|||||||
<div className='mt-1 text-xs text-red-500'>{formik.errors.categoryId}</div>
|
<div className='mt-1 text-xs text-red-500'>{formik.errors.categoryId}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='mt-5'>
|
{!isVariablePricing && (
|
||||||
<Input
|
<div className='mt-5'>
|
||||||
name='maxPurchaseQuantity'
|
<Input
|
||||||
label='حداکثر تعداد خرید'
|
name='maxPurchaseQuantity'
|
||||||
placeholder='خالی = بینهایت'
|
label='حداکثر تعداد خرید'
|
||||||
type='number'
|
placeholder='خالی = بینهایت'
|
||||||
min={0}
|
type='number'
|
||||||
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
|
min={0}
|
||||||
onChange={(e) => {
|
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
|
||||||
const v = e.target.value
|
onChange={(e) => setOptionalNumberField('maxPurchaseQuantity', e.target.value)}
|
||||||
formik.setFieldValue('maxPurchaseQuantity', v === '' ? undefined : Number(v))
|
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
|
||||||
}}
|
/>
|
||||||
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* بخش قیمتگذاری و تنوع */}
|
{/* بخش قیمتگذاری و تنوع */}
|
||||||
<div className='mt-6 p-5 sm:p-6 rounded-2xl border border-gray-200/80 bg-gray-50/50'>
|
<div className='mt-6 p-5 sm:p-6 rounded-2xl border border-gray-200/80 bg-gray-50/50'>
|
||||||
@@ -160,7 +200,7 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
|
|||||||
<label className='block text-sm text-gray-700 mb-2'>کالای شما دارای تنوع است؟</label>
|
<label className='block text-sm text-gray-700 mb-2'>کالای شما دارای تنوع است؟</label>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
items={[
|
items={[
|
||||||
{ label: 'خیر، فقط یک قیمت دارد', value: false },
|
{ label: 'خیر، فقط یک نوع دارد', value: false },
|
||||||
{ label: 'بله، دارای تنوع است', value: true }
|
{ label: 'بله، دارای تنوع است', value: true }
|
||||||
]}
|
]}
|
||||||
selected={hasVariants}
|
selected={hasVariants}
|
||||||
@@ -168,51 +208,161 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-5'>
|
||||||
|
<label className='block text-sm text-gray-700 mb-2'>نوع قیمتگذاری</label>
|
||||||
|
<RadioGroup
|
||||||
|
items={[
|
||||||
|
{ label: 'قیمت ثابت', value: PricingType.FIXED },
|
||||||
|
{ label: 'قیمت بر اساس مقدار', value: PricingType.VARIABLE }
|
||||||
|
]}
|
||||||
|
selected={formik.values.pricingType}
|
||||||
|
onChange={handlePricingTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mb-5'>
|
||||||
|
<label className='block text-sm text-gray-700 mb-2'>نوع پرداخت</label>
|
||||||
|
<RadioGroup
|
||||||
|
items={[
|
||||||
|
{ label: 'پرداخت آنی', value: PaymentType.INSTANT },
|
||||||
|
{ label: 'پرداخت پس از آمادهسازی', value: PaymentType.AFTER_PREPARATION }
|
||||||
|
]}
|
||||||
|
selected={formik.values.paymentType}
|
||||||
|
onChange={handlePaymentTypeChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{!hasVariants ? (
|
{!hasVariants ? (
|
||||||
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
|
isVariablePricing ? (
|
||||||
<Input
|
<div className='space-y-4'>
|
||||||
name='price'
|
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||||||
label='قیمت (تومان)'
|
<Select
|
||||||
placeholder='۰'
|
items={purchaseUnitOptions}
|
||||||
type='number'
|
label='واحد اندازهگیری'
|
||||||
seprator={true}
|
name='purchaseUnit'
|
||||||
value={formik.values.price}
|
value={purchaseUnit}
|
||||||
onChange={(e) => formik.setFieldValue('price', Number(e.target.value))}
|
onChange={(e) => formik.setFieldValue('purchaseUnit', e.target.value as PurchaseUnit)}
|
||||||
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
name='price'
|
||||||
name='stock'
|
label='قیمت واحد (تومان)'
|
||||||
label='موجودی'
|
placeholder='۰'
|
||||||
placeholder='خالی = بینهایت'
|
seprator={true}
|
||||||
type='number'
|
unit='تومان'
|
||||||
min={0}
|
value={formik.values.price}
|
||||||
value={formik.values.stock === undefined || formik.values.stock === null ? '' : formik.values.stock}
|
onChange={(e) => formik.setFieldValue('price', Number(e.target.value))}
|
||||||
onChange={(e) => {
|
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
||||||
const v = e.target.value
|
/>
|
||||||
formik.setFieldValue('stock', v === '' ? undefined : Number(v))
|
</div>
|
||||||
}}
|
|
||||||
error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''}
|
<div className='grid grid-cols-1 sm:grid-cols-3 gap-4'>
|
||||||
/>
|
<Input
|
||||||
<Input
|
name='minPurchaseQuantity'
|
||||||
name='discount'
|
label='حداقل مقدار سفارش'
|
||||||
label='تخفیف (تومان)'
|
placeholder=''
|
||||||
placeholder='۰'
|
allowDecimal
|
||||||
type='number'
|
unit={unitLabel}
|
||||||
seprator={true}
|
value={formik.values.minPurchaseQuantity === undefined || formik.values.minPurchaseQuantity === null ? '' : formik.values.minPurchaseQuantity}
|
||||||
value={formik.values.discount}
|
onChange={(e) => setOptionalDecimalField('minPurchaseQuantity', e.target.value)}
|
||||||
onChange={(e) => formik.setFieldValue('discount', Number(e.target.value))}
|
error_text={formik.touched.minPurchaseQuantity && formik.errors.minPurchaseQuantity ? formik.errors.minPurchaseQuantity : ''}
|
||||||
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''}
|
/>
|
||||||
/>
|
<Input
|
||||||
<Input
|
name='maxPurchaseQuantity'
|
||||||
name='order'
|
label='حداکثر مقدار سفارش'
|
||||||
label='اولویت'
|
placeholder=''
|
||||||
placeholder='عدد'
|
allowDecimal
|
||||||
type='number'
|
unit={unitLabel}
|
||||||
value={formik.values.order}
|
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
|
||||||
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
onChange={(e) => setOptionalDecimalField('maxPurchaseQuantity', e.target.value)}
|
||||||
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Input
|
||||||
|
name='purchasePitch'
|
||||||
|
label='گام تغییر مقدار'
|
||||||
|
placeholder=''
|
||||||
|
allowDecimal
|
||||||
|
unit={unitLabel}
|
||||||
|
value={formik.values.purchasePitch === undefined || formik.values.purchasePitch === null ? '' : formik.values.purchasePitch}
|
||||||
|
onChange={(e) => setOptionalDecimalField('purchasePitch', e.target.value)}
|
||||||
|
error_text={formik.touched.purchasePitch && formik.errors.purchasePitch ? formik.errors.purchasePitch : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-3 gap-4'>
|
||||||
|
<Input
|
||||||
|
name='stock'
|
||||||
|
label='موجودی'
|
||||||
|
placeholder='خالی = بینهایت'
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={formik.values.stock === undefined || formik.values.stock === null ? '' : formik.values.stock}
|
||||||
|
onChange={(e) => setOptionalNumberField('stock', e.target.value)}
|
||||||
|
error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name='discount'
|
||||||
|
label='تخفیف (تومان)'
|
||||||
|
placeholder='۰'
|
||||||
|
type='number'
|
||||||
|
seprator={true}
|
||||||
|
value={formik.values.discount}
|
||||||
|
onChange={(e) => formik.setFieldValue('discount', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name='order'
|
||||||
|
label='اولویت'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
value={formik.values.order}
|
||||||
|
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4'>
|
||||||
|
<Input
|
||||||
|
name='price'
|
||||||
|
label='قیمت (تومان)'
|
||||||
|
placeholder='۰'
|
||||||
|
type='number'
|
||||||
|
seprator={true}
|
||||||
|
value={formik.values.price}
|
||||||
|
onChange={(e) => formik.setFieldValue('price', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name='stock'
|
||||||
|
label='موجودی'
|
||||||
|
placeholder='خالی = بینهایت'
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={formik.values.stock === undefined || formik.values.stock === null ? '' : formik.values.stock}
|
||||||
|
onChange={(e) => setOptionalNumberField('stock', e.target.value)}
|
||||||
|
error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name='discount'
|
||||||
|
label='تخفیف (تومان)'
|
||||||
|
placeholder='۰'
|
||||||
|
type='number'
|
||||||
|
seprator={true}
|
||||||
|
value={formik.values.discount}
|
||||||
|
onChange={(e) => formik.setFieldValue('discount', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name='order'
|
||||||
|
label='اولویت'
|
||||||
|
placeholder='عدد'
|
||||||
|
type='number'
|
||||||
|
value={formik.values.order}
|
||||||
|
onChange={(e) => formik.setFieldValue('order', Number(e.target.value))}
|
||||||
|
error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-5'>
|
<div className='space-y-5'>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export enum PricingType {
|
||||||
|
FIXED = "fixed",
|
||||||
|
VARIABLE = "variable",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PaymentType {
|
||||||
|
INSTANT = "instant",
|
||||||
|
AFTER_PREPARATION = "after_preparation",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PurchaseUnit {
|
||||||
|
NUMBER = "number",
|
||||||
|
KILOGRAM = "kilogram",
|
||||||
|
GRAM = "gram",
|
||||||
|
CENTIMETER = "centimeter",
|
||||||
|
METER = "meter",
|
||||||
|
MILLILITER = "milliliter",
|
||||||
|
LITER = "liter",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PURCHASE_UNIT_LABELS: Record<PurchaseUnit, string> = {
|
||||||
|
[PurchaseUnit.NUMBER]: "عدد",
|
||||||
|
[PurchaseUnit.KILOGRAM]: "کیلوگرم",
|
||||||
|
[PurchaseUnit.GRAM]: "گرم",
|
||||||
|
[PurchaseUnit.CENTIMETER]: "سانتیمتر",
|
||||||
|
[PurchaseUnit.METER]: "متر",
|
||||||
|
[PurchaseUnit.MILLILITER]: "میلیلیتر",
|
||||||
|
[PurchaseUnit.LITER]: "لیتر",
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { IResponse } from "@/types/response.types";
|
import type { IResponse } from "@/types/response.types";
|
||||||
|
import type { PaymentType, PricingType, PurchaseUnit } from "../enum/Enum";
|
||||||
|
|
||||||
export type ProductVariant = {
|
export type ProductVariant = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -22,6 +23,12 @@ export type CreateProductType = {
|
|||||||
/** فقط در حالت بدون تنوع استفاده میشود */
|
/** فقط در حالت بدون تنوع استفاده میشود */
|
||||||
stock?: number;
|
stock?: number;
|
||||||
maxPurchaseQuantity?: number;
|
maxPurchaseQuantity?: number;
|
||||||
|
minPurchaseQuantity?: number;
|
||||||
|
purchasePitch?: number;
|
||||||
|
pricingType: PricingType;
|
||||||
|
paymentType: PaymentType;
|
||||||
|
purchaseUnit?: PurchaseUnit;
|
||||||
|
needAdminAcceptance?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Category = {
|
export type Category = {
|
||||||
@@ -125,6 +132,12 @@ export type Product = {
|
|||||||
discount: number;
|
discount: number;
|
||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
maxPurchaseQuantity?: number;
|
maxPurchaseQuantity?: number;
|
||||||
|
minPurchaseQuantity?: number;
|
||||||
|
purchasePitch?: number;
|
||||||
|
pricingType?: PricingType;
|
||||||
|
paymentType?: PaymentType;
|
||||||
|
purchaseUnit?: PurchaseUnit;
|
||||||
|
needAdminAcceptance?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetProductsParams = {
|
export type GetProductsParams = {
|
||||||
@@ -157,6 +170,12 @@ export type ProductDetails = {
|
|||||||
discount: number;
|
discount: number;
|
||||||
isSpecialOffer: boolean;
|
isSpecialOffer: boolean;
|
||||||
maxPurchaseQuantity?: number;
|
maxPurchaseQuantity?: number;
|
||||||
|
minPurchaseQuantity?: number;
|
||||||
|
purchasePitch?: number;
|
||||||
|
pricingType?: PricingType;
|
||||||
|
paymentType?: PaymentType;
|
||||||
|
purchaseUnit?: PurchaseUnit;
|
||||||
|
needAdminAcceptance?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PaginationMeta = {
|
export type PaginationMeta = {
|
||||||
|
|||||||
Reference in New Issue
Block a user