/** * Компонент страницы администратора * @module AdminPage */ import { useNavigate } from '@solidjs/router' import { Component, For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' import { query } from './graphql' import { isAuthenticated, logout } from './auth' /** * Интерфейс для данных пользователя */ 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[] } /** * Компонент страницы администратора */ const AdminPage: Component = () => { const [activeTab, setActiveTab] = createSignal('users') const [users, setUsers] = createSignal([]) const [roles, setRoles] = createSignal([]) const [loading, setLoading] = createSignal(true) const [error, setError] = createSignal(null) const [selectedUser, setSelectedUser] = createSignal(null) const [showRolesModal, setShowRolesModal] = createSignal(false) const [successMessage, setSuccessMessage] = createSignal(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('') const navigate = useNavigate() // Периодическая проверка авторизации 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( ` 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 { const data = await query(` query AdminGetRoles { adminGetRoles { id name description } } `) 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) { if (page < 1 || page > pagination().totalPages) return setPagination((prev) => ({ ...prev, page })) loadUsers() } /** * Обработчик изменения количества записей на странице * @param limit - Количество записей на странице */ function handlePerPageChange(limit: number) { setPagination((prev) => ({ ...prev, page: 1, limit })) loadUsers() } /** * Обработчик изменения поискового запроса * @param e - Событие изменения ввода */ function handleSearchChange(e: Event) { const target = e.target as HTMLInputElement setSearchQuery(target.value) } /** * Выполняет поиск при нажатии Enter или кнопки поиска */ function handleSearch() { setPagination((prev) => ({ ...prev, page: 1 })) // Сбрасываем на первую страницу при поиске loadUsers() } /** * Обработчик нажатия клавиши в поле поиска * @param e - Событие нажатия клавиши */ function handleSearchKeyDown(e: KeyboardEvent) { if (e.key === 'Enter') { e.preventDefault() handleSearch() } } /** * Блокировка/разблокировка пользователя * @param userId - ID пользователя * @param isActive - Текущий статус активности */ async function toggleUserBlock(userId: number, isActive: boolean) { // Запрашиваем подтверждение const action = isActive ? 'заблокировать' : 'разблокировать' if (!confirm(`Вы действительно хотите ${action} этого пользователя?`)) { return } try { await query( ` mutation AdminToggleUserBlock($userId: Int!) { adminToggleUserBlock(userId: $userId) { success error } } `, { userId } ) // Обновляем статус пользователя setUsers((prev) => prev.map((user) => { if (user.id === userId) { return { ...user, is_active: !isActive } } return user }) ) // Показываем сообщение об успехе setSuccessMessage(`Пользователь успешно ${isActive ? 'заблокирован' : 'разблокирован'}`) // Скрываем сообщение через 3 секунды setTimeout(() => setSuccessMessage(null), 3000) } catch (err) { console.error('Ошибка изменения статуса блокировки:', err) setError(err instanceof Error ? err.message : 'Ошибка изменения статуса блокировки') } } /** * Включение/отключение режима "mute" для пользователя * @param userId - ID пользователя * @param isMuted - Текущий статус mute */ async function toggleUserMute(userId: number, isMuted: boolean) { // Запрашиваем подтверждение const action = isMuted ? 'включить звук' : 'отключить звук' if (!confirm(`Вы действительно хотите ${action} для этого пользователя?`)) { return } try { await query( ` mutation AdminToggleUserMute($userId: Int!) { adminToggleUserMute(userId: $userId) { success error } } `, { userId } ) // Обновляем статус пользователя setUsers((prev) => prev.map((user) => { if (user.id === userId) { return { ...user, muted: !isMuted } } return user }) ) // Показываем сообщение об успехе setSuccessMessage(`Звук для пользователя успешно ${isMuted ? 'включен' : 'отключен'}`) // Скрываем сообщение через 3 секунды setTimeout(() => setSuccessMessage(null), 3000) } catch (err) { console.error('Ошибка изменения статуса mute:', err) setError(err instanceof Error ? err.message : 'Ошибка изменения статуса mute') } } /** * Закрывает модальное окно управления ролями */ function closeRolesModal() { setShowRolesModal(false) setSelectedUser(null) } /** * Обновляет роли пользователя * @param userId - ID пользователя * @param roles - Новый список ролей */ async function updateUserRoles(userId: number, newRoles: string[]) { try { await query( ` 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(() => { // Для гарантии перенаправления после выхода window.location.href = '/login' }) } /** * Форматирование даты * @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 ( ) } /** * Компонент модального окна для управления ролями */ const RolesModal: Component = () => { const user = selectedUser() const [selectedRoles, setSelectedRoles] = createSignal(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 ( ) } return (

Панель администратора

{error()}
{successMessage()}
Загрузка данных...
Нет данных для отображения
0}>
{(user) => ( )}
ID Email Имя Роли Создан Последний вход Статус Действия
{user.id} {user.email} {user.name || '-'} {user.roles.join(', ') || '-'} {formatDate(user.created_at)} {formatDate(user.last_seen)} {user.is_active ? 'Активен' : 'Заблокирован'}
) } export default AdminPage