feat(wallet-credit): enhance user search functionality and input handling

- Integrated user search with debounced input for improved performance and user experience.
- Added dropdown for user selection based on search input, displaying user details.
- Updated amount input to format currency with thousands separators.
- Refactored form handling to improve state management and validation.

These changes significantly enhance the Wallet Credit page's usability and functionality.
This commit is contained in:
hosseintaromi 2026-02-08 15:05:43 +03:30
parent 785f97b26d
commit 85fdf1f7a2
1 changed files with 106 additions and 15 deletions

View File

@ -1,15 +1,17 @@
import React, { useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { Wallet, Plus, Loader2 } from 'lucide-react';
import { Plus } from 'lucide-react';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { useWalletCredit } from '../core/_hooks';
import { WalletCreditRequest, WalletType } from '../core/_credit-models';
import { formatCurrency } from '@/utils/formatters';
import { persianToEnglish } from '@/utils/numberUtils';
import { formatWithThousands, persianToEnglish } from '@/utils/numberUtils';
import { useSearchUsers, useUsers } from '@/pages/users-admin/core/_hooks';
import { UserFilters } from '@/pages/users-admin/core/_models';
const schema = yup.object({
user_id: yup.number().required('شناسه کاربر الزامی است').positive('شناسه کاربر باید عدد مثبت باشد'),
@ -24,9 +26,14 @@ type FormData = yup.InferType<typeof schema>;
const WalletCreditPage = () => {
const { mutate: creditWallet, isPending } = useWalletCredit();
const [successData, setSuccessData] = useState<{ new_balance: number; message: string } | null>(null);
const [searchText, setSearchText] = useState('');
const [debouncedText, setDebouncedText] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [amountDisplay, setAmountDisplay] = useState('');
const {
register,
setValue,
handleSubmit,
formState: { errors },
reset,
@ -37,6 +44,44 @@ const WalletCreditPage = () => {
},
});
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedText(searchText);
}, 300);
return () => clearTimeout(handler);
}, [searchText]);
const { data: usersData, isLoading: isUsersLoading } = useUsers({
limit: 20,
offset: 0,
});
const searchFilters = useMemo<UserFilters>(() => {
if (!debouncedText) return {};
return {
search_text: debouncedText,
limit: 20,
offset: 0,
};
}, [debouncedText]);
const { data: searchData, isLoading: isSearchLoading } = useSearchUsers(searchFilters);
const userOptions = useMemo(() => {
if (debouncedText) {
return searchData?.users || [];
}
return usersData || [];
}, [debouncedText, searchData, usersData]);
const isLoadingUsers = debouncedText ? isSearchLoading : isUsersLoading;
const handleSelectUser = (user: any) => {
setValue('user_id', user.id, { shouldValidate: true, shouldDirty: true });
setSearchText(user.phone_number);
setIsDropdownOpen(false);
};
const onSubmit = (data: FormData) => {
const payload: WalletCreditRequest = {
user_id: data.user_id,
@ -53,6 +98,7 @@ const WalletCreditPage = () => {
message: response.message,
});
reset();
setAmountDisplay('');
setTimeout(() => setSuccessData(null), 5000);
},
});
@ -80,14 +126,55 @@ const WalletCreditPage = () => {
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
شناسه کاربر <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
type="number"
{...register('user_id', {
setValueAs: (value) => (value === '' ? undefined : Number(persianToEnglish(value))),
})}
value={searchText}
onChange={(e) => {
const normalized = persianToEnglish(e.target.value).replace(/[^\d]/g, '');
setSearchText(normalized);
setIsDropdownOpen(true);
setValue('user_id', undefined as any, { shouldValidate: true, shouldDirty: true });
}}
onFocus={() => setIsDropdownOpen(true)}
onBlur={() => setTimeout(() => setIsDropdownOpen(false), 150)}
error={errors.user_id?.message}
placeholder="مثلاً 52"
placeholder="جستجو با شماره موبایل"
/>
<input
type="hidden"
{...register('user_id')}
/>
{isDropdownOpen && (
<div className="absolute z-20 mt-2 w-full rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-lg max-h-60 overflow-y-auto">
{isLoadingUsers && (
<div className="p-3 text-sm text-gray-500 dark:text-gray-400">
در حال بارگذاری...
</div>
)}
{!isLoadingUsers && userOptions.length === 0 && (
<div className="p-3 text-sm text-gray-500 dark:text-gray-400">
کاربری یافت نشد
</div>
)}
{!isLoadingUsers && userOptions.map((user: any) => (
<button
key={user.id}
type="button"
onMouseDown={(e) => e.preventDefault()}
onClick={() => handleSelectUser(user)}
className="w-full text-right px-4 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
<div className="text-sm text-gray-900 dark:text-gray-100">
{user.first_name} {user.last_name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400">
{user.phone_number}
</div>
</button>
))}
</div>
)}
</div>
</div>
<div>
@ -114,11 +201,15 @@ const WalletCreditPage = () => {
</label>
<Input
type="text"
{...register('amount', {
setValueAs: (value) => (value === '' ? undefined : Number(persianToEnglish(value))),
})}
value={amountDisplay}
onChange={(e) => {
const normalized = persianToEnglish(e.target.value).replace(/[^\d]/g, '');
const numericValue = normalized ? Number(normalized) : undefined;
setAmountDisplay(normalized ? formatWithThousands(Number(normalized)) : '');
setValue('amount', numericValue as any, { shouldValidate: true, shouldDirty: true });
}}
error={errors.amount?.message}
placeholder="مثلاً 1000000"
placeholder="مثلاً 1,000,000"
numeric
/>
</div>