fix api calls

This commit is contained in:
2025-12-09 12:25:29 +03:00
parent fd55d5a214
commit 2a08d9df35
10 changed files with 674 additions and 4196 deletions

View File

@@ -5,7 +5,6 @@ import { keycloak } from '../kc'
import type {
ChallengeTask,
ChallengeChain,
ChallengeUser,
ChallengeSubmission,
SystemStats,
SystemStatsV2,
@@ -113,13 +112,6 @@ export const api = createApi({
invalidatesTags: ['Chain'],
}),
// Users
getUsers: builder.query<ChallengeUser[], void>({
query: () => '/challenge/users',
transformResponse: (response: { body: ChallengeUser[] }) => response.body,
providesTags: ['User'],
}),
// Statistics
getSystemStats: builder.query<SystemStats, void>({
query: () => '/challenge/stats',
@@ -149,11 +141,6 @@ export const api = createApi({
transformResponse: (response: { body: ChallengeSubmission[] }) => response.body,
providesTags: ['Submission'],
}),
getAllSubmissions: builder.query<ChallengeSubmission[], void>({
query: () => '/challenge/submissions',
transformResponse: (response: { body: ChallengeSubmission[] }) => response.body,
providesTags: ['Submission'],
}),
}),
})
@@ -168,11 +155,9 @@ export const {
useCreateChainMutation,
useUpdateChainMutation,
useDeleteChainMutation,
useGetUsersQuery,
useGetSystemStatsQuery,
useGetSystemStatsV2Query,
useGetUserStatsQuery,
useGetUserSubmissionsQuery,
useGetAllSubmissionsQuery,
} = api

View File

