Skip to content

Commit a7f5a77

Browse files
committed
QAGDEV-723 - [FE] Прикрепления тестов к заданиям v6
1 parent 97ecebe commit a7f5a77

File tree

13 files changed

+652
-255
lines changed

13 files changed

+652
-255
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
query testAttempt($id: ID!) {
2+
testAttempt(id: $id) {
3+
id
4+
startTime
5+
endTime
6+
successfulCount
7+
errorsCount
8+
result
9+
testAttemptQuestionResults {
10+
testQuestion {
11+
id
12+
text
13+
}
14+
result
15+
testAnswerResults {
16+
testAnswer {
17+
id
18+
text
19+
}
20+
result
21+
answer
22+
}
23+
}
24+
}
25+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
query testAttemptsByLecture($lectureId: ID!, $trainingId: ID!) {
2+
testAttempts(lectureId: $lectureId, trainingId: $trainingId) {
3+
id
4+
startTime
5+
endTime
6+
successfulCount
7+
errorsCount
8+
result
9+
}
10+
}

src/features/admin-panel/tests-admin/components/create-test-form.tsx

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -175,43 +175,19 @@ const CreateTestForm: FC<CreateTestFormProps> = ({
175175
setQuestions(updatedQuestions);
176176
};
177177

178-
const validateForm = (): string | null => {
178+
const validateForm = () => {
179179
if (!testName.trim()) {
180180
return "Название теста обязательно";
181181
}
182-
183-
if (successThreshold < 0 || successThreshold > 100) {
184-
return "Проходной балл должен быть от 0 до 100%";
182+
if (successThreshold < 1) {
183+
return "Проходной балл должен быть не менее 1 правильного ответа";
184+
}
185+
if (successThreshold > questions.length) {
186+
return "Проходной балл не может быть больше количества вопросов";
185187
}
186-
187188
if (questions.length === 0) {
188189
return "Добавьте хотя бы один вопрос";
189190
}
190-
191-
for (let i = 0; i < questions.length; i++) {
192-
const question = questions[i];
193-
if (!question.text.trim()) {
194-
return `Текст вопроса ${i + 1} не может быть пустым`;
195-
}
196-
197-
if (question.answers.length < 2) {
198-
return `В вопросе ${i + 1} должно быть минимум 2 варианта ответа`;
199-
}
200-
201-
const correctAnswers = question.answers.filter((a) => a.correct);
202-
if (correctAnswers.length === 0) {
203-
return `В вопросе ${i + 1} должен быть хотя бы один правильный ответ`;
204-
}
205-
206-
for (let j = 0; j < question.answers.length; j++) {
207-
if (!question.answers[j].text.trim()) {
208-
return `Текст ответа ${j + 1} в вопросе ${
209-
i + 1
210-
} не может быть пустым`;
211-
}
212-
}
213-
}
214-
215191
return null;
216192
};
217193

@@ -253,15 +229,14 @@ const CreateTestForm: FC<CreateTestFormProps> = ({
253229
</Grid>
254230
<Grid item xs={12} md={4}>
255231
<TextField
256-
fullWidth
257-
label="Проходной балл (%)"
258232
type="number"
233+
label="Проходной балл (количество правильных ответов)"
259234
value={successThreshold}
260235
onChange={(e) => setSuccessThreshold(Number(e.target.value))}
261-
required
262-
disabled={isLoading}
263-
inputProps={{ min: 0, max: 100 }}
264-
helperText="Минимальный процент правильных ответов для прохождения"
236+
fullWidth
237+
margin="normal"
238+
helperText="Минимальное количество правильных ответов для прохождения теста"
239+
inputProps={{ min: 1, max: questions.length }}
265240
/>
266241
</Grid>
267242
</Grid>

src/features/admin-panel/tests-admin/test-attempt-detail-view.tsx

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,7 @@ const TestAttemptDetailView: FC = () => {
5252

5353
const attempt = attemptData.testAttemptForAdmin;
5454

55-
const formatDate = (dateString: string | null | undefined) => {
56-
if (!dateString) return "Не завершен";
55+
const formatDate = (dateString: string) => {
5756
return new Date(dateString).toLocaleString("ru-RU");
5857
};
5958

@@ -66,10 +65,12 @@ const TestAttemptDetailView: FC = () => {
6665
return `${diffMins} минут`;
6766
};
6867

69-
const getScorePercentage = () => {
68+
const getScoreDisplay = () => {
69+
if (!attemptData?.testAttemptForAdmin) return "Нет данных";
70+
const attempt = attemptData.testAttemptForAdmin;
7071
const total = (attempt.successfulCount || 0) + (attempt.errorsCount || 0);
71-
if (total === 0) return 0;
72-
return Math.round(((attempt.successfulCount || 0) / total) * 100);
72+
if (total === 0) return "0 правильных ответов";
73+
return `${attempt.successfulCount} из ${total} правильных ответов`;
7374
};
7475

7576
return (
@@ -202,12 +203,12 @@ const TestAttemptDetailView: FC = () => {
202203
sx={{ fontSize: "1.2rem", py: 1 }}
203204
/>
204205
</Grid>
205-
<Grid item xs={12}>
206-
<Typography variant="body2" color="text.secondary">
207-
Процент правильных ответов:
206+
<Grid item xs={12} md={6}>
207+
<Typography variant="h6" gutterBottom>
208+
Результат теста:
208209
</Typography>
209210
<Typography variant="h4" color="primary">
210-
{getScorePercentage()}%
211+
{getScoreDisplay()}
211212
</Typography>
212213
</Grid>
213214
</Grid>

src/features/admin-panel/tests-admin/test-attempt-detail.tsx

Lines changed: 79 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ import {
2626
Schedule as ScheduleIcon,
2727
} from "@mui/icons-material";
2828

29-
import {
30-
useTestAttemptQuery,
31-
useTestAttemptQuestionsQuery,
32-
} from "api/graphql/generated/graphql";
29+
import { useTestAttemptQuery } from "api/graphql/generated/graphql";
3330
import { AppSpinner } from "shared/components/spinners";
3431
import NoDataErrorMessage from "shared/components/no-data-error-message";
3532

@@ -46,21 +43,15 @@ const TestAttemptDetail: FC = () => {
4643
skip: !attemptId,
4744
});
4845

49-
const { data: questionsData, loading: questionsLoading } =
50-
useTestAttemptQuestionsQuery({
51-
variables: { attemptId: attemptId! },
52-
skip: !attemptId,
53-
});
54-
5546
const handleGoBack = () => {
5647
navigate(-1);
5748
};
5849

59-
if (attemptLoading || questionsLoading) return <AppSpinner />;
50+
if (attemptLoading || !attemptData?.testAttempt) return <AppSpinner />;
6051
if (attemptError || !attemptData?.testAttempt) return <NoDataErrorMessage />;
6152

6253
const attempt = attemptData.testAttempt;
63-
const questions = questionsData?.testAttemptQuestions || [];
54+
const questions = attempt.testAttemptQuestionResults || [];
6455

6556
const formatDate = (dateString: string | null | undefined) => {
6657
if (!dateString) return "Не завершен";
@@ -197,7 +188,7 @@ const TestAttemptDetail: FC = () => {
197188
<Chip
198189
label={attempt.successfulCount || 0}
199190
color="success"
200-
size="large"
191+
size="medium"
201192
sx={{ fontSize: "1.2rem", py: 1 }}
202193
/>
203194
</Grid>
@@ -208,7 +199,7 @@ const TestAttemptDetail: FC = () => {
208199
<Chip
209200
label={attempt.errorsCount || 0}
210201
color="error"
211-
size="large"
202+
size="medium"
212203
sx={{ fontSize: "1.2rem", py: 1 }}
213204
/>
214205
</Grid>
@@ -249,75 +240,81 @@ const TestAttemptDetail: FC = () => {
249240
</TableRow>
250241
</TableHead>
251242
<TableBody>
252-
{questions.map((questionResult, index) => {
253-
const question = questionResult.testQuestion;
254-
if (!question) return null;
243+
{questions
244+
.filter((questionResult) => questionResult !== null)
245+
.map((questionResult, index) => {
246+
const question = questionResult!.testQuestion;
247+
if (!question) return null;
255248

256-
return (
257-
<TableRow key={index} hover>
258-
<TableCell>
259-
<Typography variant="body1" fontWeight="medium">
260-
{index + 1}. {question.text}
261-
</Typography>
262-
</TableCell>
263-
<TableCell>
264-
<Box
265-
sx={{
266-
display: "flex",
267-
flexDirection: "column",
268-
gap: 1,
269-
}}
270-
>
271-
{question.testAnswers?.map((answer) => (
272-
<Chip
273-
key={answer?.id}
274-
label={answer?.text}
275-
color={answer?.correct ? "success" : "default"}
276-
variant={
277-
answer?.correct ? "filled" : "outlined"
278-
}
279-
size="small"
280-
/>
281-
))}
282-
</Box>
283-
</TableCell>
284-
<TableCell>
285-
<Box
286-
sx={{
287-
display: "flex",
288-
flexDirection: "column",
289-
gap: 1,
290-
}}
291-
>
292-
{questionResult.testAnswerResults?.map(
293-
(answerResult) => (
294-
<Chip
295-
key={answerResult.testAnswer?.id}
296-
label={answerResult.testAnswer?.text}
297-
color={
298-
answerResult.answer ? "primary" : "default"
299-
}
300-
variant="outlined"
301-
size="small"
302-
/>
303-
)
304-
)}
305-
</Box>
306-
</TableCell>
307-
<TableCell>
308-
<Chip
309-
label={
310-
questionResult.result
311-
? "Правильно"
312-
: "Неправильно"
313-
}
314-
color={questionResult.result ? "success" : "error"}
315-
size="small"
316-
/>
317-
</TableCell>
318-
</TableRow>
319-
);
320-
})}
249+
return (
250+
<TableRow key={index} hover>
251+
<TableCell>
252+
<Typography variant="body1" fontWeight="medium">
253+
{index + 1}. {question.text}
254+
</Typography>
255+
</TableCell>
256+
<TableCell>
257+
<Box
258+
sx={{
259+
display: "flex",
260+
flexDirection: "column",
261+
gap: 1,
262+
}}
263+
>
264+
{questionResult.testAnswerResults?.map(
265+
(answerResult) => (
266+
<Chip
267+
key={answerResult?.testAnswer?.id}
268+
label={answerResult?.testAnswer?.text}
269+
color="default"
270+
variant="outlined"
271+
size="small"
272+
/>
273+
)
274+
)}
275+
</Box>
276+
</TableCell>
277+
<TableCell>
278+
<Box
279+
sx={{
280+
display: "flex",
281+
flexDirection: "column",
282+
gap: 1,
283+
}}
284+
>
285+
{questionResult.testAnswerResults?.map(
286+
(answerResult) => (
287+
<Chip
288+
key={answerResult?.testAnswer?.id}
289+
label={answerResult?.testAnswer?.text}
290+
color={
291+
answerResult?.answer
292+
? "primary"
293+
: "default"
294+
}
295+
variant="outlined"
296+
size="small"
297+
/>
298+
)
299+
)}
300+
</Box>
301+
</TableCell>
302+
<TableCell>
303+
<Chip
304+
label={
305+
questionResult.result
306+
? "Правильно"
307+
: "Неправильно"
308+
}
309+
color={
310+
questionResult.result ? "success" : "error"
311+
}
312+
size="small"
313+
/>
314+
</TableCell>
315+
</TableRow>
316+
);
317+
})}
321318
</TableBody>
322319
</Table>
323320
</TableContainer>

0 commit comments

Comments
 (0)