Files
core/panel/routes/authors.tsx

242 lines
7.6 KiB
TypeScript
Raw Normal View History

2025-06-30 21:25:26 +03:00
import { Component, createSignal, For, onMount, Show } from 'solid-js'
2025-07-02 22:30:21 +03:00
import type { AuthorsSortField } from '../context/sort'
import { AUTHORS_SORT_CONFIG } from '../context/sortConfig'
2025-06-30 21:25:26 +03:00
import { query } from '../graphql'
import type { Query, AdminUserInfo as User } from '../graphql/generated/schema'
2025-07-25 10:50:03 +03:00
import { ADMIN_GET_USERS_QUERY, ADMIN_UPDATE_USER_MUTATION } from '../graphql/queries'
2025-06-30 21:25:26 +03:00
import UserEditModal from '../modals/RolesModal'
import styles from '../styles/Admin.module.css'
import Pagination from '../ui/Pagination'
2025-07-02 22:30:21 +03:00
import SortableHeader from '../ui/SortableHeader'
import TableControls from '../ui/TableControls'
2025-06-30 21:25:26 +03:00
import { formatDateRelative } from '../utils/date'
export interface AuthorsRouteProps {
onError?: (error: string) => void
onSuccess?: (message: string) => void
}
const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
2025-07-25 10:50:03 +03:00
const [users, setUsers] = createSignal<User[]>([])
2025-06-30 21:25:26 +03:00
const [loading, setLoading] = createSignal(true)
const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
const [showEditModal, setShowEditModal] = createSignal(false)
// Pagination state
2025-07-25 10:50:03 +03:00
const [pagination, setPagination] = createSignal({
2025-06-30 21:25:26 +03:00
page: 1,
2025-07-02 22:30:21 +03:00
limit: 20,
2025-06-30 21:25:26 +03:00
total: 0,
totalPages: 1
})
// Search state
const [searchQuery, setSearchQuery] = createSignal('')
/**
* Загрузка списка пользователей с учетом пагинации и поиска
*/
async function loadUsers() {
try {
setLoading(true)
const data = await query<{ adminGetUsers: Query['adminGetUsers'] }>(
`${location.origin}/graphql`,
ADMIN_GET_USERS_QUERY,
{
search: searchQuery(),
limit: pagination().limit,
offset: (pagination().page - 1) * pagination().limit
}
)
if (data?.adminGetUsers?.authors) {
setUsers(data.adminGetUsers.authors)
setPagination((prev) => ({
...prev,
total: data.adminGetUsers.total || 0,
totalPages: data.adminGetUsers.totalPages || 1
}))
}
} catch (error) {
console.error('[AuthorsRoute] Failed to load authors:', error)
2025-07-02 22:30:21 +03:00
props.onError?.(error instanceof Error ? error.message : 'Не удалось загрузить список пользователей')
2025-06-30 21:25:26 +03:00
} finally {
setLoading(false)
}
}
/**
* Обновляет данные пользователя (профиль и роли)
*/
2025-07-25 10:50:03 +03:00
const updateUser = async (userData: {
2025-06-30 21:25:26 +03:00
id: number
email?: string
name?: string
slug?: string
2025-07-25 10:50:03 +03:00
roles: string
}) => {
2025-06-30 21:25:26 +03:00
try {
2025-07-25 10:50:03 +03:00
const result = await query<{
updateUser: User
}>(`${location.origin}/graphql`, ADMIN_UPDATE_USER_MUTATION, {
...userData,
roles: userData.roles
2025-06-30 21:25:26 +03:00
})
2025-07-25 10:50:03 +03:00
if (result.updateUser) {
// Обновляем локальный список пользователей
setUsers((prevUsers) =>
prevUsers.map((user) => (user.id === result.updateUser.id ? result.updateUser : user))
)
// Закрываем модальное окно
setShowEditModal(false)
2025-06-30 21:25:26 +03:00
}
2025-07-25 10:50:03 +03:00
} catch (error) {
console.error('Ошибка при обновлении пользователя:', error)
props.onError?.(error instanceof Error ? error.message : 'Не удалось обновить пользователя')
2025-06-30 21:25:26 +03:00
}
}
// Pagination handlers
function handlePageChange(page: number) {
setPagination((prev) => ({ ...prev, page }))
void loadUsers()
}
function handlePerPageChange(limit: number) {
setPagination((prev) => ({ ...prev, page: 1, limit }))
void loadUsers()
}
function handleSearch() {
setPagination((prev) => ({ ...prev, page: 1 }))
void loadUsers()
}
// Load authors on mount
onMount(() => {
void loadUsers()
})
/**
2025-07-02 22:30:21 +03:00
* Компонент для отображения роли с эмоджи и тултипом
2025-06-30 21:25:26 +03:00
*/
const RoleBadge: Component<{ role: string }> = (props) => {
const getRoleIcon = (role: string): string => {
2025-07-02 22:30:21 +03:00
switch (role.toLowerCase().trim()) {
2025-06-30 21:25:26 +03:00
case 'admin':
2025-07-25 10:50:03 +03:00
return '🔧'
2025-06-30 21:25:26 +03:00
case 'editor':
2025-07-02 22:30:21 +03:00
return '✒️'
2025-06-30 21:25:26 +03:00
case 'expert':
2025-07-02 22:30:21 +03:00
return '🔬'
2025-06-30 21:25:26 +03:00
case 'author':
return '📝'
case 'reader':
2025-07-02 22:30:21 +03:00
return '📖'
2025-06-30 21:25:26 +03:00
default:
2025-07-03 00:20:10 +03:00
return '👤'
2025-06-30 21:25:26 +03:00
}
}
2025-07-25 11:05:42 +03:00
return <span title={props.role}>{getRoleIcon(props.role)}</span>
2025-06-30 21:25:26 +03:00
}
return (
<div class={styles['authors-container']}>
<Show when={loading()}>
<div class={styles['loading']}>Загрузка данных...</div>
</Show>
2025-07-25 10:50:03 +03:00
<Show when={!loading() && users().length === 0}>
2025-06-30 21:25:26 +03:00
<div class={styles['empty-state']}>Нет данных для отображения</div>
</Show>
2025-07-25 10:50:03 +03:00
<Show when={!loading() && users().length > 0}>
2025-07-02 22:30:21 +03:00
<TableControls
searchValue={searchQuery()}
2025-07-25 10:50:03 +03:00
onSearchChange={setSearchQuery}
2025-07-02 22:30:21 +03:00
onSearch={handleSearch}
searchPlaceholder="Поиск по email, имени или ID..."
/>
2025-06-30 21:25:26 +03:00
2025-07-25 10:50:03 +03:00
<table>
<thead>
<tr>
<SortableHeader
field={'id' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
ID
</SortableHeader>
<SortableHeader
field={'email' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Email
</SortableHeader>
<SortableHeader
field={'name' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Имя
</SortableHeader>
<SortableHeader
field={'created_at' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Создан
</SortableHeader>
<th>Роли</th>
</tr>
</thead>
<tbody>
<For each={users()}>
{(user) => (
<tr
onClick={() => {
setSelectedUser(user)
setShowEditModal(true)
}}
2025-07-02 22:30:21 +03:00
>
2025-07-25 10:50:03 +03:00
<td>{user.id}</td>
<td>{user.email}</td>
<td>{user.name || '-'}</td>
<td>{formatDateRelative(user.created_at || Date.now())()}</td>
<td class={styles['roles-cell']}>
<div class={styles['roles-container']}>
<For each={user.roles || []}>{(role) => <RoleBadge role={role.trim()} />}</For>
{(!user.roles || user.roles.length === 0) && (
<span style="color: #999; font-size: 0.875rem;">Нет ролей</span>
)}
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
2025-06-30 21:25:26 +03:00
<Pagination
currentPage={pagination().page}
totalPages={pagination().totalPages}
total={pagination().total}
limit={pagination().limit}
onPageChange={handlePageChange}
onPerPageChange={handlePerPageChange}
/>
</Show>
<Show when={showEditModal() && selectedUser()}>
<UserEditModal
user={selectedUser()!}
isOpen={showEditModal()}
2025-07-25 10:50:03 +03:00
onClose={() => setShowEditModal(false)}
2025-06-30 21:25:26 +03:00
onSave={updateUser}
/>
</Show>
</div>
)
}
export default AuthorsRoute