Files
challenge-admin-pl/src/pages/chains/ChainsListPage.tsx

237 lines
8.6 KiB
TypeScript

import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import {
Box,
Heading,
Button,
Table,
Flex,
Input,
HStack,
Text,
Badge,
} from '@chakra-ui/react'
import { useGetChainsQuery, useDeleteChainMutation, useUpdateChainMutation } from '../../__data__/api/api'
import { URLs } from '../../__data__/urls'
import { LoadingSpinner } from '../../components/LoadingSpinner'
import { ErrorAlert } from '../../components/ErrorAlert'
import { EmptyState } from '../../components/EmptyState'
import { DuplicateChainDialog } from '../../components/DuplicateChainDialog'
import { ClearSubmissionsDialog } from '../../components/ClearSubmissionsDialog'
import type { ChallengeChain } from '../../types/challenge'
import { toaster } from '../../components/ui/toaster'
export const ChainsListPage: React.FC = () => {
const navigate = useNavigate()
const { t } = useTranslation()
const { data: chains, isLoading, error, refetch } = useGetChainsQuery()
const [deleteChain] = useDeleteChainMutation()
const [searchQuery, setSearchQuery] = useState('')
const [chainToDuplicate, setChainToDuplicate] = useState<ChallengeChain | null>(null)
const [chainToClearSubmissions, setChainToClearSubmissions] = useState<ChallengeChain | null>(null)
const [updatingChainId, setUpdatingChainId] = useState<string | null>(null)
const [updateChain] = useUpdateChainMutation()
const handleDeleteChain = async (chain: ChallengeChain) => {
const confirmed = window.confirm(
t('challenge.admin.chains.delete.confirm.message', { name: chain.name })
)
if (!confirmed) return
try {
await deleteChain(chain.id).unwrap()
toaster.create({
title: t('challenge.admin.common.success'),
description: t('challenge.admin.chains.deleted'),
type: 'success',
})
} catch (err) {
toaster.create({
title: t('challenge.admin.common.error'),
description: t('challenge.admin.chains.delete.error'),
type: 'error',
})
}
}
const handleToggleActive = async (chain: ChallengeChain, nextValue: boolean) => {
setUpdatingChainId(chain.id)
try {
await updateChain({
id: chain.id,
data: { isActive: nextValue },
}).unwrap()
toaster.create({
title: t('challenge.admin.common.success'),
description: t('challenge.admin.chains.updated'),
type: 'success',
})
} catch {
toaster.create({
title: t('challenge.admin.common.error'),
description: t('challenge.admin.chains.save.error'),
type: 'error',
})
} finally {
setUpdatingChainId(null)
}
}
if (isLoading) {
return <LoadingSpinner message={t('challenge.admin.chains.list.loading')} />
}
if (error || !chains) {
return <ErrorAlert message={t('challenge.admin.chains.list.load.error')} onRetry={refetch} />
}
const filteredChains = chains.filter((chain) =>
chain.name.toLowerCase().includes(searchQuery.toLowerCase())
)
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
return (
<Box>
<Flex justify="space-between" align="center" mb={6}>
<Heading>{t('challenge.admin.chains.list.title')}</Heading>
<Button colorPalette="teal" onClick={() => navigate(URLs.chainNew)}>
{t('challenge.admin.chains.list.create.button')}
</Button>
</Flex>
{chains.length > 0 && (
<Box mb={4}>
<Input
placeholder={t('challenge.admin.chains.list.search.placeholder')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
maxW="400px"
/>
</Box>
)}
{filteredChains.length === 0 && chains.length === 0 ? (
<EmptyState
title={t('challenge.admin.chains.list.empty.title')}
description={t('challenge.admin.chains.list.empty.description')}
actionLabel={t('challenge.admin.chains.list.empty.action')}
onAction={() => navigate(URLs.chainNew)}
/>
) : filteredChains.length === 0 ? (
<EmptyState
title={t('challenge.admin.common.not.found')}
description={t('challenge.admin.chains.list.search.empty', { query: searchQuery })}
/>
) : (
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
<Table.Root size="sm">
<Table.Header>
<Table.Row>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.name')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.tasks.count')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.created')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.chains.list.table.status')}</Table.ColumnHeader>
<Table.ColumnHeader textAlign="right">{t('challenge.admin.chains.list.table.actions')}</Table.ColumnHeader>
</Table.Row>
</Table.Header>
<Table.Body>
{filteredChains.map((chain) => (
<Table.Row key={chain.id}>
<Table.Cell fontWeight="medium">{chain.name}</Table.Cell>
<Table.Cell>
<Badge colorPalette="teal" variant="subtle">
{chain.tasks.length} {t('challenge.admin.chains.list.badge.tasks')}
</Badge>
</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{formatDate(chain.createdAt)}
</Text>
</Table.Cell>
<Table.Cell>
<HStack gap={3} justify="flex-start">
<Badge colorPalette={chain.isActive ? 'green' : 'gray'} variant="subtle">
{chain.isActive
? t('challenge.admin.chains.list.status.active')
: t('challenge.admin.chains.list.status.inactive')}
</Badge>
<Button
size="xs"
variant="outline"
onClick={() => handleToggleActive(chain, !chain.isActive)}
disabled={updatingChainId === chain.id}
>
{chain.isActive
? t('challenge.admin.chains.list.status.inactive')
: t('challenge.admin.chains.list.status.active')}
</Button>
</HStack>
</Table.Cell>
<Table.Cell textAlign="right">
<HStack gap={2} justify="flex-end">
<Button
size="sm"
variant="ghost"
onClick={() => navigate(URLs.chainEdit(chain.id))}
>
{t('challenge.admin.chains.list.button.edit')}
</Button>
<Button
size="sm"
variant="ghost"
onClick={() => setChainToDuplicate(chain)}
>
{t('challenge.admin.chains.duplicate.button')}
</Button>
<Button
size="sm"
variant="ghost"
colorPalette="red"
onClick={() => setChainToClearSubmissions(chain)}
>
{t('challenge.admin.chains.clear.submissions.button')}
</Button>
<Button
size="sm"
variant="ghost"
colorPalette="red"
onClick={() => handleDeleteChain(chain)}
>
{t('challenge.admin.chains.list.button.delete')}
</Button>
</HStack>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table.Root>
</Box>
)}
<DuplicateChainDialog
isOpen={!!chainToDuplicate}
onClose={() => setChainToDuplicate(null)}
chain={chainToDuplicate}
/>
<ClearSubmissionsDialog
isOpen={!!chainToClearSubmissions}
onClose={() => setChainToClearSubmissions(null)}
chain={chainToClearSubmissions}
/>
</Box>
)
}