228 lines
7.5 KiB
Vue
228 lines
7.5 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-background">
|
|
<NuxtLayout name="default">
|
|
<main class="container mx-auto px-4 py-8">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<Button variant="ghost" size="sm" @click="navigateTo('/setup/users')" class="mb-2">
|
|
← Back to Users
|
|
</Button>
|
|
<h1 class="text-3xl font-bold">{{ getUserName(user) }}</h1>
|
|
<p class="text-muted-foreground">{{ user?.email }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
|
|
<Tabs v-else default-value="details" class="w-full">
|
|
<TabsList>
|
|
<TabsTrigger value="details">Details</TabsTrigger>
|
|
<TabsTrigger value="roles">Roles</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="details" class="mt-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>User Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label class="text-muted-foreground">Email</Label>
|
|
<p class="font-medium">{{ user?.email }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">First Name</Label>
|
|
<p class="font-medium">{{ user?.firstName || 'N/A' }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Last Name</Label>
|
|
<p class="font-medium">{{ user?.lastName || 'N/A' }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Created At</Label>
|
|
<p class="font-medium">{{ formatDate(user?.createdAt) }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Updated At</Label>
|
|
<p class="font-medium">{{ formatDate(user?.updatedAt) }}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="roles" class="mt-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Assigned Roles</CardTitle>
|
|
<CardDescription>Manage role assignments for this user</CardDescription>
|
|
</div>
|
|
<Button @click="showAddRoleDialog = true" size="sm">
|
|
<Plus class="mr-2 h-4 w-4" />
|
|
Add Role
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div v-if="userRoles.length === 0" class="text-center py-8 text-muted-foreground">
|
|
No roles assigned. Add roles to grant permissions.
|
|
</div>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="role in userRoles"
|
|
:key="role.id"
|
|
class="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div>
|
|
<p class="font-medium">{{ role.name }}</p>
|
|
<p class="text-sm text-muted-foreground">{{ role.description || 'No description' }}</p>
|
|
</div>
|
|
<Button variant="ghost" size="sm" @click="removeRole(role.id)">
|
|
<X class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<!-- Add Role Dialog -->
|
|
<Dialog v-model:open="showAddRoleDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Add Role</DialogTitle>
|
|
<DialogDescription>
|
|
Select a role to assign to this user
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label>Available Roles</Label>
|
|
<Select v-model="selectedRoleId" @update:model-value="(value) => selectedRoleId = value">
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Choose a role" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem v-for="role in availableRoles" :key="role.id" :value="role.id">
|
|
{{ role.name }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showAddRoleDialog = false">Cancel</Button>
|
|
<Button @click="addRole" :disabled="!selectedRoleId">
|
|
Add Role
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</NuxtLayout>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, computed } from 'vue';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card';
|
|
import { Button } from '~/components/ui/button';
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '~/components/ui/tabs';
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '~/components/ui/dialog';
|
|
import { Label } from '~/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
|
import { Plus, X } from 'lucide-vue-next';
|
|
|
|
|
|
const route = useRoute();
|
|
const { api } = useApi();
|
|
const { toast } = useToast();
|
|
|
|
const loading = ref(true);
|
|
const user = ref<any>(null);
|
|
const userRoles = ref<any[]>([]);
|
|
const allRoles = ref<any[]>([]);
|
|
const showAddRoleDialog = ref(false);
|
|
const selectedRoleId = ref('');
|
|
|
|
const availableRoles = computed(() => {
|
|
const assignedIds = new Set(userRoles.value.map(r => r.id));
|
|
return allRoles.value.filter(r => !assignedIds.has(r.id));
|
|
});
|
|
|
|
const loadUser = async () => {
|
|
try {
|
|
loading.value = true;
|
|
const userId = route.params.id;
|
|
const response = await api.get(`/setup/users/${userId}`);
|
|
user.value = response;
|
|
userRoles.value = response.roles || [];
|
|
} catch (error: any) {
|
|
console.error('Failed to load user:', error);
|
|
toast.error('Failed to load user');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadAllRoles = async () => {
|
|
try {
|
|
const response = await api.get('/setup/roles');
|
|
allRoles.value = response || [];
|
|
} catch (error: any) {
|
|
console.error('Failed to load roles:', error);
|
|
}
|
|
};
|
|
|
|
const addRole = async () => {
|
|
if (!selectedRoleId.value) return;
|
|
|
|
try {
|
|
await api.post(`/setup/users/${route.params.id}/roles`, {
|
|
roleId: selectedRoleId.value,
|
|
});
|
|
toast.success('Role added successfully');
|
|
showAddRoleDialog.value = false;
|
|
selectedRoleId.value = '';
|
|
await loadUser();
|
|
} catch (error: any) {
|
|
console.error('Failed to add role:', error);
|
|
toast.error(error.message || 'Failed to add role');
|
|
}
|
|
};
|
|
|
|
const removeRole = async (roleId: string) => {
|
|
try {
|
|
await api.delete(`/setup/users/${route.params.id}/roles/${roleId}`);
|
|
toast.success('Role removed successfully');
|
|
await loadUser();
|
|
} catch (error: any) {
|
|
console.error('Failed to remove role:', error);
|
|
toast.error(error.message || 'Failed to remove role');
|
|
}
|
|
};
|
|
|
|
const getUserName = (user: any) => {
|
|
if (!user) return 'User';
|
|
if (user.firstName || user.lastName) {
|
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
|
}
|
|
return user.email || 'User';
|
|
};
|
|
|
|
const formatDate = (date: string) => {
|
|
if (!date) return 'N/A';
|
|
return new Date(date).toLocaleDateString();
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadUser(), loadAllRoles()]);
|
|
});
|
|
</script>
|