From 7693de456cb4532845666b6321c2bd2d90e6dc6d Mon Sep 17 00:00:00 2001 From: Saumya Date: Thu, 9 Oct 2025 16:07:23 +0530 Subject: [PATCH] show paginated details in admin dialog --- backend/core/admin/admin_api.py | 72 +++++++ backend/core/admin/billing_admin_api.py | 77 +++++--- .../admin/admin-user-details-dialog.tsx | 178 +++++++++++++----- .../react-query/admin/use-admin-billing.ts | 27 ++- .../react-query/admin/use-admin-users.ts | 28 +++ 5 files changed, 304 insertions(+), 78 deletions(-) diff --git a/backend/core/admin/admin_api.py b/backend/core/admin/admin_api.py index 368076b4..f3443bf0 100644 --- a/backend/core/admin/admin_api.py +++ b/backend/core/admin/admin_api.py @@ -314,6 +314,78 @@ async def get_user_stats_overview( logger.error(f"Failed to get user stats: {e}", exc_info=True) raise HTTPException(status_code=500, detail="Failed to retrieve user statistics") +@router.get("/users/{user_id}/activity") +async def get_user_activity( + user_id: str, + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(20, ge=1, le=100, description="Items per page"), + status_filter: Optional[str] = Query(None, description="Filter by status"), + admin: dict = Depends(require_admin) +): + """Get paginated activity (agent runs) for a specific user.""" + try: + db = DBConnection() + client = await db.client + + pagination_params = PaginationParams(page=page, page_size=page_size) + + # Build base query for agent runs with thread info + base_query = client.from_('agent_runs').select( + '*, threads!inner(account_id, thread_id)' + ).eq('threads.account_id', user_id) + + if status_filter: + base_query = base_query.eq('status', status_filter) + + # Get total count + count_result = await client.from_('agent_runs').select( + 'id, threads!inner(account_id)', count='exact' + ).eq('threads.account_id', user_id).execute() + + total_count = count_result.count or 0 + + # Get paginated activity + offset = (pagination_params.page - 1) * pagination_params.page_size + activity_query = client.from_('agent_runs').select( + '*, threads!inner(account_id, thread_id)' + ).eq('threads.account_id', user_id) + + if status_filter: + activity_query = activity_query.eq('status', status_filter) + + activity_result = await activity_query.order('created_at', desc=True).range( + offset, offset + pagination_params.page_size - 1 + ).execute() + + # Format activity data + activities = [] + for run in activity_result.data or []: + thread = run.get('threads', {}) + + activities.append({ + 'id': run.get('id'), + 'created_at': run.get('created_at'), + 'updated_at': run.get('updated_at'), + 'status': run.get('status'), + 'thread_id': run.get('thread_id'), + 'thread_name': f"Thread {run.get('thread_id', '').split('-')[0] if run.get('thread_id') else 'Unknown'}", + 'agent_id': run.get('agent_id'), + 'agent_name': 'Agent', # We'll need to fetch agent names separately if needed + 'credit_cost': float(run.get('credit_cost', 0) if run.get('credit_cost') else 0), + 'error': run.get('error'), + 'duration_ms': run.get('duration_ms') + }) + + return await PaginationService.paginate_with_total_count( + items=activities, + total_count=total_count, + params=pagination_params + ) + + except Exception as e: + logger.error(f"Failed to get user activity: {e}") + raise HTTPException(status_code=500, detail="Failed to retrieve user activity") + @router.get("/users/threads/by-email") async def get_user_threads_by_email( email: str = Query(..., description="User email to fetch threads for"), diff --git a/backend/core/admin/billing_admin_api.py b/backend/core/admin/billing_admin_api.py index 37d18125..e6988273 100644 --- a/backend/core/admin/billing_admin_api.py +++ b/backend/core/admin/billing_admin_api.py @@ -4,7 +4,7 @@ Handles all administrative billing operations: credits, refunds, transactions. User search has been moved to admin_api.py as it's user-focused, not billing-focused. """ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Query from typing import Optional, List from decimal import Decimal from datetime import datetime, timezone, timedelta @@ -185,31 +185,60 @@ async def get_user_billing_summary( @router.get("/user/{account_id}/transactions") async def get_user_transactions( account_id: str, - limit: int = 100, - offset: int = 0, + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(20, ge=1, le=100, description="Items per page"), type_filter: Optional[str] = None, admin: dict = Depends(require_admin) ): - """Get transaction history for a specific user.""" - db = DBConnection() - client = await db.client - - query = client.from_('credit_ledger').select('*').eq('account_id', account_id).order('created_at', desc=True) - - if type_filter: - query = query.eq('type', type_filter) - - if offset: - query = query.range(offset, offset + limit - 1) - else: - query = query.limit(limit) - - transactions_result = await query.execute() - - return { - 'account_id': account_id, - 'transactions': transactions_result.data or [], - 'count': len(transactions_result.data or []) - } + try: + from core.utils.pagination import PaginationService, PaginationParams + + db = DBConnection() + client = await db.client + + pagination_params = PaginationParams(page=page, page_size=page_size) + + # Get total count + count_query = client.from_('credit_ledger').select('*', count='exact').eq('account_id', account_id) + if type_filter: + count_query = count_query.eq('type', type_filter) + count_result = await count_query.execute() + total_count = count_result.count or 0 + + # Get paginated transactions + offset = (pagination_params.page - 1) * pagination_params.page_size + transactions_query = client.from_('credit_ledger').select('*').eq('account_id', account_id) + + if type_filter: + transactions_query = transactions_query.eq('type', type_filter) + + transactions_result = await transactions_query.order('created_at', desc=True).range( + offset, offset + pagination_params.page_size - 1 + ).execute() + + # Format transactions + transactions = [] + for tx in transactions_result.data or []: + transactions.append({ + 'id': tx.get('id'), + 'created_at': tx.get('created_at'), + 'amount': float(tx.get('amount', 0)), + 'balance_after': float(tx.get('balance_after', 0)), + 'type': tx.get('type'), + 'description': tx.get('description'), + 'is_expiring': tx.get('is_expiring', False), + 'expires_at': tx.get('expires_at'), + 'metadata': tx.get('metadata', {}) + }) + + return await PaginationService.paginate_with_total_count( + items=transactions, + total_count=total_count, + params=pagination_params + ) + + except Exception as e: + logger.error(f"Failed to get user transactions: {e}") + raise HTTPException(status_code=500, detail="Failed to retrieve transactions") diff --git a/frontend/src/components/admin/admin-user-details-dialog.tsx b/frontend/src/components/admin/admin-user-details-dialog.tsx index 2b26c6d1..3a65c909 100644 --- a/frontend/src/components/admin/admin-user-details-dialog.tsx +++ b/frontend/src/components/admin/admin-user-details-dialog.tsx @@ -40,11 +40,12 @@ import { MessageSquare, ExternalLink, } from 'lucide-react'; -import { useAdminUserDetails, useAdminUserThreads } from '@/hooks/react-query/admin/use-admin-users'; +import { useAdminUserDetails, useAdminUserThreads, useAdminUserActivity } from '@/hooks/react-query/admin/use-admin-users'; import { useUserBillingSummary, useAdjustCredits, useProcessRefund, + useAdminUserTransactions, } from '@/hooks/react-query/admin/use-admin-billing'; import type { UserSummary } from '@/hooks/react-query/admin/use-admin-users'; @@ -68,6 +69,8 @@ export function AdminUserDetailsDialog({ const [adjustIsExpiring, setAdjustIsExpiring] = useState(true); const [refundIsExpiring, setRefundIsExpiring] = useState(false); const [threadsPage, setThreadsPage] = useState(1); + const [transactionsPage, setTransactionsPage] = useState(1); + const [activityPage, setActivityPage] = useState(1); const { data: userDetails, isLoading } = useAdminUserDetails(user?.id || null); const { data: billingSummary, refetch: refetchBilling } = useUserBillingSummary(user?.id || null); @@ -76,6 +79,16 @@ export function AdminUserDetailsDialog({ page: threadsPage, page_size: 10, }); + const { data: userTransactions, isLoading: transactionsLoading } = useAdminUserTransactions({ + userId: user?.id || '', + page: transactionsPage, + page_size: 10, + }); + const { data: userActivity, isLoading: activityLoading } = useAdminUserActivity({ + userId: user?.id || '', + page: activityPage, + page_size: 10, + }); const adjustCreditsMutation = useAdjustCredits(); const processRefundMutation = useProcessRefund(); @@ -385,40 +398,73 @@ export function AdminUserDetailsDialog({ - {billingSummary && ( - - + + + + + Transactions + + + + {transactionsLoading ? (
- {billingSummary.recent_transactions?.length > 0 ? ( - billingSummary.recent_transactions.map((transaction: any) => ( -
-
-

{transaction.description}

-

- {formatDate(transaction.created_at)} -

-
-
-

- {transaction.amount > 0 ? '+' : ''} - {formatCurrency(Math.abs(transaction.amount))} -

-

- Balance: {formatCurrency(transaction.balance_after)} -

-
+ {[...Array(3)].map((_, i) => ( + + ))} +
+ ) : userTransactions && userTransactions.data?.length > 0 ? ( +
+ {userTransactions.data.map((transaction: any) => ( +
+
+

{transaction.description}

+

+ {formatDate(transaction.created_at)} +

- )) - ) : ( -

No recent transactions

+
+

+ {transaction.amount > 0 ? '+' : ''} + {formatCurrency(Math.abs(transaction.amount))} +

+

+ Balance: {formatCurrency(transaction.balance_after)} +

+
+
+ ))} + {userTransactions.pagination && userTransactions.pagination.total_pages > 1 && ( +
+ + + Page {userTransactions.pagination.page} of {userTransactions.pagination.total_pages} + + +
)}
- - - )} + ) : ( +

No transactions found

+ )} + + @@ -426,33 +472,77 @@ export function AdminUserDetailsDialog({ - Recent Activity + Activity - {userDetails?.recent_activity?.length > 0 ? ( + {activityLoading ? (
- {userDetails.recent_activity.map((activity) => ( + {[...Array(3)].map((_, i) => ( + + ))} +
+ ) : userActivity && userActivity.data?.length > 0 ? ( +
+ {userActivity.data.map((activity: any) => (
-
-

Agent Run

-

- {formatDate(activity.created_at)} • Thread {activity.thread_id.slice(-8)} +

+
+

{activity.agent_name}

+ + {activity.status} + +
+

+ {formatDate(activity.created_at)} • Thread: {activity.thread_name || activity.thread_id.slice(-8)}

+ {activity.error && ( +

+ Error: {activity.error} +

+ )}
- - {activity.status} - + {activity.credit_cost > 0 && ( +
+

+ {formatCurrency(activity.credit_cost)} +

+
+ )}
))} + {userActivity.pagination && userActivity.pagination.total_pages > 1 && ( +
+ + + Page {userActivity.pagination.page} of {userActivity.pagination.total_pages} + + +
+ )}
) : ( -

No recent activity

+

No activity found

)} diff --git a/frontend/src/hooks/react-query/admin/use-admin-billing.ts b/frontend/src/hooks/react-query/admin/use-admin-billing.ts index 69d6ffc6..40ad4c41 100644 --- a/frontend/src/hooks/react-query/admin/use-admin-billing.ts +++ b/frontend/src/hooks/react-query/admin/use-admin-billing.ts @@ -33,24 +33,31 @@ export function useUserBillingSummary(userId: string | null) { }); } -export function useUserTransactions(userId: string | null, limit = 100, offset = 0, typeFilter?: string) { +interface TransactionParams { + userId: string; + page?: number; + page_size?: number; + type_filter?: string; +} + +export function useAdminUserTransactions(params: TransactionParams) { return useQuery({ - queryKey: ['admin', 'billing', 'transactions', userId, limit, offset, typeFilter], + queryKey: ['admin', 'billing', 'transactions', params.userId, params.page, params.page_size, params.type_filter], queryFn: async () => { - if (!userId) return null; - const params = new URLSearchParams({ - limit: limit.toString(), - offset: offset.toString(), - }); - if (typeFilter) params.append('type_filter', typeFilter); + const searchParams = new URLSearchParams(); - const response = await backendApi.get(`/admin/billing/user/${userId}/transactions?${params}`); + if (params.page) searchParams.append('page', params.page.toString()); + if (params.page_size) searchParams.append('page_size', params.page_size.toString()); + if (params.type_filter) searchParams.append('type_filter', params.type_filter); + + const response = await backendApi.get(`/admin/billing/user/${params.userId}/transactions?${searchParams.toString()}`); if (response.error) { throw new Error(response.error.message); } return response.data; }, - enabled: !!userId, + enabled: !!params.userId, + staleTime: 30000, }); } diff --git a/frontend/src/hooks/react-query/admin/use-admin-users.ts b/frontend/src/hooks/react-query/admin/use-admin-users.ts index 4f81d841..e29c61b9 100644 --- a/frontend/src/hooks/react-query/admin/use-admin-users.ts +++ b/frontend/src/hooks/react-query/admin/use-admin-users.ts @@ -184,6 +184,34 @@ export function useAdminUserThreads(params: UserThreadsParams) { }); } +interface UserActivityParams { + userId: string; + page?: number; + page_size?: number; + status_filter?: string; +} + +export function useAdminUserActivity(params: UserActivityParams) { + return useQuery({ + queryKey: ['admin', 'users', 'activity', params.userId, params.page, params.page_size, params.status_filter], + queryFn: async () => { + const searchParams = new URLSearchParams(); + + if (params.page) searchParams.append('page', params.page.toString()); + if (params.page_size) searchParams.append('page_size', params.page_size.toString()); + if (params.status_filter) searchParams.append('status_filter', params.status_filter); + + const response = await backendApi.get(`/admin/users/${params.userId}/activity?${searchParams.toString()}`); + if (response.error) { + throw new Error(response.error.message); + } + return response.data; + }, + enabled: !!params.userId, + staleTime: 30000, + }); +} + export function useRefreshUserData() { const queryClient = useQueryClient();