VERSAO FINAL (core features)

This commit is contained in:
2026-05-17 18:27:22 +01:00
parent ba58228467
commit 7a26223a01
5 changed files with 203 additions and 56 deletions

View File

@@ -13,6 +13,7 @@ class ClassStats {
final int totalContent; final int totalContent;
final List<WeeklyStats> weeklyStats; final List<WeeklyStats> weeklyStats;
final List<StudentNeedingSupport> studentsNeedingSupport; final List<StudentNeedingSupport> studentsNeedingSupport;
final DateTime? lastUpdated;
const ClassStats({ const ClassStats({
required this.classId, required this.classId,
@@ -26,6 +27,7 @@ class ClassStats {
required this.totalContent, required this.totalContent,
required this.weeklyStats, required this.weeklyStats,
required this.studentsNeedingSupport, required this.studentsNeedingSupport,
this.lastUpdated,
}); });
factory ClassStats.fromFirestore(Map<String, dynamic> data, String classId) { factory ClassStats.fromFirestore(Map<String, dynamic> data, String classId) {
@@ -47,6 +49,7 @@ class ClassStats {
?.map((s) => StudentNeedingSupport.fromFirestore(s)) ?.map((s) => StudentNeedingSupport.fromFirestore(s))
.toList() ?? .toList() ??
[], [],
lastUpdated: (data['lastUpdated'] as Timestamp?)?.toDate(),
); );
} }
@@ -62,6 +65,7 @@ class ClassStats {
'totalContent': totalContent, 'totalContent': totalContent,
'weeklyStats': weeklyStats.map((w) => w.toFirestore()).toList(), 'weeklyStats': weeklyStats.map((w) => w.toFirestore()).toList(),
'studentsNeedingSupport': studentsNeedingSupport.map((s) => s.toFirestore()).toList(), 'studentsNeedingSupport': studentsNeedingSupport.map((s) => s.toFirestore()).toList(),
'lastUpdated': lastUpdated != null ? Timestamp.fromDate(lastUpdated!) : null,
}; };
} }
} }

View File

