232 lines
7.7 KiB
Vue
232 lines
7.7 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/roles')" class="mb-2">
|
|
← Back to Roles
|
|
</Button>
|
|
<h1 class="text-3xl font-bold">{{ role?.name || 'Role' }}</h1>
|
|
<p class="text-muted-foreground">{{ role?.description || 'No description' }}</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="users">Users</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="details" class="mt-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Role Information</CardTitle>
|
|
</CardHeader>
|
|
<CardContent class="space-y-4">
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label class="text-muted-foreground">Name</Label>
|
|
<p class="font-medium">{{ role?.name }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Guard</Label>
|
|
<Badge variant="outline">{{ role?.guardName || 'tenant' }}</Badge>
|
|
</div>
|
|
<div class="col-span-2">
|
|
<Label class="text-muted-foreground">Description</Label>
|
|
<p class="font-medium">{{ role?.description || 'No description' }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Created At</Label>
|
|
<p class="font-medium">{{ formatDate(role?.createdAt) }}</p>
|
|
</div>
|
|
<div>
|
|
<Label class="text-muted-foreground">Updated At</Label>
|
|
<p class="font-medium">{{ formatDate(role?.updatedAt) }}</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="users" class="mt-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Assigned Users</CardTitle>
|
|
<CardDescription>Manage user assignments for this role</CardDescription>
|
|
</div>
|
|
<Button @click="showAddUserDialog = true" size="sm">
|
|
<Plus class="mr-2 h-4 w-4" />
|
|
Add User
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div v-if="roleUsers.length === 0" class="text-center py-8 text-muted-foreground">
|
|
No users assigned. Add users to grant them this role.
|
|
</div>
|
|
<div v-else class="space-y-2">
|
|
<div
|
|
v-for="user in roleUsers"
|
|
:key="user.id"
|
|
class="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div>
|
|
<p class="font-medium">{{ getUserName(user) }}</p>
|
|
<p class="text-sm text-muted-foreground">{{ user.email }}</p>
|
|
</div>
|
|
<Button variant="ghost" size="sm" @click="removeUser(user.id)">
|
|
<X class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<!-- Add User Dialog -->
|
|
<Dialog v-model:open="showAddUserDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Add User</DialogTitle>
|
|
<DialogDescription>
|
|
Select a user to assign this role
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label>Available Users</Label>
|
|
<Select v-model="selectedUserId" @update:model-value="(value) => selectedUserId = value">
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Choose a user" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem v-for="user in availableUsers" :key="user.id" :value="user.id">
|
|
{{ getUserName(user) }} ({{ user.email }})
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showAddUserDialog = false">Cancel</Button>
|
|
<Button @click="addUser" :disabled="!selectedUserId">
|
|
Add User
|
|
</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 { Badge } from '~/components/ui/badge';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select';
|
|
import { Plus, X } from 'lucide-vue-next';
|
|
|
|
definePageMeta({
|
|
layout: 'default',
|
|
});
|
|
|
|
const route = useRoute();
|
|
const { api } = useApi();
|
|
const { toast } = useToast();
|
|
|
|
const loading = ref(true);
|
|
const role = ref<any>(null);
|
|
const roleUsers = ref<any[]>([]);
|
|
const allUsers = ref<any[]>([]);
|
|
const showAddUserDialog = ref(false);
|
|
const selectedUserId = ref('');
|
|
|
|
const availableUsers = computed(() => {
|
|
const assignedIds = new Set(roleUsers.value.map(u => u.id));
|
|
return allUsers.value.filter(u => !assignedIds.has(u.id));
|
|
});
|
|
|
|
const loadRole = async () => {
|
|
try {
|
|
loading.value = true;
|
|
const roleId = route.params.id;
|
|
const response = await api.get(`/setup/roles/${roleId}`);
|
|
role.value = response;
|
|
roleUsers.value = response.users || [];
|
|
} catch (error: any) {
|
|
console.error('Failed to load role:', error);
|
|
toast.error('Failed to load role');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const loadAllUsers = async () => {
|
|
try {
|
|
const response = await api.get('/setup/users');
|
|
allUsers.value = response || [];
|
|
} catch (error: any) {
|
|
console.error('Failed to load users:', error);
|
|
}
|
|
};
|
|
|
|
const addUser = async () => {
|
|
if (!selectedUserId.value) return;
|
|
|
|
try {
|
|
await api.post(`/setup/roles/${route.params.id}/users`, {
|
|
userId: selectedUserId.value,
|
|
});
|
|
toast.success('User added successfully');
|
|
showAddUserDialog.value = false;
|
|
selectedUserId.value = '';
|
|
await loadRole();
|
|
} catch (error: any) {
|
|
console.error('Failed to add user:', error);
|
|
toast.error(error.message || 'Failed to add user');
|
|
}
|
|
};
|
|
|
|
const removeUser = async (userId: string) => {
|
|
try {
|
|
await api.delete(`/setup/roles/${route.params.id}/users/${userId}`);
|
|
toast.success('User removed successfully');
|
|
await loadRole();
|
|
} catch (error: any) {
|
|
console.error('Failed to remove user:', error);
|
|
toast.error(error.message || 'Failed to remove user');
|
|
}
|
|
};
|
|
|
|
const getUserName = (user: any) => {
|
|
if (!user) return 'Unknown';
|
|
if (user.firstName || user.lastName) {
|
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
|
}
|
|
return user.email || 'Unknown';
|
|
};
|
|
|
|
const formatDate = (date: string) => {
|
|
if (!date) return 'N/A';
|
|
return new Date(date).toLocaleDateString();
|
|
};
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([loadRole(), loadAllUsers()]);
|
|
});
|
|
</script>
|