Skip to content

Commit 7449649

Browse files
author
Lasim
committed
feat: add user detail view and navigation from users list
1 parent e5c3671 commit 7449649

File tree

4 files changed

+240
-32
lines changed

4 files changed

+240
-32
lines changed

services/frontend/src/router/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,12 @@ const routes = [
108108
{
109109
path: 'users',
110110
name: 'AdminUsers',
111-
component: () => import('../views/admin/Users.vue'), // Assuming the new component will be in views/admin/
111+
component: () => import('../views/admin/Users.vue'),
112+
},
113+
{
114+
path: 'users/:id',
115+
name: 'AdminUserDetail',
116+
component: () => import('../views/admin/UserDetail.vue'),
112117
},
113118
],
114119
},
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, computed } from 'vue'
3+
import { useRoute, useRouter } from 'vue-router'
4+
import { useI18n } from 'vue-i18n'
5+
import { Button } from '@/components/ui/button'
6+
import {
7+
Card,
8+
CardContent,
9+
CardDescription,
10+
CardHeader,
11+
CardTitle,
12+
} from '@/components/ui/card'
13+
import { Label } from '@/components/ui/label'
14+
import { Badge } from '@/components/ui/badge'
15+
import { ArrowLeft, Mail, Github } from 'lucide-vue-next'
16+
import DashboardLayout from '@/components/DashboardLayout.vue'
17+
import { getEnv } from '@/utils/env'
18+
import type { User } from './users/types'
19+
20+
const { t } = useI18n()
21+
const route = useRoute()
22+
const router = useRouter()
23+
24+
const user = ref<User | null>(null)
25+
const isLoading = ref(true)
26+
const error = ref<string | null>(null)
27+
28+
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL') || ''
29+
const userId = route.params.id as string
30+
31+
// Fetch user details from API
32+
async function fetchUser(id: string): Promise<User> {
33+
if (!apiUrl) {
34+
throw new Error('VITE_DEPLOYSTACK_BACKEND_URL is not configured.')
35+
}
36+
37+
const response = await fetch(`${apiUrl}/api/users/${id}`, {
38+
credentials: 'include'
39+
})
40+
41+
if (!response.ok) {
42+
const errorData = await response.json().catch(() => ({}))
43+
throw new Error(errorData.error || `Failed to fetch user: ${response.statusText} (status: ${response.status})`)
44+
}
45+
46+
return await response.json()
47+
}
48+
49+
// Load user on component mount
50+
onMounted(async () => {
51+
try {
52+
isLoading.value = true
53+
user.value = await fetchUser(userId)
54+
error.value = null
55+
} catch (err) {
56+
error.value = err instanceof Error ? err.message : 'An unknown error occurred'
57+
user.value = null
58+
} finally {
59+
isLoading.value = false
60+
}
61+
})
62+
63+
// Computed properties for display
64+
const displayName = computed(() => {
65+
if (!user.value) return ''
66+
const firstName = user.value.first_name || ''
67+
const lastName = user.value.last_name || ''
68+
const fullName = `${firstName} ${lastName}`.trim()
69+
return fullName || user.value.username
70+
})
71+
72+
const authTypeBadge = computed(() => {
73+
if (!user.value) return null
74+
const isEmail = user.value.auth_type === 'email_signup'
75+
return {
76+
variant: (isEmail ? 'default' : 'secondary') as 'default' | 'secondary',
77+
icon: isEmail ? Mail : Github,
78+
text: isEmail ? 'Email' : 'GitHub'
79+
}
80+
})
81+
82+
const goBack = () => {
83+
router.push('/admin/users')
84+
}
85+
</script>
86+
87+
<template>
88+
<DashboardLayout :title="`User: ${user?.username || 'Loading...'}`">
89+
<div class="space-y-6">
90+
<!-- Back Button -->
91+
<div>
92+
<Button
93+
variant="outline"
94+
@click="goBack"
95+
class="mb-4"
96+
>
97+
<ArrowLeft class="h-4 w-4 mr-2" />
98+
Back to Users
99+
</Button>
100+
</div>
101+
102+
<!-- Loading State -->
103+
<div v-if="isLoading" class="text-muted-foreground">
104+
Loading user details...
105+
</div>
106+
107+
<!-- Error State -->
108+
<div v-else-if="error" class="text-red-500">
109+
Error loading user: {{ error }}
110+
</div>
111+
112+
<!-- User Details -->
113+
<div v-else-if="user" class="space-y-6">
114+
<Card>
115+
<CardHeader>
116+
<CardTitle>Profile</CardTitle>
117+
<CardDescription>
118+
User information and account details.
119+
</CardDescription>
120+
</CardHeader>
121+
<CardContent class="space-y-6">
122+
<!-- Username -->
123+
<div class="space-y-2">
124+
<Label>Username</Label>
125+
<div class="text-sm font-medium">{{ user.username }}</div>
126+
</div>
127+
128+
<!-- Email -->
129+
<div class="space-y-2">
130+
<Label>Email</Label>
131+
<div class="text-sm">{{ user.email }}</div>
132+
</div>
133+
134+
<!-- Full Name -->
135+
<div class="space-y-2">
136+
<Label>Full Name</Label>
137+
<div class="text-sm">
138+
{{ displayName === user.username ? 'Not provided' : displayName }}
139+
</div>
140+
</div>
141+
142+
<!-- First Name -->
143+
<div class="space-y-2">
144+
<Label>First Name</Label>
145+
<div class="text-sm">{{ user.first_name || 'Not provided' }}</div>
146+
</div>
147+
148+
<!-- Last Name -->
149+
<div class="space-y-2">
150+
<Label>Last Name</Label>
151+
<div class="text-sm">{{ user.last_name || 'Not provided' }}</div>
152+
</div>
153+
154+
<!-- Authentication Type -->
155+
<div class="space-y-2">
156+
<Label>Registration Method</Label>
157+
<div v-if="authTypeBadge">
158+
<Badge
159+
:variant="authTypeBadge.variant"
160+
class="flex items-center gap-1 w-fit"
161+
>
162+
<component :is="authTypeBadge.icon" class="h-3 w-3" />
163+
{{ authTypeBadge.text }}
164+
</Badge>
165+
</div>
166+
</div>
167+
168+
<!-- GitHub ID (if applicable) -->
169+
<div v-if="user.github_id" class="space-y-2">
170+
<Label>GitHub ID</Label>
171+
<div class="text-sm font-mono">{{ user.github_id }}</div>
172+
</div>
173+
174+
<!-- Role -->
175+
<div class="space-y-2">
176+
<Label>Role</Label>
177+
<div class="text-sm">
178+
{{ user.role ? user.role.name : 'No role assigned' }}
179+
</div>
180+
</div>
181+
182+
<!-- Role Permissions (if role exists) -->
183+
<div v-if="user.role && user.role.permissions.length > 0" class="space-y-2">
184+
<Label>Permissions</Label>
185+
<div class="flex flex-wrap gap-1">
186+
<Badge
187+
v-for="permission in user.role.permissions"
188+
:key="permission"
189+
variant="outline"
190+
class="text-xs"
191+
>
192+
{{ permission }}
193+
</Badge>
194+
</div>
195+
</div>
196+
197+
<!-- User ID -->
198+
<div class="space-y-2">
199+
<Label>User ID</Label>
200+
<div class="text-sm font-mono text-muted-foreground">{{ user.id }}</div>
201+
</div>
202+
</CardContent>
203+
</Card>
204+
</div>
205+
</div>
206+
</DashboardLayout>
207+
</template>

