656 lines
23 KiB
Dart
656 lines
23 KiB
Dart
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import '../models/user_stats.dart';
|
|
import '../models/class_stats.dart';
|
|
import '../models/achievement.dart';
|
|
import '../utils/logger.dart';
|
|
|
|
/// Serviço para gerenciar gamificação e conquistas
|
|
class GamificationService {
|
|
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
|
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
|
|
/// Atualizar streak diário do usuário
|
|
static Future<void> updateDailyStreak(String userId) async {
|
|
try {
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
final userStatsDoc = await userStatsRef.get();
|
|
|
|
if (!userStatsDoc.exists) {
|
|
// Criar estatísticas iniciais
|
|
await userStatsRef.set({
|
|
'userId': userId,
|
|
'currentStreak': 1,
|
|
'longestStreak': 1,
|
|
'totalStudyTime': 0,
|
|
'lastActivityDate': Timestamp.now(),
|
|
'weeklyStudyTime': 0,
|
|
'monthlyStudyTime': 0,
|
|
'masteredConcepts': [],
|
|
'unlockedAchievements': [],
|
|
});
|
|
return;
|
|
}
|
|
|
|
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
|
final now = DateTime.now();
|
|
final lastActivity = userStats.lastActivityDate;
|
|
|
|
int newStreak = userStats.currentStreak;
|
|
int newLongestStreak = userStats.longestStreak;
|
|
|
|
Logger.info('=== UPDATE DAILY STREAK DEBUG ===');
|
|
Logger.info('Last activity: $lastActivity');
|
|
Logger.info('Current streak before update: $newStreak');
|
|
|
|
if (lastActivity == null) {
|
|
// Primeira atividade - iniciar streak
|
|
Logger.info('First activity detected - setting streak to 1');
|
|
newStreak = 1;
|
|
newLongestStreak = 1;
|
|
} else {
|
|
// Normalizar para início do dia para comparação correta
|
|
final today = DateTime(now.year, now.month, now.day);
|
|
final lastDay = DateTime(lastActivity.year, lastActivity.month, lastActivity.day);
|
|
final difference = today.difference(lastDay).inDays;
|
|
|
|
if (difference == 0) {
|
|
// Já ativou hoje, não alterar streak mas atualiza timestamp
|
|
// Corrigir streak se estiver em 0 (erro de dados)
|
|
if (newStreak == 0) {
|
|
newStreak = 1;
|
|
newLongestStreak = 1;
|
|
Logger.info('Correcting invalid streak (0) to 1');
|
|
await userStatsRef.update({
|
|
'currentStreak': 1,
|
|
'longestStreak': 1,
|
|
'lastActivityDate': Timestamp.now(),
|
|
});
|
|
} else {
|
|
Logger.info('Already active today, streak unchanged: $newStreak');
|
|
await userStatsRef.update({
|
|
'lastActivityDate': Timestamp.now(),
|
|
});
|
|
}
|
|
return;
|
|
} else if (difference == 1) {
|
|
Logger.info('Consecutive activity detected, incrementing streak');
|
|
// Atividade consecutiva
|
|
newStreak++;
|
|
newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak;
|
|
} else {
|
|
// Quebrou o streak
|
|
newStreak = 1;
|
|
newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak;
|
|
}
|
|
}
|
|
|
|
Logger.info('Updating streak to: $newStreak');
|
|
await userStatsRef.update({
|
|
'currentStreak': newStreak,
|
|
'longestStreak': newLongestStreak,
|
|
'lastActivityDate': Timestamp.now(),
|
|
});
|
|
|
|
Logger.info('Streak updated successfully');
|
|
// Verificar conquistas de streak
|
|
await _checkStreakAchievements(userId, newStreak);
|
|
Logger.info('=== END UPDATE DAILY STREAK DEBUG ===');
|
|
} catch (e) {
|
|
Logger.error('Error updating daily streak: $e');
|
|
}
|
|
}
|
|
|
|
/// Registrar tempo de estudo
|
|
static Future<void> recordStudyTime(String userId, int minutes) async {
|
|
try {
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
final userStatsDoc = await userStatsRef.get();
|
|
|
|
if (!userStatsDoc.exists) {
|
|
await _createInitialUserStats(userId);
|
|
}
|
|
|
|
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
|
final newTotalTime = userStats.totalStudyTime + minutes;
|
|
|
|
await userStatsRef.update({
|
|
'totalStudyTime': newTotalTime,
|
|
'weeklyStudyTime': FieldValue.increment(minutes),
|
|
'monthlyStudyTime': FieldValue.increment(minutes),
|
|
});
|
|
|
|
// Verificar conquistas de tempo de estudo
|
|
await _checkStudyTimeAchievements(userId, newTotalTime);
|
|
} catch (e) {
|
|
Logger.error('Error recording study time: $e');
|
|
}
|
|
}
|
|
|
|
/// Registrar atividade de quiz
|
|
static Future<void> recordQuizActivity(String userId, {
|
|
required int score,
|
|
required int totalQuestions,
|
|
required String materialName,
|
|
}) async {
|
|
try {
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
|
|
// Atualizar streak
|
|
await updateDailyStreak(userId);
|
|
|
|
// Registrar tempo de estudo (estimado)
|
|
await recordStudyTime(userId, 15); // 15 minutos por quiz
|
|
|
|
// Verificar conquistas de quiz
|
|
await _checkQuizAchievements(userId, score, totalQuestions);
|
|
|
|
// Incrementar contador de quizzes completos
|
|
await userStatsRef.update({
|
|
'completedQuizzes': FieldValue.increment(1),
|
|
});
|
|
Logger.info('Incremented completed quizzes count');
|
|
|
|
// Atualizar conceitos dominados se score >= 50%
|
|
Logger.info('Checking if score qualifies for mastered concept: ${score / totalQuestions >= 0.5}');
|
|
if (score / totalQuestions >= 0.5) {
|
|
Logger.info('Adding mastered concept: $materialName with score: $score');
|
|
await _addMasteredConcept(userId, materialName, score);
|
|
} else {
|
|
Logger.info('Score too low for mastered concept: $score/$totalQuestions');
|
|
}
|
|
|
|
Logger.info('Quiz activity recorded for user $userId: $score/$totalQuestions');
|
|
} catch (e) {
|
|
Logger.error('Error recording quiz activity: $e');
|
|
}
|
|
}
|
|
|
|
/// Obter estatísticas do usuário
|
|
static Future<UserStats?> getUserStats(String userId) async {
|
|
try {
|
|
final doc = await _firestore.collection('users').doc(userId).get();
|
|
|
|
if (!doc.exists) {
|
|
// Criar estatísticas iniciais se não existirem
|
|
await _initializeUserStats(userId);
|
|
return await getUserStats(userId); // Chamada recursiva após inicialização
|
|
}
|
|
|
|
final data = doc.data() as Map<String, dynamic>;
|
|
|
|
// Garantir que completedQuizzes exista
|
|
if (!data.containsKey('completedQuizzes')) {
|
|
await _firestore.collection('users').doc(userId).update({
|
|
'completedQuizzes': 0,
|
|
});
|
|
data['completedQuizzes'] = 0;
|
|
}
|
|
|
|
return UserStats.fromFirestore(data, userId);
|
|
} catch (e) {
|
|
Logger.error('Error getting user stats: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Obter estatísticas da turma
|
|
static Future<ClassStats?> getClassStats(String classId) async {
|
|
try {
|
|
final classStatsDoc = await _firestore.collection('classStats').doc(classId).get();
|
|
if (!classStatsDoc.exists) {
|
|
return await _calculateClassStats(classId);
|
|
}
|
|
return ClassStats.fromFirestore(classStatsDoc.data()!, classId);
|
|
} catch (e) {
|
|
Logger.error('Error getting class stats: $e');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Obter ranking de alunos da turma
|
|
static Future<List<StudentRanking>> getClassRanking(String classId) async {
|
|
try {
|
|
// Primeiro, obter todos os alunos matriculados na turma
|
|
final enrollmentsSnapshot = await _firestore
|
|
.collection('enrollments')
|
|
.where('classId', isEqualTo: classId)
|
|
.get();
|
|
|
|
if (enrollmentsSnapshot.docs.isEmpty) {
|
|
Logger.info('No students enrolled in class $classId');
|
|
return [];
|
|
}
|
|
|
|
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc['studentId'] as String).toList();
|
|
final rankings = <StudentRanking>[];
|
|
|
|
// Para cada aluno, obter suas estatísticas
|
|
for (final studentId in studentIds) {
|
|
try {
|
|
final userStats = await getUserStats(studentId);
|
|
if (userStats != null) {
|
|
// Obter informações do usuário
|
|
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
|
final userData = userDoc.data() as Map<String, dynamic>?;
|
|
|
|
// Calcular estatísticas para o ranking
|
|
// Usar contador real de quizzes completos
|
|
final completedQuizzes = userStats.completedQuizzes;
|
|
final totalQuizzes = completedQuizzes + 5; // Estimativa de quizzes disponíveis
|
|
final quizCompletionRate = totalQuizzes > 0 ? completedQuizzes / totalQuizzes : 0.0;
|
|
|
|
// Calcular score geral baseado em múltiplos fatores
|
|
final overallScore = _calculateOverallScore(userStats, quizCompletionRate);
|
|
|
|
rankings.add(StudentRanking(
|
|
studentId: studentId,
|
|
studentName: userData?['displayName'] ?? 'Aluno $studentId',
|
|
studentEmail: userData?['email'] ?? '',
|
|
overallScore: overallScore,
|
|
completedQuizzes: completedQuizzes,
|
|
totalQuizzes: totalQuizzes,
|
|
quizCompletionRate: quizCompletionRate,
|
|
currentStreak: userStats.currentStreak,
|
|
studyTimeMinutes: userStats.totalStudyTime,
|
|
lastActivity: userStats.lastActivityDate ?? DateTime.now(),
|
|
));
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error getting stats for student $studentId: $e');
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Ordenar por score geral
|
|
rankings.sort((a, b) => b.overallScore.compareTo(a.overallScore));
|
|
|
|
return rankings;
|
|
} catch (e) {
|
|
Logger.error('Error getting class ranking: $e');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// Calcular score geral para ranking
|
|
static double _calculateOverallScore(UserStats userStats, double quizCompletionRate) {
|
|
double score = 0.0;
|
|
|
|
// Peso de 40% para completion rate de quizzes
|
|
score += quizCompletionRate * 40;
|
|
|
|
// Peso de 20% para streak (máximo 20 pontos)
|
|
score += (userStats.currentStreak / 30.0 * 20).clamp(0.0, 20.0);
|
|
|
|
// Peso de 20% para tempo de estudo (máximo 20 pontos para 10 horas)
|
|
score += (userStats.totalStudyTime / 600.0 * 20).clamp(0.0, 20.0);
|
|
|
|
// Peso de 20% para conceitos dominados (máximo 20 pontos para 10 conceitos)
|
|
score += (userStats.masteredConcepts.length / 10.0 * 20).clamp(0.0, 20.0);
|
|
|
|
return score;
|
|
}
|
|
|
|
/// Criar conquista personalizada (professor)
|
|
static Future<String> createCustomAchievement({
|
|
required String teacherId,
|
|
required String name,
|
|
required String description,
|
|
required String icon,
|
|
required String category,
|
|
required AchievementRequirement requirements,
|
|
required int points,
|
|
required String rarity,
|
|
}) async {
|
|
try {
|
|
final achievementRef = await _firestore.collection('achievements').add({
|
|
'name': name,
|
|
'description': description,
|
|
'icon': icon,
|
|
'category': category,
|
|
'requirements': requirements.toFirestore(),
|
|
'points': points,
|
|
'rarity': rarity,
|
|
'isActive': true,
|
|
'createdAt': Timestamp.now(),
|
|
'createdBy': teacherId,
|
|
});
|
|
|
|
Logger.info('Custom achievement created: ${achievementRef.id}');
|
|
return achievementRef.id;
|
|
} catch (e) {
|
|
Logger.error('Error creating custom achievement: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
/// Obter conquistas disponíveis
|
|
static Future<List<Achievement>> getAvailableAchievements({String? teacherId}) async {
|
|
try {
|
|
// Sempre incluir conquistas do sistema
|
|
List<Achievement> achievements = List.from(SystemAchievements.defaultAchievements);
|
|
|
|
// Adicionar conquistas personalizadas do professor
|
|
Query query = _firestore.collection('achievements').where('isActive', isEqualTo: true);
|
|
|
|
if (teacherId != null) {
|
|
query = query.where('createdBy', isEqualTo: teacherId);
|
|
}
|
|
|
|
final snapshot = await query.get();
|
|
achievements.addAll(
|
|
snapshot.docs
|
|
.map((doc) => Achievement.fromFirestore(doc.data() as Map<String, dynamic>, doc.id))
|
|
.toList(),
|
|
);
|
|
|
|
Logger.info('Total achievements loaded: ${achievements.length}');
|
|
return achievements;
|
|
} catch (e) {
|
|
Logger.error('Error getting available achievements: $e');
|
|
return SystemAchievements.defaultAchievements;
|
|
}
|
|
}
|
|
|
|
|
|
/// Métodos privados
|
|
|
|
static Future<void> _createInitialUserStats(String userId) async {
|
|
await _firestore.collection('users').doc(userId).set({
|
|
'userId': userId,
|
|
'currentStreak': 0,
|
|
'longestStreak': 0,
|
|
'totalStudyTime': 0,
|
|
'lastActivityDate': null, // null para que primeira atividade inicie streak
|
|
'weeklyStudyTime': 0,
|
|
'monthlyStudyTime': 0,
|
|
'masteredConcepts': [],
|
|
'unlockedAchievements': [],
|
|
});
|
|
}
|
|
|
|
static Future<void> _addMasteredConcept(String userId, String conceptName, int score) async {
|
|
try {
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
final userStatsDoc = await userStatsRef.get();
|
|
|
|
if (!userStatsDoc.exists) return;
|
|
|
|
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
|
|
|
// Verificar se conceito já está dominado
|
|
final existingConcept = userStats.masteredConcepts
|
|
.where((c) => c.conceptName == conceptName)
|
|
.firstOrNull;
|
|
|
|
if (existingConcept != null) {
|
|
// Atualizar nível de maestria se score maior
|
|
if (score > existingConcept.masteryLevel) {
|
|
final updatedConcepts = userStats.masteredConcepts.map((c) {
|
|
if (c.conceptName == conceptName) {
|
|
return MasteredConcept(
|
|
conceptName: conceptName,
|
|
masteredAt: DateTime.now(),
|
|
masteryLevel: score,
|
|
);
|
|
}
|
|
return c;
|
|
}).toList();
|
|
|
|
await userStatsRef.update({
|
|
'masteredConcepts': updatedConcepts.map((c) => c.toFirestore()).toList(),
|
|
});
|
|
}
|
|
} else {
|
|
// Adicionar novo conceito
|
|
final newConcept = MasteredConcept(
|
|
conceptName: conceptName,
|
|
masteredAt: DateTime.now(),
|
|
masteryLevel: score,
|
|
);
|
|
|
|
await userStatsRef.update({
|
|
'masteredConcepts': FieldValue.arrayUnion([newConcept.toFirestore()]),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error adding mastered concept: $e');
|
|
}
|
|
}
|
|
|
|
static Future<ClassStats> _calculateClassStats(String classId) async {
|
|
try {
|
|
// Obter informações da turma
|
|
final classDoc = await _firestore.collection('classes').doc(classId).get();
|
|
if (!classDoc.exists) {
|
|
throw Exception('Class not found');
|
|
}
|
|
|
|
final className = classDoc.data()?['name'] ?? 'Unknown Class';
|
|
final teacherId = classDoc.data()?['teacherId'] ?? '';
|
|
|
|
// Obter alunos matriculados
|
|
final enrollmentsSnapshot = await _firestore
|
|
.collection('enrollments')
|
|
.where('classId', isEqualTo: classId)
|
|
.get();
|
|
|
|
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc.data()['studentId'] as String).toList();
|
|
|
|
// Calcular estatísticas
|
|
int activeStudents = 0;
|
|
double totalProgress = 0;
|
|
List<StudentNeedingSupport> needingSupport = [];
|
|
|
|
for (final studentId in studentIds) {
|
|
final userStats = await getUserStats(studentId);
|
|
if (userStats != null) {
|
|
// Verificar se está ativo (atividade nos últimos 7 dias)
|
|
int daysSinceLastActivity = 999; // Valor alto para inatividade
|
|
if (userStats.lastActivityDate != null) {
|
|
daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays;
|
|
if (daysSinceLastActivity <= 7) {
|
|
activeStudents++;
|
|
}
|
|
}
|
|
|
|
// Calcular progresso baseado em conceitos dominados
|
|
final progress = userStats.masteredConcepts.isEmpty
|
|
? 0.0
|
|
: userStats.masteredConcepts.map((c) => c.masteryLevel).reduce((a, b) => a + b) / userStats.masteredConcepts.length / 100;
|
|
totalProgress += progress;
|
|
|
|
// Verificar se precisa de apoio
|
|
if (progress < 0.5 || daysSinceLastActivity > 14) {
|
|
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
|
final userName = userDoc.data()?['displayName'] ?? 'Unknown';
|
|
|
|
needingSupport.add(StudentNeedingSupport(
|
|
studentId: studentId,
|
|
studentName: userName,
|
|
reason: progress < 0.5 ? 'low_scores' : 'inactivity',
|
|
lastActivity: userStats.lastActivityDate ?? DateTime.now(),
|
|
averageScore: progress * 100,
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
final averageProgress = studentIds.isEmpty ? 0.0 : totalProgress / studentIds.length;
|
|
|
|
// Obter estatísticas de quizzes
|
|
final quizzesSnapshot = await _firestore
|
|
.collection('teacherQuizzes')
|
|
.where('classIds', arrayContains: classId)
|
|
.get();
|
|
|
|
final classStats = ClassStats(
|
|
classId: classId,
|
|
teacherId: teacherId,
|
|
className: className,
|
|
totalStudents: studentIds.length,
|
|
activeStudents: activeStudents,
|
|
averageProgress: averageProgress,
|
|
totalQuizzes: quizzesSnapshot.docs.length,
|
|
activeQuizzes: quizzesSnapshot.docs.length, // Simplificado
|
|
totalContent: 0, // Implementar depois
|
|
weeklyStats: [],
|
|
studentsNeedingSupport: needingSupport,
|
|
);
|
|
|
|
// Salvar estatísticas calculadas
|
|
await _firestore.collection('classStats').doc(classId).set(classStats.toFirestore());
|
|
|
|
return classStats;
|
|
} catch (e) {
|
|
Logger.error('Error calculating class stats: $e');
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
static Future<void> _checkStreakAchievements(String userId, int streakDays) async {
|
|
final achievements = await getAvailableAchievements();
|
|
final streakAchievements = achievements.where((a) => a.category == 'streak');
|
|
|
|
for (final achievement in streakAchievements) {
|
|
if (achievement.requirements.checkCondition(streakDays)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<void> _checkStudyTimeAchievements(String userId, int totalMinutes) async {
|
|
final achievements = await getAvailableAchievements();
|
|
final studyAchievements = achievements.where((a) => a.category == 'study_time');
|
|
|
|
for (final achievement in studyAchievements) {
|
|
if (achievement.requirements.checkCondition(totalMinutes)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
static Future<void> _checkQuizAchievements(String userId, int score, int totalQuestions) async {
|
|
Logger.info('=== CHECKING QUIZ ACHIEVEMENTS ===');
|
|
Logger.info('Score: $score/$totalQuestions');
|
|
|
|
final achievements = await getAvailableAchievements();
|
|
final userStats = await getUserStats(userId);
|
|
|
|
if (userStats == null) {
|
|
Logger.error('User stats null for achievement checking');
|
|
return;
|
|
}
|
|
|
|
final percentage = (score / totalQuestions) * 100;
|
|
// Usar contador real de quizzes completos
|
|
final completedQuizzes = userStats.completedQuizzes;
|
|
|
|
Logger.info('Percentage: $percentage%');
|
|
Logger.info('Completed quizzes: $completedQuizzes');
|
|
Logger.info('Mastered concepts: ${userStats.masteredConcepts.length}');
|
|
Logger.info('Available achievements: ${achievements.length}');
|
|
|
|
for (final achievement in achievements) {
|
|
if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_score' &&
|
|
achievement.requirements.checkCondition(percentage)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
} else if (achievement.category == 'quiz_count' && achievement.requirements.type == 'quiz_completion' &&
|
|
achievement.requirements.checkCondition(completedQuizzes)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
} else if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_completion' &&
|
|
achievement.id == 'first_quiz' &&
|
|
achievement.requirements.checkCondition(1)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
} else if (achievement.category == 'concept' && achievement.requirements.type == 'concepts_mastered' &&
|
|
achievement.requirements.checkCondition(userStats.masteredConcepts.length)) {
|
|
await _unlockAchievement(userId, achievement.id);
|
|
} else {
|
|
Logger.info('Achievement not matched: ${achievement.id} - category: ${achievement.category}, type: ${achievement.requirements.type}');
|
|
}
|
|
}
|
|
|
|
Logger.info('=== END CHECKING QUIZ ACHIEVEMENTS ===');
|
|
}
|
|
|
|
|
|
static Future<void> _unlockAchievement(String userId, String achievementId) async {
|
|
try {
|
|
Logger.info('=== ATTEMPTING TO UNLOCK ACHIEVEMENT ===');
|
|
Logger.info('Achievement ID: $achievementId');
|
|
Logger.info('User ID: $userId');
|
|
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
final userStatsDoc = await userStatsRef.get();
|
|
|
|
if (!userStatsDoc.exists) {
|
|
Logger.error('User stats document does not exist for user $userId');
|
|
return;
|
|
}
|
|
|
|
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
|
Logger.info('Current unlocked achievements count: ${userStats.unlockedAchievements.length}');
|
|
|
|
// Verificar se já desbloqueou
|
|
final alreadyUnlocked = userStats.unlockedAchievements
|
|
.any((a) => a.achievementId == achievementId);
|
|
|
|
Logger.info('Already unlocked: $alreadyUnlocked');
|
|
|
|
if (!alreadyUnlocked) {
|
|
final unlockedAchievement = UnlockedAchievement(
|
|
achievementId: achievementId,
|
|
unlockedAt: DateTime.now(),
|
|
metadata: {},
|
|
);
|
|
|
|
Logger.info('Adding achievement to Firestore...');
|
|
await userStatsRef.update({
|
|
'unlockedAchievements': FieldValue.arrayUnion([unlockedAchievement.toFirestore()]),
|
|
});
|
|
|
|
Logger.info('Achievement unlocked successfully: $achievementId for user $userId');
|
|
} else {
|
|
Logger.info('Achievement $achievementId already unlocked for user $userId');
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error unlocking achievement: $e');
|
|
}
|
|
}
|
|
|
|
/// Inicializar estatísticas do usuário
|
|
static Future<void> _initializeUserStats(String userId) async {
|
|
try {
|
|
final userStatsRef = _firestore.collection('users').doc(userId);
|
|
|
|
// Verificar se já existe
|
|
final doc = await userStatsRef.get();
|
|
if (doc.exists) {
|
|
// Apenas atualizar com completedQuizzes se não existir
|
|
final data = doc.data() as Map<String, dynamic>;
|
|
if (!data.containsKey('completedQuizzes')) {
|
|
await userStatsRef.update({
|
|
'completedQuizzes': 0,
|
|
});
|
|
}
|
|
} else {
|
|
// Criar documento inicial
|
|
await userStatsRef.set({
|
|
'completedQuizzes': 0,
|
|
'currentStreak': 0,
|
|
'longestStreak': 0,
|
|
'totalStudyTime': 0,
|
|
'weeklyStudyTime': 0,
|
|
'monthlyStudyTime': 0,
|
|
'masteredConcepts': [],
|
|
'unlockedAchievements': [],
|
|
});
|
|
}
|
|
|
|
Logger.info('User stats initialized for user $userId');
|
|
} catch (e) {
|
|
Logger.error('Error initializing user stats: $e');
|
|
}
|
|
}
|
|
}
|