Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 212 additions & 4 deletions packages/web/src/app/(public)/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ import {
CardTitle,
} from '@/components/ui/card';
import { TableIcon, LayoutGrid, RefreshCw } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';

export default function AdminPage() {
const [adminToken, setAdminToken] = useState('');
Expand Down Expand Up @@ -56,11 +63,27 @@ export default function AdminPage() {
setIsTokenValid(true);
};

const [selectedUser, setSelectedUser] = useState<any | null>(null);
const [isUserDetailsOpen, setIsUserDetailsOpen] = useState(false);

const {
data: userDetails,
isLoading: isLoadingDetails,
refetch: refetchUserDetails,
} = api.admin.getUserDetails.useQuery(
{
adminToken,
privyDid: selectedUser?.privyDid || '',
},
{
enabled: !!selectedUser && isUserDetailsOpen && isTokenValid,
retry: false,
},
);
Comment on lines +69 to +82
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user details dialog doesn't handle error states from the tRPC query, potentially showing "No details available" for both failed requests and legitimately empty data.

View Details
📝 Patch Details
diff --git a/packages/web/src/app/(public)/admin/page.tsx b/packages/web/src/app/(public)/admin/page.tsx
index 81411614..9148031d 100644
--- a/packages/web/src/app/(public)/admin/page.tsx
+++ b/packages/web/src/app/(public)/admin/page.tsx
@@ -69,6 +69,7 @@ export default function AdminPage() {
   const {
     data: userDetails,
     isLoading: isLoadingDetails,
+    error: userDetailsError,
     refetch: refetchUserDetails,
   } = api.admin.getUserDetails.useQuery(
     {
@@ -221,6 +222,10 @@ export default function AdminPage() {
 
           {isLoadingDetails ? (
             <div className="py-8 text-center">Loading user details...</div>
+          ) : userDetailsError ? (
+            <div className="py-8 text-center text-red-600">
+              Error loading user details: {userDetailsError.message}
+            </div>
           ) : userDetails ? (
             <div className="space-y-6">
               <Card>

Analysis

Admin panel user details dialog lacks error handling for failed tRPC queries

What fails: api.admin.getUserDetails.useQuery() in admin page doesn't extract or handle error state, causing all failure scenarios to show generic "No details available" message

How to reproduce:

  1. Open admin panel and click on a user to view details
  2. Trigger any error condition (invalid admin token, network failure, server error)
  3. Dialog shows "No details available" instead of specific error message

Result: All error conditions (network failures, authorization errors, server errors) are indistinguishable from legitimate empty data cases

Expected: Show specific error messages when tRPC queries fail per TanStack Query error handling docs - useQuery returns error property for failed requests

Root cause: Query hook only destructures data, isLoading, refetch but omits error property. Conditional rendering only checks loading and data states.


const handleUserClick = (user: any) => {
// You can add logic here to show user details in a modal
// or navigate to a user detail page
console.log('User clicked:', user);
toast.info(`Selected user: ${user.email}`);
setSelectedUser(user);
setIsUserDetailsOpen(true);
};

const handleSyncKyc = async () => {
Expand Down Expand Up @@ -186,6 +209,191 @@ export default function AdminPage() {
/>
</TabsContent>
</Tabs>

<Dialog open={isUserDetailsOpen} onOpenChange={setIsUserDetailsOpen}>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>User Details</DialogTitle>
<DialogDescription>
Detailed information about {selectedUser?.email}
</DialogDescription>
</DialogHeader>

{isLoadingDetails ? (
<div className="py-8 text-center">Loading user details...</div>
) : userDetails ? (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">Basic Information</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="grid grid-cols-2 gap-2">
<div>
<strong>Email:</strong> {selectedUser?.email}
</div>
<div>
<strong>User Role:</strong>{' '}
{userDetails.user.userRole || 'N/A'}
</div>
<div>
<strong>First Name:</strong>{' '}
{userDetails.user.firstName || 'N/A'}
</div>
<div>
<strong>Last Name:</strong>{' '}
{userDetails.user.lastName || 'N/A'}
</div>
<div>
<strong>Company:</strong>{' '}
{userDetails.user.companyName || 'N/A'}
</div>
<div>
<strong>Beneficiary Type:</strong>{' '}
{userDetails.user.beneficiaryType || 'N/A'}
</div>
</div>
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle className="text-lg">Workspace</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
{userDetails.primaryWorkspace ? (
<>
<div>
<strong>Primary Workspace:</strong>{' '}
{userDetails.primaryWorkspace.name}
</div>
<div>
<strong>Workspace ID:</strong>{' '}
{userDetails.primaryWorkspace.id}
</div>
</>
) : (
<div className="text-muted-foreground">
No primary workspace
</div>
)}

{userDetails.workspaceMemberships.length > 0 && (
<div className="mt-4">
<strong>All Workspaces:</strong>
<ul className="list-disc list-inside mt-2 space-y-1">
{userDetails.workspaceMemberships.map((wm: any) => (
<li key={wm.workspaceId}>
{wm.workspaceName} ({wm.role})
{wm.isPrimary && ' - Primary'}
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle className="text-lg">Features</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div>
<strong>Savings Enabled:</strong>{' '}
<span
className={
userDetails.hasSavings
? 'text-green-600'
: 'text-red-600'
}
>
{userDetails.hasSavings ? 'Yes' : 'No'}
</span>
</div>

{userDetails.features.length > 0 ? (
<div className="mt-4">
<strong>All Features:</strong>
<ul className="list-disc list-inside mt-2 space-y-1">
{userDetails.features.map((f: any) => (
<li key={f.featureName}>
{f.featureName} -{' '}
{f.isActive ? 'Active' : 'Inactive'}
{f.purchaseSource && ` (${f.purchaseSource})`}
</li>
))}
</ul>
</div>
) : (
<div className="mt-2 text-muted-foreground">
No features enabled
</div>
)}
</CardContent>
</Card>

<Card>
<CardHeader>
<CardTitle className="text-lg">Account Status</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<div className="grid grid-cols-2 gap-2">
<div>
<strong>KYC Status:</strong>{' '}
<span
className={
userDetails.user.kycStatus === 'approved'
? 'text-green-600'
: userDetails.user.kycStatus === 'rejected'
? 'text-red-600'
: userDetails.user.kycStatus === 'pending'
? 'text-yellow-600'
: ''
}
>
{userDetails.user.kycStatus || 'N/A'}
</span>
</div>
<div>
<strong>KYC Provider:</strong>{' '}
{userDetails.user.kycProvider || 'N/A'}
</div>
<div>
<strong>Align Customer ID:</strong>{' '}
{userDetails.user.alignCustomerId || 'N/A'}
</div>
<div>
<strong>Virtual Account:</strong>{' '}
{userDetails.user.alignVirtualAccountId || 'N/A'}
</div>
<div>
<strong>Loops Synced:</strong>{' '}
{userDetails.user.loopsContactSynced ? 'Yes' : 'No'}
</div>
<div>
<strong>Created At:</strong>{' '}
{new Date(
userDetails.user.createdAt,
).toLocaleDateString()}
</div>
</div>
</CardContent>
</Card>

<div className="flex justify-end">
<Button onClick={() => refetchUserDetails()} size="sm">
Refresh Details
</Button>
</div>
</div>
) : (
<div className="py-8 text-center text-muted-foreground">
No details available
</div>
)}
</DialogContent>
</Dialog>
</div>
);
}
Loading