@@ -195,19 +195,51 @@ class GamificationService {
} }
/// Obter estatísticas da turma /// Obter estatísticas da turma
static Future<ClassStats?> getClassStats(String classId) async { static Future<ClassStats?> getClassStats(String classId, {bool forceRefresh = false}) async {
try { 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) { if (!classStatsDoc.exists) {
return await _calculateClassStats(classId); 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) { } catch (e) {
Logger.error('Error getting class stats: $e'); Logger.error('Error getting class stats: $e');
return null; return null;
} }
} }
/// Forçar atualização de estatísticas de todas as turmas de um professor
static Future<void> 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 /// Obter ranking de alunos da turma
static Future<List<StudentRanking>> getClassRanking(String classId) async { static Future<List<StudentRanking>> getClassRanking(String classId) async {
try { try {
@@ -225,6 +257,13 @@ class GamificationService {
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>[]; final rankings = <StudentRanking>[];
// 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 // Para cada aluno, obter suas estatísticas
for (final studentId in studentIds) { for (final studentId in studentIds) {
try { try {
@@ -235,17 +274,37 @@ class GamificationService {
final userData = userDoc.data() as Map<String, dynamic>?; final userData = userDoc.data() as Map<String, dynamic>?;
// Calcular estatísticas para o ranking // Calcular estatísticas para o ranking
// Usar contador real de quizzes completos
final completedQuizzes = userStats.completedQuizzes; final completedQuizzes = userStats.completedQuizzes;
final totalQuizzes = completedQuizzes + 5; // Estimativa de quizzes disponíveis final totalQuizzes = totalAvailableQuizzes > 0 ? totalAvailableQuizzes : 1;
final quizCompletionRate = totalQuizzes > 0 ? completedQuizzes / totalQuizzes : 0.0; 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 // Calcular score geral baseado em múltiplos fatores
final overallScore = _calculateOverallScore(userStats, quizCompletionRate); 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( rankings.add(StudentRanking(
studentId: studentId, studentId: studentId,
studentName: userData?['displayName'] ?? 'Aluno $studentId', studentName: studentName,
studentEmail: userData?['email'] ?? '', studentEmail: userData?['email'] ?? '',
overallScore: overallScore, overallScore: overallScore,
completedQuizzes: completedQuizzes, completedQuizzes: completedQuizzes,
@@ -274,21 +333,28 @@ class GamificationService {
/// Calcular score geral para ranking /// Calcular score geral para ranking
static double _calculateOverallScore(UserStats userStats, double quizCompletionRate) { 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 // Para completion < 100%, calcular proporcionalmente
score += quizCompletionRate * 40; double baseScore = quizCompletionRate * 90; // 90% baseado em completion
// Peso de 20% para streak (máximo 20 pontos) // Bônus adicionais (máximo 10% extra)
score += (userStats.currentStreak / 30.0 * 20).clamp(0.0, 20.0); double bonusScore = 0.0;
// Peso de 20% para tempo de estudo (máximo 20 pontos para 10 horas) // 5% para conceitos dominados
score += (userStats.totalStudyTime / 600.0 * 20).clamp(0.0, 20.0); 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) // 3% para streak
score += (userStats.masteredConcepts.length / 10.0 * 20).clamp(0.0, 20.0); 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) /// Criar conquista personalizada (professor)
@@ -444,46 +510,97 @@ class GamificationService {
for (final studentId in studentIds) { for (final studentId in studentIds) {
final userStats = await getUserStats(studentId); final userStats = await getUserStats(studentId);
if (userStats != null) {
// Verificar se está ativo (atividade nos últimos 7 dias) // Verificar se está ativo (atividade nos últimos 30 dias - mais realista)
int daysSinceLastActivity = 999; // Valor alto para inatividade int daysSinceLastActivity = 999; // Valor alto para inatividade
if (userStats.lastActivityDate != null) { bool hasStats = userStats != null;
daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays;
if (daysSinceLastActivity <= 7) { if (hasStats && userStats!.lastActivityDate != null) {
activeStudents++; daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays;
} if (daysSinceLastActivity <= 30) {
activeStudents++;
} }
}
// Calcular progresso baseado em conceitos dominados // Calcular progresso baseado em quizzes completos e conceitos dominados
final progress = userStats.masteredConcepts.isEmpty double progress = 0.0;
? 0.0 if (hasStats) {
: userStats.masteredConcepts.map((c) => c.masteryLevel).reduce((a, b) => a + b) / userStats.masteredConcepts.length / 100; final completedQuizzes = userStats!.completedQuizzes;
totalProgress += progress; 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 // Verificar se precisa de apoio (ajustado para nova fórmula)
if (progress < 0.5 || daysSinceLastActivity > 14) { 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 userName = userDoc.data()?['displayName'] ?? 'Unknown'; final userData = userDoc.data();
needingSupport.add(StudentNeedingSupport( // Tentar obter um nome melhor para o aluno
studentId: studentId, String studentName = 'Aluno ${studentId.substring(0, 8)}...';
studentName: userName, if (userData != null) {
reason: progress < 0.5 ? 'low_scores' : 'inactivity', studentName = userData['displayName'] ??
lastActivity: userStats.lastActivityDate ?? DateTime.now(), userData['email']?.split('@')[0] ??
averageScore: progress * 100, '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; 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 final quizzesSnapshot = await _firestore
.collection('teacherQuizzes') .collection('teacherQuizzes')
.where('classIds', arrayContains: classId) .where('classIds', arrayContains: classId)
.get(); .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( final classStats = ClassStats(
classId: classId, classId: classId,
teacherId: teacherId, teacherId: teacherId,
@@ -492,14 +609,17 @@ class GamificationService {
activeStudents: activeStudents, activeStudents: activeStudents,
averageProgress: averageProgress, averageProgress: averageProgress,
totalQuizzes: quizzesSnapshot.docs.length, totalQuizzes: quizzesSnapshot.docs.length,
activeQuizzes: quizzesSnapshot.docs.length, // Simplificado activeQuizzes: activeQuizzesCount,
totalContent: 0, // Implementar depois totalContent: materialsSnapshot.docs.length,
weeklyStats: [], weeklyStats: [],
studentsNeedingSupport: needingSupport, 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()); await _firestore.collection('classStats').doc(classId).set(classStats.toFirestore());
Logger.info('Class stats refreshed and saved for class $classId');
return classStats; return classStats;
} catch (e) { } catch (e) {

View File

@@ -55,7 +55,8 @@ class _AnalyticsPageState extends State<AnalyticsPage>
for (final classDoc in classesSnapshot.docs) { for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id; 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) { if (stats != null) {
classStatsList.add(stats); classStatsList.add(stats);
} }

View File

@@ -41,7 +41,8 @@ class _TeacherAnalyticsPreviewWidgetState extends State<TeacherAnalyticsPreviewW
for (final classDoc in classesSnapshot.docs) { for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id; 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) { if (stats != null) {
classStatsList.add(stats); classStatsList.add(stats);
} }

View File

@@ -45,7 +45,8 @@ class _TeacherHeroWidgetState extends State<TeacherHeroWidget> {
for (final classDoc in classesSnapshot.docs) { for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id; 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) { if (stats != null) {
classStatsList.add(stats); classStatsList.add(stats);
} }
@@ -73,7 +74,18 @@ class _TeacherHeroWidgetState extends State<TeacherHeroWidget> {
double get classAverageProgress { double get classAverageProgress {
if (_classStats.isEmpty) return 0.0; if (_classStats.isEmpty) return 0.0;
final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress); 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) { Widget build(BuildContext context) {
@@ -184,13 +196,22 @@ class _TeacherHeroWidgetState extends State<TeacherHeroWidget> {
), ),
), ),
), ),
Text( Builder(
'${(classAverageProgress * 100).toInt()}%', builder: (context) {
style: const TextStyle( final displayValue = (classAverageProgress * 100).toInt();
color: Colors.white, print('=== RENDER DEBUG ===');
fontSize: 24, print('classAverageProgress: $classAverageProgress');
fontWeight: FontWeight.bold, print('displayValue: $displayValue%');
), print('=== END RENDER DEBUG ===');
return Text(
'$displayValue%',
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
);
},
), ),
], ],
), ),