feat: update UI
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-12">
|
||||
<!-- Left: Role List -->
|
||||
<div class="panel flex flex-col p-4 md:col-span-4" style="height: 620px;">
|
||||
<div class="mb-3">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search text-xs" />
|
||||
<InputText
|
||||
v-model.trim="roleSearchTerm"
|
||||
placeholder="Search roles..."
|
||||
class="search-input w-full text-sm"
|
||||
/>
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingRoles" class="flex flex-1 items-center justify-center">
|
||||
<ProgressSpinner style="width: 32px; height: 32px;" />
|
||||
</div>
|
||||
<div v-else-if="filteredRoles.length === 0" class="muted-note p-4 text-center">
|
||||
No roles found
|
||||
</div>
|
||||
<div v-else class="flex-1 overflow-y-auto pr-1 space-y-1">
|
||||
<div
|
||||
v-for="r in filteredRoles"
|
||||
:key="r.id"
|
||||
class="flex cursor-pointer items-center justify-between rounded-xl p-3 transition-colors"
|
||||
:class="selectedRoleId === r.id
|
||||
? 'bg-indigo-500/10 text-indigo-500 dark:bg-indigo-500/20 font-semibold'
|
||||
: 'hover:bg-slate-100 dark:hover:bg-slate-800/60'"
|
||||
@click="selectRole(r.id)"
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class="text-sm leading-tight">{{ r.name }}</span>
|
||||
<Tag :value="r.isSystem ? 'System' : 'Custom'" :severity="r.isSystem ? 'warn' : 'secondary'" class="text-xs" />
|
||||
</div>
|
||||
<i v-if="selectedRoleId === r.id" class="pi pi-chevron-right text-xs"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Role Screen Permissions Matrix -->
|
||||
<div class="panel md:col-span-8 flex flex-col overflow-hidden p-4" style="height: 620px;">
|
||||
<div v-if="!selectedRoleId" class="flex flex-1 items-center justify-center muted-note">
|
||||
Select a role from the left list to view/edit permissions
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="eyebrow">Screen Permissions ({{ selectedRole?.name }})</div>
|
||||
<Button
|
||||
label="Save Changes"
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
:loading="savingRolePermissions"
|
||||
@click="handleSaveRolePermissions"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="loadingRoleDetail" class="flex flex-1 items-center justify-center">
|
||||
<ProgressSpinner style="width: 32px; height: 32px;" />
|
||||
</div>
|
||||
<div v-else class="flex-1 overflow-x-auto">
|
||||
<table class="w-full border-collapse text-sm">
|
||||
<thead>
|
||||
<tr class="border-b" style="border-color: var(--hairline)">
|
||||
<th class="py-2.5 text-left font-medium">Screen</th>
|
||||
<th class="w-20 text-center font-medium">View</th>
|
||||
<th class="w-20 text-center font-medium">Create</th>
|
||||
<th class="w-20 text-center font-medium">Edit</th>
|
||||
<th class="w-20 text-center font-medium">Delete</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in rolePermissions" :key="item.screen" class="border-b" style="border-color: var(--hairline)">
|
||||
<td class="py-3 font-medium">{{ screenLabel(item.screen) }}</td>
|
||||
<td class="text-center">
|
||||
<Checkbox v-model="item.canView" :binary="true" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<Checkbox v-model="item.canCreate" :binary="true" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<Checkbox v-model="item.canEdit" :binary="true" />
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<Checkbox v-model="item.canDelete" :binary="true" />
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getRoles, getRole, updateRole } from '../../../services/backend'
|
||||
import { errorMessage } from '../../../services/api'
|
||||
import type { Role, PermissionEntry } from '../../../types'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const roles = ref<Role[]>([])
|
||||
const loadingRoles = ref(false)
|
||||
const roleSearchTerm = ref('')
|
||||
const selectedRoleId = ref<string | null>(null)
|
||||
const selectedRole = ref<Role | null>(null)
|
||||
const rolePermissions = ref<PermissionEntry[]>([])
|
||||
const loadingRoleDetail = ref(false)
|
||||
const savingRolePermissions = ref(false)
|
||||
|
||||
const ALL_SCREENS = [
|
||||
{ key: 'dashboard', label: 'Dashboard' },
|
||||
{ key: 'projects', label: 'Projects' },
|
||||
{ key: 'documents', label: 'Documents' },
|
||||
{ key: 'tasks', label: 'Tasks' },
|
||||
{ key: 'users', label: 'Users' },
|
||||
{ key: 'permissions', label: 'Permissions' },
|
||||
{ key: 'masterdata', label: 'Master Data' },
|
||||
]
|
||||
|
||||
const filteredRoles = computed(() => {
|
||||
if (!roleSearchTerm.value) return roles.value
|
||||
const term = roleSearchTerm.value.toLowerCase()
|
||||
return roles.value.filter((r) => r.name.toLowerCase().includes(term))
|
||||
})
|
||||
|
||||
async function fetchRoles() {
|
||||
loadingRoles.value = true
|
||||
try {
|
||||
const res = await getRoles(1, 100)
|
||||
roles.value = res.items
|
||||
if (roles.value.length > 0 && !selectedRoleId.value) {
|
||||
selectRole(roles.value[0].id)
|
||||
}
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
loadingRoles.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function selectRole(roleId: string) {
|
||||
selectedRoleId.value = roleId
|
||||
loadingRoleDetail.value = true
|
||||
try {
|
||||
const data = await getRole(roleId)
|
||||
selectedRole.value = data
|
||||
rolePermissions.value = ALL_SCREENS.map((s) => {
|
||||
const existing = data.permissions.find((p) => p.screen === s.key)
|
||||
return {
|
||||
screen: s.key,
|
||||
canView: existing?.canView ?? false,
|
||||
canCreate: existing?.canCreate ?? false,
|
||||
canEdit: existing?.canEdit ?? false,
|
||||
canDelete: existing?.canDelete ?? false,
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
loadingRoleDetail.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function screenLabel(key: string): string {
|
||||
return ALL_SCREENS.find((s) => s.key === key)?.label ?? key
|
||||
}
|
||||
|
||||
async function handleSaveRolePermissions() {
|
||||
if (!selectedRoleId.value || !selectedRole.value) return
|
||||
savingRolePermissions.value = true
|
||||
try {
|
||||
await updateRole(selectedRoleId.value, {
|
||||
name: selectedRole.value.name,
|
||||
permissions: rolePermissions.value,
|
||||
})
|
||||
toast.add({ severity: 'success', summary: 'Permissions saved', life: 2000 })
|
||||
await fetchRoles()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
|
||||
} finally {
|
||||
savingRolePermissions.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void fetchRoles()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user