172 lines
6.0 KiB
TypeScript
172 lines
6.0 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 { useGetTasksQuery, useDeleteTaskMutation } 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 type { ChallengeTask } from '../../types/challenge'
|
|
import { toaster } from '../../components/ui/toaster'
|
|
|
|
export const TasksListPage: React.FC = () => {
|
|
const navigate = useNavigate()
|
|
const { t } = useTranslation()
|
|
const { data: tasks, isLoading, error, refetch } = useGetTasksQuery()
|
|
const [deleteTask] = useDeleteTaskMutation()
|
|
|
|
const [searchQuery, setSearchQuery] = useState('')
|
|
|
|
const handleDeleteTask = async (task: ChallengeTask) => {
|
|
const confirmed = window.confirm(
|
|
t('challenge.admin.tasks.delete.confirm.message', { title: task.title })
|
|
)
|
|
|
|
if (!confirmed) return
|
|
|
|
try {
|
|
await deleteTask(task.id).unwrap()
|
|
toaster.create({
|
|
title: t('challenge.admin.common.success'),
|
|
description: t('challenge.admin.tasks.deleted'),
|
|
type: 'success',
|
|
})
|
|
} catch (_err) {
|
|
toaster.create({
|
|
title: t('challenge.admin.common.error'),
|
|
description: t('challenge.admin.tasks.delete.error'),
|
|
type: 'error',
|
|
})
|
|
}
|
|
}
|
|
|
|
if (isLoading) {
|
|
return <LoadingSpinner message={t('challenge.admin.tasks.list.loading')} />
|
|
}
|
|
|
|
if (error || !tasks) {
|
|
return <ErrorAlert message={t('challenge.admin.tasks.list.load.error')} onRetry={refetch} />
|
|
}
|
|
|
|
const filteredTasks = tasks.filter((task) =>
|
|
task.title.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.tasks.list.title')}</Heading>
|
|
<Button colorPalette="teal" onClick={() => navigate(URLs.taskNew)}>
|
|
{t('challenge.admin.tasks.list.create.button')}
|
|
</Button>
|
|
</Flex>
|
|
|
|
{tasks.length > 0 && (
|
|
<Box mb={4}>
|
|
<Input
|
|
placeholder={t('challenge.admin.tasks.list.search.placeholder')}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
maxW="400px"
|
|
/>
|
|
</Box>
|
|
)}
|
|
|
|
{filteredTasks.length === 0 && tasks.length === 0 ? (
|
|
<EmptyState
|
|
title={t('challenge.admin.tasks.list.empty.title')}
|
|
description={t('challenge.admin.tasks.list.empty.description')}
|
|
actionLabel={t('challenge.admin.tasks.list.empty.action')}
|
|
onAction={() => navigate(URLs.taskNew)}
|
|
/>
|
|
) : filteredTasks.length === 0 ? (
|
|
<EmptyState
|
|
title={t('challenge.admin.common.not.found')}
|
|
description={t('challenge.admin.tasks.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.tasks.list.table.title')}</Table.ColumnHeader>
|
|
<Table.ColumnHeader>{t('challenge.admin.tasks.list.table.creator')}</Table.ColumnHeader>
|
|
<Table.ColumnHeader>{t('challenge.admin.tasks.list.table.created')}</Table.ColumnHeader>
|
|
<Table.ColumnHeader>{t('challenge.admin.tasks.list.table.hidden.instructions')}</Table.ColumnHeader>
|
|
<Table.ColumnHeader textAlign="right">{t('challenge.admin.tasks.list.table.actions')}</Table.ColumnHeader>
|
|
</Table.Row>
|
|
</Table.Header>
|
|
<Table.Body>
|
|
{filteredTasks.map((task) => (
|
|
<Table.Row key={task.id}>
|
|
<Table.Cell fontWeight="medium">{task.title}</Table.Cell>
|
|
<Table.Cell>
|
|
<Text fontSize="sm" color="gray.600">
|
|
{task.creator?.preferred_username || 'N/A'}
|
|
</Text>
|
|
</Table.Cell>
|
|
<Table.Cell>
|
|
<Text fontSize="sm" color="gray.600">
|
|
{formatDate(task.createdAt)}
|
|
</Text>
|
|
</Table.Cell>
|
|
<Table.Cell>
|
|
{task.hiddenInstructions ? (
|
|
<Badge colorPalette="purple" variant="subtle">
|
|
{t('challenge.admin.tasks.list.badge.has.instructions')}
|
|
</Badge>
|
|
) : (
|
|
<Text fontSize="sm" color="gray.400">
|
|
—
|
|
</Text>
|
|
)}
|
|
</Table.Cell>
|
|
<Table.Cell textAlign="right">
|
|
<HStack gap={2} justify="flex-end">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
onClick={() => navigate(URLs.taskEdit(task.id))}
|
|
>
|
|
{t('challenge.admin.tasks.list.button.edit')}
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
colorPalette="red"
|
|
onClick={() => handleDeleteTask(task)}
|
|
>
|
|
{t('challenge.admin.tasks.list.button.delete')}
|
|
</Button>
|
|
</HStack>
|
|
</Table.Cell>
|
|
</Table.Row>
|
|
))}
|
|
</Table.Body>
|
|
</Table.Root>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
)
|
|
}
|
|
|