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:
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user