186 lines
5.6 KiB
Vue
186 lines
5.6 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-background">
|
|
<NuxtLayout name="default">
|
|
<main class="container mx-auto px-4 py-8">
|
|
<div class="mb-6 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold">Roles & Permissions</h1>
|
|
<p class="text-muted-foreground">Manage user roles and their permissions across objects</p>
|
|
</div>
|
|
<Button @click="showCreateDialog = true">
|
|
<Plus class="w-4 h-4 mr-2" />
|
|
New Role
|
|
</Button>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-center py-12">Loading roles...</div>
|
|
|
|
<div v-else class="space-y-4">
|
|
<Card
|
|
v-for="role in roles"
|
|
:key="role.id"
|
|
class="cursor-pointer hover:border-primary transition-colors"
|
|
@click="handleSelectRole(role)"
|
|
>
|
|
<CardHeader>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>{{ role.name }}</CardTitle>
|
|
<CardDescription v-if="role.description">
|
|
{{ role.description }}
|
|
</CardDescription>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
@click.stop="handleDeleteRole(role.id)"
|
|
>
|
|
<Trash2 class="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
|
|
<div v-if="roles.length === 0" class="text-center py-12 text-muted-foreground">
|
|
No roles yet. Create one to get started.
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Role Dialog -->
|
|
<Dialog v-model:open="showCreateDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New Role</DialogTitle>
|
|
<DialogDescription>
|
|
Define a new role for your organization
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4 py-4">
|
|
<div class="space-y-2">
|
|
<Label>Role Name</Label>
|
|
<Input v-model="newRole.name" placeholder="e.g., Account Manager" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label>Description</Label>
|
|
<Input v-model="newRole.description" placeholder="Optional description" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showCreateDialog = false">Cancel</Button>
|
|
<Button @click="handleCreateRole" :disabled="!newRole.name || creating">
|
|
{{ creating ? 'Creating...' : 'Create' }}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<!-- Role Permissions Editor Dialog -->
|
|
<Dialog v-model:open="showPermissionsDialog">
|
|
<DialogContent class="max-w-4xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Manage Permissions: {{ selectedRole?.name }}</DialogTitle>
|
|
<DialogDescription>
|
|
Configure what this role can do with each object
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<RolePermissionsEditor
|
|
v-if="selectedRole"
|
|
:role="selectedRole"
|
|
@saved="handlePermissionsSaved"
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</NuxtLayout>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import RolePermissionsEditor from '@/components/RolePermissionsEditor.vue'
|
|
|
|
const { api } = useApi()
|
|
const { toast } = useToast()
|
|
|
|
const roles = ref<any[]>([])
|
|
const loading = ref(true)
|
|
const creating = ref(false)
|
|
|
|
const showCreateDialog = ref(false)
|
|
const showPermissionsDialog = ref(false)
|
|
const selectedRole = ref<any>(null)
|
|
|
|
const newRole = ref({
|
|
name: '',
|
|
description: '',
|
|
})
|
|
|
|
const fetchRoles = async () => {
|
|
try {
|
|
loading.value = true
|
|
roles.value = await api.get('/roles')
|
|
} catch (e: any) {
|
|
console.error('Error fetching roles:', e)
|
|
toast.error('Failed to load roles')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleCreateRole = async () => {
|
|
try {
|
|
creating.value = true
|
|
const created = await api.post('/roles', newRole.value)
|
|
roles.value.push(created)
|
|
toast.success('Role created successfully')
|
|
showCreateDialog.value = false
|
|
newRole.value = { name: '', description: '' }
|
|
} catch (e: any) {
|
|
console.error('Error creating role:', e)
|
|
toast.error('Failed to create role')
|
|
} finally {
|
|
creating.value = false
|
|
}
|
|
}
|
|
|
|
const handleSelectRole = (role: any) => {
|
|
selectedRole.value = role
|
|
showPermissionsDialog.value = true
|
|
}
|
|
|
|
const handleDeleteRole = async (roleId: string) => {
|
|
if (!confirm('Are you sure you want to delete this role?')) return
|
|
|
|
try {
|
|
await api.delete(`/roles/${roleId}`)
|
|
roles.value = roles.value.filter(r => r.id !== roleId)
|
|
toast.success('Role deleted successfully')
|
|
} catch (e: any) {
|
|
console.error('Error deleting role:', e)
|
|
toast.error('Failed to delete role')
|
|
}
|
|
}
|
|
|
|
const handlePermissionsSaved = () => {
|
|
showPermissionsDialog.value = false
|
|
toast.success('Permissions saved successfully')
|
|
}
|
|
|
|
onMounted(() => {
|
|
fetchRoles()
|
|
})
|
|
</script>
|