From cbf2168e5263e79670c03b860ca84cd9935881df Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Date: Tue, 9 Dec 2025 14:37:04 +0300 Subject: [PATCH] Add user filtering and progress overview to submissions page; enhance localization for user selection and progress display --- locales/en.json | 1 + locales/ru.json | 7 ++ src/pages/submissions/SubmissionsPage.tsx | 134 +++++++++++++++++++++- src/pages/users/UsersPage.tsx | 4 +- stubs/api/data/stats-v2.json | 22 ++++ stubs/api/index.js | 28 ++++- 6 files changed, 189 insertions(+), 7 deletions(-) diff --git a/locales/en.json b/locales/en.json index c5c1bb5..04d47f4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -147,6 +147,7 @@ "challenge.admin.submissions.loading": "Loading attempts...", "challenge.admin.submissions.load.error": "Failed to load attempts list", "challenge.admin.submissions.search.placeholder": "Search by user or task...", + "challenge.admin.submissions.filter.user": "Select user", "challenge.admin.submissions.filter.status": "Status", "challenge.admin.submissions.status.all": "All statuses", "challenge.admin.submissions.status.accepted": "Accepted", diff --git a/locales/ru.json b/locales/ru.json index 9592f78..3042bfd 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -146,6 +146,8 @@ "challenge.admin.submissions.loading": "Загрузка попыток...", "challenge.admin.submissions.load.error": "Не удалось загрузить список попыток", "challenge.admin.submissions.search.placeholder": "Поиск по пользователю или заданию...", + "challenge.admin.submissions.filter.user": "Выберите пользователя", + "challenge.admin.submissions.filter.user.clear": "Показать всех", "challenge.admin.submissions.filter.status": "Статус", "challenge.admin.submissions.status.all": "Все статусы", "challenge.admin.submissions.status.accepted": "Принято", @@ -175,6 +177,11 @@ "challenge.admin.submissions.details.solution": "Решение пользователя:", "challenge.admin.submissions.details.feedback": "Обратная связь от LLM:", "challenge.admin.submissions.details.close": "Закрыть", + "challenge.admin.submissions.overview.title": "Общий прогресс по участникам", + "challenge.admin.submissions.overview.description": "Ниже — сводка по прогрессу всех участников и цепочек. Выберите пользователя выше, чтобы просмотреть его отдельные попытки.", + "challenge.admin.submissions.overview.table.user": "Участник", + "challenge.admin.submissions.overview.table.chain": "Цепочка", + "challenge.admin.submissions.overview.table.progress": "Прогресс", "challenge.admin.layout.title": "Challenge Admin", "challenge.admin.layout.nav.dashboard": "Dashboard", "challenge.admin.layout.nav.detailed.stats": "Детальная статистика", diff --git a/src/pages/submissions/SubmissionsPage.tsx b/src/pages/submissions/SubmissionsPage.tsx index 9b22463..136182a 100644 --- a/src/pages/submissions/SubmissionsPage.tsx +++ b/src/pages/submissions/SubmissionsPage.tsx @@ -17,6 +17,7 @@ import { DialogBody, DialogFooter, DialogActionTrigger, + Progress, createListCollection, } from '@chakra-ui/react' import ReactMarkdown from 'react-markdown' @@ -31,6 +32,7 @@ import type { SubmissionStatus, ChallengeTask, ChallengeUser, + ParticipantProgress, } from '../../types/challenge' export const SubmissionsPage: React.FC = () => { @@ -125,6 +127,28 @@ export const SubmissionsPage: React.FC = () => { const hasParticipants = participants.length > 0 const hasSelectedUser = !!selectedUserId + const participantProgressRows = stats.chainsDetailed + .reduce( + (acc, chain) => { + const rows = chain.participantProgress.map((participant) => ({ + ...participant, + chainId: chain.chainId, + chainName: chain.name, + totalTasks: chain.totalTasks, + })) + + return acc.concat(rows) + }, + [] as Array< + ParticipantProgress & { + chainId: string + chainName: string + totalTasks: number + } + > + ) + .sort((a, b) => a.progressPercent - b.progressPercent) + return ( {t('challenge.admin.submissions.title')} @@ -132,7 +156,7 @@ export const SubmissionsPage: React.FC = () => { {/* Filters */} {hasParticipants && ( - + { + {hasSelectedUser && ( + + )} + {submissionsList.length > 0 && ( <> { description={t('challenge.admin.submissions.empty.description')} /> ) : !hasSelectedUser ? ( - + + + {t('challenge.admin.submissions.overview.title')} + + + {t('challenge.admin.submissions.overview.description')} + + + {participantProgressRows.length === 0 ? ( + + ) : ( + + + + + + {t('challenge.admin.submissions.overview.table.user')} + + + {t('challenge.admin.submissions.overview.table.chain')} + + + {t('challenge.admin.submissions.overview.table.progress')} + + + + + {participantProgressRows.map((row) => { + const colorPalette = + row.progressPercent >= 70 + ? 'green' + : row.progressPercent >= 40 + ? 'orange' + : 'red' + + return ( + setSelectedUserId(row.userId)} + > + + {row.nickname} + + + + {row.chainName} + + + {t('challenge.admin.detailed.stats.chains.total.tasks')}{' '} + {row.completedCount} / {row.totalTasks} + + + + + + + {row.progressPercent}% + + + + + + + + + + + ) + })} + + + + )} + ) : filteredSubmissions.length === 0 ? ( { const users: ActiveParticipant[] = stats.activeParticipants || [] + const normalizedQuery = (searchQuery ?? '').toLowerCase() + const filteredUsers = users.filter((user) => - user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) + (user.nickname ?? '').toLowerCase().includes(normalizedQuery) ) return ( diff --git a/stubs/api/data/stats-v2.json b/stubs/api/data/stats-v2.json index 6b1d8ae..dd3771d 100644 --- a/stubs/api/data/stats-v2.json +++ b/stubs/api/data/stats-v2.json @@ -200,6 +200,28 @@ } ], "activeParticipants": [ + { + "userId": "6909b51512c75d75a36a52bf", + "nickname": "Примаков А.А.", + "totalSubmissions": 14, + "completedTasks": 1, + "chainProgress": [ + { + "chainId": "6909ad8612c75d75a36a4c58", + "chainName": "Это тестовая цепочка заданий", + "totalTasks": 2, + "completedTasks": 1, + "progressPercent": 50 + }, + { + "chainId": "690a30b1e723507972c44098", + "chainName": "Навыки работы с нейросетями для начинающих (пошагово)", + "totalTasks": 20, + "completedTasks": 1, + "progressPercent": 5 + } + ] + }, { "userId": "user_1", "nickname": "alex_dev", diff --git a/stubs/api/index.js b/stubs/api/index.js index 039c7cd..6d989ac 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -61,6 +61,32 @@ const getStatsV2 = () => { return statsV2Cache; }; +// Enrich SystemStatsV2 with real user ids/nicknames from users.json +const getStatsV2WithUsers = () => { + const statsV2 = getStatsV2(); + const users = getUsers(); + + const mapParticipant = (participant, index) => { + const user = users[index]; + if (!user) return participant; + + return { + ...participant, + userId: user.id, + nickname: user.nickname, + }; + }; + + return { + ...statsV2, + activeParticipants: statsV2.activeParticipants.map(mapParticipant), + chainsDetailed: statsV2.chainsDetailed.map((chain) => ({ + ...chain, + participantProgress: chain.participantProgress.map(mapParticipant), + })), + }; +}; + router.use(timer()); // ============= TASKS ============= @@ -282,7 +308,7 @@ router.get('/challenge/stats', (req, res) => { // GET /api/challenge/stats/v2 router.get('/challenge/stats/v2', (req, res) => { - const statsV2 = getStatsV2(); + const statsV2 = getStatsV2WithUsers(); const chainId = req.query.chainId; // Если chainId не передан, возвращаем все данные