136cc1bed9
Support fixed vs variable pricing, payment type, purchase units, and decimal quantity inputs in create/update flows. Co-authored-by: Cursor <cursoragent@cursor.com>
469 lines
19 KiB
TypeScript
469 lines
19 KiB
TypeScript
import { type FC, useState, useEffect, useMemo } from 'react'
|
||
import type { FormikProps } from 'formik'
|
||
import Input from '@/components/Input'
|
||
import Select from '@/components/Select'
|
||
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 = {
|
||
formik: FormikProps<CreateProductType>
|
||
categories: Category[]
|
||
/** در صفحه ویرایش، اگر کالا تنوع دارد مقدار true پاس داده شود */
|
||
initialHasVariants?: boolean
|
||
}
|
||
|
||
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]
|
||
)
|
||
|
||
const selectedParent = useMemo(
|
||
() => categories.find((c) => c.id === parentCategoryId),
|
||
[categories, parentCategoryId]
|
||
)
|
||
|
||
const subCategories = useMemo(
|
||
() =>
|
||
(selectedParent?.children || []).map((c) => ({ label: c.title, value: c.id })),
|
||
[selectedParent]
|
||
)
|
||
|
||
useEffect(() => {
|
||
if (initialHasVariants !== undefined) {
|
||
setHasVariants(initialHasVariants)
|
||
}
|
||
}, [initialHasVariants])
|
||
|
||
/** همگامسازی parentCategoryId با categoryId وقتی فرم مقداردهی میشود (مثلاً در ویرایش) */
|
||
useEffect(() => {
|
||
const catId = formik.values.categoryId
|
||
if (!catId) return
|
||
const cat = categories.find((c) => c.id === catId)
|
||
const child = categories.flatMap((c) => c.children || []).find((ch) => ch.id === catId)
|
||
if (child) {
|
||
setParentCategoryId(child.parent || '')
|
||
} else if (cat && !cat.parent) {
|
||
setParentCategoryId(catId)
|
||
}
|
||
}, [formik.values.categoryId, categories])
|
||
|
||
const handleHasVariantsChange = (value: string | boolean) => {
|
||
const next = value === true
|
||
setHasVariants(next)
|
||
if (!next) {
|
||
formik.setFieldValue('variants', [])
|
||
formik.setFieldValue('attribute', '')
|
||
} else if (formik.values.variants.length === 0) {
|
||
formik.setFieldValue('variants', [newVariant()])
|
||
}
|
||
}
|
||
|
||
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()])
|
||
}
|
||
|
||
const updateVariant = (index: number, field: keyof ProductVariant, value: string | number | undefined) => {
|
||
const next = [...formik.values.variants]
|
||
next[index] = { ...next[index], [field]: value }
|
||
formik.setFieldValue('variants', next)
|
||
}
|
||
|
||
const removeVariant = (index: number) => {
|
||
formik.setFieldValue(
|
||
'variants',
|
||
formik.values.variants.filter((_, i) => i !== index)
|
||
)
|
||
}
|
||
|
||
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
|
||
label='نام کالا'
|
||
name='title'
|
||
placeholder=''
|
||
value={formik.values.title}
|
||
onChange={formik.handleChange}
|
||
error_text={formik.touched.title && formik.errors.title ? formik.errors.title : ''}
|
||
/>
|
||
|
||
<div className='mt-5 rowTwoInput flex flex-wrap gap-4'>
|
||
<div className='flex-1 min-w-[200px]'>
|
||
<Select
|
||
items={mainCategories}
|
||
label='دسته اصلی'
|
||
placeholder='انتخاب کنید'
|
||
name='parentCategoryId'
|
||
value={parentCategoryId}
|
||
onChange={(e) => {
|
||
const value = e.target.value
|
||
setParentCategoryId(value)
|
||
formik.setFieldValue('categoryId', value)
|
||
}}
|
||
/>
|
||
</div>
|
||
{subCategories.length > 0 && (
|
||
<div className='flex-1 min-w-[200px]'>
|
||
<Select
|
||
items={subCategories}
|
||
label='زیر دسته'
|
||
placeholder='انتخاب کنید (اختیاری)'
|
||
name='subcategoryId'
|
||
value={
|
||
subCategories.some((s) => s.value === formik.values.categoryId)
|
||
? formik.values.categoryId
|
||
: ''
|
||
}
|
||
onChange={(e) => {
|
||
const value = e.target.value
|
||
formik.setFieldValue('categoryId', value || parentCategoryId)
|
||
}}
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{formik.touched.categoryId && formik.errors.categoryId && (
|
||
<div className='mt-1 text-xs text-red-500'>{formik.errors.categoryId}</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'>
|
||
<div className='text-sm font-medium text-gray-800 mb-1'>قیمتگذاری و تنوع</div>
|
||
<p className='text-xs text-gray-500 mb-4'>تعیین کنید کالا دارای تنوع (مثلاً رنگ، سایز) است یا یک قیمت ثابت دارد.</p>
|
||
|
||
<div className='mb-5'>
|
||
<label className='block text-sm text-gray-700 mb-2'>کالای شما دارای تنوع است؟</label>
|
||
<RadioGroup
|
||
items={[
|
||
{ label: 'خیر، فقط یک نوع دارد', value: false },
|
||
{ label: 'بله، دارای تنوع است', value: true }
|
||
]}
|
||
selected={hasVariants}
|
||
onChange={handleHasVariantsChange}
|
||
/>
|
||
</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 ? (
|
||
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
|
||
name='attribute'
|
||
label='نام ویژگی تنوع (مثلاً رنگ، سایز)'
|
||
placeholder='مثلاً رنگ، سایز'
|
||
value={formik.values.attribute}
|
||
onChange={formik.handleChange}
|
||
error_text={formik.touched.attribute && formik.errors.attribute ? formik.errors.attribute : ''}
|
||
/>
|
||
|
||
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
|
||
<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>
|
||
<label className='block text-sm font-medium text-gray-700 mb-2'>تنوعها، قیمت و موجودی هر کدام</label>
|
||
<div className='space-y-3'>
|
||
{formik.values.variants.map((v, index) => (
|
||
<div
|
||
key={index}
|
||
className='flex flex-nowrap gap-3 items-center p-3 rounded-xl border border-gray-200 bg-white shadow-sm'
|
||
>
|
||
<Input
|
||
placeholder='مقدار (مثلاً قرمز، M)'
|
||
value={v.value}
|
||
onChange={(e) => updateVariant(index, 'value', e.target.value)}
|
||
className='flex-1 min-w-0'
|
||
/>
|
||
<Input
|
||
placeholder='قیمت'
|
||
type='number'
|
||
seprator={true}
|
||
value={v.price}
|
||
onChange={(e) => updateVariant(index, 'price', Number(e.target.value))}
|
||
className='flex-1 min-w-0 shrink-0'
|
||
/>
|
||
<Input
|
||
placeholder='موجودی (خالی=بینهایت)'
|
||
type='number'
|
||
min={0}
|
||
value={v.stock === undefined || v.stock === null ? '' : v.stock}
|
||
onChange={(e) => {
|
||
const val = e.target.value
|
||
updateVariant(index, 'stock', val === '' ? undefined : Number(val))
|
||
}}
|
||
className='flex-1 min-w-0 shrink-0'
|
||
/>
|
||
<div className='min-w-5'>
|
||
<Trash
|
||
onClick={() => removeVariant(index)}
|
||
color='red' size={20} />
|
||
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<Button
|
||
type='button'
|
||
className='mt-3 w-fit px-5 !bg-white border border-dashed border-gray-300 text-gray-600 hover:!bg-gray-100 hover:border-gray-400'
|
||
onClick={addVariant}
|
||
>
|
||
<Add color='gray' className='size-5 ml-2 inline' />
|
||
افزودن تنوع
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className='mt-5'>
|
||
<Textarea
|
||
name='desc'
|
||
label='توضیحات کالا'
|
||
placeholder=''
|
||
value={formik.values.desc}
|
||
onChange={formik.handleChange}
|
||
error_text={formik.touched.desc && formik.errors.desc ? formik.errors.desc : ''}
|
||
/>
|
||
</div>
|
||
</>
|
||
)
|
||
}
|
||
|
||
export default BasicInfoSection
|