From 162cb84244c81f7c19d6d54c18edbe2a297e29c0 Mon Sep 17 00:00:00 2001 From: Dmitry Date: Wed, 17 Dec 2025 03:56:22 +0300 Subject: [PATCH] fixes --- .../src/components/QuestionEditorDialog.tsx | 10 +- .../src/components/TestQuestionsManager.tsx | 6 +- .../forms/InputButtonsQuestionForm.tsx | 5 +- .../components/forms/SimpleQuestionForm.tsx | 15 +-- mnemo_cards_admin/src/types/questions.ts | 2 +- .../lib/api/v2/admin_tests_api_v2.dart | 12 +- .../lib/api/v2/tests_api_v2.dart | 8 +- .../lib/tests/test_manager.dart | 120 +++++++++++++++--- .../lib/user/user_manager.dart | 16 ++- 9 files changed, 136 insertions(+), 58 deletions(-) diff --git a/mnemo_cards_admin/src/components/QuestionEditorDialog.tsx b/mnemo_cards_admin/src/components/QuestionEditorDialog.tsx index b8abb6f..d31e3c4 100644 --- a/mnemo_cards_admin/src/components/QuestionEditorDialog.tsx +++ b/mnemo_cards_admin/src/components/QuestionEditorDialog.tsx @@ -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({
{questionType === QuestionType.SIMPLE ? ( ) || {}} onChange={(updated) => { setCurrentQuestion({ ...updated, @@ -199,7 +199,7 @@ export function QuestionEditorDialog({ /> ) : ( ) || {}} onChange={(updated) => { setCurrentQuestion({ ...updated, @@ -210,7 +210,7 @@ export function QuestionEditorDialog({ updated.word && updated.answer && updated.options?.length && - (updated as any).template + updated.template ), ) }} diff --git a/mnemo_cards_admin/src/components/TestQuestionsManager.tsx b/mnemo_cards_admin/src/components/TestQuestionsManager.tsx index 266036c..00a003d 100644 --- a/mnemo_cards_admin/src/components/TestQuestionsManager.tsx +++ b/mnemo_cards_admin/src/components/TestQuestionsManager.tsx @@ -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, diff --git a/mnemo_cards_admin/src/components/forms/InputButtonsQuestionForm.tsx b/mnemo_cards_admin/src/components/forms/InputButtonsQuestionForm.tsx index 0ab36a1..f4baac7 100644 --- a/mnemo_cards_admin/src/components/forms/InputButtonsQuestionForm.tsx +++ b/mnemo_cards_admin/src/components/forms/InputButtonsQuestionForm.tsx @@ -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({ setImage(value || '')} />
diff --git a/mnemo_cards_admin/src/components/forms/SimpleQuestionForm.tsx b/mnemo_cards_admin/src/components/forms/SimpleQuestionForm.tsx index 3930c61..0d504b7 100644 --- a/mnemo_cards_admin/src/components/forms/SimpleQuestionForm.tsx +++ b/mnemo_cards_admin/src/components/forms/SimpleQuestionForm.tsx @@ -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(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({ setImage(value || '')} /> diff --git a/mnemo_cards_admin/src/types/questions.ts b/mnemo_cards_admin/src/types/questions.ts index eff5055..047bc66 100644 --- a/mnemo_cards_admin/src/types/questions.ts +++ b/mnemo_cards_admin/src/types/questions.ts @@ -1,4 +1,4 @@ -export enum QuestionType { +export const enum QuestionType { SIMPLE = 'simple', INPUT_BUTTONS = 'input_buttons', } diff --git a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart index 07f3730..4d86308 100644 --- a/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/admin_tests_api_v2.dart @@ -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)), ), ); } diff --git a/mnemo_cards_backend/lib/api/v2/tests_api_v2.dart b/mnemo_cards_backend/lib/api/v2/tests_api_v2.dart index f50cfaf..0cc9c4f 100644 --- a/mnemo_cards_backend/lib/api/v2/tests_api_v2.dart +++ b/mnemo_cards_backend/lib/api/v2/tests_api_v2.dart @@ -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, где ключ 'attempts' содержит список попыток - final resultsMap = testStatistic.results as Map? ?? {}; + // Parse attempts from metadata JSON + // metadata хранится как Map, где ключ 'attempts' содержит список попыток + final resultsMap = json.decode(testStatistic.metadata) as Map? ?? {}; final resultsList = resultsMap['attempts'] as List? ?? []; final attempts = resultsList.map((result) { diff --git a/mnemo_cards_backend/lib/tests/test_manager.dart b/mnemo_cards_backend/lib/tests/test_manager.dart index 02b3b6f..1023805 100644 --- a/mnemo_cards_backend/lib/tests/test_manager.dart +++ b/mnemo_cards_backend/lib/tests/test_manager.dart @@ -21,9 +21,11 @@ class TestManager { if (statistics == null) return null; // Convert TestStatistic to TestStatisticsDto - // results is stored as Map with 'attempts' key - final results = statistics.results ?? {}; - final attempts = (results['attempts'] as List?) ?? []; + // metadata is stored as JSON string with 'attempts' key + final metadataMap = statistics.metadata.isNotEmpty + ? (json.decode(statistics.metadata) as Map? ?? {}) + : {}; + final attempts = (metadataMap['attempts'] as List?) ?? []; // 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; - return AbstractTestQuestion.fromJson({ + // Build question JSON from separate fields + final questionJson = { '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; + 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; + 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; - return AbstractTestQuestion.fromJson({ + // Build question JSON from separate fields + final questionJson = { '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; + 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; + 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; - return AbstractTestQuestion.fromJson({ + // Build question JSON from separate fields + final questionJson = { '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; + 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; + 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? ?? []; + + // UI data (image, text, audio, template) + final uiData = {}; + 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); diff --git a/mnemo_cards_backend/lib/user/user_manager.dart b/mnemo_cards_backend/lib/user/user_manager.dart index 9a87507..8395316 100644 --- a/mnemo_cards_backend/lib/user/user_manager.dart +++ b/mnemo_cards_backend/lib/user/user_manager.dart @@ -209,10 +209,12 @@ class UserManager { final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId); // Обновить результаты теста - // results хранится как Map, где ключ 'attempts' содержит список попыток - final currentResults = existingStat?.results ?? {}; + // metadata хранится как JSON string, где ключ 'attempts' содержит список попыток + final currentMetadata = existingStat?.metadata.isNotEmpty == true + ? (json.decode(existingStat!.metadata) as Map? ?? {}) + : {}; final attemptsKey = 'attempts'; - final currentAttempts = (currentResults[attemptsKey] as List?) ?? []; + final currentAttempts = (currentMetadata[attemptsKey] as List?) ?? []; final newAttempt = { 'sessionToken': testStat.sessionToken, @@ -220,15 +222,15 @@ class UserManager { }; final updatedAttempts = [...currentAttempts, newAttempt]; - final updatedResults = { - ...currentResults, + final updatedMetadata = { + ...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())), ), );