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 { useCustomerDiscountUsageReport } from '../core/_hooks';
|
||||||
import { CustomerDiscountUsageFilters } from '../core/_models';
|
import { CustomerDiscountUsageFilters } from '../core/_models';
|
||||||
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 { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
|
||||||
import { Table } from '@/components/ui/Table';
|
import { Table } from '@/components/ui/Table';
|
||||||
import { TableColumn } from '@/types';
|
import { TableColumn } from '@/types';
|
||||||
import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
|
import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
|
||||||
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
||||||
import { Pagination } from '@/components/ui/Pagination';
|
import { Pagination } from '@/components/ui/Pagination';
|
||||||
import { Filter, TrendingUp, Users, DollarSign, Hash, X } from 'lucide-react';
|
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 { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||||||
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
|
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 CustomerDiscountUsagePage = () => {
|
||||||
const [filters, setFilters] = useState<CustomerDiscountUsageFilters>({
|
const [filters, setFilters] = useState<CustomerDiscountUsageFilters>({
|
||||||
|
|
@ -29,6 +29,20 @@ const CustomerDiscountUsagePage = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const [refetchKey, setRefetchKey] = useState(0);
|
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(() => ({
|
const filtersWithKey = useMemo(() => ({
|
||||||
...filters,
|
...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 converted = persianToEnglish(raw).replace(/[^\d]/g, '');
|
||||||
const numeric = converted ? Number(converted) : undefined;
|
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 = () => {
|
const handleApplyFilters = () => {
|
||||||
|
|
@ -88,6 +104,7 @@ const CustomerDiscountUsagePage = () => {
|
||||||
};
|
};
|
||||||
setTempFilters(clearedFilters);
|
setTempFilters(clearedFilters);
|
||||||
setFilters(clearedFilters);
|
setFilters(clearedFilters);
|
||||||
|
setUserSearchText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: TableColumn[] = [
|
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">
|
<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">
|
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||||
<span className="font-semibold">توجه:</span> برای مشاهده گزارش، لطفاً شناسه کاربر را وارد کنید. این فیلد الزامی است.
|
<span className="font-semibold">توجه:</span> برای مشاهده گزارش، کاربر را از لیست انتخاب کنید یا با شماره موبایل جستجو کنید. این فیلد الزامی است.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -172,13 +189,44 @@ const CustomerDiscountUsagePage = () => {
|
||||||
<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>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
value={tempFilters.user_id?.toString() || ''}
|
value={userSearchText}
|
||||||
onChange={(e) => handleNumericFilterChange('user_id', e.target.value)}
|
onChange={(e) => {
|
||||||
placeholder="مثلاً 456"
|
setUserSearchText(persianToEnglish(e.target.value));
|
||||||
numeric
|
setUserDropdownOpen(true);
|
||||||
required
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo, useEffect } from 'react';
|
||||||
import { useDiscountUsageReport } from '../core/_hooks';
|
import { useDiscountUsageReport } from '../core/_hooks';
|
||||||
import { DiscountUsageFilters } from '../core/_models';
|
import { DiscountUsageFilters } from '../core/_models';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
|
@ -9,10 +9,11 @@ import { JalaliDateTimePicker } from '@/components/ui/JalaliDateTimePicker';
|
||||||
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
import { PageContainer, PageTitle } from '@/components/ui/Typography';
|
||||||
import { Pagination } from '@/components/ui/Pagination';
|
import { Pagination } from '@/components/ui/Pagination';
|
||||||
import { Filter, TrendingUp, Users, DollarSign, Hash, X } from 'lucide-react';
|
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 { formatCurrency, formatDateTime } from '@/utils/formatters';
|
||||||
import { ReportSkeleton } from '@/components/common/ReportSkeleton';
|
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 DiscountUsageReportPage = () => {
|
||||||
const [filters, setFilters] = useState<DiscountUsageFilters>({
|
const [filters, setFilters] = useState<DiscountUsageFilters>({
|
||||||
|
|
@ -21,6 +22,21 @@ const DiscountUsageReportPage = () => {
|
||||||
group_by_code: false,
|
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 { data, isLoading, error } = useDiscountUsageReport(filters);
|
||||||
|
|
||||||
const handleFilterChange = (key: keyof DiscountUsageFilters, value: any) => {
|
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 converted = persianToEnglish(raw).replace(/[^\d]/g, '');
|
||||||
const numeric = converted ? Number(converted) : undefined;
|
const numeric = converted ? Number(converted) : undefined;
|
||||||
handleFilterChange(key, numeric);
|
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) => {
|
const handlePageChange = (page: number) => {
|
||||||
setFilters(prev => ({
|
setFilters(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
|
|
@ -61,6 +83,7 @@ const DiscountUsageReportPage = () => {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
group_by_code: false,
|
group_by_code: false,
|
||||||
});
|
});
|
||||||
|
setUserSearchText('');
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: TableColumn[] = [
|
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 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
شناسه کاربر
|
شناسه کاربر
|
||||||
</label>
|
</label>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
value={filters.user_id?.toString() || ''}
|
value={userSearchText}
|
||||||
onChange={(e) => handleNumericFilterChange('user_id', e.target.value)}
|
onChange={(e) => {
|
||||||
placeholder="مثلاً 456"
|
setUserSearchText(persianToEnglish(e.target.value));
|
||||||
numeric
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue