Finalização de detalhes e pequenas adições em dashboards de alunos e professores
This commit is contained in:
@@ -15,7 +15,7 @@ class GamificationService {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
final userStatsDoc = await userStatsRef.get();
|
||||
|
||||
|
||||
if (!userStatsDoc.exists) {
|
||||
// Criar estatísticas iniciais
|
||||
await userStatsRef.set({
|
||||
@@ -35,14 +35,14 @@ class GamificationService {
|
||||
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');
|
||||
@@ -51,7 +51,11 @@ class GamificationService {
|
||||
} 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 lastDay = DateTime(
|
||||
lastActivity.year,
|
||||
lastActivity.month,
|
||||
lastActivity.day,
|
||||
);
|
||||
final difference = today.difference(lastDay).inDays;
|
||||
|
||||
if (difference == 0) {
|
||||
@@ -68,20 +72,22 @@ class GamificationService {
|
||||
});
|
||||
} else {
|
||||
Logger.info('Already active today, streak unchanged: $newStreak');
|
||||
await userStatsRef.update({
|
||||
'lastActivityDate': Timestamp.now(),
|
||||
});
|
||||
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;
|
||||
newLongestStreak = newStreak > newLongestStreak
|
||||
? newStreak
|
||||
: newLongestStreak;
|
||||
} else {
|
||||
// Quebrou o streak
|
||||
newStreak = 1;
|
||||
newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak;
|
||||
newLongestStreak = newStreak > newLongestStreak
|
||||
? newStreak
|
||||
: newLongestStreak;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,39 +134,46 @@ class GamificationService {
|
||||
}
|
||||
|
||||
/// Registrar atividade de quiz
|
||||
static Future<void> recordQuizActivity(String userId, {
|
||||
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
|
||||
// Tempo de estudo agora é calculado em tempo real no quiz sheet
|
||||
// Não adicionamos tempo fixo aqui
|
||||
|
||||
// Verificar conquistas de quiz
|
||||
await _checkQuizAchievements(userId, score, totalQuestions);
|
||||
|
||||
// Incrementar contador de quizzes completos
|
||||
await userStatsRef.update({
|
||||
'completedQuizzes': FieldValue.increment(1),
|
||||
});
|
||||
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}');
|
||||
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');
|
||||
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(
|
||||
'Score too low for mastered concept: $score/$totalQuestions',
|
||||
);
|
||||
}
|
||||
|
||||
Logger.info('Quiz activity recorded for user $userId: $score/$totalQuestions');
|
||||
Logger.info(
|
||||
'Quiz activity recorded for user $userId: $score/$totalQuestions',
|
||||
);
|
||||
} catch (e) {
|
||||
Logger.error('Error recording quiz activity: $e');
|
||||
}
|
||||
@@ -170,15 +183,17 @@ class GamificationService {
|
||||
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
|
||||
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({
|
||||
@@ -186,7 +201,7 @@ class GamificationService {
|
||||
});
|
||||
data['completedQuizzes'] = 0;
|
||||
}
|
||||
|
||||
|
||||
return UserStats.fromFirestore(data, userId);
|
||||
} catch (e) {
|
||||
Logger.error('Error getting user stats: $e');
|
||||
@@ -195,26 +210,32 @@ class GamificationService {
|
||||
}
|
||||
|
||||
/// Obter estatísticas da turma
|
||||
static Future<ClassStats?> getClassStats(String classId, {bool forceRefresh = false}) async {
|
||||
static Future<ClassStats?> getClassStats(
|
||||
String classId, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
if (forceRefresh) {
|
||||
// Forçar recálculo completo
|
||||
return await _calculateClassStats(classId);
|
||||
}
|
||||
|
||||
final classStatsDoc = await _firestore.collection('classStats').doc(classId).get();
|
||||
|
||||
final classStatsDoc = await _firestore
|
||||
.collection('classStats')
|
||||
.doc(classId)
|
||||
.get();
|
||||
if (!classStatsDoc.exists) {
|
||||
return await _calculateClassStats(classId);
|
||||
}
|
||||
|
||||
|
||||
// Verificar se os dados estão desatualizados (mais de 1 hora)
|
||||
final data = classStatsDoc.data()!;
|
||||
final lastUpdated = data['lastUpdated'] as Timestamp?;
|
||||
if (lastUpdated == null ||
|
||||
if (lastUpdated == null ||
|
||||
DateTime.now().difference(lastUpdated.toDate()).inHours > 1) {
|
||||
return await _calculateClassStats(classId);
|
||||
}
|
||||
|
||||
|
||||
return ClassStats.fromFirestore(data, classId);
|
||||
} catch (e) {
|
||||
Logger.error('Error getting class stats: $e');
|
||||
@@ -229,11 +250,11 @@ class GamificationService {
|
||||
.collection('classes')
|
||||
.where('teacherId', isEqualTo: teacherId)
|
||||
.get();
|
||||
|
||||
|
||||
for (final classDoc in classesSnapshot.docs) {
|
||||
await _calculateClassStats(classDoc.id);
|
||||
}
|
||||
|
||||
|
||||
Logger.info('Refreshed stats for ${classesSnapshot.docs.length} classes');
|
||||
} catch (e) {
|
||||
Logger.error('Error refreshing class stats: $e');
|
||||
@@ -254,7 +275,9 @@ class GamificationService {
|
||||
return [];
|
||||
}
|
||||
|
||||
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc['studentId'] as String).toList();
|
||||
final studentIds = enrollmentsSnapshot.docs
|
||||
.map((doc) => doc['studentId'] as String)
|
||||
.toList();
|
||||
final rankings = <StudentRanking>[];
|
||||
|
||||
// Obter número real de quizzes disponíveis na turma
|
||||
@@ -270,50 +293,71 @@ class GamificationService {
|
||||
final userStats = await getUserStats(studentId);
|
||||
if (userStats != null) {
|
||||
// Obter informações do usuário
|
||||
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
||||
final userDoc = await _firestore
|
||||
.collection('users')
|
||||
.doc(studentId)
|
||||
.get();
|
||||
final userData = userDoc.data() as Map<String, dynamic>?;
|
||||
|
||||
// Calcular estatísticas para o ranking
|
||||
final completedQuizzes = userStats.completedQuizzes;
|
||||
final totalQuizzes = totalAvailableQuizzes > 0 ? totalAvailableQuizzes : 1;
|
||||
final totalQuizzes = totalAvailableQuizzes > 0
|
||||
? totalAvailableQuizzes
|
||||
: 1;
|
||||
final quizCompletionRate = completedQuizzes / totalQuizzes;
|
||||
|
||||
|
||||
Logger.info('=== RANKING SCORE DEBUG ===');
|
||||
Logger.info('Student ID: $studentId');
|
||||
Logger.info('Completed quizzes: $completedQuizzes');
|
||||
Logger.info('Total quizzes: $totalQuizzes');
|
||||
Logger.info('Quiz completion rate: $quizCompletionRate (${(quizCompletionRate * 100).toInt()}%)');
|
||||
Logger.info(
|
||||
'Quiz completion rate: $quizCompletionRate (${(quizCompletionRate * 100).toInt()}%)',
|
||||
);
|
||||
Logger.info('Current streak: ${userStats.currentStreak}');
|
||||
Logger.info('Total study time: ${userStats.totalStudyTime} minutes');
|
||||
Logger.info('Mastered concepts: ${userStats.masteredConcepts.length}');
|
||||
Logger.info('Unlocked achievements: ${userStats.unlockedAchievements.length}');
|
||||
|
||||
Logger.info(
|
||||
'Total study time: ${userStats.totalStudyTime} minutes',
|
||||
);
|
||||
Logger.info(
|
||||
'Mastered concepts: ${userStats.masteredConcepts.length}',
|
||||
);
|
||||
Logger.info(
|
||||
'Unlocked achievements: ${userStats.unlockedAchievements.length}',
|
||||
);
|
||||
|
||||
// Calcular score geral baseado em múltiplos fatores
|
||||
final overallScore = _calculateOverallScore(userStats, quizCompletionRate);
|
||||
|
||||
Logger.info('Overall score calculated: $overallScore (${overallScore.toInt()}%)');
|
||||
final overallScore = _calculateOverallScore(
|
||||
userStats,
|
||||
quizCompletionRate,
|
||||
);
|
||||
|
||||
Logger.info(
|
||||
'Overall score calculated: $overallScore (${overallScore.toInt()}%)',
|
||||
);
|
||||
Logger.info('=== END RANKING SCORE DEBUG ===');
|
||||
|
||||
// Tentar obter um nome melhor para o aluno
|
||||
String studentName = 'Aluno $studentId';
|
||||
if (userData != null) {
|
||||
studentName = userData['displayName'] ??
|
||||
userData['email']?.split('@')[0] ??
|
||||
'Aluno ${studentId.substring(0, 8)}...';
|
||||
studentName =
|
||||
userData['displayName'] ??
|
||||
userData['email']?.split('@')[0] ??
|
||||
'Aluno ${studentId.substring(0, 8)}...';
|
||||
}
|
||||
|
||||
rankings.add(StudentRanking(
|
||||
studentId: studentId,
|
||||
studentName: studentName,
|
||||
studentEmail: userData?['email'] ?? '',
|
||||
overallScore: overallScore,
|
||||
completedQuizzes: completedQuizzes,
|
||||
totalQuizzes: totalQuizzes,
|
||||
quizCompletionRate: quizCompletionRate,
|
||||
currentStreak: userStats.currentStreak,
|
||||
studyTimeMinutes: userStats.totalStudyTime,
|
||||
lastActivity: userStats.lastActivityDate ?? DateTime.now(),
|
||||
));
|
||||
rankings.add(
|
||||
StudentRanking(
|
||||
studentId: studentId,
|
||||
studentName: studentName,
|
||||
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');
|
||||
@@ -323,7 +367,7 @@ class GamificationService {
|
||||
|
||||
// Ordenar por score geral
|
||||
rankings.sort((a, b) => b.overallScore.compareTo(a.overallScore));
|
||||
|
||||
|
||||
return rankings;
|
||||
} catch (e) {
|
||||
Logger.error('Error getting class ranking: $e');
|
||||
@@ -332,27 +376,33 @@ class GamificationService {
|
||||
}
|
||||
|
||||
/// Calcular score geral para ranking
|
||||
static double _calculateOverallScore(UserStats userStats, double quizCompletionRate) {
|
||||
static double _calculateOverallScore(
|
||||
UserStats userStats,
|
||||
double quizCompletionRate,
|
||||
) {
|
||||
// Se completou 100% dos quizzes, score é 100%
|
||||
if (quizCompletionRate >= 1.0) {
|
||||
return 100.0;
|
||||
}
|
||||
|
||||
|
||||
// Para completion < 100%, calcular proporcionalmente
|
||||
double baseScore = quizCompletionRate * 90; // 90% baseado em completion
|
||||
|
||||
|
||||
// Bônus adicionais (máximo 10% extra)
|
||||
double bonusScore = 0.0;
|
||||
|
||||
|
||||
// 5% para conceitos dominados
|
||||
bonusScore += (userStats.masteredConcepts.length / 5.0 * 5).clamp(0.0, 5.0);
|
||||
|
||||
|
||||
// 3% para streak
|
||||
bonusScore += (userStats.currentStreak / 7.0 * 3).clamp(0.0, 3.0);
|
||||
|
||||
|
||||
// 2% para conquistas
|
||||
bonusScore += (userStats.unlockedAchievements.length / 10.0 * 2).clamp(0.0, 2.0);
|
||||
|
||||
bonusScore += (userStats.unlockedAchievements.length / 10.0 * 2).clamp(
|
||||
0.0,
|
||||
2.0,
|
||||
);
|
||||
|
||||
final totalScore = baseScore + bonusScore;
|
||||
return totalScore.clamp(0.0, 100.0);
|
||||
}
|
||||
@@ -391,14 +441,20 @@ class GamificationService {
|
||||
}
|
||||
|
||||
/// Obter conquistas disponíveis
|
||||
static Future<List<Achievement>> getAvailableAchievements({String? teacherId}) async {
|
||||
static Future<List<Achievement>> getAvailableAchievements({
|
||||
String? teacherId,
|
||||
}) async {
|
||||
try {
|
||||
// Sempre incluir conquistas do sistema
|
||||
List<Achievement> achievements = List.from(SystemAchievements.defaultAchievements);
|
||||
|
||||
List<Achievement> achievements = List.from(
|
||||
SystemAchievements.defaultAchievements,
|
||||
);
|
||||
|
||||
// Adicionar conquistas personalizadas do professor
|
||||
Query query = _firestore.collection('achievements').where('isActive', isEqualTo: true);
|
||||
|
||||
Query query = _firestore
|
||||
.collection('achievements')
|
||||
.where('isActive', isEqualTo: true);
|
||||
|
||||
if (teacherId != null) {
|
||||
query = query.where('createdBy', isEqualTo: teacherId);
|
||||
}
|
||||
@@ -406,10 +462,15 @@ class GamificationService {
|
||||
final snapshot = await query.get();
|
||||
achievements.addAll(
|
||||
snapshot.docs
|
||||
.map((doc) => Achievement.fromFirestore(doc.data() as Map<String, dynamic>, doc.id))
|
||||
.map(
|
||||
(doc) => Achievement.fromFirestore(
|
||||
doc.data() as Map<String, dynamic>,
|
||||
doc.id,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
|
||||
Logger.info('Total achievements loaded: ${achievements.length}');
|
||||
return achievements;
|
||||
} catch (e) {
|
||||
@@ -418,7 +479,6 @@ class GamificationService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Métodos privados
|
||||
|
||||
static Future<void> _createInitialUserStats(String userId) async {
|
||||
@@ -427,7 +487,8 @@ class GamificationService {
|
||||
'currentStreak': 0,
|
||||
'longestStreak': 0,
|
||||
'totalStudyTime': 0,
|
||||
'lastActivityDate': null, // null para que primeira atividade inicie streak
|
||||
'lastActivityDate':
|
||||
null, // null para que primeira atividade inicie streak
|
||||
'weeklyStudyTime': 0,
|
||||
'monthlyStudyTime': 0,
|
||||
'masteredConcepts': [],
|
||||
@@ -435,15 +496,19 @@ class GamificationService {
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _addMasteredConcept(String userId, String conceptName, int score) async {
|
||||
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)
|
||||
@@ -464,7 +529,9 @@ class GamificationService {
|
||||
}).toList();
|
||||
|
||||
await userStatsRef.update({
|
||||
'masteredConcepts': updatedConcepts.map((c) => c.toFirestore()).toList(),
|
||||
'masteredConcepts': updatedConcepts
|
||||
.map((c) => c.toFirestore())
|
||||
.toList(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -487,7 +554,10 @@ class GamificationService {
|
||||
static Future<ClassStats> _calculateClassStats(String classId) async {
|
||||
try {
|
||||
// Obter informações da turma
|
||||
final classDoc = await _firestore.collection('classes').doc(classId).get();
|
||||
final classDoc = await _firestore
|
||||
.collection('classes')
|
||||
.doc(classId)
|
||||
.get();
|
||||
if (!classDoc.exists) {
|
||||
throw Exception('Class not found');
|
||||
}
|
||||
@@ -501,7 +571,9 @@ class GamificationService {
|
||||
.where('classId', isEqualTo: classId)
|
||||
.get();
|
||||
|
||||
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc.data()['studentId'] as String).toList();
|
||||
final studentIds = enrollmentsSnapshot.docs
|
||||
.map((doc) => doc.data()['studentId'] as String)
|
||||
.toList();
|
||||
|
||||
// Calcular estatísticas
|
||||
int activeStudents = 0;
|
||||
@@ -510,13 +582,15 @@ class GamificationService {
|
||||
|
||||
for (final studentId in studentIds) {
|
||||
final userStats = await getUserStats(studentId);
|
||||
|
||||
|
||||
// Verificar se está ativo (atividade nos últimos 30 dias - mais realista)
|
||||
int daysSinceLastActivity = 999; // Valor alto para inatividade
|
||||
bool hasStats = userStats != null;
|
||||
|
||||
|
||||
if (hasStats && userStats!.lastActivityDate != null) {
|
||||
daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays;
|
||||
daysSinceLastActivity = DateTime.now()
|
||||
.difference(userStats.lastActivityDate!)
|
||||
.inDays;
|
||||
if (daysSinceLastActivity <= 30) {
|
||||
activeStudents++;
|
||||
}
|
||||
@@ -527,59 +601,78 @@ class GamificationService {
|
||||
if (hasStats) {
|
||||
final completedQuizzes = userStats!.completedQuizzes;
|
||||
final masteredConcepts = userStats.masteredConcepts.length;
|
||||
|
||||
|
||||
Logger.info('=== PROGRESS CALCULATION DEBUG ===');
|
||||
Logger.info('Student ID: $studentId');
|
||||
Logger.info('Completed quizzes: $completedQuizzes');
|
||||
Logger.info('Mastered concepts: $masteredConcepts');
|
||||
|
||||
|
||||
// Progresso mais representativo: 60% quizzes + 40% conceitos
|
||||
// Primeiro quiz já dá 30% de progresso (incentivo inicial)
|
||||
final quizProgress = completedQuizzes > 0 ?
|
||||
(0.3 + (completedQuizzes - 1) * 0.15).clamp(0.0, 0.6) : 0.0;
|
||||
final quizProgress = completedQuizzes > 0
|
||||
? (0.3 + (completedQuizzes - 1) * 0.15).clamp(0.0, 0.6)
|
||||
: 0.0;
|
||||
// Primeiro conceito já dá 15% de progresso
|
||||
final conceptProgress = (masteredConcepts * 0.15).clamp(0.0, 0.4);
|
||||
progress = quizProgress + conceptProgress;
|
||||
|
||||
Logger.info('Quiz progress: $quizProgress (${(quizProgress * 100).toInt()}%)');
|
||||
Logger.info('Concept progress: $conceptProgress (${(conceptProgress * 100).toInt()}%)');
|
||||
Logger.info('Total progress: $progress (${(progress * 100).toInt()}%)');
|
||||
|
||||
Logger.info(
|
||||
'Quiz progress: $quizProgress (${(quizProgress * 100).toInt()}%)',
|
||||
);
|
||||
Logger.info(
|
||||
'Concept progress: $conceptProgress (${(conceptProgress * 100).toInt()}%)',
|
||||
);
|
||||
Logger.info(
|
||||
'Total progress: $progress (${(progress * 100).toInt()}%)',
|
||||
);
|
||||
Logger.info('=== END PROGRESS CALCULATION DEBUG ===');
|
||||
} else {
|
||||
Logger.info('Student $studentId has no stats - progress = 0.0');
|
||||
}
|
||||
|
||||
|
||||
totalProgress += progress;
|
||||
|
||||
// Verificar se precisa de apoio (ajustado para nova fórmula)
|
||||
if (progress < 0.25 || daysSinceLastActivity > 30) {
|
||||
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
||||
final userDoc = await _firestore
|
||||
.collection('users')
|
||||
.doc(studentId)
|
||||
.get();
|
||||
final userData = userDoc.data();
|
||||
|
||||
|
||||
// Tentar obter um nome melhor para o aluno
|
||||
String studentName = 'Aluno ${studentId.substring(0, 8)}...';
|
||||
if (userData != null) {
|
||||
studentName = userData['displayName'] ??
|
||||
userData['email']?.split('@')[0] ??
|
||||
'Aluno ${studentId.substring(0, 8)}...';
|
||||
studentName =
|
||||
userData['displayName'] ??
|
||||
userData['email']?.split('@')[0] ??
|
||||
'Aluno ${studentId.substring(0, 8)}...';
|
||||
}
|
||||
|
||||
needingSupport.add(StudentNeedingSupport(
|
||||
studentId: studentId,
|
||||
studentName: studentName,
|
||||
reason: progress < 0.3 ? 'low_scores' : 'inactivity',
|
||||
lastActivity: hasStats ? userStats!.lastActivityDate ?? DateTime.now() : DateTime.now().subtract(const Duration(days: 45)),
|
||||
averageScore: progress * 100,
|
||||
));
|
||||
|
||||
needingSupport.add(
|
||||
StudentNeedingSupport(
|
||||
studentId: studentId,
|
||||
studentName: studentName,
|
||||
reason: progress < 0.3 ? 'low_scores' : 'inactivity',
|
||||
lastActivity: hasStats
|
||||
? userStats!.lastActivityDate ?? DateTime.now()
|
||||
: DateTime.now().subtract(const Duration(days: 45)),
|
||||
averageScore: progress * 100,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final averageProgress = studentIds.isEmpty ? 0.0 : totalProgress / studentIds.length;
|
||||
|
||||
final averageProgress = studentIds.isEmpty
|
||||
? 0.0
|
||||
: totalProgress / studentIds.length;
|
||||
|
||||
Logger.info('=== AVERAGE PROGRESS DEBUG ===');
|
||||
Logger.info('Total students: ${studentIds.length}');
|
||||
Logger.info('Total progress sum: $totalProgress');
|
||||
Logger.info('Average progress: $averageProgress (${(averageProgress * 100).toInt()}%)');
|
||||
Logger.info(
|
||||
'Average progress: $averageProgress (${(averageProgress * 100).toInt()}%)',
|
||||
);
|
||||
Logger.info('=== END AVERAGE PROGRESS DEBUG ===');
|
||||
|
||||
// Obter estatísticas de quizzes e conteúdos
|
||||
@@ -618,7 +711,10 @@ class GamificationService {
|
||||
|
||||
// Limpar cache primeiro e depois salvar estatísticas calculadas
|
||||
await _firestore.collection('classStats').doc(classId).delete();
|
||||
await _firestore.collection('classStats').doc(classId).set(classStats.toFirestore());
|
||||
await _firestore
|
||||
.collection('classStats')
|
||||
.doc(classId)
|
||||
.set(classStats.toFirestore());
|
||||
Logger.info('Class stats refreshed and saved for class $classId');
|
||||
|
||||
return classStats;
|
||||
@@ -628,9 +724,14 @@ class GamificationService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkStreakAchievements(String userId, int streakDays) async {
|
||||
static Future<void> _checkStreakAchievements(
|
||||
String userId,
|
||||
int streakDays,
|
||||
) async {
|
||||
final achievements = await getAvailableAchievements();
|
||||
final streakAchievements = achievements.where((a) => a.category == 'streak');
|
||||
final streakAchievements = achievements.where(
|
||||
(a) => a.category == 'streak',
|
||||
);
|
||||
|
||||
for (final achievement in streakAchievements) {
|
||||
if (achievement.requirements.checkCondition(streakDays)) {
|
||||
@@ -639,9 +740,14 @@ class GamificationService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkStudyTimeAchievements(String userId, int totalMinutes) async {
|
||||
static Future<void> _checkStudyTimeAchievements(
|
||||
String userId,
|
||||
int totalMinutes,
|
||||
) async {
|
||||
final achievements = await getAvailableAchievements();
|
||||
final studyAchievements = achievements.where((a) => a.category == 'study_time');
|
||||
final studyAchievements = achievements.where(
|
||||
(a) => a.category == 'study_time',
|
||||
);
|
||||
|
||||
for (final achievement in studyAchievements) {
|
||||
if (achievement.requirements.checkCondition(totalMinutes)) {
|
||||
@@ -650,13 +756,17 @@ class GamificationService {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkQuizAchievements(String userId, int score, int totalQuestions) async {
|
||||
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;
|
||||
@@ -665,41 +775,51 @@ class GamificationService {
|
||||
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' &&
|
||||
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)) {
|
||||
} 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)) {
|
||||
} 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)) {
|
||||
} 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(
|
||||
'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 {
|
||||
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();
|
||||
|
||||
@@ -709,11 +829,14 @@ class GamificationService {
|
||||
}
|
||||
|
||||
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
||||
Logger.info('Current unlocked achievements count: ${userStats.unlockedAchievements.length}');
|
||||
|
||||
Logger.info(
|
||||
'Current unlocked achievements count: ${userStats.unlockedAchievements.length}',
|
||||
);
|
||||
|
||||
// Verificar se já desbloqueou
|
||||
final alreadyUnlocked = userStats.unlockedAchievements
|
||||
.any((a) => a.achievementId == achievementId);
|
||||
final alreadyUnlocked = userStats.unlockedAchievements.any(
|
||||
(a) => a.achievementId == achievementId,
|
||||
);
|
||||
|
||||
Logger.info('Already unlocked: $alreadyUnlocked');
|
||||
|
||||
@@ -726,12 +849,18 @@ class GamificationService {
|
||||
|
||||
Logger.info('Adding achievement to Firestore...');
|
||||
await userStatsRef.update({
|
||||
'unlockedAchievements': FieldValue.arrayUnion([unlockedAchievement.toFirestore()]),
|
||||
'unlockedAchievements': FieldValue.arrayUnion([
|
||||
unlockedAchievement.toFirestore(),
|
||||
]),
|
||||
});
|
||||
|
||||
Logger.info('Achievement unlocked successfully: $achievementId for user $userId');
|
||||
Logger.info(
|
||||
'Achievement unlocked successfully: $achievementId for user $userId',
|
||||
);
|
||||
} else {
|
||||
Logger.info('Achievement $achievementId already unlocked for user $userId');
|
||||
Logger.info(
|
||||
'Achievement $achievementId already unlocked for user $userId',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error unlocking achievement: $e');
|
||||
@@ -742,16 +871,14 @@ class GamificationService {
|
||||
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,
|
||||
});
|
||||
await userStatsRef.update({'completedQuizzes': 0});
|
||||
}
|
||||
} else {
|
||||
// Criar documento inicial
|
||||
@@ -766,7 +893,7 @@ class GamificationService {
|
||||
'unlockedAchievements': [],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Logger.info('User stats initialized for user $userId');
|
||||
} catch (e) {
|
||||
Logger.error('Error initializing user stats: $e');
|
||||
|
||||
Reference in New Issue
Block a user