first commit

This commit is contained in:
sajjadtalkhabi
2026-05-13 01:43:05 +03:30
commit d2a577f254
297 changed files with 36274 additions and 0 deletions
@@ -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>