From 7a26223a01dfa18b22ca3b113f20687d94db6bbf Mon Sep 17 00:00:00 2001 From: 240403 <240403@epvc.pt> Date: Sun, 17 May 2026 18:27:22 +0100 Subject: [PATCH] VERSAO FINAL (core features) --- lib/core/models/class_stats.dart | 4 + lib/core/services/gamification_service.dart | 210 ++++++++++++++---- .../presentation/pages/analytics_page.dart | 3 +- .../teacher_analytics_preview_widget.dart | 3 +- .../widgets/teacher_hero_widget.dart | 39 +++- 5 files changed, 203 insertions(+), 56 deletions(-) diff --git a/lib/core/models/class_stats.dart b/lib/core/models/class_stats.dart index 1af2d2b..90027f7 100644 --- a/lib/core/models/class_stats.dart +++ b/lib/core/models/class_stats.dart @@ -13,6 +13,7 @@ class ClassStats { final int totalContent; final List weeklyStats; final List studentsNeedingSupport; + final DateTime? lastUpdated; const ClassStats({ required this.classId, @@ -26,6 +27,7 @@ class ClassStats { required this.totalContent, required this.weeklyStats, required this.studentsNeedingSupport, + this.lastUpdated, }); factory ClassStats.fromFirestore(Map data, String classId) { @@ -47,6 +49,7 @@ class ClassStats { ?.map((s) => StudentNeedingSupport.fromFirestore(s)) .toList() ?? [], + lastUpdated: (data['lastUpdated'] as Timestamp?)?.toDate(), ); } @@ -62,6 +65,7 @@ class ClassStats { 'totalContent': totalContent, 'weeklyStats': weeklyStats.map((w) => w.toFirestore()).toList(), 'studentsNeedingSupport': studentsNeedingSupport.map((s) => s.toFirestore()).toList(), + 'lastUpdated': lastUpdated != null ? Timestamp.fromDate(lastUpdated!) : null, }; } } diff --git a/lib/core/services/gamification_service.dart b/lib/core/services/gamification_service.dart index 5de7583..0a60042 100644 --- a/lib/core/services/gamification_service.dart +++ b/lib/core/services/gamification_service.dart @@ -195,19 +195,51 @@ class GamificationService { } /// Obter estatísticas da turma - static Future getClassStats(String classId) async { + static Future 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(); if (!classStatsDoc.exists) { return await _calculateClassStats(classId); } - return ClassStats.fromFirestore(classStatsDoc.data()!, 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 || + 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'); return null; } } + /// Forçar atualização de estatísticas de todas as turmas de um professor + static Future refreshAllClassStats(String teacherId) async { + try { + final classesSnapshot = await _firestore + .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'); + } + } + /// Obter ranking de alunos da turma static Future> getClassRanking(String classId) async { try { @@ -225,6 +257,13 @@ class GamificationService { final studentIds = enrollmentsSnapshot.docs.map((doc) => doc['studentId'] as String).toList(); final rankings = []; + // Obter número real de quizzes disponíveis na turma + final quizzesSnapshot = await _firestore + .collection('teacherQuizzes') + .where('classIds', arrayContains: classId) + .get(); + final totalAvailableQuizzes = quizzesSnapshot.docs.length; + // Para cada aluno, obter suas estatísticas for (final studentId in studentIds) { try { @@ -235,17 +274,37 @@ class GamificationService { final userData = userDoc.data() as Map?; // 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; + 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('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}'); // Calcular score geral baseado em múltiplos fatores 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)}...'; + } rankings.add(StudentRanking( studentId: studentId, - studentName: userData?['displayName'] ?? 'Aluno $studentId', + studentName: studentName, studentEmail: userData?['email'] ?? '', overallScore: overallScore, completedQuizzes: completedQuizzes, @@ -274,21 +333,28 @@ class GamificationService { /// Calcular score geral para ranking static double _calculateOverallScore(UserStats userStats, double quizCompletionRate) { - double score = 0.0; + // Se completou 100% dos quizzes, score é 100% + if (quizCompletionRate >= 1.0) { + return 100.0; + } - // Peso de 40% para completion rate de quizzes - score += quizCompletionRate * 40; + // Para completion < 100%, calcular proporcionalmente + double baseScore = quizCompletionRate * 90; // 90% baseado em completion - // Peso de 20% para streak (máximo 20 pontos) - score += (userStats.currentStreak / 30.0 * 20).clamp(0.0, 20.0); + // Bônus adicionais (máximo 10% extra) + double bonusScore = 0.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); + // 5% para conceitos dominados + bonusScore += (userStats.masteredConcepts.length / 5.0 * 5).clamp(0.0, 5.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); + // 3% para streak + bonusScore += (userStats.currentStreak / 7.0 * 3).clamp(0.0, 3.0); - return score; + // 2% para conquistas + bonusScore += (userStats.unlockedAchievements.length / 10.0 * 2).clamp(0.0, 2.0); + + final totalScore = baseScore + bonusScore; + return totalScore.clamp(0.0, 100.0); } /// Criar conquista personalizada (professor) @@ -444,46 +510,97 @@ class GamificationService { 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++; - } + + // 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; + if (daysSinceLastActivity <= 30) { + 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; + // Calcular progresso baseado em quizzes completos e conceitos dominados + double progress = 0.0; + 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; + // 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('=== END PROGRESS CALCULATION DEBUG ==='); + } else { + Logger.info('Student $studentId has no stats - progress = 0.0'); + } + + 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, - )); + // 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 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)}...'; } + + 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; + + 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('=== END AVERAGE PROGRESS DEBUG ==='); - // Obter estatísticas de quizzes + // Obter estatísticas de quizzes e conteúdos final quizzesSnapshot = await _firestore .collection('teacherQuizzes') .where('classIds', arrayContains: classId) .get(); + // Obter materiais/conteúdos da turma + final materialsSnapshot = await _firestore + .collection('materials') + .where('classId', isEqualTo: classId) + .get(); + + // Contar quizzes ativos (últimos 30 dias) + final thirtyDaysAgo = DateTime.now().subtract(const Duration(days: 30)); + final activeQuizzesCount = quizzesSnapshot.docs.where((doc) { + final createdAt = (doc.data()['createdAt'] as Timestamp?)?.toDate(); + return createdAt != null && createdAt.isAfter(thirtyDaysAgo); + }).length; + final classStats = ClassStats( classId: classId, teacherId: teacherId, @@ -492,14 +609,17 @@ class GamificationService { activeStudents: activeStudents, averageProgress: averageProgress, totalQuizzes: quizzesSnapshot.docs.length, - activeQuizzes: quizzesSnapshot.docs.length, // Simplificado - totalContent: 0, // Implementar depois + activeQuizzes: activeQuizzesCount, + totalContent: materialsSnapshot.docs.length, weeklyStats: [], studentsNeedingSupport: needingSupport, + lastUpdated: DateTime.now(), ); - // Salvar estatísticas calculadas + // 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()); + Logger.info('Class stats refreshed and saved for class $classId'); return classStats; } catch (e) { diff --git a/lib/features/analytics/presentation/pages/analytics_page.dart b/lib/features/analytics/presentation/pages/analytics_page.dart index 4f16b47..c14c0fd 100644 --- a/lib/features/analytics/presentation/pages/analytics_page.dart +++ b/lib/features/analytics/presentation/pages/analytics_page.dart @@ -55,7 +55,8 @@ class _AnalyticsPageState extends State for (final classDoc in classesSnapshot.docs) { final classId = classDoc.id; - final stats = await GamificationService.getClassStats(classId); + // Forçar atualização para obter dados mais recentes + final stats = await GamificationService.getClassStats(classId, forceRefresh: true); if (stats != null) { classStatsList.add(stats); } diff --git a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart index 9bf89f8..a4a3d12 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart @@ -41,7 +41,8 @@ class _TeacherAnalyticsPreviewWidgetState extends State { for (final classDoc in classesSnapshot.docs) { final classId = classDoc.id; - final stats = await GamificationService.getClassStats(classId); + // Forçar atualização para obter dados mais recentes + final stats = await GamificationService.getClassStats(classId, forceRefresh: true); if (stats != null) { classStatsList.add(stats); } @@ -73,7 +74,18 @@ class _TeacherHeroWidgetState extends State { double get classAverageProgress { if (_classStats.isEmpty) return 0.0; final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress); - return totalProgress / _classStats.length; + final average = totalProgress / _classStats.length; + + print('=== UI PROGRESS DEBUG ==='); + print('Number of classes: ${_classStats.length}'); + for (int i = 0; i < _classStats.length; i++) { + print('Class ${i + 1}: ${_classStats[i].className} - ${_classStats[i].averageProgress} (${(_classStats[i].averageProgress * 100).toInt()}%)'); + } + print('Total progress sum: $totalProgress'); + print('Calculated average: $average (${(average * 100).toInt()}%)'); + print('=== END UI PROGRESS DEBUG ==='); + + return average; } Widget build(BuildContext context) { @@ -184,13 +196,22 @@ class _TeacherHeroWidgetState extends State { ), ), ), - Text( - '${(classAverageProgress * 100).toInt()}%', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), + Builder( + builder: (context) { + final displayValue = (classAverageProgress * 100).toInt(); + print('=== RENDER DEBUG ==='); + print('classAverageProgress: $classAverageProgress'); + print('displayValue: $displayValue%'); + print('=== END RENDER DEBUG ==='); + return Text( + '$displayValue%', + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ); + }, ), ], ),