Files
mws.frontend.vue/src/views/settings/roles/RolesView.vue
T

214 lines
8.0 KiB
Vue
Raw Normal View History

2026-08-24 08:07:23 +07:00
<template>
2026-08-26 00:11:45 +07:00
<div class="flex h-full min-h-0 flex-col">
<div class="grid min-h-0 flex-1 grid-cols-1 gap-4 md:grid-cols-12">
2026-08-24 08:07:23 +07:00
<!-- Left: User List -->
2026-08-26 00:11:45 +07:00
<div class="panel flex min-h-[420px] flex-col p-4 md:col-span-4">
2026-08-24 08:07:23 +07:00
<div class="mb-3">
<IconField>
<InputIcon class="pi pi-search text-xs" />
<InputText
v-model.trim="userSearchTerm"
placeholder="Search users..."
class="search-input w-full text-sm"
@input="onUserSearch"
/>
</IconField>
</div>
<div v-if="loadingUsers" class="flex flex-1 items-center justify-center">
<ProgressSpinner style="width: 32px; height: 32px;" />
</div>
<div v-else-if="users.length === 0" class="muted-note p-4 text-center">
No users found
</div>
2026-08-26 00:11:45 +07:00
<div v-else class="min-h-0 flex-1 overflow-y-auto pr-1">
2026-08-24 08:07:23 +07:00
<div
v-for="u in users"
:key="u.id"
class="flex cursor-pointer items-center justify-between rounded-xl p-3 transition-colors mb-1"
:class="selectedUserId === u.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="selectUser(u.id)"
>
<div class="flex items-center gap-2.5">
<Avatar
:label="(u.displayName || u.username).slice(0, 2).toUpperCase()"
style="background: var(--primary); color: #fff"
shape="circle"
size="normal"
/>
<div>
<div class="text-sm leading-tight">{{ u.displayName }}</div>
<div class="muted-note text-xs">@{{ u.username }}</div>
</div>
</div>
<i v-if="selectedUserId === u.id" class="pi pi-chevron-right text-xs"></i>
</div>
</div>
</div>
<!-- Right Top & Bottom: Roles (Unassigned / Assigned) -->
2026-08-26 00:11:45 +07:00
<div class="flex min-h-0 flex-col gap-4 md:col-span-8">
<div v-if="!selectedUserId" class="panel flex min-h-[420px] flex-1 items-center justify-center p-8 muted-note">
2026-08-24 08:07:23 +07:00
Select a user from the left list to manage roles
</div>
<template v-else>
<!-- Top Right: Unassigned Roles -->
2026-08-26 00:11:45 +07:00
<div class="panel flex min-h-[240px] flex-1 flex-col p-4" style="flex-basis: 0;">
2026-08-24 08:07:23 +07:00
<div class="eyebrow mb-3 flex items-center gap-2">
<i class="pi pi-plus-circle text-indigo-500 text-sm"></i> Unassigned Roles
</div>
<div v-if="loadingUserRoles" class="flex flex-1 items-center justify-center">
<ProgressSpinner style="width: 32px; height: 32px;" />
</div>
<div v-else-if="unassignedRoles.length === 0" class="flex flex-1 items-center justify-center muted-note">
All available roles assigned
</div>
2026-08-26 00:11:45 +07:00
<div v-else class="min-h-0 flex-1 overflow-y-auto space-y-2 pr-1">
2026-08-24 08:07:23 +07:00
<div
v-for="r in unassignedRoles"
:key="r.id"
class="flex items-center justify-between rounded-xl border p-3"
style="border-color: var(--hairline)"
>
<div class="flex items-center gap-3">
<span class="font-medium text-sm">{{ r.name }}</span>
<Tag :value="r.isSystem ? 'System' : 'Custom'" :severity="r.isSystem ? 'warn' : 'secondary'" class="text-xs" />
</div>
<Button
label="Assign"
icon="pi pi-plus"
size="small"
:loading="assigningRoleId === r.id"
@click="handleAssignRole(r.id)"
/>
</div>
</div>
</div>
<!-- Bottom Right: Assigned Roles -->
2026-08-26 00:11:45 +07:00
<div class="panel flex min-h-[240px] flex-1 flex-col p-4" style="flex-basis: 0;">
2026-08-24 08:07:23 +07:00
<div class="eyebrow mb-3 flex items-center gap-2">
<i class="pi pi-check-circle text-green-500 text-sm"></i> Assigned Roles
</div>
<div v-if="loadingUserRoles" class="flex flex-1 items-center justify-center">
<ProgressSpinner style="width: 32px; height: 32px;" />
</div>
<div v-else-if="assignedRoles.length === 0" class="flex flex-1 items-center justify-center muted-note">
No roles assigned yet
</div>
2026-08-26 00:11:45 +07:00
<div v-else class="min-h-0 flex-1 overflow-y-auto space-y-2 pr-1">
2026-08-24 08:07:23 +07:00
<div
v-for="r in assignedRoles"
:key="r.id"
class="flex items-center justify-between rounded-xl border p-3"
style="border-color: var(--hairline)"
>
<div class="flex items-center gap-3">
<span class="font-medium text-sm">{{ r.name }}</span>
<Tag :value="r.isSystem ? 'System' : 'Custom'" :severity="r.isSystem ? 'warn' : 'secondary'" class="text-xs" />
</div>
<Button
label="Unassign"
icon="pi pi-times"
severity="danger"
text
size="small"
:loading="unassigningRoleId === r.id"
@click="handleUnassignRole(r.id)"
/>
</div>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getUsers, getUserRoles, assignUserRole, unassignUserRole } from '../../../services/backend'
import { errorMessage } from '../../../services/api'
import type { User, Role } from '../../../types'
const toast = useToast()
const users = ref<User[]>([])
const loadingUsers = ref(false)
const userSearchTerm = ref('')
const selectedUserId = ref<string | null>(null)
const assignedRoles = ref<Role[]>([])
const unassignedRoles = ref<Role[]>([])
const loadingUserRoles = ref(false)
const assigningRoleId = ref<string | null>(null)
const unassigningRoleId = ref<string | null>(null)
let userSearchTimer: ReturnType<typeof setTimeout> | undefined
async function fetchUsers(q?: string) {
loadingUsers.value = true
try {
users.value = await getUsers(q)
if (users.value.length > 0 && !selectedUserId.value) {
selectUser(users.value[0].id)
}
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
loadingUsers.value = false
}
}
function onUserSearch() {
clearTimeout(userSearchTimer)
userSearchTimer = setTimeout(() => void fetchUsers(userSearchTerm.value), 300)
}
async function selectUser(userId: string) {
selectedUserId.value = userId
loadingUserRoles.value = true
try {
const data = await getUserRoles(userId)
assignedRoles.value = data.assignedRoles
unassignedRoles.value = data.unassignedRoles
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
loadingUserRoles.value = false
}
}
async function handleAssignRole(roleId: string) {
if (!selectedUserId.value) return
assigningRoleId.value = roleId
try {
await assignUserRole(selectedUserId.value, roleId)
toast.add({ severity: 'success', summary: 'Role assigned', life: 2000 })
await selectUser(selectedUserId.value)
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
assigningRoleId.value = null
}
}
async function handleUnassignRole(roleId: string) {
if (!selectedUserId.value) return
unassigningRoleId.value = roleId
try {
await unassignUserRole(selectedUserId.value, roleId)
toast.add({ severity: 'success', summary: 'Role unassigned', life: 2000 })
await selectUser(selectedUserId.value)
} catch (e) {
toast.add({ severity: 'error', summary: 'Error', detail: errorMessage(e), life: 4000 })
} finally {
unassigningRoleId.value = null
}
}
onMounted(() => {
void fetchUsers()
})
</script>