diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 8dd6e96..c9bbed0 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -14,21 +14,30 @@ type Props = { onEnter?: () => void; unit?: string; seprator?: boolean; + allowDecimal?: boolean; isNotRequired?: boolean; onChangeSearchFinal?: (value: string) => void; } & InputHTMLAttributes const formatNumber = (value: string | number): string => { - if (!value) return ''; + if (value === '' || value === null || value === undefined) return ''; 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) => { + const { allowDecimal, seprator, unit, variant, error_text, onChangeSearchFinal, ...inputProps } = props const [formattedValue, setFormattedValue] = useState( - props.value ? formatNumber(props.value as string) : '' + props.value !== undefined && props.value !== null && props.value !== '' ? formatNumber(props.value as string) : '' ); + const [decimalValue, setDecimalValue] = useState( + props.value !== undefined && props.value !== null && props.value !== '' ? String(props.value) : '' + ) + const [isDecimalFocused, setIsDecimalFocused] = useState(false) const [showPassword, setShowPassword] = useState(false) const [search, setSearch] = useState('') @@ -41,7 +50,23 @@ const Input: FC = (props: Props) => { ); const handleInputChange = (event: React.ChangeEvent) => { - 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) + return + } + + if (seprator) { const inputValue = event.target.value.replace(/,/g, ''); // حذف کاماها const formatted = formatNumber(inputValue); @@ -65,22 +90,54 @@ const Input: FC = (props: Props) => { }; 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)); } else { setFormattedValue(''); } - }, [props.value]) + }, [props.value, allowDecimal, isDecimalFocused]) useEffect(() => { - if (props.variant === 'search' && props.onChangeSearchFinal) { + if (variant === 'search' && onChangeSearchFinal) { const timeout = setTimeout(() => { - props.onChangeSearchFinal?.(search) + onChangeSearchFinal?.(search) }, 1000) 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 (
@@ -89,25 +146,62 @@ const Input: FC = (props: Props) => {
- { - setSearch(e.target.value) - handleInputChange(e) - }} value={props.seprator ? formattedValue : props.value} type={props.type === 'password' && showPassword ? 'text' : props.type === 'password' ? 'password' : undefined} className={inputClass} /> + { + 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) + } + inputProps.onBlur?.(e) + }} + onChange={(e) => { + setSearch(e.target.value) + handleInputChange(e) + }} + value={inputValue} + className={clx(inputClass, unit && 'pl-14')} + /> { - props.type === 'password' && + unit && + + {unit} + + } + + { + inputProps.type === 'password' && setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' /> } { - props.variant === 'search' && + variant === 'search' && } { - props.error_text && + error_text && }
diff --git a/src/pages/product/Create.tsx b/src/pages/product/Create.tsx index 7b17e40..2a53822 100644 --- a/src/pages/product/Create.tsx +++ b/src/pages/product/Create.tsx @@ -11,6 +11,7 @@ import { useCreateProduct, useGetCategories } from './hooks/useProductData' import { useMultipleUpload } from '../uploader/hooks/useUploaderData' import { Pages } from '@/config/Pages' import type { ErrorType } from '@/helpers/types' +import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum' @@ -39,7 +40,13 @@ const CreateProduct: FC = () => { isSpecialOffer: false, order: 0, 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({ title: Yup.string().required('نام کالا الزامی است'), @@ -47,6 +54,17 @@ const CreateProduct: FC = () => { price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'), discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'), 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( Yup.object().shape({ id: Yup.string(), @@ -66,7 +84,13 @@ const CreateProduct: FC = () => { variants, isActive, 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, { diff --git a/src/pages/product/Update.tsx b/src/pages/product/Update.tsx index c506ba1..d5cd9ec 100644 --- a/src/pages/product/Update.tsx +++ b/src/pages/product/Update.tsx @@ -14,6 +14,7 @@ import { useMultipleUpload } from '../uploader/hooks/useUploaderData' import { Pages } from '@/config/Pages' import type { ErrorType } from '@/helpers/types' import { extractErrorMessage } from '@/config/func' +import { PaymentType, PricingType, PurchaseUnit } from './enum/Enum' const UpdateProduct: FC = () => { const { id } = useParams<{ id: string }>() @@ -45,7 +46,13 @@ const UpdateProduct: FC = () => { isSpecialOffer: false, order: 0, 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({ title: Yup.string().required('نام کالا الزامی است'), @@ -53,6 +60,17 @@ const UpdateProduct: FC = () => { price: Yup.number().required('قیمت الزامی است').min(0, 'قیمت باید مثبت باشد'), discount: Yup.number().min(0, 'تخفیف باید مثبت باشد'), 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( Yup.object().shape({ id: Yup.string(), @@ -83,7 +101,13 @@ const UpdateProduct: FC = () => { variants, isActive, 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 }, { @@ -132,7 +156,13 @@ const UpdateProduct: FC = () => { isSpecialOffer: product.isSpecialOffer || false, order: product.order ?? 0, 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) setIsSpecial(product.isSpecialOffer || false) diff --git a/src/pages/product/components/BasicInfoSection.tsx b/src/pages/product/components/BasicInfoSection.tsx index 9d1ee6f..efa26c2 100644 --- a/src/pages/product/components/BasicInfoSection.tsx +++ b/src/pages/product/components/BasicInfoSection.tsx @@ -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 = ({ formik, categories, initialHasVariants }) => { const [hasVariants, setHasVariants] = useState(initialHasVariants ?? false) /** دسته اصلی انتخاب‌شده برای نمایش زیردسته‌ها */ const [parentCategoryId, setParentCategoryId] = useState('') + 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 = ({ 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 = ({ 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 ( <> = ({ formik, categories, initi
{formik.errors.categoryId}
)} -
- { - const v = e.target.value - formik.setFieldValue('maxPurchaseQuantity', v === '' ? undefined : Number(v)) - }} - error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''} - /> -
+ {!isVariablePricing && ( +
+ setOptionalNumberField('maxPurchaseQuantity', e.target.value)} + error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''} + /> +
+ )} {/* بخش قیمت‌گذاری و تنوع */}
@@ -160,7 +200,7 @@ const BasicInfoSection: FC = ({ formik, categories, initi = ({ formik, categories, initi />
+
+ + +
+ +
+ + +
+ {!hasVariants ? ( -
- formik.setFieldValue('price', Number(e.target.value))} - error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''} - /> - { - const v = e.target.value - formik.setFieldValue('stock', v === '' ? undefined : Number(v)) - }} - error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''} - /> - formik.setFieldValue('discount', Number(e.target.value))} - error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''} - /> - formik.setFieldValue('order', Number(e.target.value))} - error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''} - /> -
+ isVariablePricing ? ( +
+
+ formik.setFieldValue('price', Number(e.target.value))} + error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''} + /> +
+ +
+ setOptionalDecimalField('minPurchaseQuantity', e.target.value)} + error_text={formik.touched.minPurchaseQuantity && formik.errors.minPurchaseQuantity ? formik.errors.minPurchaseQuantity : ''} + /> + setOptionalDecimalField('maxPurchaseQuantity', e.target.value)} + error_text={formik.touched.maxPurchaseQuantity && formik.errors.maxPurchaseQuantity ? formik.errors.maxPurchaseQuantity : ''} + /> + setOptionalDecimalField('purchasePitch', e.target.value)} + error_text={formik.touched.purchasePitch && formik.errors.purchasePitch ? formik.errors.purchasePitch : ''} + /> +
+ +
+ setOptionalNumberField('stock', e.target.value)} + error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''} + /> + formik.setFieldValue('discount', Number(e.target.value))} + error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''} + /> + formik.setFieldValue('order', Number(e.target.value))} + error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''} + /> +
+
+ ) : ( +
+ formik.setFieldValue('price', Number(e.target.value))} + error_text={formik.touched.price && formik.errors.price ? formik.errors.price : ''} + /> + setOptionalNumberField('stock', e.target.value)} + error_text={formik.touched.stock && formik.errors.stock ? formik.errors.stock : ''} + /> + formik.setFieldValue('discount', Number(e.target.value))} + error_text={formik.touched.discount && formik.errors.discount ? formik.errors.discount : ''} + /> + formik.setFieldValue('order', Number(e.target.value))} + error_text={formik.touched.order && formik.errors.order ? formik.errors.order : ''} + /> +
+ ) ) : (
= { + [PurchaseUnit.NUMBER]: "عدد", + [PurchaseUnit.KILOGRAM]: "کیلوگرم", + [PurchaseUnit.GRAM]: "گرم", + [PurchaseUnit.CENTIMETER]: "سانتی‌متر", + [PurchaseUnit.METER]: "متر", + [PurchaseUnit.MILLILITER]: "میلی‌لیتر", + [PurchaseUnit.LITER]: "لیتر", +} diff --git a/src/pages/product/types/Types.ts b/src/pages/product/types/Types.ts index f7df173..5e01fca 100644 --- a/src/pages/product/types/Types.ts +++ b/src/pages/product/types/Types.ts @@ -1,4 +1,5 @@ import type { IResponse } from "@/types/response.types"; +import type { PaymentType, PricingType, PurchaseUnit } from "../enum/Enum"; export type ProductVariant = { id?: string; @@ -22,6 +23,12 @@ export type CreateProductType = { /** فقط در حالت بدون تنوع استفاده می‌شود */ stock?: number; maxPurchaseQuantity?: number; + minPurchaseQuantity?: number; + purchasePitch?: number; + pricingType: PricingType; + paymentType: PaymentType; + purchaseUnit?: PurchaseUnit; + needAdminAcceptance?: boolean; }; export type Category = { @@ -125,6 +132,12 @@ export type Product = { discount: number; isSpecialOffer: boolean; maxPurchaseQuantity?: number; + minPurchaseQuantity?: number; + purchasePitch?: number; + pricingType?: PricingType; + paymentType?: PaymentType; + purchaseUnit?: PurchaseUnit; + needAdminAcceptance?: boolean; }; export type GetProductsParams = { @@ -157,6 +170,12 @@ export type ProductDetails = { discount: number; isSpecialOffer: boolean; maxPurchaseQuantity?: number; + minPurchaseQuantity?: number; + purchasePitch?: number; + pricingType?: PricingType; + paymentType?: PaymentType; + purchaseUnit?: PurchaseUnit; + needAdminAcceptance?: boolean; }; export type PaginationMeta = {