backaend + admin
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
e4e6a259c7
commit
4f29ccd3c8
9 changed files with 185 additions and 72 deletions
|
|
@ -226,7 +226,7 @@ export default function CardsPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorMessage = error instanceof CardsApiError
|
const errorMessage = isCardsApiError(error)
|
||||||
? error.message
|
? error.message
|
||||||
: formatApiError(error)
|
: formatApiError(error)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default function PacksPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const errorMessage = error instanceof PacksApiError
|
const errorMessage = isPacksApiError(error)
|
||||||
? error.message
|
? error.message
|
||||||
: formatApiError(error)
|
: formatApiError(error)
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -73,9 +73,6 @@ extension GetItInjectableX on _i174.GetIt {
|
||||||
gh.lazySingleton<_i240.TelegramAuthCodeService>(
|
gh.lazySingleton<_i240.TelegramAuthCodeService>(
|
||||||
() => _i240.TelegramAuthCodeService(),
|
() => _i240.TelegramAuthCodeService(),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i1029.StatisticsCalculator>(
|
|
||||||
() => _i1029.StatisticsCalculator(),
|
|
||||||
);
|
|
||||||
gh.lazySingleton<_i377.SubscriptionManager>(
|
gh.lazySingleton<_i377.SubscriptionManager>(
|
||||||
() => _i377.SubscriptionManager(gh<_i1072.AppDatabase>()),
|
() => _i377.SubscriptionManager(gh<_i1072.AppDatabase>()),
|
||||||
);
|
);
|
||||||
|
|
@ -97,6 +94,9 @@ extension GetItInjectableX on _i174.GetIt {
|
||||||
gh.lazySingleton<_i71.SessionTracker>(
|
gh.lazySingleton<_i71.SessionTracker>(
|
||||||
() => _i71.SessionTracker(gh<_i1072.AppDatabase>()),
|
() => _i71.SessionTracker(gh<_i1072.AppDatabase>()),
|
||||||
);
|
);
|
||||||
|
gh.lazySingleton<_i1029.StatisticsCalculator>(
|
||||||
|
() => _i1029.StatisticsCalculator(gh<_i1072.AppDatabase>()),
|
||||||
|
);
|
||||||
gh.lazySingleton<_i909.WordStatisticsManager>(
|
gh.lazySingleton<_i909.WordStatisticsManager>(
|
||||||
() => _i909.WordStatisticsManager(gh<_i1072.AppDatabase>()),
|
() => _i909.WordStatisticsManager(gh<_i1072.AppDatabase>()),
|
||||||
);
|
);
|
||||||
|
|
@ -125,15 +125,6 @@ extension GetItInjectableX on _i174.GetIt {
|
||||||
gh<_i222.RustorePurchaseHandler>(),
|
gh<_i222.RustorePurchaseHandler>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i985.TasksApiV2>(
|
|
||||||
() => _i985.TasksApiV2(gh<_i586.TaskManager>()),
|
|
||||||
);
|
|
||||||
gh.lazySingleton<_i151.PromoCodesManager>(
|
|
||||||
() => _i151.PromoCodesManager(
|
|
||||||
gh<_i1072.AppDatabase>(),
|
|
||||||
gh<_i1009.PaymentManager>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
gh.lazySingleton<_i280.UserManager>(
|
gh.lazySingleton<_i280.UserManager>(
|
||||||
() => _i280.UserManager(
|
() => _i280.UserManager(
|
||||||
gh<_i1072.AppDatabase>(),
|
gh<_i1072.AppDatabase>(),
|
||||||
|
|
@ -141,6 +132,23 @@ extension GetItInjectableX on _i174.GetIt {
|
||||||
gh<_i71.SessionTracker>(),
|
gh<_i71.SessionTracker>(),
|
||||||
gh<_i1029.StatisticsCalculator>(),
|
gh<_i1029.StatisticsCalculator>(),
|
||||||
gh<_i802.AchievementManager>(),
|
gh<_i802.AchievementManager>(),
|
||||||
|
gh<_i909.WordStatisticsManager>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
gh.lazySingleton<_i985.TasksApiV2>(
|
||||||
|
() => _i985.TasksApiV2(gh<_i586.TaskManager>()),
|
||||||
|
);
|
||||||
|
gh.lazySingleton<_i483.AdminAuthApiV2>(
|
||||||
|
() => _i483.AdminAuthApiV2(
|
||||||
|
gh<_i240.TelegramAuthCodeService>(),
|
||||||
|
gh<_i280.UserManager>(),
|
||||||
|
gh<_i108.JwtService>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
gh.lazySingleton<_i151.PromoCodesManager>(
|
||||||
|
() => _i151.PromoCodesManager(
|
||||||
|
gh<_i1072.AppDatabase>(),
|
||||||
|
gh<_i1009.PaymentManager>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i560.UserManager>(
|
gh.lazySingleton<_i560.UserManager>(
|
||||||
|
|
@ -194,13 +202,6 @@ extension GetItInjectableX on _i174.GetIt {
|
||||||
gh<_i240.TelegramAuthCodeService>(),
|
gh<_i240.TelegramAuthCodeService>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
gh.lazySingleton<_i483.AdminAuthApiV2>(
|
|
||||||
() => _i483.AdminAuthApiV2(
|
|
||||||
gh<_i240.TelegramAuthCodeService>(),
|
|
||||||
gh<_i280.UserManager>(),
|
|
||||||
gh<_i108.JwtService>(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
gh.lazySingleton<_i433.PackDtoConverter>(
|
gh.lazySingleton<_i433.PackDtoConverter>(
|
||||||
() => _i433.PackDtoConverter(
|
() => _i433.PackDtoConverter(
|
||||||
gh<_i908.ProductsPriceResolver>(),
|
gh<_i908.ProductsPriceResolver>(),
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ class UsersApiV2 {
|
||||||
|
|
||||||
/// GET /api/v2/users/me
|
/// GET /api/v2/users/me
|
||||||
/// Returns current authenticated user.
|
/// Returns current authenticated user.
|
||||||
|
///
|
||||||
|
/// Рассчитывает packProgress, studyDates, categoryMinutes на лету
|
||||||
|
/// из WordStatistics и StudySessions (вместо удаленных полей UserDatas)
|
||||||
@Route.get('/users/me')
|
@Route.get('/users/me')
|
||||||
Future<Response> getCurrentUser(Request request) async {
|
Future<Response> getCurrentUser(Request request) async {
|
||||||
final user = request.user;
|
final user = request.user;
|
||||||
|
|
@ -63,7 +66,37 @@ class UsersApiV2 {
|
||||||
return _unauthorized();
|
return _unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
final dto = await user.toDto();
|
if (user.id == null) {
|
||||||
|
return _json({'error': 'user_id_not_found'}, statusCode: 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рассчитать статистику на лету
|
||||||
|
final packProgress = await _statisticsCalculator.calculateAllPackProgress(user.id!);
|
||||||
|
final studyDates = await _statisticsCalculator.calculateStudyDates(user.id!);
|
||||||
|
final categoryMinutes = await _statisticsCalculator.calculateCategoryMinutes(user.id!);
|
||||||
|
|
||||||
|
// Получить статистику по словам из WordStatistics
|
||||||
|
final wordStats = await _db.wordStatisticsDao.getUserStatistics(user.id!);
|
||||||
|
final wordsDto = AllWordsStatisticsDto(
|
||||||
|
words: wordStats.map((stat) {
|
||||||
|
// Найти карточку для получения слова
|
||||||
|
// Для простоты используем cardId как word (в будущем можно улучшить)
|
||||||
|
return WordStatisticsDto(
|
||||||
|
word: stat.cardId, // Временное решение - нужно получить original из GameCard
|
||||||
|
correct: stat.correctAnswers.toDouble(),
|
||||||
|
incorrect: stat.incorrectAnswers.toDouble(),
|
||||||
|
skipped: 0,
|
||||||
|
questionTypes: {},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final dto = await user.toDtoWithCalculatedData(
|
||||||
|
packProgress: packProgress,
|
||||||
|
studyDates: studyDates,
|
||||||
|
categoryMinutes: categoryMinutes,
|
||||||
|
wordsStatistics: wordsDto,
|
||||||
|
);
|
||||||
return _json(dto.toJson());
|
return _json(dto.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,14 +278,14 @@ class UsersApiV2 {
|
||||||
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
return _json({'error': 'user_data_not_found'}, statusCode: 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use UserDataModel extension to convert to DTO
|
// Рассчитать studyDates на лету из StudySessions
|
||||||
// TODO: Need to create proper conversion from Drift UserData to UserDataDto
|
final studyDates = await _statisticsCalculator.calculateStudyDates(user.id!);
|
||||||
// For now, return basic structure
|
|
||||||
return _json({
|
return _json({
|
||||||
'totalStudyTimeMinutes': userData.totalStudyTimeMinutes,
|
'totalStudyTimeMinutes': userData.totalStudyTimeMinutes,
|
||||||
'currentStreak': userData.currentStreak,
|
'currentStreak': userData.currentStreak,
|
||||||
'longestStreak': userData.longestStreak,
|
'longestStreak': userData.longestStreak,
|
||||||
'studyDates': (userData.studyDates ?? []).map((d) => d.toIso8601String()).toList(),
|
'studyDates': studyDates.map((d) => d.toIso8601String()).toList(),
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return _json(
|
return _json(
|
||||||
|
|
@ -280,11 +313,14 @@ class UsersApiV2 {
|
||||||
return _json([]);
|
return _json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Рассчитать packProgress на лету из WordStatistics
|
||||||
|
final allPackProgress = await _statisticsCalculator.calculateAllPackProgress(user.id!);
|
||||||
|
|
||||||
final packProgress = packId != null
|
final packProgress = packId != null
|
||||||
? userData.packProgress.where((p) => p.packId == packId).toList()
|
? allPackProgress.where((p) => p.packId == packId).toList()
|
||||||
: userData.packProgress;
|
: allPackProgress;
|
||||||
|
|
||||||
final dtos = packProgress.map((p) => p.toDto()).toList();
|
final dtos = packProgress;
|
||||||
return _json(dtos.map((dto) => dto.toJson()).toList());
|
return _json(dtos.map((dto) => dto.toJson()).toList());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return _json(
|
return _json(
|
||||||
|
|
@ -444,8 +480,9 @@ class UsersApiV2 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final timelineData = _statisticsCalculator.getTimelineStatistics(
|
// Рассчитать timeline statistics (теперь принимает userId вместо UserDataModel)
|
||||||
userData,
|
final timelineData = await _statisticsCalculator.getTimelineStatistics(
|
||||||
|
user.id!,
|
||||||
period: period,
|
period: period,
|
||||||
from: fromDate,
|
from: fromDate,
|
||||||
to: toDate,
|
to: toDate,
|
||||||
|
|
@ -498,24 +535,20 @@ class UsersApiV2 {
|
||||||
throw StateError('User data not found');
|
throw StateError('User data not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update study dates
|
|
||||||
final now = DateTime.now();
|
|
||||||
final studyDates = List<DateTime>.from(userData.studyDates ?? []);
|
|
||||||
studyDates.add(now);
|
|
||||||
|
|
||||||
// Update total study time
|
// Update total study time
|
||||||
|
final now = DateTime.now();
|
||||||
final totalStudyTime =
|
final totalStudyTime =
|
||||||
userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes;
|
userData.totalStudyTimeMinutes + sessionDto.duration.inMinutes;
|
||||||
|
|
||||||
// Recalculate streak
|
// Recalculate streak from StudySessions (studyDates теперь рассчитывается на лету)
|
||||||
|
final studyDates = await _statisticsCalculator.calculateStudyDates(user.id!);
|
||||||
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
|
final currentStreak = _statisticsCalculator.calculateStreak(studyDates);
|
||||||
final longestStreak = max(userData.longestStreak, currentStreak);
|
final longestStreak = max(userData.longestStreak, currentStreak);
|
||||||
|
|
||||||
// Update user data
|
// Update user data (без studyDates - это поле удалено, рассчитывается на лету)
|
||||||
await _db.userDao.updateUserDataPartial(
|
await _db.userDao.updateUserDataPartial(
|
||||||
UserDatasCompanion(
|
UserDatasCompanion(
|
||||||
userId: drift.Value(user.id!),
|
userId: drift.Value(user.id!),
|
||||||
studyDates: drift.Value(studyDates),
|
|
||||||
totalStudyTimeMinutes: drift.Value(totalStudyTime),
|
totalStudyTimeMinutes: drift.Value(totalStudyTime),
|
||||||
currentStreak: drift.Value(currentStreak),
|
currentStreak: drift.Value(currentStreak),
|
||||||
longestStreak: drift.Value(longestStreak),
|
longestStreak: drift.Value(longestStreak),
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,15 @@ class PackDao extends DatabaseAccessor<AppDatabase> with _$PackDaoMixin {
|
||||||
return (select(gameCards)..where((c) => c.id.isIn(ids))).get();
|
return (select(gameCards)..where((c) => c.id.isIn(ids))).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Найти карточки по original (слову)
|
||||||
|
/// Возвращает все карточки с указанным original (может быть несколько)
|
||||||
|
Future<List<GameCard>> searchCardsByOriginal(String original) {
|
||||||
|
return (select(gameCards)
|
||||||
|
..where((c) => c.original.equals(original) & c.isDeleted.equals(false))
|
||||||
|
..limit(10) // Ограничиваем для производительности
|
||||||
|
).get();
|
||||||
|
}
|
||||||
|
|
||||||
/// Создать карточку
|
/// Создать карточку
|
||||||
Future<String> createCard(GameCardsCompanion card) async {
|
Future<String> createCard(GameCardsCompanion card) async {
|
||||||
final inserted = await into(gameCards).insertReturning(card);
|
final inserted = await into(gameCards).insertReturning(card);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import 'package:injectable/injectable.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
import 'package:mnemo_cards_backend/database/database.dart';
|
import 'package:mnemo_cards_backend/database/database.dart';
|
||||||
import 'package:drift_postgres/drift_postgres.dart';
|
|
||||||
|
|
||||||
/// Service for calculating various user statistics from raw data
|
/// Service for calculating various user statistics from raw data
|
||||||
@lazySingleton
|
@lazySingleton
|
||||||
|
|
@ -139,10 +138,10 @@ class StatisticsCalculator {
|
||||||
if (session.packId == null) continue;
|
if (session.packId == null) continue;
|
||||||
|
|
||||||
// Получить категорию пака
|
// Получить категорию пака
|
||||||
// Примечание: если в CardPacks нет поля category, используем 'unknown'
|
// Примечание: в CardPacks нет поля category, используем 'unknown'
|
||||||
// В будущем можно добавить поле category в CardPacks или использовать другую логику
|
// В будущем можно добавить поле category в CardPacks или использовать другую логику
|
||||||
final pack = await _db.packDao.getPackById(session.packId!);
|
final pack = await _db.packDao.getPackById(session.packId!);
|
||||||
final category = pack != null ? (pack.category ?? 'unknown') : 'unknown';
|
final category = pack != null ? 'unknown' : 'unknown'; // TODO: добавить category в CardPacks
|
||||||
|
|
||||||
// Рассчитать время сессии в минутах
|
// Рассчитать время сессии в минутах
|
||||||
int sessionMinutes = 0;
|
int sessionMinutes = 0;
|
||||||
|
|
@ -296,18 +295,42 @@ class StatisticsCalculator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get timeline statistics for a specific period
|
/// Get timeline statistics for a specific period
|
||||||
Map<String, dynamic> getTimelineStatistics(
|
///
|
||||||
UserDataModel data, {
|
/// Примечание: теперь принимает рассчитанные данные вместо UserDataModel
|
||||||
|
/// для работы с новой структурой (без удаленных полей)
|
||||||
|
Future<Map<String, dynamic>> getTimelineStatistics(
|
||||||
|
String userId, {
|
||||||
required String period,
|
required String period,
|
||||||
DateTime? from,
|
DateTime? from,
|
||||||
DateTime? to,
|
DateTime? to,
|
||||||
}) {
|
}) async {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final startDate = from ?? _getStartDateForPeriod(period, now);
|
final startDate = from ?? _getStartDateForPeriod(period, now);
|
||||||
final endDate = to ?? now;
|
final endDate = to ?? now;
|
||||||
|
|
||||||
final dailyTime = calculateDailyStudyTime(data);
|
// Получить сессии для расчета времени
|
||||||
final studyDates = data.studyDates;
|
final sessions = await _db.statisticsDao.getSessionsByUserId(
|
||||||
|
userId,
|
||||||
|
fromDate: startDate,
|
||||||
|
toDate: endDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Рассчитать ежедневное время из сессий
|
||||||
|
final dailyTime = <DateTime, int>{};
|
||||||
|
for (final session in sessions) {
|
||||||
|
if (session.endTime != null && session.startTime != null) {
|
||||||
|
final date = DateTime(
|
||||||
|
session.startTime.dateTime.year,
|
||||||
|
session.startTime.dateTime.month,
|
||||||
|
session.startTime.dateTime.day,
|
||||||
|
);
|
||||||
|
final duration = session.endTime!.dateTime.difference(session.startTime.dateTime);
|
||||||
|
dailyTime[date] = (dailyTime[date] ?? 0) + duration.inMinutes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получить studyDates
|
||||||
|
final studyDates = await calculateStudyDates(userId);
|
||||||
|
|
||||||
// Filter data for the period
|
// Filter data for the period
|
||||||
final periodDailyTime = <DateTime, int>{};
|
final periodDailyTime = <DateTime, int>{};
|
||||||
|
|
@ -334,7 +357,7 @@ class StatisticsCalculator {
|
||||||
periodDailyTime.values.fold<int>(0, (sum, time) => sum + time);
|
periodDailyTime.values.fold<int>(0, (sum, time) => sum + time);
|
||||||
final averageDailyMinutes = activeDays > 0 ? totalMinutes / activeDays : 0;
|
final averageDailyMinutes = activeDays > 0 ? totalMinutes / activeDays : 0;
|
||||||
|
|
||||||
// Calculate streak in period
|
// Calculate streak in period (из рассчитанных studyDates)
|
||||||
final periodStreak = calculateStreak(periodStudyDates);
|
final periodStreak = calculateStreak(periodStudyDates);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,18 @@ import 'package:mnemo_cards_common_backend/mnemo_cards_common_backend.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
extension UserDataModelExtension on UserDataModel {
|
extension UserDataModelExtension on UserDataModel {
|
||||||
UserDataDto toDto() {
|
/// Конвертация в DTO с использованием данных из модели
|
||||||
|
///
|
||||||
|
/// Примечание: packProgress, studyDates, categoryMinutes теперь рассчитываются на лету
|
||||||
|
/// и должны передаваться как параметры
|
||||||
|
UserDataDto toDto({
|
||||||
|
List<PackProgressDto>? calculatedPackProgress,
|
||||||
|
List<DateTime>? calculatedStudyDates,
|
||||||
|
Map<String, int>? calculatedCategoryMinutes,
|
||||||
|
AllWordsStatisticsDto? calculatedWordsStatistics,
|
||||||
|
}) {
|
||||||
return UserDataDto(
|
return UserDataDto(
|
||||||
allWordsStatistics: AllWordsStatisticsDto(
|
allWordsStatistics: calculatedWordsStatistics ?? AllWordsStatisticsDto(
|
||||||
words: words.map((model) => model.toDto()).toList(),
|
words: words.map((model) => model.toDto()).toList(),
|
||||||
),
|
),
|
||||||
allTestsStatistics: AllTestsStatisticsDto(
|
allTestsStatistics: AllTestsStatisticsDto(
|
||||||
|
|
@ -20,11 +29,11 @@ extension UserDataModelExtension on UserDataModel {
|
||||||
totalStudyTimeMinutes: totalStudyTimeMinutes,
|
totalStudyTimeMinutes: totalStudyTimeMinutes,
|
||||||
currentStreak: currentStreak,
|
currentStreak: currentStreak,
|
||||||
longestStreak: longestStreak,
|
longestStreak: longestStreak,
|
||||||
packProgress: {
|
packProgress: calculatedPackProgress != null
|
||||||
for (final progress in packProgress) progress.packId: progress.toDto()
|
? {for (final progress in calculatedPackProgress) progress.packId: progress}
|
||||||
},
|
: {for (final progress in packProgress) progress.packId: progress.toDto()},
|
||||||
studyDates: List<DateTime>.from(studyDates),
|
studyDates: calculatedStudyDates ?? List<DateTime>.from(studyDates),
|
||||||
categoryMinutes: Map<String, int>.from(categoryMinutes),
|
categoryMinutes: calculatedCategoryMinutes ?? Map<String, int>.from(categoryMinutes),
|
||||||
achievements: achievements.map((model) => model.toDto()).toList(),
|
achievements: achievements.map((model) => model.toDto()).toList(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class UserManager {
|
||||||
if (token.expires.dateTime.isAfter(now)) {
|
if (token.expires.dateTime.isAfter(now)) {
|
||||||
return token.token;
|
return token.token;
|
||||||
}
|
}
|
||||||
await _db.userDao.deleteToken(token.id);
|
await _db.userDao.softDeleteToken(token.id);
|
||||||
}
|
}
|
||||||
final userToken = Secure.token();
|
final userToken = Secure.token();
|
||||||
await _db.userDao.createToken(
|
await _db.userDao.createToken(
|
||||||
|
|
@ -76,7 +76,7 @@ class UserManager {
|
||||||
}
|
}
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (token.expires.dateTime.isBefore(now)) {
|
if (token.expires.dateTime.isBefore(now)) {
|
||||||
await _db.userDao.deleteToken(token.id);
|
await _db.userDao.softDeleteToken(token.id);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
final user = await fetchUser(token.userId);
|
final user = await fetchUser(token.userId);
|
||||||
|
|
@ -253,22 +253,28 @@ class UserManager {
|
||||||
if (cards.isNotEmpty) {
|
if (cards.isNotEmpty) {
|
||||||
final card = cards.first;
|
final card = cards.first;
|
||||||
|
|
||||||
// Записать каждый ответ (correct/incorrect)
|
// Записать ответы
|
||||||
// correct и incorrect в WordStatisticsDto - это уже агрегированные значения
|
// correct и incorrect в WordStatisticsDto - это уже агрегированные значения
|
||||||
// Но для простоты запишем их как один ответ
|
// Записываем их как несколько ответов для точности статистики
|
||||||
if (wordStat.correct > 0) {
|
// Но для простоты запишем один раз с правильным количеством
|
||||||
await _wordStatisticsManager.recordAnswer(
|
// WordStatisticsManager.recordAnswer() обновит существующую запись
|
||||||
userId: user.id!,
|
final totalAnswers = (wordStat.correct + wordStat.incorrect).toInt();
|
||||||
cardId: card.id,
|
if (totalAnswers > 0) {
|
||||||
isCorrect: true,
|
// Записываем все ответы (правильные и неправильные)
|
||||||
);
|
for (var i = 0; i < wordStat.correct.toInt(); i++) {
|
||||||
}
|
await _wordStatisticsManager.recordAnswer(
|
||||||
if (wordStat.incorrect > 0) {
|
userId: user.id!,
|
||||||
await _wordStatisticsManager.recordAnswer(
|
cardId: card.id,
|
||||||
userId: user.id!,
|
isCorrect: true,
|
||||||
cardId: card.id,
|
);
|
||||||
isCorrect: false,
|
}
|
||||||
);
|
for (var i = 0; i < wordStat.incorrect.toInt(); i++) {
|
||||||
|
await _wordStatisticsManager.recordAnswer(
|
||||||
|
userId: user.id!,
|
||||||
|
cardId: card.id,
|
||||||
|
isCorrect: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import 'package:mnemo_cards_backend/user/user_data_model.dart';
|
||||||
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
import 'package:mnemo_cards_common/mnemo_cards_common.dart';
|
||||||
|
|
||||||
extension UserModelExtension on UserModel {
|
extension UserModelExtension on UserModel {
|
||||||
|
/// Конвертация в DTO
|
||||||
|
///
|
||||||
|
/// Примечание: для расчета packProgress, studyDates, categoryMinutes на лету
|
||||||
|
/// используйте toDtoWithCalculatedData()
|
||||||
Future<UserDto> toDto() async {
|
Future<UserDto> toDto() async {
|
||||||
final activeSubscription = subscriptionModel != null && subscriptionModel!.isActive;
|
final activeSubscription = subscriptionModel != null && subscriptionModel!.isActive;
|
||||||
return UserDto(
|
return UserDto(
|
||||||
|
|
@ -16,4 +20,32 @@ extension UserModelExtension on UserModel {
|
||||||
userDataDto: userData?.toDto(),
|
userDataDto: userData?.toDto(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Конвертация в DTO с рассчитанными данными на лету
|
||||||
|
///
|
||||||
|
/// Используется когда нужно рассчитать packProgress, studyDates, categoryMinutes
|
||||||
|
/// из WordStatistics и StudySessions вместо использования удаленных полей UserDatas
|
||||||
|
Future<UserDto> toDtoWithCalculatedData({
|
||||||
|
required List<PackProgressDto> packProgress,
|
||||||
|
required List<DateTime> studyDates,
|
||||||
|
required Map<String, int> categoryMinutes,
|
||||||
|
AllWordsStatisticsDto? wordsStatistics,
|
||||||
|
}) async {
|
||||||
|
final activeSubscription = subscriptionModel != null && subscriptionModel!.isActive;
|
||||||
|
return UserDto(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
email: email,
|
||||||
|
admin: admin,
|
||||||
|
packs: packs.map((e) => e.id?.toString()).whereNotNull().toList(),
|
||||||
|
subscription: activeSubscription,
|
||||||
|
subscriptionFeatures: subscriptionModel?.features.toSet() ?? {},
|
||||||
|
userDataDto: userData?.toDto(
|
||||||
|
calculatedPackProgress: packProgress,
|
||||||
|
calculatedStudyDates: studyDates,
|
||||||
|
calculatedCategoryMinutes: categoryMinutes,
|
||||||
|
calculatedWordsStatistics: wordsStatistics,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue