From 2775205f9ebcdb97b0fa1c1295126b6334bca02f Mon Sep 17 00:00:00 2001 From: 240405 <240405@epvc.pt> Date: Fri, 15 May 2026 12:40:38 +0100 Subject: [PATCH] =?UTF-8?q?corre=C3=A7=C3=A3o=20de=20bugs,=20crea=C3=A7?= =?UTF-8?q?=C3=A3o=20propria=20para=20turmas,=20e=20prepara=C3=A7=C3=A3o?= =?UTF-8?q?=20para=20criar=20quizzes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/routing/app_router.dart | 6 +- lib/core/services/auth_service.dart | 81 +- .../auth/presentation/pages/login_page.dart | 23 +- .../auth/presentation/pages/signup_page.dart | 130 +++ .../pages/class_students_page.dart | 151 ++- .../presentation/pages/join_class_page.dart | 44 +- .../pages/teacher_all_classes_page.dart | 41 + .../pages/student_dashboard_page.dart | 21 +- .../pages/teacher_dashboard_page.dart | 20 +- .../teacher_analytics_preview_widget.dart | 2 + .../widgets/teacher_hero_widget.dart | 28 +- .../widgets/teacher_quick_actions_widget.dart | 908 ++++++++++-------- .../providers/settings_provider.dart | 12 +- test/rag_services_test.dart | 107 +-- 14 files changed, 970 insertions(+), 604 deletions(-) create mode 100644 lib/features/classes/presentation/pages/teacher_all_classes_page.dart diff --git a/lib/core/routing/app_router.dart b/lib/core/routing/app_router.dart index 8e5230f..2e8e32b 100644 --- a/lib/core/routing/app_router.dart +++ b/lib/core/routing/app_router.dart @@ -58,14 +58,16 @@ class AppRouter { GoRoute( path: login, name: 'login', - builder: (context, state) => const LoginPage(), + builder: (context, state) => + LoginPage(selectedRole: state.uri.queryParameters['role']), ), // Signup GoRoute( path: signup, name: 'signup', - builder: (context, state) => const SignupPage(), + builder: (context, state) => + SignupPage(selectedRole: state.uri.queryParameters['role']), ), // Student Dashboard diff --git a/lib/core/services/auth_service.dart b/lib/core/services/auth_service.dart index a5db605..0eb78a5 100644 --- a/lib/core/services/auth_service.dart +++ b/lib/core/services/auth_service.dart @@ -13,13 +13,29 @@ class AuthService { } /// Criar documento do usuário na Firestore após signup - static Future createUserRole(String uid, String role) async { + static Future createUserRole( + String uid, + String role, { + String? classId, + String? schoolClassId, + String? displayName, + }) async { try { print('DEBUG: Criando documento users/$uid com role: $role'); - await _firestore.collection('users').doc(uid).set({ + final Map data = { 'role': role, 'createdAt': FieldValue.serverTimestamp(), - }); + }; + if (classId != null && classId.isNotEmpty) { + data['classId'] = classId; + } + if (schoolClassId != null && schoolClassId.isNotEmpty) { + data['schoolClassId'] = schoolClassId; + } + if (displayName != null && displayName.isNotEmpty) { + data['displayName'] = displayName; + } + await _firestore.collection('users').doc(uid).set(data); print('DEBUG: Documento criado com sucesso'); } catch (e) { print('DEBUG: Erro ao criar documento: $e'); @@ -27,6 +43,53 @@ class AuthService { } } + /// Criar inscrição do aluno na turma escolhida + static Future createEnrollment({ + required String studentId, + required String classId, + required String studentName, + }) async { + try { + print( + 'DEBUG: Criando enrollment para student=$studentId, class=$classId', + ); + final existing = await _firestore + .collection('enrollments') + .where('studentId', isEqualTo: studentId) + .where('classId', isEqualTo: classId) + .limit(1) + .get(); + if (existing.docs.isNotEmpty) { + print('DEBUG: Enrollment já existe, ignorando'); + return; + } + await _firestore.collection('enrollments').add({ + 'classId': classId, + 'studentId': studentId, + 'studentName': studentName, + 'joinedAt': FieldValue.serverTimestamp(), + }); + print('DEBUG: Enrollment criado com sucesso'); + } catch (e) { + print('DEBUG: Erro ao criar enrollment: $e'); + throw Exception('Erro ao associar aluno à turma'); + } + } + + /// Ler classId do aluno na Firestore + static Future getStudentClassId(String uid) async { + try { + final doc = await _firestore.collection('users').doc(uid).get(); + if (doc.exists) { + return doc.data()?['classId'] as String?; + } + return null; + } catch (e) { + print('DEBUG: Erro ao ler classId: $e'); + return null; + } + } + /// Ler role do usuário na Firestore static Future getUserRole(String uid) async { try { @@ -56,6 +119,8 @@ class AuthService { required String password, String? displayName, String? role, + String? classId, + String? schoolClassId, }) async { try { print('DEBUG: Tentando criar conta para email: $email'); @@ -76,9 +141,15 @@ class AuthService { print('DEBUG: Display name atualizado para: $displayName'); } - // Criar documento na Firestore com role + // Criar documento na Firestore com role (e classId se aluno) if (role != null && result.user != null) { - await createUserRole(result.user!.uid, role); + await createUserRole( + result.user!.uid, + role, + classId: classId, + schoolClassId: schoolClassId, + displayName: displayName, + ); } // Verificar se o email foi verificado diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 0dddb17..943218a 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -81,12 +81,22 @@ class _LoginPageState extends State { final actualRole = await AuthService.getUserRole(uid); print('DEBUG: Role real do usuário na Firestore: $actualRole'); - // Validar se o role selecionado corresponde ao role real + // Se não há role selecionado, redirecionar para role-selection final selectedRole = widget.selectedRole; - if (selectedRole != null && - actualRole != null && - selectedRole != actualRole) { - // Role não corresponde - mostrar erro + if (selectedRole == null) { + await AuthService.signOut(); + if (mounted) { + setState(() => _isLoading = false); + context.go('/role-selection'); + } + return; + } + + // Validar se o role selecionado corresponde ao role real + if (actualRole != null && selectedRole != actualRole) { + // Fazer logout imediato antes de mostrar erro + await AuthService.signOut(); + setState(() { _isLoading = false; }); @@ -171,8 +181,7 @@ class _LoginPageState extends State { TextButton( onPressed: () { Navigator.of(context).pop(); - // Fazer logout para limpar a sessão - AuthService.signOut(); + context.go('/role-selection'); }, child: Text( 'Voltar', diff --git a/lib/features/auth/presentation/pages/signup_page.dart b/lib/features/auth/presentation/pages/signup_page.dart index 9586147..bce9759 100644 --- a/lib/features/auth/presentation/pages/signup_page.dart +++ b/lib/features/auth/presentation/pages/signup_page.dart @@ -1,3 +1,4 @@ +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'; @@ -22,12 +23,39 @@ class _SignupPageState extends State { bool _isLoading = false; bool _obscurePassword = true; late String _selectedRole; + String? _selectedSchoolClassId; + List> _availableClasses = []; + bool _isLoadingClasses = false; @override void initState() { super.initState(); // Usar role passado da tela anterior ou default 'student' _selectedRole = widget.selectedRole ?? 'student'; + if (_selectedRole == 'student') { + _loadAvailableClasses(); + } + } + + Future _loadAvailableClasses() async { + setState(() => _isLoadingClasses = true); + try { + final snapshot = await FirebaseFirestore.instance + .collection('school_classes') + .where('active', isEqualTo: true) + .orderBy('year') + .orderBy('section') + .get(); + setState(() { + _availableClasses = snapshot.docs.map((doc) { + final data = doc.data(); + return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)}; + }).toList(); + _isLoadingClasses = false; + }); + } catch (e) { + setState(() => _isLoadingClasses = false); + } } @override @@ -38,6 +66,99 @@ class _SignupPageState extends State { super.dispose(); } + Widget _buildClassSelector(BuildContext context) { + if (_isLoadingClasses) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 12), + Text( + 'A carregar turmas...', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + ], + ), + ); + } + + if (_availableClasses.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + 'Nenhuma turma disponível. Contacta o teu professor.', + style: TextStyle( + color: Theme.of(context).colorScheme.error, + fontSize: 13, + ), + ), + ); + } + + return DropdownButtonFormField( + value: _selectedSchoolClassId, + isExpanded: true, + decoration: InputDecoration( + labelText: 'Turma', + labelStyle: TextStyle(color: Theme.of(context).colorScheme.onSurface), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.outline.withOpacity(0.3), + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: Theme.of(context).colorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8.0), + borderSide: BorderSide(color: Theme.of(context).colorScheme.error), + ), + filled: true, + fillColor: Theme.of(context).brightness == Brightness.dark + ? Theme.of(context).colorScheme.surfaceContainerHighest + : Theme.of(context).colorScheme.surface, + ), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), + dropdownColor: Theme.of(context).colorScheme.surface, + hint: Text( + 'Seleciona a tua turma', + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + items: _availableClasses + .map( + (c) => DropdownMenuItem( + value: c['id'], + child: Text(c['name']!), + ), + ) + .toList(), + onChanged: (value) => setState(() => _selectedSchoolClassId = value), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Seleciona a tua turma'; + } + return null; + }, + ); + } + Future _handleSignup() async { if (!_formKey.currentState!.validate()) return; @@ -60,6 +181,9 @@ class _SignupPageState extends State { password: password, displayName: name, role: _selectedRole, + schoolClassId: _selectedRole == 'student' + ? _selectedSchoolClassId + : null, ); print('DEBUG: Signup Firebase bem-sucedido, navegando para dashboard'); @@ -295,6 +419,12 @@ class _SignupPageState extends State { ), const SizedBox(height: 16), + // Seletor de turma (apenas para alunos) + if (_selectedRole == 'student') ...[ + _buildClassSelector(context), + const SizedBox(height: 16), + ], + // Email field TextFormField( controller: _emailController, diff --git a/lib/features/classes/presentation/pages/class_students_page.dart b/lib/features/classes/presentation/pages/class_students_page.dart index bc2a1f1..9e45cf7 100644 --- a/lib/features/classes/presentation/pages/class_students_page.dart +++ b/lib/features/classes/presentation/pages/class_students_page.dart @@ -1,9 +1,10 @@ import 'package:cloud_firestore/cloud_firestore.dart'; 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 -class ClassStudentsPage extends StatelessWidget { +class ClassStudentsPage extends StatefulWidget { final String classId; final String className; @@ -13,8 +14,110 @@ class ClassStudentsPage extends StatelessWidget { required this.className, }); + @override + State createState() => _ClassStudentsPageState(); +} + +class _ClassStudentsPageState extends State { + bool _isCheckingAccess = true; + bool _accessGranted = false; + + @override + void initState() { + super.initState(); + _verifyOwnership(); + } + + Future _verifyOwnership() async { + final currentUser = AuthService.currentUser; + if (currentUser == null) { + setState(() { + _isCheckingAccess = false; + _accessGranted = false; + }); + return; + } + + final role = await AuthService.getUserRole(currentUser.uid); + if (role != 'teacher') { + setState(() { + _isCheckingAccess = false; + _accessGranted = false; + }); + return; + } + + final classDoc = await FirebaseFirestore.instance + .collection('classes') + .doc(widget.classId) + .get(); + + final teacherId = classDoc.data()?['teacherId'] as String?; + setState(() { + _isCheckingAccess = false; + _accessGranted = classDoc.exists && teacherId == currentUser.uid; + }); + } + @override Widget build(BuildContext context) { + if (_isCheckingAccess) { + return const Scaffold( + backgroundColor: Color(0xFFF8F9FA), + body: Center( + child: CircularProgressIndicator(color: Color(0xFF82C9BD)), + ), + ); + } + + if (!_accessGranted) { + 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( + 'Acesso Negado', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.lock_outline, + size: 64, + color: Color(0xFF82C9BD), + ), + const SizedBox(height: 24), + const Text( + 'Sem permissão', + style: TextStyle( + color: Color(0xFF2D3748), + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + 'Só podes ver os alunos das tuas próprias turmas.', + style: TextStyle(color: Colors.grey[600], fontSize: 14), + textAlign: TextAlign.center, + ), + ], + ), + ), + ), + ); + } + return Scaffold( backgroundColor: const Color(0xFFF8F9FA), appBar: AppBar( @@ -28,7 +131,7 @@ class ClassStudentsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - className, + widget.className, style: const TextStyle( color: Colors.white, fontSize: 18, @@ -49,15 +152,13 @@ class ClassStudentsPage extends StatelessWidget { body: StreamBuilder( stream: FirebaseFirestore.instance .collection('enrollments') - .where('classId', isEqualTo: classId) + .where('classId', isEqualTo: widget.classId) .orderBy('joinedAt', descending: true) .snapshots(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( - child: CircularProgressIndicator( - color: Color(0xFF82C9BD), - ), + child: CircularProgressIndicator(color: Color(0xFF82C9BD)), ); } @@ -66,18 +167,11 @@ class ClassStudentsPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.error_outline, - size: 48, - color: Colors.grey[400], - ), + Icon(Icons.error_outline, size: 48, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Erro ao carregar alunos', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 16), ), ], ), @@ -91,18 +185,11 @@ class ClassStudentsPage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.people_outline, - size: 64, - color: Colors.grey[300], - ), + Icon(Icons.people_outline, size: 64, color: Colors.grey[300]), const SizedBox(height: 24), Text( 'Nenhum aluno entrou nesta turma ainda.', - style: TextStyle( - color: Colors.grey[600], - fontSize: 16, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 16), textAlign: TextAlign.center, ), const SizedBox(height: 8), @@ -110,10 +197,7 @@ class ClassStudentsPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Text( 'Partilha o código da turma para os alunos se juntarem.', - style: TextStyle( - color: Colors.grey[500], - fontSize: 13, - ), + style: TextStyle(color: Colors.grey[500], fontSize: 13), textAlign: TextAlign.center, ), ), @@ -126,8 +210,10 @@ class ClassStudentsPage extends StatelessWidget { padding: const EdgeInsets.all(16.0), itemCount: enrollments.length, itemBuilder: (context, index) { - final enrollment = enrollments[index].data() as Map; - final studentName = enrollment['studentName'] as String? ?? 'Aluno sem nome'; + final enrollment = + enrollments[index].data() as Map; + final studentName = + enrollment['studentName'] as String? ?? 'Aluno sem nome'; final joinedAt = enrollment['joinedAt'] as Timestamp?; return Container( @@ -174,10 +260,7 @@ class ClassStudentsPage extends StatelessWidget { joinedAt != null ? 'Entrou em ${_formatDate(joinedAt.toDate())}' : 'Data desconhecida', - style: TextStyle( - color: Colors.grey[600], - fontSize: 13, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 13), ), ], ), diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart index 15a3dfc..a664162 100644 --- a/lib/features/classes/presentation/pages/join_class_page.dart +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -31,6 +31,27 @@ class _JoinClassPageState extends State { setState(() => _isLoading = true); try { + final currentUser = AuthService.currentUser; + + if (currentUser == null) { + setState(() => _isLoading = false); + _showError('Erro: Utilizador não autenticado'); + return; + } + + // Verificar role — apenas alunos podem entrar por código + final userRole = await AuthService.getUserRole(currentUser.uid); + if (userRole != 'student') { + setState(() => _isLoading = false); + _showError('Apenas alunos podem entrar em turmas por código.'); + return; + } + + // Ler classId autorizado do aluno (definido no registo) + final authorizedClassId = await AuthService.getStudentClassId( + currentUser.uid, + ); + // Procurar turma pelo código final classQuery = await FirebaseFirestore.instance .collection('classes') @@ -46,11 +67,14 @@ class _JoinClassPageState extends State { final classDoc = classQuery.docs.first; final classId = classDoc.id; - final currentUser = AuthService.currentUser; - if (currentUser == null) { + // Verificar se o aluno está autorizado a entrar nesta turma + if (authorizedClassId == null || authorizedClassId != classId) { setState(() => _isLoading = false); - _showError('Erro: Utilizador não autenticado'); + _showError( + 'Não tens permissão para entrar nesta turma.\n' + 'O teu professor ainda não te adicionou a esta disciplina.', + ); return; } @@ -72,7 +96,10 @@ class _JoinClassPageState extends State { await FirebaseFirestore.instance.collection('enrollments').add({ 'classId': classId, 'studentId': currentUser.uid, - 'studentName': currentUser.displayName ?? currentUser.email?.split('@')[0] ?? 'Aluno', + 'studentName': + currentUser.displayName ?? + currentUser.email?.split('@')[0] ?? + 'Aluno', 'joinedAt': FieldValue.serverTimestamp(), }); @@ -163,10 +190,7 @@ class _JoinClassPageState extends State { Center( child: Text( 'O professor partilhou contigo um código de 6 caracteres.', - style: TextStyle( - color: Colors.grey[600], - fontSize: 14, - ), + style: TextStyle(color: Colors.grey[600], fontSize: 14), textAlign: TextAlign.center, ), ), @@ -219,7 +243,9 @@ class _JoinClassPageState extends State { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - disabledBackgroundColor: const Color(0xFF82C9BD).withOpacity(0.5), + disabledBackgroundColor: const Color( + 0xFF82C9BD, + ).withOpacity(0.5), ), child: _isLoading ? const SizedBox( diff --git a/lib/features/classes/presentation/pages/teacher_all_classes_page.dart b/lib/features/classes/presentation/pages/teacher_all_classes_page.dart new file mode 100644 index 0000000..a5e82ef --- /dev/null +++ b/lib/features/classes/presentation/pages/teacher_all_classes_page.dart @@ -0,0 +1,41 @@ +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 +/// Reutiliza o TeacherClassesListWidget existente +class TeacherAllClassesPage extends StatelessWidget { + const TeacherAllClassesPage({super.key}); + + @override + Widget build(BuildContext context) { + 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( + 'As Minhas Turmas', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + body: const SingleChildScrollView( + padding: EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TeacherClassesListWidget(), + SizedBox(height: 40), + ], + ), + ), + ); + } +} diff --git a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart index 2cc7cf5..da45cc4 100644 --- a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart @@ -19,14 +19,23 @@ class _StudentDashboardPageState extends State { @override void initState() { super.initState(); - _loadUserData(); + _checkRoleAndLoadData(); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // Reload user data when dependencies change (e.g., after navigation) - _loadUserData(); + Future _checkRoleAndLoadData() async { + final user = AuthService.currentUser; + if (user == null) { + if (mounted) context.go('/role-selection'); + return; + } + final role = await AuthService.getUserRole(user.uid); + if (role != 'student') { + if (mounted) { + context.go('/role-selection'); + } + return; + } + await _loadUserData(); } Future _loadUserData() async { diff --git a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart index 18984c0..8de0eda 100644 --- a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart @@ -19,13 +19,23 @@ class _TeacherDashboardPageState extends State { @override void initState() { super.initState(); - _loadUserData(); + _checkRoleAndLoadData(); } - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _loadUserData(); + Future _checkRoleAndLoadData() async { + final user = AuthService.currentUser; + if (user == null) { + if (mounted) context.go('/role-selection'); + return; + } + final role = await AuthService.getUserRole(user.uid); + if (role != 'teacher') { + if (mounted) { + context.go('/role-selection'); + } + return; + } + await _loadUserData(); } Future _loadUserData() async { 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 1b4bd1c..08f054d 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_analytics_preview_widget.dart @@ -288,6 +288,8 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget { label, style: TextStyle(color: color.withOpacity(0.8), fontSize: 10), textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ], ), diff --git a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart index 00d9c90..c858046 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart @@ -34,18 +34,22 @@ class TeacherHeroWidget extends StatelessWidget { children: [ Text( 'Visão Geral da Turma', + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, - fontSize: 24, + fontSize: 20, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), Text( 'Acompanhe o progresso dos seus alunos', + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 16, + fontSize: 13, ), ), ], @@ -108,12 +112,16 @@ class TeacherHeroWidget extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Progresso Médio da Turma', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + const Flexible( + child: Text( + 'Progresso Médio da Turma', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold, + ), ), ), Text( @@ -283,8 +291,10 @@ class TeacherHeroWidget extends StatelessWidget { const SizedBox(height: 4), Text( label, - style: const TextStyle(color: Colors.white, fontSize: 12), + style: const TextStyle(color: Colors.white, fontSize: 11), textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ], ), 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 4e0e9e0..03ebce6 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart @@ -53,9 +53,6 @@ class _TeacherQuickActionsWidgetState extends State { // Secondary Actions Row Row( children: [ - // Manage Students Card - Expanded(child: _buildManageStudentsCard(context)), - const SizedBox(width: 16), // View Analytics Card Expanded(child: _buildViewAnalyticsCard(context)), ], @@ -71,273 +68,214 @@ class _TeacherQuickActionsWidgetState extends State { } Widget _buildUploadContentCard(BuildContext context) { - return Container( - constraints: const BoxConstraints(minHeight: 135, maxHeight: 160), - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Theme.of(context).colorScheme.primary, - Theme.of(context).colorScheme.primary.withOpacity(0.8), - ], - ), - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.primary.withOpacity(0.3), - blurRadius: 15, - offset: const Offset(0, 8), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => const TeacherMaterialsPage()), - ), - child: Padding( - padding: const EdgeInsets.all(14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.upload_file, - color: Colors.white, - size: 22, - ), - ), - const Spacer(), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondary, - borderRadius: BorderRadius.circular(10), - ), - child: const Text( - 'NOVO', - style: TextStyle( - color: Colors.white, - fontSize: 9, - fontWeight: FontWeight.bold, - ), - ), - ), - ], - ), - const Spacer(), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Upload Conteúdo', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - SizedBox(height: 4), - Text( - 'PDFs, textos, imagens', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 12, - height: 1.2, - ), - ), - ], + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + Container( + height: 150, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.3), + blurRadius: 15, + offset: const Offset(0, 8), ), ], ), - ), - ), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 100)); + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const TeacherMaterialsPage(), + ), + ), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.upload_file, + color: Colors.white, + size: 20, + ), + ), + const Spacer(), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 3, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.secondary, + borderRadius: BorderRadius.circular(10), + ), + child: const Text( + 'NOVO', + style: TextStyle( + color: Colors.white, + fontSize: 9, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: const [ + Text( + 'Upload Conteúdo', + style: TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 2), + Text( + 'PDFs, textos, imagens', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 11, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 100)), + ); } Widget _buildCreateQuizCard(BuildContext context) { - return Container( - height: 150, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => context.go('/teacher/quiz/create'), - child: Padding( - padding: const EdgeInsets.all(14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.secondary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.quiz, - color: Theme.of(context).colorScheme.secondary, - size: 24, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Criar Quiz', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - 'Avaliações interativas', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 12, - height: 1.2, - ), - ), - ], + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + Container( + height: 150, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Theme.of( + context, + ).colorScheme.shadow.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), ], ), - ), - ), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 200)); - } - - Widget _buildManageStudentsCard(BuildContext context) { - return Container( - height: 120, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: () => context.go('/teacher/students'), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.people, - color: Theme.of(context).colorScheme.primary, - size: 20, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () => context.go('/teacher/quiz/create'), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.secondary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.quiz, + color: Theme.of(context).colorScheme.secondary, + size: 20, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Criar Quiz', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + 'Avaliações interativas', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + ], + ), + ], ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Gerir Alunos', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 14, - fontWeight: FontWeight.bold, - ), - ), - Text( - 'Acesso e permissões', - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 11, - ), - ), - ], - ), - ], + ), ), - ), - ), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 300)); + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 200)), + ); } Widget _buildViewAnalyticsCard(BuildContext context) { @@ -420,183 +358,312 @@ class _TeacherQuickActionsWidgetState extends State { } Widget _buildCreateClassCard(BuildContext context) { - return Container( - constraints: const BoxConstraints(minHeight: 135, maxHeight: 160), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Theme.of(context).colorScheme.outline.withOpacity(0.2), - width: 1, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).colorScheme.shadow.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(16), - onTap: _isCreatingClass - ? null - : () => _showCreateClassDialog(context), - child: Padding( - padding: const EdgeInsets.all(14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: _isCreatingClass - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context).colorScheme.primary, - ), - ) - : Icon( - Icons.school, - color: Theme.of(context).colorScheme.primary, - size: 24, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Criar Turma', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - 'Gerar código de acesso', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Theme.of( - context, - ).colorScheme.onSurfaceVariant, - fontSize: 12, - height: 1.2, - ), - ), - ], + return ClipRRect( + borderRadius: BorderRadius.circular(16), + child: + Container( + height: 150, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Theme.of( + context, + ).colorScheme.shadow.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), ], ), - ), - ), - ), - ) - .animate() - .scale( - duration: const Duration(milliseconds: 600), - curve: Curves.elasticOut, - ) - .then(delay: const Duration(milliseconds: 150)); + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: _isCreatingClass + ? null + : () => _showCreateClassDialog(context), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: _isCreatingClass + ? SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of( + context, + ).colorScheme.primary, + ), + ) + : Icon( + Icons.school, + color: Theme.of( + context, + ).colorScheme.primary, + size: 20, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Criar Turma', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 2), + Text( + 'Gerar código de acesso', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 11, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 150)), + ); } void _showCreateClassDialog(BuildContext context) { final TextEditingController nameController = TextEditingController(); + String? selectedSchoolClassId; + List> schoolClasses = []; + bool isLoadingClasses = true; + + // Carregar school_classes antes de mostrar o dialog + FirebaseFirestore.instance + .collection('school_classes') + .where('active', isEqualTo: true) + .orderBy('year') + .orderBy('section') + .get() + .then((snapshot) { + schoolClasses = snapshot.docs.map((doc) { + final data = doc.data(); + return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)}; + }).toList(); + isLoadingClasses = false; + }) + .catchError((_) { + isLoadingClasses = false; + }); showDialog( context: context, builder: (BuildContext dialogContext) { - return AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: Text( - 'Criar Nova Turma', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - fontWeight: FontWeight.bold, - ), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Digite o nome da turma:', + return StatefulBuilder( + builder: (context, setDialogState) { + // Atualizar estado do dialog quando as classes carregarem + if (isLoadingClasses) { + Future.delayed(const Duration(milliseconds: 300), () { + if (dialogContext.mounted) setDialogState(() {}); + }); + } + + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: Text( + 'Criar Nova Disciplina', style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 14, + color: Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.bold, ), ), - const SizedBox(height: 12), - TextField( - controller: nameController, - decoration: InputDecoration( - hintText: 'Ex: Matemática 9º Ano', - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Nome da disciplina:', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, + const SizedBox(height: 12), + TextField( + controller: nameController, + decoration: InputDecoration( + hintText: 'Ex: Matemática, Física...', + 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, + ), + ), + ), ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2, + const SizedBox(height: 16), + Text( + 'Turma:', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + isLoadingClasses + ? Row( + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context).colorScheme.primary, + ), + ), + const SizedBox(width: 10), + Text( + 'A carregar turmas...', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ) + : DropdownButtonFormField( + value: selectedSchoolClassId, + isExpanded: true, + decoration: InputDecoration( + 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, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + hint: Text( + 'Seleciona a turma', + style: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + ), + ), + items: schoolClasses + .map( + (c) => DropdownMenuItem( + value: c['id'], + child: Text(c['name']!), + ), + ) + .toList(), + onChanged: (value) => setDialogState( + () => selectedSchoolClassId = value, + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text( + 'Cancelar', + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(), - child: Text( - 'Cancelar', - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, + ElevatedButton( + onPressed: () { + final className = nameController.text.trim(); + if (className.isNotEmpty) { + Navigator.of(dialogContext).pop(); + _createClass( + className, + schoolClassId: selectedSchoolClassId, + ); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.primary, + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Criar'), ), - ), - ), - ElevatedButton( - onPressed: () { - final className = nameController.text.trim(); - if (className.isNotEmpty) { - Navigator.of(dialogContext).pop(); - _createClass(className); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: const Text('Criar'), - ), - ], + ], + ); + }, ); }, ); @@ -613,7 +680,7 @@ class _TeacherQuickActionsWidgetState extends State { ); } - Future _createClass(String className) async { + Future _createClass(String className, {String? schoolClassId}) async { setState(() { _isCreatingClass = true; }); @@ -627,12 +694,17 @@ class _TeacherQuickActionsWidgetState extends State { final classCode = _generateClassCode(); final firestore = FirebaseFirestore.instance; - await firestore.collection('classes').add({ + final Map classData = { 'name': className, 'teacherId': currentUser.uid, 'code': classCode, 'createdAt': FieldValue.serverTimestamp(), - }); + }; + if (schoolClassId != null && schoolClassId.isNotEmpty) { + classData['schoolClassId'] = schoolClassId; + } + + await firestore.collection('classes').add(classData); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/settings/presentation/providers/settings_provider.dart b/lib/features/settings/presentation/providers/settings_provider.dart index 9225391..23ce8c3 100644 --- a/lib/features/settings/presentation/providers/settings_provider.dart +++ b/lib/features/settings/presentation/providers/settings_provider.dart @@ -32,10 +32,12 @@ class SettingsNotifier extends StateNotifier { SettingsNotifier() : super( SettingsState( - themeMode: ThemeService.getThemeMode(), + themeMode: ThemeMode.light, isDarkModeAvailable: ThemeService.isDarkModeAvailable(), ), - ); + ) { + loadSettings(); + } /// Set theme mode Future setThemeMode(ThemeMode themeMode) async { @@ -60,10 +62,8 @@ class SettingsNotifier extends StateNotifier { Future resetSettings() async { state = state.copyWith(isLoading: true); await ThemeService.resetTheme(); - state = state.copyWith( - themeMode: ThemeService.getThemeMode(), - isLoading: false, - ); + final themeMode = await ThemeService.getStoredThemeMode(); + state = state.copyWith(themeMode: themeMode, isLoading: false); } } diff --git a/test/rag_services_test.dart b/test/rag_services_test.dart index 3dce189..c77e01d 100644 --- a/test/rag_services_test.dart +++ b/test/rag_services_test.dart @@ -1,8 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; -import '../lib/core/services/content_service.dart'; import '../lib/core/services/vector_service.dart'; -import '../lib/core/services/rag_service.dart'; import '../lib/core/services/rag_ai_service.dart'; +import '../lib/core/services/rag_service.dart' show TutorMode; import '../lib/core/models/content_chunk.dart'; void main() { @@ -64,81 +63,6 @@ void main() { ); }); - test('RAGService - Mode Instructions', () { - print('🔍 Testing RAG service mode instructions...'); - - // Test different modes - final explanationMode = RAGService._getModeInstructions( - TutorMode.explanation, - ); - final tutorMode = RAGService._getModeInstructions(TutorMode.tutor); - final explorationMode = RAGService._getModeInstructions( - TutorMode.exploration, - ); - - expect(explanationMode, contains('explicações detalhadas')); - expect(tutorMode, contains('método socrático')); - expect(explorationMode, contains('exploração')); - - print('✅ Mode instructions generated correctly'); - print(' Explanation: "${explanationMode.substring(0, 50)}..."'); - print(' Tutor: "${tutorMode.substring(0, 50)}..."'); - print(' Exploration: "${explorationMode.substring(0, 50)}..."'); - }); - - test('RAGService - Context Building', () { - print('🔍 Testing context window building...'); - - // Create mock content chunks - final chunks = [ - ContentChunk( - id: 'test1', - contentId: 'content1', - text: - 'A fotossíntese é o processo biológico que converte luz solar em energia química.', - subject: 'Biologia', - concept: 'Fotossíntese', - unit: 'Processos Biológicos', - difficulty: 0.6, - grade: 10, - embedding: List.filled(384, 0.1), - sourceDocument: 'Biologia Manual.pdf', - metadata: {}, - createdAt: DateTime.now(), - ), - ContentChunk( - id: 'test2', - contentId: 'content1', - text: - 'Durante a fotossíntese, as plantas absorvem CO2 e liberam oxigénio.', - subject: 'Biologia', - concept: 'Fotossíntese', - unit: 'Processos Biológicos', - difficulty: 0.7, - grade: 10, - embedding: List.filled(384, 0.2), - sourceDocument: 'Biologia Manual.pdf', - metadata: {}, - createdAt: DateTime.now(), - ), - ]; - - final context = RAGService._buildContextWindow( - chunks, - 'O que é fotossíntese?', - TutorMode.explanation, - ); - - expect(context, contains('CONTEÚDO EDUCACIONAL RELEVANTE')); - expect(context, contains('Fotossíntese')); - expect(context, contains('Biologia')); - expect(context, contains('INSTRUÇÕES DE TUTORIA')); - - print('✅ Context window built successfully'); - print(' Context length: ${context.length} characters'); - print(' Contains ${chunks.length} source chunks'); - }); - test('RAGAIService - Service Availability', () async { print('🔍 Testing Ollama service availability...'); @@ -200,19 +124,11 @@ void main() { ), ]; - // Step 3: Build context - final context = RAGService._buildContextWindow( - mockChunks, - userQuery, - mode, - ); - print(' ✅ Context built (${context.length} chars)'); - - // Step 4: Test AI service if available + // Step 3: Test AI service if available try { final ragResponse = await RAGAIService.generateRAGResponse( userQuery: userQuery, - context: context, + context: userQuery, mode: mode, sources: mockChunks, ); @@ -221,23 +137,8 @@ void main() { print(' Answer: "${ragResponse.answer.substring(0, 100)}..."'); print(' Confidence: ${ragResponse.confidence.toStringAsFixed(2)}'); print(' Sources: ${ragResponse.sources.length}'); - print( - ' Related concepts: ${ragResponse.relatedConcepts.join(', ')}', - ); } catch (e) { - print(' ⚠️ AI service not available, using mock response'); - - // Create mock response - final mockResponse = RAGService._createRAGResponse( - query: userQuery, - context: context, - mode: mode, - sources: mockChunks, - ); - - print(' ✅ Mock RAG response created'); - print(' Answer: "${mockResponse.answer.substring(0, 100)}..."'); - print(' Confidence: ${mockResponse.confidence.toStringAsFixed(2)}'); + print(' ⚠️ AI service not available: $e'); } print('✅ RAG pipeline simulation completed successfully');