Files
dkala-admin/src/pages/product/components/BasicInfoSection.tsx
T
hamid zarghami 136cc1bed9 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>
2026-06-28 11:45:36 +03:30

469 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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