fix api calls
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user