Implement chain submissions API and update frontend to utilize new endpoint; enhance submissions page with feature flag for API selection, participant progress display, and improved filtering logic.

This commit is contained in:
2025-12-13 20:32:23 +03:00
parent 18e2ccb6bc
commit 04836ea6ce
5 changed files with 286 additions and 44 deletions

View File

@@ -17,7 +17,13 @@ import {
Select,
createListCollection,
} from '@chakra-ui/react'
import { useGetChainsQuery, useGetSystemStatsV2Query, useGetUserSubmissionsQuery } from '../../__data__/api/api'
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'
@@ -28,7 +34,6 @@ import type {
SubmissionStatus,
ChallengeTask,
ChallengeUser,
ActiveParticipant,
} from '../../types/challenge'
export const SubmissionsPage: React.FC = () => {
@@ -36,30 +41,48 @@ export const SubmissionsPage: React.FC = () => {
const navigate = useNavigate()
const { chainId } = useParams<{ chainId?: string }>()
// Проверяем feature flag
const featureValue = getFeatureValue('challenge-admin', 'use-chain-submissions-api')
const useNewApi = featureValue?.value === 'true'
// Состояние для выбранного пользователя и фильтров
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
// Получаем список цепочек
const {
data: chains,
isLoading: isChainsLoading,
const {
data: chains,
isLoading: isChainsLoading,
error: chainsError,
refetch: refetchChains,
} = useGetChainsQuery()
// Получаем общую статистику (без фильтра по chainId - получаем всех участников)
// Новый API: получаем данные по цепочке через новый эндпоинт
const {
data: chainData,
isLoading: isChainDataLoading,
error: chainDataError,
refetch: refetchChainData,
} = useGetChainSubmissionsQuery(
{
chainId: chainId!,
userId: selectedUserId || undefined,
status: statusFilter !== 'all' ? statusFilter : undefined,
},
{ skip: !chainId || !useNewApi }
)
// Старый API: получаем общую статистику и submissions отдельно
const {
data: stats,
isLoading: isStatsLoading,
error: statsError,
refetch: refetchStats,
} = useGetSystemStatsV2Query(undefined, {
skip: !chainId, // Загружаем только когда выбрана цепочка
skip: !chainId || useNewApi,
})
// Получаем submissions для выбранного пользователя
const {
data: submissions,
isLoading: isSubmissionsLoading,
@@ -67,43 +90,50 @@ export const SubmissionsPage: React.FC = () => {
refetch: refetchSubmissions,
} = useGetUserSubmissionsQuery(
{ userId: selectedUserId!, taskId: undefined },
{ skip: !selectedUserId }
{ skip: !selectedUserId || useNewApi }
)
const isLoading = isChainsLoading || (chainId && isStatsLoading) || (selectedUserId && isSubmissionsLoading)
const error = chainsError || statsError || submissionsError
const isLoading =
isChainsLoading ||
(chainId && useNewApi && isChainDataLoading) ||
(chainId && !useNewApi && isStatsLoading) ||
(selectedUserId && !useNewApi && isSubmissionsLoading)
const error = chainsError || (useNewApi ? chainDataError : statsError || submissionsError)
const handleRetry = () => {
refetchChains()
refetchStats()
if (selectedUserId) {
refetchSubmissions()
if (chainId) {
if (useNewApi) {
refetchChainData()
} else {
refetchStats()
if (selectedUserId) {
refetchSubmissions()
}
}
}
}
// Получаем данные выбранной цепочки из списка chains
// Получаем данные выбранной цепочки из списка chains (для старого API)
const selectedChain = useMemo(() => {
if (!chainId || !chains) return null
return chains.find((c) => c.id === chainId) || null
}, [chainId, chains])
// Получаем taskIds из текущей цепочки
// Получаем taskIds из текущей цепочки (для старого API)
const chainTaskIds = useMemo(() => {
if (!selectedChain) return new Set<string>()
return new Set(selectedChain.tasks.map((t) => t.id))
}, [selectedChain])
// Фильтруем участников - только те, кто имеет прогресс в этой цепочке
const chainParticipants = useMemo(() => {
if (!stats?.activeParticipants || !chainId) return []
// Старый API: фильтруем участников - только те, кто имеет прогресс в этой цепочке
const chainParticipantsOld = useMemo(() => {
if (!stats?.activeParticipants || !chainId || useNewApi) return []
return stats.activeParticipants
.map((participant) => {
// Ищем прогресс участника по выбранной цепочке
const chainProgress = participant.chainProgress?.find((cp) => cp.chainId === chainId)
// Если нет прогресса по этой цепочке, пробуем рассчитать на основе submissions
// Для простоты показываем всех участников с базовым прогрессом 0%
return {
...participant,
progressPercent: chainProgress?.progressPercent ?? 0,
@@ -112,43 +142,90 @@ export const SubmissionsPage: React.FC = () => {
}
})
.sort((a, b) => a.progressPercent - b.progressPercent)
}, [stats?.activeParticipants, chainId, selectedChain])
}, [stats?.activeParticipants, chainId, selectedChain, useNewApi])
// Фильтруем submissions только по заданиям из текущей цепочки
const filteredSubmissions = useMemo(() => {
if (!submissions || chainTaskIds.size === 0) return []
// Старый API: фильтруем submissions только по заданиям из текущей цепочки
const filteredSubmissionsOld = useMemo(() => {
if (!submissions || chainTaskIds.size === 0 || useNewApi) return []
const normalizedSearchQuery = (searchQuery ?? '').toLowerCase()
return submissions.filter((submission) => {
// Фильтр по цепочке (по taskId)
const rawTask = submission.task as ChallengeTask | string | undefined
const taskId = rawTask && typeof rawTask === 'object' && 'id' in rawTask
? rawTask.id
: typeof rawTask === 'string' ? rawTask : ''
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])
}, [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', {
@@ -250,7 +327,7 @@ export const SubmissionsPage: React.FC = () => {
}
// Если цепочка выбрана но данных нет
if (!selectedChain) {
if (useNewApi && !chainData) {
return (
<Box>
<Link to={URLs.submissions} style={{ textDecoration: 'none', color: '#319795' }}>
@@ -263,7 +340,21 @@ export const SubmissionsPage: React.FC = () => {
)
}
const participants: ActiveParticipant[] = stats?.activeParticipants || []
if (!useNewApi && !selectedChain) {
return (
<Box>
<Link to={URLs.submissions} style={{ textDecoration: 'none', color: '#319795' }}>
<Text fontSize="sm" _hover={{ textDecoration: 'underline' }} mb={4}>
{t('challenge.admin.submissions.back.to.chains')}
</Text>
</Link>
<ErrorAlert message={t('challenge.admin.common.not.found')} onRetry={handleRetry} />
</Box>
)
}
const chainName = useNewApi ? chainData?.chain.name : selectedChain?.name
const chainTasksCount = useNewApi ? chainData?.chain.tasks.length : selectedChain?.tasks.length
return (
<Box>
@@ -276,14 +367,14 @@ export const SubmissionsPage: React.FC = () => {
</Text>
</Link>
</HStack>
<Heading mb={2}>{selectedChain.name}</Heading>
<Heading mb={2}>{chainName}</Heading>
<Text color="gray.600" fontSize="sm">
{t('challenge.admin.submissions.chain.description', { count: selectedChain.tasks.length })}
{t('challenge.admin.submissions.chain.description', { count: chainTasksCount ?? 0 })}
</Text>
</Box>
{/* Выбор участника и фильтры */}
{participants.length > 0 && (
{sortedParticipants.length > 0 && (
<VStack mb={4} gap={3} align="stretch">
<HStack gap={4} align="center" wrap="wrap">
{selectedUserId && (
@@ -342,7 +433,7 @@ export const SubmissionsPage: React.FC = () => {
{t('challenge.admin.submissions.participants.description')}
</Text>
{chainParticipants.length === 0 ? (
{sortedParticipants.length === 0 ? (
<EmptyState
title={t('challenge.admin.submissions.participants.empty.title')}
description={t('challenge.admin.submissions.participants.empty.description')}
@@ -357,7 +448,7 @@ export const SubmissionsPage: React.FC = () => {
}}
gap={3}
>
{chainParticipants.map((participant) => {
{sortedParticipants.map((participant) => {
const colorPalette =
participant.progressPercent >= 70
? 'green'