From e388ca3b6742269d187451429395a1f241de682b Mon Sep 17 00:00:00 2001 From: 240405 <240405@epvc.pt> Date: Sun, 17 May 2026 19:42:49 +0100 Subject: [PATCH] =?UTF-8?q?Muitas=20coisas=20e=20j=C3=A1=20me=20esqueci=20?= =?UTF-8?q?delas=20todas,=20cenas=20principalmente=20no=20dashboard=20do?= =?UTF-8?q?=20aluno=20bug=20fixes=20e=20etc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/student_achievements_page.dart | 195 +++--- .../presentation/pages/analytics_page.dart | 60 +- .../widgets/class_ranking_widget.dart | 50 +- .../auth/presentation/pages/signup_page.dart | 22 +- .../pages/class_students_page.dart | 8 +- .../presentation/pages/join_class_page.dart | 36 +- .../pages/teacher_all_classes_page.dart | 9 +- .../widgets/dashboard_action_card.dart | 8 +- .../widgets/profile_section_widget.dart | 75 ++- .../widgets/progress_hero_widget.dart | 20 +- .../widgets/quick_access_widget.dart | 445 ++++++++++--- .../widgets/student_classes_list_widget.dart | 430 ++++++++++-- .../teacher_analytics_preview_widget.dart | 477 ++++++-------- .../widgets/teacher_classes_list_widget.dart | 6 +- .../widgets/teacher_hero_widget.dart | 211 +++--- .../widgets/teacher_quick_actions_widget.dart | 243 +++---- .../pages/teacher_materials_page.dart | 79 +-- .../presentation/pages/quiz_list_page.dart | 46 +- .../pages/quiz_management_page.dart | 178 ++--- .../presentation/pages/teacher_quiz_page.dart | 615 +++++++++++++----- 20 files changed, 1989 insertions(+), 1224 deletions(-) diff --git a/lib/features/achievements/presentation/pages/student_achievements_page.dart b/lib/features/achievements/presentation/pages/student_achievements_page.dart index f6777ea..f17cac7 100644 --- a/lib/features/achievements/presentation/pages/student_achievements_page.dart +++ b/lib/features/achievements/presentation/pages/student_achievements_page.dart @@ -13,7 +13,8 @@ class StudentAchievementsPage extends StatefulWidget { const StudentAchievementsPage({super.key}); @override - State createState() => _StudentAchievementsPageState(); + State createState() => + _StudentAchievementsPageState(); } class _StudentAchievementsPageState extends State { @@ -82,66 +83,45 @@ class _StudentAchievementsPageState extends State { return Scaffold( backgroundColor: cs.surface, + appBar: AppBar( + backgroundColor: cs.primary, + foregroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go('/student-dashboard'), + ), + title: const Text( + 'Minhas Conquistas', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: Center( + child: Text( + '${_unlockedAchievements.length}/${_allAchievements.length}', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - cs.primary.withValues(alpha: 0.05), - cs.surface, - ], + 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), - ), - ), - ), - // Content Expanded( child: _loading @@ -203,7 +183,8 @@ class _StudentAchievementsPageState extends State { return _buildEmptyState( icon: Icons.emoji_events_outlined, title: 'Nenhuma conquista desbloqueada', - subtitle: 'Complete quizzes e mantenha seu streak para desbloquear conquistas!', + subtitle: + 'Complete quizzes e mantenha seu streak para desbloquear conquistas!', ); } @@ -214,7 +195,10 @@ class _StudentAchievementsPageState extends State { final achievement = _unlockedAchievements[index]; return _buildAchievementCard(achievement, isUnlocked: true) .animate() - .slideX(duration: const Duration(milliseconds: 300)) + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) .then(delay: Duration(milliseconds: index * 50)); }, ); @@ -236,13 +220,19 @@ class _StudentAchievementsPageState extends State { final achievement = _lockedAchievements[index]; return _buildAchievementCard(achievement, isUnlocked: false) .animate() - .slideX(duration: const Duration(milliseconds: 300)) + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) .then(delay: Duration(milliseconds: index * 50)); }, ); } - Widget _buildAchievementCard(Achievement achievement, {required bool isUnlocked}) { + Widget _buildAchievementCard( + Achievement achievement, { + required bool isUnlocked, + }) { final cs = Theme.of(context).colorScheme; final color = _getRarityColor(achievement.rarity); @@ -253,7 +243,7 @@ class _StudentAchievementsPageState extends State { color: isUnlocked ? cs.surface : cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(16), border: Border.all( - color: isUnlocked + color: isUnlocked ? color.withValues(alpha: 0.3) : cs.outline.withValues(alpha: 0.2), width: isUnlocked ? 2 : 1, @@ -273,7 +263,7 @@ class _StudentAchievementsPageState extends State { width: 48, height: 48, decoration: BoxDecoration( - color: isUnlocked + color: isUnlocked ? color.withValues(alpha: 0.2) : cs.outline.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(24), @@ -302,18 +292,20 @@ class _StudentAchievementsPageState extends State { const SizedBox(height: 4), Text( achievement.description, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 14, - ), + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), ), const SizedBox(height: 8), Row( children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( - color: _getRarityColor(achievement.rarity).withValues(alpha: 0.2), + color: _getRarityColor( + achievement.rarity, + ).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( @@ -342,17 +334,9 @@ class _StudentAchievementsPageState extends State { // Status if (isUnlocked) - Icon( - Icons.check_circle, - color: Colors.green, - size: 24, - ) + Icon(Icons.check_circle, color: Colors.green, size: 24) else - Icon( - Icons.lock, - color: cs.outline, - size: 24, - ), + Icon(Icons.lock, color: cs.outline, size: 24), ], ), ); @@ -371,11 +355,7 @@ class _StudentAchievementsPageState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - icon, - size: 64, - color: cs.onSurfaceVariant, - ), + Icon(icon, size: 64, color: cs.onSurfaceVariant), const SizedBox(height: 16), Text( title, @@ -389,10 +369,7 @@ class _StudentAchievementsPageState extends State { const SizedBox(height: 8), Text( subtitle, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 14, - ), + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), textAlign: TextAlign.center, ), ], @@ -403,29 +380,47 @@ class _StudentAchievementsPageState extends State { 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; + 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; + 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 index c14c0fd..5adba30 100644 --- a/lib/features/analytics/presentation/pages/analytics_page.dart +++ b/lib/features/analytics/presentation/pages/analytics_page.dart @@ -45,18 +45,21 @@ class _AnalyticsPageState extends State final user = AuthService.currentUser; if (user == null) return; - // Obter turmas do professor + // Obter disciplinas 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; // Forçar atualização para obter dados mais recentes - final stats = await GamificationService.getClassStats(classId, forceRefresh: true); + final stats = await GamificationService.getClassStats( + classId, + forceRefresh: true, + ); if (stats != null) { classStatsList.add(stats); } @@ -105,7 +108,10 @@ class _AnalyticsPageState extends State Row( children: [ IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), + icon: const Icon( + Icons.arrow_back, + color: Colors.white, + ), onPressed: () => context.go('/teacher-dashboard'), ), const SizedBox(width: 16), @@ -123,7 +129,7 @@ class _AnalyticsPageState extends State ), const SizedBox(height: 4), Text( - 'Acompanhe o desempenho das turmas', + 'Acompanhe o desempenho das disciplinas', style: TextStyle( color: Colors.white.withValues(alpha: 0.8), fontSize: 16, @@ -147,7 +153,7 @@ class _AnalyticsPageState extends State indicatorColor: Colors.white, indicatorWeight: 2, tabs: const [ - Tab(text: 'Turmas'), + Tab(text: 'Disciplinas'), Tab(text: 'Rankings'), ], ), @@ -159,10 +165,7 @@ class _AnalyticsPageState extends State Expanded( child: TabBarView( controller: _tabController, - children: [ - _buildClassesTab(), - _buildRankingsTab(), - ], + children: [_buildClassesTab(), _buildRankingsTab()], ), ), ], @@ -174,7 +177,9 @@ class _AnalyticsPageState extends State Widget _buildClassesTab() { if (_loading) { - return const Center(child: CircularProgressIndicator(color: Colors.white)); + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); } if (_classStats.isEmpty) { @@ -189,7 +194,7 @@ class _AnalyticsPageState extends State ), const SizedBox(height: 16), Text( - 'Nenhuma turma encontrada', + 'Nenhuma disciplina encontrada', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 18, @@ -197,7 +202,7 @@ class _AnalyticsPageState extends State ), const SizedBox(height: 8), Text( - 'Crie turmas para ver as analytics aqui', + 'Crie disciplinas para ver as analytics aqui', style: TextStyle( color: Colors.white.withValues(alpha: 0.5), fontSize: 14, @@ -238,13 +243,15 @@ class _AnalyticsPageState extends State const SizedBox(height: 20), // Class Cards - ..._classStats.map((stats) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: ClassAnalyticsCard( - classStats: stats, - onTap: () => _showClassRanking(stats), + ..._classStats.map( + (stats) => Padding( + padding: const EdgeInsets.only(bottom: 16), + child: ClassAnalyticsCard( + classStats: stats, + onTap: () => _showClassRanking(stats), + ), ), - )), + ), ], ), ); @@ -263,7 +270,7 @@ class _AnalyticsPageState extends State ), const SizedBox(height: 16), Text( - 'Selecione uma turma', + 'Selecione uma disciplina', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 18, @@ -271,7 +278,7 @@ class _AnalyticsPageState extends State ), const SizedBox(height: 8), Text( - 'Clique em uma turma na aba "Turmas" para ver o ranking', + 'Clique em uma disciplina na aba "Disciplinas" para ver o ranking', style: TextStyle( color: Colors.white.withValues(alpha: 0.5), fontSize: 14, @@ -285,7 +292,12 @@ class _AnalyticsPageState extends State return ClassRankingWidget(classId: _selectedClassId!); } - Widget _buildOverviewCard(String title, String value, IconData icon, Color color) { + Widget _buildOverviewCard( + String title, + String value, + IconData icon, + Color color, + ) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( @@ -333,7 +345,9 @@ class _AnalyticsPageState extends State onAchievementCreated: (achievement) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Conquista "${achievement.name}" criada com sucesso!'), + content: Text( + 'Conquista "${achievement.name}" criada com sucesso!', + ), backgroundColor: Colors.green, ), ); diff --git a/lib/features/analytics/presentation/widgets/class_ranking_widget.dart b/lib/features/analytics/presentation/widgets/class_ranking_widget.dart index 9fc81c5..d95c994 100644 --- a/lib/features/analytics/presentation/widgets/class_ranking_widget.dart +++ b/lib/features/analytics/presentation/widgets/class_ranking_widget.dart @@ -10,10 +10,7 @@ import '../../../../core/theme/app_theme_extension.dart'; class ClassRankingWidget extends StatefulWidget { final String classId; - const ClassRankingWidget({ - super.key, - required this.classId, - }); + const ClassRankingWidget({super.key, required this.classId}); @override State createState() => _ClassRankingWidgetState(); @@ -60,11 +57,18 @@ class _ClassRankingWidgetState extends State { 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(); + + return _rankings + .where( + (student) => + student.studentName.toLowerCase().contains( + _searchQuery.toLowerCase(), + ) || + student.studentEmail.toLowerCase().contains( + _searchQuery.toLowerCase(), + ), + ) + .toList(); } @override @@ -118,7 +122,10 @@ class _ClassRankingWidgetState extends State { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(16), @@ -126,7 +133,11 @@ class _ClassRankingWidgetState extends State { child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.leaderboard, color: Colors.white, size: 16), + const Icon( + Icons.leaderboard, + color: Colors.white, + size: 16, + ), const SizedBox(width: 4), Text( 'Ranking', @@ -177,7 +188,10 @@ class _ClassRankingWidgetState extends State { ), if (_searchQuery.isNotEmpty) IconButton( - icon: Icon(Icons.clear, color: Colors.white.withValues(alpha: 0.7)), + icon: Icon( + Icons.clear, + color: Colors.white.withValues(alpha: 0.7), + ), onPressed: () { setState(() { _searchQuery = ''; @@ -199,7 +213,7 @@ class _ClassRankingWidgetState extends State { 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), @@ -255,7 +269,7 @@ class _ClassRankingWidgetState extends State { ), const SizedBox(height: 16), Text( - 'Nenhum aluno na turma', + 'Nenhum aluno na disciplina', style: TextStyle( color: Colors.white.withValues(alpha: 0.7), fontSize: 18, @@ -276,11 +290,11 @@ class _ClassRankingWidgetState extends State { 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; @@ -361,7 +375,9 @@ class _ClassRankingWidgetState extends State { Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: _getScoreColor(student.overallScore).withValues(alpha: 0.2), + color: _getScoreColor( + student.overallScore, + ).withValues(alpha: 0.2), borderRadius: BorderRadius.circular(8), ), child: Text( diff --git a/lib/features/auth/presentation/pages/signup_page.dart b/lib/features/auth/presentation/pages/signup_page.dart index 7ad2405..8319237 100644 --- a/lib/features/auth/presentation/pages/signup_page.dart +++ b/lib/features/auth/presentation/pages/signup_page.dart @@ -83,7 +83,7 @@ class _SignupPageState extends State { ), const SizedBox(width: 12), Text( - 'A carregar turmas...', + 'A carregar anos letivos...', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14, @@ -98,7 +98,7 @@ class _SignupPageState extends State { return Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Text( - 'Nenhuma turma disponível. Contacta o teu professor.', + 'Nenhum ano letivo disponível. Contacta o teu professor.', style: TextStyle( color: Theme.of(context).colorScheme.error, fontSize: 13, @@ -111,7 +111,7 @@ class _SignupPageState extends State { value: _selectedSchoolClassId, isExpanded: true, decoration: InputDecoration( - labelText: 'Turma', + labelText: 'Ano letivo', labelStyle: TextStyle(color: Theme.of(context).colorScheme.onSurface), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8.0), @@ -139,7 +139,7 @@ class _SignupPageState extends State { style: TextStyle(color: Theme.of(context).colorScheme.onSurface), dropdownColor: Theme.of(context).colorScheme.surface, hint: Text( - 'Seleciona a tua turma', + 'Escolha o seu ano letivo', style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), ), items: _availableClasses @@ -153,7 +153,7 @@ class _SignupPageState extends State { onChanged: (value) => setState(() => _selectedSchoolClassId = value), validator: (value) { if (value == null || value.isEmpty) { - return 'Seleciona a tua turma'; + return 'Seleciona o teu ano letivo'; } return null; }, @@ -246,10 +246,12 @@ class _SignupPageState extends State { ? AppThemeExtras.of(context).authBackgroundGradient : [ Theme.of(context).colorScheme.background, - Theme.of(context).colorScheme.primary - .withOpacity(0.1), - Theme.of(context).colorScheme.secondary - .withOpacity(0.05), + Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1), + Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.05), Theme.of(context).colorScheme.background, ], ), @@ -424,7 +426,7 @@ class _SignupPageState extends State { ), const SizedBox(height: 16), - // Seletor de turma (apenas para alunos) + // Seletor de ano letivo (apenas para alunos) if (_selectedRole == 'student') ...[ _buildClassSelector(context), const SizedBox(height: 16), diff --git a/lib/features/classes/presentation/pages/class_students_page.dart b/lib/features/classes/presentation/pages/class_students_page.dart index 9e45cf7..d1cc66a 100644 --- a/lib/features/classes/presentation/pages/class_students_page.dart +++ b/lib/features/classes/presentation/pages/class_students_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../../../../core/services/auth_service.dart'; -/// Página para visualizar os alunos de uma turma específica +/// Página para visualizar os alunos de uma disciplina específica class ClassStudentsPage extends StatefulWidget { final String classId; final String className; @@ -107,7 +107,7 @@ class _ClassStudentsPageState extends State { ), const SizedBox(height: 12), Text( - 'Só podes ver os alunos das tuas próprias turmas.', + 'Só podes ver os alunos das tuas próprias disciplinas.', style: TextStyle(color: Colors.grey[600], fontSize: 14), textAlign: TextAlign.center, ), @@ -188,7 +188,7 @@ class _ClassStudentsPageState extends State { Icon(Icons.people_outline, size: 64, color: Colors.grey[300]), const SizedBox(height: 24), Text( - 'Nenhum aluno entrou nesta turma ainda.', + 'Nenhum aluno entrou nesta disciplina ainda.', style: TextStyle(color: Colors.grey[600], fontSize: 16), textAlign: TextAlign.center, ), @@ -196,7 +196,7 @@ class _ClassStudentsPageState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Text( - 'Partilha o código da turma para os alunos se juntarem.', + 'Partilha o código da disciplina para os alunos se juntarem.', style: TextStyle(color: Colors.grey[500], fontSize: 13), textAlign: TextAlign.center, ), diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart index 641427a..e0927e7 100644 --- a/lib/features/classes/presentation/pages/join_class_page.dart +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/services/auth_service.dart'; -/// Página para o aluno entrar numa turma usando o código +/// Página para o aluno entrar numa disciplina usando o código class JoinClassPage extends ConsumerStatefulWidget { const JoinClassPage({super.key}); @@ -25,7 +25,7 @@ class _JoinClassPageState extends ConsumerState { final code = _codeController.text.trim().toUpperCase(); if (code.isEmpty) { - _showError('Insere o código da turma'); + _showError('Insere o código da disciplina'); return; } @@ -44,7 +44,7 @@ class _JoinClassPageState extends ConsumerState { final userRole = await AuthService.getUserRole(currentUser.uid); if (userRole != 'student') { setState(() => _isLoading = false); - _showError('Apenas alunos podem entrar em turmas por código.'); + _showError('Apenas alunos podem entrar em disciplinas por código.'); return; } @@ -53,7 +53,7 @@ class _JoinClassPageState extends ConsumerState { currentUser.uid, ); - // Procurar turma pelo código + // Procurar disciplina pelo código final classQuery = await FirebaseFirestore.instance .collection('classes') .where('code', isEqualTo: code) @@ -62,7 +62,7 @@ class _JoinClassPageState extends ConsumerState { if (classQuery.docs.isEmpty) { setState(() => _isLoading = false); - _showError('Código de turma inválido'); + _showError('Código de disciplina inválido'); return; } @@ -70,20 +70,20 @@ class _JoinClassPageState extends ConsumerState { final classId = classDoc.id; final classSchoolClassId = classDoc.data()['schoolClassId'] as String?; - // Verificar se o aluno está autorizado a entrar nesta turma + // Verificar se o aluno está autorizado a entrar nesta disciplina // O schoolClassId do aluno deve corresponder ao schoolClassId da disciplina if (studentSchoolClassId == null || classSchoolClassId == null || studentSchoolClassId != classSchoolClassId) { setState(() => _isLoading = false); _showError( - 'Não tens permissão para entrar nesta turma.\n' + 'Não tens permissão para entrar nesta disciplina.\n' 'O teu professor ainda não te adicionou a esta disciplina.', ); return; } - // Verificar se já está inscrito nesta turma + // Verificar se já está inscrito nesta disciplina final existingEnrollment = await FirebaseFirestore.instance .collection('enrollments') .where('classId', isEqualTo: classId) @@ -93,7 +93,7 @@ class _JoinClassPageState extends ConsumerState { if (existingEnrollment.docs.isNotEmpty) { setState(() => _isLoading = false); - _showError('Já estás inscrito nesta turma'); + _showError('Já estás inscrito nesta disciplina'); return; } @@ -119,7 +119,7 @@ class _JoinClassPageState extends ConsumerState { children: [ Icon(Icons.check_circle, color: Colors.white), const SizedBox(width: 8), - const Text('Entraste na turma com sucesso!'), + const Text('Entraste na disciplina com sucesso!'), ], ), backgroundColor: colorScheme.primary, @@ -136,7 +136,7 @@ class _JoinClassPageState extends ConsumerState { } } catch (e) { setState(() => _isLoading = false); - _showError('Erro ao entrar na turma: $e'); + _showError('Erro ao entrar na disciplina: $e'); } } @@ -201,7 +201,7 @@ class _JoinClassPageState extends ConsumerState { ), Expanded( child: Text( - 'Entrar numa Turma', + 'Entrar numa Disciplina', style: TextStyle( color: colorScheme.onSurface, fontSize: 18, @@ -272,7 +272,7 @@ class _JoinClassPageState extends ConsumerState { ), const SizedBox(height: 24), Text( - 'Insere o código da turma', + 'Insere o código da disciplina', style: theme.textTheme.headlineSmall ?.copyWith( color: colorScheme.onSurface, @@ -331,7 +331,7 @@ class _JoinClassPageState extends ConsumerState { _buildInstructionItem( context, '1.', - 'Pedir ao professor o código da turma', + 'Pedir ao professor o código da disciplina', colorScheme, ), const SizedBox(height: 8), @@ -345,7 +345,7 @@ class _JoinClassPageState extends ConsumerState { _buildInstructionItem( context, '3.', - 'Clicar em "Entrar na Turma" para confirmar', + 'Clicar em "Entrar na Disciplina" para confirmar', colorScheme, ), ], @@ -445,7 +445,7 @@ class _JoinClassPageState extends ConsumerState { ), const SizedBox(width: 8), Text( - 'Entrar na Turma', + 'Entrar na Disciplina', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -541,7 +541,7 @@ class _JoinClassPageState extends ConsumerState { Icon(Icons.help_outline, color: colorScheme.primary), const SizedBox(width: 8), Text( - 'Ajuda - Código da Turma', + 'Ajuda - Código da Disciplina', style: TextStyle(color: colorScheme.onSurface), ), ], @@ -551,7 +551,7 @@ class _JoinClassPageState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'O código da turma é um código único de 6 caracteres que o teu professor cria para cada turma.', + 'O código da disciplina é um código único de 6 caracteres que o teu professor cria para cada disciplina.', style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 12), diff --git a/lib/features/classes/presentation/pages/teacher_all_classes_page.dart b/lib/features/classes/presentation/pages/teacher_all_classes_page.dart index a5e82ef..323d2d6 100644 --- a/lib/features/classes/presentation/pages/teacher_all_classes_page.dart +++ b/lib/features/classes/presentation/pages/teacher_all_classes_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import '../../../dashboard/presentation/widgets/teacher_classes_list_widget.dart'; -/// Página dedicada para o professor ver todas as suas turmas +/// Página dedicada para o professor ver todas as suas disciplinas /// Reutiliza o TeacherClassesListWidget existente class TeacherAllClassesPage extends StatelessWidget { const TeacherAllClassesPage({super.key}); @@ -18,7 +18,7 @@ class TeacherAllClassesPage extends StatelessWidget { onPressed: () => Navigator.of(context).pop(), ), title: const Text( - 'As Minhas Turmas', + 'As Minhas Disciplinas', style: TextStyle( color: Colors.white, fontSize: 18, @@ -30,10 +30,7 @@ class TeacherAllClassesPage extends StatelessWidget { padding: EdgeInsets.all(24.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TeacherClassesListWidget(), - SizedBox(height: 40), - ], + children: [TeacherClassesListWidget(), SizedBox(height: 40)], ), ), ); diff --git a/lib/features/dashboard/presentation/widgets/dashboard_action_card.dart b/lib/features/dashboard/presentation/widgets/dashboard_action_card.dart index aa4bea0..d97f8c7 100644 --- a/lib/features/dashboard/presentation/widgets/dashboard_action_card.dart +++ b/lib/features/dashboard/presentation/widgets/dashboard_action_card.dart @@ -78,8 +78,7 @@ class DashboardActionCard extends StatelessWidget { ); final titleColor = useGradient ? Colors.white : cs.onSurface; - final subtitleColor = - useGradient ? Colors.white : cs.onSurfaceVariant; + final subtitleColor = useGradient ? Colors.white : cs.onSurfaceVariant; final iconBgColor = useGradient ? Colors.white.withOpacity(0.2) : cs.primary.withOpacity(0.1); @@ -246,7 +245,7 @@ class DashboardActionCard extends StatelessWidget { } } -/// Surface-styled vertical card (Quiz, Criar Turma, etc.). +/// Surface-styled vertical card (Quiz, Criar Disciplina, etc.). class DashboardActionCardSurface extends StatelessWidget { const DashboardActionCardSurface({ super.key, @@ -291,7 +290,8 @@ class DashboardActionCardSurface extends StatelessWidget { minHeight: minHeight, useGradient: false, iconSize: iconSize, - leadingIcon: leadingWidget ?? + leadingIcon: + leadingWidget ?? Container( padding: const EdgeInsets.all(10), decoration: BoxDecoration( diff --git a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart index bcbe3ca..9319369 100644 --- a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart +++ b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart @@ -39,9 +39,9 @@ class _ProfileSectionWidgetState extends State { final achievements = results[1] as List; // Obter conquistas desbloqueadas recentemente - final unlockedAchievementIds = stats?.unlockedAchievements - .map((ua) => ua.achievementId) - .toSet() ?? {}; + final unlockedAchievementIds = + stats?.unlockedAchievements.map((ua) => ua.achievementId).toSet() ?? + {}; final recentUnlocked = achievements .where((a) => unlockedAchievementIds.contains(a.id)) @@ -291,11 +291,11 @@ class _ProfileSectionWidgetState extends State { ), ) .animate() - .slideY( - duration: const Duration(milliseconds: 800), + .fadeIn( + duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ) - .then(delay: const Duration(milliseconds: 400)); + .then(delay: const Duration(milliseconds: 200)); } Widget _buildAchievementItem( @@ -314,9 +314,7 @@ class _ProfileSectionWidgetState extends State { color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), - child: Center( - child: Icon(icon, color: color, size: 16), - ), + child: Center(child: Icon(icon, color: color, size: 16)), ), const SizedBox(width: 12), Expanded( @@ -349,37 +347,54 @@ class _ProfileSectionWidgetState extends State { 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; + 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; + 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! 📈'; @@ -388,10 +403,10 @@ class _ProfileSectionWidgetState extends State { 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'; diff --git a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart index 589df15..5ce89d0 100644 --- a/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/progress_hero_widget.dart @@ -10,10 +10,7 @@ import '../../../../core/services/auth_service.dart'; class ProgressHeroWidget extends StatefulWidget { final String userName; - const ProgressHeroWidget({ - super.key, - required this.userName, - }); + const ProgressHeroWidget({super.key, required this.userName}); @override State createState() => _ProgressHeroWidgetState(); @@ -28,11 +25,11 @@ class _ProgressHeroWidgetState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return _buildLoadingState(); } - + if (snapshot.hasError) { return _buildErrorState(); } - + final userStats = snapshot.data; return _buildContent(userStats); }, @@ -71,10 +68,10 @@ class _ProgressHeroWidgetState extends State { } Widget _buildContent(UserStats? userStats) { - final streakDays = userStats?.currentStreak ?? 0; final overallProgress = _calculateOverallProgress(userStats); - final masteredConcepts = userStats?.masteredConcepts.map((c) => c.conceptName).toList() ?? []; + final masteredConcepts = + userStats?.masteredConcepts.map((c) => c.conceptName).toList() ?? []; final studyTimeMinutes = userStats?.totalStudyTime ?? 0; return Container( @@ -236,9 +233,6 @@ class _ProgressHeroWidgetState extends State { ), ], ), - ).animate().scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, ), const SizedBox(height: 20), @@ -324,8 +318,8 @@ class _ProgressHeroWidgetState extends State { ), ) .animate() - .slideX( - duration: const Duration(milliseconds: 800), + .fadeIn( + duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ) .then(delay: const Duration(milliseconds: 200)), diff --git a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart index e29b7ba..8c51b95 100644 --- a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart +++ b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart @@ -4,138 +4,198 @@ import 'package:go_router/go_router.dart'; import '../../../classes/presentation/pages/join_class_page.dart'; import 'dashboard_action_card.dart'; -/// Quick access cards for Tutor IA and Quiz +/// Quick access cards for Student Dashboard with horizontal scrollable row class QuickAccessWidget extends StatelessWidget { const QuickAccessWidget({super.key}); + /// Mesmas dimensões dos cards em "Ações Rápidas" do professor. + static const double _scrollCardWidth = 200; + static const double _scrollRowHeight = 150; + static const double _cardMinHeight = 150; + static const EdgeInsets _cardPadding = EdgeInsets.all(16); + static const double _titleFontSize = 16; + static const double _subtitleFontSize = 13; + static const double _iconSize = 24; + static const double _iconPadding = 10; + @override Widget build(BuildContext context) { + final cards = [ + _buildTutorIACard(context), + _buildQuizCard(context), + _buildAchievementsCard(context), + _buildQuizManagementCard(context), + ]; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'Acesso Rápido', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 20, - fontWeight: FontWeight.bold, + InkWell( + onTap: () => _showQuickAccessList(context), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Acesso Rápido', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.expand_more, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, + ), + ], + ), ), ), - const SizedBox(height: 16), - 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(height: 12), + IntrinsicHeight( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + padding: const EdgeInsets.only(right: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SizedBox(width: _scrollCardWidth, child: cards[0]), + const SizedBox(width: 12), + SizedBox(width: _scrollCardWidth, child: cards[1]), + const SizedBox(width: 12), + SizedBox(width: _scrollCardWidth, child: cards[2]), + const SizedBox(width: 12), + SizedBox(width: _scrollCardWidth, child: cards[3]), + ], ), - ], + ), ), + const SizedBox(height: 16), - _buildQuizManagementCard(context), - const SizedBox(height: 16), + + // Entrar numa Disciplina (full width) _buildJoinClassCard(context), ], ) .animate() - .slideY( - duration: const Duration(milliseconds: 800), + .fadeIn( + duration: const Duration(milliseconds: 300), curve: Curves.easeOut, ) - .then(delay: const Duration(milliseconds: 200)); - } - - Widget _buildTutorIACard(BuildContext context) { - return DashboardActionCard( - title: 'Tutor IA', - subtitle: 'Assistente de estudos', - icon: Icons.psychology, - useGradient: true, - minHeight: 150, - onTap: () => context.go('/ai-tutor'), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) .then(delay: const Duration(milliseconds: 100)); } + Widget _buildTutorIACard(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + DashboardActionCard( + title: 'Tutor IA', + subtitle: 'Assistente de estudos', + icon: Icons.psychology, + useGradient: true, + minHeight: _cardMinHeight, + iconSize: _iconSize, + iconPadding: _iconPadding, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + padding: _cardPadding, + onTap: () => context.go('/ai-tutor'), + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .then(delay: const Duration(milliseconds: 100)), + ); + } + Widget _buildQuizCard(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return DashboardActionCardSurface( - title: 'Quiz', - subtitle: 'Testa os teus conhecimentos', - icon: Icons.quiz, - minHeight: 150, - iconColor: cs.secondary, - onTap: () => context.go('/quiz'), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 200)); + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + DashboardActionCardSurface( + title: 'Quiz', + subtitle: 'Testa os teus conhecimentos', + icon: Icons.quiz, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + onTap: () => context.go('/quiz'), + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .then(delay: const Duration(milliseconds: 150)), + ); } 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)); + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + DashboardActionCardSurface( + title: 'Conquistas', + subtitle: 'Ver medals', + icon: Icons.emoji_events, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + iconColor: Colors.amber, + onTap: () => context.go('/student/achievements'), + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .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)); + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + DashboardActionCardSurface( + title: 'Gerenciar Quizzes', + subtitle: 'Ver histórico ou eliminar', + icon: Icons.manage_history, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + iconColor: cs.tertiary, + onTap: () => context.go('/quiz-management'), + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .then(delay: const Duration(milliseconds: 250)), + ); } Widget _buildJoinClassCard(BuildContext context) { return DashboardActionCard( - title: 'Entrar numa Turma', - subtitle: 'Junta-te a uma turma com o código', + title: 'Entrar numa Disciplina', + subtitle: 'Junta-te a uma disciplina com o código', icon: Icons.group_add, layout: DashboardActionCardLayout.horizontal, minHeight: 0, @@ -147,10 +207,193 @@ class QuickAccessWidget extends StatelessWidget { }, ) .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, ) .then(delay: const Duration(milliseconds: 300)); } + + void _showQuickAccessList(BuildContext context) { + final items = [ + _QuickAccessItem( + title: 'Tutor IA', + subtitle: 'Assistente de estudos', + icon: Icons.psychology, + useGradient: true, + onTap: () { + Navigator.pop(context); + context.go('/ai-tutor'); + }, + ), + _QuickAccessItem( + title: 'Quiz', + subtitle: 'Testa os teus conhecimentos', + icon: Icons.quiz, + onTap: () { + Navigator.pop(context); + context.go('/quiz'); + }, + ), + _QuickAccessItem( + title: 'Conquistas', + subtitle: 'Ver medals', + icon: Icons.emoji_events, + iconColor: Colors.amber, + onTap: () { + Navigator.pop(context); + context.go('/student/achievements'); + }, + ), + _QuickAccessItem( + title: 'Gerenciar Quizzes', + subtitle: 'Ver histórico ou eliminar', + icon: Icons.manage_history, + onTap: () { + Navigator.pop(context); + context.go('/quiz-management'); + }, + ), + _QuickAccessItem( + title: 'Entrar numa Disciplina', + subtitle: 'Junta-te a uma disciplina com o código', + icon: Icons.group_add, + onTap: () { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const JoinClassPage()), + ); + }, + ), + ]; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Text( + 'Acesso Rápido', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: items.length, + itemBuilder: (context, index) { + final item = items[index]; + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + ), + ), + child: ListTile( + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: item.useGradient + ? Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1) + : (item.iconColor ?? + Theme.of( + context, + ).colorScheme.secondary) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + item.icon, + color: item.useGradient + ? Theme.of(context).colorScheme.primary + : (item.iconColor ?? + Theme.of(context).colorScheme.secondary), + size: 24, + ), + ), + title: Text( + item.title, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + item.subtitle, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), + trailing: Icon( + Icons.arrow_forward_ios, + color: Theme.of(context).colorScheme.primary, + size: 16, + ), + onTap: item.onTap, + ), + ); + }, + ), + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ); + } +} + +class _QuickAccessItem { + final String title; + final String subtitle; + final IconData icon; + final bool useGradient; + final Color? iconColor; + final VoidCallback onTap; + + _QuickAccessItem({ + required this.title, + required this.subtitle, + required this.icon, + this.useGradient = false, + this.iconColor, + required this.onTap, + }); } diff --git a/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart b/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart index d8f12f1..54e6858 100644 --- a/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart +++ b/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart @@ -1,12 +1,21 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; -/// Widget para listar as turmas onde o aluno está inscrito -class StudentClassesListWidget extends StatelessWidget { +/// Widget para listar as disciplinas onde o aluno está inscrito +class StudentClassesListWidget extends StatefulWidget { const StudentClassesListWidget({super.key}); + @override + State createState() => + _StudentClassesListWidgetState(); +} + +class _StudentClassesListWidgetState extends State { + List? _enrollments; + @override Widget build(BuildContext context) { final currentUser = AuthService.currentUser; @@ -22,7 +31,8 @@ class StudentClassesListWidget extends StatelessWidget { .orderBy('joinedAt', descending: true) .snapshots(), builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { + if (snapshot.connectionState == ConnectionState.waiting && + _enrollments == null) { return Center( child: Padding( padding: const EdgeInsets.all(16.0), @@ -33,17 +43,20 @@ class StudentClassesListWidget extends StatelessWidget { ); } - if (snapshot.hasError) { + if (snapshot.hasError && _enrollments == null) { return const SizedBox.shrink(); } - final enrollments = snapshot.data?.docs ?? []; + final enrollments = snapshot.data?.docs ?? _enrollments ?? []; + if (snapshot.data?.docs != null) { + _enrollments = snapshot.data?.docs; + } if (enrollments.isEmpty) { return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Text( - 'Ainda não entraste em nenhuma turma.', + 'Ainda não entraste em nenhuma disciplina.', style: TextStyle(color: Colors.grey[600], fontSize: 14), ), ); @@ -52,11 +65,30 @@ class StudentClassesListWidget extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - 'As Minhas Turmas', - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, + InkWell( + onTap: () => _showClassesList(context, enrollments), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'As Minhas Disciplinas', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(width: 8), + Icon( + Icons.expand_more, + color: Theme.of(context).colorScheme.onSurfaceVariant, + size: 20, + ), + ], + ), ), ), const SizedBox(height: 16), @@ -93,6 +125,7 @@ class StudentClassesListWidget extends StatelessWidget { Widget _buildClassCard(DocumentSnapshot enrollmentDoc) { final enrollmentData = enrollmentDoc.data() as Map; final classId = enrollmentData['classId'] as String? ?? ''; + final enrollmentId = enrollmentDoc.id; if (classId.isEmpty) { return const SizedBox.shrink(); @@ -133,60 +166,341 @@ class StudentClassesListWidget extends StatelessWidget { final className = classData['name'] as String? ?? 'Sem nome'; final classCode = classData['code'] as String? ?? '----'; - return Container( - width: 200, - constraints: const BoxConstraints(minHeight: 150), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), + return GestureDetector( + onTap: () => _showRemoveClassDialog(context, enrollmentId, className), + child: Container( + width: 200, + constraints: const BoxConstraints(minHeight: 150), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - child: Icon( - Icons.school, - color: Theme.of(context).colorScheme.primary, - size: 24, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.school, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), ), - ), - const SizedBox(height: 12), - Text( - className, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.bold, + const SizedBox(height: 12), + Text( + className, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - 'Código: $classCode', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 13, + const SizedBox(height: 4), + Text( + 'Código: $classCode', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 13, + ), ), - ), - ], + ], + ), ), ); }, ); } + + void _showClassesList( + BuildContext context, + List enrollments, + ) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + builder: (context) => DraggableScrollableSheet( + initialChildSize: 0.6, + minChildSize: 0.3, + maxChildSize: 0.9, + expand: false, + builder: (context, scrollController) { + return Column( + children: [ + Container( + margin: const EdgeInsets.only(top: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + Padding( + padding: const EdgeInsets.all(20), + child: Text( + 'As Minhas Disciplinas', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: ListView.builder( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: enrollments.length, + itemBuilder: (context, index) { + return _buildClassListTile(context, enrollments[index]); + }, + ), + ), + const SizedBox(height: 20), + ], + ); + }, + ), + ); + } + + Widget _buildClassListTile( + BuildContext context, + DocumentSnapshot enrollmentDoc, + ) { + final enrollmentData = enrollmentDoc.data() as Map; + final classId = enrollmentData['classId'] as String? ?? ''; + final enrollmentId = enrollmentDoc.id; + + if (classId.isEmpty) { + return const SizedBox.shrink(); + } + + return FutureBuilder( + future: FirebaseFirestore.instance + .collection('classes') + .doc(classId) + .get(), + builder: (context, snapshot) { + if (!snapshot.hasData || !snapshot.data!.exists) { + return const Card( + child: ListTile( + leading: CircularProgressIndicator(), + title: Text('Carregando...'), + ), + ); + } + + final classData = snapshot.data!.data() as Map; + final className = classData['name'] as String? ?? 'Sem nome'; + final classCode = classData['code'] as String? ?? '----'; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + ), + child: ListTile( + leading: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.school, + color: Theme.of(context).colorScheme.primary, + size: 24, + ), + ), + title: Text( + className, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + subtitle: Text( + 'Código: $classCode', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), + trailing: Icon( + Icons.delete_outline, + color: Theme.of(context).colorScheme.error, + size: 20, + ), + onTap: () => + _showRemoveClassDialog(context, enrollmentId, className), + ), + ); + }, + ); + } + + void _showRemoveClassDialog( + BuildContext context, + String enrollmentId, + String className, + ) { + final textController = TextEditingController(); + bool isConfirmEnabled = false; + + showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text('Sair da disciplina'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Para confirmar que queres sair da disciplina "$className", escreve o nome desta disciplina:', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + const SizedBox(height: 16), + TextField( + controller: textController, + onChanged: (value) { + setDialogState(() { + isConfirmEnabled = value.trim() == className.trim(); + }); + }, + decoration: InputDecoration( + hintText: 'Nome da disciplina', + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2, + ), + ), + ), + ), + const SizedBox(height: 8), + Text( + 'Esta ação não pode ser desfeita.', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 12, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text( + 'Cancelar', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ElevatedButton( + onPressed: isConfirmEnabled + ? () async { + Navigator.pop(context); + await _removeEnrollment(enrollmentId); + } + : null, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Colors.white, + disabledBackgroundColor: Theme.of( + context, + ).colorScheme.error.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Sair'), + ), + ], + ); + }, + ), + ); + } + + Future _removeEnrollment(String enrollmentId) async { + try { + await FirebaseFirestore.instance + .collection('enrollments') + .doc(enrollmentId) + .delete(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text('Saíste da disciplina com sucesso'), + backgroundColor: Theme.of(context).colorScheme.primary, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao sair da disciplina: $e'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } + } + } } 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 a4a3d12..b9d2950 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart @@ -1,311 +1,243 @@ 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 StatefulWidget { +class TeacherAnalyticsPreviewWidget extends StatelessWidget { 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; - // Forçar atualização para obter dados mais recentes - final stats = await GamificationService.getClassStats(classId, forceRefresh: true); - 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 ?? ''; return Container( - margin: const EdgeInsets.only(top: 24), - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], + margin: const EdgeInsets.only(top: 24), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Profile Header + Row( children: [ - // Profile Header - Row( - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppThemeExtras.of(context).actionCardGradientStart, - AppThemeExtras.of(context).actionCardGradientEnd, - ], - ), - borderRadius: BorderRadius.circular(24), - ), - child: const Icon( - Icons.school, - color: Colors.white, - size: 24, - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - userName, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 2), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - userEmail, - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 14, - ), - ), - if (userEmail.length > 20) ...[ - const SizedBox(width: 8), - Icon( - Icons.more_horiz, - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - size: 16, - ), - ], - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 20), - - // Quick Stats Row - Row( - children: [ - _buildQuickStat( - icon: Icons.check_circle, - label: 'Alunos Ativos', - value: '$activeStudents/$totalStudents', - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 12), - _buildQuickStat( - icon: Icons.warning_amber, - label: 'Precisam Apoio', - value: '$studentsNeedingSupport', - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(width: 12), - _buildQuickStat( - icon: Icons.emoji_events, - label: 'Média Turma', - value: '${(averageProgress * 100).toInt()}%', - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.8), - ), - ], - ), - const SizedBox(height: 20), - - // Top Performing Students Preview - Row( - children: [ - Icon( - Icons.leaderboard, - color: Theme.of(context).colorScheme.secondary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Melhores Desempenhos', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - const SizedBox(height: 12), - - // Student List Preview - ..._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), - - // Content Quality Alert Container( - padding: const EdgeInsets.all(16), + width: 48, + height: 48, decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), + gradient: LinearGradient( + colors: [ + AppThemeExtras.of(context).actionCardGradientStart, + AppThemeExtras.of(context).actionCardGradientEnd, + ], ), + borderRadius: BorderRadius.circular(24), ), - child: Row( + child: const Icon(Icons.school, color: Colors.white, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.primary, - size: 20, + Text( + userName, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.bold, + ), ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 2), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, children: [ Text( - 'Qualidade do Conteúdo', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 2), - Text( - '${_classStats.fold(0, (sum, stats) => sum + stats.totalContent)} conteúdos • $studentsNeedingSupport alunos precisam de apoio', + userEmail, style: TextStyle( color: Theme.of( context, ).colorScheme.onSurfaceVariant, - fontSize: 12, + fontSize: 14, ), ), + if (userEmail.length > 20) ...[ + const SizedBox(width: 8), + Icon( + Icons.more_horiz, + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + size: 16, + ), + ], ], ), ), ], ), ), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.settings, + color: Theme.of(context).colorScheme.secondary, + size: 20, + ), + ), ], ), - ) - .animate() - .slideY( - duration: const Duration(milliseconds: 800), - curve: Curves.easeOut, - ) - .then(delay: const Duration(milliseconds: 400)); + const SizedBox(height: 20), + + // Quick Stats Row + Row( + children: [ + _buildQuickStat( + icon: Icons.check_circle, + label: 'Alunos Ativos', + value: '18/24', + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 12), + _buildQuickStat( + icon: Icons.warning_amber, + label: 'Precisam Apoio', + value: '3', + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(width: 12), + _buildQuickStat( + icon: Icons.emoji_events, + label: 'Média Disciplina', + value: '72%', + color: Theme.of(context).colorScheme.primary.withOpacity(0.8), + ), + ], + ), + const SizedBox(height: 20), + + // Top Performing Students Preview + Row( + children: [ + Icon( + Icons.leaderboard, + color: Theme.of(context).colorScheme.secondary, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Melhores Desempenhos', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + 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, + ), + + const SizedBox(height: 20), + + // Content Quality Alert + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + ), + child: Row( + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Qualidade do Conteúdo', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + '12 conteúdos verificados • 2 pendentes de revisão', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); } Widget _buildQuickStat({ @@ -401,11 +333,4 @@ class _TeacherAnalyticsPreviewWidgetState extends State= 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_classes_list_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart index ea9773d..56d92bd 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import '../../../../core/services/auth_service.dart'; import '../../../classes/presentation/pages/class_students_page.dart'; -/// Widget para listar as turmas criadas pelo professor +/// Widget para listar as disciplinas criadas pelo professor class TeacherClassesListWidget extends StatelessWidget { const TeacherClassesListWidget({super.key}); @@ -44,7 +44,7 @@ class TeacherClassesListWidget extends StatelessWidget { return Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: Text( - 'Ainda não criaste nenhuma turma.', + 'Ainda não criaste nenhuma disciplina.', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14, @@ -57,7 +57,7 @@ class TeacherClassesListWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'As Minhas Turmas', + 'As Minhas Disciplinas', style: Theme.of(context).textTheme.titleLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.bold, diff --git a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart index 69cba5f..fc26c6b 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart @@ -1,5 +1,4 @@ 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'; @@ -11,10 +10,7 @@ import '../../../../core/services/auth_service.dart'; class TeacherHeroWidget extends StatefulWidget { final String userName; - const TeacherHeroWidget({ - super.key, - required this.userName, - }); + const TeacherHeroWidget({super.key, required this.userName}); @override State createState() => _TeacherHeroWidgetState(); @@ -35,18 +31,21 @@ class _TeacherHeroWidgetState extends State { final user = AuthService.currentUser; if (user == null) return; - // Obter turmas do professor + // Obter disciplinas 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; // Forçar atualização para obter dados mais recentes - final stats = await GamificationService.getClassStats(classId, forceRefresh: true); + final stats = await GamificationService.getClassStats( + classId, + forceRefresh: true, + ); if (stats != null) { classStatsList.add(stats); } @@ -68,23 +67,31 @@ class _TeacherHeroWidgetState extends State { } } - 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); + 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); + final totalProgress = _classStats.fold( + 0.0, + (sum, stats) => sum + stats.averageProgress, + ); final average = totalProgress / _classStats.length; - + print('=== UI PROGRESS DEBUG ==='); print('Number of classes: ${_classStats.length}'); for (int i = 0; i < _classStats.length; i++) { - print('Class ${i + 1}: ${_classStats[i].className} - ${_classStats[i].averageProgress} (${(_classStats[i].averageProgress * 100).toInt()}%)'); + print( + 'Class ${i + 1}: ${_classStats[i].className} - ${_classStats[i].averageProgress} (${(_classStats[i].averageProgress * 100).toInt()}%)', + ); } print('Total progress sum: $totalProgress'); print('Calculated average: $average (${(average * 100).toInt()}%)'); print('=== END UI PROGRESS DEBUG ==='); - + return average; } @@ -109,7 +116,7 @@ class _TeacherHeroWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Visão Geral da Turma', + 'Visão Geral da Disciplina', style: TextStyle( color: Theme.of(context).colorScheme.onSurface, fontSize: 20, @@ -186,7 +193,7 @@ class _TeacherHeroWidgetState extends State { children: [ const Flexible( child: Text( - 'Progresso Médio da Turma', + 'Progresso Médio da Disciplina', maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -198,7 +205,8 @@ class _TeacherHeroWidgetState extends State { ), Builder( builder: (context) { - final displayValue = (classAverageProgress * 100).toInt(); + final displayValue = (classAverageProgress * 100) + .toInt(); print('=== RENDER DEBUG ==='); print('classAverageProgress: $classAverageProgress'); print('displayValue: $displayValue%'); @@ -264,66 +272,53 @@ class _TeacherHeroWidgetState extends State { ), ], ), - ).animate().scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, ), const SizedBox(height: 20), // Recent Activity Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withOpacity(0.2), - ), - boxShadow: [ - BoxShadow( - color: Theme.of( - context, - ).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.outline.withOpacity(0.2), + ), + boxShadow: [ + BoxShadow( + color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - Icon( - Icons.trending_up, - color: Theme.of(context).colorScheme.secondary, - size: 20, - ), - const SizedBox(width: 8), - Text( - 'Atividade Recente', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ], + Icon( + Icons.trending_up, + color: Theme.of(context).colorScheme.secondary, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Atividade Recente', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), - const SizedBox(height: 12), - ..._buildRecentActivities(), ], ), - ) - .animate() - .slideX( - duration: const Duration(milliseconds: 800), - curve: Curves.easeOut, - ) - .then(delay: const Duration(milliseconds: 200)), + const SizedBox(height: 12), + ..._buildRecentActivities(), + ], + ), + ), ], ), ); @@ -368,46 +363,54 @@ class _TeacherHeroWidgetState extends State { 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, - )); + activities.add( + _buildActivityItem( + context, + 'Nenhuma atividade recente', + 'Comece criando disciplinas e conteúdos', + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); return activities; } - // Adicionar atividades baseadas nas estatísticas das turmas + // Adicionar atividades baseadas nas estatísticas das disciplinas 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( + _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( + _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( + _buildActivityItem( + context, + '${stats.totalContent} conteúdos disponíveis em ${stats.className}', + 'Atualizado', + Theme.of(context).colorScheme.secondary, + ), + ); activities.add(const SizedBox(height: 8)); } } @@ -417,14 +420,16 @@ class _TeacherHeroWidgetState extends State { activities.removeLast(); } - return activities.isEmpty ? [ - _buildActivityItem( - context, - 'Nenhuma atividade recente', - 'Comece criando turmas e conteúdos', - Theme.of(context).colorScheme.onSurfaceVariant, - ) - ] : activities; + return activities.isEmpty + ? [ + _buildActivityItem( + context, + 'Nenhuma atividade recente', + 'Comece criando disciplinas e conteúdos', + Theme.of(context).colorScheme.onSurfaceVariant, + ), + ] + : activities; } Widget _buildActivityItem( diff --git a/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart index 4d61426..37925ff 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart @@ -2,7 +2,6 @@ import 'dart:math'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; @@ -21,7 +20,7 @@ class TeacherQuickActionsWidget extends StatefulWidget { class _TeacherQuickActionsWidgetState extends State { bool _isCreatingClass = false; - /// Mesmas dimensões dos cards em "As Minhas Turmas". + /// Mesmas dimensões dos cards em "As Minhas Disciplinas". static const double _scrollCardWidth = 200; static const double _scrollRowHeight = 150; static const double _cardMinHeight = 150; @@ -41,68 +40,52 @@ class _TeacherQuickActionsWidgetState extends State { ]; return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Ações Rápidas', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), - SizedBox( - height: _scrollRowHeight, - child: ListView.separated( - scrollDirection: Axis.horizontal, - clipBehavior: Clip.none, - padding: const EdgeInsets.only(right: 16), - itemCount: cards.length, - separatorBuilder: (_, __) => const SizedBox(width: 12), - itemBuilder: (context, index) => SizedBox( - width: _scrollCardWidth, - child: cards[index], - ), - ), - ), - ], - ) - .animate() - .slideY( - duration: const Duration(milliseconds: 800), - curve: Curves.easeOut, - ) - .then(delay: const Duration(milliseconds: 200)); + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Ações Rápidas', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + SizedBox( + height: _scrollRowHeight, + child: ListView.separated( + scrollDirection: Axis.horizontal, + clipBehavior: Clip.none, + padding: const EdgeInsets.only(right: 16), + itemCount: cards.length, + separatorBuilder: (_, __) => const SizedBox(width: 12), + itemBuilder: (context, index) => + SizedBox(width: _scrollCardWidth, child: cards[index]), + ), + ), + ], + ); } Widget _buildUploadContentCard(BuildContext context) { return ClipRRect( borderRadius: BorderRadius.circular(16), child: DashboardActionCard( - title: 'Upload Conteúdo', - subtitle: 'PDFs, textos, imagens', - icon: Icons.upload_file, - useGradient: true, - minHeight: _cardMinHeight, - iconSize: _iconSize, - iconPadding: _iconPadding, - titleFontSize: _titleFontSize, - subtitleFontSize: _subtitleFontSize, - padding: _cardPadding, - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (_) => const TeacherMaterialsPage(), - ), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 100)), + title: 'Upload Conteúdo', + subtitle: 'PDFs, textos, imagens', + icon: Icons.upload_file, + useGradient: true, + minHeight: _cardMinHeight, + iconSize: _iconSize, + iconPadding: _iconPadding, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + padding: _cardPadding, + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => const TeacherMaterialsPage()), + ), + ), ); } @@ -110,53 +93,41 @@ class _TeacherQuickActionsWidgetState extends State { return ClipRRect( borderRadius: BorderRadius.circular(16), child: DashboardActionCardSurface( - title: 'Criar Quiz', - subtitle: 'Avaliações interativas', - icon: Icons.quiz, - minHeight: _cardMinHeight, - titleFontSize: _titleFontSize, - subtitleFontSize: _subtitleFontSize, - iconSize: _iconSize, - padding: _cardPadding, - onTap: () => context.go('/teacher/quiz/create'), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 200)), + title: 'Criar Quiz', + subtitle: 'Avaliações interativas', + icon: Icons.quiz, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + onTap: () => context.go('/teacher/quiz/create'), + ), ); } Widget _buildViewAnalyticsCard(BuildContext context) { final cs = Theme.of(context).colorScheme; return DashboardActionCardSurface( - title: 'Analytics', - subtitle: 'Desempenho da turma', - icon: Icons.analytics, - minHeight: _cardMinHeight, - titleFontSize: _titleFontSize, - subtitleFontSize: _subtitleFontSize, - iconSize: _iconSize, - padding: _cardPadding, - iconColor: cs.primary, - leadingWidget: Container( - padding: const EdgeInsets.all(_iconPadding), - decoration: BoxDecoration( - color: cs.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon(Icons.analytics, color: cs.primary, size: _iconSize), - ), - onTap: () => context.go('/teacher/analytics'), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 400)); + title: 'Analytics', + subtitle: 'Desempenho da disciplina', + icon: Icons.analytics, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + iconColor: cs.primary, + leadingWidget: Container( + padding: const EdgeInsets.all(_iconPadding), + decoration: BoxDecoration( + color: cs.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(Icons.analytics, color: cs.primary, size: _iconSize), + ), + onTap: () => context.go('/teacher/analytics'), + ); } Widget _buildCreateClassCard(BuildContext context) { @@ -164,41 +135,35 @@ class _TeacherQuickActionsWidgetState extends State { return ClipRRect( borderRadius: BorderRadius.circular(16), child: DashboardActionCardSurface( - title: 'Criar Turma', - subtitle: 'Gerar código de acesso', - icon: Icons.school, - minHeight: _cardMinHeight, - titleFontSize: _titleFontSize, - subtitleFontSize: _subtitleFontSize, - iconSize: _iconSize, - padding: _cardPadding, - iconColor: cs.primary, - onTapDisabled: _isCreatingClass, - onTap: () => _showCreateClassDialog(context), - leadingWidget: Container( - padding: const EdgeInsets.all(_iconPadding), - decoration: BoxDecoration( - color: cs.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: _isCreatingClass - ? SizedBox( - width: _iconSize, - height: _iconSize, - child: CircularProgressIndicator( - strokeWidth: 2, - color: cs.primary, - ), - ) - : Icon(Icons.school, color: cs.primary, size: _iconSize), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 150)), + title: 'Criar Disciplina', + subtitle: 'Gerar código de acesso', + icon: Icons.school, + minHeight: _cardMinHeight, + titleFontSize: _titleFontSize, + subtitleFontSize: _subtitleFontSize, + iconSize: _iconSize, + padding: _cardPadding, + iconColor: cs.primary, + onTapDisabled: _isCreatingClass, + onTap: () => _showCreateClassDialog(context), + leadingWidget: Container( + padding: const EdgeInsets.all(_iconPadding), + decoration: BoxDecoration( + color: cs.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: _isCreatingClass + ? SizedBox( + width: _iconSize, + height: _iconSize, + child: CircularProgressIndicator( + strokeWidth: 2, + color: cs.primary, + ), + ) + : Icon(Icons.school, color: cs.primary, size: _iconSize), + ), + ), ); } @@ -288,7 +253,7 @@ class _TeacherQuickActionsWidgetState extends State { ), const SizedBox(height: 16), Text( - 'Turma:', + 'Ano letivo:', style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, fontSize: 14, @@ -308,7 +273,7 @@ class _TeacherQuickActionsWidgetState extends State { ), const SizedBox(width: 10), Text( - 'A carregar turmas...', + 'A carregar disciplinas...', style: TextStyle( color: Theme.of( context, @@ -347,7 +312,7 @@ class _TeacherQuickActionsWidgetState extends State { ), ), hint: Text( - 'Seleciona a turma', + 'Seleciona o ano letivo', style: TextStyle( color: Theme.of( context, @@ -447,7 +412,7 @@ class _TeacherQuickActionsWidgetState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - 'Turma "$className" criada com sucesso! Código: $classCode', + 'Disciplina "$className" criada com sucesso! Código: $classCode', ), backgroundColor: Theme.of(context).colorScheme.primary, behavior: SnackBarBehavior.floating, @@ -461,7 +426,7 @@ class _TeacherQuickActionsWidgetState extends State { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Erro ao criar turma: $e'), + content: Text('Erro ao criar disciplina: $e'), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( diff --git a/lib/features/materials/presentation/pages/teacher_materials_page.dart b/lib/features/materials/presentation/pages/teacher_materials_page.dart index 81ec316..626bdf5 100644 --- a/lib/features/materials/presentation/pages/teacher_materials_page.dart +++ b/lib/features/materials/presentation/pages/teacher_materials_page.dart @@ -46,11 +46,8 @@ class _TeacherMaterialsPageState extends State { return Scaffold( appBar: AppBar( title: const Text( - 'Materiais da Turma', - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), + 'Materiais da Disciplina', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), ), backgroundColor: const Color(0xFF82C9BD), elevation: 0, @@ -88,10 +85,7 @@ class _TeacherMaterialsPageState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFF82C9BD), - Color(0xFFF8F9FA), - ], + colors: [Color(0xFF82C9BD), Color(0xFFF8F9FA)], stops: [0.0, 0.4], ), ), @@ -101,9 +95,7 @@ class _TeacherMaterialsPageState extends State { builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( - child: CircularProgressIndicator( - color: Color(0xFF82C9BD), - ), + child: CircularProgressIndicator(color: Color(0xFF82C9BD)), ); } @@ -168,13 +160,19 @@ class _TeacherMaterialsPageState extends State { padding: const EdgeInsets.all(16), itemCount: materials.length, itemBuilder: (context, index) { - final material = materials[index].data() as Map; + final material = + materials[index].data() as Map; final fileName = material['fileName'] ?? 'Ficheiro sem nome'; final createdAt = material['createdAt'] as Timestamp?; // Inferir tipo pela extensão do filename final extension = path.extension(fileName).toLowerCase(); - final fileType = extension == '.pdf' ? 'pdf' : - (extension == '.jpg' || extension == '.jpeg' || extension == '.png') ? 'image' : 'other'; + final fileType = extension == '.pdf' + ? 'pdf' + : (extension == '.jpg' || + extension == '.jpeg' || + extension == '.png') + ? 'image' + : 'other'; final docId = materials[index].id; final url = material['url'] as String?; @@ -204,9 +202,7 @@ class _TeacherMaterialsPageState extends State { builder: (context) => Container( decoration: const BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.vertical( - top: Radius.circular(20), - ), + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), child: SafeArea( child: Padding( @@ -291,10 +287,7 @@ class _TeacherMaterialsPageState extends State { decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: color.withOpacity(0.2), - width: 1, - ), + border: Border.all(color: color.withOpacity(0.2), width: 1), ), child: Row( children: [ @@ -305,11 +298,7 @@ class _TeacherMaterialsPageState extends State { color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), - child: Icon( - icon, - color: color, - size: 24, - ), + child: Icon(icon, color: color, size: 24), ), const SizedBox(width: 16), Expanded( @@ -327,19 +316,12 @@ class _TeacherMaterialsPageState extends State { const SizedBox(height: 2), Text( subtitle, - style: TextStyle( - fontSize: 13, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 13, color: Colors.grey[600]), ), ], ), ), - Icon( - Icons.arrow_forward_ios, - color: Colors.grey[400], - size: 16, - ), + Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16), ], ), ), @@ -424,7 +406,7 @@ class _TeacherMaterialsPageState extends State { return; } - // Carregar as turmas do professor + // Carregar as disciplinas do professor List> teacherClasses = []; try { final snapshot = await _firestore @@ -440,7 +422,7 @@ class _TeacherMaterialsPageState extends State { if (!mounted) return; - // Se o professor não tem turmas, fazer upload sem associar turma + // Se o professor não tem disciplinas, fazer upload sem associar disciplina if (teacherClasses.isEmpty) { await _uploadFile( filePath: filePath, @@ -450,7 +432,7 @@ class _TeacherMaterialsPageState extends State { return; } - // Mostrar diálogo de seleção de turma + // Mostrar diálogo de seleção de disciplina String? selectedClassId = await showDialog( context: context, builder: (dialogContext) { @@ -461,7 +443,7 @@ class _TeacherMaterialsPageState extends State { borderRadius: BorderRadius.circular(16), ), title: const Text( - 'Escolher Turma', + 'Escolher Disciplina', style: TextStyle(fontWeight: FontWeight.bold), ), content: Column( @@ -469,7 +451,7 @@ class _TeacherMaterialsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( - 'Seleciona a turma que terá acesso a este material:', + 'Seleciona a disciplina que terá acesso a este material:', style: TextStyle(fontSize: 14), ), const SizedBox(height: 16), @@ -501,7 +483,7 @@ class _TeacherMaterialsPageState extends State { vertical: 10, ), ), - hint: const Text('Seleciona a turma'), + hint: const Text('Seleciona a disciplina'), items: teacherClasses .map( (c) => DropdownMenuItem( @@ -510,8 +492,7 @@ class _TeacherMaterialsPageState extends State { ), ) .toList(), - onChanged: (value) => - setDialogState(() => picked = value), + onChanged: (value) => setDialogState(() => picked = value), ), ], ), @@ -763,9 +744,7 @@ class _TeacherMaterialsPageState extends State { return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: ListTile( contentPadding: const EdgeInsets.symmetric( horizontal: 16, @@ -778,11 +757,7 @@ class _TeacherMaterialsPageState extends State { color: iconColor.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), - child: Icon( - iconData, - color: iconColor, - size: 28, - ), + child: Icon(iconData, color: iconColor, size: 28), ), title: Text( fileName, diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index 38595cd..83f242d 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -70,14 +70,16 @@ class _QuizListPageState extends State .collection('enrollments') .where('studentId', isEqualTo: uid) .get(); - Logger.info('Enrollments found for student $uid: ${enrollSnap.docs.length}'); - + Logger.info( + 'Enrollments found for student $uid: ${enrollSnap.docs.length}', + ); + final classIds = enrollSnap.docs .map((d) => d.data()['classId'] as String?) .whereType() .toSet(); Logger.info('ClassIds from enrollments: $classIds'); - + if (classIds.isEmpty) { Logger.info('No classIds found for student, returning empty'); if (mounted) setState(() => _loadingTeacherQuizzes = false); @@ -85,7 +87,10 @@ class _QuizListPageState extends State } // Obter também os teacherIds (fallback para quizzes sem classIds) final classSnaps = await Future.wait( - classIds.map((id) => FirebaseFirestore.instance.collection('classes').doc(id).get()), + classIds.map( + (id) => + FirebaseFirestore.instance.collection('classes').doc(id).get(), + ), ); final teacherIds = classSnaps .where((d) => d.exists) @@ -98,7 +103,7 @@ class _QuizListPageState extends State // Simplificar: tentar query básica primeiro final classIdList = classIds.toList(); final batches = >[]; - + // Query 1: por teacherId (mais simples e compatível) if (teacherIds.isNotEmpty) { Logger.info('Executing simple teacherIds query with: $teacherIds'); @@ -111,7 +116,7 @@ class _QuizListPageState extends State batches.add(query.get()); Logger.info('Added teacherIds batch query: $batch'); } - + // Query 2: por classIds (se a primeira não retornar resultados) if (classIdList.isNotEmpty) { Logger.info('Executing classIds query with: $classIdList'); @@ -153,18 +158,19 @@ class _QuizListPageState extends State final allQuizzes = allSnap.docs .map((d) => {'id': d.id, ...d.data() as Map}) .toList(); - + // Filtrar manualmente para testar final filteredQuizzes = allQuizzes.where((quiz) { final quizTeacherId = quiz['teacherId'] as String?; - final quizClassIds = (quiz['classIds'] as List?)?.cast() ?? []; - return teacherIds.contains(quizTeacherId) || - quizClassIds.any((cid) => classIds.contains(cid)); + final quizClassIds = + (quiz['classIds'] as List?)?.cast() ?? []; + return teacherIds.contains(quizTeacherId) || + quizClassIds.any((cid) => classIds.contains(cid)); }).toList(); - + Logger.info('Manual filtered quizzes: ${filteredQuizzes.length}'); Logger.info('=== END ALL QUIZZES NO FILTER ==='); - + // Usar este resultado temporariamente para debug if (filteredQuizzes.isNotEmpty) { Logger.info('Using manually filtered quizzes for UI'); @@ -183,7 +189,7 @@ class _QuizListPageState extends State // Executar queries e processar resultados final results = await Future.wait(batches); Logger.info('Query batches completed: ${results.length} results'); - + // deduplicar por id (pode aparecer em múltiplos batches) final seen = {}; final quizzes = results @@ -191,13 +197,17 @@ class _QuizListPageState extends State .where((d) => seen.add(d.id)) .map((d) => {'id': d.id, ...d.data() as Map}) .toList(); - + Logger.info('Final quizzes after deduplication: ${quizzes.length}'); for (final quiz in quizzes.take(3)) { - Logger.info('Quiz sample: ${quiz['materialName']} - classIds: ${quiz['classIds']} - teacherId: ${quiz['teacherId']}'); + Logger.info( + 'Quiz sample: ${quiz['materialName']} - classIds: ${quiz['classIds']} - teacherId: ${quiz['teacherId']}', + ); } if (mounted) { - Logger.info('Updating UI state: _teacherQuizzes.length = ${quizzes.length}'); + Logger.info( + 'Updating UI state: _teacherQuizzes.length = ${quizzes.length}', + ); setState(() { _teacherQuizzes = quizzes; _loadingTeacherQuizzes = false; @@ -558,7 +568,7 @@ class _QuizListPageState extends State ), const SizedBox(height: 8), Text( - 'Inscreve-te numa turma para aceder aos PDFs do professor.', + 'Inscreve-te numa disciplina para aceder aos PDFs do professor.', textAlign: TextAlign.center, style: TextStyle( color: cs.onSurfaceVariant.withOpacity(0.7), @@ -1233,7 +1243,7 @@ class _TeacherQuizInteractiveSheetState 'total': widget.questions.length, 'submittedAt': FieldValue.serverTimestamp(), }); - + // Registrar atividade no sistema de gamificação await GamificationService.recordQuizActivity( user.uid, diff --git a/lib/features/quiz/presentation/pages/quiz_management_page.dart b/lib/features/quiz/presentation/pages/quiz_management_page.dart index 39ae49d..4cc78e3 100644 --- a/lib/features/quiz/presentation/pages/quiz_management_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_management_page.dart @@ -34,7 +34,7 @@ class _QuizManagementPageState extends State { .collection('users') .doc(user.uid) .get(); - + if (userDoc.exists) { setState(() { _userRole = userDoc.data()?['role'] ?? ''; @@ -49,7 +49,7 @@ class _QuizManagementPageState extends State { if (user == null) return; Query query; - + if (_userRole == 'teacher') { // Professor: ver todos os quizzes criados query = FirebaseFirestore.instance @@ -59,7 +59,10 @@ class _QuizManagementPageState extends State { } 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('userStats') + .doc(user.uid) + .get(), FirebaseFirestore.instance .collection('quizHistory') .doc(user.uid) @@ -67,12 +70,12 @@ class _QuizManagementPageState extends State { .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); @@ -81,15 +84,17 @@ class _QuizManagementPageState extends State { 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() ?? []; - + final masteredConcepts = + (stats['masteredConcepts'] as List?) + ?.map((c) => Map.from(c as Map)) + .toList() ?? + []; + for (final concept in masteredConcepts) { quizList.add({ 'id': concept['conceptName'] ?? '', @@ -102,7 +107,7 @@ class _QuizManagementPageState extends State { } } } - + setState(() { _quizHistory = quizList; _loading = false; @@ -111,13 +116,16 @@ class _QuizManagementPageState extends State { } 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(); + _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) { @@ -132,21 +140,27 @@ class _QuizManagementPageState extends State { 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.'; + 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"?'; + 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?'; + confirmMessage = + 'Tem certeza que deseja remover o conceito "$quizTitle" do seu histórico?'; successMessage = 'Conceito removido com sucesso!'; } } - - final confirmed = await _showDeleteConfirmation(quizTitle, confirmMessage); + + final confirmed = await _showDeleteConfirmation( + quizTitle, + confirmMessage, + ); if (!confirmed) return; if (_userRole == 'teacher') { @@ -155,13 +169,13 @@ class _QuizManagementPageState extends State { .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(); } @@ -186,26 +200,30 @@ class _QuizManagementPageState extends State { .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() ?? []; - + 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]) - }); + 'masteredConcepts': FieldValue.arrayRemove([ + conceptToRemove, + ]), + }); } } } @@ -213,7 +231,7 @@ class _QuizManagementPageState extends State { } _loadQuizHistory(); // Recarregar lista - + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -239,7 +257,9 @@ class _QuizManagementPageState extends State { final result = await showDialog( context: context, builder: (context) => AlertDialog( - title: Text(_userRole == 'teacher' ? 'Eliminar Quiz' : 'Confirmar Eliminação'), + title: Text( + _userRole == 'teacher' ? 'Eliminar Quiz' : 'Confirmar Eliminação', + ), content: Text(message), actions: [ TextButton( @@ -264,15 +284,25 @@ class _QuizManagementPageState extends State { return Scaffold( backgroundColor: cs.surface, appBar: AppBar( - title: Text(_userRole == 'teacher' ? 'Gerenciar Quizzes' : 'Meu Histórico'), + 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'), + onPressed: () { + if (Navigator.of(context).canPop()) { + Navigator.of(context).pop(); + } else { + context.go( + _userRole == 'teacher' + ? '/teacher-dashboard' + : '/student-dashboard', + ); + } + }, ), ), body: Container( @@ -280,34 +310,31 @@ class _QuizManagementPageState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - cs.primary.withValues(alpha: 0.05), - cs.surface, - ], + 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)); - }, - ), + ? _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), @@ -315,13 +342,17 @@ class _QuizManagementPageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( - _userRole == 'teacher' ? Icons.quiz_outlined : Icons.history_edu_outlined, + _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', + _userRole == 'teacher' + ? 'Nenhum quiz criado' + : 'Nenhum quiz no histórico', style: TextStyle( color: cs.onSurface, fontSize: 20, @@ -331,13 +362,10 @@ class _QuizManagementPageState extends State { ), const SizedBox(height: 8), Text( - _userRole == 'teacher' + _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, - ), + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), textAlign: TextAlign.center, ), ], @@ -348,7 +376,7 @@ class _QuizManagementPageState extends State { Widget _buildQuizCard(Map quiz) { final cs = Theme.of(context).colorScheme; - + return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), @@ -405,11 +433,12 @@ class _QuizManagementPageState extends State { ), ), IconButton( - onPressed: () => _deleteQuiz(quiz['id'], quiz['title'] ?? 'Quiz', quiz['type'] ?? 'unknown'), - icon: Icon( - Icons.delete_outline, - color: Colors.red, + onPressed: () => _deleteQuiz( + quiz['id'], + quiz['title'] ?? 'Quiz', + quiz['type'] ?? 'unknown', ), + icon: Icon(Icons.delete_outline, color: Colors.red), tooltip: _userRole == 'teacher' ? 'Eliminar Quiz' : 'Remover', ), ], @@ -420,13 +449,16 @@ class _QuizManagementPageState extends State { spacing: 4, children: (quiz['classIds'] as List).map((classId) { return Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: cs.primaryContainer, borderRadius: BorderRadius.circular(8), ), child: Text( - 'Turma: $classId', + 'Disciplina: $classId', style: TextStyle( color: cs.onPrimaryContainer, fontSize: 10, @@ -443,7 +475,7 @@ class _QuizManagementPageState extends State { String _formatDate(dynamic date) { if (date == null) return 'Data desconhecida'; - + DateTime dateTime; if (date is Timestamp) { dateTime = date.toDate(); @@ -452,7 +484,7 @@ class _QuizManagementPageState extends State { } else { return 'Data desconhecida'; } - + return '${dateTime.day}/${dateTime.month}/${dateTime.year}'; } } diff --git a/lib/features/quiz/presentation/pages/teacher_quiz_page.dart b/lib/features/quiz/presentation/pages/teacher_quiz_page.dart index 2874cc4..687badb 100644 --- a/lib/features/quiz/presentation/pages/teacher_quiz_page.dart +++ b/lib/features/quiz/presentation/pages/teacher_quiz_page.dart @@ -24,11 +24,11 @@ class _QuizQuestion { }); Map toJson() => { - 'q': question, - 'opts': options, - 'ans': correctIndex, - 'exp': explanation, - }; + 'q': question, + 'opts': options, + 'ans': correctIndex, + 'exp': explanation, + }; static _QuizQuestion? fromMap(dynamic e) { if (e is! Map) return null; @@ -38,7 +38,12 @@ class _QuizQuestion { final exp = e['exp'] as String? ?? ''; if (q == null || opts == null || ans == null) return null; if (opts.length < 2 || ans < 0 || ans >= opts.length) return null; - return _QuizQuestion(question: q, options: opts, correctIndex: ans, explanation: exp); + return _QuizQuestion( + question: q, + options: opts, + correctIndex: ans, + explanation: exp, + ); } } @@ -98,17 +103,33 @@ class _TeacherQuizPageState extends State Future _loadMaterials() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; - if (uid == null) { if (mounted) setState(() => _loadingMaterials = false); return; } + if (uid == null) { + if (mounted) setState(() => _loadingMaterials = false); + return; + } final snap = await FirebaseFirestore.instance .collection('materials') .where('teacherId', isEqualTo: uid) .orderBy('createdAt', descending: true) .get(); final mats = snap.docs - .where((d) => (d.data()['fileName'] as String? ?? '').toLowerCase().endsWith('.pdf')) - .map((d) => {'id': d.id, 'name': d.data()['fileName'] as String? ?? 'Material'}) + .where( + (d) => (d.data()['fileName'] as String? ?? '') + .toLowerCase() + .endsWith('.pdf'), + ) + .map( + (d) => { + 'id': d.id, + 'name': d.data()['fileName'] as String? ?? 'Material', + }, + ) .toList(); - if (mounted) setState(() { _materials = mats; _loadingMaterials = false; }); + if (mounted) + setState(() { + _materials = mats; + _loadingMaterials = false; + }); } catch (e) { Logger.error('Teacher quiz load materials: $e'); if (mounted) setState(() => _loadingMaterials = false); @@ -118,7 +139,10 @@ class _TeacherQuizPageState extends State Future _loadHistory() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; - if (uid == null) { if (mounted) setState(() => _loadingHistory = false); return; } + if (uid == null) { + if (mounted) setState(() => _loadingHistory = false); + return; + } final snap = await FirebaseFirestore.instance .collection('teacherQuizzes') .where('teacherId', isEqualTo: uid) @@ -126,7 +150,11 @@ class _TeacherQuizPageState extends State .limit(30) .get(); final list = snap.docs.map((d) => {'id': d.id, ...d.data()}).toList(); - if (mounted) setState(() { _history = list; _loadingHistory = false; }); + if (mounted) + setState(() { + _history = list; + _loadingHistory = false; + }); } catch (e) { Logger.error('Teacher quiz load history: $e'); if (mounted) setState(() => _loadingHistory = false); @@ -163,7 +191,8 @@ class _TeacherQuizPageState extends State final questions = _parseQuizJson(raw); if (questions.isEmpty) { - if (mounted) _showSnack('Não foi possível gerar o quiz. Tenta novamente.'); + if (mounted) + _showSnack('Não foi possível gerar o quiz. Tenta novamente.'); return; } @@ -242,16 +271,14 @@ class _TeacherQuizPageState extends State ), body: TabBarView( controller: _tabController, - children: [ - _buildMaterialsTab(cs), - _buildHistoryTab(cs), - ], + children: [_buildMaterialsTab(cs), _buildHistoryTab(cs)], ), ); } Widget _buildMaterialsTab(ColorScheme cs) { - if (_loadingMaterials) return const Center(child: CircularProgressIndicator()); + if (_loadingMaterials) + return const Center(child: CircularProgressIndicator()); if (_materials.isEmpty) { return Center( child: Padding( @@ -259,13 +286,25 @@ class _TeacherQuizPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.folder_open, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)), + Icon( + Icons.folder_open, + size: 64, + color: cs.onSurfaceVariant.withValues(alpha: 0.4), + ), const SizedBox(height: 16), - Text('Sem PDFs carregados.', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16)), + Text( + 'Sem PDFs carregados.', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), const SizedBox(height: 8), - Text('Faz upload de um PDF nos teus materiais primeiro.', - textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant.withValues(alpha: 0.7), fontSize: 13)), + Text( + 'Faz upload de um PDF nos teus materiais primeiro.', + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurfaceVariant.withValues(alpha: 0.7), + fontSize: 13, + ), + ), ], ), ), @@ -278,7 +317,9 @@ class _TeacherQuizPageState extends State itemBuilder: (context, i) { final mat = _materials[i]; final isGenerating = _generatingForId == mat['id']; - final name = (mat['name'] ?? 'Material').replaceAll('.pdf', '').replaceAll('_', ' '); + final name = (mat['name'] ?? 'Material') + .replaceAll('.pdf', '') + .replaceAll('_', ' '); return _MaterialCard( name: name, isGenerating: isGenerating, @@ -290,7 +331,8 @@ class _TeacherQuizPageState extends State } Widget _buildHistoryTab(ColorScheme cs) { - if (_loadingHistory) return const Center(child: CircularProgressIndicator()); + if (_loadingHistory) + return const Center(child: CircularProgressIndicator()); if (_history.isEmpty) { return Center( child: Padding( @@ -298,10 +340,16 @@ class _TeacherQuizPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.history, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)), + Icon( + Icons.history, + size: 64, + color: cs.onSurfaceVariant.withValues(alpha: 0.4), + ), const SizedBox(height: 16), - Text('Ainda não criaste nenhum quiz.', - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16)), + Text( + 'Ainda não criaste nenhum quiz.', + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), ], ), ), @@ -328,10 +376,19 @@ class _TeacherQuizPageState extends State color: cs.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: cs.outline.withValues(alpha: 0.15)), - boxShadow: [BoxShadow(color: cs.shadow.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2))], + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), leading: Container( width: 44, height: 44, @@ -341,12 +398,21 @@ class _TeacherQuizPageState extends State ), child: Icon(Icons.quiz, color: cs.primary, size: 22), ), - title: Text(name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface)), + title: Text( + name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: cs.onSurface, + ), + ), subtitle: dateStr.isNotEmpty - ? Text(dateStr, style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)) + ? Text( + dateStr, + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ) : null, trailing: Icon(Icons.bar_chart, color: cs.onSurfaceVariant), onTap: () => _showResultsPopup(item), @@ -375,7 +441,12 @@ class _MaterialCard extends StatelessWidget { final bool isGenerating; final VoidCallback? onTap; final ColorScheme cs; - const _MaterialCard({required this.name, required this.isGenerating, required this.onTap, required this.cs}); + const _MaterialCard({ + required this.name, + required this.isGenerating, + required this.onTap, + required this.cs, + }); @override Widget build(BuildContext context) { @@ -384,7 +455,13 @@ class _MaterialCard extends StatelessWidget { color: cs.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: cs.outline.withValues(alpha: 0.15)), - boxShadow: [BoxShadow(color: cs.shadow.withValues(alpha: 0.05), blurRadius: 8, offset: const Offset(0, 2))], + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -397,17 +474,28 @@ class _MaterialCard extends StatelessWidget { ), child: Icon(Icons.picture_as_pdf, color: cs.secondary, size: 22), ), - title: Text(name, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface)), - subtitle: Text('Gerar quiz com IA', - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)), + title: Text( + name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: cs.onSurface, + ), + ), + subtitle: Text( + 'Gerar quiz com IA', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ), trailing: isGenerating ? SizedBox( width: 24, height: 24, - child: CircularProgressIndicator(strokeWidth: 2.5, color: cs.primary), + child: CircularProgressIndicator( + strokeWidth: 2.5, + color: cs.primary, + ), ) : Icon(Icons.auto_awesome, color: cs.primary, size: 26), onTap: onTap, @@ -445,14 +533,16 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { super.initState(); // cópia editável _questions = widget.questions - .map((q) => _QuizQuestion( - question: q.question, - options: List.from(q.options), - correctIndex: q.correctIndex, - explanation: q.explanation, - )) + .map( + (q) => _QuizQuestion( + question: q.question, + options: List.from(q.options), + correctIndex: q.correctIndex, + explanation: q.explanation, + ), + ) .toList(); - // selecionar todas as turmas por defeito + // selecionar todas as disciplinas por defeito for (final c in widget.availableClasses) { if (c['id'] != null) _selectedClassIds.add(c['id']!); } @@ -460,12 +550,14 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { void _addBlankQuestion() { setState(() { - _questions.add(_QuizQuestion( - question: '', - options: ['A) ', 'B) ', 'C) ', 'D) '], - correctIndex: 0, - explanation: '', - )); + _questions.add( + _QuizQuestion( + question: '', + options: ['A) ', 'B) ', 'C) ', 'D) '], + correctIndex: 0, + explanation: '', + ), + ); }); } @@ -488,7 +580,10 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { Logger.error('Error publishing teacher quiz: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Erro ao publicar: $e'), behavior: SnackBarBehavior.floating), + SnackBar( + content: Text('Erro ao publicar: $e'), + behavior: SnackBarBehavior.floating, + ), ); } } finally { @@ -514,10 +609,20 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { _saving ? const Padding( padding: EdgeInsets.only(right: 16), - child: Center(child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2))), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ), ) : TextButton.icon( - onPressed: (widget.availableClasses.isEmpty || _selectedClassIds.isNotEmpty) ? _publish : null, + onPressed: + (widget.availableClasses.isEmpty || + _selectedClassIds.isNotEmpty) + ? _publish + : null, icon: const Icon(Icons.publish), label: const Text('Publicar'), ), @@ -525,7 +630,7 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { ), body: ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), - itemCount: _questions.length + 2, // +1 turmas header +1 add button + itemCount: _questions.length + 2, // +1 disciplinas header +1 add button itemBuilder: (context, i) { if (i == 0) return _buildClassSelector(cs); if (i == _questions.length + 1) { @@ -537,7 +642,9 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { label: const Text('Adicionar Pergunta'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), ), ); @@ -569,7 +676,13 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { 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.05), blurRadius: 8, offset: const Offset(0, 2))], + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -578,8 +691,14 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { children: [ Icon(Icons.groups, color: cs.primary, size: 20), const SizedBox(width: 8), - Text('Turmas com acesso', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: cs.onSurface)), + Text( + 'Disciplinas com acesso', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), + ), ], ), const SizedBox(height: 12), @@ -591,21 +710,39 @@ class _QuizEditorPageState extends State<_QuizEditorPage> { final name = c['name'] ?? id; final selected = _selectedClassIds.contains(id); return FilterChip( - label: Text(name, style: TextStyle(fontSize: 13, color: selected ? cs.onPrimary : cs.onSurface)), + label: Text( + name, + style: TextStyle( + fontSize: 13, + color: selected ? cs.onPrimary : cs.onSurface, + ), + ), selected: selected, - onSelected: (v) => setState(() => v ? _selectedClassIds.add(id) : _selectedClassIds.remove(id)), + onSelected: (v) => setState( + () => v + ? _selectedClassIds.add(id) + : _selectedClassIds.remove(id), + ), selectedColor: cs.primary, checkmarkColor: cs.onPrimary, - backgroundColor: cs.surfaceContainerHighest.withValues(alpha: 0.5), - side: BorderSide(color: selected ? cs.primary : cs.outline.withValues(alpha: 0.3)), + backgroundColor: cs.surfaceContainerHighest.withValues( + alpha: 0.5, + ), + side: BorderSide( + color: selected + ? cs.primary + : cs.outline.withValues(alpha: 0.3), + ), ); }).toList(), ), if (_selectedClassIds.isEmpty) Padding( padding: const EdgeInsets.only(top: 8), - child: Text('Seleciona pelo menos uma turma.', - style: TextStyle(fontSize: 12, color: cs.error)), + child: Text( + 'Seleciona pelo menos uma disciplina.', + style: TextStyle(fontSize: 12, color: cs.error), + ), ), ], ), @@ -621,7 +758,13 @@ class _QuestionEditor extends StatefulWidget { final ColorScheme cs; final VoidCallback onChanged; final VoidCallback? onDelete; - const _QuestionEditor({required this.index, required this.question, required this.cs, required this.onChanged, this.onDelete}); + const _QuestionEditor({ + required this.index, + required this.question, + required this.cs, + required this.onChanged, + this.onDelete, + }); @override State<_QuestionEditor> createState() => _QuestionEditorState(); @@ -638,7 +781,9 @@ class _QuestionEditorState extends State<_QuestionEditor> { super.initState(); _qCtrl = TextEditingController(text: widget.question.question); _expCtrl = TextEditingController(text: widget.question.explanation); - _optCtrl = widget.question.options.map((o) => TextEditingController(text: o)).toList(); + _optCtrl = widget.question.options + .map((o) => TextEditingController(text: o)) + .toList(); } @override @@ -666,7 +811,13 @@ class _QuestionEditorState extends State<_QuestionEditor> { 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.05), blurRadius: 8, offset: const Offset(0, 2))], + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], ), child: Column( children: [ @@ -686,29 +837,48 @@ class _QuestionEditorState extends State<_QuestionEditor> { borderRadius: BorderRadius.circular(8), ), child: Center( - child: Text('${widget.index + 1}', - style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold, color: cs.primary)), + child: Text( + '${widget.index + 1}', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.bold, + color: cs.primary, + ), + ), ), ), const SizedBox(width: 10), Expanded( child: Text( - widget.question.question.isEmpty ? 'Pergunta ${widget.index + 1}' : widget.question.question, + widget.question.question.isEmpty + ? 'Pergunta ${widget.index + 1}' + : widget.question.question, maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), ), ), if (widget.onDelete != null) IconButton( - icon: Icon(Icons.delete_outline, color: cs.error, size: 18), + icon: Icon( + Icons.delete_outline, + color: cs.error, + size: 18, + ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: widget.onDelete, tooltip: 'Remover', ), const SizedBox(width: 4), - Icon(_expanded ? Icons.expand_less : Icons.expand_more, color: cs.onSurfaceVariant), + Icon( + _expanded ? Icons.expand_less : Icons.expand_more, + color: cs.onSurfaceVariant, + ), ], ), ), @@ -746,31 +916,59 @@ class _QuestionEditorState extends State<_QuestionEditor> { margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( shape: BoxShape.circle, - color: isCorrect ? cs.primary : cs.surfaceContainerHighest, - border: Border.all(color: isCorrect ? cs.primary : cs.outline.withValues(alpha: 0.3)), + color: isCorrect + ? cs.primary + : cs.surfaceContainerHighest, + border: Border.all( + color: isCorrect + ? cs.primary + : cs.outline.withValues(alpha: 0.3), + ), ), child: isCorrect - ? Icon(Icons.check, size: 16, color: cs.onPrimary) + ? Icon( + Icons.check, + size: 16, + color: cs.onPrimary, + ) : Center( - child: Text(String.fromCharCode(65 + i), - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)), + child: Text( + String.fromCharCode(65 + i), + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), ), ), ), - Expanded(child: _textField(_optCtrl[i], cs, hint: 'Opção ${String.fromCharCode(65 + i)}')), + Expanded( + child: _textField( + _optCtrl[i], + cs, + hint: 'Opção ${String.fromCharCode(65 + i)}', + ), + ), ], ), ); }), - Text('Toca no círculo para marcar a correcta', - style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant)), + Text( + 'Toca no círculo para marcar a correcta', + style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant), + ), const SizedBox(height: 14), // Explicação _label('Explicação (quando erra)', cs), const SizedBox(height: 6), - _textField(_expCtrl, cs, maxLines: 3, hint: 'Explica porque esta é a resposta correcta...'), + _textField( + _expCtrl, + cs, + maxLines: 3, + hint: 'Explica porque esta é a resposta correcta...', + ), const SizedBox(height: 12), Align( @@ -790,28 +988,47 @@ class _QuestionEditorState extends State<_QuestionEditor> { ); } - Widget _label(String text, ColorScheme cs) => Text(text, - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant)); + Widget _label(String text, ColorScheme cs) => Text( + text, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: cs.onSurfaceVariant, + ), + ); - Widget _textField(TextEditingController ctrl, ColorScheme cs, {int maxLines = 1, String? hint}) => - TextField( - controller: ctrl, - maxLines: maxLines, - style: TextStyle(fontSize: 14, color: cs.onSurface), - decoration: InputDecoration( - hintText: hint, - hintStyle: TextStyle(color: cs.onSurfaceVariant.withValues(alpha: 0.5), fontSize: 13), - filled: true, - fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.5), - contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), - enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide(color: cs.primary, width: 1.5), - ), - ), - ); + Widget _textField( + TextEditingController ctrl, + ColorScheme cs, { + int maxLines = 1, + String? hint, + }) => TextField( + controller: ctrl, + maxLines: maxLines, + style: TextStyle(fontSize: 14, color: cs.onSurface), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle( + color: cs.onSurfaceVariant.withValues(alpha: 0.5), + fontSize: 13, + ), + filled: true, + fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.5), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: cs.primary, width: 1.5), + ), + ), + ); } // ─── Popup de resultados por aluno ──────────────────────────────────────────── @@ -851,9 +1068,13 @@ class _StudentResultsDialogState extends State<_StudentResultsDialog> { String studentName = data['studentName'] as String? ?? 'Aluno'; if (studentId != null && studentName == 'Aluno') { try { - final userDoc = await FirebaseFirestore.instance.collection('users').doc(studentId).get(); + final userDoc = await FirebaseFirestore.instance + .collection('users') + .doc(studentId) + .get(); if (userDoc.exists) { - studentName = userDoc.data()?['displayName'] as String? ?? + studentName = + userDoc.data()?['displayName'] as String? ?? userDoc.data()?['name'] as String? ?? studentName; } @@ -861,7 +1082,11 @@ class _StudentResultsDialogState extends State<_StudentResultsDialog> { } results.add({...data, 'studentName': studentName}); } - if (mounted) setState(() { _results = results; _loading = false; }); + if (mounted) + setState(() { + _results = results; + _loading = false; + }); } catch (e) { Logger.error('Error loading quiz submissions: $e'); if (mounted) setState(() => _loading = false); @@ -884,7 +1109,9 @@ class _StudentResultsDialogState extends State<_StudentResultsDialog> { padding: const EdgeInsets.fromLTRB(20, 20, 12, 16), decoration: BoxDecoration( color: cs.primary.withValues(alpha: 0.08), - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(20), + ), ), child: Row( children: [ @@ -895,7 +1122,11 @@ class _StudentResultsDialogState extends State<_StudentResultsDialog> { widget.quizTitle, maxLines: 2, overflow: TextOverflow.ellipsis, - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15, color: cs.onSurface), + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: cs.onSurface, + ), ), ), IconButton( @@ -909,69 +1140,101 @@ class _StudentResultsDialogState extends State<_StudentResultsDialog> { // Body Flexible( child: _loading - ? const Center(child: Padding(padding: EdgeInsets.all(32), child: CircularProgressIndicator())) + ? const Center( + child: Padding( + padding: EdgeInsets.all(32), + child: CircularProgressIndicator(), + ), + ) : _results.isEmpty - ? Center( - child: Padding( - padding: const EdgeInsets.all(32), - child: Text( - 'Nenhum aluno submeteu este quiz ainda.', - textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant), - ), - ), - ) - : ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - shrinkWrap: true, - itemCount: _results.length, - separatorBuilder: (_, __) => const SizedBox(height: 8), - itemBuilder: (_, i) { - final r = _results[i]; - final score = r['score'] as int? ?? 0; - final total = r['total'] as int? ?? 1; - final pct = (score / total * 100).round(); - final Color c = pct >= 80 - ? const Color(0xFF10B981) - : pct >= 50 - ? const Color(0xFFF59E0B) - : const Color(0xFFEF4444); - final ts = r['submittedAt']; - String dateStr = ''; - if (ts is Timestamp) { - final dt = ts.toDate(); - dateStr = '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; - } - return Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: c.withValues(alpha: 0.07), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: c.withValues(alpha: 0.2)), - ), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(r['studentName'] as String, - style: TextStyle(fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface)), - if (dateStr.isNotEmpty) - Text(dateStr, - style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant)), - ], - ), - ), - Text('$score/$total', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16, color: c)), - const SizedBox(width: 6), - Text('($pct%)', style: TextStyle(fontSize: 12, color: c)), - ], - ), - ); - }, + ? Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + 'Nenhum aluno submeteu este quiz ainda.', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant), ), + ), + ) + : ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + shrinkWrap: true, + itemCount: _results.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (_, i) { + final r = _results[i]; + final score = r['score'] as int? ?? 0; + final total = r['total'] as int? ?? 1; + final pct = (score / total * 100).round(); + final Color c = pct >= 80 + ? const Color(0xFF10B981) + : pct >= 50 + ? const Color(0xFFF59E0B) + : const Color(0xFFEF4444); + final ts = r['submittedAt']; + String dateStr = ''; + if (ts is Timestamp) { + final dt = ts.toDate(); + dateStr = + '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; + } + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + decoration: BoxDecoration( + color: c.withValues(alpha: 0.07), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: c.withValues(alpha: 0.2)), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + r['studentName'] as String, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 14, + color: cs.onSurface, + ), + ), + if (dateStr.isNotEmpty) + Text( + dateStr, + style: TextStyle( + fontSize: 11, + color: cs.onSurfaceVariant, + ), + ), + ], + ), + ), + Text( + '$score/$total', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: c, + ), + ), + const SizedBox(width: 6), + Text( + '($pct%)', + style: TextStyle(fontSize: 12, color: c), + ), + ], + ), + ); + }, + ), ), ], ),