Files
mws.frontend.vue/src/views/settings/permissions/PermissionsView.vue
T
2026-08-24 08:07:23 +07:00

191 lines
6.9 KiB
Vue

<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>