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
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:
parent
3d179c95ef
commit
162cb84244
9 changed files with 136 additions and 58 deletions
|
|
@ -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
|
||||
),
|
||||
)
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export enum QuestionType {
|
||||
export const enum QuestionType {
|
||||
SIMPLE = 'simple',
|
||||
INPUT_BUTTONS = 'input_buttons',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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())),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue