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 { useState, useEffect } from 'react'
|
||||||
import type { Question } from '@/types/questions'
|
import type { Question, SimpleQuestion, InputButtonsQuestion } from '@/types/questions'
|
||||||
import { QuestionType, isSimpleQuestion, isInputButtonsQuestion } from '@/types/questions'
|
import { QuestionType, isInputButtonsQuestion } from '@/types/questions'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -186,7 +186,7 @@ export function QuestionEditorDialog({
|
||||||
<div>
|
<div>
|
||||||
{questionType === QuestionType.SIMPLE ? (
|
{questionType === QuestionType.SIMPLE ? (
|
||||||
<SimpleQuestionForm
|
<SimpleQuestionForm
|
||||||
question={currentQuestion || {}}
|
question={(currentQuestion as Partial<SimpleQuestion>) || {}}
|
||||||
onChange={(updated) => {
|
onChange={(updated) => {
|
||||||
setCurrentQuestion({
|
setCurrentQuestion({
|
||||||
...updated,
|
...updated,
|
||||||
|
|
@ -199,7 +199,7 @@ export function QuestionEditorDialog({
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<InputButtonsQuestionForm
|
<InputButtonsQuestionForm
|
||||||
question={currentQuestion || {}}
|
question={(currentQuestion as Partial<InputButtonsQuestion>) || {}}
|
||||||
onChange={(updated) => {
|
onChange={(updated) => {
|
||||||
setCurrentQuestion({
|
setCurrentQuestion({
|
||||||
...updated,
|
...updated,
|
||||||
|
|
@ -210,7 +210,7 @@ export function QuestionEditorDialog({
|
||||||
updated.word &&
|
updated.word &&
|
||||||
updated.answer &&
|
updated.answer &&
|
||||||
updated.options?.length &&
|
updated.options?.length &&
|
||||||
(updated as any).template
|
updated.template
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import type { Question } from '@/types/questions'
|
import type { Question } from '@/types/questions'
|
||||||
import { QuestionType, isSimpleQuestion, isInputButtonsQuestion } from '@/types/questions'
|
import { QuestionType, isInputButtonsQuestion } from '@/types/questions'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Edit, Trash2, Plus, FileText, Type } from 'lucide-react'
|
import { Edit, Trash2, FileText, Type } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import type { InputButtonsQuestion, TestButton } from '@/types/questions'
|
import type { InputButtonsQuestion, TestButton } from '@/types/questions'
|
||||||
|
import { QuestionType } from '@/types/questions'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
@ -34,7 +35,7 @@ export function InputButtonsQuestionForm({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onChange({
|
onChange({
|
||||||
questionType: 'input_buttons',
|
questionType: QuestionType.INPUT_BUTTONS,
|
||||||
word,
|
word,
|
||||||
text: text || undefined,
|
text: text || undefined,
|
||||||
image: image || undefined,
|
image: image || undefined,
|
||||||
|
|
@ -103,7 +104,7 @@ export function InputButtonsQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Question Image"
|
label="Question Image"
|
||||||
value={image}
|
value={image}
|
||||||
onChange={setImage}
|
onChange={(value) => setImage(value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import type { SimpleQuestion, TestButton } from '@/types/questions'
|
import type { SimpleQuestion, TestButton } from '@/types/questions'
|
||||||
|
import { QuestionType } from '@/types/questions'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
|
@ -31,18 +32,6 @@ export function SimpleQuestionForm({
|
||||||
const [buttons, setButtons] = useState<TestButton[]>(question.options || [])
|
const [buttons, setButtons] = useState<TestButton[]>(question.options || [])
|
||||||
const [answer, setAnswer] = useState(question.answer || '')
|
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 addButton = () => {
|
||||||
const newButton: TestButton = {
|
const newButton: TestButton = {
|
||||||
id: `btn_${Date.now()}`,
|
id: `btn_${Date.now()}`,
|
||||||
|
|
@ -94,7 +83,7 @@ export function SimpleQuestionForm({
|
||||||
<ImageUpload
|
<ImageUpload
|
||||||
label="Question Image"
|
label="Question Image"
|
||||||
value={image}
|
value={image}
|
||||||
onChange={setImage}
|
onChange={(value) => setImage(value || '')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export enum QuestionType {
|
export const enum QuestionType {
|
||||||
SIMPLE = 'simple',
|
SIMPLE = 'simple',
|
||||||
INPUT_BUTTONS = 'input_buttons',
|
INPUT_BUTTONS = 'input_buttons',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -389,12 +389,12 @@ class AdminTestsApiV2 {
|
||||||
await _db.testDao.createTestQuestion(
|
await _db.testDao.createTestQuestion(
|
||||||
TestQuestionsCompanion.insert(
|
TestQuestionsCompanion.insert(
|
||||||
testId: testId,
|
testId: testId,
|
||||||
orderIndex: orderIndex++,
|
orderIndex: drift.Value(orderIndex++),
|
||||||
questionType: questionType,
|
questionType: questionType,
|
||||||
word: word,
|
word: word,
|
||||||
answer: answer,
|
answer: answer,
|
||||||
options: jsonEncode(buttons),
|
options: drift.Value(jsonEncode(buttons)),
|
||||||
uiData: jsonEncode(uiData),
|
uiData: drift.Value(jsonEncode(uiData)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -456,12 +456,12 @@ class AdminTestsApiV2 {
|
||||||
await _db.testDao.createTestQuestion(
|
await _db.testDao.createTestQuestion(
|
||||||
TestQuestionsCompanion.insert(
|
TestQuestionsCompanion.insert(
|
||||||
testId: newTest,
|
testId: newTest,
|
||||||
orderIndex: orderIndex++,
|
orderIndex: drift.Value(orderIndex++),
|
||||||
questionType: questionType,
|
questionType: questionType,
|
||||||
word: word,
|
word: word,
|
||||||
answer: answer,
|
answer: answer,
|
||||||
options: jsonEncode(buttons),
|
options: drift.Value(jsonEncode(buttons)),
|
||||||
uiData: jsonEncode(uiData),
|
uiData: drift.Value(jsonEncode(uiData)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ class TestsApiV2 {
|
||||||
// Get test statistics from Drift
|
// Get test statistics from Drift
|
||||||
final testStatistic = await _db.testDao.getTestStatistics(user.id!, testId);
|
final testStatistic = await _db.testDao.getTestStatistics(user.id!, testId);
|
||||||
|
|
||||||
if (testStatistic == null || testStatistic.results == null) {
|
if (testStatistic == null || testStatistic.metadata.isEmpty) {
|
||||||
return _ok({
|
return _ok({
|
||||||
'items': [],
|
'items': [],
|
||||||
'total': 0,
|
'total': 0,
|
||||||
|
|
@ -190,9 +190,9 @@ class TestsApiV2 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse attempts from results JSON
|
// Parse attempts from metadata JSON
|
||||||
// results хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
|
// metadata хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
|
||||||
final resultsMap = testStatistic.results as Map<String, dynamic>? ?? {};
|
final resultsMap = json.decode(testStatistic.metadata) as Map<String, dynamic>? ?? {};
|
||||||
final resultsList = resultsMap['attempts'] as List<dynamic>? ?? [];
|
final resultsList = resultsMap['attempts'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
final attempts = resultsList.map((result) {
|
final attempts = resultsList.map((result) {
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ class TestManager {
|
||||||
if (statistics == null) return null;
|
if (statistics == null) return null;
|
||||||
|
|
||||||
// Convert TestStatistic to TestStatisticsDto
|
// Convert TestStatistic to TestStatisticsDto
|
||||||
// results is stored as Map<String, dynamic> with 'attempts' key
|
// metadata is stored as JSON string with 'attempts' key
|
||||||
final results = statistics.results ?? <String, dynamic>{};
|
final metadataMap = statistics.metadata.isNotEmpty
|
||||||
final attempts = (results['attempts'] as List<dynamic>?) ?? [];
|
? (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
|
// Get words from the last attempt, or empty list if no attempts
|
||||||
final lastAttempt = attempts.isNotEmpty
|
final lastAttempt = attempts.isNotEmpty
|
||||||
|
|
@ -55,12 +57,33 @@ class TestManager {
|
||||||
|
|
||||||
// Convert Test to TestDto
|
// Convert Test to TestDto
|
||||||
final questionsList = questions.map((q) {
|
final questionsList = questions.map((q) {
|
||||||
// Parse question body based on type
|
// Build question JSON from separate fields
|
||||||
final body = json.decode(q.body) as Map<String, dynamic>;
|
final questionJson = <String, dynamic>{
|
||||||
return AbstractTestQuestion.fromJson({
|
|
||||||
'questionType': q.questionType,
|
'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();
|
}).toList();
|
||||||
|
|
||||||
return TestDto(
|
return TestDto(
|
||||||
|
|
@ -85,11 +108,33 @@ class TestManager {
|
||||||
final statistics = await _testStatisticsDto(userModel.id!, test.id);
|
final statistics = await _testStatisticsDto(userModel.id!, test.id);
|
||||||
|
|
||||||
final questionsList = questions.map((q) {
|
final questionsList = questions.map((q) {
|
||||||
final body = json.decode(q.body) as Map<String, dynamic>;
|
// Build question JSON from separate fields
|
||||||
return AbstractTestQuestion.fromJson({
|
final questionJson = <String, dynamic>{
|
||||||
'questionType': q.questionType,
|
'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();
|
}).toList();
|
||||||
|
|
||||||
testDtos.add(TestDto(
|
testDtos.add(TestDto(
|
||||||
|
|
@ -123,11 +168,33 @@ class TestManager {
|
||||||
final statistics = await _testStatisticsDto(user.id!, test.id);
|
final statistics = await _testStatisticsDto(user.id!, test.id);
|
||||||
|
|
||||||
final questionsList = questions.map((q) {
|
final questionsList = questions.map((q) {
|
||||||
final body = json.decode(q.body) as Map<String, dynamic>;
|
// Build question JSON from separate fields
|
||||||
return AbstractTestQuestion.fromJson({
|
final questionJson = <String, dynamic>{
|
||||||
'questionType': q.questionType,
|
'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();
|
}).toList();
|
||||||
|
|
||||||
testDtos.add(TestDto(
|
testDtos.add(TestDto(
|
||||||
|
|
@ -223,11 +290,30 @@ class TestManager {
|
||||||
final testId = await _db.testDao.createTest(testCompanion);
|
final testId = await _db.testDao.createTest(testCompanion);
|
||||||
|
|
||||||
// Create questions
|
// Create questions
|
||||||
|
int orderIndex = 0;
|
||||||
for (final question in testDto.questions) {
|
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(
|
final questionCompanion = TestQuestionsCompanion.insert(
|
||||||
testId: testId,
|
testId: testId,
|
||||||
|
orderIndex: drift.Value(orderIndex++),
|
||||||
questionType: question.questionType.name,
|
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);
|
await _db.testDao.createTestQuestion(questionCompanion);
|
||||||
|
|
|
||||||
|
|
@ -209,10 +209,12 @@ class UserManager {
|
||||||
final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId);
|
final existingStat = await _db.testDao.getTestStatistics(user.id!, testStat.testId);
|
||||||
|
|
||||||
// Обновить результаты теста
|
// Обновить результаты теста
|
||||||
// results хранится как Map<String, dynamic>, где ключ 'attempts' содержит список попыток
|
// metadata хранится как JSON string, где ключ 'attempts' содержит список попыток
|
||||||
final currentResults = existingStat?.results ?? <String, dynamic>{};
|
final currentMetadata = existingStat?.metadata.isNotEmpty == true
|
||||||
|
? (json.decode(existingStat!.metadata) as Map<String, dynamic>? ?? {})
|
||||||
|
: <String, dynamic>{};
|
||||||
final attemptsKey = 'attempts';
|
final attemptsKey = 'attempts';
|
||||||
final currentAttempts = (currentResults[attemptsKey] as List<dynamic>?) ?? [];
|
final currentAttempts = (currentMetadata[attemptsKey] as List<dynamic>?) ?? [];
|
||||||
|
|
||||||
final newAttempt = <String, dynamic>{
|
final newAttempt = <String, dynamic>{
|
||||||
'sessionToken': testStat.sessionToken,
|
'sessionToken': testStat.sessionToken,
|
||||||
|
|
@ -220,15 +222,15 @@ class UserManager {
|
||||||
};
|
};
|
||||||
|
|
||||||
final updatedAttempts = <dynamic>[...currentAttempts, newAttempt];
|
final updatedAttempts = <dynamic>[...currentAttempts, newAttempt];
|
||||||
final updatedResults = <String, dynamic>{
|
final updatedMetadata = <String, dynamic>{
|
||||||
...currentResults,
|
...currentMetadata,
|
||||||
attemptsKey: updatedAttempts,
|
attemptsKey: updatedAttempts,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingStat != null) {
|
if (existingStat != null) {
|
||||||
// Обновить существующую статистику
|
// Обновить существующую статистику
|
||||||
final updatedStat = existingStat.copyWith(
|
final updatedStat = existingStat.copyWith(
|
||||||
results: drift.Value(updatedResults),
|
metadata: json.encode(updatedMetadata),
|
||||||
updatedAt: PgDateTime(DateTime.now()),
|
updatedAt: PgDateTime(DateTime.now()),
|
||||||
);
|
);
|
||||||
await _db.testDao.updateTestStatistics(updatedStat);
|
await _db.testDao.updateTestStatistics(updatedStat);
|
||||||
|
|
@ -238,7 +240,7 @@ class UserManager {
|
||||||
TestStatisticsCompanion.insert(
|
TestStatisticsCompanion.insert(
|
||||||
userId: user.id!,
|
userId: user.id!,
|
||||||
testId: testStat.testId,
|
testId: testStat.testId,
|
||||||
results: drift.Value(updatedResults),
|
metadata: drift.Value(json.encode(updatedMetadata)),
|
||||||
completedAt: drift.Value(PgDateTime(DateTime.now())),
|
completedAt: drift.Value(PgDateTime(DateTime.now())),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue