first commit
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="users-table">
|
||||
<div v-for="user in users" :key="user.id" class="users-table__row">
|
||||
<div class="users-table__avatar">
|
||||
<img v-if="user.avatarUrl" :src="user.avatarUrl" :alt="fullName(user)" />
|
||||
<SvgIcon v-else name="user" :size="36" color="#cbd5e1" />
|
||||
</div>
|
||||
<div class="users-table__info">
|
||||
<p class="users-table__name">{{ fullName(user) || '—' }}</p>
|
||||
<div class="users-table__meta">
|
||||
<span v-if="user.phoneNumber">{{ user.phoneNumber }}</span>
|
||||
<span v-if="user.nationalCode" class="users-table__sep">|</span>
|
||||
<span v-if="user.nationalCode">کد ملی: {{ user.nationalCode }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="users-table__role-badge"
|
||||
:class="`users-table__role-badge--${roleName(user) || 'unknown'}`"
|
||||
:disabled="updateRoleMutation.isPending.value && activeUser?.id === user.id"
|
||||
@click="onRoleClick($event, user)"
|
||||
>
|
||||
<span class="users-table__role-dot" />
|
||||
<span>{{ roleLabel(user) || 'بدون نقش' }}</span>
|
||||
<SvgIcon name="caret-down" :size="12" color="currentColor" />
|
||||
</button>
|
||||
|
||||
<div class="users-table__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="users-table__more"
|
||||
aria-label="گزینههای بیشتر"
|
||||
@click="onMoreClick($event, user)"
|
||||
>
|
||||
<span class="users-table__more-dot" />
|
||||
<span class="users-table__more-dot" />
|
||||
<span class="users-table__more-dot" />
|
||||
</button>
|
||||
<BaseButton
|
||||
text="جزئیات کاربر"
|
||||
custom-class="users-table__details-btn"
|
||||
@click="emit('show-details', user)"
|
||||
>
|
||||
<template #appendIcon>
|
||||
<SvgIcon name="caret-left" :size="16" color="#fff" />
|
||||
</template>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu
|
||||
v-model="menuOpen"
|
||||
:reference="menuTrigger"
|
||||
:items="menuItems"
|
||||
placement="bottom-end"
|
||||
/>
|
||||
|
||||
<DropdownMenu
|
||||
v-model="roleMenuOpen"
|
||||
:reference="roleMenuTrigger"
|
||||
:items="roleMenuItems"
|
||||
placement="bottom-end"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useQueryClient } from '@tanstack/vue-query'
|
||||
|
||||
import SvgIcon from '@/components/icons/SvgIcon.vue'
|
||||
import DropdownMenu from '@/components/DropdownMenu.vue'
|
||||
import { CHANGEABLE_ROLES, ROLE_LABELS } from '@/enums'
|
||||
import BaseButton from '@/components/BaseButton.vue'
|
||||
import {
|
||||
adminUsersKeys,
|
||||
useAdminRolesListQuery,
|
||||
useUpdateAdminUserRoleMutation,
|
||||
} from '@/services/query/admin-users'
|
||||
|
||||
defineProps({
|
||||
users: { type: Array, required: true },
|
||||
})
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const emit = defineEmits([
|
||||
'edit',
|
||||
'delete',
|
||||
'block',
|
||||
'show-details',
|
||||
'show-assignments',
|
||||
'show-exams',
|
||||
'export-pdf',
|
||||
])
|
||||
|
||||
const fullName = (user) => `${user.firstName || ''} ${user.lastName || ''}`.trim()
|
||||
|
||||
const roleName = (user) => {
|
||||
const first = user.roles?.[0]
|
||||
return typeof first === 'string' ? first : first?.name
|
||||
}
|
||||
|
||||
const roleLabel = (user) => {
|
||||
const name = roleName(user)
|
||||
return name ? ROLE_LABELS[name] || name : ''
|
||||
}
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const menuTrigger = ref(null)
|
||||
const activeUser = ref(null)
|
||||
|
||||
const onMoreClick = (event, user) => {
|
||||
const trigger = event.currentTarget
|
||||
if (menuOpen.value && menuTrigger.value === trigger) {
|
||||
menuOpen.value = false
|
||||
return
|
||||
}
|
||||
activeUser.value = user
|
||||
menuTrigger.value = trigger
|
||||
menuOpen.value = true
|
||||
}
|
||||
|
||||
const roleMenuOpen = ref(false)
|
||||
const roleMenuTrigger = ref(null)
|
||||
|
||||
const { data: rolesList = ref([]) } = useAdminRolesListQuery()
|
||||
const roleIdByName = computed(() => {
|
||||
const map = {}
|
||||
;(rolesList.value ?? []).forEach((r) => {
|
||||
if (r?.name) map[r.name] = r.id
|
||||
})
|
||||
return map
|
||||
})
|
||||
|
||||
const updateRoleMutation = useUpdateAdminUserRoleMutation()
|
||||
|
||||
const invalidateUsers = () => queryClient.invalidateQueries({ queryKey: adminUsersKeys.all })
|
||||
|
||||
const onRoleClick = (event, user) => {
|
||||
const trigger = event.currentTarget
|
||||
if (roleMenuOpen.value && roleMenuTrigger.value === trigger) {
|
||||
roleMenuOpen.value = false
|
||||
return
|
||||
}
|
||||
activeUser.value = user
|
||||
roleMenuTrigger.value = trigger
|
||||
roleMenuOpen.value = true
|
||||
}
|
||||
|
||||
const onChangeRole = (targetRoleName) => {
|
||||
const user = activeUser.value
|
||||
if (!user) return
|
||||
const roleId = roleIdByName.value[targetRoleName]
|
||||
const payload = roleId ? { roleId } : { role: targetRoleName }
|
||||
updateRoleMutation.mutate({ id: user.id, payload }, { onSuccess: invalidateUsers })
|
||||
}
|
||||
|
||||
const roleMenuItems = computed(() => {
|
||||
const current = activeUser.value ? roleName(activeUser.value) : null
|
||||
return CHANGEABLE_ROLES.filter((r) => r !== current).map((r) => ({
|
||||
key: `role-${r}`,
|
||||
label: `تغییر به ${ROLE_LABELS[r]}`,
|
||||
icon: 'arrows-clockwise',
|
||||
onClick: () => onChangeRole(r),
|
||||
}))
|
||||
})
|
||||
|
||||
const menuItems = computed(() => [
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'ویرایش',
|
||||
icon: 'pencil',
|
||||
onClick: () => emit('edit', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'حذف',
|
||||
icon: 'trash',
|
||||
onClick: () => emit('delete', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'block',
|
||||
label: 'مسدود کردن کاربر',
|
||||
icon: 'close',
|
||||
danger: true,
|
||||
onClick: () => emit('block', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'show-assignments',
|
||||
label: 'جزئیات تکلیف',
|
||||
icon: 'file',
|
||||
onClick: () => emit('show-assignments', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'show-exams',
|
||||
label: 'جزئیات آزمون',
|
||||
icon: 'file',
|
||||
onClick: () => emit('show-exams', activeUser.value),
|
||||
},
|
||||
{
|
||||
key: 'export-pdf',
|
||||
label: 'خروجی PDF',
|
||||
icon: 'upload',
|
||||
onClick: () => emit('export-pdf', activeUser.value),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.users-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: rgba(255, 255, 255, 85%);
|
||||
border-radius: 0.875rem;
|
||||
box-shadow: 0 4px 10px -6px rgba(241, 241, 241, 70%);
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 9999px;
|
||||
overflow: hidden;
|
||||
background: #f3f4f6;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-family: var(--font-family-fa);
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
color: #1f2937;
|
||||
margin: 0 0 0.25rem;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.375rem;
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-prim-gray);
|
||||
}
|
||||
|
||||
&__sep {
|
||||
color: var(--color-thd-gray);
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__more {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 3px;
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 9999px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
}
|
||||
|
||||
&__more-dot {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--color-prim-gray);
|
||||
}
|
||||
|
||||
&__details-btn {
|
||||
min-width: fit-content;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
&__role-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.375rem 0.875rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 9999px;
|
||||
background: rgba(0, 0, 0, 4%);
|
||||
font-family: var(--font-family-fa);
|
||||
font-size: 0.8rem;
|
||||
color: #4b4b4b;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: background-color 0.15s ease, border-color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(0, 0, 0, 7%);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--student {
|
||||
color: var(--color-primary);
|
||||
background: rgba(var(--color-primary-rgb, 204, 40, 49), 8%);
|
||||
}
|
||||
|
||||
&--teacher {
|
||||
color: #ff9d00;
|
||||
background: rgba(255, 157, 0, 10%);
|
||||
}
|
||||
|
||||
&--counselor {
|
||||
color: #0048ff;
|
||||
background: rgba(0, 72, 255, 8%);
|
||||
}
|
||||
|
||||
&--missionary {
|
||||
color: var(--color-secondary, #6b6b6b);
|
||||
background: rgba(107, 107, 107, 8%);
|
||||
}
|
||||
|
||||
&--admin {
|
||||
color: #1f2937;
|
||||
background: rgba(31, 41, 55, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
&__role-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background: currentcolor;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user