2025-05-16 06:23:48 +00:00
|
|
|
|
/**
|
|
|
|
|
* Компонент страницы администратора
|
|
|
|
|
* @module AdminPage
|
|
|
|
|
*/
|
|
|
|
|
|
2025-05-16 07:30:02 +00:00
|
|
|
|
import { Component, For, Show, createSignal, onMount } from 'solid-js'
|
|
|
|
|
import { logout } from './auth'
|
2025-05-16 06:23:48 +00:00
|
|
|
|
import { query } from './graphql'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для данных пользователя
|
|
|
|
|
*/
|
|
|
|
|
interface User {
|
|
|
|
|
id: number
|
|
|
|
|
email: string
|
|
|
|
|
name?: string
|
|
|
|
|
slug?: string
|
|
|
|
|
roles: string[]
|
|
|
|
|
created_at?: number
|
|
|
|
|
last_seen?: number
|
|
|
|
|
muted: boolean
|
|
|
|
|
is_active: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для роли пользователя
|
|
|
|
|
*/
|
|
|
|
|
interface Role {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
description?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для ответа API с пользователями
|
|
|
|
|
*/
|
|
|
|
|
interface AdminGetUsersResponse {
|
|
|
|
|
adminGetUsers: {
|
|
|
|
|
users: User[]
|
|
|
|
|
total: number
|
|
|
|
|
page: number
|
|
|
|
|
perPage: number
|
|
|
|
|
totalPages: number
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для ответа API с ролями
|
|
|
|
|
*/
|
|
|
|
|
interface AdminGetRolesResponse {
|
|
|
|
|
adminGetRoles: Role[]
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-19 08:25:41 +00:00
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для ответа изменения статуса пользователя
|
|
|
|
|
*/
|
|
|
|
|
interface AdminSetUserStatusResponse {
|
|
|
|
|
adminSetUserStatus: {
|
|
|
|
|
success: boolean
|
|
|
|
|
error?: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Интерфейс для ответа изменения статуса блокировки чата
|
|
|
|
|
*/
|
|
|
|
|
interface AdminMuteUserResponse {
|
|
|
|
|
adminMuteUser: {
|
|
|
|
|
success: boolean
|
|
|
|
|
error?: string
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 07:30:02 +00:00
|
|
|
|
// Интерфейс для пропсов AdminPage
|
|
|
|
|
interface AdminPageProps {
|
|
|
|
|
onLogout?: () => void
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-16 06:23:48 +00:00
|
|
|
|
/**
|
|
|
|
|
* Компонент страницы администратора
|
|
|
|
|
*/
|
2025-05-16 07:30:02 +00:00
|
|
|
|
const AdminPage: Component<AdminPageProps> = (props) => {
|
2025-05-16 06:23:48 +00:00
|
|
|
|
const [activeTab, setActiveTab] = createSignal('users')
|
|
|
|
|
const [users, setUsers] = createSignal<User[]>([])
|
|
|
|
|
const [roles, setRoles] = createSignal<Role[]>([])
|
|
|
|
|
const [loading, setLoading] = createSignal(true)
|
|
|
|
|
const [error, setError] = createSignal<string | null>(null)
|
|
|
|
|
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
|
|
|
|
|
const [showRolesModal, setShowRolesModal] = createSignal(false)
|
|
|
|
|
const [successMessage, setSuccessMessage] = createSignal<string | null>(null)
|
|
|
|
|
|
|
|
|
|
// Параметры пагинации
|
|
|
|
|
const [pagination, setPagination] = createSignal<{
|
|
|
|
|
page: number
|
|
|
|
|
limit: number
|
|
|
|
|
total: number
|
|
|
|
|
totalPages: number
|
|
|
|
|
}>({
|
|
|
|
|
page: 1,
|
|
|
|
|
limit: 10,
|
|
|
|
|
total: 0,
|
|
|
|
|
totalPages: 1
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Поиск
|
|
|
|
|
const [searchQuery, setSearchQuery] = createSignal('')
|
|
|
|
|
|
|
|
|
|
// Периодическая проверка авторизации
|
|
|
|
|
onMount(() => {
|
|
|
|
|
// Загружаем данные при монтировании
|
|
|
|
|
loadUsers()
|
|
|
|
|
loadRoles()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Загрузка списка пользователей с учетом пагинации и поиска
|
|
|
|
|
*/
|
|
|
|
|
async function loadUsers() {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { page, limit } = pagination()
|
|
|
|
|
const offset = (page - 1) * limit
|
|
|
|
|
const search = searchQuery().trim()
|
|
|
|
|
|
|
|
|
|
const data = await query<AdminGetUsersResponse>(
|
2025-05-16 07:30:02 +00:00
|
|
|
|
`${location.origin}/graphql`,
|
2025-05-16 06:23:48 +00:00
|
|
|
|
`
|
|
|
|
|
query AdminGetUsers($limit: Int, $offset: Int, $search: String) {
|
|
|
|
|
adminGetUsers(limit: $limit, offset: $offset, search: $search) {
|
|
|
|
|
users {
|
|
|
|
|
id
|
|
|
|
|
email
|
|
|
|
|
name
|
|
|
|
|
slug
|
|
|
|
|
roles
|
|
|
|
|
created_at
|
|
|
|
|
last_seen
|
|
|
|
|
muted
|
|
|
|
|
is_active
|
|
|
|
|
}
|
|
|
|
|
total
|
|
|
|
|
page
|
|
|
|
|
perPage
|
|
|
|
|
totalPages
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
{ limit, offset, search: search || null }
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (data?.adminGetUsers) {
|
|
|
|
|
setUsers(data.adminGetUsers.users)
|
|
|
|
|
setPagination({
|
|
|
|
|
page: data.adminGetUsers.page,
|
|
|
|
|
limit: data.adminGetUsers.perPage,
|
|
|
|
|
total: data.adminGetUsers.total,
|
|
|
|
|
totalPages: data.adminGetUsers.totalPages
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Ошибка загрузки пользователей:', err)
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Неизвестная ошибка')
|
|
|
|
|
|
|
|
|
|
// Если ошибка авторизации - перенаправляем на логин
|
|
|
|
|
if (
|
|
|
|
|
err instanceof Error &&
|
|
|
|
|
(err.message.includes('401') ||
|
|
|
|
|
err.message.includes('авторизации') ||
|
|
|
|
|
err.message.includes('unauthorized') ||
|
|
|
|
|
err.message.includes('Unauthorized'))
|
|
|
|
|
) {
|
|
|
|
|
handleLogout()
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Загрузка списка ролей
|
|
|
|
|
*/
|
|
|
|
|
async function loadRoles() {
|
|
|
|
|
try {
|
2025-05-16 07:30:02 +00:00
|
|
|
|
const data = await query<AdminGetRolesResponse>(
|
|
|
|
|
`${location.origin}/graphql`,
|
|
|
|
|
`
|
2025-05-16 06:23:48 +00:00
|
|
|
|
query AdminGetRoles {
|
|
|
|
|
adminGetRoles {
|
|
|
|
|
id
|
|
|
|
|
name
|
|
|
|
|
description
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-05-16 07:30:02 +00:00
|
|
|
|
`
|
|
|
|
|
)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
|
|
|
|
|
if (data?.adminGetRoles) {
|
|
|
|
|
setRoles(data.adminGetRoles)
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Ошибка загрузки ролей:', err)
|
|
|
|
|
// Если ошибка авторизации - перенаправляем на логин
|
|
|
|
|
if (
|
|
|
|
|
err instanceof Error &&
|
|
|
|
|
(err.message.includes('401') ||
|
|
|
|
|
err.message.includes('авторизации') ||
|
|
|
|
|
err.message.includes('unauthorized') ||
|
|
|
|
|
err.message.includes('Unauthorized'))
|
|
|
|
|
) {
|
|
|
|
|
handleLogout()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обработчик изменения страницы
|
|
|
|
|
* @param page - Номер страницы
|
|
|
|
|
*/
|
|
|
|
|
function handlePageChange(page: number) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
setPagination({ ...pagination(), page })
|
2025-05-16 06:23:48 +00:00
|
|
|
|
loadUsers()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Обработчик изменения количества элементов на странице
|
|
|
|
|
* @param limit - Количество элементов
|
2025-05-16 06:23:48 +00:00
|
|
|
|
*/
|
|
|
|
|
function handlePerPageChange(limit: number) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
setPagination({ ...pagination(), page: 1, limit })
|
2025-05-16 06:23:48 +00:00
|
|
|
|
loadUsers()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обработчик изменения поискового запроса
|
|
|
|
|
*/
|
|
|
|
|
function handleSearchChange(e: Event) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
const input = e.target as HTMLInputElement
|
|
|
|
|
setSearchQuery(input.value)
|
2025-05-16 06:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Выполняет поиск
|
2025-05-16 06:23:48 +00:00
|
|
|
|
*/
|
|
|
|
|
function handleSearch() {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
setPagination({ ...pagination(), page: 1 })
|
2025-05-16 06:23:48 +00:00
|
|
|
|
loadUsers()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Обработчик нажатия клавиш в поле поиска
|
|
|
|
|
* @param e - Событие клавиатуры
|
2025-05-16 06:23:48 +00:00
|
|
|
|
*/
|
|
|
|
|
function handleSearchKeyDown(e: KeyboardEvent) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
// Если нажат Enter, выполняем поиск
|
2025-05-16 06:23:48 +00:00
|
|
|
|
if (e.key === 'Enter') {
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
handleSearch()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Блокирует/разблокирует пользователя
|
2025-05-16 06:23:48 +00:00
|
|
|
|
* @param userId - ID пользователя
|
|
|
|
|
* @param isActive - Текущий статус активности
|
|
|
|
|
*/
|
|
|
|
|
async function toggleUserBlock(userId: number, isActive: boolean) {
|
|
|
|
|
try {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
// Устанавливаем новый статус (противоположный текущему)
|
|
|
|
|
const newStatus = !isActive
|
|
|
|
|
|
|
|
|
|
// Выполняем мутацию
|
|
|
|
|
const result = await query<AdminSetUserStatusResponse>(
|
2025-05-16 07:30:02 +00:00
|
|
|
|
`${location.origin}/graphql`,
|
2025-05-16 06:23:48 +00:00
|
|
|
|
`
|
2025-05-19 08:25:41 +00:00
|
|
|
|
mutation AdminSetUserStatus($userId: Int!, $isActive: Boolean!) {
|
|
|
|
|
adminSetUserStatus(userId: $userId, isActive: $isActive) {
|
|
|
|
|
success
|
|
|
|
|
error
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
}
|
2025-05-19 08:25:41 +00:00
|
|
|
|
`,
|
|
|
|
|
{ userId, isActive: newStatus }
|
2025-05-16 06:23:48 +00:00
|
|
|
|
)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
// Проверяем результат
|
|
|
|
|
if (result?.adminSetUserStatus?.success) {
|
|
|
|
|
// Обновляем список пользователей
|
|
|
|
|
setSuccessMessage(`Пользователь ${newStatus ? 'разблокирован' : 'заблокирован'}`)
|
|
|
|
|
|
|
|
|
|
// Обновляем пользователя в текущем списке
|
|
|
|
|
setUsers(
|
|
|
|
|
users().map((user) =>
|
|
|
|
|
user.id === userId ? { ...user, is_active: newStatus } : user
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Скрываем сообщение через 3 секунды
|
|
|
|
|
setTimeout(() => setSuccessMessage(null), 3000)
|
|
|
|
|
} else {
|
|
|
|
|
setError(result?.adminSetUserStatus?.error || 'Ошибка обновления статуса пользователя')
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
} catch (err) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
console.error('Ошибка при изменении статуса пользователя:', err)
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Неизвестная ошибка')
|
2025-05-16 06:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Включает/отключает режим блокировки чата для пользователя
|
2025-05-16 06:23:48 +00:00
|
|
|
|
* @param userId - ID пользователя
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* @param isMuted - Текущий статус блокировки чата
|
2025-05-16 06:23:48 +00:00
|
|
|
|
*/
|
|
|
|
|
async function toggleUserMute(userId: number, isMuted: boolean) {
|
|
|
|
|
try {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
setError(null)
|
|
|
|
|
|
|
|
|
|
// Устанавливаем новый статус (противоположный текущему)
|
|
|
|
|
const newMuteStatus = !isMuted
|
|
|
|
|
|
|
|
|
|
// Выполняем мутацию
|
|
|
|
|
const result = await query<AdminMuteUserResponse>(
|
2025-05-16 07:30:02 +00:00
|
|
|
|
`${location.origin}/graphql`,
|
2025-05-16 06:23:48 +00:00
|
|
|
|
`
|
2025-05-19 08:25:41 +00:00
|
|
|
|
mutation AdminMuteUser($userId: Int!, $muted: Boolean!) {
|
|
|
|
|
adminMuteUser(userId: $userId, muted: $muted) {
|
|
|
|
|
success
|
|
|
|
|
error
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
}
|
2025-05-19 08:25:41 +00:00
|
|
|
|
`,
|
|
|
|
|
{ userId, muted: newMuteStatus }
|
2025-05-16 06:23:48 +00:00
|
|
|
|
)
|
2025-05-19 08:25:41 +00:00
|
|
|
|
|
|
|
|
|
// Проверяем результат
|
|
|
|
|
if (result?.adminMuteUser?.success) {
|
|
|
|
|
// Обновляем сообщение об успехе
|
|
|
|
|
setSuccessMessage(`${newMuteStatus ? 'Блокировка' : 'Разблокировка'} чата выполнена`)
|
|
|
|
|
|
|
|
|
|
// Обновляем пользователя в текущем списке
|
|
|
|
|
setUsers(
|
|
|
|
|
users().map((user) =>
|
|
|
|
|
user.id === userId ? { ...user, muted: newMuteStatus } : user
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Скрываем сообщение через 3 секунды
|
|
|
|
|
setTimeout(() => setSuccessMessage(null), 3000)
|
|
|
|
|
} else {
|
|
|
|
|
setError(result?.adminMuteUser?.error || 'Ошибка обновления статуса блокировки чата')
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
} catch (err) {
|
2025-05-19 08:25:41 +00:00
|
|
|
|
console.error('Ошибка при изменении статуса блокировки чата:', err)
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Неизвестная ошибка')
|
2025-05-16 06:23:48 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-19 08:25:41 +00:00
|
|
|
|
* Закрывает модальное окно ролей
|
2025-05-16 06:23:48 +00:00
|
|
|
|
*/
|
|
|
|
|
function closeRolesModal() {
|
|
|
|
|
setShowRolesModal(false)
|
|
|
|
|
setSelectedUser(null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обновляет роли пользователя
|
|
|
|
|
* @param userId - ID пользователя
|
|
|
|
|
* @param roles - Новый список ролей
|
|
|
|
|
*/
|
|
|
|
|
async function updateUserRoles(userId: number, newRoles: string[]) {
|
|
|
|
|
try {
|
|
|
|
|
await query(
|
2025-05-16 07:30:02 +00:00
|
|
|
|
`${location.origin}/graphql`,
|
2025-05-16 06:23:48 +00:00
|
|
|
|
`
|
|
|
|
|
mutation AdminUpdateUser($userId: Int!, $input: AdminUserUpdateInput!) {
|
|
|
|
|
adminUpdateUser(userId: $userId, input: $input) {
|
|
|
|
|
success
|
|
|
|
|
error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
{
|
|
|
|
|
userId,
|
|
|
|
|
input: { roles: newRoles }
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Обновляем роли пользователя в списке
|
|
|
|
|
setUsers((prev) =>
|
|
|
|
|
prev.map((user) => {
|
|
|
|
|
if (user.id === userId) {
|
|
|
|
|
return { ...user, roles: newRoles }
|
|
|
|
|
}
|
|
|
|
|
return user
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Закрываем модальное окно
|
|
|
|
|
closeRolesModal()
|
|
|
|
|
|
|
|
|
|
// Показываем сообщение об успехе
|
|
|
|
|
setSuccessMessage('Роли пользователя успешно обновлены')
|
|
|
|
|
|
|
|
|
|
// Скрываем сообщение через 3 секунды
|
|
|
|
|
setTimeout(() => setSuccessMessage(null), 3000)
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Ошибка обновления ролей:', err)
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Ошибка обновления ролей')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Выход из системы
|
|
|
|
|
*/
|
|
|
|
|
function handleLogout() {
|
|
|
|
|
// Сначала выполняем локальные действия по очистке данных
|
|
|
|
|
setUsers([])
|
|
|
|
|
setRoles([])
|
|
|
|
|
|
|
|
|
|
// Затем выполняем выход
|
|
|
|
|
logout(() => {
|
2025-05-16 07:30:02 +00:00
|
|
|
|
// Вызываем коллбэк для оповещения родителя о выходе
|
|
|
|
|
if (props.onLogout) {
|
|
|
|
|
props.onLogout()
|
|
|
|
|
}
|
2025-05-16 06:23:48 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Форматирование даты
|
|
|
|
|
* @param timestamp - Временная метка
|
|
|
|
|
*/
|
|
|
|
|
function formatDate(timestamp?: number): string {
|
|
|
|
|
if (!timestamp) return 'Н/Д'
|
|
|
|
|
return new Date(timestamp * 1000).toLocaleString('ru')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Формирует массив номеров страниц для отображения в пагинации
|
|
|
|
|
* @returns Массив номеров страниц
|
|
|
|
|
*/
|
|
|
|
|
function getPageNumbers(): number[] {
|
|
|
|
|
const result: number[] = []
|
|
|
|
|
const maxVisible = 5 // Максимальное количество видимых номеров страниц
|
|
|
|
|
|
|
|
|
|
const paginationData = pagination()
|
|
|
|
|
const currentPage = paginationData.page
|
|
|
|
|
const totalPages = paginationData.totalPages
|
|
|
|
|
|
|
|
|
|
let startPage = Math.max(1, currentPage - Math.floor(maxVisible / 2))
|
|
|
|
|
const endPage = Math.min(totalPages, startPage + maxVisible - 1)
|
|
|
|
|
|
|
|
|
|
// Если endPage достиг предела, сдвигаем startPage назад
|
|
|
|
|
if (endPage - startPage + 1 < maxVisible && startPage > 1) {
|
|
|
|
|
startPage = Math.max(1, endPage - maxVisible + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Генерируем номера страниц
|
|
|
|
|
for (let i = startPage; i <= endPage; i++) {
|
|
|
|
|
result.push(i)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Компонент пагинации
|
|
|
|
|
*/
|
|
|
|
|
const Pagination: Component = () => {
|
|
|
|
|
const paginationData = pagination()
|
|
|
|
|
const currentPage = paginationData.page
|
|
|
|
|
const total = paginationData.totalPages
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div class="pagination">
|
|
|
|
|
<div class="pagination-info">
|
|
|
|
|
Показано {users().length} из {paginationData.total} пользователей
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pagination-controls">
|
|
|
|
|
<button
|
|
|
|
|
class="pagination-button"
|
|
|
|
|
onClick={() => handlePageChange(currentPage - 1)}
|
|
|
|
|
disabled={currentPage === 1}
|
|
|
|
|
>
|
|
|
|
|
«
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
|
|
<For each={getPageNumbers()}>
|
|
|
|
|
{(page) =>
|
|
|
|
|
typeof page === 'number' ? (
|
|
|
|
|
<button
|
|
|
|
|
class={`pagination-button ${page === currentPage ? 'active' : ''}`}
|
|
|
|
|
onClick={() => handlePageChange(page)}
|
|
|
|
|
>
|
|
|
|
|
{page}
|
|
|
|
|
</button>
|
|
|
|
|
) : (
|
|
|
|
|
<span class="pagination-ellipsis">{page}</span>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
</For>
|
|
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
class="pagination-button"
|
|
|
|
|
onClick={() => handlePageChange(currentPage + 1)}
|
|
|
|
|
disabled={currentPage === total}
|
|
|
|
|
>
|
|
|
|
|
»
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="pagination-per-page">
|
|
|
|
|
<label>
|
|
|
|
|
Записей на странице:
|
|
|
|
|
<select
|
|
|
|
|
value={paginationData.limit}
|
|
|
|
|
onChange={(e) => handlePerPageChange(Number.parseInt(e.target.value))}
|
|
|
|
|
>
|
|
|
|
|
<option value={5}>5</option>
|
|
|
|
|
<option value={10}>10</option>
|
|
|
|
|
<option value={20}>20</option>
|
|
|
|
|
<option value={50}>50</option>
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Компонент модального окна для управления ролями
|
|
|
|
|
*/
|
|
|
|
|
const RolesModal: Component = () => {
|
|
|
|
|
const user = selectedUser()
|
|
|
|
|
const [selectedRoles, setSelectedRoles] = createSignal<string[]>(user ? [...user.roles] : [])
|
|
|
|
|
|
|
|
|
|
const toggleRole = (role: string) => {
|
|
|
|
|
const current = selectedRoles()
|
|
|
|
|
if (current.includes(role)) {
|
|
|
|
|
setSelectedRoles(current.filter((r) => r !== role))
|
|
|
|
|
} else {
|
|
|
|
|
setSelectedRoles([...current, role])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const saveRoles = () => {
|
|
|
|
|
if (user) {
|
|
|
|
|
updateUserRoles(user.id, selectedRoles())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!user) return null
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div class="modal-overlay">
|
|
|
|
|
<div class="modal-content">
|
|
|
|
|
<h2>Управление ролями пользователя</h2>
|
|
|
|
|
<p>Пользователь: {user.email}</p>
|
|
|
|
|
|
|
|
|
|
<div class="roles-list">
|
|
|
|
|
<For each={roles()}>
|
|
|
|
|
{(role) => (
|
|
|
|
|
<div class="role-item">
|
|
|
|
|
<label>
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedRoles().includes(role.name)}
|
|
|
|
|
onChange={() => toggleRole(role.name)}
|
|
|
|
|
/>
|
|
|
|
|
{role.name}
|
|
|
|
|
</label>
|
|
|
|
|
<Show when={role.description}>
|
|
|
|
|
<p class="role-description">{role.description}</p>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="modal-actions">
|
|
|
|
|
<button class="cancel-button" onClick={closeRolesModal}>
|
|
|
|
|
Отмена
|
|
|
|
|
</button>
|
|
|
|
|
<button class="save-button" onClick={saveRoles}>
|
|
|
|
|
Сохранить
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div class="admin-page">
|
|
|
|
|
<header>
|
|
|
|
|
<div class="header-container">
|
|
|
|
|
<h1>Панель администратора</h1>
|
|
|
|
|
<button class="logout-button" onClick={handleLogout}>
|
|
|
|
|
Выйти
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<nav class="admin-tabs">
|
|
|
|
|
<button class={activeTab() === 'users' ? 'active' : ''} onClick={() => setActiveTab('users')}>
|
|
|
|
|
Пользователи
|
|
|
|
|
</button>
|
|
|
|
|
</nav>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<main>
|
|
|
|
|
<Show when={error()}>
|
|
|
|
|
<div class="error-message">{error()}</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={successMessage()}>
|
|
|
|
|
<div class="success-message">{successMessage()}</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<div class="loading">Загрузка данных...</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={!loading() && users().length === 0 && !error()}>
|
|
|
|
|
<div class="empty-state">Нет данных для отображения</div>
|
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={!loading() && users().length > 0}>
|
|
|
|
|
<div class="users-controls">
|
|
|
|
|
<div class="search-container">
|
|
|
|
|
<div class="search-input-group">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Поиск по email, имени или ID..."
|
|
|
|
|
value={searchQuery()}
|
|
|
|
|
onInput={handleSearchChange}
|
|
|
|
|
onKeyDown={handleSearchKeyDown}
|
|
|
|
|
class="search-input"
|
|
|
|
|
/>
|
|
|
|
|
<button class="search-button" onClick={handleSearch}>
|
|
|
|
|
Поиск
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="users-list">
|
|
|
|
|
<table>
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>ID</th>
|
|
|
|
|
<th>Email</th>
|
|
|
|
|
<th>Имя</th>
|
|
|
|
|
<th>Роли</th>
|
|
|
|
|
<th>Создан</th>
|
|
|
|
|
<th>Последний вход</th>
|
|
|
|
|
<th>Статус</th>
|
|
|
|
|
<th>Действия</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody>
|
|
|
|
|
<For each={users()}>
|
|
|
|
|
{(user) => (
|
|
|
|
|
<tr class={user.is_active ? '' : 'blocked'}>
|
|
|
|
|
<td>{user.id}</td>
|
|
|
|
|
<td>{user.email}</td>
|
|
|
|
|
<td>{user.name || '-'}</td>
|
|
|
|
|
<td>{user.roles.join(', ') || '-'}</td>
|
|
|
|
|
<td>{formatDate(user.created_at)}</td>
|
|
|
|
|
<td>{formatDate(user.last_seen)}</td>
|
|
|
|
|
<td>
|
|
|
|
|
<span class={`status ${user.is_active ? 'active' : 'inactive'}`}>
|
|
|
|
|
{user.is_active ? 'Активен' : 'Заблокирован'}
|
|
|
|
|
</span>
|
|
|
|
|
</td>
|
|
|
|
|
<td class="actions">
|
|
|
|
|
<button
|
|
|
|
|
class={user.is_active ? 'block' : 'unblock'}
|
|
|
|
|
onClick={() => toggleUserBlock(user.id, user.is_active)}
|
|
|
|
|
>
|
|
|
|
|
{user.is_active ? 'Блокировать' : 'Разблокировать'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
class={user.muted ? 'unmute' : 'mute'}
|
|
|
|
|
onClick={() => toggleUserMute(user.id, user.muted)}
|
|
|
|
|
>
|
|
|
|
|
{user.muted ? 'Unmute' : 'Mute'}
|
|
|
|
|
</button>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
)}
|
|
|
|
|
</For>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Pagination />
|
|
|
|
|
</Show>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<Show when={showRolesModal()}>
|
|
|
|
|
<RolesModal />
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default AdminPage
|