import React, { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate, useParams, Link } from 'react-router-dom' import { Box, Heading, Table, Input, Text, Button, HStack, VStack, Badge, Progress, Grid, SimpleGrid, Select, createListCollection, } from '@chakra-ui/react' import { getFeatureValue } from '@brojs/cli' import { useGetChainsQuery, useGetChainSubmissionsQuery, useGetSystemStatsV2Query, useGetUserSubmissionsQuery, } from '../../__data__/api/api' import { LoadingSpinner } from '../../components/LoadingSpinner' import { ErrorAlert } from '../../components/ErrorAlert' import { EmptyState } from '../../components/EmptyState' import { StatusBadge } from '../../components/StatusBadge' import { URLs } from '../../__data__/urls' import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser, } from '../../types/challenge' export const SubmissionsPage: React.FC = () => { const { t } = useTranslation() const navigate = useNavigate() const { chainId } = useParams<{ chainId?: string }>() // Проверяем feature flags const featureValue = getFeatureValue('challenge-admin', 'use-chain-submissions-api') const useNewApi = featureValue?.value === 'true' const pollingIntervalFeatureValue = getFeatureValue( 'challenge-admin', 'submissions-polling-interval-ms' ) const pollingIntervalMs = (() => { const rawValue = pollingIntervalFeatureValue?.value ?? '' const parsed = Number.parseInt(rawValue, 10) return Number.isFinite(parsed) && parsed > 0 ? parsed : 1200 })() // Состояние для выбранного пользователя и фильтров const [selectedUserId, setSelectedUserId] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [statusFilter, setStatusFilter] = useState('all') // Получаем список цепочек const { data: chains, isLoading: isChainsLoading, error: chainsError, refetch: refetchChains, } = useGetChainsQuery() // Новый API: получаем данные по цепочке через новый эндпоинт const { data: chainData, isLoading: isChainDataLoading, error: chainDataError, refetch: refetchChainData, } = useGetChainSubmissionsQuery( { chainId: chainId!, userId: selectedUserId || undefined, status: statusFilter !== 'all' ? statusFilter : undefined, }, { skip: !chainId || !useNewApi, pollingInterval: pollingIntervalMs, } ) // Старый API: получаем общую статистику и submissions отдельно const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats, } = useGetSystemStatsV2Query(undefined, { skip: !chainId || useNewApi, }) const { data: submissions, isLoading: isSubmissionsLoading, error: submissionsError, refetch: refetchSubmissions, } = useGetUserSubmissionsQuery( { userId: selectedUserId!, taskId: undefined }, { skip: !selectedUserId || useNewApi } ) const isLoading = isChainsLoading || (chainId && useNewApi && isChainDataLoading) || (chainId && !useNewApi && isStatsLoading) || (selectedUserId && !useNewApi && isSubmissionsLoading) const error = chainsError || (useNewApi ? chainDataError : statsError || submissionsError) const handleRetry = () => { refetchChains() if (chainId) { if (useNewApi) { refetchChainData() } else { refetchStats() if (selectedUserId) { refetchSubmissions() } } } } // Получаем данные выбранной цепочки из списка chains (для старого API) const selectedChain = useMemo(() => { if (!chainId || !chains) return null return chains.find((c) => c.id === chainId) || null }, [chainId, chains]) // Получаем taskIds из текущей цепочки (для старого API) const chainTaskIds = useMemo(() => { if (!selectedChain) return new Set() return new Set(selectedChain.tasks.map((t) => t.id)) }, [selectedChain]) // Старый API: фильтруем участников - только те, кто имеет прогресс в этой цепочке const chainParticipantsOld = useMemo(() => { if (!stats?.activeParticipants || !chainId || useNewApi) return [] return stats.activeParticipants .map((participant) => { const chainProgress = participant.chainProgress?.find((cp) => cp.chainId === chainId) return { ...participant, progressPercent: chainProgress?.progressPercent ?? 0, completedTasks: chainProgress?.completedTasks ?? 0, totalTasks: selectedChain?.tasks.length ?? 0, } }) .sort((a, b) => a.progressPercent - b.progressPercent) }, [stats?.activeParticipants, chainId, selectedChain, useNewApi]) // Старый API: фильтруем submissions только по заданиям из текущей цепочки const filteredSubmissionsOld = useMemo(() => { if (!submissions || chainTaskIds.size === 0 || useNewApi) return [] const normalizedSearchQuery = (searchQuery ?? '').toLowerCase() return submissions.filter((submission) => { const rawTask = submission.task as ChallengeTask | string | undefined const taskId = rawTask && typeof rawTask === 'object' && 'id' in rawTask ? rawTask.id : typeof rawTask === 'string' ? rawTask : '' if (!chainTaskIds.has(taskId)) return false const rawUser = submission.user as ChallengeUser | string | undefined const nickname = rawUser && typeof rawUser === 'object' && 'nickname' in rawUser ? (rawUser.nickname ?? '') : '' const title = rawTask && typeof rawTask === 'object' && 'title' in rawTask ? (rawTask.title ?? '') : '' const matchesSearch = nickname.toLowerCase().includes(normalizedSearchQuery) || title.toLowerCase().includes(normalizedSearchQuery) const matchesStatus = statusFilter === 'all' || submission.status === statusFilter return matchesSearch && matchesStatus }) }, [submissions, chainTaskIds, searchQuery, statusFilter, useNewApi]) // Новый API: фильтруем submissions по поисковому запросу (статус уже отфильтрован на сервере) const filteredSubmissionsNew = useMemo(() => { if (!chainData?.submissions || !useNewApi) return [] const normalizedSearchQuery = (searchQuery ?? '').toLowerCase() if (!normalizedSearchQuery) return chainData.submissions return chainData.submissions.filter((submission) => { const rawUser = submission.user as ChallengeUser | string | undefined const rawTask = submission.task as ChallengeTask | string | undefined const nickname = rawUser && typeof rawUser === 'object' && 'nickname' in rawUser ? (rawUser.nickname ?? '') : typeof rawUser === 'string' ? rawUser : '' const title = rawTask && typeof rawTask === 'object' && 'title' in rawTask ? (rawTask.title ?? '') : typeof rawTask === 'string' ? rawTask : '' return ( nickname.toLowerCase().includes(normalizedSearchQuery) || title.toLowerCase().includes(normalizedSearchQuery) ) }) }, [chainData?.submissions, searchQuery, useNewApi]) // Выбираем данные в зависимости от фичи const filteredSubmissions = useNewApi ? filteredSubmissionsNew : filteredSubmissionsOld // Сортируем участников по прогрессу const sortedParticipants = useMemo(() => { if (useNewApi) { if (!chainData?.participants) return [] return [...chainData.participants].sort((a, b) => a.progressPercent - b.progressPercent) } else { return chainParticipantsOld } }, [chainData?.participants, chainParticipantsOld, useNewApi]) const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleString('ru-RU', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }) } const getCheckTime = (submission: ChallengeSubmission) => { if (!submission.checkedAt) return '—' const submitted = new Date(submission.submittedAt).getTime() const checked = new Date(submission.checkedAt).getTime() const diff = Math.round((checked - submitted) / 1000) return t('challenge.admin.submissions.check.time', { time: diff }) } const statusOptions = createListCollection({ items: [ { label: t('challenge.admin.submissions.status.all'), value: 'all' }, { label: t('challenge.admin.submissions.status.accepted'), value: 'accepted' }, { label: t('challenge.admin.submissions.status.needs.revision'), value: 'needs_revision' }, { label: t('challenge.admin.submissions.status.in.progress'), value: 'in_progress' }, { label: t('challenge.admin.submissions.status.pending'), value: 'pending' }, ], }) if (isLoading) { return } if (error) { return } // Если chainId не указан - показываем выбор цепочки if (!chainId) { return ( {t('challenge.admin.submissions.title')} {t('challenge.admin.submissions.select.chain')} {chains && chains.length > 0 ? ( {chains.map((chain) => ( {chain.name} {chain.tasks.length} {t('challenge.admin.submissions.chain.tasks')} {!chain.isActive && ( {t('challenge.admin.chains.list.status.inactive')} )} {t('challenge.admin.submissions.chain.click')} ))} ) : ( )} ) } // Если цепочка выбрана но данных нет if (useNewApi && !chainData) { return ( ← {t('challenge.admin.submissions.back.to.chains')} ) } if (!useNewApi && !selectedChain) { return ( ← {t('challenge.admin.submissions.back.to.chains')} ) } const chainName = useNewApi ? chainData?.chain.name : selectedChain?.name const chainTasksCount = useNewApi ? chainData?.chain.tasks.length : selectedChain?.tasks.length return ( {/* Header с навигацией */} ← {t('challenge.admin.submissions.back.to.chains')} {chainName} {t('challenge.admin.submissions.chain.description', { count: chainTasksCount ?? 0 })} {/* Выбор участника и фильтры */} {sortedParticipants.length > 0 && ( {selectedUserId && ( )} {selectedUserId && filteredSubmissions.length > 0 && ( <> setSearchQuery(e.target.value)} maxW="300px" /> setStatusFilter(e.value[0] as SubmissionStatus | 'all')} maxW="200px" > {statusOptions.items.map((option) => ( {option.label} ))} )} )} {/* Если не выбран пользователь - показываем обзор участников */} {!selectedUserId ? ( {t('challenge.admin.submissions.participants.title')} {t('challenge.admin.submissions.participants.description')} {sortedParticipants.length === 0 ? ( ) : ( {sortedParticipants.map((participant) => { const colorPalette = participant.progressPercent >= 70 ? 'green' : participant.progressPercent >= 40 ? 'orange' : 'red' return ( setSelectedUserId(participant.userId)} transition="all 0.2s" > {participant.workplaceNumber && ( {participant.workplaceNumber} )} {participant.nickname} {participant.progressPercent}% {participant.completedTasks} / {participant.totalTasks} {t('challenge.admin.submissions.participants.click.to.view')} ) })} )} ) : filteredSubmissions.length === 0 ? ( ) : ( /* Таблица попыток выбранного пользователя */ {t('challenge.admin.submissions.table.user')} {t('challenge.admin.submissions.table.workplace')} {t('challenge.admin.submissions.table.task')} {t('challenge.admin.submissions.table.status')} {t('challenge.admin.submissions.table.attempt')} {t('challenge.admin.submissions.table.submitted')} {t('challenge.admin.submissions.table.check.time')} {t('challenge.admin.submissions.table.actions')} {filteredSubmissions.map((submission) => { const rawUser = submission.user as ChallengeUser | string | undefined const rawTask = submission.task as ChallengeTask | string | undefined const nickname = rawUser && typeof rawUser === 'object' && 'nickname' in rawUser ? (rawUser.nickname ?? '') : typeof rawUser === 'string' ? rawUser : '' const workplaceNumber = rawUser && typeof rawUser === 'object' && 'workplaceNumber' in rawUser ? rawUser.workplaceNumber ?? '' : '' const title = rawTask && typeof rawTask === 'object' && 'title' in rawTask ? (rawTask.title ?? '') : typeof rawTask === 'string' ? rawTask : '' return ( {nickname} {workplaceNumber || '—'} {title} #{submission.attemptNumber} {formatDate(submission.submittedAt)} {getCheckTime(submission)} ) })} )} ) }