Приложение для тестирования сотрудников клиники методом один вопрос - до пяти ответов один из которых правильный. Сотрудник должен выбрать правильный вариант ответа
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

285 lines
10 KiB

import {
ArrowLeftOutlined,
CheckCircleTwoTone,
CloseCircleTwoTone,
EditOutlined,
PlayCircleOutlined,
} from '@ant-design/icons'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { Alert, Button, Card, Descriptions, List, Space, Spin, Table, Tag, Typography, message } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { Answer, CreateTestDto, TestListItem, testsApi } from '../../api/tests'
import TestForm, { TestFormValues } from '../../components/TestForm'
const { Title, Text } = Typography
export default function TestEdit() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const queryClient = useQueryClient()
const [editMode, setEditMode] = useState(false)
const { data: test, isLoading } = useQuery({
queryKey: ['tests', id],
queryFn: () => testsApi.get(Number(id)).then((r) => r.data),
})
const { data: versions = [] } = useQuery({
queryKey: ['tests', id, 'versions'],
queryFn: () => testsApi.versions(Number(id)).then((r) => r.data),
enabled: !editMode,
})
const { mutate: activateVersion, isPending: isActivating } = useMutation({
mutationFn: (versionId: number) => testsApi.activate(versionId).then((r) => r.data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tests'] })
queryClient.invalidateQueries({ queryKey: ['tests', id, 'versions'] })
queryClient.invalidateQueries({ queryKey: ['tests', id] })
message.success('Активная версия изменена')
},
onError: () => message.error('Не удалось изменить активную версию'),
})
const { mutate: updateTest, isPending } = useMutation({
mutationFn: (data: CreateTestDto) => testsApi.update(Number(id), data).then((r) => r.data),
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ['tests'] })
if (result.is_new_version) {
message.success(`Создана новая версия теста (v${result.test.version})`)
navigate(`/tests/${result.test.id}/edit`)
} else {
message.success('Тест обновлён')
queryClient.invalidateQueries({ queryKey: ['tests', id] })
setEditMode(false)
}
},
onError: (error: unknown) => {
const err = error as { response?: { data?: { detail?: string } } }
message.error(err.response?.data?.detail || 'Ошибка при сохранении теста')
},
})
if (isLoading) {
return <Spin size="large" style={{ display: 'block', margin: '48px auto' }} />
}
if (!test) return null
// Режим редактирования — показываем форму с предзаполненными данными
if (editMode) {
const initialValues: TestFormValues = {
title: test.title,
description: test.description ?? undefined,
passing_score: test.passing_score,
has_timer: test.time_limit !== null,
time_limit: test.time_limit ?? undefined,
allow_navigation_back: test.allow_navigation_back,
questions: test.questions.map((q) => ({
text: q.text,
answers: q.answers.map((a) => ({ text: a.text, is_correct: a.is_correct })),
})),
}
const onSubmit = (values: TestFormValues) => {
updateTest({
title: values.title,
description: values.description,
passing_score: values.passing_score,
time_limit: values.has_timer ? values.time_limit : undefined,
allow_navigation_back: values.allow_navigation_back ?? true,
questions: values.questions,
})
}
return (
<TestForm
heading={`Редактирование теста — v${test.version}`}
initialValues={initialValues}
onSubmit={onSubmit}
isPending={isPending}
submitLabel="Сохранить"
onCancel={() => setEditMode(false)}
onBack={() => setEditMode(false)}
backLabel="К просмотру теста"
/>
)
}
// Режим просмотра (вид автора)
return (
<div style={{ maxWidth: 820, margin: '0 auto', padding: 24 }}>
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }}>
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/')}>
К списку тестов
</Button>
<Space>
{versions.length > 1 && (!test.is_active || versions.some(v => v.id !== test.id && v.is_active)) && (
<Button
loading={isActivating}
onClick={() => activateVersion(test.id)}
>
Сделать активной
</Button>
)}
<Button
icon={<EditOutlined />}
onClick={() => setEditMode(true)}
>
Редактировать
</Button>
<Button
type="primary"
icon={<PlayCircleOutlined />}
onClick={() => navigate(`/tests/${test.id}/take`)}
>
Пройти тест
</Button>
</Space>
</Space>
<Alert
type="warning"
showIcon
message="Вид автора — правильные ответы отмечены"
style={{ marginBottom: 16 }}
/>
<Title level={2}>{test.title}</Title>
{test.description && (
<Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
{test.description}
</Text>
)}
<Card style={{ marginBottom: 24 }}>
<Descriptions column={3}>
<Descriptions.Item label="Вопросов">{test.questions.length}</Descriptions.Item>
<Descriptions.Item label="Порог зачёта">{test.passing_score}%</Descriptions.Item>
<Descriptions.Item label="Таймер">
{test.time_limit ? `${test.time_limit} мин` : 'Без ограничений'}
</Descriptions.Item>
<Descriptions.Item label="Возврат к вопросу">
{test.allow_navigation_back ? (
<Tag color="green">Разрешён</Tag>
) : (
<Tag color="red">Запрещён</Tag>
)}
</Descriptions.Item>
<Descriptions.Item label="Версия">{test.version}</Descriptions.Item>
<Descriptions.Item label="Создан">
{new Date(test.created_at).toLocaleDateString('ru-RU')}
</Descriptions.Item>
</Descriptions>
</Card>
{versions.length > 1 && (
<>
<Title level={3}>История версий</Title>
<Table<TestListItem>
dataSource={versions}
rowKey="id"
size="small"
pagination={false}
style={{ marginBottom: 24 }}
rowClassName={(record) => (record.id === test.id ? 'ant-table-row-selected' : '')}
columns={
[
{
title: 'Версия',
dataIndex: 'version',
width: 70,
render: (v: number) => <Tag color="default">v{v}</Tag>,
},
{
title: 'Статус',
dataIndex: 'is_active',
width: 130,
render: (active: boolean) =>
active ? (
<Tag color="green">Активная</Tag>
) : (
<Tag color="default">Неактивная</Tag>
),
},
{
title: 'Дата',
dataIndex: 'created_at',
width: 120,
render: (d: string) => new Date(d).toLocaleDateString('ru-RU'),
},
{
title: 'Вопросов',
dataIndex: 'questions_count',
width: 100,
align: 'center' as const,
},
{
title: 'Порог зачёта',
dataIndex: 'passing_score',
width: 120,
align: 'center' as const,
render: (s: number) => `${s}%`,
},
{
title: '',
key: 'action',
render: (_: unknown, record: TestListItem) => (
<Space>
{record.id !== test.id && (
<Button size="small" onClick={() => navigate(`/tests/${record.id}/edit`)}>
Открыть
</Button>
)}
{(record.id !== test.id || !record.is_active) && (
<Button
size="small"
type={record.is_active ? 'default' : 'primary'}
loading={isActivating}
onClick={() => activateVersion(record.id)}
>
Сделать активной
</Button>
)}
</Space>
),
},
] as ColumnsType<TestListItem>
}
/>
</>
)}
<Title level={3}>Вопросы ({test.questions.length})</Title>
{test.questions.map((question, index) => (
<Card key={question.id} style={{ marginBottom: 12 }}>
<Text strong>
{index + 1}. {question.text}
</Text>
<List
style={{ marginTop: 10 }}
dataSource={question.answers}
renderItem={(answer: Answer) => (
<List.Item style={{ padding: '4px 0' }}>
<Space>
{answer.is_correct ? (
<CheckCircleTwoTone twoToneColor="#52c41a" />
) : (
<CloseCircleTwoTone twoToneColor="#d9d9d9" />
)}
<Text>{answer.text}</Text>
</Space>
</List.Item>
)}
/>
</Card>
))}
</div>
)
}