@@ -20,30 +20,61 @@ import {
createListCollection,
} from '@chakra-ui/react'
import ReactMarkdown from 'react-markdown'
import { useGetAllSubmissionsQuery } from '../../__data__/api/api'
import { 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 type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge'
import type {
ActiveParticipant,
ChallengeSubmission,
SubmissionStatus,
ChallengeTask,
ChallengeUser,
} from '../../types/challenge'
export const SubmissionsPage: React.FC = () => {
const { t } = useTranslation()
const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery()
const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats } =
useGetSystemStatsV2Query(undefined)
const [searchQuery, setSearchQuery] = useState('')
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
const [selectedSubmission, setSelectedSubmission] = useState<ChallengeSubmission | null>(null)
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const {
data: submissions,
isLoading: isSubmissionsLoading,
error: submissionsError,
refetch: refetchSubmissions,
} = useGetUserSubmissionsQuery(
{ userId: selectedUserId!, taskId: undefined },
{ skip: !selectedUserId }
)
const isLoading = isStatsLoading || (selectedUserId && isSubmissionsLoading)
const error = statsError || submissionsError
const handleRetry = () => {
refetchStats()
if (selectedUserId) {
refetchSubmissions()
}
}
if (isLoading) {
return <LoadingSpinner message={t('challenge.admin.submissions.loading')} />
}
if (error || !submissions) {
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={refetch} />
if (error || !stats) {
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={handleRetry} />
}
const filteredSubmissions = submissions.filter((submission) => {
const participants: ActiveParticipant[] = stats.activeParticipants || []
const submissionsList: ChallengeSubmission[] = submissions || []
const filteredSubmissions = submissionsList.filter((submission) => {
const user = submission.user as ChallengeUser
const task = submission.task as ChallengeTask
@@ -84,43 +115,88 @@ export const SubmissionsPage: React.FC = () => {
],
})
const userOptions = createListCollection({
items: participants.map((participant) => ({
label: `${participant.nickname} (${participant.userId})`,
value: participant.userId,
})),
})
const hasParticipants = participants.length > 0
const hasSelectedUser = !!selectedUserId
return (
<Box>
<Heading mb={6}>{t('challenge.admin.submissions.title')}</Heading>
{/* Filters */}
{submissions.length > 0 && (
<HStack mb={4} gap={4}>
<Input
placeholder={t('challenge.admin.submissions.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
maxW="400px"
/>
<Select.Root
collection={statusOptions}
value={[statusFilter]}
onValueChange={(e) => setStatusFilter(e.value[0] as SubmissionStatus | 'all')}
maxW="200px"
>
<Select.Trigger>
<Select.ValueText placeholder={t('challenge.admin.submissions.filter.status')} />
</Select.Trigger>
<Select.Content>
{statusOptions.items.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</HStack>
{hasParticipants && (
<VStack mb={4} gap={3} align="stretch">
<HStack gap={4}>
<Select.Root
collection={userOptions}
value={selectedUserId ? [selectedUserId] : []}
onValueChange={(e) => setSelectedUserId(e.value[0] ?? null)}
maxW="300px"
>
<Select.Trigger>
<Select.ValueText placeholder={t('challenge.admin.submissions.filter.user')} />
</Select.Trigger>
<Select.Content>
{userOptions.items.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
{submissionsList.length > 0 && (
<>
<Input
placeholder={t('challenge.admin.submissions.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
maxW="400px"
/>
<Select.Root
collection={statusOptions}
value={[statusFilter]}
onValueChange={(e) => setStatusFilter(e.value[0] as SubmissionStatus | 'all')}
maxW="200px"
>
<Select.Trigger>
<Select.ValueText placeholder={t('challenge.admin.submissions.filter.status')} />
</Select.Trigger>
<Select.Content>
{statusOptions.items.map((option) => (
<Select.Item key={option.value} item={option}>
{option.label}
</Select.Item>
))}
</Select.Content>
</Select.Root>
</>
)}
</HStack>
</VStack>
)}
{filteredSubmissions.length === 0 && submissions.length === 0 ? (
<EmptyState title={t('challenge.admin.submissions.empty.title')} description={t('challenge.admin.submissions.empty.description')} />
{!hasParticipants ? (
<EmptyState
title={t('challenge.admin.submissions.empty.title')}
description={t('challenge.admin.submissions.empty.description')}
/>
) : !hasSelectedUser ? (
<EmptyState
title={t('challenge.admin.submissions.empty.title')}
description={t('challenge.admin.submissions.filter.user')}
/>
) : filteredSubmissions.length === 0 ? (
<EmptyState title={t('challenge.admin.submissions.search.empty.title')} description={t('challenge.admin.submissions.search.empty.description')} />
<EmptyState
title={t('challenge.admin.submissions.search.empty.title')}
description={t('challenge.admin.submissions.search.empty.description')}
/>
) : (
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
<Table.Root size="sm">

View File

@@ -20,14 +20,17 @@ import {
Badge,
Progress,
} from '@chakra-ui/react'
import { useGetUsersQuery, useGetUserStatsQuery } from '../../__data__/api/api'
import { useGetSystemStatsV2Query, useGetUserStatsQuery } from '../../__data__/api/api'
import type { ActiveParticipant } from '../../types/challenge'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert'
import { EmptyState } from '../../components/EmptyState'
export const UsersPage: React.FC = () => {
const { t } = useTranslation()
const { data: users, isLoading, error, refetch } = useGetUsersQuery()
const { data: stats, isLoading, error, refetch } = useGetSystemStatsV2Query(undefined, {
pollingInterval: 10000,
})
const [searchQuery, setSearchQuery] = useState('')
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
@@ -35,22 +38,16 @@ export const UsersPage: React.FC = () => {
return <LoadingSpinner message={t('challenge.admin.users.loading')} />
}
if (error || !users) {
if (error || !stats) {
return <ErrorAlert message={t('challenge.admin.users.load.error')} onRetry={refetch} />
}
const users: ActiveParticipant[] = stats.activeParticipants || []
const filteredUsers = users.filter((user) =>
user.nickname.toLowerCase().includes(searchQuery.toLowerCase())
)
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
return (
<Box>
<Heading mb={6}>{t('challenge.admin.users.title')}</Heading>
@@ -80,22 +77,28 @@ export const UsersPage: React.FC = () => {
<Table.Row>
<Table.ColumnHeader>{t('challenge.admin.users.table.nickname')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.id')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.registered')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.total.submissions')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.completed')}</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">{t('challenge.admin.users.table.actions')}</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{filteredUsers.map((user) => (
<Table.Row key={user.id}>
<Table.Row key={user.userId}>
<Table.Cell fontWeight="medium">{user.nickname}</Table.Cell>
<Table.Cell>
<Text fontSize="xs" fontFamily="monospace" color="gray.600">
{user.id}
{user.userId}
</Text>
</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{formatDate(user.createdAt)}
{user.totalSubmissions}
</Text>
</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{user.completedTasks}
</Text>
</Table.Cell>
<Table.Cell textAlign="right">
@@ -103,7 +106,7 @@ export const UsersPage: React.FC = () => {
size="sm"
variant="ghost"
colorPalette="teal"
onClick={() => setSelectedUserId(user.id)}
onClick={() => setSelectedUserId(user.userId)}
>
{t('challenge.admin.users.button.stats')}
</Button>