This commit is contained in:
@@ -379,3 +379,27 @@ export const DELETE_CUSTOM_ROLE_MUTATION: string =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`.loc?.source.body || ''
|
`.loc?.source.body || ''
|
||||||
|
|
||||||
|
export const ADMIN_UPDATE_USER_MUTATION = `
|
||||||
|
mutation UpdateUser(
|
||||||
|
$id: Int!
|
||||||
|
$email: String
|
||||||
|
$name: String
|
||||||
|
$slug: String
|
||||||
|
$roles: String!
|
||||||
|
) {
|
||||||
|
updateUser(
|
||||||
|
id: $id
|
||||||
|
email: $email
|
||||||
|
name: $name
|
||||||
|
slug: $slug
|
||||||
|
roles: $roles
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
@@ -4,6 +4,9 @@ import formStyles from '../styles/Form.module.css'
|
|||||||
import Button from '../ui/Button'
|
import Button from '../ui/Button'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
|
||||||
|
// Список администраторских email
|
||||||
|
const ADMIN_EMAILS = ['welcome@discours.io']
|
||||||
|
|
||||||
export interface UserEditModalProps {
|
export interface UserEditModalProps {
|
||||||
user: AdminUserInfo
|
user: AdminUserInfo
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
@@ -13,7 +16,7 @@ export interface UserEditModalProps {
|
|||||||
email?: string
|
email?: string
|
||||||
name?: string
|
name?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
roles: string[]
|
roles: string
|
||||||
}) => Promise<void>
|
}) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +68,8 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
|
|
||||||
// Проверяем, является ли пользователь администратором по ролям, которые приходят с сервера
|
// Проверяем, является ли пользователь администратором по ролям, которые приходят с сервера
|
||||||
const isAdmin = () => {
|
const isAdmin = () => {
|
||||||
return (props.user.roles || []).includes('admin')
|
const roles = formData().roles
|
||||||
|
return roles.includes('admin') || (props.user.email ? ADMIN_EMAILS.includes(props.user.email) : false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем информацию о роли по ID
|
// Получаем информацию о роли по ID
|
||||||
@@ -73,23 +77,16 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
return AVAILABLE_ROLES.find((role) => role.id === roleId) || { name: roleId, emoji: '👤' }
|
return AVAILABLE_ROLES.find((role) => role.id === roleId) || { name: roleId, emoji: '👤' }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Формируем строку с ролями и эмоджи
|
// Обновляем поле формы
|
||||||
const getRolesDisplay = () => {
|
const updateField = (field: keyof ReturnType<typeof formData>, value: string) => {
|
||||||
const roles = formData().roles
|
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||||
if (roles.length === 0) {
|
if (errors()[field]) {
|
||||||
return isAdmin() ? '🪄 Администратор' : 'Роли не назначены'
|
setErrors((prev) => {
|
||||||
}
|
const newErrors = { ...prev }
|
||||||
|
delete newErrors[field]
|
||||||
const roleTexts = roles.map((roleId) => {
|
return newErrors
|
||||||
const role = getRoleInfo(roleId)
|
|
||||||
return `${role.emoji} ${role.name}`
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isAdmin()) {
|
|
||||||
return `🪄 Администратор, ${roleTexts.join(', ')}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return roleTexts.join(', ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем форму при изменении пользователя
|
// Обновляем форму при изменении пользователя
|
||||||
@@ -106,31 +103,25 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const updateField = (field: string, value: string) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
|
||||||
// Очищаем ошибку при изменении поля
|
|
||||||
if (errors()[field]) {
|
|
||||||
setErrors((prev) => ({ ...prev, [field]: '' }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleRoleToggle = (roleId: string) => {
|
const handleRoleToggle = (roleId: string) => {
|
||||||
// Роль администратора нельзя изменить вручную
|
|
||||||
if (roleId === 'admin') {
|
if (roleId === 'admin') {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormData((prev) => {
|
setFormData((prev) => {
|
||||||
const currentRoles = prev.roles
|
const currentRoles = prev.roles || []
|
||||||
const newRoles = currentRoles.includes(roleId)
|
const newRoles = currentRoles.includes(roleId)
|
||||||
? currentRoles.filter((r) => r !== roleId)
|
? currentRoles.filter((r: string) => r !== roleId)
|
||||||
: [...currentRoles, roleId]
|
: [...currentRoles, roleId]
|
||||||
return { ...prev, roles: newRoles }
|
return { ...prev, roles: newRoles }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Очищаем ошибку ролей при изменении
|
|
||||||
if (errors().roles) {
|
if (errors().roles) {
|
||||||
setErrors((prev) => ({ ...prev, roles: '' }))
|
setErrors((prev) => {
|
||||||
|
const newErrors = { ...prev }
|
||||||
|
delete newErrors.roles
|
||||||
|
return newErrors
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,23 +129,19 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
const newErrors: Record<string, string> = {}
|
const newErrors: Record<string, string> = {}
|
||||||
const data = formData()
|
const data = formData()
|
||||||
|
|
||||||
// Email
|
|
||||||
if (!data.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim())) {
|
if (!data.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim())) {
|
||||||
newErrors.email = 'Неверный формат email'
|
newErrors.email = 'Неверный формат email'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Имя
|
|
||||||
if (!data.name.trim() || data.name.trim().length < 2) {
|
if (!data.name.trim() || data.name.trim().length < 2) {
|
||||||
newErrors.name = 'Имя должно содержать минимум 2 символа'
|
newErrors.name = 'Имя должно содержать минимум 2 символа'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slug
|
|
||||||
if (!data.slug.trim() || !/^[a-z0-9_-]+$/.test(data.slug.trim())) {
|
if (!data.slug.trim() || !/^[a-z0-9_-]+$/.test(data.slug.trim())) {
|
||||||
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
|
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
|
||||||
}
|
}
|
||||||
|
|
||||||
// Роли (админы освобождаются от этого требования)
|
if (!isAdmin() && (data.roles || []).filter((role: string) => role !== 'admin').length === 0) {
|
||||||
if (!isAdmin() && data.roles.filter((role) => role !== 'admin').length === 0) {
|
|
||||||
newErrors.roles = 'Выберите хотя бы одну роль'
|
newErrors.roles = 'Выберите хотя бы одну роль'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,8 +156,10 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
// Отправляем только обычные роли, админская роль определяется на сервере по email
|
await props.onSave({
|
||||||
await props.onSave(formData())
|
...formData(),
|
||||||
|
roles: (formData().roles || []).join(',')
|
||||||
|
})
|
||||||
props.onClose()
|
props.onClose()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Ошибка при сохранении пользователя:', error)
|
console.error('Ошибка при сохранении пользователя:', error)
|
||||||
@@ -185,63 +174,10 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
isOpen={props.isOpen}
|
isOpen={props.isOpen}
|
||||||
onClose={props.onClose}
|
onClose={props.onClose}
|
||||||
title={`Редактирование пользователя #${props.user.id}`}
|
title={`Редактирование пользователя #${props.user.id}`}
|
||||||
size="large"
|
|
||||||
>
|
>
|
||||||
<div class={formStyles.form}>
|
<div class={formStyles.form}>
|
||||||
{/* Компактная системная информация */}
|
{/* Основные данные */}
|
||||||
<div class={formStyles.fieldGroup}>
|
<div class={formStyles.fieldGroup}>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'grid',
|
|
||||||
'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
|
|
||||||
gap: '1rem',
|
|
||||||
padding: '1rem',
|
|
||||||
background: 'var(--form-bg-light)',
|
|
||||||
'font-size': '0.875rem',
|
|
||||||
color: 'var(--form-text-light)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong>ID:</strong> {props.user.id}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Регистрация:</strong>{' '}
|
|
||||||
{props.user.created_at
|
|
||||||
? new Date(props.user.created_at * 1000).toLocaleDateString('ru-RU')
|
|
||||||
: '—'}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<strong>Активность:</strong>{' '}
|
|
||||||
{props.user.last_seen
|
|
||||||
? new Date(props.user.last_seen * 1000).toLocaleDateString('ru-RU')
|
|
||||||
: '—'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Текущие роли в строку */}
|
|
||||||
<div class={formStyles.fieldGroup} style={{ display: 'none' }}>
|
|
||||||
<label class={formStyles.label}>
|
|
||||||
<span class={formStyles.labelText}>
|
|
||||||
<span class={formStyles.labelIcon}>👤</span>
|
|
||||||
Текущие роли
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
padding: '0.875rem 1rem',
|
|
||||||
background: isAdmin() ? 'rgba(245, 158, 11, 0.1)' : 'var(--form-bg-light)',
|
|
||||||
border: isAdmin() ? '1px solid rgba(245, 158, 11, 0.3)' : '1px solid var(--form-divider)',
|
|
||||||
'font-size': '0.95rem',
|
|
||||||
'font-weight': '500',
|
|
||||||
color: isAdmin() ? '#d97706' : 'var(--form-text)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{getRolesDisplay()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Основные данные в компактной сетке */}
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -271,10 +207,6 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
{errors().email}
|
{errors().email}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div class={formStyles.hint}>
|
|
||||||
<span class={formStyles.hintIcon}>💡</span>
|
|
||||||
Администраторы определяются автоматически по настройкам сервера
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class={formStyles.fieldGroup}>
|
<div class={formStyles.fieldGroup}>
|
||||||
@@ -317,10 +249,6 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
disabled={loading()}
|
disabled={loading()}
|
||||||
placeholder="ivan-ivanov"
|
placeholder="ivan-ivanov"
|
||||||
/>
|
/>
|
||||||
<div class={formStyles.hint}>
|
|
||||||
<span class={formStyles.hintIcon}>💡</span>
|
|
||||||
Только латинские буквы, цифры, дефисы и подчеркивания
|
|
||||||
</div>
|
|
||||||
{errors().slug && (
|
{errors().slug && (
|
||||||
<div class={formStyles.fieldError}>
|
<div class={formStyles.fieldError}>
|
||||||
<span class={formStyles.errorIcon}>⚠️</span>
|
<span class={formStyles.errorIcon}>⚠️</span>
|
||||||
@@ -329,6 +257,7 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Роли */}
|
{/* Роли */}
|
||||||
<div class={formStyles.fieldGroup}>
|
<div class={formStyles.fieldGroup}>
|
||||||
@@ -346,8 +275,9 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
<For each={AVAILABLE_ROLES}>
|
<For each={AVAILABLE_ROLES}>
|
||||||
{(role) => {
|
{(role) => {
|
||||||
const isAdminRole = role.id === 'admin'
|
const isAdminRole = role.id === 'admin'
|
||||||
const isSelected = formData().roles.includes(role.id)
|
const isSelected = (formData().roles || []).includes(role.id)
|
||||||
const isDisabled = isAdminRole
|
const isDisabled = isAdminRole
|
||||||
|
const roleInfo = getRoleInfo(role.id)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
@@ -369,7 +299,7 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
<div class={formStyles.roleHeader}>
|
<div class={formStyles.roleHeader}>
|
||||||
<span class={formStyles.roleName}>
|
<span class={formStyles.roleName}>
|
||||||
<span style={{ 'margin-right': '0.5rem', 'font-size': '1.1rem' }}>
|
<span style={{ 'margin-right': '0.5rem', 'font-size': '1.1rem' }}>
|
||||||
{role.emoji}
|
{roleInfo.emoji}
|
||||||
</span>
|
</span>
|
||||||
{role.name}
|
{role.name}
|
||||||
{isAdminRole && (
|
{isAdminRole && (
|
||||||
@@ -418,20 +348,11 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Компактные кнопки действий */}
|
{/* Компактные кнопки действий */}
|
||||||
<div
|
<div class={formStyles.actions}>
|
||||||
style={{
|
<Button type="button" onClick={props.onClose} disabled={loading()}>
|
||||||
display: 'flex',
|
|
||||||
gap: '0.75rem',
|
|
||||||
'justify-content': 'flex-end',
|
|
||||||
'margin-top': '1.5rem',
|
|
||||||
'padding-top': '1rem',
|
|
||||||
'border-top': '1px solid var(--form-divider)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button variant="secondary" onClick={props.onClose} disabled={loading()}>
|
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" onClick={handleSave} loading={loading()}>
|
<Button type="button" onClick={handleSave} disabled={loading()}>
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -3,8 +3,7 @@ import type { AuthorsSortField } from '../context/sort'
|
|||||||
import { AUTHORS_SORT_CONFIG } from '../context/sortConfig'
|
import { AUTHORS_SORT_CONFIG } from '../context/sortConfig'
|
||||||
import { query } from '../graphql'
|
import { query } from '../graphql'
|
||||||
import type { Query, AdminUserInfo as User } from '../graphql/generated/schema'
|
import type { Query, AdminUserInfo as User } from '../graphql/generated/schema'
|
||||||
import { ADMIN_UPDATE_USER_MUTATION } from '../graphql/mutations'
|
import { ADMIN_GET_USERS_QUERY, ADMIN_UPDATE_USER_MUTATION } from '../graphql/queries'
|
||||||
import { ADMIN_GET_USERS_QUERY } from '../graphql/queries'
|
|
||||||
import UserEditModal from '../modals/RolesModal'
|
import UserEditModal from '../modals/RolesModal'
|
||||||
import styles from '../styles/Admin.module.css'
|
import styles from '../styles/Admin.module.css'
|
||||||
import Pagination from '../ui/Pagination'
|
import Pagination from '../ui/Pagination'
|
||||||
@@ -18,19 +17,13 @@ export interface AuthorsRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
||||||
console.log('[AuthorsRoute] Initializing...')
|
const [users, setUsers] = createSignal<User[]>([])
|
||||||
const [authors, setUsers] = createSignal<User[]>([])
|
|
||||||
const [loading, setLoading] = createSignal(true)
|
const [loading, setLoading] = createSignal(true)
|
||||||
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
|
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
|
||||||
const [showEditModal, setShowEditModal] = createSignal(false)
|
const [showEditModal, setShowEditModal] = createSignal(false)
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const [pagination, setPagination] = createSignal<{
|
const [pagination, setPagination] = createSignal({
|
||||||
page: number
|
|
||||||
limit: number
|
|
||||||
total: number
|
|
||||||
totalPages: number
|
|
||||||
}>({
|
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 20,
|
limit: 20,
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -44,7 +37,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
* Загрузка списка пользователей с учетом пагинации и поиска
|
* Загрузка списка пользователей с учетом пагинации и поиска
|
||||||
*/
|
*/
|
||||||
async function loadUsers() {
|
async function loadUsers() {
|
||||||
console.log('[AuthorsRoute] Loading authors...')
|
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const data = await query<{ adminGetUsers: Query['adminGetUsers'] }>(
|
const data = await query<{ adminGetUsers: Query['adminGetUsers'] }>(
|
||||||
@@ -57,7 +49,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if (data?.adminGetUsers?.authors) {
|
if (data?.adminGetUsers?.authors) {
|
||||||
console.log('[AuthorsRoute] Users loaded:', data.adminGetUsers.authors.length)
|
|
||||||
setUsers(data.adminGetUsers.authors)
|
setUsers(data.adminGetUsers.authors)
|
||||||
setPagination((prev) => ({
|
setPagination((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -76,51 +67,33 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
/**
|
/**
|
||||||
* Обновляет данные пользователя (профиль и роли)
|
* Обновляет данные пользователя (профиль и роли)
|
||||||
*/
|
*/
|
||||||
async function updateUser(userData: {
|
const updateUser = async (userData: {
|
||||||
id: number
|
id: number
|
||||||
email?: string
|
email?: string
|
||||||
name?: string
|
name?: string
|
||||||
slug?: string
|
slug?: string
|
||||||
roles: string[]
|
roles: string
|
||||||
}) {
|
}) => {
|
||||||
try {
|
try {
|
||||||
await query(`${location.origin}/graphql`, ADMIN_UPDATE_USER_MUTATION, {
|
const result = await query<{
|
||||||
user: userData
|
updateUser: User
|
||||||
})
|
}>(`${location.origin}/graphql`, ADMIN_UPDATE_USER_MUTATION, {
|
||||||
|
...userData,
|
||||||
setUsers((prev) =>
|
|
||||||
prev.map((user) => {
|
|
||||||
if (user.id === userData.id) {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
email: userData.email || user.email,
|
|
||||||
name: userData.name || user.name,
|
|
||||||
slug: userData.slug || user.slug,
|
|
||||||
roles: userData.roles
|
roles: userData.roles
|
||||||
}
|
|
||||||
}
|
|
||||||
return user
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (result.updateUser) {
|
||||||
|
// Обновляем локальный список пользователей
|
||||||
|
setUsers((prevUsers) =>
|
||||||
|
prevUsers.map((user) => (user.id === result.updateUser.id ? result.updateUser : user))
|
||||||
)
|
)
|
||||||
|
// Закрываем модальное окно
|
||||||
closeEditModal()
|
|
||||||
props.onSuccess?.('Данные пользователя успешно обновлены')
|
|
||||||
void loadUsers()
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Ошибка обновления пользователя:', err)
|
|
||||||
let errorMessage = err instanceof Error ? err.message : 'Ошибка обновления данных пользователя'
|
|
||||||
|
|
||||||
if (errorMessage.includes('author_role.community')) {
|
|
||||||
errorMessage = 'Ошибка: для роли author требуется указать community. Обратитесь к администратору.'
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onError?.(errorMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeEditModal() {
|
|
||||||
setShowEditModal(false)
|
setShowEditModal(false)
|
||||||
setSelectedUser(null)
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при обновлении пользователя:', error)
|
||||||
|
props.onError?.(error instanceof Error ? error.message : 'Не удалось обновить пользователя')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pagination handlers
|
// Pagination handlers
|
||||||
@@ -134,11 +107,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
void loadUsers()
|
void loadUsers()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search handlers
|
|
||||||
function handleSearchChange(value: string) {
|
|
||||||
setSearchQuery(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
setPagination((prev) => ({ ...prev, page: 1 }))
|
setPagination((prev) => ({ ...prev, page: 1 }))
|
||||||
void loadUsers()
|
void loadUsers()
|
||||||
@@ -146,7 +114,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
|
|
||||||
// Load authors on mount
|
// Load authors on mount
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
console.log('[AuthorsRoute] Component mounted, loading authors...')
|
|
||||||
void loadUsers()
|
void loadUsers()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -156,35 +123,24 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
const RoleBadge: Component<{ role: string }> = (props) => {
|
const RoleBadge: Component<{ role: string }> = (props) => {
|
||||||
const getRoleIcon = (role: string): string => {
|
const getRoleIcon = (role: string): string => {
|
||||||
switch (role.toLowerCase().trim()) {
|
switch (role.toLowerCase().trim()) {
|
||||||
case 'администратор':
|
|
||||||
case 'admin':
|
case 'admin':
|
||||||
return '🪄'
|
return '🔧'
|
||||||
case 'редактор':
|
|
||||||
case 'editor':
|
case 'editor':
|
||||||
return '✒️'
|
return '✒️'
|
||||||
case 'эксперт':
|
|
||||||
case 'expert':
|
case 'expert':
|
||||||
return '🔬'
|
return '🔬'
|
||||||
case 'автор':
|
|
||||||
case 'author':
|
case 'author':
|
||||||
return '📝'
|
return '📝'
|
||||||
case 'читатель':
|
|
||||||
case 'reader':
|
case 'reader':
|
||||||
return '📖'
|
return '📖'
|
||||||
case 'banned':
|
|
||||||
case 'заблокирован':
|
|
||||||
return '🚫'
|
|
||||||
case 'verified':
|
|
||||||
case 'проверен':
|
|
||||||
return '✓'
|
|
||||||
default:
|
default:
|
||||||
return '👤'
|
return '👤'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span title={props.role} style={{ 'margin-right': '0.25rem' }}>
|
<span title={props.role}>
|
||||||
{getRoleIcon(props.role)}
|
{getRoleIcon(props.role)} {props.role}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -195,20 +151,18 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
<div class={styles['loading']}>Загрузка данных...</div>
|
<div class={styles['loading']}>Загрузка данных...</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!loading() && authors().length === 0}>
|
<Show when={!loading() && users().length === 0}>
|
||||||
<div class={styles['empty-state']}>Нет данных для отображения</div>
|
<div class={styles['empty-state']}>Нет данных для отображения</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<Show when={!loading() && authors().length > 0}>
|
<Show when={!loading() && users().length > 0}>
|
||||||
<TableControls
|
<TableControls
|
||||||
searchValue={searchQuery()}
|
searchValue={searchQuery()}
|
||||||
onSearchChange={handleSearchChange}
|
onSearchChange={setSearchQuery}
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
searchPlaceholder="Поиск по email, имени или ID..."
|
searchPlaceholder="Поиск по email, имени или ID..."
|
||||||
isLoading={loading()}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class={styles['authors-list']}>
|
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -240,7 +194,7 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<For each={authors()}>
|
<For each={users()}>
|
||||||
{(user) => (
|
{(user) => (
|
||||||
<tr
|
<tr
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -254,10 +208,7 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
<td>{formatDateRelative(user.created_at || Date.now())()}</td>
|
<td>{formatDateRelative(user.created_at || Date.now())()}</td>
|
||||||
<td class={styles['roles-cell']}>
|
<td class={styles['roles-cell']}>
|
||||||
<div class={styles['roles-container']}>
|
<div class={styles['roles-container']}>
|
||||||
<For each={Array.from(user.roles || []).filter(Boolean)}>
|
<For each={user.roles || []}>{(role) => <RoleBadge role={role.trim()} />}</For>
|
||||||
{(role) => <RoleBadge role={role} />}
|
|
||||||
</For>
|
|
||||||
{/* Показываем сообщение если ролей нет */}
|
|
||||||
{(!user.roles || user.roles.length === 0) && (
|
{(!user.roles || user.roles.length === 0) && (
|
||||||
<span style="color: #999; font-size: 0.875rem;">Нет ролей</span>
|
<span style="color: #999; font-size: 0.875rem;">Нет ролей</span>
|
||||||
)}
|
)}
|
||||||
@@ -268,7 +219,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
</For>
|
</For>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={pagination().page}
|
currentPage={pagination().page}
|
||||||
@@ -284,7 +234,7 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
|
|||||||
<UserEditModal
|
<UserEditModal
|
||||||
user={selectedUser()!}
|
user={selectedUser()!}
|
||||||
isOpen={showEditModal()}
|
isOpen={showEditModal()}
|
||||||
onClose={closeEditModal}
|
onClose={() => setShowEditModal(false)}
|
||||||
onSave={updateUser}
|
onSave={updateUser}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
Reference in New Issue
Block a user