From 8d71350f6201145b96dd0f68833b8d2470ece9c1 Mon Sep 17 00:00:00 2001 From: hosseintaromi Date: Mon, 9 Feb 2026 13:17:13 +0330 Subject: [PATCH] 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. --- .../CustomerDiscountUsagePage.tsx | 84 +++++++++++++++---- .../DiscountUsageReportPage.tsx | 75 ++++++++++++++--- 2 files changed, 131 insertions(+), 28 deletions(-) diff --git a/src/pages/reports/discount-statistics/customer-discount-usage/CustomerDiscountUsagePage.tsx b/src/pages/reports/discount-statistics/customer-discount-usage/CustomerDiscountUsagePage.tsx index 00accfe..f04ea6a 100644 --- a/src/pages/reports/discount-statistics/customer-discount-usage/CustomerDiscountUsagePage.tsx +++ b/src/pages/reports/discount-statistics/customer-discount-usage/CustomerDiscountUsagePage.tsx @@ -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({ @@ -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(() => + 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); - } + 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 = () => {

- توجه: برای مشاهده گزارش، لطفاً شناسه کاربر را وارد کنید. این فیلد الزامی است. + توجه: برای مشاهده گزارش، کاربر را از لیست انتخاب کنید یا با شماره موبایل جستجو کنید. این فیلد الزامی است.

@@ -172,13 +189,44 @@ const CustomerDiscountUsagePage = () => { - handleNumericFilterChange('user_id', e.target.value)} - placeholder="مثلاً 456" - numeric - required - /> +
+ { + 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 && ( +
+ {(userSearchDebounced ? (userSearchData?.users ?? []) : (usersList ?? [])).length === 0 && ( +
+ کاربری یافت نشد +
+ )} + {(userSearchDebounced ? (userSearchData?.users ?? []) : (usersList ?? [])).map((user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => ( + + ))} +
+ )} +
diff --git a/src/pages/reports/discount-statistics/discount-usage-report/DiscountUsageReportPage.tsx b/src/pages/reports/discount-statistics/discount-usage-report/DiscountUsageReportPage.tsx index 115897a..9c69ce6 100644 --- a/src/pages/reports/discount-statistics/discount-usage-report/DiscountUsageReportPage.tsx +++ b/src/pages/reports/discount-statistics/discount-usage-report/DiscountUsageReportPage.tsx @@ -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({ @@ -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(() => + 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 = () => { - handleNumericFilterChange('user_id', e.target.value)} - placeholder="مثلاً 456" - numeric - /> +
+ { + 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 && ( +
+ {userOptions.length === 0 && ( +
+ کاربری یافت نشد +
+ )} + {userOptions.map((user: { id: number; phone_number?: string; first_name?: string; last_name?: string }) => ( + + ))} +
+ )} +