diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart index 2180e5a..641427a 100644 --- a/lib/features/classes/presentation/pages/join_class_page.dart +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -1,16 +1,17 @@ import 'package:cloud_firestore/cloud_firestore.dart'; 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 -class JoinClassPage extends StatefulWidget { +class JoinClassPage extends ConsumerStatefulWidget { const JoinClassPage({super.key}); @override - State createState() => _JoinClassPageState(); + ConsumerState createState() => _JoinClassPageState(); } -class _JoinClassPageState extends State { +class _JoinClassPageState extends ConsumerState { final _codeController = TextEditingController(); bool _isLoading = false; @@ -67,8 +68,7 @@ class _JoinClassPageState extends State { final classDoc = classQuery.docs.first; final classId = classDoc.id; - final classSchoolClassId = - classDoc.data()['schoolClassId'] as String?; + final classSchoolClassId = classDoc.data()['schoolClassId'] as String?; // Verificar se o aluno está autorizado a entrar nesta turma // O schoolClassId do aluno deve corresponder ao schoolClassId da disciplina @@ -112,11 +112,22 @@ class _JoinClassPageState extends State { // Mostrar sucesso if (mounted) { + final colorScheme = Theme.of(context).colorScheme; ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Entraste na turma com sucesso!'), - backgroundColor: Color(0xFF10B981), - duration: Duration(seconds: 2), + SnackBar( + content: Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 8), + const Text('Entraste na turma com sucesso!'), + ], + ), + backgroundColor: colorScheme.primary, + duration: const Duration(seconds: 2), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), ), ); @@ -130,149 +141,436 @@ class _JoinClassPageState extends State { } void _showError(String message) { + final colorScheme = Theme.of(context).colorScheme; ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(message), - backgroundColor: const Color(0xFFEF4444), + content: Row( + children: [ + Icon(Icons.error_outline, color: Colors.white), + const SizedBox(width: 8), + Expanded(child: Text(message)), + ], + ), + backgroundColor: colorScheme.error, duration: const Duration(seconds: 3), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), ), ); } @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + final isDark = theme.brightness == Brightness.dark; + return Scaffold( - backgroundColor: const Color(0xFFF8F9FA), - appBar: AppBar( - backgroundColor: const Color(0xFF82C9BD), - elevation: 0, - leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - title: const Text( - 'Entrar numa Turma', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + colorScheme.surfaceContainerHighest, + colorScheme.surface, + colorScheme.surfaceContainerLow, + ] + : const [ + Color(0xFFD4E8E8), + Color(0xFFE8D4C0), + Color(0xFFD8E0E8), + ], ), ), - ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Ícone e descrição - Center( - child: Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: const Color(0xFF82C9BD).withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - ), - child: const Icon( - Icons.group_add, - color: Color(0xFF82C9BD), - size: 40, - ), - ), - ), - const SizedBox(height: 24), - const Center( - child: Text( - 'Insere o código da turma', - style: TextStyle( - color: Color(0xFF2D3748), - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(height: 8), - Center( - child: Text( - 'O professor partilhou contigo um código de 6 caracteres.', - style: TextStyle(color: Colors.grey[600], fontSize: 14), - textAlign: TextAlign.center, - ), - ), - const SizedBox(height: 32), - - // Campo de código - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: const Color(0xFFE2E8F0), width: 1), - ), - child: TextField( - controller: _codeController, - textCapitalization: TextCapitalization.characters, - maxLength: 6, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 4, - color: Color(0xFF2D3748), - ), - textAlign: TextAlign.center, - decoration: InputDecoration( - hintText: 'XXXXXX', - hintStyle: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - letterSpacing: 4, - color: Colors.grey[400], - ), - border: InputBorder.none, - contentPadding: const EdgeInsets.all(20), - counterText: '', - ), - ), - ), - const SizedBox(height: 24), - - // Botão de entrar - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: _isLoading ? null : _joinClass, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF82C9BD), - foregroundColor: Colors.white, - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - disabledBackgroundColor: const Color( - 0xFF82C9BD, - ).withOpacity(0.5), - ), - child: _isLoading - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Text( - 'Entrar na Turma', + child: SafeArea( + child: Column( + children: [ + // Custom AppBar + Container( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + IconButton( + icon: Icon( + Icons.arrow_back, + color: colorScheme.onSurface, + ), + onPressed: () => Navigator.of(context).pop(), + ), + Expanded( + child: Text( + 'Entrar numa Turma', style: TextStyle( - fontSize: 16, + color: colorScheme.onSurface, + fontSize: 18, fontWeight: FontWeight.bold, ), ), + ), + ], + ), ), - ), - ], + // Body content + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + // Header illustration card + Center( + child: Card( + elevation: isDark ? 4 : 8, + shadowColor: isDark + ? Colors.black.withOpacity(0.4) + : Colors.black.withOpacity(0.1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [ + colorScheme.primary.withOpacity(0.1), + colorScheme.secondary.withOpacity(0.05), + ] + : [ + colorScheme.primary.withOpacity(0.05), + colorScheme.secondary.withOpacity(0.02), + ], + ), + ), + child: Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: colorScheme.primary.withOpacity( + 0.2, + ), + width: 2, + ), + ), + child: Icon( + Icons.group_add, + color: colorScheme.primary, + size: 40, + ), + ), + const SizedBox(height: 24), + Text( + 'Insere o código da turma', + style: theme.textTheme.headlineSmall + ?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'O professor partilhou contigo um código de 6 caracteres.', + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ), + const SizedBox(height: 32), + + // Instructions card + Card( + elevation: isDark ? 2 : 4, + shadowColor: isDark + ? Colors.black.withOpacity(0.3) + : Colors.black.withOpacity(0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: colorScheme.primary, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Como funciona', + style: theme.textTheme.titleMedium + ?.copyWith( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + _buildInstructionItem( + context, + '1.', + 'Pedir ao professor o código da turma', + colorScheme, + ), + const SizedBox(height: 8), + _buildInstructionItem( + context, + '2.', + 'Inserir o código no campo abaixo', + colorScheme, + ), + const SizedBox(height: 8), + _buildInstructionItem( + context, + '3.', + 'Clicar em "Entrar na Turma" para confirmar', + colorScheme, + ), + ], + ), + ), + ), + const SizedBox(height: 32), + + // Campo de código + Container( + decoration: BoxDecoration( + color: theme.cardColor, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: colorScheme.outline.withOpacity(0.3), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: isDark + ? Colors.black.withOpacity(0.2) + : Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 2), + ), + ], + ), + child: TextField( + controller: _codeController, + textCapitalization: TextCapitalization.characters, + maxLength: 6, + style: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 8, + color: colorScheme.onSurface, + ), + textAlign: TextAlign.center, + decoration: InputDecoration( + hintText: 'XXXXXX', + hintStyle: theme.textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + letterSpacing: 8, + color: colorScheme.onSurfaceVariant.withOpacity( + 0.5, + ), + ), + border: InputBorder.none, + contentPadding: const EdgeInsets.all(24), + counterText: '', + prefixIcon: Icon( + Icons.vpn_key, + color: colorScheme.primary, + size: 24, + ), + prefixIconConstraints: const BoxConstraints( + minWidth: 48, + minHeight: 48, + ), + ), + ), + ), + const SizedBox(height: 32), + + // Botão de entrar + SizedBox( + width: double.infinity, + height: 56, + child: ElevatedButton( + onPressed: _isLoading ? null : _joinClass, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + elevation: 2, + shadowColor: colorScheme.primary.withOpacity(0.3), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + disabledBackgroundColor: colorScheme.primary + .withOpacity(0.5), + ), + child: _isLoading + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + color: colorScheme.onPrimary, + strokeWidth: 2, + ), + ) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.login, + size: 20, + color: colorScheme.onPrimary, + ), + const SizedBox(width: 8), + Text( + 'Entrar na Turma', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: colorScheme.onPrimary, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + + // Help text + Center( + child: TextButton.icon( + onPressed: () { + _showHelpDialog(context, colorScheme); + }, + icon: Icon( + Icons.help_outline, + size: 16, + color: colorScheme.primary, + ), + label: Text( + 'Precisas de ajuda?', + style: TextStyle( + color: colorScheme.primary, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), ), ), ); } + + Widget _buildInstructionItem( + BuildContext context, + String number, + String text, + ColorScheme colorScheme, + ) { + final theme = Theme.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + height: 24, + decoration: BoxDecoration( + color: colorScheme.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Text( + number, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + text, + style: theme.textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ); + } + + void _showHelpDialog(BuildContext context, ColorScheme colorScheme) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Theme.of(context).cardColor, + title: Row( + children: [ + Icon(Icons.help_outline, color: colorScheme.primary), + const SizedBox(width: 8), + Text( + 'Ajuda - Código da Turma', + style: TextStyle(color: colorScheme.onSurface), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + 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.', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + const SizedBox(height: 12), + Text( + 'Se não tens o código, contacta o teu professor.', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Entendido', + style: TextStyle(color: colorScheme.primary), + ), + ), + ], + ), + ); + } } diff --git a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart index 899b46e..ba1b453 100644 --- a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/routing/app_router.dart'; import '../widgets/progress_hero_widget.dart'; import '../widgets/quick_access_widget.dart'; import '../widgets/student_classes_list_widget.dart'; @@ -104,7 +105,7 @@ class _StudentDashboardPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with logout + // Header with logout and settings Row( children: [ Expanded( @@ -131,6 +132,13 @@ class _StudentDashboardPageState extends State { ], ), ), + IconButton( + icon: Icon(Icons.settings, color: headerColor), + onPressed: () { + AppRouter.goToSettings(context); + }, + tooltip: 'Configurações', + ), IconButton( icon: Icon(Icons.logout, color: headerColor), onPressed: () async { diff --git a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart index 1dd86d7..ce66b6e 100644 --- a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; import '../../../../core/theme/app_theme_extension.dart'; +import '../../../../core/routing/app_router.dart'; import '../widgets/teacher_hero_widget.dart'; import '../widgets/teacher_quick_actions_widget.dart'; import '../widgets/teacher_classes_list_widget.dart'; @@ -100,7 +101,7 @@ class _TeacherDashboardPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Header with logout + // Header with logout and settings Row( children: [ Expanded( @@ -127,6 +128,13 @@ class _TeacherDashboardPageState extends State { ], ), ), + IconButton( + icon: Icon(Icons.settings, color: headerColor), + onPressed: () { + AppRouter.goToSettings(context); + }, + tooltip: 'Configurações', + ), IconButton( icon: Icon(Icons.logout, color: headerColor), onPressed: () async { diff --git a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart index f843891..1c7a564 100644 --- a/lib/features/dashboard/presentation/widgets/profile_section_widget.dart +++ b/lib/features/dashboard/presentation/widgets/profile_section_widget.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import '../../../../core/theme/app_theme_extension.dart'; import 'package:flutter_animate/flutter_animate.dart'; import '../../../../core/services/auth_service.dart'; -import '../../../../core/routing/app_router.dart'; /// Profile section with user info and achievements class ProfileSectionWidget extends StatelessWidget { @@ -100,25 +99,6 @@ class ProfileSectionWidget extends StatelessWidget { ], ), ), - GestureDetector( - onTap: () { - AppRouter.goToSettings(context); - }, - child: 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, - ), - ), - ), ], ), const SizedBox(height: 20), 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 9058038..118de8b 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart @@ -99,20 +99,6 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { ], ), ), - 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, - ), - ), ], ), const SizedBox(height: 20), diff --git a/lib/features/quiz/presentation/pages/quiz_list_page.dart b/lib/features/quiz/presentation/pages/quiz_list_page.dart index 75e2126..ee58b9a 100644 --- a/lib/features/quiz/presentation/pages/quiz_list_page.dart +++ b/lib/features/quiz/presentation/pages/quiz_list_page.dart @@ -45,13 +45,20 @@ class _QuizListPageState extends State Future _loadMaterials() async { final mats = await MaterialsRAGService.getAvailableMaterialsForStudent(); - if (mounted) setState(() { _materials = mats; _loadingMaterials = false; }); + if (mounted) + setState(() { + _materials = mats; + _loadingMaterials = false; + }); } Future _loadTeacherQuizzes() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; - if (uid == null) { if (mounted) setState(() => _loadingTeacherQuizzes = false); return; } + if (uid == null) { + if (mounted) setState(() => _loadingTeacherQuizzes = false); + return; + } // Obter teacherIds dos professores do aluno final enrollSnap = await FirebaseFirestore.instance @@ -70,11 +77,19 @@ class _QuizListPageState extends State final classIdList = classIds.toList(); final batches = >[]; for (int i = 0; i < classIdList.length; i += 10) { - batches.add(FirebaseFirestore.instance - .collection('teacherQuizzes') - .where('classIds', arrayContainsAny: classIdList.sublist(i, (i + 10).clamp(0, classIdList.length))) - .orderBy('createdAt', descending: true) - .get()); + batches.add( + FirebaseFirestore.instance + .collection('teacherQuizzes') + .where( + 'classIds', + arrayContainsAny: classIdList.sublist( + i, + (i + 10).clamp(0, classIdList.length), + ), + ) + .orderBy('createdAt', descending: true) + .get(), + ); } final results = await Future.wait(batches); // deduplicar por id (pode aparecer em múltiplos batches) @@ -84,7 +99,11 @@ class _QuizListPageState extends State .where((d) => seen.add(d.id)) .map((d) => {'id': d.id, ...d.data() as Map}) .toList(); - if (mounted) setState(() { _teacherQuizzes = quizzes; _loadingTeacherQuizzes = false; }); + if (mounted) + setState(() { + _teacherQuizzes = quizzes; + _loadingTeacherQuizzes = false; + }); } catch (e) { Logger.error('Error loading teacher quizzes: $e'); if (mounted) setState(() => _loadingTeacherQuizzes = false); @@ -94,7 +113,10 @@ class _QuizListPageState 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('quizHistory') .doc(uid) @@ -103,7 +125,11 @@ class _QuizListPageState 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('Error loading quiz history: $e'); if (mounted) setState(() => _loadingHistory = false); @@ -141,7 +167,8 @@ class _QuizListPageState 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; } @@ -153,11 +180,11 @@ class _QuizListPageState extends State .doc(uid) .collection('quizzes') .add({ - 'materialId': matId, - 'materialName': matName, - 'quizJson': raw, - 'createdAt': FieldValue.serverTimestamp(), - }); + 'materialId': matId, + 'materialName': matName, + 'quizJson': raw, + 'createdAt': FieldValue.serverTimestamp(), + }); await _loadHistory(); } @@ -171,7 +198,8 @@ class _QuizListPageState extends State } Widget _buildTeacherQuizzesTab(ColorScheme cs) { - if (_loadingTeacherQuizzes) return const Center(child: CircularProgressIndicator()); + if (_loadingTeacherQuizzes) + return const Center(child: CircularProgressIndicator()); if (_teacherQuizzes.isEmpty) { return Center( child: Padding( @@ -179,15 +207,26 @@ class _QuizListPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.school_outlined, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)), + Icon( + Icons.school_outlined, + size: 64, + color: cs.onSurfaceVariant.withValues(alpha: 0.4), + ), const SizedBox(height: 16), - Text('Sem quizzes do professor disponíveis.', - textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16)), + Text( + 'Sem quizzes do professor disponíveis.', + textAlign: TextAlign.center, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), + ), const SizedBox(height: 8), - Text('O teu professor ainda não publicou quizzes.', - textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant.withValues(alpha: 0.7), fontSize: 13)), + Text( + 'O teu professor ainda não publicou quizzes.', + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurfaceVariant.withValues(alpha: 0.7), + fontSize: 13, + ), + ), ], ), ), @@ -206,17 +245,27 @@ class _QuizListPageState extends State 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}'; + dateStr = + '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; } return Container( decoration: BoxDecoration( 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, @@ -226,14 +275,27 @@ class _QuizListPageState extends State ), child: Icon(Icons.school, color: cs.secondary, 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('Publicado em $dateStr', style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)) + ? Text( + 'Publicado em $dateStr', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ) : null, - trailing: Icon(Icons.play_circle_outline, color: cs.secondary, size: 28), + trailing: Icon( + Icons.play_circle_outline, + color: cs.secondary, + size: 28, + ), onTap: () => _showTeacherQuiz(quiz), ), ); @@ -275,16 +337,24 @@ class _QuizListPageState extends State final jsonStr = raw.substring(start, end + 1); final decoded = _jsonDecode(jsonStr); if (decoded is! List) return []; - return decoded.map<_QuizQuestion?>((e) { - if (e is! Map) return null; - final q = e['q'] as String?; - final opts = (e['opts'] as List?)?.cast(); - final ans = e['ans'] as int?; - 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); - }).whereType<_QuizQuestion>().toList(); + return decoded + .map<_QuizQuestion?>((e) { + if (e is! Map) return null; + final q = e['q'] as String?; + final opts = (e['opts'] as List?)?.cast(); + final ans = e['ans'] as int?; + 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, + ); + }) + .whereType<_QuizQuestion>() + .toList(); } catch (e) { Logger.error('Quiz JSON parse error: $e'); return []; @@ -373,7 +443,11 @@ class _QuizListPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.folder_open, size: 64, color: cs.onSurfaceVariant.withOpacity(0.4)), + Icon( + Icons.folder_open, + size: 64, + color: cs.onSurfaceVariant.withOpacity(0.4), + ), const SizedBox(height: 16), Text( 'Nenhum material disponível.', @@ -384,7 +458,10 @@ class _QuizListPageState extends State Text( 'Inscreve-te numa turma para aceder aos PDFs do professor.', textAlign: TextAlign.center, - style: TextStyle(color: cs.onSurfaceVariant.withOpacity(0.7), fontSize: 13), + style: TextStyle( + color: cs.onSurfaceVariant.withOpacity(0.7), + fontSize: 13, + ), ), ], ), @@ -413,7 +490,10 @@ class _QuizListPageState extends State ], ), child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), leading: Container( width: 44, height: 44, @@ -465,7 +545,11 @@ class _QuizListPageState extends State child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.history, size: 64, color: cs.onSurfaceVariant.withOpacity(0.4)), + Icon( + Icons.history, + size: 64, + color: cs.onSurfaceVariant.withOpacity(0.4), + ), const SizedBox(height: 16), Text( 'Ainda não geraste nenhum quiz.', @@ -490,7 +574,8 @@ class _QuizListPageState extends State 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} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + dateStr = + '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } return Container( decoration: BoxDecoration( @@ -506,7 +591,10 @@ class _QuizListPageState extends State ], ), child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), leading: Container( width: 44, height: 44, @@ -527,10 +615,16 @@ class _QuizListPageState extends State ), ), 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.chevron_right, color: cs.onSurfaceVariant), - onTap: () => _showQuizFromHistory(matName, item['quizJson'] as String? ?? item['quizText'] as String? ?? ''), + onTap: () => _showQuizFromHistory( + matName, + item['quizJson'] as String? ?? item['quizText'] as String? ?? '', + ), ), ); }, @@ -584,19 +678,24 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { void _next() { if (_current < widget.questions.length - 1) { - setState(() { _current++; }); + setState(() { + _current++; + }); } else { setState(() => _submitted = true); } } void _prev() { - if (_current > 0) setState(() { _current--; }); + if (_current > 0) + setState(() { + _current--; + }); } int get _score => List.generate(widget.questions.length, (i) { - return _chosen[i] == widget.questions[i].correctIndex ? 1 : 0; - }).fold(0, (a, b) => a + b); + return _chosen[i] == widget.questions[i].correctIndex ? 1 : 0; + }).fold(0, (a, b) => a + b); @override Widget build(BuildContext context) { @@ -627,44 +726,48 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { } Widget _buildHandle(ColorScheme cs) => Container( - margin: const EdgeInsets.only(top: 12, bottom: 4), - width: 40, - height: 4, - decoration: BoxDecoration( - color: cs.outline.withOpacity(0.3), - borderRadius: BorderRadius.circular(2), - ), - ); + margin: const EdgeInsets.only(top: 12, bottom: 4), + width: 40, + height: 4, + decoration: BoxDecoration( + color: cs.outline.withOpacity(0.3), + borderRadius: BorderRadius.circular(2), + ), + ); Widget _buildHeader(ColorScheme cs) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: cs.onSurface), - ), - if (!_submitted) - Text( - 'Pergunta ${_current + 1} de ${widget.questions.length}', - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), - ), - ], + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), ), - ), - IconButton( - icon: Icon(Icons.close, color: cs.onSurfaceVariant), - onPressed: () => Navigator.of(context).pop(), - ), - ], + if (!_submitted) + Text( + 'Pergunta ${_current + 1} de ${widget.questions.length}', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ), + ], + ), ), - ); + IconButton( + icon: Icon(Icons.close, color: cs.onSurfaceVariant), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ); Widget _buildQuestion(ColorScheme cs, ScrollController sc) { final q = widget.questions[_current]; @@ -687,7 +790,12 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { // Pergunta Text( q.question, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface, height: 1.4), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.onSurface, + height: 1.4, + ), ), const SizedBox(height: 20), // Opções @@ -700,12 +808,19 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { onTap: () => _selectOption(i), child: AnimatedContainer( duration: const Duration(milliseconds: 150), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), decoration: BoxDecoration( - color: isSelected ? cs.primary.withOpacity(0.12) : cs.surfaceVariant.withOpacity(0.4), + color: isSelected + ? cs.primary.withOpacity(0.12) + : cs.surfaceVariant.withOpacity(0.4), borderRadius: BorderRadius.circular(12), border: Border.all( - color: isSelected ? cs.primary : cs.outline.withOpacity(0.2), + color: isSelected + ? cs.primary + : cs.outline.withOpacity(0.2), width: isSelected ? 2 : 1, ), ), @@ -717,11 +832,14 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { style: TextStyle( fontSize: 14, color: isSelected ? cs.primary : cs.onSurface, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, ), ), ), - if (isSelected) Icon(Icons.check_circle, color: cs.primary, size: 20), + if (isSelected) + Icon(Icons.check_circle, color: cs.primary, size: 20), ], ), ), @@ -738,7 +856,9 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { onPressed: _prev, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: const Text('Anterior'), ), @@ -751,9 +871,15 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { backgroundColor: cs.primary, foregroundColor: cs.onPrimary, padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + _current < widget.questions.length - 1 + ? 'Próxima' + : 'Submeter', ), - child: Text(_current < widget.questions.length - 1 ? 'Próxima' : 'Submeter'), ), ), ], @@ -769,8 +895,8 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { final Color scoreColor = pct >= 80 ? const Color(0xFF10B981) : pct >= 50 - ? const Color(0xFFF59E0B) - : const Color(0xFFEF4444); + ? const Color(0xFFF59E0B) + : const Color(0xFFEF4444); return ListView( controller: sc, @@ -788,7 +914,11 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { children: [ Text( '$score / $total', - style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold, color: scoreColor), + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: scoreColor, + ), ), const SizedBox(height: 4), Text( @@ -801,7 +931,11 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { const SizedBox(height: 24), Text( 'Revisão', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: cs.onSurface), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), ), const SizedBox(height: 12), // Revisão pergunta a pergunta @@ -809,7 +943,9 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { final q = widget.questions[i]; final chosen = _chosen[i]; final isCorrect = chosen == q.correctIndex; - final revColor = isCorrect ? const Color(0xFF10B981) : const Color(0xFFEF4444); + final revColor = isCorrect + ? const Color(0xFF10B981) + : const Color(0xFFEF4444); return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), @@ -823,27 +959,49 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { children: [ Row( children: [ - Icon(isCorrect ? Icons.check_circle : Icons.cancel, color: revColor, size: 18), + Icon( + isCorrect ? Icons.check_circle : Icons.cancel, + color: revColor, + size: 18, + ), const SizedBox(width: 8), Expanded( child: Text( 'Pergunta ${i + 1}', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: revColor), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: revColor, + ), ), ), ], ), const SizedBox(height: 8), - Text(q.question, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface)), + Text( + q.question, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), const SizedBox(height: 8), if (chosen >= 0 && chosen < q.options.length && !isCorrect) Text( 'A tua resposta: ${q.options[chosen]}', - style: const TextStyle(fontSize: 13, color: Color(0xFFEF4444)), + style: const TextStyle( + fontSize: 13, + color: Color(0xFFEF4444), + ), ), Text( 'Resposta correcta: ${q.options[q.correctIndex]}', - style: TextStyle(fontSize: 13, color: isCorrect ? const Color(0xFF10B981) : cs.onSurface, fontWeight: FontWeight.w500), + style: TextStyle( + fontSize: 13, + color: isCorrect ? const Color(0xFF10B981) : cs.onSurface, + fontWeight: FontWeight.w500, + ), ), if (q.explanation.isNotEmpty) ...[ const SizedBox(height: 8), @@ -856,12 +1014,20 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.info_outline, size: 14, color: cs.onSurfaceVariant), + Icon( + Icons.info_outline, + size: 14, + color: cs.onSurfaceVariant, + ), const SizedBox(width: 6), Expanded( child: Text( q.explanation, - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant, height: 1.4), + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + height: 1.4, + ), ), ), ], @@ -879,7 +1045,9 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> { backgroundColor: cs.primary, foregroundColor: cs.onPrimary, padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: const Text('Fechar'), ), @@ -901,10 +1069,12 @@ class _TeacherQuizInteractiveSheet extends StatefulWidget { }); @override - State<_TeacherQuizInteractiveSheet> createState() => _TeacherQuizInteractiveSheetState(); + State<_TeacherQuizInteractiveSheet> createState() => + _TeacherQuizInteractiveSheetState(); } -class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveSheet> { +class _TeacherQuizInteractiveSheetState + extends State<_TeacherQuizInteractiveSheet> { int _current = 0; late List _chosen; bool _submitted = false; @@ -933,11 +1103,16 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe if (_current > 0) setState(() => _current--); } - int get _score => List.generate(widget.questions.length, - (i) => _chosen[i] == widget.questions[i].correctIndex ? 1 : 0).fold(0, (a, b) => a + b); + int get _score => List.generate( + widget.questions.length, + (i) => _chosen[i] == widget.questions[i].correctIndex ? 1 : 0, + ).fold(0, (a, b) => a + b); Future _submit() async { - setState(() { _submitted = true; _saving = true; }); + setState(() { + _submitted = true; + _saving = true; + }); try { final user = FirebaseAuth.instance.currentUser; if (user != null) { @@ -947,12 +1122,13 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe .collection('submissions') .doc(user.uid) // 1 submissão por aluno (sobrescreve) .set({ - 'studentId': user.uid, - 'studentName': user.displayName ?? user.email?.split('@')[0] ?? 'Aluno', - 'score': _score, - 'total': widget.questions.length, - 'submittedAt': FieldValue.serverTimestamp(), - }); + 'studentId': user.uid, + 'studentName': + user.displayName ?? user.email?.split('@')[0] ?? 'Aluno', + 'score': _score, + 'total': widget.questions.length, + 'submittedAt': FieldValue.serverTimestamp(), + }); } } catch (e) { Logger.error('Error submitting teacher quiz result: $e'); @@ -978,7 +1154,8 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe // Handle Container( margin: const EdgeInsets.only(top: 12, bottom: 4), - width: 40, height: 4, + width: 40, + height: 4, decoration: BoxDecoration( color: cs.outline.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), @@ -993,18 +1170,33 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(widget.title, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: cs.onSurface)), + Text( + widget.title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), + ), if (!_submitted) - Text('Pergunta ${_current + 1} de ${widget.questions.length}', - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)), + Text( + 'Pergunta ${_current + 1} de ${widget.questions.length}', + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + ), + ), ], ), ), if (_saving) - const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ) else IconButton( icon: Icon(Icons.close, color: cs.onSurfaceVariant), @@ -1042,8 +1234,15 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe ), ), const SizedBox(height: 20), - Text(q.question, - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface, height: 1.4)), + Text( + q.question, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.onSurface, + height: 1.4, + ), + ), const SizedBox(height: 20), ...List.generate(q.options.length, (i) { final isSelected = chosen == i; @@ -1054,26 +1253,38 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe onTap: () => _selectOption(i), child: AnimatedContainer( duration: const Duration(milliseconds: 150), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), decoration: BoxDecoration( - color: isSelected ? cs.secondary.withValues(alpha: 0.12) : cs.surfaceContainerHighest.withValues(alpha: 0.4), + color: isSelected + ? cs.secondary.withValues(alpha: 0.12) + : cs.surfaceContainerHighest.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(12), border: Border.all( - color: isSelected ? cs.secondary : cs.outline.withValues(alpha: 0.2), + color: isSelected + ? cs.secondary + : cs.outline.withValues(alpha: 0.2), width: isSelected ? 2 : 1, ), ), child: Row( children: [ Expanded( - child: Text(q.options[i], - style: TextStyle( - fontSize: 14, - color: isSelected ? cs.secondary : cs.onSurface, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - )), + child: Text( + q.options[i], + style: TextStyle( + fontSize: 14, + color: isSelected ? cs.secondary : cs.onSurface, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + ), + ), ), - if (isSelected) Icon(Icons.check_circle, color: cs.secondary, size: 20), + if (isSelected) + Icon(Icons.check_circle, color: cs.secondary, size: 20), ], ), ), @@ -1089,7 +1300,9 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe onPressed: _prev, style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: const Text('Anterior'), ), @@ -1103,9 +1316,15 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe backgroundColor: cs.secondary, foregroundColor: cs.onSecondary, padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + _current < widget.questions.length - 1 + ? 'Próxima' + : 'Submeter', ), - child: Text(_current < widget.questions.length - 1 ? 'Próxima' : 'Submeter'), ), ), ], @@ -1121,8 +1340,8 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe final Color scoreColor = pct >= 80 ? const Color(0xFF10B981) : pct >= 50 - ? const Color(0xFFF59E0B) - : const Color(0xFFEF4444); + ? const Color(0xFFF59E0B) + : const Color(0xFFEF4444); return ListView( controller: sc, padding: const EdgeInsets.fromLTRB(20, 20, 20, 32), @@ -1136,25 +1355,44 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe ), child: Column( children: [ - Text('$score / $total', - style: TextStyle(fontSize: 40, fontWeight: FontWeight.bold, color: scoreColor)), + Text( + '$score / $total', + style: TextStyle( + fontSize: 40, + fontWeight: FontWeight.bold, + color: scoreColor, + ), + ), const SizedBox(height: 4), - Text('$pct% de respostas correctas', - style: TextStyle(fontSize: 14, color: scoreColor)), + Text( + '$pct% de respostas correctas', + style: TextStyle(fontSize: 14, color: scoreColor), + ), const SizedBox(height: 4), - Text('Resultado enviado ao professor ✓', - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant)), + Text( + 'Resultado enviado ao professor ✓', + style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), + ), ], ), ), const SizedBox(height: 24), - Text('Revisão', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: cs.onSurface)), + Text( + 'Revisão', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: cs.onSurface, + ), + ), const SizedBox(height: 12), ...List.generate(total, (i) { final q = widget.questions[i]; final chosen = _chosen[i]; final isCorrect = chosen == q.correctIndex; - final revColor = isCorrect ? const Color(0xFF10B981) : const Color(0xFFEF4444); + final revColor = isCorrect + ? const Color(0xFF10B981) + : const Color(0xFFEF4444); return Container( margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16), @@ -1168,26 +1406,50 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe children: [ Row( children: [ - Icon(isCorrect ? Icons.check_circle : Icons.cancel, color: revColor, size: 18), + Icon( + isCorrect ? Icons.check_circle : Icons.cancel, + color: revColor, + size: 18, + ), const SizedBox(width: 8), Expanded( - child: Text('Pergunta ${i + 1}', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600, color: revColor)), + child: Text( + 'Pergunta ${i + 1}', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: revColor, + ), + ), ), ], ), const SizedBox(height: 8), - Text(q.question, - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface)), + Text( + q.question, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), const SizedBox(height: 8), if (chosen >= 0 && chosen < q.options.length && !isCorrect) - Text('A tua resposta: ${q.options[chosen]}', - style: const TextStyle(fontSize: 13, color: Color(0xFFEF4444))), - Text('Resposta correcta: ${q.options[q.correctIndex]}', - style: TextStyle( - fontSize: 13, - color: isCorrect ? const Color(0xFF10B981) : cs.onSurface, - fontWeight: FontWeight.w500)), + Text( + 'A tua resposta: ${q.options[chosen]}', + style: const TextStyle( + fontSize: 13, + color: Color(0xFFEF4444), + ), + ), + Text( + 'Resposta correcta: ${q.options[q.correctIndex]}', + style: TextStyle( + fontSize: 13, + color: isCorrect ? const Color(0xFF10B981) : cs.onSurface, + fontWeight: FontWeight.w500, + ), + ), if (q.explanation.isNotEmpty) ...[ const SizedBox(height: 8), Container( @@ -1199,11 +1461,21 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.info_outline, size: 14, color: cs.onSurfaceVariant), + Icon( + Icons.info_outline, + size: 14, + color: cs.onSurfaceVariant, + ), const SizedBox(width: 6), Expanded( - child: Text(q.explanation, - style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant, height: 1.4)), + child: Text( + q.explanation, + style: TextStyle( + fontSize: 12, + color: cs.onSurfaceVariant, + height: 1.4, + ), + ), ), ], ), @@ -1219,7 +1491,9 @@ class _TeacherQuizInteractiveSheetState extends State<_TeacherQuizInteractiveShe backgroundColor: cs.secondary, foregroundColor: cs.onSecondary, padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: const Text('Fechar'), ), diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart index de7ce82..b0bc9bb 100644 --- a/lib/features/settings/presentation/pages/settings_page.dart +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -5,6 +5,7 @@ import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_theme_extension.dart'; import '../../../../core/services/theme_service.dart'; import '../../../../core/providers/theme_provider.dart'; +import '../../../../core/services/auth_service.dart'; /// Settings page for app configuration class SettingsPage extends ConsumerStatefulWidget { @@ -22,10 +23,20 @@ class _SettingsPageState extends ConsumerState { return PopScope( canPop: false, - onPopInvokedWithResult: (didPop, result) { + onPopInvokedWithResult: (didPop, result) async { if (!didPop) { - // Navigate to student dashboard on back button - context.go('/student-dashboard'); + // Navigate to appropriate dashboard based on user role + final user = AuthService.currentUser; + if (user != null) { + final role = await AuthService.getUserRole(user.uid); + if (role == 'teacher') { + context.go('/teacher-dashboard'); + } else { + context.go('/student-dashboard'); + } + } else { + context.go('/student-dashboard'); + } } }, child: Scaffold( @@ -56,7 +67,22 @@ class _SettingsPageState extends ConsumerState { Icons.arrow_back, color: Theme.of(context).colorScheme.onSurface, ), - onPressed: () => context.go('/student-dashboard'), + onPressed: () async { + // Navigate to appropriate dashboard based on user role + final user = AuthService.currentUser; + if (user != null) { + final role = await AuthService.getUserRole( + user.uid, + ); + if (role == 'teacher') { + context.go('/teacher-dashboard'); + } else { + context.go('/student-dashboard'); + } + } else { + context.go('/student-dashboard'); + } + }, ), Expanded( child: Text(