feat(discount-codes): enhance DiscountCodeFormPage with multi-level application support and validation

This commit is contained in:
hossein taromi 2025-09-29 17:28:53 +03:30
parent bcb52961a2
commit f69e48dc0e
3 changed files with 263 additions and 86 deletions

View File

@ -7,6 +7,10 @@ export type DiscountApplicationLevel =
| "shipping" | "shipping"
| "product_fee"; | "product_fee";
export type DiscountApplicationLevels =
| DiscountApplicationLevel
| DiscountApplicationLevel[];
export type DiscountStatus = "active" | "inactive"; export type DiscountStatus = "active" | "inactive";
export type UserGroup = "new" | "loyal" | "all"; export type UserGroup = "new" | "loyal" | "all";
@ -42,7 +46,7 @@ export interface DiscountCode {
type: DiscountCodeType; type: DiscountCodeType;
value: number; value: number;
status: DiscountStatus; status: DiscountStatus;
application_level: DiscountApplicationLevel; application_level: DiscountApplicationLevels;
min_purchase_amount?: number; min_purchase_amount?: number;
max_discount_amount?: number; max_discount_amount?: number;
usage_limit?: number; usage_limit?: number;
@ -53,8 +57,8 @@ export interface DiscountCode {
user_restrictions?: DiscountUserRestrictions; user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount; stepped_discount?: SteppedDiscount;
meta?: DiscountMeta; meta?: DiscountMeta;
specific_product_ids?: number[]; product_ids?: number[];
specific_category_ids?: number[]; category_ids?: number[];
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -64,7 +68,7 @@ export interface DiscountCodeFilters {
limit?: number; limit?: number;
status?: DiscountStatus; status?: DiscountStatus;
type?: DiscountCodeType; type?: DiscountCodeType;
application_level?: DiscountApplicationLevel; application_level?: DiscountApplicationLevels;
code?: string; code?: string;
active_only?: boolean; active_only?: boolean;
} }
@ -76,7 +80,7 @@ export interface CreateDiscountCodeRequest {
type: DiscountCodeType; type: DiscountCodeType;
value: number; value: number;
status: DiscountStatus; status: DiscountStatus;
application_level: DiscountApplicationLevel; application_level: DiscountApplicationLevels;
min_purchase_amount?: number; min_purchase_amount?: number;
max_discount_amount?: number; max_discount_amount?: number;
usage_limit?: number; usage_limit?: number;
@ -87,8 +91,8 @@ export interface CreateDiscountCodeRequest {
user_restrictions?: DiscountUserRestrictions; user_restrictions?: DiscountUserRestrictions;
stepped_discount?: SteppedDiscount; stepped_discount?: SteppedDiscount;
meta?: DiscountMeta; meta?: DiscountMeta;
specific_product_ids?: number[]; product_ids?: number[];
specific_category_ids?: number[]; category_ids?: number[];
} }
export interface UpdateDiscountCodeRequest export interface UpdateDiscountCodeRequest

View File

@ -21,8 +21,15 @@ export const getDiscountCodes = async (filters?: DiscountCodeFilters) => {
if (filters?.limit) queryParams.limit = filters.limit; if (filters?.limit) queryParams.limit = filters.limit;
if (filters?.status) queryParams.status = filters.status; if (filters?.status) queryParams.status = filters.status;
if (filters?.type) queryParams.type = filters.type; if (filters?.type) queryParams.type = filters.type;
if (filters?.application_level) if (filters?.application_level) {
queryParams.application_level = filters.application_level; if (Array.isArray(filters.application_level)) {
if (filters.application_level.length > 0) {
queryParams.application_level = filters.application_level.join(",");
}
} else {
queryParams.application_level = filters.application_level;
}
}
if (filters?.code) queryParams.code = filters.code; if (filters?.code) queryParams.code = filters.code;
if (typeof filters?.active_only === "boolean") if (typeof filters?.active_only === "boolean")
queryParams.active_only = filters.active_only ? "true" : "false"; queryParams.active_only = filters.active_only ? "true" : "false";

View File

@ -26,21 +26,24 @@ const schema = yup.object({
.transform((val, original) => parseFormattedNumber(original) as any) .transform((val, original) => parseFormattedNumber(original) as any)
.typeError('مقدار نامعتبر است') .typeError('مقدار نامعتبر است')
.required('مقدار الزامی است') .required('مقدار الزامی است')
.min(0.01, 'مقدار باید بیشتر از صفر باشد'), .min(0.01, 'مقدار باید بیشتر از صفر باشد')
.max(999999, 'مقدار نباید بیشتر از ۹۹۹,۹۹۹ باشد'),
status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'), status: yup.mixed<'active' | 'inactive'>().oneOf(['active', 'inactive']).required('وضعیت الزامی است'),
application_level: yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee']).required('سطح اعمال الزامی است'), application_level: yup.array().of(yup.mixed<'invoice' | 'category' | 'product' | 'shipping' | 'product_fee'>().oneOf(['invoice', 'category', 'product', 'shipping', 'product_fee'])).min(1, 'حداقل یک سطح اعمال انتخاب کنید').required('سطح اعمال الزامی است'),
min_purchase_amount: yup min_purchase_amount: yup
.number() .number()
.transform((val, original) => parseFormattedNumber(original) as any) .transform((val, original) => parseFormattedNumber(original) as any)
.min(0.01, 'مبلغ باید بیشتر از صفر باشد') .min(0.01, 'مبلغ باید بیشتر از صفر باشد')
.max(99999999, 'مبلغ نباید بیشتر از ۹۹,۹۹۹,۹۹۹ باشد')
.nullable(), .nullable(),
max_discount_amount: yup max_discount_amount: yup
.number() .number()
.transform((val, original) => parseFormattedNumber(original) as any) .transform((val, original) => parseFormattedNumber(original) as any)
.min(0.01, 'مبلغ باید بیشتر از صفر باشد') .min(0.01, 'مبلغ باید بیشتر از صفر باشد')
.max(99999999, 'مبلغ نباید بیشتر از ۹۹,۹۹۹,۹۹۹ باشد')
.nullable(), .nullable(),
usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(), usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').max(999999, 'حداکثر ۹۹۹,۹۹۹ بار استفاده').nullable(),
user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').nullable(), user_usage_limit: yup.number().transform((v, o) => o === '' ? undefined : v).min(1, 'حداقل ۱ بار استفاده').max(99999, 'حداکثر ۹۹,۹۹۹ بار استفاده').nullable(),
single_use: yup.boolean().required('این فیلد الزامی است'), single_use: yup.boolean().required('این فیلد الزامی است'),
valid_from: yup.string().nullable(), valid_from: yup.string().nullable(),
valid_to: yup.string().nullable(), valid_to: yup.string().nullable(),
@ -77,6 +80,7 @@ const DiscountCodeFormPage = () => {
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]); const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]); const [selectedProductIds, setSelectedProductIds] = useState<number[]>([]);
const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]); const [selectedCategoryIds, setSelectedCategoryIds] = useState<number[]>([]);
const [selectedApplicationLevels, setSelectedApplicationLevels] = useState<string[]>(['invoice']);
const { data: dc, isLoading: dcLoading } = useDiscountCode(id || ''); const { data: dc, isLoading: dcLoading } = useDiscountCode(id || '');
const { mutate: create, isPending: creating } = useCreateDiscountCode(); const { mutate: create, isPending: creating } = useCreateDiscountCode();
@ -176,7 +180,7 @@ const DiscountCodeFormPage = () => {
const { register, handleSubmit, formState: { errors, isValid }, reset, watch } = useForm<CreateDiscountCodeRequest>({ const { register, handleSubmit, formState: { errors, isValid }, reset, watch } = useForm<CreateDiscountCodeRequest>({
resolver: yupResolver(schema as any), resolver: yupResolver(schema as any),
mode: 'onChange', mode: 'onChange',
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false } defaultValues: { status: 'active', type: 'percentage', application_level: ['invoice'], single_use: false }
}); });
const applicationLevel = watch('application_level'); const applicationLevel = watch('application_level');
@ -207,14 +211,23 @@ const DiscountCodeFormPage = () => {
setSelectedUserIds(dc.user_restrictions.user_ids); setSelectedUserIds(dc.user_restrictions.user_ids);
} }
// Set selected application levels
if (dc.application_level) {
setSelectedApplicationLevels(
Array.isArray(dc.application_level)
? dc.application_level
: [dc.application_level]
);
}
// Set selected product IDs // Set selected product IDs
if (dc.specific_product_ids) { if (dc.product_ids) {
setSelectedProductIds(dc.specific_product_ids); setSelectedProductIds(dc.product_ids);
} }
// Set selected category IDs // Set selected category IDs
if (dc.specific_category_ids) { if (dc.category_ids) {
setSelectedCategoryIds(dc.specific_category_ids); setSelectedCategoryIds(dc.category_ids);
} }
} }
}, [isEdit, dc, reset]); }, [isEdit, dc, reset]);
@ -225,14 +238,17 @@ const DiscountCodeFormPage = () => {
const formData: CreateDiscountCodeRequest = { const formData: CreateDiscountCodeRequest = {
...data, ...data,
application_level: selectedApplicationLevels.length === 1
? selectedApplicationLevels[0] as any
: selectedApplicationLevels as any,
valid_from: toApiDateTime(data.valid_from), valid_from: toApiDateTime(data.valid_from),
valid_to: toApiDateTime(data.valid_to), valid_to: toApiDateTime(data.valid_to),
user_restrictions: { user_restrictions: selectedUserIds.length > 0 ? {
...cleanRestrictions, ...cleanRestrictions,
user_ids: selectedUserIds.length > 0 ? selectedUserIds : undefined, user_ids: selectedUserIds,
}, } : cleanRestrictions.user_group ? cleanRestrictions : undefined,
specific_product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined, product_ids: selectedProductIds.length > 0 ? selectedProductIds : undefined,
specific_category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined, category_ids: selectedCategoryIds.length > 0 ? selectedCategoryIds : undefined,
}; };
if (isEdit && id) { if (isEdit && id) {
@ -337,7 +353,7 @@ const DiscountCodeFormPage = () => {
label="مقدار تخفیف" label="مقدار تخفیف"
type="number" type="number"
step="0.01" step="0.01"
placeholder="300000" placeholder="حداکثر ۹۹۹,۹۹۹"
error={errors.value?.message as string} error={errors.value?.message as string}
thousandSeparator thousandSeparator
numeric numeric
@ -358,73 +374,223 @@ const DiscountCodeFormPage = () => {
</select> </select>
{errors.status && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errors.status.message as string}</p>} {errors.status && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errors.status.message as string}</p>}
</div> </div>
<div className="space-y-2"> <div className="lg:col-span-3">
<Label>سطح اعمال</Label> <Label className="text-base font-semibold text-gray-800 dark:text-gray-200 mb-4 block">سطح اعمال تخفیف</Label>
<select <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-100 transition-colors" {[
{...register('application_level')} {
data-testid="discount-application-level-select" value: 'invoice',
required label: 'کل سبد خرید',
aria-required="true" description: 'تخفیف روی کل مبلغ سبد خرید',
> icon: '🛒',
<option value="invoice">کل سبد خرید</option> color: 'blue'
<option value="category">دستهبندی خاص</option> },
<option value="product">محصول خاص</option> {
<option value="shipping">هزینه ارسال</option> value: 'category',
<option value="product_fee">کارمزد محصول</option> label: 'دسته‌بندی خاص',
</select> description: 'تخفیف روی محصولات دسته‌بندی‌های خاص',
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400" role="alert">{errors.application_level.message as string}</p>} icon: '📁',
color: 'green'
},
{
value: 'product',
label: 'محصول خاص',
description: 'تخفیف روی محصولات خاص',
icon: '📦',
color: 'purple'
},
{
value: 'shipping',
label: 'هزینه ارسال',
description: 'تخفیف روی هزینه ارسال',
icon: '🚚',
color: 'orange'
},
{
value: 'product_fee',
label: 'کارمزد محصول',
description: 'تخفیف روی کارمزد محصول',
icon: '💰',
color: 'red'
}
].map(option => {
const isSelected = selectedApplicationLevels.includes(option.value);
const getColorClasses = (color: string, selected: boolean) => {
const baseClasses = 'relative flex flex-col p-4 border-2 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-lg transform hover:-translate-y-1';
if (selected) {
switch (color) {
case 'blue':
return `${baseClasses} border-blue-500 bg-blue-50 dark:bg-blue-900/30 shadow-lg`;
case 'green':
return `${baseClasses} border-green-500 bg-green-50 dark:bg-green-900/30 shadow-lg`;
case 'purple':
return `${baseClasses} border-purple-500 bg-purple-50 dark:bg-purple-900/30 shadow-lg`;
case 'orange':
return `${baseClasses} border-orange-500 bg-orange-50 dark:bg-orange-900/30 shadow-lg`;
case 'red':
return `${baseClasses} border-red-500 bg-red-50 dark:bg-red-900/30 shadow-lg`;
default:
return `${baseClasses} border-gray-500 bg-gray-50 dark:bg-gray-900/30 shadow-lg`;
}
} else {
return `${baseClasses} border-gray-200 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-500`;
}
};
const getIndicatorClasses = (color: string, selected: boolean) => {
const baseClasses = 'absolute top-3 left-3 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all';
if (selected) {
switch (color) {
case 'blue':
return `${baseClasses} border-blue-500 bg-blue-500`;
case 'green':
return `${baseClasses} border-green-500 bg-green-500`;
case 'purple':
return `${baseClasses} border-purple-500 bg-purple-500`;
case 'orange':
return `${baseClasses} border-orange-500 bg-orange-500`;
case 'red':
return `${baseClasses} border-red-500 bg-red-500`;
default:
return `${baseClasses} border-gray-500 bg-gray-500`;
}
} else {
return `${baseClasses} border-gray-300 dark:border-gray-600`;
}
};
return (
<label
key={option.value}
className={getColorClasses(option.color, isSelected)}
>
<input
type="checkbox"
checked={isSelected}
onChange={(e) => {
if (e.target.checked) {
setSelectedApplicationLevels(prev => [...prev, option.value]);
} else {
setSelectedApplicationLevels(prev => prev.filter(level => level !== option.value));
}
}}
className="sr-only"
/>
{/* Selection indicator */}
<div className={getIndicatorClasses(option.color, isSelected)}>
{isSelected && (
<svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
</svg>
)}
</div>
{/* Icon */}
<div className="text-2xl mb-3 mt-2 text-center">
{option.icon}
</div>
{/* Content */}
<div className="text-center">
<div className="text-sm font-semibold text-gray-900 dark:text-gray-100 mb-2">
{option.label}
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 leading-relaxed">
{option.description}
</div>
</div>
</label>
);
})}
</div>
{errors.application_level && <p className="text-sm text-red-600 dark:text-red-400 mt-3 font-medium" role="alert">{errors.application_level.message as string}</p>}
</div> </div>
{/* Conditional Product Selection */} {/* Conditional Product Selection */}
{applicationLevel === 'product' && ( {selectedApplicationLevels.includes('product') && (
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<MultiSelectAutocomplete <div className="bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-6 border border-purple-200 dark:border-purple-700">
label="انتخاب محصولات خاص" <div className="flex items-center gap-3 mb-4">
options={productOptions} <div className="p-2 bg-purple-100 dark:bg-purple-900 rounded-lg">
selectedValues={selectedProductIds} <span className="text-xl">📦</span>
onChange={setSelectedProductIds} </div>
placeholder="جستجو و انتخاب محصولات..." <div>
isLoading={productsLoading && productPage === 1} <h3 className="text-base font-semibold text-purple-900 dark:text-purple-100">انتخاب محصولات خاص</h3>
disabled={false} <p className="text-sm text-purple-700 dark:text-purple-300">محصولات مورد نظر برای اعمال تخفیف را انتخاب کنید</p>
onSearchChange={(q) => { setProductSearch(q); setProductPage(1); }} </div>
onLoadMore={() => { </div>
if (!productsLoading && productSearchResult && (productSearchResult.total > accumulatedProducts.length)) { <MultiSelectAutocomplete
setProductPage(prev => prev + 1); label=""
} options={productOptions}
}} selectedValues={selectedProductIds}
hasMore={!!productSearchResult && accumulatedProducts.length < (productSearchResult.total || 0)} onChange={setSelectedProductIds}
loadingMore={productsLoading && productPage > 1} placeholder="جستجو و انتخاب محصولات..."
/> isLoading={productsLoading && productPage === 1}
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> disabled={false}
در صورت انتخاب محصولات، تخفیف فقط روی این محصولات اعمال خواهد شد. onSearchChange={(q) => { setProductSearch(q); setProductPage(1); }}
</p> onLoadMore={() => {
if (!productsLoading && productSearchResult && ((productSearchResult.total || 0) > accumulatedProducts.length)) {
setProductPage(prev => prev + 1);
}
}}
hasMore={!!productSearchResult && accumulatedProducts.length < (productSearchResult.total || 0)}
loadingMore={productsLoading && productPage > 1}
/>
<div className="mt-3 p-3 bg-purple-100/50 dark:bg-purple-900/30 rounded-lg">
<p className="text-sm text-purple-700 dark:text-purple-300 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
در صورت انتخاب محصولات، تخفیف فقط روی این محصولات اعمال خواهد شد.
</p>
</div>
</div>
</div> </div>
)} )}
{/* Conditional Category Selection */} {/* Conditional Category Selection */}
{applicationLevel === 'category' && ( {selectedApplicationLevels.includes('category') && (
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<MultiSelectAutocomplete <div className="bg-gradient-to-r from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-6 border border-green-200 dark:border-green-700">
label="انتخاب دسته‌بندی‌های خاص" <div className="flex items-center gap-3 mb-4">
options={categoryOptions} <div className="p-2 bg-green-100 dark:bg-green-900 rounded-lg">
selectedValues={selectedCategoryIds} <span className="text-xl">📁</span>
onChange={setSelectedCategoryIds} </div>
placeholder="جستجو و انتخاب دسته‌بندی‌ها..." <div>
isLoading={categoriesLoading && categoryPage === 1} <h3 className="text-base font-semibold text-green-900 dark:text-green-100">انتخاب دستهبندیهای خاص</h3>
disabled={false} <p className="text-sm text-green-700 dark:text-green-300">دستهبندیهای مورد نظر برای اعمال تخفیف را انتخاب کنید</p>
onSearchChange={(q) => { setCategorySearch(q); setCategoryPage(1); }} </div>
onLoadMore={() => { </div>
if (!categoriesLoading && categorySearchResult && (categorySearchResult.length > 0)) { <MultiSelectAutocomplete
setCategoryPage(prev => prev + 1); label=""
} options={categoryOptions}
}} selectedValues={selectedCategoryIds}
hasMore={!!categorySearchResult && categorySearchResult.length >= CATEGORIES_PAGE_SIZE} onChange={setSelectedCategoryIds}
loadingMore={categoriesLoading && categoryPage > 1} placeholder="جستجو و انتخاب دسته‌بندی‌ها..."
/> isLoading={categoriesLoading && categoryPage === 1}
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2"> disabled={false}
در صورت انتخاب دستهبندیها، تخفیف فقط روی محصولات این دستهها اعمال خواهد شد. onSearchChange={(q) => { setCategorySearch(q); setCategoryPage(1); }}
</p> onLoadMore={() => {
if (!categoriesLoading && categorySearchResult && (categorySearchResult.length > 0)) {
setCategoryPage(prev => prev + 1);
}
}}
hasMore={!!categorySearchResult && categorySearchResult.length >= CATEGORIES_PAGE_SIZE}
loadingMore={categoriesLoading && categoryPage > 1}
/>
<div className="mt-3 p-3 bg-green-100/50 dark:bg-green-900/30 rounded-lg">
<p className="text-sm text-green-700 dark:text-green-300 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
در صورت انتخاب دستهبندیها، تخفیف فقط روی محصولات این دستهها اعمال خواهد شد.
</p>
</div>
</div>
</div> </div>
)} )}
@ -432,7 +598,7 @@ const DiscountCodeFormPage = () => {
label="حداقل مبلغ خرید" label="حداقل مبلغ خرید"
type="number" type="number"
step="0.01" step="0.01"
placeholder="100000" placeholder="حداکثر ۹۹,۹۹۹,۹۹۹"
error={errors.min_purchase_amount?.message as string} error={errors.min_purchase_amount?.message as string}
thousandSeparator thousandSeparator
numeric numeric
@ -442,7 +608,7 @@ const DiscountCodeFormPage = () => {
label="حداکثر مبلغ تخفیف" label="حداکثر مبلغ تخفیف"
type="number" type="number"
step="0.01" step="0.01"
placeholder="50000" placeholder="حداکثر ۹۹,۹۹۹,۹۹۹"
error={errors.max_discount_amount?.message as string} error={errors.max_discount_amount?.message as string}
thousandSeparator thousandSeparator
numeric numeric
@ -454,14 +620,14 @@ const DiscountCodeFormPage = () => {
<Input <Input
label="حداکثر تعداد استفاده" label="حداکثر تعداد استفاده"
type="number" type="number"
placeholder="1000" placeholder="حداکثر ۹۹۹,۹۹۹"
error={errors.usage_limit?.message as string} error={errors.usage_limit?.message as string}
{...register('usage_limit')} {...register('usage_limit')}
/> />
<Input <Input
label="حداکثر استفاده هر کاربر" label="حداکثر استفاده هر کاربر"
type="number" type="number"
placeholder="1" placeholder="حداکثر ۹۹,۹۹۹"
error={errors.user_usage_limit?.message as string} error={errors.user_usage_limit?.message as string}
{...register('user_usage_limit')} {...register('user_usage_limit')}
/> />