feat(discount-reports): enhance user selection functionality in discount usage reports
- 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 user ID input to allow searching by phone number or selecting from a list. - Refactored filter handling to improve state management and clarity. These changes significantly enhance the usability and functionality of the discount usage reporting pages.
This commit is contained in:
parent
85fdf1f7a2
commit
8d71350f62
|
|
@ -1,19 +1,19 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useCustomerDiscountUsageReport } from '../core/_hooks';
|
||||
import { CustomerDiscountUsageFilters } from '../core/_models';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { Table } from '@/components/ui/Table';
|
||||
import { TableColumn } from '@/types';
|
||||
import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
|
||||
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { Filter, TrendingUp, Users, DollarSign, Hash, X } from 'lucide-react';
|
||||
import { formatWithThousands, parseFormattedNumber, persianToEnglish } from '@/utils/numberUtils';
|
||||
import { formatWithThousands, persianToEnglish } from '@/utils/numberUtils';
|
||||
import { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||||
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
|
||||
|
||||
import { useSearchUsers, useUsers } from '@/pages/users-admin/core/_hooks';
|
||||
import { UserFilters } from '@/pages/users-admin/core/_models';
|
||||
|
||||
const CustomerDiscountUsagePage = () => {
|
||||
const [filters, setFilters] = useState<CustomerDiscountUsageFilters>({
|
||||
|
|
@ -29,6 +29,20 @@ const CustomerDiscountUsagePage = () => {
|
|||
});
|
||||
|
||||
const [refetchKey, setRefetchKey] = useState(0);
|
||||
const [userSearchText, setUserSearchText] = useState('');
|
||||
const [userSearchDebounced, setUserSearchDebounced] = useState('');
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setUserSearchDebounced(userSearchText), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [userSearchText]);
|
||||
|
||||
const { data: usersList } = useUsers({ limit: 20, offset: 0 });
|
||||
const userSearchFilters = useMemo<UserFilters>(() =>
|
||||
userSearchDebounced ? { search_text: userSearchDebounced, limit: 20, offset: 0 } : {}, [userSearchDebounced]);
|
||||
const { data: userSearchData } = useSearchUsers(userSearchFilters);
|
||||
const userOptions = userSearchDebounced ? (userSearchData?.users || []) : (usersList || []);
|
||||
|
||||
const filtersWithKey = useMemo(() => ({
|
||||
...filters,
|
||||
|
|
@ -54,14 +68,16 @@ const CustomerDiscountUsagePage = () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const handleNumericFilterChange = (key: 'discount_id' | 'user_id', raw: string) => {
|
||||
const handleNumericFilterChange = (key: 'discount_id', raw: string) => {
|
||||
const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
|
||||
const numeric = converted ? Number(converted) : undefined;
|
||||
if (key === 'user_id') {
|
||||
handleTempFilterChange('user_id', numeric || 0);
|
||||
} else {
|
||||
handleTempFilterChange(key, numeric);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectUser = (user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => {
|
||||
handleTempFilterChange('user_id', user.id);
|
||||
setUserSearchText(user.phone_number || `${user.first_name || ''} ${user.last_name || ''}`.trim() || String(user.id));
|
||||
setUserDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleApplyFilters = () => {
|
||||
|
|
@ -88,6 +104,7 @@ const CustomerDiscountUsagePage = () => {
|
|||
};
|
||||
setTempFilters(clearedFilters);
|
||||
setFilters(clearedFilters);
|
||||
setUserSearchText('');
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
|
|
@ -163,7 +180,7 @@ const CustomerDiscountUsagePage = () => {
|
|||
|
||||
<div className="mb-4 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
<span className="font-semibold">توجه:</span> برای مشاهده گزارش، لطفاً شناسه کاربر را وارد کنید. این فیلد الزامی است.
|
||||
<span className="font-semibold">توجه:</span> برای مشاهده گزارش، کاربر را از لیست انتخاب کنید یا با شماره موبایل جستجو کنید. این فیلد الزامی است.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -172,13 +189,44 @@ const CustomerDiscountUsagePage = () => {
|
|||
<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
|
||||
value={tempFilters.user_id?.toString() || ''}
|
||||
onChange={(e) => handleNumericFilterChange('user_id', e.target.value)}
|
||||
placeholder="مثلاً 456"
|
||||
numeric
|
||||
required
|
||||
value={userSearchText}
|
||||
onChange={(e) => {
|
||||
setUserSearchText(persianToEnglish(e.target.value));
|
||||
setUserDropdownOpen(true);
|
||||
if (tempFilters.user_id) handleTempFilterChange('user_id', 0);
|
||||
}}
|
||||
onFocus={() => setUserDropdownOpen(true)}
|
||||
onBlur={() => setTimeout(() => setUserDropdownOpen(false), 150)}
|
||||
placeholder="جستجو با شماره موبایل یا انتخاب از لیست"
|
||||
/>
|
||||
{userDropdownOpen && (
|
||||
<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">
|
||||
{(userSearchDebounced ? (userSearchData?.users ?? []) : (usersList ?? [])).length === 0 && (
|
||||
<div className="p-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
کاربری یافت نشد
|
||||
</div>
|
||||
)}
|
||||
{(userSearchDebounced ? (userSearchData?.users ?? []) : (usersList ?? [])).map((user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => (
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { useDiscountUsageReport } from '../core/_hooks';
|
||||
import { DiscountUsageFilters } from '../core/_models';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
|
@ -9,10 +9,11 @@ import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
|
|||
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
||||
import { Pagination } from '@/components/ui/Pagination';
|
||||
import { Filter, TrendingUp, Users, DollarSign, Hash, X } from 'lucide-react';
|
||||
import { formatWithThousands, parseFormattedNumber, persianToEnglish } from '@/utils/numberUtils';
|
||||
import { formatWithThousands, persianToEnglish } from '@/utils/numberUtils';
|
||||
import { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||||
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
|
||||
|
||||
import { useSearchUsers, useUsers } from '@/pages/users-admin/core/_hooks';
|
||||
import { UserFilters } from '@/pages/users-admin/core/_models';
|
||||
|
||||
const DiscountUsageReportPage = () => {
|
||||
const [filters, setFilters] = useState<DiscountUsageFilters>({
|
||||
|
|
@ -21,6 +22,21 @@ const DiscountUsageReportPage = () => {
|
|||
group_by_code: false,
|
||||
});
|
||||
|
||||
const [userSearchText, setUserSearchText] = useState('');
|
||||
const [userSearchDebounced, setUserSearchDebounced] = useState('');
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => setUserSearchDebounced(userSearchText), 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [userSearchText]);
|
||||
|
||||
const { data: usersList } = useUsers({ limit: 20, offset: 0 });
|
||||
const userSearchFilters = useMemo<UserFilters>(() =>
|
||||
userSearchDebounced ? { search_text: userSearchDebounced, limit: 20, offset: 0 } : {}, [userSearchDebounced]);
|
||||
const { data: userSearchData } = useSearchUsers(userSearchFilters);
|
||||
const userOptions = userSearchDebounced ? (userSearchData?.users || []) : (usersList || []);
|
||||
|
||||
const { data, isLoading, error } = useDiscountUsageReport(filters);
|
||||
|
||||
const handleFilterChange = (key: keyof DiscountUsageFilters, value: any) => {
|
||||
|
|
@ -42,12 +58,18 @@ const DiscountUsageReportPage = () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const handleNumericFilterChange = (key: 'discount_id' | 'user_id', raw: string) => {
|
||||
const handleNumericFilterChange = (key: 'discount_id', raw: string) => {
|
||||
const converted = persianToEnglish(raw).replace(/[^\d]/g, '');
|
||||
const numeric = converted ? Number(converted) : undefined;
|
||||
handleFilterChange(key, numeric);
|
||||
};
|
||||
|
||||
const handleSelectUser = (user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => {
|
||||
handleFilterChange('user_id', user.id);
|
||||
setUserSearchText(user.phone_number || `${user.first_name || ''} ${user.last_name || ''}`.trim() || String(user.id));
|
||||
setUserDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setFilters(prev => ({
|
||||
...prev,
|
||||
|
|
@ -61,6 +83,7 @@ const DiscountUsageReportPage = () => {
|
|||
offset: 0,
|
||||
group_by_code: false,
|
||||
});
|
||||
setUserSearchText('');
|
||||
};
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
|
|
@ -169,12 +192,44 @@ const DiscountUsageReportPage = () => {
|
|||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
شناسه کاربر
|
||||
</label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
value={filters.user_id?.toString() || ''}
|
||||
onChange={(e) => handleNumericFilterChange('user_id', e.target.value)}
|
||||
placeholder="مثلاً 456"
|
||||
numeric
|
||||
value={userSearchText}
|
||||
onChange={(e) => {
|
||||
setUserSearchText(persianToEnglish(e.target.value));
|
||||
setUserDropdownOpen(true);
|
||||
if (filters.user_id) handleFilterChange('user_id', undefined);
|
||||
}}
|
||||
onFocus={() => setUserDropdownOpen(true)}
|
||||
onBlur={() => setTimeout(() => setUserDropdownOpen(false), 150)}
|
||||
placeholder="جستجو با شماره موبایل یا انتخاب از لیست"
|
||||
/>
|
||||
{userDropdownOpen && (
|
||||
<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">
|
||||
{userOptions.length === 0 && (
|
||||
<div className="p-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
کاربری یافت نشد
|
||||
</div>
|
||||
)}
|
||||
{userOptions.map((user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => (
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue