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:
hamid zarghami
2026-06-28 11:45:36 +03:30
parent a6ff4c6967
commit 136cc1bed9
6 changed files with 428 additions and 82 deletions
+210 -60
View File
@@ -6,6 +6,7 @@ import Textarea from '@/components/Textarea'
import Button from '@/components/Button'
import RadioGroup from '@/components/RadioGroup'
import type { CreateProductType, ProductVariant, Category } from '../types/Types'
import { PaymentType, PricingType, PurchaseUnit, PURCHASE_UNIT_LABELS } from '../enum/Enum'
import { Add, Trash } from 'iconsax-react'
type BasicInfoSectionProps = {
@@ -17,11 +18,20 @@ type BasicInfoSectionProps = {
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 [hasVariants, setHasVariants] = useState<boolean>(initialHasVariants ?? false)
/** دسته اصلی انتخاب‌شده برای نمایش زیردسته‌ها */
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(
() => categories.filter((c) => !c.parent).map((c) => ({ label: c.title, value: c.id })),
[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 = () => {
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 (
<>
<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-5'>
<Input
name='maxPurchaseQuantity'
label='حداکثر تعداد خرید'
placeholder='خالی = بی‌نهایت'
type='number'
min={0}
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
onChange={(e) => {
const v = e.target.value
formik.setFieldValue('maxPurchaseQuantity', v === '' ? undefined : Number(v))
}}
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
/>
</div>
{!isVariablePricing && (
<div className='mt-5'>
<Input
name='maxPurchaseQuantity'
label='حداکثر تعداد خرید'
placeholder='خالی = بی‌نهایت'
type='number'
min={0}
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
onChange={(e) => setOptionalNumberField('maxPurchaseQuantity', e.target.value)}
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
/>
</div>
)}
{/* بخش قیمت‌گذاری و تنوع */}
<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>
<RadioGroup
items={[
{ label: 'خیر، فقط یک قیمت دارد', value: false },
{ label: 'خیر، فقط یک نوع دارد', value: false },
{ label: 'بله، دارای تنوع است', value: true }
]}
selected={hasVariants}
@@ -168,51 +208,161 @@ const BasicInfoSection: FC<BasicInfoSectionProps> = ({ formik, categories, initi
/>
</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 ? (
<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) => {
const v = e.target.value
formik.setFieldValue('stock', v === '' ? undefined : Number(v))
}}
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>
isVariablePricing ? (
<div className='space-y-4'>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<Select
items={purchaseUnitOptions}
label='واحد اندازه‌گیری'
name='purchaseUnit'
value={purchaseUnit}
onChange={(e) => formik.setFieldValue('purchaseUnit', e.target.value as PurchaseUnit)}
/>
<Input
name='price'
label='قیمت واحد (تومان)'
placeholder='۰'
seprator={true}
unit='تومان'
value={formik.values.price}
onChange={(e) => formik.setFieldValue('price', Number(e.target.value))}
error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''}
/>
</div>
<div className='grid grid-cols-1 sm:grid-cols-3 gap-4'>
<Input
name='minPurchaseQuantity'
label='حداقل مقدار سفارش'
placeholder=''
allowDecimal
unit={unitLabel}
value={formik.values.minPurchaseQuantity === undefined || formik.values.minPurchaseQuantity === null ? '' : formik.values.minPurchaseQuantity}
onChange={(e) => setOptionalDecimalField('minPurchaseQuantity', e.target.value)}
error_text={formik.touched.minPurchaseQuantity && formik.errors.minPurchaseQuantity ? formik.errors.minPurchaseQuantity : ''}
/>
<Input
name='maxPurchaseQuantity'
label='حداکثر مقدار سفارش'
placeholder=''
allowDecimal
unit={unitLabel}
value={formik.values.maxPurchaseQuantity === undefined || formik.values.maxPurchaseQuantity === null ? '' : formik.values.maxPurchaseQuantity}
onChange={(e) => setOptionalDecimalField('maxPurchaseQuantity', e.target.value)}
error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''}
/>
<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'>
<Input