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:
parent
e52ea39e60
commit
07fd4e1d2d
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "./_models";
|
} from "./_models";
|
||||||
|
|
||||||
export const getDiscountCodes = async (filters?: DiscountCodeFilters) => {
|
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?.page) queryParams.page = filters.page;
|
||||||
if (filters?.limit) queryParams.limit = filters.limit;
|
if (filters?.limit) queryParams.limit = filters.limit;
|
||||||
|
|
@ -25,7 +25,7 @@ export const getDiscountCodes = async (filters?: DiscountCodeFilters) => {
|
||||||
queryParams.application_level = filters.application_level;
|
queryParams.application_level = filters.application_level;
|
||||||
if (filters?.code) queryParams.code = filters.code;
|
if (filters?.code) queryParams.code = filters.code;
|
||||||
if (typeof filters?.active_only === "boolean")
|
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>(
|
const response = await httpGetRequest<PaginatedDiscountCodesResponse>(
|
||||||
APIUrlGenerator(API_ROUTES.GET_DISCOUNT_CODES, queryParams)
|
APIUrlGenerator(API_ROUTES.GET_DISCOUNT_CODES, queryParams)
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ const DiscountCodeFormPage = () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { register, handleSubmit, formState: { errors, isValid }, reset } = useForm<CreateDiscountCodeRequest>({
|
const { register, handleSubmit, formState: { errors, isValid }, reset } = useForm<CreateDiscountCodeRequest>({
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema as any),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
|
defaultValues: { status: 'active', type: 'percentage', application_level: 'invoice', single_use: false }
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -126,7 +126,7 @@ const DiscountCodesListPage = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{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 ? (
|
) : !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="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg">
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
|
|
|
||||||
|
|
@ -24,10 +24,12 @@ export const useOrder = (id: string) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useOrderStats = () => {
|
export const useOrderStats = (enabled: boolean = true) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [QUERY_KEYS.GET_ORDERS, "stats"],
|
queryKey: [QUERY_KEYS.GET_ORDERS, "stats"],
|
||||||
queryFn: getOrderStats,
|
queryFn: getOrderStats,
|
||||||
|
enabled,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -84,17 +84,37 @@ export interface Order {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrderFilters {
|
export interface OrderFilters {
|
||||||
|
// Pagination
|
||||||
page?: number;
|
page?: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|
||||||
|
// Filter Parameters
|
||||||
|
user_id?: number;
|
||||||
|
invoice_id?: number;
|
||||||
status?: OrderStatus;
|
status?: OrderStatus;
|
||||||
payment_status?: PaymentStatus;
|
payment_status?: PaymentStatus;
|
||||||
customer_id?: number;
|
customer_id?: number;
|
||||||
order_number?: string;
|
order_number?: string;
|
||||||
date_from?: string;
|
transaction_id?: string;
|
||||||
date_to?: string;
|
discount_code?: string;
|
||||||
min_amount?: number;
|
|
||||||
max_amount?: number;
|
// 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 {
|
export interface PaginatedOrdersResponse {
|
||||||
|
|
|
||||||
|
|
@ -10,23 +10,43 @@ import {
|
||||||
PaginatedOrdersResponse,
|
PaginatedOrdersResponse,
|
||||||
UpdateOrderStatusRequest,
|
UpdateOrderStatusRequest,
|
||||||
OrderStats,
|
OrderStats,
|
||||||
|
OrderStatus,
|
||||||
} from "./_models";
|
} from "./_models";
|
||||||
|
|
||||||
export const getOrders = async (filters?: OrderFilters) => {
|
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?.page) queryParams.page = filters.page;
|
||||||
if (filters?.limit) queryParams.limit = filters.limit;
|
if (filters?.limit) queryParams.limit = filters.limit;
|
||||||
if (filters?.offset) queryParams.offset = filters.offset;
|
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?.status) queryParams.status = filters.status;
|
||||||
if (filters?.payment_status)
|
if (filters?.payment_status) queryParams.payment_status = filters.payment_status;
|
||||||
queryParams.payment_status = filters.payment_status;
|
|
||||||
if (filters?.customer_id) queryParams.customer_id = filters.customer_id;
|
if (filters?.customer_id) queryParams.customer_id = filters.customer_id;
|
||||||
if (filters?.order_number) queryParams.order_number = filters.order_number;
|
if (filters?.order_number) queryParams.order_number = filters.order_number;
|
||||||
if (filters?.date_from) queryParams.date_from = filters.date_from;
|
if (filters?.transaction_id) queryParams.transaction_id = filters.transaction_id;
|
||||||
if (filters?.date_to) queryParams.date_to = filters.date_to;
|
if (filters?.discount_code) queryParams.discount_code = filters.discount_code;
|
||||||
if (filters?.min_amount) queryParams.min_amount = filters.min_amount;
|
|
||||||
if (filters?.max_amount) queryParams.max_amount = filters.max_amount;
|
// 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>(
|
const response = await httpGetRequest<PaginatedOrdersResponse>(
|
||||||
APIUrlGenerator(API_ROUTES.GET_ORDERS, queryParams)
|
APIUrlGenerator(API_ROUTES.GET_ORDERS, queryParams)
|
||||||
|
|
|
||||||
|
|
@ -67,10 +67,12 @@ const OrdersListPage = () => {
|
||||||
limit: 20,
|
limit: 20,
|
||||||
order_number: '',
|
order_number: '',
|
||||||
status: undefined,
|
status: undefined,
|
||||||
|
payment_status: undefined,
|
||||||
|
search: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: ordersData, isLoading, error } = useOrders(filters);
|
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 { mutate: updateStatus, isPending: isUpdating } = useUpdateOrderStatus();
|
||||||
|
|
||||||
const columns: TableColumn[] = useMemo(() => [
|
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="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">
|
<div className="relative">
|
||||||
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
<Search className="absolute right-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="شماره سفارش..."
|
placeholder="جستجو عمومی (شماره سفارش، کد تراکنش، کد تخفیف)..."
|
||||||
value={filters.order_number || ''}
|
value={filters.search || ''}
|
||||||
onChange={(e) => setFilters(prev => ({ ...prev, order_number: e.target.value, page: 1 }))}
|
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"
|
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>
|
</div>
|
||||||
|
|
@ -247,9 +249,24 @@ const OrdersListPage = () => {
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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
|
<Button
|
||||||
variant="secondary"
|
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"
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<Filter className="h-4 w-4" />
|
<Filter className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,13 @@ import {
|
||||||
|
|
||||||
// Get all users with pagination
|
// Get all users with pagination
|
||||||
export const getUsers = async (filters?: UserFilters): Promise<User[]> => {
|
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?.limit) queryParams.limit = filters.limit;
|
||||||
if (filters?.offset) queryParams.offset = filters.offset;
|
if (filters?.offset) queryParams.offset = filters.offset;
|
||||||
if (filters?.search_text) queryParams.search_text = filters.search_text;
|
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>(
|
const response = await httpGetRequest<PaginatedUsersResponse>(
|
||||||
APIUrlGenerator(API_ROUTES.GET_USERS, queryParams)
|
APIUrlGenerator(API_ROUTES.GET_USERS, queryParams)
|
||||||
|
|
@ -46,9 +47,10 @@ export const getUser = async (id: string): Promise<User> => {
|
||||||
export const searchUsers = async (
|
export const searchUsers = async (
|
||||||
filters: UserFilters
|
filters: UserFilters
|
||||||
): Promise<PaginatedUsersResponse> => {
|
): 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.search_text) queryParams.search_text = filters.search_text;
|
||||||
if (filters.phone_number) queryParams.phone_number = filters.phone_number;
|
if (filters.phone_number) queryParams.phone_number = filters.phone_number;
|
||||||
if (filters.email) queryParams.email = filters.email;
|
if (filters.email) queryParams.email = filters.email;
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ const updateUserSchema = yup.object({
|
||||||
verified: yup.boolean().required('وضعیت تأیید الزامی است'),
|
verified: yup.boolean().required('وضعیت تأیید الزامی است'),
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormData = CreateUserRequest | UpdateUserRequest;
|
type FormData = CreateUserRequest & Partial<UpdateUserRequest>;
|
||||||
|
|
||||||
const UserAdminFormPage: React.FC = () => {
|
const UserAdminFormPage: React.FC = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
@ -86,7 +86,7 @@ const UserAdminFormPage: React.FC = () => {
|
||||||
reset,
|
reset,
|
||||||
setValue,
|
setValue,
|
||||||
} = useForm<FormData>({
|
} = useForm<FormData>({
|
||||||
resolver: yupResolver(isEdit ? updateUserSchema : createUserSchema),
|
resolver: yupResolver(isEdit ? (updateUserSchema as any) : (createUserSchema as any)),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
verified: false,
|
verified: false,
|
||||||
|
|
|
||||||
|
|
@ -324,6 +324,8 @@ const UsersAdminListPage: React.FC = () => {
|
||||||
currentPage={Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1}
|
currentPage={Math.floor((filters.offset || 0) / (filters.limit || 20)) + 1}
|
||||||
totalPages={Math.ceil((stats?.total_users || 0) / (filters.limit || 20))}
|
totalPages={Math.ceil((stats?.total_users || 0) / (filters.limit || 20))}
|
||||||
onPageChange={(page) => handlePageChange((page - 1) * (filters.limit || 20))}
|
onPageChange={(page) => handlePageChange((page - 1) * (filters.limit || 20))}
|
||||||
|
itemsPerPage={filters.limit || 20}
|
||||||
|
totalItems={stats?.total_users || users.length}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue