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:
parent
785f97b26d
commit
85fdf1f7a2
|
|
@ -1,15 +1,17 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
import * as yup from '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 { PageContainer, PageTitle } from '@/components/ui/Typography';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { useWalletCredit } from '../core/_hooks';
|
import { useWalletCredit } from '../core/_hooks';
|
||||||
import { WalletCreditRequest, WalletType } from '../core/_credit-models';
|
import { WalletCreditRequest, WalletType } from '../core/_credit-models';
|
||||||
import { formatCurrency } from '@/utils/formatters';
|
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({
|
const schema = yup.object({
|
||||||
user_id: yup.number().required('شناسه کاربر الزامی است').positive('شناسه کاربر باید عدد مثبت باشد'),
|
user_id: yup.number().required('شناسه کاربر الزامی است').positive('شناسه کاربر باید عدد مثبت باشد'),
|
||||||
|
|
@ -24,9 +26,14 @@ type FormData = yup.InferType<typeof schema>;
|
||||||
const WalletCreditPage = () => {
|
const WalletCreditPage = () => {
|
||||||
const { mutate: creditWallet, isPending } = useWalletCredit();
|
const { mutate: creditWallet, isPending } = useWalletCredit();
|
||||||
const [successData, setSuccessData] = useState<{ new_balance: number; message: string } | null>(null);
|
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 {
|
const {
|
||||||
register,
|
register,
|
||||||
|
setValue,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
reset,
|
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 onSubmit = (data: FormData) => {
|
||||||
const payload: WalletCreditRequest = {
|
const payload: WalletCreditRequest = {
|
||||||
user_id: data.user_id,
|
user_id: data.user_id,
|
||||||
|
|
@ -53,6 +98,7 @@ const WalletCreditPage = () => {
|
||||||
message: response.message,
|
message: response.message,
|
||||||
});
|
});
|
||||||
reset();
|
reset();
|
||||||
|
setAmountDisplay('');
|
||||||
setTimeout(() => setSuccessData(null), 5000);
|
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">
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
شناسه کاربر <span className="text-red-500">*</span>
|
شناسه کاربر <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<div className="relative">
|
||||||
type="number"
|
<Input
|
||||||
{...register('user_id', {
|
value={searchText}
|
||||||
setValueAs: (value) => (value === '' ? undefined : Number(persianToEnglish(value))),
|
onChange={(e) => {
|
||||||
})}
|
const normalized = persianToEnglish(e.target.value).replace(/[^\d]/g, '');
|
||||||
error={errors.user_id?.message}
|
setSearchText(normalized);
|
||||||
placeholder="مثلاً 52"
|
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="جستجو با شماره موبایل"
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -114,11 +201,15 @@ const WalletCreditPage = () => {
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
{...register('amount', {
|
value={amountDisplay}
|
||||||
setValueAs: (value) => (value === '' ? undefined : Number(persianToEnglish(value))),
|
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}
|
error={errors.amount?.message}
|
||||||
placeholder="مثلاً 1000000"
|
placeholder="مثلاً 1,000,000"
|
||||||
numeric
|
numeric
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue