feat(orders): optimize API calls and add comprehensive filters

- Update OrderFilters interface to match complete API documentation
- Add all supported query parameters (user_id, invoice_id, transaction_id, discount_code, etc.)
- Optimize useOrderStats to prevent duplicate API calls by making it dependent on orders query
- Add payment status filter to orders list page
- Replace order number search with general search field supporting order numbers, transaction IDs, and discount codes
- Maintain backward compatibility with legacy filter names
- Add stale time to stats query for better caching
This commit is contained in:
hosseintaromi 2025-09-26 10:17:38 +03:30
parent e52ea39e60
commit 07fd4e1d2d
10 changed files with 91 additions and 28 deletions

View File

@ -15,7 +15,7 @@ import {
} from "./_models";
export const getDiscountCodes = async (filters?: DiscountCodeFilters) => {
const queryParams: Record<string, string | number | boolean | null> = {};
const queryParams: Record<string, string | number | null> = {};
if (filters?.page) queryParams.page = filters.page;
if (filters?.limit) queryParams.limit = filters.limit;
@ -25,7 +25,7 @@ export const getDiscountCodes = async (filters?: DiscountCodeFilters) => {
queryParams.application_level = filters.application_level;
if (filters?.code) queryParams.code = filters.code;
if (typeof filters?.active_only === "boolean")
queryParams.active_only = filters.active_only;
queryParams.active_only = filters.active_only ? "true" : "false";
const response = await httpGetRequest<PaginatedDiscountCodesResponse>(
APIUrlGenerator(API_ROUTES.GET_DISCOUNT_CODES, queryParams)

View File

@ -108,7 +108,7 @@ const DiscountCodeFormPage = () => {
}));
const { register, handleSubmit, formState: { errors, isValid }, reset } = useForm<CreateDiscountCodeRequest>({
resolver: yupResolver(schema),
resolver: yupResolver(schema as any),
mode: 'onChange',
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
});

View File

@ -126,7 +126,7 @@ const DiscountCodesListPage = () => {
</div>
{isLoading ? (
<Table columns={columns} data={discountCodes as any[] || []} loading={true} />
<Table columns={columns} data={Array.isArray(discountCodes) ? (discountCodes as any[]) : []} loading={true} />
) : !discountCodes || discountCodes.length === 0 ? (
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
<div className="text-center py-12">

View File

@ -24,10 +24,12 @@ export const useOrder = (id: string) => {
});
};
export const useOrderStats = () => {
export const useOrderStats = (enabled: boolean = true) => {
return useQuery({
queryKey: [QUERY_KEYS.GET_ORDERS, "stats"],
queryFn: getOrderStats,
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
});
};

View File

@ -84,17 +84,37 @@ export interface Order {
}
export interface OrderFilters {
// Pagination
page?: number;
limit?: number;
offset?: number;
// Filter Parameters
user_id?: number;
invoice_id?: number;
status?: OrderStatus;
payment_status?: PaymentStatus;
customer_id?: number;
order_number?: string;
date_from?: string;
date_to?: string;
min_amount?: number;
max_amount?: number;
transaction_id?: string;
discount_code?: string;
// Amount Range Parameters
min_total?: number;
max_total?: number;
min_amount?: number; // legacy support
max_amount?: number; // legacy support
// Date Range Parameters
created_from?: string;
created_to?: string;
updated_from?: string;
updated_to?: string;
date_from?: string; // legacy support
date_to?: string; // legacy support
// Search Parameter
search?: string;
}
export interface PaginatedOrdersResponse {

View File

@ -10,23 +10,43 @@ import {
PaginatedOrdersResponse,
UpdateOrderStatusRequest,
OrderStats,
OrderStatus,
} from "./_models";
export const getOrders = async (filters?: OrderFilters) => {
const queryParams: Record<string, string | number | boolean | null> = {};
const queryParams: Record<string, string | number | null> = {};
// Pagination
if (filters?.page) queryParams.page = filters.page;
if (filters?.limit) queryParams.limit = filters.limit;
if (filters?.offset) queryParams.offset = filters.offset;
// Filter Parameters
if (filters?.user_id) queryParams.user_id = filters.user_id;
if (filters?.invoice_id) queryParams.invoice_id = filters.invoice_id;
if (filters?.status) queryParams.status = filters.status;
if (filters?.payment_status)
queryParams.payment_status = filters.payment_status;
if (filters?.payment_status) queryParams.payment_status = filters.payment_status;
if (filters?.customer_id) queryParams.customer_id = filters.customer_id;
if (filters?.order_number) queryParams.order_number = filters.order_number;
if (filters?.date_from) queryParams.date_from = filters.date_from;
if (filters?.date_to) queryParams.date_to = filters.date_to;
if (filters?.min_amount) queryParams.min_amount = filters.min_amount;
if (filters?.max_amount) queryParams.max_amount = filters.max_amount;
if (filters?.transaction_id) queryParams.transaction_id = filters.transaction_id;
if (filters?.discount_code) queryParams.discount_code = filters.discount_code;
// Amount Range Parameters (prefer new API naming)
if (filters?.min_total) queryParams.min_total = filters.min_total;
if (filters?.max_total) queryParams.max_total = filters.max_total;
if (filters?.min_amount && !filters?.min_total) queryParams.min_total = filters.min_amount;
if (filters?.max_amount && !filters?.max_total) queryParams.max_total = filters.max_amount;
// Date Range Parameters (prefer new API naming)
if (filters?.created_from) queryParams.created_from = filters.created_from;
if (filters?.created_to) queryParams.created_to = filters.created_to;
if (filters?.updated_from) queryParams.updated_from = filters.updated_from;
if (filters?.updated_to) queryParams.updated_to = filters.updated_to;
if (filters?.date_from && !filters?.created_from) queryParams.created_from = filters.date_from;
if (filters?.date_to && !filters?.created_to) queryParams.created_to = filters.date_to;
// Search Parameter
if (filters?.search) queryParams.search = filters.search;
const response = await httpGetRequest<PaginatedOrdersResponse>(
APIUrlGenerator(API_ROUTES.GET_ORDERS, queryParams)

View File

@ -67,10 +67,12 @@ const OrdersListPage = () => {
limit: 20,
order_number: '',
status: undefined,
payment_status: undefined,
search: '',
});
const { data: ordersData, isLoading, error } = useOrders(filters);
const { data: stats, isLoading: statsLoading } = useOrderStats();
const { data: stats, isLoading: statsLoading } = useOrderStats(!isLoading);
const { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
const columns: TableColumn[] = useMemo(() => [
@ -219,14 +221,14 @@ const OrdersListPage = () => {
{/* فیلترها */}
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="relative">
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<input
type="text"
placeholder="شماره سفارش..."
value={filters.order_number || ''}
onChange={(e) => setFilters(prev => ({ ...prev, order_number: e.target.value, page: 1 }))}
placeholder="جستجو عمومی (شماره سفارش، کد تراکنش، کد تخفیف)..."
value={filters.search || ''}
onChange={(e) => setFilters(prev => ({ ...prev, search: e.target.value, page: 1 }))}
className="w-full pr-10 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
/>
</div>
@ -247,9 +249,24 @@ const OrdersListPage = () => {
</select>
</div>
<div>
<select
value={filters.payment_status || ''}
onChange={(e) => setFilters(prev => ({ ...prev, payment_status: e.target.value as any || undefined, page: 1 }))}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
>
<option value="">همه وضعیتهای پرداخت</option>
<option value="pending">در انتظار پرداخت</option>
<option value="paid">پرداخت شده</option>
<option value="failed">پرداخت ناموفق</option>
<option value="refunded">بازپرداخت شده</option>
<option value="cancelled">لغو شده</option>
</select>
</div>
<Button
variant="secondary"
onClick={() => setFilters({ page: 1, limit: 20, order_number: '', status: undefined })}
onClick={() => setFilters({ page: 1, limit: 20, order_number: '', status: undefined, payment_status: undefined, search: '' })}
className="flex items-center gap-2"
>
<Filter className="h-4 w-4" />

View File

@ -22,12 +22,13 @@ import {
// Get all users with pagination
export const getUsers = async (filters?: UserFilters): Promise<User[]> => {
const queryParams: Record<string, string | number | boolean | null> = {};
const queryParams: Record<string, string | number | null> = {};
if (filters?.limit) queryParams.limit = filters.limit;
if (filters?.offset) queryParams.offset = filters.offset;
if (filters?.search_text) queryParams.search_text = filters.search_text;
if (filters?.verified !== undefined) queryParams.verified = filters.verified;
if (filters?.verified !== undefined)
queryParams.verified = filters.verified ? "true" : "false";
const response = await httpGetRequest<PaginatedUsersResponse>(
APIUrlGenerator(API_ROUTES.GET_USERS, queryParams)
@ -46,9 +47,10 @@ export const getUser = async (id: string): Promise<User> => {
export const searchUsers = async (
filters: UserFilters
): Promise<PaginatedUsersResponse> => {
const queryParams: Record<string, string | number | boolean | null> = {};
const queryParams: Record<string, string | number | null> = {};
if (filters.verified !== undefined) queryParams.verified = filters.verified;
if (filters.verified !== undefined)
queryParams.verified = filters.verified ? "true" : "false";
if (filters.search_text) queryParams.search_text = filters.search_text;
if (filters.phone_number) queryParams.phone_number = filters.phone_number;
if (filters.email) queryParams.email = filters.email;

View File

@ -67,7 +67,7 @@ const updateUserSchema = yup.object({
verified: yup.boolean().required('وضعیت تأیید الزامی است'),
});
type FormData = CreateUserRequest | UpdateUserRequest;
type FormData = CreateUserRequest & Partial<UpdateUserRequest>;
const UserAdminFormPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@ -86,7 +86,7 @@ const UserAdminFormPage: React.FC = () => {
reset,
setValue,
} = useForm<FormData>({
resolver: yupResolver(isEdit ? updateUserSchema : createUserSchema),
resolver: yupResolver(isEdit ? (updateUserSchema as any) : (createUserSchema as any)),
mode: 'onChange',
defaultValues: {
verified: false,

View File

@ -324,6 +324,8 @@ const UsersAdminListPage: React.FC = () => {
currentPage={Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1}
totalPages={Math.ceil((stats?.total_users || 0) / (filters.limit || 20))}
onPageChange={(page) => handlePageChange((page - 1) * (filters.limit || 20))}
itemsPerPage={filters.limit || 20}
totalItems={stats?.total_users || users.length}
/>
)}