import { Component, createSignal, For, onMount, Show } from 'solid-js' import { ADMIN_DELETE_INVITE_MUTATION, ADMIN_DELETE_INVITES_BATCH_MUTATION } from '../graphql/mutations' import { ADMIN_GET_INVITES_QUERY } from '../graphql/queries' import styles from '../styles/Table.module.css' import Button from '../ui/Button' import Modal from '../ui/Modal' import Pagination from '../ui/Pagination' import { getAuthTokenFromCookie } from '../utils/auth' /** * Интерфейсы для приглашений */ interface Author { id: number name: string email: string slug: string } interface Shout { id: number title: string slug: string created_by: Author } interface Invite { inviter_id: number author_id: number shout_id: number status: 'PENDING' | 'ACCEPTED' | 'REJECTED' inviter: Author author: Author shout: Shout created_at?: number } interface InvitesRouteProps { onError: (error: string) => void onSuccess: (message: string) => void } /** * Компонент для управления приглашениями */ const InvitesRoute: Component = (props) => { const [invites, setInvites] = createSignal([]) const [loading, setLoading] = createSignal(false) const [search, setSearch] = createSignal('') const [statusFilter, setStatusFilter] = createSignal('all') const [pagination, setPagination] = createSignal({ page: 1, perPage: 10, total: 0, totalPages: 1 }) // Состояние для выбранных приглашений const [selectedInvites, setSelectedInvites] = createSignal>({}) const [selectAll, setSelectAll] = createSignal(false) // Состояние для модального окна подтверждения удаления const [deleteModal, setDeleteModal] = createSignal<{ show: boolean; invite: Invite | null }>({ show: false, invite: null }) // Состояние для модального окна подтверждения пакетного удаления const [batchDeleteModal, setBatchDeleteModal] = createSignal<{ show: boolean }>({ show: false }) /** * Загружает список приглашений с учетом фильтров и пагинации */ const loadInvites = async (page = 1) => { setLoading(true) try { const limit = pagination().perPage const offset = (page - 1) * limit // Получаем токен авторизации из localStorage или cookie const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() console.log(`[InvitesRoute] Загрузка приглашений, токен: ${authToken ? 'найден' : 'не найден'}`) const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: ADMIN_GET_INVITES_QUERY, variables: { limit, offset, search: search().trim() || null, status: statusFilter() === 'all' ? null : statusFilter() } }) }) const result = await response.json() if (result.errors) { throw new Error(result.errors[0].message) } const data = result.data.adminGetInvites setInvites(data.invites || []) setPagination({ page: data.page || 1, perPage: data.perPage || 10, total: data.total || 0, totalPages: data.totalPages || 1 }) // Сбрасываем выбранные приглашения при загрузке новых данных setSelectedInvites({}) setSelectAll(false) } catch (error) { props.onError(`Ошибка загрузки приглашений: ${(error as Error).message}`) } finally { setLoading(false) } } /** * Обработчик изменения страницы */ const handlePageChange = (page: number) => { void loadInvites(page) } /** * Обработчик поиска */ const handleSearch = () => { void loadInvites(1) // Сброс на первую страницу при поиске } /** * Обработчик изменения фильтра статуса */ const handleStatusFilterChange = (status: string) => { setStatusFilter(status) void loadInvites(1) } /** * Получает отображаемое название статуса */ const getStatusDisplay = (status: string) => { switch (status) { case 'PENDING': return { text: 'Ожидает', badge: 'warning' } case 'ACCEPTED': return { text: 'Принято', badge: 'success' } case 'REJECTED': return { text: 'Отклонено', badge: 'error' } default: return { text: status, badge: 'secondary' } } } /** * Удаляет приглашение */ const deleteInvite = async (invite: Invite) => { try { // Получаем токен авторизации из localStorage или cookie const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() console.log(`[InvitesRoute] Удаление приглашения, токен: ${authToken ? 'найден' : 'не найден'}`) const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: ADMIN_DELETE_INVITE_MUTATION, variables: { inviter_id: invite.inviter_id, author_id: invite.author_id, shout_id: invite.shout_id } }) }) const result = await response.json() if (result.errors) { throw new Error(result.errors[0].message) } if (!result.data.adminDeleteInvite.success) { throw new Error(result.data.adminDeleteInvite.error || 'Неизвестная ошибка') } props.onSuccess('Приглашение успешно удалено') setDeleteModal({ show: false, invite: null }) await loadInvites(pagination().page) } catch (error) { props.onError(`Ошибка удаления приглашения: ${(error as Error).message}`) } } /** * Пакетное удаление выбранных приглашений */ const deleteSelectedInvites = async () => { try { const selected = selectedInvites() const invitesToDelete = invites().filter(invite => { const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}` return selected[key] }) if (invitesToDelete.length === 0) { props.onError('Не выбрано ни одного приглашения для удаления') return } // Получаем токен авторизации из localStorage или cookie const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie() console.log(`[InvitesRoute] Пакетное удаление приглашений, токен: ${authToken ? 'найден' : 'не найден'}`) const response = await fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authToken ? `Bearer ${authToken}` : '' }, body: JSON.stringify({ query: ADMIN_DELETE_INVITES_BATCH_MUTATION, variables: { invites: invitesToDelete.map(invite => ({ inviter_id: invite.inviter_id, author_id: invite.author_id, shout_id: invite.shout_id })) } }) }) const result = await response.json() if (result.errors) { throw new Error(result.errors[0].message) } const deleteResult = result.data.adminDeleteInvitesBatch if (!deleteResult.success) { throw new Error(deleteResult.error || 'Неизвестная ошибка') } props.onSuccess(`Успешно удалено ${invitesToDelete.length} приглашений`) setBatchDeleteModal({ show: false }) setSelectedInvites({}) setSelectAll(false) await loadInvites(pagination().page) } catch (error) { props.onError(`Ошибка пакетного удаления приглашений: ${(error as Error).message}`) } } /** * Обработчик выбора/снятия выбора с приглашения */ const handleSelectInvite = (invite: Invite, checked: boolean) => { const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}` setSelectedInvites(prev => ({ ...prev, [key]: checked })) // Если снимаем выбор с элемента, то снимаем и "выбрать все" if (!checked && selectAll()) { setSelectAll(false) } } /** * Обработчик выбора/снятия выбора со всех приглашений */ const handleSelectAll = (checked: boolean) => { setSelectAll(checked) const newSelected: Record = {} if (checked) { // Выбираем все приглашения на текущей странице invites().forEach(invite => { const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}` newSelected[key] = true }) } setSelectedInvites(newSelected) } /** * Получает количество выбранных приглашений */ const getSelectedCount = () => { return Object.values(selectedInvites()).filter(Boolean).length } // Загружаем приглашения при монтировании компонента onMount(() => { void loadInvites() }) return (
setSearch(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSearch()} class={styles.searchInput} />
{/* Панель пакетных действий */} 0}>
handleSelectAll(e.target.checked)} class={styles.checkbox} />
0}>
Выбрано: {getSelectedCount()}
Загрузка приглашений...
Приглашения не найдены
0}>
{(invite) => { const statusDisplay = getStatusDisplay(invite.status) const inviteKey = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}` const isSelected = selectedInvites()[inviteKey] || false return ( ) }}
Приглашающий Приглашаемый Публикация Статус Действия
handleSelectInvite(invite, e.target.checked)} class={styles.checkbox} onClick={(e) => e.stopPropagation()} />
{invite.inviter.name || 'Без имени'}
{invite.inviter.email}
ID: {invite.inviter_id}
{invite.author.name || 'Без имени'}
{invite.author.email}
ID: {invite.author_id}
{invite.shout.title}
Автор: {invite.shout.created_by.name}
ID: {invite.shout_id}
{statusDisplay.text}
{/* Модальное окно подтверждения удаления */} setDeleteModal({ show: false, invite: null })} title="Подтверждение удаления" size="small" >

Вы действительно хотите удалить приглашение от{' '} {deleteModal().invite?.inviter.name} для{' '} {deleteModal().invite?.author.name} к публикации{' '} "{deleteModal().invite?.shout.title}"?

{/* Модальное окно подтверждения пакетного удаления */} setBatchDeleteModal({ show: false })} title="Подтверждение пакетного удаления" size="small" >

Вы действительно хотите удалить {getSelectedCount()} выбранных приглашений?
Это действие нельзя отменить.

) } export default InvitesRoute