380 lines
9.1 KiB
Vue
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>
|