From 34d7ae8afcc11d891f62347fe74f8c54f9cef442 Mon Sep 17 00:00:00 2001 From: 240405 <240405@epvc.pt> Date: Thu, 11 Jun 2026 23:59:58 +0100 Subject: [PATCH] =?UTF-8?q?=C3=BAltimas=20mudan=C3=A7as?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/pages/login_page.dart | 23 +- .../auth/presentation/pages/signup_page.dart | 3 + .../pages/class_students_page.dart | 409 ++++++++++-------- .../presentation/pages/join_class_page.dart | 19 - 4 files changed, 248 insertions(+), 206 deletions(-) diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index d0ce7d7..f882e00 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -93,6 +93,20 @@ class _LoginPageState extends State { return; } + // Se role não existe na Firestore (null), permitir login e criar documento + if (actualRole == null) { + print( + 'DEBUG: Role não encontrado na Firestore, criando documento com role selecionado: $selectedRole', + ); + try { + await AuthService.createUserRole(uid, selectedRole); + print('DEBUG: Role criado com sucesso'); + } catch (e) { + print('DEBUG: Erro ao criar role: $e'); + // Continuar mesmo se falhar, pois o usuário já está autenticado + } + } + // Validar se o role selecionado corresponde ao role real if (actualRole != null && selectedRole != actualRole) { // Fazer logout imediato antes de mostrar erro @@ -118,6 +132,9 @@ class _LoginPageState extends State { return; } + // Usar selectedRole se actualRole for null (caso acabamos de criar) + final finalRole = actualRole ?? selectedRole; + // Save session based on remember me preference await SessionService.saveSession( rememberMe: _rememberMe, @@ -137,7 +154,7 @@ class _LoginPageState extends State { ); // Redirecionar baseado no role real - if (actualRole == 'teacher') { + if (finalRole == 'teacher') { context.go('/teacher-dashboard'); } else { context.go('/student-dashboard'); @@ -523,7 +540,9 @@ class _LoginPageState extends State { // Signup link GestureDetector( onTap: () { - context.go('/signup'); + context.go( + '/signup?role=${widget.selectedRole}', + ); }, child: Text( 'Não tem conta? Criar aqui', diff --git a/lib/features/auth/presentation/pages/signup_page.dart b/lib/features/auth/presentation/pages/signup_page.dart index adf1e62..3439284 100644 --- a/lib/features/auth/presentation/pages/signup_page.dart +++ b/lib/features/auth/presentation/pages/signup_page.dart @@ -41,12 +41,14 @@ class _SignupPageState extends State { Future _loadAvailableClasses() async { setState(() => _isLoadingClasses = true); try { + print('DEBUG: Loading school_classes from Firestore'); final snapshot = await FirebaseFirestore.instance .collection('school_classes') .where('active', isEqualTo: true) .orderBy('year') .orderBy('section') .get(); + print('DEBUG: Loaded ${snapshot.docs.length} school classes'); setState(() { _availableClasses = snapshot.docs.map((doc) { final data = doc.data(); @@ -55,6 +57,7 @@ class _SignupPageState extends State { _isLoadingClasses = false; }); } catch (e) { + print('DEBUG: Error loading school_classes: $e'); setState(() => _isLoadingClasses = false); } } diff --git a/lib/features/classes/presentation/pages/class_students_page.dart b/lib/features/classes/presentation/pages/class_students_page.dart index 13e35c0..55dec3b 100644 --- a/lib/features/classes/presentation/pages/class_students_page.dart +++ b/lib/features/classes/presentation/pages/class_students_page.dart @@ -1,9 +1,11 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../../../core/services/auth_service.dart'; +import '../../../../core/theme/app_colors.dart'; import '../../../../core/theme/app_theme_extension.dart'; /// Página para visualizar os alunos de uma turma específica @@ -202,9 +204,7 @@ class _ClassStudentsPageState extends State { controller: textController, decoration: InputDecoration( labelText: 'Nome da Turma', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)), prefixIcon: const Icon(Icons.school), ), autofocus: true, @@ -254,10 +254,14 @@ class _ClassStudentsPageState extends State { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Theme.of(context).colorScheme.error.withValues(alpha: 0.1), + color: Theme.of( + context, + ).colorScheme.error.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all( - color: Theme.of(context).colorScheme.error.withValues(alpha: 0.3), + color: Theme.of( + context, + ).colorScheme.error.withValues(alpha: 0.3), ), ), child: Row( @@ -420,12 +424,7 @@ class _ClassStudentsPageState extends State { Widget _buildAppBar(ColorScheme cs) { return Container( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 52, - bottom: 16, - ), + padding: const EdgeInsets.only(left: 16, right: 16, top: 52, bottom: 16), child: Column( children: [ // Top Row with Back and Actions @@ -459,11 +458,20 @@ class _ClassStudentsPageState extends State { // Delete Button Container( decoration: BoxDecoration( - color: Colors.red.withValues(alpha: 0.2), + color: + (Theme.of(context).brightness == Brightness.dark + ? DarkBrandColors.primaryOrange + : AppColors.primaryOrange) + .withValues(alpha: 0.2), borderRadius: BorderRadius.circular(12), ), child: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.white), + icon: Icon( + Icons.delete_outline, + color: Theme.of(context).brightness == Brightness.dark + ? DarkBrandColors.primaryOrange + : AppColors.primaryOrange, + ), onPressed: _showDeleteClassDialog, tooltip: 'Eliminar turma', ), @@ -587,79 +595,85 @@ class _ClassStudentsPageState extends State { children: [ const SizedBox(height: 40), Container( - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - color: cs.surface, - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow( - color: cs.shadow.withValues(alpha: 0.08), - blurRadius: 20, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - children: [ - Icon( - Icons.people_outline, - size: 80, - color: cs.primary.withValues(alpha: 0.5), - ), - const SizedBox(height: 24), - Text( - 'Nenhum aluno entrou nesta turma ainda.', - style: TextStyle( - color: cs.onSurface, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 12), - Text( - 'Partilha o código da turma para os alunos se juntarem.', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 14, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 24), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 12, - ), - decoration: BoxDecoration( - color: cs.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: cs.primary.withValues(alpha: 0.3), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.08), + blurRadius: 20, + offset: const Offset(0, 8), ), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.copy, color: cs.primary, size: 18), - const SizedBox(width: 8), - Text( - 'Código: $_classCode', - style: TextStyle( - color: cs.primary, - fontSize: 16, - fontWeight: FontWeight.bold, + ], + ), + child: Column( + children: [ + Icon( + Icons.people_outline, + size: 80, + color: cs.primary.withValues(alpha: 0.5), + ), + const SizedBox(height: 24), + Text( + 'Nenhum aluno entrou nesta turma ainda.', + style: TextStyle( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + 'Partilha o código da turma para os alunos se juntarem.', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + GestureDetector( + onTap: _copyCodeToClipboard, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 12, + ), + decoration: BoxDecoration( + color: cs.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: cs.primary.withValues(alpha: 0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.copy, color: cs.primary, size: 18), + const SizedBox(width: 8), + Text( + 'Código: $_classCode', + style: TextStyle( + color: cs.primary, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], ), ), - ], - ), + ), + ], ), - ], - ), - ).animate().fadeIn( - duration: const Duration(milliseconds: 400), - curve: Curves.easeOut, - ).then(delay: const Duration(milliseconds: 100)), + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 400), + curve: Curves.easeOut, + ) + .then(delay: const Duration(milliseconds: 100)), ], ), ); @@ -679,11 +693,7 @@ class _ClassStudentsPageState extends State { margin: const EdgeInsets.only(bottom: 16, left: 8), child: Row( children: [ - Icon( - Icons.people, - color: cs.onSurface, - size: 20, - ), + Icon(Icons.people, color: cs.onSurface, size: 20), const SizedBox(width: 8), Text( 'Alunos Matriculados', @@ -746,118 +756,144 @@ class _ClassStudentsPageState extends State { int index, ) { return Container( - margin: const EdgeInsets.only(bottom: 12), - decoration: BoxDecoration( - color: cs.surface, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: cs.shadow.withValues(alpha: 0.06), - blurRadius: 12, - offset: const Offset(0, 4), + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: cs.shadow.withValues(alpha: 0.06), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], ), - ], - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(16), - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: null, - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - // Avatar - Container( - width: 52, - height: 52, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - cs.primary.withValues(alpha: 0.8), - cs.primary.withValues(alpha: 0.4), - ], - ), - borderRadius: BorderRadius.circular(14), - ), - child: Center( - child: Text( - studentName.isNotEmpty - ? studentName[0].toUpperCase() - : '?', - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - const SizedBox(width: 16), - // Info - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - studentName, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: null, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // Avatar + Container( + width: 52, + height: 52, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + cs.primary.withValues(alpha: 0.8), + cs.primary.withValues(alpha: 0.4), + ], ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + borderRadius: BorderRadius.circular(14), ), - const SizedBox(height: 4), - Text( - joinedAt != null - ? 'Entrou em ${_formatDate(joinedAt.toDate())}' - : 'Data desconhecida', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 13, + child: Center( + child: Text( + studentName.isNotEmpty + ? studentName[0].toUpperCase() + : '?', + style: const TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), ), ), - ], - ), - ), - // Delete Button - Container( - decoration: BoxDecoration( - color: cs.error.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(10), - ), - child: IconButton( - icon: Icon(Icons.delete_outline, color: cs.error, size: 20), - onPressed: () => _showRemoveStudentDialog( - context, - enrollmentId, - studentName, ), - tooltip: 'Remover aluno', - padding: const EdgeInsets.all(10), - constraints: const BoxConstraints(), - ), + const SizedBox(width: 16), + // Info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + studentName, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + joinedAt != null + ? 'Entrou em ${_formatDate(joinedAt.toDate())}' + : 'Data desconhecida', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ), + ), + // Delete Button + Container( + decoration: BoxDecoration( + color: cs.error.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: IconButton( + icon: Icon( + Icons.delete_outline, + color: cs.error, + size: 20, + ), + onPressed: () => _showRemoveStudentDialog( + context, + enrollmentId, + studentName, + ), + tooltip: 'Remover aluno', + padding: const EdgeInsets.all(10), + constraints: const BoxConstraints(), + ), + ), + ], ), - ], + ), ), ), - ), - ), - ).animate().fadeIn( - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ).then(delay: Duration(milliseconds: index * 50)); + ) + .animate() + .fadeIn( + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ) + .then(delay: Duration(milliseconds: index * 50)); } String _formatDate(DateTime date) { return DateFormat('dd/MM/yyyy').format(date); } + void _copyCodeToClipboard() { + Clipboard.setData(ClipboardData(text: _classCode ?? '')); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, color: Colors.white), + const SizedBox(width: 12), + const Text('Código copiado!'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + duration: const Duration(seconds: 2), + ), + ); + } + Future _showRemoveStudentDialog( BuildContext context, String enrollmentId, @@ -869,7 +905,10 @@ class _ClassStudentsPageState extends State { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: Row( children: [ - Icon(Icons.person_remove, color: Theme.of(context).colorScheme.error), + Icon( + Icons.person_remove, + color: Theme.of(context).colorScheme.error, + ), const SizedBox(width: 8), const Text('Remover Aluno'), ], diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart index f094b84..73bb53f 100644 --- a/lib/features/classes/presentation/pages/join_class_page.dart +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -56,11 +56,6 @@ class _JoinClassPageState extends ConsumerState { return; } - // Ler schoolClassId autorizado do aluno (definido no registo) - final studentSchoolClassId = await AuthService.getStudentSchoolClassId( - currentUser.uid, - ); - // Procurar disciplina pelo código final classQuery = await FirebaseFirestore.instance .collection('classes') @@ -76,20 +71,6 @@ class _JoinClassPageState extends ConsumerState { final classDoc = classQuery.docs.first; final classId = classDoc.id; - final classSchoolClassId = classDoc.data()['schoolClassId'] as String?; - - // 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 disciplina.\n' - 'O teu professor ainda não te adicionou a esta disciplina.', - ); - return; - } // Verificar se já está inscrito nesta disciplina final existingEnrollment = await FirebaseFirestore.instance