services/frontend/src/views/admin/Users.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { ref, onMounted, computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
4+
import { useRouter } from 'vue-router'
45
import {
56
FlexRender,
67
getCoreRowModel,
@@ -24,10 +25,11 @@ import {
2425
import { Button } from '@/components/ui/button'
2526
import DashboardLayout from '@/components/DashboardLayout.vue'
2627
import { getEnv } from '@/utils/env'
27-
import { columns } from './users/columns'
28+
import { createColumns } from './users/columns'
2829
import type { User, UsersApiResponse } from './users/types'
2930
3031
const { t } = useI18n()
32+
const router = useRouter()
3133
3234
const users = ref<User[]>([])
3335
const isLoading = ref(true)
@@ -39,6 +41,14 @@ const rowSelection = ref({})
3941
4042
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL') || ''
4143
44+
// Navigation function for viewing user details
45+
const handleViewUser = (userId: string) => {
46+
router.push(`/admin/users/${userId}`)
47+
}
48+
49+
// Create columns with navigation callback
50+
const columns = createColumns(handleViewUser)
51+
4252
// Fetch users from API
4353
async function fetchUsers(): Promise<User[]> {
4454
if (!apiUrl) {

services/frontend/src/views/admin/users/columns.ts

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,11 @@ import { h } from 'vue'
22
import type { ColumnDef } from '@tanstack/vue-table'
33
import { Badge } from '@/components/ui/badge'
44
import { Button } from '@/components/ui/button'
5-
import {
6-
DropdownMenu,
7-
DropdownMenuContent,
8-
DropdownMenuItem,
9-
DropdownMenuTrigger,
10-
} from '@/components/ui/dropdown-menu'
11-
import { MoreHorizontal, Mail, Github } from 'lucide-vue-next'
5+
import { Mail, Github, Eye } from 'lucide-vue-next'
126
import type { User } from './types'
137

14-
export const columns: ColumnDef<User>[] = [
8+
export function createColumns(onViewUser: (userId: string) => void): ColumnDef<User>[] {
9+
return [
1510
{
1611
accessorKey: 'auth_type',
1712
header: 'Registration',
@@ -73,28 +68,19 @@ export const columns: ColumnDef<User>[] = [
7368
const user = row.original
7469

7570
return h('div', { class: 'flex justify-end' }, [
76-
h(DropdownMenu, {}, {
77-
default: () => [
78-
h(DropdownMenuTrigger, { asChild: true }, () => [
79-
h(Button, {
80-
variant: 'ghost',
81-
class: 'h-8 w-8 p-0'
82-
}, () => [
83-
h('span', { class: 'sr-only' }, 'Open menu'),
84-
h(MoreHorizontal, { class: 'h-4 w-4' })
85-
])
86-
]),
87-
h(DropdownMenuContent, { align: 'end' }, () => [
88-
h(DropdownMenuItem, {
89-
onClick: () => {
90-
// Placeholder for reset password functionality
91-
console.log('Reset password for user:', user.id)
92-
}
93-
}, () => 'Reset Password')
94-
])
95-
]
96-
})
71+
h(Button, {
72+
variant: 'outline',
73+
size: 'sm',
74+
class: 'h-8 px-3',
75+
onClick: () => {
76+
onViewUser(user.id)
77+
}
78+
}, () => [
79+
h(Eye, { class: 'h-4 w-4 mr-1' }),
80+
'View User'
81+
])
9782
])
9883
},
9984
},
100-
]
85+
]
86+
}

0 commit comments

Comments
 (0)