From 49a7a6fe0239bc8815cb4704ae833b7696eb968f Mon Sep 17 00:00:00 2001 From: 240403 <240403@epvc.pt> Date: Sun, 17 May 2026 17:29:47 +0100 Subject: [PATCH] placeholders removidos e todos os dados reais colocados, com conquistas e tudo --- lib/core/models/achievement.dart | 334 ++++++++ lib/core/models/class_stats.dart | 195 +++++ lib/core/models/user_stats.dart | 151 ++++ lib/core/routing/app_router.dart | 27 + lib/core/services/gamification_service.dart | 734 ++++++++++++++++++ .../pages/student_achievements_page.dart | 485 ++++++++++++ .../presentation/pages/analytics_page.dart | 343 ++++++++ .../widgets/class_analytics_card.dart | 317 ++++++++ .../widgets/class_ranking_widget.dart | 432 +++++++++++ .../widgets/create_achievement_dialog.dart | 633 +++++++++++++++ .../widgets/profile_section_widget.dart | 254 ++++-- .../widgets/progress_hero_widget.dart | 79 +- .../widgets/quick_access_widget.dart | 75 +- .../teacher_analytics_preview_widget.dart | 137 +++- .../widgets/teacher_hero_widget.dart | 164 +++- .../presentation/pages/quiz_list_page.dart | 12 + .../pages/quiz_management_page.dart | 458 +++++++++++ 17 files changed, 4688 insertions(+), 142 deletions(-) create mode 100644 lib/core/models/achievement.dart create mode 100644 lib/core/models/class_stats.dart create mode 100644 lib/core/models/user_stats.dart create mode 100644 lib/core/services/gamification_service.dart create mode 100644 lib/features/achievements/presentation/pages/student_achievements_page.dart create mode 100644 lib/features/analytics/presentation/pages/analytics_page.dart create mode 100644 lib/features/analytics/presentation/widgets/class_analytics_card.dart create mode 100644 lib/features/analytics/presentation/widgets/class_ranking_widget.dart create mode 100644 lib/features/analytics/presentation/widgets/create_achievement_dialog.dart create mode 100644 lib/features/quiz/presentation/pages/quiz_management_page.dart diff --git a/lib/core/models/achievement.dart b/lib/core/models/achievement.dart new file mode 100644 index 0000000..154a553 --- /dev/null +++ b/lib/core/models/achievement.dart @@ -0,0 +1,334 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +/// Model para definição de conquistas +class Achievement { + final String id; + final String name; + final String description; + final String icon; + final String category; // 'streak', 'study_time', 'quiz', 'concept' + final AchievementRequirement requirements; + final int points; + final String rarity; // 'common', 'rare', 'epic', 'legendary' + final bool isActive; + final DateTime createdAt; + final String? createdBy; // teacherId se criada por professor + + const Achievement({ + required this.id, + required this.name, + required this.description, + required this.icon, + required this.category, + required this.requirements, + required this.points, + required this.rarity, + required this.isActive, + required this.createdAt, + this.createdBy, + }); + + factory Achievement.fromFirestore(Map data, String id) { + return Achievement( + id: id, + name: data['name'] ?? '', + description: data['description'] ?? '', + icon: data['icon'] ?? 'star', + category: data['category'] ?? 'general', + requirements: AchievementRequirement.fromFirestore(data['requirements'] ?? {}), + points: data['points'] ?? 0, + rarity: data['rarity'] ?? 'common', + isActive: data['isActive'] ?? true, + createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(), + createdBy: data['createdBy'], + ); + } + + Map toFirestore() { + return { + 'name': name, + 'description': description, + 'icon': icon, + 'category': category, + 'requirements': requirements.toFirestore(), + 'points': points, + 'rarity': rarity, + 'isActive': isActive, + 'createdAt': Timestamp.fromDate(createdAt), + if (createdBy != null) 'createdBy': createdBy, + }; + } + + Achievement copyWith({ + String? id, + String? name, + String? description, + String? icon, + String? category, + AchievementRequirement? requirements, + int? points, + String? rarity, + bool? isActive, + String? createdBy, + }) { + return Achievement( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + icon: icon ?? this.icon, + category: category ?? this.category, + requirements: requirements ?? this.requirements, + points: points ?? this.points, + rarity: rarity ?? this.rarity, + isActive: isActive ?? this.isActive, + createdAt: createdAt, + createdBy: createdBy ?? this.createdBy, + ); + } +} + +/// Requisitos para desbloquear uma conquista +class AchievementRequirement { + final String type; // 'streak_days', 'study_time', 'quiz_score', 'concepts_mastered', 'quiz_completion' + final num value; + final String operator; // '>=', '==', '>' + final Map? additionalParams; + + const AchievementRequirement({ + required this.type, + required this.value, + required this.operator, + this.additionalParams, + }); + + factory AchievementRequirement.fromFirestore(Map data) { + return AchievementRequirement( + type: data['type'] ?? '', + value: data['value'] ?? 0, + operator: data['operator'] ?? '>=', + additionalParams: data['additionalParams'], + ); + } + + Map toFirestore() { + return { + 'type': type, + 'value': value, + 'operator': operator, + if (additionalParams != null) 'additionalParams': additionalParams, + }; + } + + bool checkCondition(num currentValue) { + switch (operator) { + case '>=': + return currentValue >= value; + case '==': + return currentValue == value; + case '>': + return currentValue > value; + default: + return false; + } + } +} + +/// Conquistas predefinidas do sistema +class SystemAchievements { + static List get defaultAchievements => [ + Achievement( + id: 'first_quiz', + name: 'Primeiro Passo', + description: 'Complete seu primeiro quiz', + icon: 'emoji_events', + category: 'quiz', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 1, + operator: '>=', + ), + points: 10, + rarity: 'common', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'week_streak', + name: 'Semana de Dedicação', + description: 'Mantenha uma streak de 7 dias', + icon: 'local_fire_department', + category: 'streak', + requirements: AchievementRequirement( + type: 'streak_days', + value: 7, + operator: '>=', + ), + points: 50, + rarity: 'rare', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'study_marathon', + name: 'Maratona de Estudos', + description: 'Estude por 100 minutos em um dia', + icon: 'schedule', + category: 'study_time', + requirements: AchievementRequirement( + type: 'study_time', + value: 100, + operator: '>=', + additionalParams: {'period': 'daily'}, + ), + points: 30, + rarity: 'rare', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'first_quiz', + name: 'Primeiro Passo', + description: 'Complete seu primeiro quiz', + icon: 'emoji_events', + category: 'quiz', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 1, + operator: '>=', + ), + points: 10, + rarity: 'common', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'perfect_score', + name: 'Perfeição', + description: 'Obtenha 100% em um quiz', + icon: 'star', + category: 'quiz', + requirements: AchievementRequirement( + type: 'quiz_score', + value: 100, + operator: '==', + ), + points: 25, + rarity: 'rare', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'concept_master', + name: 'Mestre de Conceitos', + description: 'Domine 5 conceitos', + icon: 'school', + category: 'concept', + requirements: AchievementRequirement( + type: 'concepts_mastered', + value: 5, + operator: '>=', + ), + points: 40, + rarity: 'epic', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'month_streak', + name: 'Lendário', + description: 'Mantenha uma streak de 30 dias', + icon: 'whatshot', + category: 'streak', + requirements: AchievementRequirement( + type: 'streak_days', + value: 30, + operator: '>=', + ), + points: 200, + rarity: 'legendary', + isActive: true, + createdAt: DateTime.now(), + ), + // Conquistas genéricas de número de quizzes + Achievement( + id: 'quiz_novice_5', + name: 'Iniciante', + description: 'Complete 5 quizzes', + icon: 'emoji_events', + category: 'quiz_count', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 5, + operator: '>=', + ), + points: 15, + rarity: 'common', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'quiz_intermediate_10', + name: 'Estudante Dedicao', + description: 'Complete 10 quizzes', + icon: 'school', + category: 'quiz_count', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 10, + operator: '>=', + ), + points: 30, + rarity: 'common', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'quiz_advanced_25', + name: 'Mestre dos Quizzes', + description: 'Complete 25 quizzes', + icon: 'military_tech', + category: 'quiz_count', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 25, + operator: '>=', + ), + points: 75, + rarity: 'rare', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'quiz_expert_50', + name: 'Especialista', + description: 'Complete 50 quizzes', + icon: 'workspace_premium', + category: 'quiz_count', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 50, + operator: '>=', + ), + points: 150, + rarity: 'epic', + isActive: true, + createdAt: DateTime.now(), + ), + Achievement( + id: 'quiz_legend_100', + name: 'Lenda dos Quizzes', + description: 'Complete 100 quizzes', + icon: 'stars', + category: 'quiz_count', + requirements: AchievementRequirement( + type: 'quiz_completion', + value: 100, + operator: '>=', + ), + points: 300, + rarity: 'legendary', + isActive: true, + createdAt: DateTime.now(), + ), + ]; +} diff --git a/lib/core/models/class_stats.dart b/lib/core/models/class_stats.dart new file mode 100644 index 0000000..1af2d2b --- /dev/null +++ b/lib/core/models/class_stats.dart @@ -0,0 +1,195 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +/// Model para estatísticas da turma (professor) +class ClassStats { + final String classId; + final String teacherId; + final String className; + final int totalStudents; + final int activeStudents; + final double averageProgress; + final int totalQuizzes; + final int activeQuizzes; + final int totalContent; + final List weeklyStats; + final List studentsNeedingSupport; + + const ClassStats({ + required this.classId, + required this.teacherId, + required this.className, + required this.totalStudents, + required this.activeStudents, + required this.averageProgress, + required this.totalQuizzes, + required this.activeQuizzes, + required this.totalContent, + required this.weeklyStats, + required this.studentsNeedingSupport, + }); + + factory ClassStats.fromFirestore(Map data, String classId) { + return ClassStats( + classId: classId, + teacherId: data['teacherId'] ?? '', + className: data['className'] ?? '', + totalStudents: data['totalStudents'] ?? 0, + activeStudents: data['activeStudents'] ?? 0, + averageProgress: (data['averageProgress'] ?? 0).toDouble(), + totalQuizzes: data['totalQuizzes'] ?? 0, + activeQuizzes: data['activeQuizzes'] ?? 0, + totalContent: data['totalContent'] ?? 0, + weeklyStats: (data['weeklyStats'] as List?) + ?.map((w) => WeeklyStats.fromFirestore(w)) + .toList() ?? + [], + studentsNeedingSupport: (data['studentsNeedingSupport'] as List?) + ?.map((s) => StudentNeedingSupport.fromFirestore(s)) + .toList() ?? + [], + ); + } + + Map toFirestore() { + return { + 'teacherId': teacherId, + 'className': className, + 'totalStudents': totalStudents, + 'activeStudents': activeStudents, + 'averageProgress': averageProgress, + 'totalQuizzes': totalQuizzes, + 'activeQuizzes': activeQuizzes, + 'totalContent': totalContent, + 'weeklyStats': weeklyStats.map((w) => w.toFirestore()).toList(), + 'studentsNeedingSupport': studentsNeedingSupport.map((s) => s.toFirestore()).toList(), + }; + } +} + +/// Estatísticas semanais da turma +class WeeklyStats { + final DateTime weekStart; + final int activeStudents; + final double averageScore; + final int totalStudyTime; + + const WeeklyStats({ + required this.weekStart, + required this.activeStudents, + required this.averageScore, + required this.totalStudyTime, + }); + + factory WeeklyStats.fromFirestore(Map data) { + return WeeklyStats( + weekStart: (data['weekStart'] as Timestamp?)?.toDate() ?? DateTime.now(), + activeStudents: data['activeStudents'] ?? 0, + averageScore: (data['averageScore'] ?? 0).toDouble(), + totalStudyTime: data['totalStudyTime'] ?? 0, + ); + } + + Map toFirestore() { + return { + 'weekStart': Timestamp.fromDate(weekStart), + 'activeStudents': activeStudents, + 'averageScore': averageScore, + 'totalStudyTime': totalStudyTime, + }; + } +} + +/// Aluno que precisa de apoio +class StudentNeedingSupport { + final String studentId; + final String studentName; + final String reason; // 'low_scores', 'inactivity', 'struggling_concept' + final DateTime lastActivity; + final double averageScore; + + const StudentNeedingSupport({ + required this.studentId, + required this.studentName, + required this.reason, + required this.lastActivity, + required this.averageScore, + }); + + factory StudentNeedingSupport.fromFirestore(Map data) { + return StudentNeedingSupport( + studentId: data['studentId'] ?? '', + studentName: data['studentName'] ?? '', + reason: data['reason'] ?? '', + lastActivity: (data['lastActivity'] as Timestamp?)?.toDate() ?? DateTime.now(), + averageScore: (data['averageScore'] ?? 0).toDouble(), + ); + } + + Map toFirestore() { + return { + 'studentId': studentId, + 'studentName': studentName, + 'reason': reason, + 'lastActivity': Timestamp.fromDate(lastActivity), + 'averageScore': averageScore, + }; + } +} + +/// Ranking de alunos em uma turma +class StudentRanking { + final String studentId; + final String studentName; + final String studentEmail; + final double overallScore; + final int completedQuizzes; + final int totalQuizzes; + final double quizCompletionRate; + final int studyTimeMinutes; + final int currentStreak; + final DateTime lastActivity; + + const StudentRanking({ + required this.studentId, + required this.studentName, + required this.studentEmail, + required this.overallScore, + required this.completedQuizzes, + required this.totalQuizzes, + required this.quizCompletionRate, + required this.studyTimeMinutes, + required this.currentStreak, + required this.lastActivity, + }); + + double get quizCompletionPercentage => quizCompletionRate * 100; + + factory StudentRanking.fromFirestore(Map data, String studentId) { + return StudentRanking( + studentId: studentId, + studentName: data['studentName'] ?? '', + studentEmail: data['studentEmail'] ?? '', + overallScore: (data['overallScore'] ?? 0).toDouble(), + completedQuizzes: data['completedQuizzes'] ?? 0, + totalQuizzes: data['totalQuizzes'] ?? 0, + quizCompletionRate: (data['quizCompletionRate'] ?? 0).toDouble(), + studyTimeMinutes: data['studyTimeMinutes'] ?? 0, + currentStreak: data['currentStreak'] ?? 0, + lastActivity: (data['lastActivity'] as Timestamp?)?.toDate() ?? DateTime.now(), + ); + } + + Map toFirestore() { + return { + 'studentName': studentName, + 'studentEmail': studentEmail, + 'overallScore': overallScore, + 'completedQuizzes': completedQuizzes, + 'totalQuizzes': totalQuizzes, + 'quizCompletionRate': quizCompletionRate, + 'studyTimeMinutes': studyTimeMinutes, + 'currentStreak': currentStreak, + 'lastActivity': Timestamp.fromDate(lastActivity), + }; + } +} diff --git a/lib/core/models/user_stats.dart b/lib/core/models/user_stats.dart new file mode 100644 index 0000000..d69a9f3 --- /dev/null +++ b/lib/core/models/user_stats.dart @@ -0,0 +1,151 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; + +/// Model para estatísticas do usuário (aluno) +class UserStats { + final String userId; + final int currentStreak; + final int longestStreak; + final int totalStudyTime; // em minutos + final DateTime? lastActivityDate; + final int weeklyStudyTime; // minutos esta semana + final int monthlyStudyTime; // minutos este mês + final int completedQuizzes; // total de quizzes completos + final List masteredConcepts; + final List unlockedAchievements; + + const UserStats({ + required this.userId, + required this.currentStreak, + required this.longestStreak, + required this.totalStudyTime, + required this.weeklyStudyTime, + required this.monthlyStudyTime, + required this.completedQuizzes, + required this.masteredConcepts, + required this.unlockedAchievements, + this.lastActivityDate, + }); + + factory UserStats.fromFirestore(Map data, String userId) { + return UserStats( + userId: userId, + currentStreak: data['currentStreak'] ?? 0, + longestStreak: data['longestStreak'] ?? 0, + totalStudyTime: data['totalStudyTime'] ?? 0, + lastActivityDate: (data['lastActivityDate'] as Timestamp?)?.toDate(), + weeklyStudyTime: data['weeklyStudyTime'] ?? 0, + monthlyStudyTime: data['monthlyStudyTime'] ?? 0, + completedQuizzes: data['completedQuizzes'] ?? 0, + masteredConcepts: (data['masteredConcepts'] as List?) + ?.map((c) => MasteredConcept.fromFirestore(c)) + .toList() ?? + [], + unlockedAchievements: (data['unlockedAchievements'] as List?) + ?.map((a) => UnlockedAchievement.fromFirestore(a)) + .toList() ?? + [], + ); + } + + Map toFirestore() { + final data = { + 'currentStreak': currentStreak, + 'longestStreak': longestStreak, + 'totalStudyTime': totalStudyTime, + 'weeklyStudyTime': weeklyStudyTime, + 'monthlyStudyTime': monthlyStudyTime, + 'completedQuizzes': completedQuizzes, + 'masteredConcepts': masteredConcepts.map((c) => c.toFirestore()).toList(), + 'unlockedAchievements': unlockedAchievements.map((a) => a.toFirestore()).toList(), + }; + + if (lastActivityDate != null) { + data['lastActivityDate'] = Timestamp.fromDate(lastActivityDate!); + } + + return data; + } + + UserStats copyWith({ + int? currentStreak, + int? longestStreak, + int? totalStudyTime, + DateTime? lastActivityDate, + int? weeklyStudyTime, + int? monthlyStudyTime, + int? completedQuizzes, + List? masteredConcepts, + List? unlockedAchievements, + }) { + return UserStats( + userId: userId, + currentStreak: currentStreak ?? this.currentStreak, + longestStreak: longestStreak ?? this.longestStreak, + totalStudyTime: totalStudyTime ?? this.totalStudyTime, + lastActivityDate: lastActivityDate ?? this.lastActivityDate, + weeklyStudyTime: weeklyStudyTime ?? this.weeklyStudyTime, + monthlyStudyTime: monthlyStudyTime ?? this.monthlyStudyTime, + completedQuizzes: completedQuizzes ?? this.completedQuizzes, + masteredConcepts: masteredConcepts ?? this.masteredConcepts, + unlockedAchievements: unlockedAchievements ?? this.unlockedAchievements, + ); + } +} + +/// Conceito dominado pelo aluno +class MasteredConcept { + final String conceptName; + final DateTime masteredAt; + final int masteryLevel; // 0-100 + + const MasteredConcept({ + required this.conceptName, + required this.masteredAt, + required this.masteryLevel, + }); + + factory MasteredConcept.fromFirestore(Map data) { + return MasteredConcept( + conceptName: data['conceptName'] ?? '', + masteredAt: (data['masteredAt'] as Timestamp?)?.toDate() ?? DateTime.now(), + masteryLevel: data['masteryLevel'] ?? 0, + ); + } + + Map toFirestore() { + return { + 'conceptName': conceptName, + 'masteredAt': Timestamp.fromDate(masteredAt), + 'masteryLevel': masteryLevel, + }; + } +} + +/// Conquista desbloqueada pelo aluno +class UnlockedAchievement { + final String achievementId; + final DateTime unlockedAt; + final Map metadata; + + const UnlockedAchievement({ + required this.achievementId, + required this.unlockedAt, + required this.metadata, + }); + + factory UnlockedAchievement.fromFirestore(Map data) { + return UnlockedAchievement( + achievementId: data['achievementId'] ?? '', + unlockedAt: (data['unlockedAt'] as Timestamp?)?.toDate() ?? DateTime.now(), + metadata: data['metadata'] ?? {}, + ); + } + + Map toFirestore() { + return { + 'achievementId': achievementId, + 'unlockedAt': Timestamp.fromDate(unlockedAt), + 'metadata': metadata, + }; + } +} diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index f0ce6b1..8da2dca 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -15,6 +15,9 @@ import '../../features/quiz/presentation/pages/teacher_quiz_page.dart'; import '../../features/profile/presentation/pages/profile_page.dart'; import '../../features/splash/presentation/pages/splash_page.dart'; import '../../features/auth/presentation/pages/role_selection_page.dart'; +import '../../features/analytics/presentation/pages/analytics_page.dart'; +import '../../features/achievements/presentation/pages/student_achievements_page.dart'; +import '../../features/quiz/presentation/pages/quiz_management_page.dart'; /// App Router Configuration class AppRouter { @@ -29,6 +32,9 @@ class AppRouter { static const String quiz = '/quiz/:quizId'; static const String profile = '/profile'; static const String settings = '/settings'; + static const String teacherAnalytics = '/teacher/analytics'; + static const String studentAchievements = '/student/achievements'; + static const String quizManagement = '/quiz-management'; // Nested route paths (without leading slash) static const String tutorNested = 'tutor'; @@ -141,6 +147,27 @@ class AppRouter { builder: (context, state) => const TutorChatPageSimple(), ), + // Teacher Analytics Route + GoRoute( + path: teacherAnalytics, + name: 'teacherAnalytics', + builder: (context, state) => const AnalyticsPage(), + ), + + // Student Achievements Route + GoRoute( + path: studentAchievements, + name: 'studentAchievements', + builder: (context, state) => const StudentAchievementsPage(), + ), + + // Quiz Management Route + GoRoute( + path: quizManagement, + name: 'quizManagement', + builder: (context, state) => const QuizManagementPage(), + ), + // Quiz List Route (independent — student access) GoRoute( path: quizList, diff --git a/lib/core/services/gamification_service.dart b/lib/core/services/gamification_service.dart new file mode 100644 index 0000000..eac1aca --- /dev/null +++ b/lib/core/services/gamification_service.dart @@ -0,0 +1,734 @@ +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 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 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 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 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; + + // 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 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> 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 = []; + + // 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?; + + // 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 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> getAvailableAchievements({String? teacherId}) async { + try { + // Sempre incluir conquistas do sistema + List 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, 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étodo público para inicializar dados de gamificação (para testes) + static Future initializeGamificationData(String userId) async { + try { + final userStats = await getUserStats(userId); + if (userStats == null) { + await _createInitialUserStats(userId); + Logger.info('Gamification data initialized for user $userId'); + } + } catch (e) { + Logger.error('Error initializing gamification data: $e'); + } + } + + /// Método público para simular quiz completion (para testes) + static Future simulateQuizCompletion(String userId, { + required int score, + required int totalQuestions, + required String materialName, + }) async { + Logger.info('=== SIMULATING QUIZ COMPLETION ==='); + Logger.info('User: $userId'); + Logger.info('Score: $score/$totalQuestions'); + Logger.info('Material: $materialName'); + + await recordQuizActivity( + userId, + score: score, + totalQuestions: totalQuestions, + materialName: materialName, + ); + + // Verificar estado após simulação + await debugUserStats(userId); + } + + /// Método de debugging para verificar estado completo do usuário + static Future debugUserStats(String userId) async { + try { + Logger.info('=== DEBUGGING USER STATS ==='); + + final userStats = await getUserStats(userId); + if (userStats == null) { + Logger.error('User stats not found for $userId'); + return; + } + + Logger.info('Current Streak: ${userStats.currentStreak}'); + Logger.info('Longest Streak: ${userStats.longestStreak}'); + Logger.info('Total Study Time: ${userStats.totalStudyTime}'); + Logger.info('Weekly Study Time: ${userStats.weeklyStudyTime}'); + Logger.info('Monthly Study Time: ${userStats.monthlyStudyTime}'); + Logger.info('Mastered Concepts: ${userStats.masteredConcepts.length}'); + Logger.info('Unlocked Achievements: ${userStats.unlockedAchievements.length}'); + + for (final concept in userStats.masteredConcepts) { + Logger.info(' - Concept: ${concept.conceptName}, Level: ${concept.masteryLevel}'); + } + + for (final achievement in userStats.unlockedAchievements) { + Logger.info(' - Achievement: ${achievement.achievementId}, Unlocked: ${achievement.unlockedAt}'); + } + + Logger.info('=== END DEBUG ==='); + } catch (e) { + Logger.error('Error debugging user stats: $e'); + } + } + + /// Métodos privados + + static Future _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 _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 _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 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 _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 _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 _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.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 ==='); + // Verificar conquistas genéricas de número de quizzes + await _checkQuizCountAchievements(userId, completedQuizzes); + } + + static Future _checkQuizCountAchievements(String userId, int completedQuizzes) async { + final achievements = await getAvailableAchievements(); + final quizCountAchievements = achievements.where((a) => a.category == 'quiz_count'); + + for (final achievement in quizCountAchievements) { + if (achievement.requirements.type == 'quiz_completion' && + achievement.requirements.checkCondition(completedQuizzes)) { + await _unlockAchievement(userId, achievement.id); + } + } + } + + static Future _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 _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; + 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'); + } + } +} diff --git a/lib/features/achievements/presentation/pages/student_achievements_page.dart b/lib/features/achievements/presentation/pages/student_achievements_page.dart new file mode 100644 index 0000000..b916ac4 --- /dev/null +++ b/lib/features/achievements/presentation/pages/student_achievements_page.dart @@ -0,0 +1,485 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/services/auth_service.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/user_stats.dart'; +import '../../../../core/models/achievement.dart'; + +/// Página de conquistas para alunos +class StudentAchievementsPage extends StatefulWidget { + const StudentAchievementsPage({super.key}); + + @override + State createState() => _StudentAchievementsPageState(); +} + +class _StudentAchievementsPageState extends State { + UserStats? _userStats; + List _allAchievements = []; + List _unlockedAchievements = []; + List _lockedAchievements = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadAchievements(); + } + + Future _loadAchievements() async { + try { + final user = AuthService.currentUser; + if (user == null) return; + + final [stats, allAchievements] = await Future.wait([ + GamificationService.getUserStats(user.uid), + GamificationService.getAvailableAchievements(), + ]); + + final userStats = stats as UserStats?; + final achievements = allAchievements as List; + + if (userStats != null) { + final unlockedIds = userStats.unlockedAchievements + .map((ua) => ua.achievementId) + .toSet(); + + final unlocked = achievements + .where((a) => unlockedIds.contains(a.id)) + .toList(); + + final locked = achievements + .where((a) => !unlockedIds.contains(a.id)) + .toList(); + + if (mounted) { + setState(() { + _userStats = userStats; + _allAchievements = achievements; + _unlockedAchievements = unlocked; + _lockedAchievements = locked; + _loading = false; + }); + } + } + } catch (e) { + print('Error loading achievements: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final themeExtras = AppThemeExtras.of(context); + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + cs.primary.withValues(alpha: 0.05), + cs.surface, + ], + ), + ), + child: SafeArea( + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => context.go('/student-dashboard'), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Minhas Conquistas', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${_unlockedAchievements.length}/${_allAchievements.length} desbloqueadas', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, cs.primary.withValues(alpha: 0.8)], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + ), + + // Debug buttons (remover em produção) + if (!const bool.fromEnvironment('dart.vm.product')) ...[ + Container( + margin: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () async { + final user = AuthService.currentUser; + if (user != null) { + await GamificationService.initializeGamificationData(user.uid); + _loadAchievements(); + } + }, + child: const Text('Inicializar Dados'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () async { + final user = AuthService.currentUser; + if (user != null) { + await GamificationService.simulateQuizCompletion( + user.uid, + score: 8, + totalQuestions: 10, + materialName: 'Matemática Básica', + ); + _loadAchievements(); + } + }, + child: const Text('Simular Quiz'), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: () async { + final user = AuthService.currentUser; + if (user != null) { + await GamificationService.debugUserStats(user.uid); + } + }, + style: ElevatedButton.styleFrom(backgroundColor: Colors.orange), + child: const Text('Debug Stats'), + ), + ), + ], + ), + ), + ], + + // Content + Expanded( + child: _loading + ? const Center(child: CircularProgressIndicator()) + : DefaultTabController( + length: 2, + child: Column( + children: [ + // Tabs + Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + child: TabBar( + labelColor: cs.onPrimary, + unselectedLabelColor: cs.onSurfaceVariant, + indicator: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(10), + ), + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [ + Tab( + icon: Icon(Icons.emoji_events), + text: 'Desbloqueadas', + ), + Tab( + icon: Icon(Icons.lock_outline), + text: 'Bloqueadas', + ), + ], + ), + ), + + // Tab Content + Expanded( + child: TabBarView( + children: [ + _buildUnlockedAchievements(), + _buildLockedAchievements(), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildUnlockedAchievements() { + if (_unlockedAchievements.isEmpty) { + return _buildEmptyState( + icon: Icons.emoji_events_outlined, + title: 'Nenhuma conquista desbloqueada', + subtitle: 'Complete quizzes e mantenha seu streak para desbloquear conquistas!', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _unlockedAchievements.length, + itemBuilder: (context, index) { + final achievement = _unlockedAchievements[index]; + return _buildAchievementCard(achievement, isUnlocked: true) + .animate() + .slideX(duration: const Duration(milliseconds: 300)) + .then(delay: Duration(milliseconds: index * 50)); + }, + ); + } + + Widget _buildLockedAchievements() { + if (_lockedAchievements.isEmpty) { + return _buildEmptyState( + icon: Icons.emoji_events, + title: 'Parabéns!', + subtitle: 'Você desbloqueou todas as conquistas disponíveis!', + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _lockedAchievements.length, + itemBuilder: (context, index) { + final achievement = _lockedAchievements[index]; + return _buildAchievementCard(achievement, isUnlocked: false) + .animate() + .slideX(duration: const Duration(milliseconds: 300)) + .then(delay: Duration(milliseconds: index * 50)); + }, + ); + } + + Widget _buildAchievementCard(Achievement achievement, {required bool isUnlocked}) { + final cs = Theme.of(context).colorScheme; + final color = _getRarityColor(achievement.rarity); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isUnlocked ? cs.surface : cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isUnlocked + ? color.withValues(alpha: 0.3) + : cs.outline.withValues(alpha: 0.2), + width: isUnlocked ? 2 : 1, + ), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: isUnlocked ? 0.1 : 0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + // Icon + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: isUnlocked + ? color.withValues(alpha: 0.2) + : cs.outline.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(24), + ), + child: Icon( + _getIconData(achievement.icon), + color: isUnlocked ? color : cs.outline, + size: 24, + ), + ), + const SizedBox(width: 16), + + // Content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + achievement.name, + style: TextStyle( + color: isUnlocked ? cs.onSurface : cs.onSurfaceVariant, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + achievement.description, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getRarityColor(achievement.rarity).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + achievement.rarity.toUpperCase(), + style: TextStyle( + color: _getRarityColor(achievement.rarity), + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + Text( + '+${achievement.points} pts', + style: TextStyle( + color: cs.primary, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + ), + + // Status + if (isUnlocked) + Icon( + Icons.check_circle, + color: Colors.green, + size: 24, + ) + else + Icon( + Icons.lock, + color: cs.outline, + size: 24, + ), + ], + ), + ); + } + + Widget _buildEmptyState({ + required IconData icon, + required String title, + required String subtitle, + }) { + final cs = Theme.of(context).colorScheme; + + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + icon, + size: 64, + color: cs.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Color _getRarityColor(String rarity) { + switch (rarity) { + case 'common': return Colors.grey; + case 'rare': return Colors.blue; + case 'epic': return Colors.purple; + case 'legendary': return Colors.orange; + default: return Colors.grey; + } + } + + IconData _getIconData(String iconName) { + switch (iconName) { + case 'emoji_events': return Icons.emoji_events; + case 'school': return Icons.school; + case 'local_fire_department': return Icons.local_fire_department; + case 'schedule': return Icons.schedule; + case 'trending_up': return Icons.trending_up; + case 'star': return Icons.star; + case 'military_tech': return Icons.military_tech; + case 'workspace_premium': return Icons.workspace_premium; + case 'psychology': return Icons.psychology; + case 'lightbulb': return Icons.lightbulb; + case 'whatshot': return Icons.whatshot; + case 'stars': return Icons.stars; + default: return Icons.emoji_events; + } + } +} diff --git a/lib/features/analytics/presentation/pages/analytics_page.dart b/lib/features/analytics/presentation/pages/analytics_page.dart new file mode 100644 index 0000000..4f16b47 --- /dev/null +++ b/lib/features/analytics/presentation/pages/analytics_page.dart @@ -0,0 +1,343 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:go_router/go_router.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/services/auth_service.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/class_stats.dart'; +import '../../../../core/models/achievement.dart'; +import '../widgets/class_analytics_card.dart'; +import '../widgets/class_ranking_widget.dart'; +import '../widgets/create_achievement_dialog.dart'; + +/// Analytics page for teachers with class breakdowns and rankings +class AnalyticsPage extends StatefulWidget { + const AnalyticsPage({super.key}); + + @override + State createState() => _AnalyticsPageState(); +} + +class _AnalyticsPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + List _classStats = []; + bool _loading = true; + String? _selectedClassId; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + _loadClassStats(); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + Future _loadClassStats() async { + try { + final user = AuthService.currentUser; + if (user == null) return; + + // Obter turmas do professor + final classesSnapshot = await FirebaseFirestore.instance + .collection('classes') + .where('teacherId', isEqualTo: user.uid) + .get(); + + final classStatsList = []; + + for (final classDoc in classesSnapshot.docs) { + final classId = classDoc.id; + final stats = await GamificationService.getClassStats(classId); + if (stats != null) { + classStatsList.add(stats); + } + } + + if (mounted) { + setState(() { + _classStats = classStatsList; + _loading = false; + }); + } + } catch (e) { + print('Error loading class stats: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final themeExtras = AppThemeExtras.of(context); + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: themeExtras.dashboardBackgroundGradient, + stops: themeExtras.dashboardGradientStops, + ), + ), + child: SafeArea( + child: Column( + children: [ + // Header + Container( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => context.go('/teacher-dashboard'), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Analytics', + style: TextStyle( + color: Colors.white, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Acompanhe o desempenho das turmas', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 16, + ), + ), + ], + ), + ), + IconButton( + icon: const Icon(Icons.add, color: Colors.white), + onPressed: _showCreateAchievementDialog, + tooltip: 'Criar Conquista', + ), + ], + ), + const SizedBox(height: 20), + TabBar( + controller: _tabController, + labelColor: Colors.white, + unselectedLabelColor: Colors.white.withValues(alpha: 0.7), + indicatorColor: Colors.white, + indicatorWeight: 2, + tabs: const [ + Tab(text: 'Turmas'), + Tab(text: 'Rankings'), + ], + ), + ], + ), + ), + + // Content + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _buildClassesTab(), + _buildRankingsTab(), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildClassesTab() { + if (_loading) { + return const Center(child: CircularProgressIndicator(color: Colors.white)); + } + + if (_classStats.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.analytics_outlined, + size: 64, + color: Colors.white.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'Nenhuma turma encontrada', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 18, + ), + ), + const SizedBox(height: 8), + Text( + 'Crie turmas para ver as analytics aqui', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 14, + ), + ), + ], + ), + ); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Overview Cards + Row( + children: [ + Expanded( + child: _buildOverviewCard( + 'Total de Alunos', + '${_classStats.fold(0, (sum, stats) => sum + stats.totalStudents)}', + Icons.people, + Colors.blue, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildOverviewCard( + 'Alunos Ativos', + '${_classStats.fold(0, (sum, stats) => sum + stats.activeStudents)}', + Icons.trending_up, + Colors.green, + ), + ), + ], + ), + const SizedBox(height: 20), + + // Class Cards + ..._classStats.map((stats) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClassAnalyticsCard( + classStats: stats, + onTap: () => _showClassRanking(stats), + ), + )), + ], + ), + ); + } + + Widget _buildRankingsTab() { + if (_selectedClassId == null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.leaderboard, + size: 64, + color: Colors.white.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'Selecione uma turma', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 18, + ), + ), + const SizedBox(height: 8), + Text( + 'Clique em uma turma na aba "Turmas" para ver o ranking', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 14, + ), + ), + ], + ), + ); + } + + return ClassRankingWidget(classId: _selectedClassId!); + } + + Widget _buildOverviewCard(String title, String value, IconData icon, Color color) { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.2)), + ), + child: Column( + children: [ + Icon(icon, color: color, size: 32), + const SizedBox(height: 12), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + title, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ).animate().scale(duration: 600.ms, curve: Curves.elasticOut); + } + + void _showClassRanking(ClassStats stats) { + setState(() { + _selectedClassId = stats.classId; + }); + _tabController.animateTo(1); // Mudar para aba de rankings + } + + void _showCreateAchievementDialog() { + showDialog( + context: context, + builder: (context) => CreateAchievementDialog( + onAchievementCreated: (achievement) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Conquista "${achievement.name}" criada com sucesso!'), + backgroundColor: Colors.green, + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/analytics/presentation/widgets/class_analytics_card.dart b/lib/features/analytics/presentation/widgets/class_analytics_card.dart new file mode 100644 index 0000000..807f654 --- /dev/null +++ b/lib/features/analytics/presentation/widgets/class_analytics_card.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +import '../../../../core/models/class_stats.dart'; +import '../../../../core/theme/app_theme_extension.dart'; + +/// Card displaying analytics for a specific class +class ClassAnalyticsCard extends StatelessWidget { + final ClassStats classStats; + final VoidCallback onTap; + + const ClassAnalyticsCard({ + super.key, + required this.classStats, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + cs.primary.withValues(alpha: 0.9), + cs.primary.withValues(alpha: 0.7), + ], + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + classStats.className, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${classStats.activeStudents} de ${classStats.totalStudents} alunos ativos', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.trending_up, color: Colors.white, size: 16), + const SizedBox(width: 4), + Text( + '${(classStats.averageProgress * 100).toInt()}%', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 20), + + // Progress Bar + Container( + height: 8, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(4), + ), + child: FractionallySizedBox( + alignment: Alignment.centerLeft, + widthFactor: classStats.averageProgress.clamp(0.0, 1.0), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + AppThemeExtras.of(context).heroProgressStart, + AppThemeExtras.of(context).heroProgressEnd, + ], + ), + borderRadius: BorderRadius.circular(4), + ), + ), + ), + ), + const SizedBox(height: 20), + + // Stats Grid + Row( + children: [ + Expanded( + child: _buildStatCard( + icon: Icons.quiz, + value: '${classStats.activeQuizzes}', + label: 'Quizzes Ativos', + context: context, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + icon: Icons.description, + value: '${classStats.totalContent}', + label: 'Conteúdos', + context: context, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + icon: Icons.warning, + value: '${classStats.studentsNeedingSupport.length}', + label: 'Precisam Apoio', + context: context, + isWarning: classStats.studentsNeedingSupport.isNotEmpty, + ), + ), + ], + ), + + // Students needing support preview + if (classStats.studentsNeedingSupport.isNotEmpty) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.priority_high, + color: Colors.orange.withValues(alpha: 0.8), + size: 16, + ), + const SizedBox(width: 6), + Text( + 'Alunos que precisam de atenção:', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + ...classStats.studentsNeedingSupport.take(3).map((student) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + children: [ + Container( + width: 6, + height: 6, + decoration: const BoxDecoration( + color: Colors.orange, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + student.studentName, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 11, + ), + ), + ), + Text( + '${student.averageScore.toInt()}%', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 11, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + )), + if (classStats.studentsNeedingSupport.length > 3) + Text( + '+${classStats.studentsNeedingSupport.length - 3} alunos', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 10, + ), + ), + ], + ), + ), + ], + + // Click indicator + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Ver ranking detalhado', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 12, + ), + ), + const SizedBox(width: 4), + Icon( + Icons.arrow_forward, + color: Colors.white.withValues(alpha: 0.8), + size: 12, + ), + ], + ), + ], + ), + ), + ), + ), + ).animate().scale(duration: 600.ms, curve: Curves.elasticOut); + } + + Widget _buildStatCard({ + required IconData icon, + required String value, + required String label, + required BuildContext context, + bool isWarning = false, + }) { + final cs = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Column( + children: [ + Icon( + icon, + color: isWarning ? Colors.orange : Colors.white, + size: 20, + ), + const SizedBox(height: 6), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 10, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ); + } +} diff --git a/lib/features/analytics/presentation/widgets/class_ranking_widget.dart b/lib/features/analytics/presentation/widgets/class_ranking_widget.dart new file mode 100644 index 0000000..9fc81c5 --- /dev/null +++ b/lib/features/analytics/presentation/widgets/class_ranking_widget.dart @@ -0,0 +1,432 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; + +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/class_stats.dart'; +import '../../../../core/theme/app_theme_extension.dart'; + +/// Widget displaying student ranking for a specific class +class ClassRankingWidget extends StatefulWidget { + final String classId; + + const ClassRankingWidget({ + super.key, + required this.classId, + }); + + @override + State createState() => _ClassRankingWidgetState(); +} + +class _ClassRankingWidgetState extends State { + List _rankings = []; + ClassStats? _classStats; + bool _loading = true; + String _searchQuery = ''; + + @override + void initState() { + super.initState(); + _loadRankingData(); + } + + Future _loadRankingData() async { + try { + final results = await Future.wait([ + GamificationService.getClassRanking(widget.classId), + GamificationService.getClassStats(widget.classId), + ]); + + final rankings = results[0] as List; + final classStats = results[1] as ClassStats?; + + if (mounted) { + setState(() { + _rankings = rankings; + _classStats = classStats; + _loading = false; + }); + } + } catch (e) { + print('Error loading ranking data: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + List get _filteredRankings { + if (_searchQuery.isEmpty) return _rankings; + + return _rankings.where((student) => + student.studentName.toLowerCase().contains(_searchQuery.toLowerCase()) || + student.studentEmail.toLowerCase().contains(_searchQuery.toLowerCase()) + ).toList(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + if (_loading) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + + return Container( + margin: const EdgeInsets.all(24), + child: Column( + children: [ + // Header with class info + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, cs.primary.withValues(alpha: 0.8)], + ), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _classStats?.className ?? 'Carregando...', + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${_rankings.length} alunos • Progresso médio: ${((_classStats?.averageProgress ?? 0) * 100).toInt()}%', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.leaderboard, color: Colors.white, size: 16), + const SizedBox(width: 4), + Text( + 'Ranking', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + + // Search bar + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.2)), + ), + child: Row( + children: [ + Icon(Icons.search, color: Colors.white.withValues(alpha: 0.7)), + const SizedBox(width: 12), + Expanded( + child: TextField( + onChanged: (value) { + setState(() { + _searchQuery = value; + }); + }, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Buscar aluno...', + hintStyle: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + ), + border: InputBorder.none, + ), + ), + ), + if (_searchQuery.isNotEmpty) + IconButton( + icon: Icon(Icons.clear, color: Colors.white.withValues(alpha: 0.7)), + onPressed: () { + setState(() { + _searchQuery = ''; + }); + }, + ), + ], + ), + ), + const SizedBox(height: 20), + + // Ranking list + Expanded( + child: _filteredRankings.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: EdgeInsets.zero, + itemCount: _filteredRankings.length, + itemBuilder: (context, index) { + final student = _filteredRankings[index]; + final rankPosition = _rankings.indexOf(student) + 1; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildStudentRankingCard(student, rankPosition), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState() { + if (_searchQuery.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.search_off, + size: 64, + color: Colors.white.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'Nenhum aluno encontrado', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 18, + ), + ), + const SizedBox(height: 8), + Text( + 'Tente buscar com outros termos', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 14, + ), + ), + ], + ), + ); + } + + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.people_outline, + size: 64, + color: Colors.white.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + 'Nenhum aluno na turma', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 18, + ), + ), + const SizedBox(height: 8), + Text( + 'Os alunos aparecerão aqui quando se matricularem', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 14, + ), + ), + ], + ), + ); + } + + Widget _buildStudentRankingCard(StudentRanking student, int rankPosition) { + final cs = Theme.of(context).colorScheme; + + // Determinar cor baseada na posição + Color rankColor; + IconData rankIcon; + + if (rankPosition == 1) { + rankColor = Colors.amber; + rankIcon = Icons.emoji_events; + } else if (rankPosition == 2) { + rankColor = Colors.grey.withValues(alpha: 0.8); + rankIcon = Icons.workspace_premium; + } else if (rankPosition == 3) { + rankColor = Colors.brown.withValues(alpha: 0.8); + rankIcon = Icons.military_tech; + } else { + rankColor = Colors.white.withValues(alpha: 0.7); + rankIcon = Icons.numbers; + } + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withValues(alpha: 0.2)), + ), + child: Row( + children: [ + // Rank position + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: rankColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: rankColor.withValues(alpha: 0.5)), + ), + child: Center( + child: rankPosition <= 3 + ? Icon(rankIcon, color: rankColor, size: 20) + : Text( + '$rankPosition', + style: TextStyle( + color: rankColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 16), + + // Student info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + student.studentName, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + student.studentEmail, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 12, + ), + ), + ], + ), + ), + + // Stats + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Overall score + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getScoreColor(student.overallScore).withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${student.overallScore.toInt()}%', + style: TextStyle( + color: _getScoreColor(student.overallScore), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(height: 4), + + // Quiz completion + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.quiz, + color: Colors.white.withValues(alpha: 0.7), + size: 12, + ), + const SizedBox(width: 4), + Text( + '${student.completedQuizzes}/${student.totalQuizzes}', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + fontSize: 11, + ), + ), + ], + ), + const SizedBox(height: 2), + + // Streak + if (student.currentStreak > 0) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.local_fire_department, + color: Colors.orange.withValues(alpha: 0.8), + size: 12, + ), + const SizedBox(width: 4), + Text( + '${student.currentStreak}d', + style: TextStyle( + color: Colors.orange.withValues(alpha: 0.8), + fontSize: 11, + ), + ), + ], + ), + ], + ), + ], + ), + ).animate().slideX(duration: 300.ms, curve: Curves.easeOut); + } + + Color _getScoreColor(double score) { + if (score >= 80) return Colors.green; + if (score >= 60) return Colors.blue; + if (score >= 40) return Colors.orange; + return Colors.red; + } +} diff --git a/lib/features/analytics/presentation/widgets/create_achievement_dialog.dart b/lib/features/analytics/presentation/widgets/create_achievement_dialog.dart new file mode 100644 index 0000000..58119d2 --- /dev/null +++ b/lib/features/analytics/presentation/widgets/create_achievement_dialog.dart @@ -0,0 +1,633 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/achievement.dart'; + +/// Dialog for creating custom achievements +class CreateAchievementDialog extends StatefulWidget { + final Function(Achievement) onAchievementCreated; + + const CreateAchievementDialog({ + super.key, + required this.onAchievementCreated, + }); + + @override + State createState() => _CreateAchievementDialogState(); +} + +class _CreateAchievementDialogState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _valueController = TextEditingController(); + + String _selectedCategory = 'quiz'; + String _selectedRequirementType = 'quiz_completion'; + String _selectedOperator = '>='; + String _selectedIcon = 'star'; + String _selectedRarity = 'common'; + int _points = 10; + + final List _categories = [ + 'quiz', + 'study_time', + 'streak', + 'concept', + 'general', + ]; + + final List _requirementTypes = [ + 'quiz_completion', + 'quiz_score', + 'study_time', + 'streak_days', + 'concepts_mastered', + ]; + + final List _operators = ['>=', '==', '>']; + + final List _icons = [ + 'star', + 'emoji_events', + 'school', + 'local_fire_department', + 'schedule', + 'trending_up', + 'military_tech', + 'workspace_premium', + 'psychology', + 'lightbulb', + ]; + + final List _rarities = [ + 'common', + 'rare', + 'epic', + 'legendary', + ]; + + final Map _rarityPoints = { + 'common': 10, + 'rare': 25, + 'epic': 50, + 'legendary': 100, + }; + + @override + void dispose() { + _nameController.dispose(); + _descriptionController.dispose(); + _valueController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Dialog( + backgroundColor: cs.surface, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Container( + width: MediaQuery.of(context).size.width * 0.8, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.75, + minWidth: 280, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primary, cs.primary.withValues(alpha: 0.8)], + ), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + child: Row( + children: [ + const Icon(Icons.emoji_events, color: Colors.white, size: 28), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Criar Nova Conquista', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + // Form + Expanded( + child: Form( + key: _formKey, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Basic Info + _buildSectionTitle('Informações Básicas'), + const SizedBox(height: 16), + + _buildTextField( + controller: _nameController, + label: 'Nome da Conquista', + hint: 'Ex: Mestre dos Quizzes', + validator: (value) { + if (value == null || value.isEmpty) { + return 'Campo obrigatório'; + } + return null; + }, + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _descriptionController, + label: 'Descrição', + hint: 'Ex: Complete 10 quizzes com 100% de acerto', + maxLines: 2, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Campo obrigatório'; + } + return null; + }, + ), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDropdownField( + label: 'Categoria', + value: _selectedCategory, + items: _categories, + onChanged: (value) { + setState(() { + _selectedCategory = value!; + }); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildDropdownField( + label: 'Ícone', + value: _selectedIcon, + items: _icons, + onChanged: (value) { + setState(() { + _selectedIcon = value!; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 16), + + // Requirements + _buildSectionTitle('Requisitos'), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDropdownField( + label: 'Tipo de Requisito', + value: _selectedRequirementType, + items: _requirementTypes, + onChanged: (value) { + setState(() { + _selectedRequirementType = value!; + }); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: _buildDropdownField( + label: 'Operador', + value: _selectedOperator, + items: _operators, + onChanged: (value) { + setState(() { + _selectedOperator = value!; + }); + }, + ), + ), + ], + ), + const SizedBox(height: 16), + + _buildTextField( + controller: _valueController, + label: 'Valor', + hint: 'Ex: 10', + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Campo obrigatório'; + } + if (int.tryParse(value) == null) { + return 'Digite um número válido'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Reward + _buildSectionTitle('Recompensa'), + const SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: _buildDropdownField( + label: 'Raridade', + value: _selectedRarity, + items: _rarities, + onChanged: (value) { + setState(() { + _selectedRarity = value!; + _points = _rarityPoints[value]!; + }); + }, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pontos', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 12, + ), + ), + const SizedBox(height: 4), + Text( + '$_points', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + const SizedBox(height: 16), + + // Preview + _buildSectionTitle('Preview'), + const SizedBox(height: 16), + _buildAchievementPreview(), + ], + ), + ), + ), + ), + + // Actions + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 12), + side: BorderSide(color: cs.outline), + ), + child: Text('Cancelar', style: TextStyle(color: cs.onSurface)), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton( + onPressed: _createAchievement, + style: ElevatedButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text('Criar Conquista'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSectionTitle(String title) { + final cs = Theme.of(context).colorScheme; + return Text( + title, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildTextField({ + required TextEditingController controller, + required String label, + required String hint, + int maxLines = 1, + TextInputType? keyboardType, + String? Function(String?)? validator, + }) { + final cs = Theme.of(context).colorScheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + TextFormField( + controller: controller, + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: cs.onSurfaceVariant), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.primary, width: 2), + ), + contentPadding: const EdgeInsets.all(16), + ), + maxLines: maxLines, + keyboardType: keyboardType, + validator: validator, + ), + ], + ); + } + + Widget _buildDropdownField({ + required String label, + required T value, + required List items, + required Function(T?) onChanged, + }) { + final cs = Theme.of(context).colorScheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + isExpanded: true, + value: value, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.primary, width: 2), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + ), + items: items.map((item) { + return DropdownMenuItem( + value: item, + child: Text( + item.toString().split('_').map((word) => + word[0].toUpperCase() + word.substring(1) + ).join(' '), + style: const TextStyle(fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ); + }).toList(), + onChanged: onChanged, + ), + ], + ); + } + + Widget _buildAchievementPreview() { + final cs = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + cs.primary.withValues(alpha: 0.1), + cs.primary.withValues(alpha: 0.05), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.primary.withValues(alpha: 0.3)), + ), + child: Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: _getRarityColor().withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: _getRarityColor().withValues(alpha: 0.5)), + ), + child: Icon( + _getIconData(), + color: _getRarityColor(), + size: 24, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _nameController.text.isEmpty ? 'Nome da Conquista' : _nameController.text, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + _descriptionController.text.isEmpty + ? 'Descrição da conquista' + : _descriptionController.text, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: _getRarityColor().withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$_points pts', + style: TextStyle( + color: _getRarityColor(), + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + } + + IconData _getIconData() { + switch (_selectedIcon) { + case 'emoji_events': return Icons.emoji_events; + case 'school': return Icons.school; + case 'local_fire_department': return Icons.local_fire_department; + case 'schedule': return Icons.schedule; + case 'trending_up': return Icons.trending_up; + case 'military_tech': return Icons.military_tech; + case 'workspace_premium': return Icons.workspace_premium; + case 'psychology': return Icons.psychology; + case 'lightbulb': return Icons.lightbulb; + default: return Icons.star; + } + } + + Color _getRarityColor() { + switch (_selectedRarity) { + case 'common': return Colors.grey; + case 'rare': return Colors.blue; + case 'epic': return Colors.purple; + case 'legendary': return Colors.orange; + default: return Colors.grey; + } + } + + Future _createAchievement() async { + if (!_formKey.currentState!.validate()) return; + + try { + final user = FirebaseAuth.instance.currentUser; + if (user == null) return; + + final achievement = Achievement( + id: '', // Will be generated by Firestore + name: _nameController.text, + description: _descriptionController.text, + icon: _selectedIcon, + category: _selectedCategory, + requirements: AchievementRequirement( + type: _selectedRequirementType, + value: int.parse(_valueController.text), + operator: _selectedOperator, + ), + points: _points, + rarity: _selectedRarity, + isActive: true, + createdAt: DateTime.now(), + createdBy: user.uid, + ); + + final achievementId = await GamificationService.createCustomAchievement( + teacherId: user.uid, + name: achievement.name, + description: achievement.description, + icon: achievement.icon, + category: achievement.category, + requirements: achievement.requirements, + points: achievement.points, + rarity: achievement.rarity, + ); + + final createdAchievement = achievement.copyWith(id: achievementId); + + widget.onAchievementCreated(createdAchievement); + Navigator.of(context).pop(); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao criar conquista: $e'), + backgroundColor: Colors.red, + ), + ); + } + } +} diff --git a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart index 1c7a564..bcbe3ca 100644 --- a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart +++ b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart @@ -3,13 +3,75 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme_extension.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../../core/services/auth_service.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/user_stats.dart'; +import '../../../../core/models/achievement.dart'; /// Profile section with user info and achievements -class ProfileSectionWidget extends StatelessWidget { +class ProfileSectionWidget extends StatefulWidget { const ProfileSectionWidget({super.key}); + @override + State createState() => _ProfileSectionWidgetState(); +} + +class _ProfileSectionWidgetState extends State { + UserStats? _userStats; + List _recentAchievements = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadUserData(); + } + + Future _loadUserData() async { + try { + final user = AuthService.currentUser; + if (user != null) { + final results = await Future.wait([ + GamificationService.getUserStats(user.uid), + GamificationService.getAvailableAchievements(), + ]); + + final stats = results[0] as UserStats?; + final achievements = results[1] as List; + + // Obter conquistas desbloqueadas recentemente + final unlockedAchievementIds = stats?.unlockedAchievements + .map((ua) => ua.achievementId) + .toSet() ?? {}; + + final recentUnlocked = achievements + .where((a) => unlockedAchievementIds.contains(a.id)) + .take(4) + .toList(); + + if (mounted) { + setState(() { + _userStats = stats; + _recentAchievements = recentUnlocked; + _loading = false; + }); + } + } + } catch (e) { + print('Error loading user data: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + @override Widget build(BuildContext context) { + if (_loading) { + return const Center(child: CircularProgressIndicator()); + } + final user = AuthService.currentUser; final userName = user?.displayName ?? 'Estudante'; final userEmail = user?.email ?? ''; @@ -124,36 +186,55 @@ class ProfileSectionWidget extends StatelessWidget { ), const SizedBox(height: 12), - // Achievement Badges - Row( - children: [ - _buildAchievementBadge( - icon: Icons.local_fire_department, - label: '7 dias', - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 12), - _buildAchievementBadge( - icon: Icons.school, - label: '3 conceitos', - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 12), - _buildAchievementBadge( - icon: Icons.speed, - label: 'Rápido', - color: Theme.of( + // Achievement List (Teacher-style design) + if (_recentAchievements.isNotEmpty) ...[ + ..._recentAchievements.map((achievement) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildAchievementItem( context, - ).colorScheme.primary.withOpacity(0.8), + achievement.name, + achievement.points, + _getRarityColor(achievement.rarity), + _getIconData(achievement.icon), + ), + ); + }), + ] else ...[ + // Streak item + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildAchievementItem( + context, + '${_userStats?.currentStreak ?? 0} dias seguidos', + (_userStats?.currentStreak ?? 0) * 5, + Theme.of(context).colorScheme.secondary, + Icons.local_fire_department, ), - const SizedBox(width: 12), - _buildAchievementBadge( - icon: Icons.star, - label: '100%', - color: Theme.of(context).colorScheme.tertiary, + ), + // Concepts item + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildAchievementItem( + context, + '${_userStats?.masteredConcepts.length ?? 0} conceitos dominados', + (_userStats?.masteredConcepts.length ?? 0) * 10, + Theme.of(context).colorScheme.primary, + Icons.school, ), - ], - ), + ), + if (_userStats != null && _userStats!.totalStudyTime > 0) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildAchievementItem( + context, + '${(_userStats!.totalStudyTime / 60).toStringAsFixed(1)}h estudadas', + (_userStats!.totalStudyTime ~/ 60) * 3, + Theme.of(context).colorScheme.tertiary, + Icons.schedule, + ), + ), + ], const SizedBox(height: 20), // Recent Activity Summary @@ -183,7 +264,7 @@ class ProfileSectionWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Ótimo progresso!', + _getProgressMessage(), style: TextStyle( color: Theme.of(context).colorScheme.onSurface, fontSize: 14, @@ -192,7 +273,7 @@ class ProfileSectionWidget extends StatelessWidget { ), const SizedBox(height: 2), Text( - 'Você está 15% acima da média esta semana', + _getProgressComparison(), style: TextStyle( color: Theme.of( context, @@ -217,32 +298,103 @@ class ProfileSectionWidget extends StatelessWidget { .then(delay: const Duration(milliseconds: 400)); } - Widget _buildAchievementBadge({ - required IconData icon, - required String label, - required Color color, - }) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - decoration: BoxDecoration( - color: color.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: color.withOpacity(0.3), width: 1), - ), - child: Column( - children: [ - Icon(icon, color: color, size: 16), - const SizedBox(height: 4), - Text( - label, + Widget _buildAchievementItem( + BuildContext context, + String name, + int points, + Color color, + IconData icon, + ) { + return Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Center( + child: Icon(icon, color: color, size: 16), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$points pts', style: TextStyle( color: color, - fontSize: 10, + fontSize: 12, fontWeight: FontWeight.bold, ), ), - ], - ), + ), + ], ); } + + IconData _getIconData(String iconName) { + switch (iconName) { + case 'emoji_events': return Icons.emoji_events; + case 'school': return Icons.school; + case 'local_fire_department': return Icons.local_fire_department; + case 'schedule': return Icons.schedule; + case 'trending_up': return Icons.trending_up; + case 'military_tech': return Icons.military_tech; + case 'workspace_premium': return Icons.workspace_premium; + case 'psychology': return Icons.psychology; + case 'lightbulb': return Icons.lightbulb; + case 'star': return Icons.star; + case 'speed': return Icons.speed; + default: return Icons.star; + } + } + + Color _getRarityColor(String rarity) { + switch (rarity) { + case 'common': return Colors.grey; + case 'rare': return Colors.blue; + case 'epic': return Colors.purple; + case 'legendary': return Colors.orange; + default: return Colors.grey; + } + } + + String _getProgressMessage() { + if (_userStats == null) return 'Continue estudando!'; + + final streak = _userStats!.currentStreak; + final studyTime = _userStats!.totalStudyTime; + + if (streak >= 7) return 'Incrível streak! 🔥'; + if (studyTime >= 300) return 'Dedicação exemplar! 📚'; + if (streak >= 3) return 'Bom progresso! 📈'; + return 'Continue estudando! 💪'; + } + + String _getProgressComparison() { + if (_userStats == null) return 'Comece sua jornada de estudos'; + + final weeklyTime = _userStats!.weeklyStudyTime; + final concepts = _userStats!.masteredConcepts.length; + + if (weeklyTime >= 180) return 'Você está 15% acima da média esta semana'; + if (concepts >= 3) return 'Domine $concepts conceitos esta semana'; + if (weeklyTime >= 60) return 'Bom tempo de estudo esta semana'; + return 'Continue assim para subir no ranking!'; + } } diff --git a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart index bf158c8..589df15 100644 --- a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart @@ -2,30 +2,81 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/user_stats.dart'; +import '../../../../core/services/auth_service.dart'; /// Progress tracking hero section for student dashboard -class ProgressHeroWidget extends StatelessWidget { +class ProgressHeroWidget extends StatefulWidget { final String userName; - final double overallProgress; - final List masteredConcepts; - final int studyTimeMinutes; - final int streakDays; const ProgressHeroWidget({ super.key, required this.userName, - this.overallProgress = 0.65, - this.masteredConcepts = const [ - 'Fundamentos de Programação', - 'Algoritmos Básicos', - 'Estruturas de Dados', - ], - this.studyTimeMinutes = 245, - this.streakDays = 7, }); + @override + State createState() => _ProgressHeroWidgetState(); +} + +class _ProgressHeroWidgetState extends State { @override Widget build(BuildContext context) { + return FutureBuilder( + future: _loadUserStats(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return _buildLoadingState(); + } + + if (snapshot.hasError) { + return _buildErrorState(); + } + + final userStats = snapshot.data; + return _buildContent(userStats); + }, + ); + } + + Future _loadUserStats() async { + try { + final user = AuthService.currentUser; + if (user != null) { + return await GamificationService.getUserStats(user.uid); + } + return null; + } catch (e) { + print('Error loading user stats: $e'); + return null; + } + } + + double _calculateOverallProgress(UserStats? userStats) { + if (userStats == null || userStats.masteredConcepts.isEmpty) { + return 0.0; + } + final totalMastery = userStats.masteredConcepts + .map((c) => c.masteryLevel) + .reduce((a, b) => a + b); + return totalMastery / (userStats.masteredConcepts.length * 100); + } + + Widget _buildLoadingState() { + return const Center(child: CircularProgressIndicator()); + } + + Widget _buildErrorState() { + return const Center(child: Text('Erro ao carregar dados')); + } + + Widget _buildContent(UserStats? userStats) { + + final streakDays = userStats?.currentStreak ?? 0; + final overallProgress = _calculateOverallProgress(userStats); + final masteredConcepts = userStats?.masteredConcepts.map((c) => c.conceptName).toList() ?? []; + final studyTimeMinutes = userStats?.totalStudyTime ?? 0; + return Container( margin: const EdgeInsets.only(bottom: 24), child: Column( @@ -48,7 +99,7 @@ class ProgressHeroWidget extends StatelessWidget { ), const SizedBox(height: 4), Text( - 'Continue assim, $userName!', + 'Continue assim, ${widget.userName}!', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 16, diff --git a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart index 0da0dc0..e29b7ba 100644 --- a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart +++ b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart @@ -22,23 +22,34 @@ class QuickAccessWidget extends StatelessWidget { ), ), const SizedBox(height: 16), - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - flex: 3, - child: _buildTutorIACard(context), + Column( + children: [ + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + flex: 2, + child: _buildTutorIACard(context), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: _buildQuizCard(context), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: _buildAchievementsCard(context), + ), + ], ), - const SizedBox(width: 16), - Expanded( - flex: 2, - child: _buildQuizCard(context), - ), - ], - ), + ), + ], ), const SizedBox(height: 16), + _buildQuizManagementCard(context), + const SizedBox(height: 16), _buildJoinClassCard(context), ], ) @@ -85,6 +96,42 @@ class QuickAccessWidget extends StatelessWidget { .then(delay: const Duration(milliseconds: 200)); } + Widget _buildAchievementsCard(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return DashboardActionCardSurface( + title: 'Conquistas', + subtitle: 'Ver medals', + icon: Icons.emoji_events, + minHeight: 150, + iconColor: Colors.amber, + onTap: () => context.go('/student/achievements'), + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 200)); + } + + Widget _buildQuizManagementCard(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return DashboardActionCardSurface( + title: 'Gerenciar Quizzes', + subtitle: 'Ver histórico ou eliminar', + icon: Icons.manage_history, + minHeight: 80, + iconColor: cs.tertiary, + onTap: () => context.go('/quiz-management'), + ) + .animate() + .slideY( + duration: const Duration(milliseconds: 800), + curve: Curves.easeOut, + ) + .then(delay: const Duration(milliseconds: 200)); + } + Widget _buildJoinClassCard(BuildContext context) { return DashboardActionCard( title: 'Entrar numa Turma', 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 118de8b..9bf89f8 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart @@ -1,15 +1,95 @@ import 'package:flutter/material.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../../core/theme/app_theme_extension.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../../core/services/auth_service.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/class_stats.dart'; /// Analytics preview section for teacher dashboard -class TeacherAnalyticsPreviewWidget extends StatelessWidget { +class TeacherAnalyticsPreviewWidget extends StatefulWidget { const TeacherAnalyticsPreviewWidget({super.key}); + @override + State createState() => _TeacherAnalyticsPreviewWidgetState(); +} + +class _TeacherAnalyticsPreviewWidgetState extends State { + List _classStats = []; + List _topStudents = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadAnalyticsData(); + } + + Future _loadAnalyticsData() async { + try { + final user = AuthService.currentUser; + if (user == null) return; + + // Obter turmas do professor + final classesSnapshot = await FirebaseFirestore.instance + .collection('classes') + .where('teacherId', isEqualTo: user.uid) + .get(); + + final classStatsList = []; + + for (final classDoc in classesSnapshot.docs) { + final classId = classDoc.id; + final stats = await GamificationService.getClassStats(classId); + if (stats != null) { + classStatsList.add(stats); + } + } + + // Obter melhores alunos de todas as turmas + final allTopStudents = []; + for (final classStats in classStatsList) { + final ranking = await GamificationService.getClassRanking(classStats.classId); + allTopStudents.addAll(ranking.take(3)); // Top 3 de cada turma + } + + // Ordenar por score e pegar os melhores + allTopStudents.sort((a, b) => b.overallScore.compareTo(a.overallScore)); + final topStudents = allTopStudents.take(4).toList(); + + if (mounted) { + setState(() { + _classStats = classStatsList; + _topStudents = topStudents; + _loading = false; + }); + } + } catch (e) { + print('Error loading analytics data: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + int get totalStudents => _classStats.fold(0, (sum, stats) => sum + stats.totalStudents); + int get activeStudents => _classStats.fold(0, (sum, stats) => sum + stats.activeStudents); + double get averageProgress { + if (_classStats.isEmpty) return 0.0; + final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress); + return totalProgress / _classStats.length; + } + int get studentsNeedingSupport => _classStats.fold(0, (sum, stats) => sum + stats.studentsNeedingSupport.length); + @override Widget build(BuildContext context) { + if (_loading) { + return const Center(child: CircularProgressIndicator()); + } + final user = AuthService.currentUser; final userName = user?.displayName ?? 'Professor'; final userEmail = user?.email ?? ''; @@ -109,24 +189,24 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { _buildQuickStat( icon: Icons.check_circle, label: 'Alunos Ativos', - value: '18/24', + value: '$activeStudents/$totalStudents', color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 12), _buildQuickStat( icon: Icons.warning_amber, label: 'Precisam Apoio', - value: '3', + value: '$studentsNeedingSupport', color: Theme.of(context).colorScheme.secondary, ), const SizedBox(width: 12), _buildQuickStat( icon: Icons.emoji_events, label: 'Média Turma', - value: '72%', + value: '${(averageProgress * 100).toInt()}%', color: Theme.of( context, - ).colorScheme.primary.withOpacity(0.8), + ).colorScheme.primary.withValues(alpha: 0.8), ), ], ), @@ -154,33 +234,17 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { const SizedBox(height: 12), // Student List Preview - _buildStudentPerformanceItem( - context, - 'Ana Silva', - 95, - Theme.of(context).colorScheme.tertiary, - ), - const SizedBox(height: 8), - _buildStudentPerformanceItem( - context, - 'João Costa', - 88, - Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 8), - _buildStudentPerformanceItem( - context, - 'Maria Santos', - 82, - Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 8), - _buildStudentPerformanceItem( - context, - 'Pedro Lima', - 45, - Theme.of(context).colorScheme.secondary, - ), + ..._topStudents.map((student) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _buildStudentPerformanceItem( + context, + student.studentName, + student.overallScore.toInt(), + _getScoreColor(student.overallScore), + ), + ); + }), const SizedBox(height: 20), @@ -218,7 +282,7 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { ), const SizedBox(height: 2), Text( - '12 conteúdos verificados • 2 pendentes de revisão', + '${_classStats.fold(0, (sum, stats) => sum + stats.totalContent)} conteúdos • $studentsNeedingSupport alunos precisam de apoio', style: TextStyle( color: Theme.of( context, @@ -336,4 +400,11 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { ], ); } + + Color _getScoreColor(double score) { + if (score >= 80) return Theme.of(context).colorScheme.tertiary; + if (score >= 60) return Theme.of(context).colorScheme.primary; + if (score >= 40) return Theme.of(context).colorScheme.secondary; + return Theme.of(context).colorScheme.error; + } } diff --git a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart index ffe0a1a..9cd85c2 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart @@ -1,27 +1,89 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/services/gamification_service.dart'; +import '../../../../core/models/class_stats.dart'; +import '../../../../core/services/auth_service.dart'; /// Hero section for teacher dashboard showing class overview -class TeacherHeroWidget extends StatelessWidget { +class TeacherHeroWidget extends StatefulWidget { final String userName; - final int totalStudents; - final int activeQuizzes; - final int uploadedContent; - final double classAverageProgress; const TeacherHeroWidget({ super.key, required this.userName, - this.totalStudents = 24, - this.activeQuizzes = 3, - this.uploadedContent = 12, - this.classAverageProgress = 0.72, }); @override + State createState() => _TeacherHeroWidgetState(); +} + +class _TeacherHeroWidgetState extends State { + List _classStats = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _loadClassStats(); + } + + Future _loadClassStats() async { + try { + final user = AuthService.currentUser; + if (user == null) return; + + // Obter turmas do professor + final classesSnapshot = await FirebaseFirestore.instance + .collection('classes') + .where('teacherId', isEqualTo: user.uid) + .get(); + + final classStatsList = []; + + for (final classDoc in classesSnapshot.docs) { + final classId = classDoc.id; + final stats = await GamificationService.getClassStats(classId); + if (stats != null) { + classStatsList.add(stats); + } + } + + if (mounted) { + setState(() { + _classStats = classStatsList; + _loading = false; + }); + } + } catch (e) { + print('Error loading class stats: $e'); + if (mounted) { + setState(() { + _loading = false; + }); + } + } + } + + int get totalStudents => _classStats.fold(0, (sum, stats) => sum + stats.totalStudents); + int get activeQuizzes => _classStats.fold(0, (sum, stats) => sum + stats.activeQuizzes); + int get uploadedContent => _classStats.fold(0, (sum, stats) => sum + stats.totalContent); + double get classAverageProgress { + if (_classStats.isEmpty) return 0.0; + final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress); + return totalProgress / _classStats.length; + } + Widget build(BuildContext context) { + if (_loading) { + return Container( + margin: const EdgeInsets.only(bottom: 24), + child: const Center(child: CircularProgressIndicator()), + ); + } + return Container( margin: const EdgeInsets.only(bottom: 24), child: Column( @@ -231,26 +293,7 @@ class TeacherHeroWidget extends StatelessWidget { ], ), const SizedBox(height: 12), - _buildActivityItem( - context, - '15 alunos completaram o quiz de Derivadas', - 'Hoje, 14:30', - Theme.of(context).colorScheme.primary, - ), - const SizedBox(height: 8), - _buildActivityItem( - context, - 'Novo conteúdo: Regra da Cadeia', - 'Ontem, 09:15', - Theme.of(context).colorScheme.secondary, - ), - const SizedBox(height: 8), - _buildActivityItem( - context, - '3 alunos precisam de apoio em Limites', - 'Ontem, 16:45', - Theme.of(context).colorScheme.error, - ), + ..._buildRecentActivities(), ], ), ) @@ -302,6 +345,67 @@ class TeacherHeroWidget extends StatelessWidget { ); } + List _buildRecentActivities() { + final activities = []; + + if (_classStats.isEmpty) { + activities.add(_buildActivityItem( + context, + 'Nenhuma atividade recente', + 'Comece criando turmas e conteúdos', + Theme.of(context).colorScheme.onSurfaceVariant, + )); + return activities; + } + + // Adicionar atividades baseadas nas estatísticas das turmas + for (final stats in _classStats.take(3)) { + if (stats.activeQuizzes > 0) { + activities.add(_buildActivityItem( + context, + '${stats.activeQuizzes} quizzes ativos em ${stats.className}', + 'Recente', + Theme.of(context).colorScheme.primary, + )); + activities.add(const SizedBox(height: 8)); + } + + if (stats.studentsNeedingSupport.isNotEmpty) { + activities.add(_buildActivityItem( + context, + '${stats.studentsNeedingSupport.length} alunos precisam de apoio em ${stats.className}', + 'Ver analytics', + Theme.of(context).colorScheme.error, + )); + activities.add(const SizedBox(height: 8)); + } + + if (stats.totalContent > 0) { + activities.add(_buildActivityItem( + context, + '${stats.totalContent} conteúdos disponíveis em ${stats.className}', + 'Atualizado', + Theme.of(context).colorScheme.secondary, + )); + activities.add(const SizedBox(height: 8)); + } + } + + // Remover o último SizedBox se existir + if (activities.isNotEmpty && activities.last is SizedBox) { + activities.removeLast(); + } + + return activities.isEmpty ? [ + _buildActivityItem( + context, + 'Nenhuma atividade recente', + 'Comece criando turmas e conteúdos', + Theme.of(context).colorScheme.onSurfaceVariant, + ) + ] : activities; + } + Widget _buildActivityItem( BuildContext context, String text, @@ -333,7 +437,7 @@ class TeacherHeroWidget extends StatelessWidget { style: TextStyle( color: Theme.of( context, - ).colorScheme.onSurfaceVariant.withOpacity(0.7), + ).colorScheme.onSurfaceVariant.withValues(alpha: 0.7), fontSize: 12, ), ), diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index eb051c9..38595cd 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -6,6 +6,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import '../../../../core/services/materials_rag_service.dart'; import '../../../../core/services/rag_ai_service.dart'; +import '../../../../core/services/gamification_service.dart'; import '../../../../core/utils/logger.dart'; class QuizListPage extends StatefulWidget { @@ -423,6 +424,7 @@ class _QuizListPageState extends State title: name, quizId: quizId, questions: questions, + materialName: name, ), ); } @@ -1162,10 +1164,12 @@ class _TeacherQuizInteractiveSheet extends StatefulWidget { final String title; final String quizId; final List<_QuizQuestion> questions; + final String? materialName; const _TeacherQuizInteractiveSheet({ required this.title, required this.quizId, required this.questions, + this.materialName, }); @override @@ -1229,6 +1233,14 @@ class _TeacherQuizInteractiveSheetState 'total': widget.questions.length, 'submittedAt': FieldValue.serverTimestamp(), }); + + // Registrar atividade no sistema de gamificação + await GamificationService.recordQuizActivity( + user.uid, + score: _score, + totalQuestions: widget.questions.length, + materialName: widget.materialName ?? 'Quiz', + ); } } catch (e) { Logger.error('Error submitting teacher quiz result: $e'); diff --git a/lib/features/quiz/presentation/pages/quiz_management_page.dart b/lib/features/quiz/presentation/pages/quiz_management_page.dart new file mode 100644 index 0000000..39ae49d --- /dev/null +++ b/lib/features/quiz/presentation/pages/quiz_management_page.dart @@ -0,0 +1,458 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/services/auth_service.dart'; +import '../../../../core/utils/logger.dart'; + +/// Página para gerenciar quizzes (eliminar quizzes feitos) +class QuizManagementPage extends StatefulWidget { + const QuizManagementPage({super.key}); + + @override + State createState() => _QuizManagementPageState(); +} + +class _QuizManagementPageState extends State { + List> _quizHistory = []; + bool _loading = true; + String _userRole = ''; + + @override + void initState() { + super.initState(); + _loadUserRole(); + _loadQuizHistory(); + } + + Future _loadUserRole() async { + final user = AuthService.currentUser; + if (user != null) { + final userDoc = await FirebaseFirestore.instance + .collection('users') + .doc(user.uid) + .get(); + + if (userDoc.exists) { + setState(() { + _userRole = userDoc.data()?['role'] ?? ''; + }); + } + } + } + + Future _loadQuizHistory() async { + try { + final user = AuthService.currentUser; + if (user == null) return; + + Query query; + + if (_userRole == 'teacher') { + // Professor: ver todos os quizzes criados + query = FirebaseFirestore.instance + .collection('teacherQuizzes') + .where('teacherId', isEqualTo: user.uid) + .orderBy('createdAt', descending: true); + } else { + // Aluno: ver quizzes criados pelo próprio aluno + conceitos dominados + final results = await Future.wait([ + FirebaseFirestore.instance.collection('userStats').doc(user.uid).get(), + FirebaseFirestore.instance + .collection('quizHistory') + .doc(user.uid) + .collection('quizzes') + .orderBy('createdAt', descending: true) + .get(), + ]); + + final userStatsSnapshot = results[0] as DocumentSnapshot; + final studentQuizzesSnapshot = results[1] as QuerySnapshot; + + List> quizList = []; + + // Adicionar quizzes criados pelo aluno + for (final doc in studentQuizzesSnapshot.docs) { + final data = Map.from(doc.data() as Map); + data['id'] = doc.id; + data['title'] = data['materialName'] ?? 'Quiz sem nome'; + data['type'] = 'created'; + quizList.add(data); + } + + // Adicionar conceitos dominados + if (userStatsSnapshot.exists) { + final stats = userStatsSnapshot.data() as Map?; + if (stats != null) { + final masteredConcepts = (stats['masteredConcepts'] as List?) + ?.map((c) => Map.from(c as Map)) + .toList() ?? []; + + for (final concept in masteredConcepts) { + quizList.add({ + 'id': concept['conceptName'] ?? '', + 'title': concept['conceptName'] ?? 'Conceito sem nome', + 'score': concept['masteryLevel'] ?? 0, + 'totalQuestions': 100, // Simulado + 'completedAt': concept['masteredAt'] ?? Timestamp.now(), + 'type': 'concept', + }); + } + } + } + + setState(() { + _quizHistory = quizList; + _loading = false; + }); + return; + } + + final snapshot = await query.get(); + + setState(() { + _quizHistory = snapshot.docs.map((doc) { + final data = Map.from(doc.data() as Map); + data['id'] = doc.id; + return data; + }).cast>().toList(); + _loading = false; + }); + } catch (e) { + Logger.error('Error loading quiz history: $e'); + setState(() { + _loading = false; + }); + } + } + + Future _deleteQuiz(String quizId, String quizTitle, String type) async { + try { + String confirmMessage; + String successMessage; + + if (_userRole == 'teacher') { + confirmMessage = 'Tem certeza que deseja eliminar o quiz "$quizTitle"? Esta ação não pode ser desfeita.'; + successMessage = 'Quiz eliminado com sucesso!'; + } else { + if (type == 'created') { + confirmMessage = 'Tem certeza que deseja eliminar seu quiz "$quizTitle"?'; + successMessage = 'Quiz eliminado com sucesso!'; + } else { + confirmMessage = 'Tem certeza que deseja remover o conceito "$quizTitle" do seu histórico?'; + successMessage = 'Conceito removido com sucesso!'; + } + } + + final confirmed = await _showDeleteConfirmation(quizTitle, confirmMessage); + if (!confirmed) return; + + if (_userRole == 'teacher') { + // Professor: eliminar quiz criado + await FirebaseFirestore.instance + .collection('teacherQuizzes') + .doc(quizId) + .delete(); + + // Também eliminar do histórico de alunos + final historySnapshot = await FirebaseFirestore.instance + .collection('quizHistory') + .where('quizId', isEqualTo: quizId) + .get(); + + for (final doc in historySnapshot.docs) { + await doc.reference.delete(); + } + } else { + // Aluno: remover quiz criado ou conceito dominado + if (type == 'created') { + // Remover quiz criado pelo aluno + final user = AuthService.currentUser; + if (user != null) { + await FirebaseFirestore.instance + .collection('quizHistory') + .doc(user.uid) + .collection('quizzes') + .doc(quizId) + .delete(); + } + } else { + // Remover conceito dominado + final user = AuthService.currentUser; + if (user != null) { + final userStatsDoc = await FirebaseFirestore.instance + .collection('userStats') + .doc(user.uid) + .get(); + + if (userStatsDoc.exists) { + final userStats = userStatsDoc.data() as Map; + final masteredConcepts = (userStats['masteredConcepts'] as List?) + ?.map((c) => Map.from(c as Map)) + .toList() ?? []; + + // Encontrar o conceito específico para remover + final conceptToRemove = masteredConcepts.firstWhere( + (c) => c['conceptName'] == quizId, + orElse: () => {}, + ); + + if (conceptToRemove.isNotEmpty) { + await FirebaseFirestore.instance + .collection('userStats') + .doc(user.uid) + .update({ + 'masteredConcepts': FieldValue.arrayRemove([conceptToRemove]) + }); + } + } + } + } + } + + _loadQuizHistory(); // Recarregar lista + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(successMessage), + backgroundColor: Colors.green, + ), + ); + } + } catch (e) { + Logger.error('Error deleting quiz: $e'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao eliminar: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + Future _showDeleteConfirmation(String quizTitle, String message) async { + final result = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(_userRole == 'teacher' ? 'Eliminar Quiz' : 'Confirmar Eliminação'), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom(backgroundColor: Colors.red), + child: const Text('Eliminar'), + ), + ], + ), + ); + return result ?? false; + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + title: Text(_userRole == 'teacher' ? 'Gerenciar Quizzes' : 'Meu Histórico'), + backgroundColor: cs.surface, + foregroundColor: cs.onSurface, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go(_userRole == 'teacher' + ? '/teacher-dashboard' + : '/student-dashboard'), + ), + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + cs.primary.withValues(alpha: 0.05), + cs.surface, + ], + ), + ), + child: _loading + ? const Center(child: CircularProgressIndicator()) + : _quizHistory.isEmpty + ? _buildEmptyState() + : ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: _quizHistory.length, + itemBuilder: (context, index) { + final quiz = _quizHistory[index]; + return _buildQuizCard(quiz) + .animate() + .slideX(duration: const Duration(milliseconds: 300)) + .then(delay: Duration(milliseconds: index * 50)); + }, + ), + ), + ); + } + + Widget _buildEmptyState() { + final cs = Theme.of(context).colorScheme; + + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + _userRole == 'teacher' ? Icons.quiz_outlined : Icons.history_edu_outlined, + size: 64, + color: cs.onSurfaceVariant, + ), + const SizedBox(height: 16), + Text( + _userRole == 'teacher' ? 'Nenhum quiz criado' : 'Nenhum quiz no histórico', + style: TextStyle( + color: cs.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + _userRole == 'teacher' + ? 'Crie seu primeiro quiz para começar!' + : 'Complete alguns quizzes para ver seu histórico aqui.', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + Widget _buildQuizCard(Map quiz) { + final cs = Theme.of(context).colorScheme; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withValues(alpha: 0.2)), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + quiz['title'] ?? 'Quiz sem título', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + if (_userRole == 'student' && quiz['score'] != null) ...[ + Text( + 'Pontuação: ${quiz['score']}/${quiz['totalQuestions'] ?? '?'}', + style: TextStyle( + color: cs.primary, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + Text( + _userRole == 'teacher' || quiz['type'] == 'created' + ? 'Criado em: ${_formatDate(quiz['createdAt'])}' + : 'Completado em: ${_formatDate(quiz['completedAt'])}', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => _deleteQuiz(quiz['id'], quiz['title'] ?? 'Quiz', quiz['type'] ?? 'unknown'), + icon: Icon( + Icons.delete_outline, + color: Colors.red, + ), + tooltip: _userRole == 'teacher' ? 'Eliminar Quiz' : 'Remover', + ), + ], + ), + if (_userRole == 'teacher' && quiz['classIds'] != null) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 4, + children: (quiz['classIds'] as List).map((classId) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: cs.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Turma: $classId', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 10, + ), + ), + ); + }).toList(), + ), + ], + ], + ), + ); + } + + String _formatDate(dynamic date) { + if (date == null) return 'Data desconhecida'; + + DateTime dateTime; + if (date is Timestamp) { + dateTime = date.toDate(); + } else if (date is DateTime) { + dateTime = date; + } else { + return 'Data desconhecida'; + } + + return '${dateTime.day}/${dateTime.month}/${dateTime.year}'; + } +}