Files
banu-front/src/features/admin/users/components/UsersTable.vue
T
sajjadtalkhabi 38a396ba21
Deploy banu-front / deploy (push) Successful in 1m27s
fix
2026-07-07 17:36:43 +03:30

380 lines
9.1 KiB
Vue

<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="user?.name" />
<SvgIcon v-else name="user" :size="36" color="#cbd5e1" />
</div>
<div class="users-table__info">
<p class="users-table__name">{{ user?.name || '—' }}</p>
<div class="users-table__meta">
<span v-if="user.phone">{{ user.phone }}</span>
<span v-if="user.nationalCode" class="users-table__sep">|</span>
<span v-if="user.nationalCode">کد ملی: {{ user.nationalCode }}</span>
</div>
</div>
<div class="users-table__middle">
<div class="users-table__date">
<span class="users-table__date-label">تاریخ ثبت نام:</span>
<span class="users-table__date-value">
{{ formatJalaaliDateTime(user.createdAt) || '—' }}
</span>
</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>
<Badge v-if="user.status === 'blocked'" variant="danger" size="sm" dot>
{{ USER_STATUS.blocked }}
</Badge>
</div>
<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 Badge from '@/components/Badge.vue'
import { ROLES, USER_STATUS } from '@/enums'
import { useQueryClient } from '@tanstack/vue-query'
import SvgIcon from '@/components/icons/SvgIcon.vue'
import BaseButton from '@/components/BaseButton.vue'
import DropdownMenu from '@/components/DropdownMenu.vue'
import { formatJalaaliDateTime } from '@/utils/date-utils'
import { adminUsersKeys, useUpdateAdminUserRoleMutation } from '@/services/query/admin-users'
defineProps({
users: { type: Array, required: true },
})
const queryClient = useQueryClient()
const emit = defineEmits(['edit', 'delete', 'block', 'unblock', 'show-details'])
const roleName = (user) => {
const first = user.roles?.[0]
return typeof first === 'string' ? first : first?.name
}
const roleLabel = (user) => {
const name = roleName(user)
if (!name) return ''
return ROLES.find((r) => r.value === name)?.label || 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 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 || !targetRoleName) return
updateRoleMutation.mutate(
{ id: user.id, payload: { roles: [targetRoleName] } },
{ onSuccess: invalidateUsers }
)
}
const roleMenuItems = computed(() => {
const current = activeUser.value ? roleName(activeUser.value) : null
return ROLES.filter((r) => r.changeable && r.value !== current).map((r) => ({
key: `role-${r.value}`,
label: `تغییر به ${r.label}`,
icon: 'arrows-clockwise',
onClick: () => onChangeRole(r.value),
}))
})
const menuItems = computed(() => {
const user = activeUser.value
const isBlocked = user?.status === 'blocked'
return [
{
key: 'edit',
label: 'ویرایش',
icon: 'pencil',
onClick: () => emit('edit', user),
},
{
key: 'delete',
label: 'حذف',
icon: 'trash',
onClick: () => emit('delete', user),
},
isBlocked
? {
key: 'unblock',
label: 'رفع مسدودیت کاربر',
icon: 'check',
onClick: () => emit('unblock', user),
}
: {
key: 'block',
label: 'مسدود کردن کاربر',
icon: 'close',
danger: true,
onClick: () => emit('block', user),
},
]
})
</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 1 33%;
min-width: 0;
}
&__middle {
display: flex;
align-items: center;
gap: 0.375rem;
flex: 1 1 33%;
}
&__date {
background: rgba(107, 107, 107, 5%);
padding: 0.25rem 1rem;
border-radius: 0.875rem;
white-space: nowrap;
line-height: 1.5;
}
&__date-label {
font-family: var(--font-family-fa);
font-weight: 300;
font-size: 0.75rem;
color: #848484;
margin-inline-end: 0.25rem;
}
&__date-value {
font-family: var(--font-family-en);
font-size: 0.75rem;
}
&__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>