Skip to content

Commit eedfad6

Browse files
feat: add user details dialog to admin panel (#285)
- Add getUserDetails tRPC procedure to fetch user info, workspace, and features - Show savings enabled status with color indicator - Display primary workspace name and memberships - Include user role, features list, and account status
1 parent f5deb4f commit eedfad6

File tree

2 files changed

+689
-153
lines changed

2 files changed

+689
-153
lines changed

packages/web/src/app/(public)/admin/page.tsx

Lines changed: 212 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ import {
1616
CardTitle,
1717
} from '@/components/ui/card';
1818
import { TableIcon, LayoutGrid, RefreshCw } from 'lucide-react';
19+
import {
20+
Dialog,
21+
DialogContent,
22+
DialogDescription,
23+
DialogHeader,
24+
DialogTitle,
25+
} from '@/components/ui/dialog';
1926

2027
export default function AdminPage() {
2128
const [adminToken, setAdminToken] = useState('');
@@ -56,11 +63,27 @@ export default function AdminPage() {
5663
setIsTokenValid(true);
5764
};
5865

66+
const [selectedUser, setSelectedUser] = useState<any | null>(null);
67+
const [isUserDetailsOpen, setIsUserDetailsOpen] = useState(false);
68+
69+
const {
70+
data: userDetails,
71+
isLoading: isLoadingDetails,
72+
refetch: refetchUserDetails,
73+
} = api.admin.getUserDetails.useQuery(
74+
{
75+
adminToken,
76+
privyDid: selectedUser?.privyDid || '',
77+
},
78+
{
79+
enabled: !!selectedUser && isUserDetailsOpen && isTokenValid,
80+
retry: false,
81+
},
82+
);
83+
5984
const handleUserClick = (user: any) => {
60-
// You can add logic here to show user details in a modal
61-
// or navigate to a user detail page
62-
console.log('User clicked:', user);
63-
toast.info(`Selected user: ${user.email}`);
85+
setSelectedUser(user);
86+
setIsUserDetailsOpen(true);
6487
};
6588

6689
const handleSyncKyc = async () => {
@@ -186,6 +209,191 @@ export default function AdminPage() {
186209
/>
187210
</TabsContent>
188211
</Tabs>
212+
213+
<Dialog open={isUserDetailsOpen} onOpenChange={setIsUserDetailsOpen}>
214+
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
215+
<DialogHeader>
216+
<DialogTitle>User Details</DialogTitle>
217+
<DialogDescription>
218+
Detailed information about {selectedUser?.email}
219+
</DialogDescription>
220+
</DialogHeader>
221+
222+
{isLoadingDetails ? (
223+
<div className="py-8 text-center">Loading user details...</div>
224+
) : userDetails ? (
225+
<div className="space-y-6">
226+
<Card>
227+
<CardHeader>
228+
<CardTitle className="text-lg">Basic Information</CardTitle>
229+
</CardHeader>
230+
<CardContent className="space-y-2 text-sm">
231+
<div className="grid grid-cols-2 gap-2">
232+
<div>
233+
<strong>Email:</strong> {selectedUser?.email}
234+
</div>
235+
<div>
236+
<strong>User Role:</strong>{' '}
237+
{userDetails.user.userRole || 'N/A'}
238+
</div>
239+
<div>
240+
<strong>First Name:</strong>{' '}
241+
{userDetails.user.firstName || 'N/A'}
242+
</div>
243+
<div>
244+
<strong>Last Name:</strong>{' '}
245+
{userDetails.user.lastName || 'N/A'}
246+
</div>
247+
<div>
248+
<strong>Company:</strong>{' '}
249+
{userDetails.user.companyName || 'N/A'}
250+
</div>
251+
<div>
252+
<strong>Beneficiary Type:</strong>{' '}
253+
{userDetails.user.beneficiaryType || 'N/A'}
254+
</div>
255+
</div>
256+
</CardContent>
257+
</Card>
258+
259+
<Card>
260+
<CardHeader>
261+
<CardTitle className="text-lg">Workspace</CardTitle>
262+
</CardHeader>
263+
<CardContent className="space-y-2 text-sm">
264+
{userDetails.primaryWorkspace ? (
265+
<>
266+
<div>
267+
<strong>Primary Workspace:</strong>{' '}
268+
{userDetails.primaryWorkspace.name}
269+
</div>
270+
<div>
271+
<strong>Workspace ID:</strong>{' '}
272+
{userDetails.primaryWorkspace.id}
273+
</div>
274+
</>
275+
) : (
276+
<div className="text-muted-foreground">
277+
No primary workspace
278+
</div>
279+
)}
280+
281+
{userDetails.workspaceMemberships.length > 0 && (
282+
<div className="mt-4">
283+
<strong>All Workspaces:</strong>
284+
<ul className="list-disc list-inside mt-2 space-y-1">
285+
{userDetails.workspaceMemberships.map((wm: any) => (
286+
<li key={wm.workspaceId}>
287+
{wm.workspaceName} ({wm.role})
288+
{wm.isPrimary && ' - Primary'}
289+
</li>
290+
))}
291+
</ul>
292+
</div>
293+
)}
294+
</CardContent>
295+
</Card>
296+
297+
<Card>
298+
<CardHeader>
299+
<CardTitle className="text-lg">Features</CardTitle>
300+
</CardHeader>
301+
<CardContent className="space-y-2 text-sm">
302+
<div>
303+
<strong>Savings Enabled:</strong>{' '}
304+
<span
305+
className={
306+
userDetails.hasSavings
307+
? 'text-green-600'
308+
: 'text-red-600'
309+
}
310+
>
311+
{userDetails.hasSavings ? 'Yes' : 'No'}
312+
</span>
313+
</div>
314+
315+
{userDetails.features.length > 0 ? (
316+
<div className="mt-4">
317+
<strong>All Features:</strong>
318+
<ul className="list-disc list-inside mt-2 space-y-1">
319+
{userDetails.features.map((f: any) => (
320+
<li key={f.featureName}>
321+
{f.featureName} -{' '}
322+
{f.isActive ? 'Active' : 'Inactive'}
323+
{f.purchaseSource && ` (${f.purchaseSource})`}
324+
</li>
325+
))}
326+
</ul>
327+
</div>
328+
) : (
329+
<div className="mt-2 text-muted-foreground">
330+
No features enabled
331+
</div>
332+
)}
333+
</CardContent>
334+
</Card>
335+
336+
<Card>
337+
<CardHeader>
338+
<CardTitle className="text-lg">Account Status</CardTitle>
339+
</CardHeader>
340+
<CardContent className="space-y-2 text-sm">
341+
<div className="grid grid-cols-2 gap-2">
342+
<div>
343+
<strong>KYC Status:</strong>{' '}
344+
<span
345+
className={
346+
userDetails.user.kycStatus === 'approved'
347+
? 'text-green-600'
348+
: userDetails.user.kycStatus === 'rejected'
349+
? 'text-red-600'
350+
: userDetails.user.kycStatus === 'pending'
351+
? 'text-yellow-600'
352+
: ''
353+
}
354+
>
355+
{userDetails.user.kycStatus || 'N/A'}
356+
</span>
357+
</div>
358+
<div>
359+
<strong>KYC Provider:</strong>{' '}
360+
{userDetails.user.kycProvider || 'N/A'}
361+
</div>
362+
<div>
363+
<strong>Align Customer ID:</strong>{' '}
364+
{userDetails.user.alignCustomerId || 'N/A'}
365+
</div>
366+
<div>
367+
<strong>Virtual Account:</strong>{' '}
368+
{userDetails.user.alignVirtualAccountId || 'N/A'}
369+
</div>
370+
<div>
371+
<strong>Loops Synced:</strong>{' '}
372+
{userDetails.user.loopsContactSynced ? 'Yes' : 'No'}
373+
</div>
374+
<div>
375+
<strong>Created At:</strong>{' '}
376+
{new Date(
377+
userDetails.user.createdAt,
378+
).toLocaleDateString()}
379+
</div>
380+
</div>
381+
</CardContent>
382+
</Card>
383+
384+
<div className="flex justify-end">
385+
<Button onClick={() => refetchUserDetails()} size="sm">
386+
Refresh Details
387+
</Button>
388+
</div>
389+
</div>
390+
) : (
391+
<div className="py-8 text-center text-muted-foreground">
392+
No details available
393+
</div>
394+
)}
395+
</DialogContent>
396+
</Dialog>
189397
</div>
190398
);
191399
}

0 commit comments

Comments
 (0)