fixes
Some checks are pending
Backend CI / test (push) Waiting to run
Backend CI / build (push) Blocked by required conditions
Deploy Admin Panel / Deploy Admin Panel (push) Waiting to run
Deploy Admin Panel / Admin Panel Verification (push) Blocked by required conditions
Deploy Mnemo Cards / Deploy Backend (push) Waiting to run
Deploy Mnemo Cards / Deploy Web App (push) Blocked by required conditions
Deploy Mnemo Cards / Final Verification (push) Blocked by required conditions

This commit is contained in:
Dmitry 2025-12-17 03:56:22 +03:00
parent 3d179c95ef
commit 162cb84244
9 changed files with 136 additions and 58 deletions

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import type { Question } from '@/types/questions'
import { QuestionType, isSimpleQuestion, isInputButtonsQuestion } from '@/types/questions'
import type { Question, SimpleQuestion, InputButtonsQuestion } from '@/types/questions'
import { QuestionType, isInputButtonsQuestion } from '@/types/questions'
import {
Dialog,
DialogContent,
@ -186,7 +186,7 @@ export function QuestionEditorDialog({
<div>
{questionType === QuestionType.SIMPLE ? (
<SimpleQuestionForm
question={currentQuestion || {}}
question={(currentQuestion as Partial<SimpleQuestion>) || {}}
onChange={(updated) => {
setCurrentQuestion({
...updated,
@ -199,7 +199,7 @@ export function QuestionEditorDialog({
/>
) : (
<InputButtonsQuestionForm
question={currentQuestion || {}}
question={(currentQuestion as Partial<InputButtonsQuestion>) || {}}
onChange={(updated) => {
setCurrentQuestion({
...updated,
@ -210,7 +210,7 @@ export function QuestionEditorDialog({
updated.word &&
updated.answer &&
updated.options?.length &&
(updated as any).template
updated.template
),
)
}}

View file

@ -1,10 +1,10 @@
import { useState } from 'react'
import type { Question } from '@/types/questions'
import { QuestionType, isSimpleQuestion, isInputButtonsQuestion } from '@/types/questions'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { QuestionType, isInputButtonsQuestion } from '@/types/questions'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Edit, Trash2, Plus, FileText, Type } from 'lucide-react'
import { Edit, Trash2, FileText, Type } from 'lucide-react'
import {
AlertDialog,
AlertDialogAction,

View file

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import type { InputButtonsQuestion, TestButton } from '@/types/questions'
import { QuestionType } from '@/types/questions'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@ -34,7 +35,7 @@ export function InputButtonsQuestionForm({
useEffect(() => {
onChange({
questionType: 'input_buttons',
questionType: QuestionType.INPUT_BUTTONS,
word,
text: text || undefined,
image: image || undefined,
@ -103,7 +104,7 @@ export function InputButtonsQuestionForm({
<ImageUpload
label="Question Image"
value={image}
onChange={setImage}
onChange={(value) => setImage(value || '')}
/>
</div>

View file

@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import type { SimpleQuestion, TestButton } from '@/types/questions'
import { QuestionType } from '@/types/questions'
import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
@ -31,18 +32,6 @@ export function SimpleQuestionForm({
const [buttons, setButtons] = useState<TestButton[]>(question.options || [])
const [answer, setAnswer] = useState(question.answer || '')
useEffect(() => {
onChange({
questionType: 'simple',
word,
text: text || undefined,
image: image || undefined,
audio: audio || undefined,
options: buttons,
answer,
})
}, [word, text, image, audio, buttons, answer, onChange])
const addButton = () => {
const newButton: TestButton = {
id: `btn_${Date.now()}`,
@ -94,7 +83,7 @@ export function SimpleQuestionForm({
<ImageUpload
label="Question Image"
value={image}
onChange={setImage}
onChange={(value) => setImage(value || '')}
/>
</div>

View file

@ -1,4 +1,4 @@
export enum QuestionType {
export const enum QuestionType {
SIMPLE = 'simple',
INPUT_BUTTONS = 'input_buttons',
}

View file

@ -389,12 +389,12 @@ class AdminTestsApiV2 {
await _db.testDao.createTestQuestion(
TestQuestionsCompanion.insert(
testId: testId,
orderIndex: orderIndex++,
orderIndex: drift.Value(orderIndex++),
questionType: questionType,
word: word,
answer: answer,
options: jsonEncode(buttons),
uiData: jsonEncode(uiData),
options: drift.Value(jsonEncode(buttons)),
uiData: drift.Value(jsonEncode(uiData)),
),
);
}
@ -456,12 +456,12 @@ class AdminTestsApiV2 {
await _db.testDao.createTestQuestion(
TestQuestionsCompanion.insert(
testId: newTest,
orderIndex: orderIndex++,
orderIndex: drift.Value(orderIndex++),
questionType: questionType,
word: word,
answer: answer,
options: jsonEncode(buttons),
uiData: jsonEncode(uiData),
options: drift.Value(jsonEncode(buttons)),
uiData: drift.Value(jsonEncode(uiData)),
),
);
}

View file

@ -180,7 +180,7 @@ class TestsApiV2 {
// Get test statistics from Drift
final testStatistic = await _db.testDao.getTestStatistics(user.id!, testId);
if (testStatistic == null || testStatistic.results == null) {
if (testStatistic == null || testStatistic.metadata.isEmpty) {
return _ok({
'items': [],
'total': 0,
@ -190,9 +190,9 @@ class TestsApiV2 {
});
}
// Parse attempts from results JSON
// results хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
final resultsMap = testStatistic.results as Map<String, dynamic>? ?? {};
// Parse attempts from metadata JSON
// metadata хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
final resultsMap = json.decode(testStatistic.metadata) as Map<String, dynamic>? ?? {};
final resultsList = resultsMap['attempts'] as List<dynamic>? ?? [];
final attempts = resultsList.map((result) {

View file

@ -21,9 +21,11 @@ class TestManager {
if (statistics == null) return null;
// Convert TestStatistic to TestStatisticsDto
// results is stored as Map<String, dynamic> with 'attempts' key
final results = statistics.results ?? <String, dynamic>{};
final attempts = (results['attempts'] as List<dynamic>?) ?? [];
// metadata is stored as JSON string with 'attempts' key
final metadataMap = statistics.metadata.isNotEmpty
? (json.decode(statistics.metadata) as Map<String, dynamic>? ?? {})
: <String, dynamic>{};
final attempts = (metadataMap['attempts'] as List<dynamic>?) ?? [];
// Get words from the last attempt, or empty list if no attempts
final lastAttempt = attempts.isNotEmpty
@ -55,12 +57,33 @@ class TestManager {
// Convert Test to TestDto
final questionsList = questions.map((q) {
// Parse question body based on type
final body = json.decode(q.body) as Map<String, dynamic>;
return AbstractTestQuestion.fromJson({
// Build question JSON from separate fields
final questionJson = <String, dynamic>{
'questionType': q.questionType,
...body,
});
'id': q.id,
'word': q.word,
};
// Parse options (JSON array of buttons)
try {
final options = json.decode(q.options) as List<dynamic>;
questionJson['buttons'] = options;
} catch (e) {
questionJson['buttons'] = [];
}
// Add answer
questionJson['answer'] = q.answer;
// Parse uiData (image, text, audio, template)
try {
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
questionJson.addAll(uiData);
} catch (e) {
// If uiData is empty or invalid, ignore
}
return AbstractTestQuestion.fromJson(questionJson);
}).toList();
return TestDto(
@ -85,11 +108,33 @@ class TestManager {
final statistics = await _testStatisticsDto(userModel.id!, test.id);
final questionsList = questions.map((q) {
final body = json.decode(q.body) as Map<String, dynamic>;
return AbstractTestQuestion.fromJson({
// Build question JSON from separate fields
final questionJson = <String, dynamic>{
'questionType': q.questionType,
...body,
});
'id': q.id,
'word': q.word,
};
// Parse options (JSON array of buttons)
try {
final options = json.decode(q.options) as List<dynamic>;
questionJson['buttons'] = options;
} catch (e) {
questionJson['buttons'] = [];
}
// Add answer
questionJson['answer'] = q.answer;
// Parse uiData (image, text, audio, template)
try {
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
questionJson.addAll(uiData);
} catch (e) {
// If uiData is empty or invalid, ignore
}
return AbstractTestQuestion.fromJson(questionJson);
}).toList();
testDtos.add(TestDto(
@ -123,11 +168,33 @@ class TestManager {
final statistics = await _testStatisticsDto(user.id!, test.id);
final questionsList = questions.map((q) {
final body = json.decode(q.body) as Map<String, dynamic>;
return AbstractTestQuestion.fromJson({
// Build question JSON from separate fields
final questionJson = <String, dynamic>{
'questionType': q.questionType,
...body,
});
'id': q.id,
'word': q.word,
};
// Parse options (JSON array of buttons)
try {
final options = json.decode(q.options) as List<dynamic>;
questionJson['buttons'] = options;
} catch (e) {
questionJson['buttons'] = [];
}
// Add answer
questionJson['answer'] = q.answer;
// Parse uiData (image, text, audio, template)
try {
final uiData = json.decode(q.uiData) as Map<String, dynamic>;
questionJson.addAll(uiData);
} catch (e) {
// If uiData is empty or invalid, ignore
}
return AbstractTestQuestion.fromJson(questionJson);
}).toList();
testDtos.add(TestDto(
@ -223,11 +290,30 @@ class TestManager {
final testId = await _db.testDao.createTest(testCompanion);
// Create questions
int orderIndex = 0;
for (final question in testDto.questions) {
final questionJson = question.toJson();
// Extract key fields
final word = questionJson['word'] as String? ?? '';
final answer = questionJson['answer'] as String? ?? '';
final buttons = questionJson['buttons'] as List<dynamic>? ?? [];
// UI data (image, text, audio, template)
final uiData = <String, dynamic>{};
if (questionJson['image'] != null) uiData['image'] = questionJson['image'];
if (questionJson['text'] != null) uiData['text'] = questionJson['text'];
if (questionJson['audio'] != null) uiData['audio'] = questionJson['audio'];
if (questionJson['template'] != null) uiData['template'] = questionJson['template'];
final questionCompanion = TestQuestionsCompanion.insert(
testId: testId,
orderIndex: drift.Value(orderIndex++),
questionType: question.questionType.name,
body: json.encode(question.toJson()),
word: word,
answer: answer,
options: drift.Value(json.encode(buttons)),
uiData: drift.Value(json.encode(uiData)),
);
await _db.testDao.createTestQuestion(questionCompanion);

View file

@ -209,10 +209,12 @@ class UserManager {
final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId);
// Обновить результаты теста
// results хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
final currentResults = existingStat?.results ?? <String, dynamic>{};
// metadata хранится как JSON string, где ключ 'attempts' содержит список попыток
final currentMetadata = existingStat?.metadata.isNotEmpty == true
? (json.decode(existingStat!.metadata) as Map<String, dynamic>? ?? {})
: <String, dynamic>{};
final attemptsKey = 'attempts';
final currentAttempts = (currentResults[attemptsKey] as List<dynamic>?) ?? [];
final currentAttempts = (currentMetadata[attemptsKey] as List<dynamic>?) ?? [];
final newAttempt = <String, dynamic>{
'sessionToken': testStat.sessionToken,
@ -220,15 +222,15 @@ class UserManager {
};
final updatedAttempts = <dynamic>[...currentAttempts, newAttempt];
final updatedResults = <String, dynamic>{
...currentResults,
final updatedMetadata = <String, dynamic>{
...currentMetadata,
attemptsKey: updatedAttempts,
};
if (existingStat != null) {
// Обновить существующую статистику
final updatedStat = existingStat.copyWith(
results: drift.Value(updatedResults),
metadata: json.encode(updatedMetadata),
updatedAt: PgDateTime(DateTime.now()),
);
await _db.testDao.updateTestStatistics(updatedStat);
@ -238,7 +240,7 @@ class UserManager {
TestStatisticsCompanion.insert(
userId: user.id!,
testId: testStat.testId,
results: drift.Value(updatedResults),
metadata: drift.Value(json.encode(updatedMetadata)),
completedAt: drift.Value(PgDateTime(DateTime.now())),
),
);