From ad400a9c37f77dd504468f0b64c456aa9bcf6cfd Mon Sep 17 00:00:00 2001 From: 240403 <240403@epvc.pt> Date: Tue, 12 May 2026 18:59:22 +0100 Subject: [PATCH] =?UTF-8?q?tela=20de=20verifica=C3=A7=C3=A3o=20de=20turma,?= =?UTF-8?q?=20possibilidade=20de=20alunos=20entrarem=20em=20turmas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/CHANGELOG.md | 37 +++ docs/PROJECT_PROGRESS.md | 68 +++++ .../pages/class_students_page.dart | 196 ++++++++++++++ .../presentation/pages/join_class_page.dart | 247 ++++++++++++++++++ .../pages/student_dashboard_page.dart | 8 +- .../widgets/quick_access_widget.dart | 93 +++++++ .../widgets/student_classes_list_widget.dart | 192 ++++++++++++++ .../widgets/teacher_classes_list_widget.dart | 115 ++++---- 8 files changed, 905 insertions(+), 51 deletions(-) create mode 100644 lib/features/classes/presentation/pages/class_students_page.dart create mode 100644 lib/features/classes/presentation/pages/join_class_page.dart create mode 100644 lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 25141f3..a46ebd5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,43 @@ ## [Unreleased] ### Added +- **Student Classes List (ETAPA 5)** - Students can now view their enrolled classes on the home page + - New `StudentClassesListWidget` at `/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart` + - Query: `.collection('enrollments').where('studentId', isEqualTo: currentUser.uid).orderBy('joinedAt', descending: true)` + - For each enrollment, fetches corresponding class document from `classes` collection using `classId` + - Layout: Same horizontal 2-row scroll pattern as `TeacherClassesListWidget` + - Cards display: Class name + Class code + - Loading state: `CircularProgressIndicator` (centered in card while loading class data) + - Empty state: Text "Ainda não entraste em nenhuma turma." + - Widget inserted in `StudentDashboardPage` after QuickAccessWidget + - Visual design: White cards (#FFFFFF), teal icon background (#82C9BD with 10% opacity), rounded corners (16px), subtle shadows + - Title: "As Minhas Turmas" (same style as teacher dashboard) + - System now bidirectional: Teachers see students, Students see classes + +- **Join Class Feature (ETAPA 4)** - Students can now join classes using class codes + - New `JoinClassPage` screen at `/lib/features/classes/presentation/pages/join_class_page.dart` + - TextField for entering 6-character class code (uppercase, centered, letter-spacing) + - "Entrar na Turma" button with loading state and visual feedback + - Firestore query: `.collection('classes').where('code', isEqualTo: enteredCode).limit(1)` + - Validation: checks if code exists, if student already enrolled + - On success: creates document in `enrollments` collection with `classId`, `studentId`, `studentName`, `joinedAt` + - Success feedback: green SnackBar "Entraste na turma com sucesso!" + - Error feedback: red SnackBar for invalid code, duplicate enrollment, or auth errors + - Auto-returns to student home after successful join + - Visual design: teal AppBar (#82C9BD), centered icon, clean input field with rounded corners + - New "Entrar numa Turma" card in Student Dashboard Quick Access section + - Card design: horizontal layout with `Icons.group_add`, white background, rounded corners + +- **Class Students View (ETAPA 3)** - Teachers can now view enrolled students in each class + - New `ClassStudentsPage` screen at `/lib/features/classes/presentation/pages/class_students_page.dart` + - StreamBuilder query on `enrollments` collection with filter by `classId` + - ListTile layout showing student name and join date + - Loading state with `CircularProgressIndicator` + - Empty state message when no students enrolled + - Date formatting using `intl` package (dd/MM/yyyy format) + - Consistent styling with existing app design (teal colors, rounded cards) + - Navigation via `MaterialPageRoute` from class card tap + - **Class Creation Feature (ETAPA 1)** - Teachers can now create classes from the dashboard - New "Criar Turma" button in Teacher Dashboard Quick Actions - Simple dialog interface for entering class name diff --git a/docs/PROJECT_PROGRESS.md b/docs/PROJECT_PROGRESS.md index 1b31145..d276cb0 100644 --- a/docs/PROJECT_PROGRESS.md +++ b/docs/PROJECT_PROGRESS.md @@ -404,6 +404,74 @@ This document tracks the overall progress of the AI Study Assistant project deve ### **Last 24 Hours:** +- ✅ **ETAPA 5: Student Classes List** - Students can now view their enrolled classes on the home page + - New `StudentClassesListWidget` component at `lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart` + - Query: `.collection('enrollments').where('studentId', isEqualTo: currentUser.uid).orderBy('joinedAt', descending: true)` + - For each enrollment document, uses `FutureBuilder` to fetch the corresponding class from `classes` collection + - Layout identical to `TeacherClassesListWidget`: + - Horizontal `ListView.builder` with `scrollDirection: Axis.horizontal` + - 2-row grid layout: Column with cards at index * 2 and index * 2 + 1 + - Card size: 200x150 pixels + - Card styling: White background, 16px border radius, subtle shadow + - Card content: Icon (school), class name (bold), class code (grey) + - Title: "As Minhas Turmas" with `textTheme.titleLarge` style + - Loading state: `CircularProgressIndicator` centered in card while loading class data + - Empty state: "Ainda não entraste em nenhuma turma." + - Widget inserted in `StudentDashboardPage` between `QuickAccessWidget` and `ProfileSectionWidget` + - **System is now bidirectional:** + - Professor creates classes → Students can join + - Professor sees list of students in each class (ETAPA 3) + - Students see list of classes they joined (ETAPA 5) + - Both use same visual patterns for consistency + - Testing: After joining a class (ETAPA 4), the class appears immediately in the student's home list + +- ✅ **ETAPA 4: Join Class Feature** - Students can now join classes using class codes + - New `JoinClassPage` component at `lib/features/classes/presentation/pages/join_class_page.dart` + - Query: `.collection('classes').where('code', isEqualTo: enteredCode).limit(1)` + - Input: TextField with uppercase formatting, 6 character limit, centered text, letter spacing + - Validation flow: + 1. Check if code is empty → show error "Insere o código da turma" + 2. Query Firestore for class with matching code + 3. If no class found → show error "Código de turma inválido" + 4. Check if student already enrolled → show error "Já estás inscrito nesta turma" + 5. Create enrollment document in `enrollments` collection + - Enrollment document structure: + ```dart + { + 'classId': classDoc.id, + 'studentId': currentUser.uid, + 'studentName': currentUser.displayName ?? email.split('@')[0] ?? 'Aluno', + 'joinedAt': FieldValue.serverTimestamp(), + } + ``` + - Success feedback: Green SnackBar "Entraste na turma com sucesso!" (2 seconds) + - Error feedback: Red SnackBar with specific error message (3 seconds) + - Auto-navigates back to Student Dashboard after successful join + - Loading state: CircularProgressIndicator in button while processing + - Visual design: Teal AppBar (#82C9BD), centered group_add icon, clean white input card + - New "Entrar numa Turma" card added to `QuickAccessWidget` in Student Dashboard + - Card design: Horizontal layout with `Icons.group_add`, white background, rounded corners (16px) + - Testing in Firebase Console: + 1. Go to Firestore Database → classes collection + 2. Copy the `code` field from any class document + 3. In app: Student Dashboard → "Entrar numa Turma" + 4. Paste code and tap "Entrar na Turma" + 5. Check enrollments collection for new document with correct data + +- ✅ **ETAPA 3: Class Students View** - Teachers can now view enrolled students in each class + - New `ClassStudentsPage` component at `lib/features/classes/presentation/pages/class_students_page.dart` + - Query: `.collection('enrollments').where('classId', isEqualTo: classId).orderBy('joinedAt', descending: true)` + - StreamBuilder for real-time updates when students join + - ListTile design with student icon, name, and formatted join date + - Empty state: "Nenhum aluno entrou nesta turma ainda." + - Loading state with `CircularProgressIndicator` + - Error state with error icon and message + - Date formatting using `intl` package (Portuguese format: dd/MM/yyyy) + - Navigation via `GestureDetector` onTap in `TeacherClassesListWidget` + - MaterialPageRoute navigation passing `classId` and `className` as parameters + - AppBar with back button and two-line title (class name + subtitle) + - Consistent visual design: teal colors (#82C9BD), white cards, rounded corners (16px), subtle shadows + - ✅ **ETAPA 2: Classes List Display** - Teachers can now view their created classes - New `TeacherClassesListWidget` component - "As Minhas Turmas" section added to Teacher Dashboard diff --git a/lib/features/classes/presentation/pages/class_students_page.dart b/lib/features/classes/presentation/pages/class_students_page.dart new file mode 100644 index 0000000..bc2a1f1 --- /dev/null +++ b/lib/features/classes/presentation/pages/class_students_page.dart @@ -0,0 +1,196 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// Página para visualizar os alunos de uma turma específica +class ClassStudentsPage extends StatelessWidget { + final String classId; + final String className; + + const ClassStudentsPage({ + super.key, + required this.classId, + required this.className, + }); + + @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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + className, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const Text( + 'Alunos Matriculados', + style: TextStyle( + color: Colors.white70, + fontSize: 13, + fontWeight: FontWeight.w300, + ), + ), + ], + ), + ), + body: StreamBuilder( + stream: FirebaseFirestore.instance + .collection('enrollments') + .where('classId', isEqualTo: classId) + .orderBy('joinedAt', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: CircularProgressIndicator( + color: Color(0xFF82C9BD), + ), + ); + } + + if (snapshot.hasError) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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, + ), + ), + ], + ), + ); + } + + final enrollments = snapshot.data?.docs ?? []; + + if (enrollments.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + 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, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Padding( + 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, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ); + } + + return ListView.builder( + 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 joinedAt = enrollment['joinedAt'] as Timestamp?; + + return Container( + margin: const EdgeInsets.only(bottom: 12.0), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16.0), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ListTile( + contentPadding: const EdgeInsets.all(16.0), + leading: Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(12.0), + ), + child: const Icon( + Icons.person, + color: Color(0xFF82C9BD), + size: 24, + ), + ), + title: Text( + studentName, + style: const TextStyle( + color: Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + joinedAt != null + ? 'Entrou em ${_formatDate(joinedAt.toDate())}' + : 'Data desconhecida', + style: TextStyle( + color: Colors.grey[600], + fontSize: 13, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ); + } + + String _formatDate(DateTime date) { + return DateFormat('dd/MM/yyyy').format(date); + } +} diff --git a/lib/features/classes/presentation/pages/join_class_page.dart b/lib/features/classes/presentation/pages/join_class_page.dart new file mode 100644 index 0000000..15a3dfc --- /dev/null +++ b/lib/features/classes/presentation/pages/join_class_page.dart @@ -0,0 +1,247 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; +import '../../../../core/services/auth_service.dart'; + +/// Página para o aluno entrar numa turma usando o código +class JoinClassPage extends StatefulWidget { + const JoinClassPage({super.key}); + + @override + State createState() => _JoinClassPageState(); +} + +class _JoinClassPageState extends State { + final _codeController = TextEditingController(); + bool _isLoading = false; + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + Future _joinClass() async { + final code = _codeController.text.trim().toUpperCase(); + + if (code.isEmpty) { + _showError('Insere o código da turma'); + return; + } + + setState(() => _isLoading = true); + + try { + // Procurar turma pelo código + final classQuery = await FirebaseFirestore.instance + .collection('classes') + .where('code', isEqualTo: code) + .limit(1) + .get(); + + if (classQuery.docs.isEmpty) { + setState(() => _isLoading = false); + _showError('Código de turma inválido'); + return; + } + + final classDoc = classQuery.docs.first; + final classId = classDoc.id; + final currentUser = AuthService.currentUser; + + if (currentUser == null) { + setState(() => _isLoading = false); + _showError('Erro: Utilizador não autenticado'); + return; + } + + // Verificar se já está inscrito nesta turma + final existingEnrollment = await FirebaseFirestore.instance + .collection('enrollments') + .where('classId', isEqualTo: classId) + .where('studentId', isEqualTo: currentUser.uid) + .limit(1) + .get(); + + if (existingEnrollment.docs.isNotEmpty) { + setState(() => _isLoading = false); + _showError('Já estás inscrito nesta turma'); + return; + } + + // Criar documento de inscrição + await FirebaseFirestore.instance.collection('enrollments').add({ + 'classId': classId, + 'studentId': currentUser.uid, + 'studentName': currentUser.displayName ?? currentUser.email?.split('@')[0] ?? 'Aluno', + 'joinedAt': FieldValue.serverTimestamp(), + }); + + setState(() => _isLoading = false); + + // Mostrar sucesso + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Entraste na turma com sucesso!'), + backgroundColor: Color(0xFF10B981), + duration: Duration(seconds: 2), + ), + ); + + // Voltar para a home + Navigator.of(context).pop(); + } + } catch (e) { + setState(() => _isLoading = false); + _showError('Erro ao entrar na turma: $e'); + } + } + + void _showError(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: const Color(0xFFEF4444), + duration: const Duration(seconds: 3), + ), + ); + } + + @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( + 'Entrar numa Turma', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + 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', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart index cce06ce..20c1376 100644 --- a/lib/features/dashboard/presentation/pages/student_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/student_dashboard_page.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; import '../widgets/progress_hero_widget.dart'; import '../widgets/quick_access_widget.dart'; +import '../widgets/student_classes_list_widget.dart'; import '../widgets/profile_section_widget.dart'; class StudentDashboardPage extends StatefulWidget { @@ -146,7 +147,12 @@ class _StudentDashboardPageState extends State { const SizedBox(height: 24), - // Profile Section (Priority 3) + // Classes List Section (Priority 3) + const StudentClassesListWidget(), + + const SizedBox(height: 24), + + // Profile Section (Priority 4) const ProfileSectionWidget(), const SizedBox(height: 40), diff --git a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart index c841a75..e01a1d6 100644 --- a/lib/features/dashboard/presentation/widgets/quick_access_widget.dart +++ b/lib/features/dashboard/presentation/widgets/quick_access_widget.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; +import '../../../classes/presentation/pages/join_class_page.dart'; /// Quick access cards for Tutor IA and Quiz with fixed overflow class QuickAccessWidget extends StatelessWidget { @@ -30,6 +31,9 @@ class QuickAccessWidget extends StatelessWidget { Expanded(flex: 2, child: _buildQuizCard(context)), ], ), + const SizedBox(height: 16), + // Join Class Card + _buildJoinClassCard(context), ], ) .animate() @@ -235,4 +239,93 @@ class QuickAccessWidget extends StatelessWidget { ) .then(delay: const Duration(milliseconds: 200)); } + + Widget _buildJoinClassCard(BuildContext context) { + return Container( + height: 80, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0), width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => const JoinClassPage(), + ), + ); + }, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.group_add, + color: Color(0xFF82C9BD), + size: 24, + ), + ), + const SizedBox(width: 16), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Entrar numa Turma', + style: TextStyle( + color: Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 4), + Text( + 'Junta-te a uma turma com o código', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Color(0xFF718096), + fontSize: 13, + ), + ), + ], + ), + ), + const Icon( + Icons.arrow_forward_ios, + color: Color(0xFF82C9BD), + size: 16, + ), + ], + ), + ), + ), + ), + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 300)); + } } diff --git a/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart b/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart new file mode 100644 index 0000000..5a73d04 --- /dev/null +++ b/lib/features/dashboard/presentation/widgets/student_classes_list_widget.dart @@ -0,0 +1,192 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; + +import '../../../../core/services/auth_service.dart'; + +/// Widget para listar as turmas onde o aluno está inscrito +class StudentClassesListWidget extends StatelessWidget { + const StudentClassesListWidget({super.key}); + + @override + Widget build(BuildContext context) { + final currentUser = AuthService.currentUser; + + if (currentUser == null) { + return const SizedBox.shrink(); + } + + return StreamBuilder( + stream: FirebaseFirestore.instance + .collection('enrollments') + .where('studentId', isEqualTo: currentUser.uid) + .orderBy('joinedAt', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator( + color: Color(0xFF82C9BD), + ), + ), + ); + } + + if (snapshot.hasError) { + return const SizedBox.shrink(); + } + + final enrollments = snapshot.data?.docs ?? []; + + if (enrollments.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'Ainda não entraste em nenhuma turma.', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'As Minhas Turmas', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: const Color(0xFF2D3748), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 330, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 16), + itemCount: (enrollments.length + 1) ~/ 2, + itemBuilder: (context, index) { + final firstIndex = index * 2; + final secondIndex = firstIndex + 1; + + return Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + children: [ + _buildClassCard(enrollments[firstIndex]), + const SizedBox(height: 12), + if (secondIndex < enrollments.length) + _buildClassCard(enrollments[secondIndex]), + ], + ), + ); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildClassCard(DocumentSnapshot enrollmentDoc) { + final enrollmentData = enrollmentDoc.data() as Map; + final classId = enrollmentData['classId'] as String? ?? ''; + + if (classId.isEmpty) { + return const SizedBox.shrink(); + } + + return FutureBuilder( + future: FirebaseFirestore.instance.collection('classes').doc(classId).get(), + builder: (context, snapshot) { + if (!snapshot.hasData || !snapshot.data!.exists) { + return Container( + width: 200, + height: 150, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Center( + child: CircularProgressIndicator( + color: Color(0xFF82C9BD), + strokeWidth: 2, + ), + ), + ); + } + + final classData = snapshot.data!.data() as Map; + final className = classData['name'] as String? ?? 'Sem nome'; + final classCode = classData['code'] as String? ?? '----'; + + return Container( + width: 200, + height: 150, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.school, + color: Color(0xFF82C9BD), + size: 24, + ), + ), + const SizedBox(height: 12), + Text( + className, + style: const TextStyle( + color: Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Código: $classCode', + style: TextStyle( + color: Colors.grey[600], + fontSize: 13, + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart index b886dad..5729a3f 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart @@ -2,6 +2,7 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import '../../../../core/services/auth_service.dart'; +import '../../../classes/presentation/pages/class_students_page.dart'; /// Widget para listar as turmas criadas pelo professor class TeacherClassesListWidget extends StatelessWidget { @@ -77,10 +78,10 @@ class TeacherClassesListWidget extends StatelessWidget { padding: const EdgeInsets.only(right: 12), child: Column( children: [ - _buildClassCard(classes[firstIndex]), + _buildClassCard(classes[firstIndex], context), const SizedBox(height: 12), if (secondIndex < classes.length) - _buildClassCard(classes[secondIndex]), + _buildClassCard(classes[secondIndex], context), ], ), ); @@ -93,62 +94,76 @@ class TeacherClassesListWidget extends StatelessWidget { ); } - Widget _buildClassCard(DocumentSnapshot doc) { + Widget _buildClassCard(DocumentSnapshot doc, BuildContext context) { final data = doc.data() as Map; + final classId = doc.id; final className = data['name'] as String? ?? 'Sem nome'; final classCode = data['code'] as String? ?? '----'; - return Container( - width: 200, - height: 150, - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: const Offset(0, 4), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: const Color(0xFF82C9BD).withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: const Icon( - Icons.school, - color: Color(0xFF82C9BD), - size: 24, + return GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ClassStudentsPage( + classId: classId, + className: className, ), ), - const SizedBox(height: 12), - Text( - className, - style: const TextStyle( - color: Color(0xFF2D3748), - fontSize: 16, - fontWeight: FontWeight.bold, + ); + }, + child: Container( + width: 200, + height: 150, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: 4), - Text( - 'Código: $classCode', - style: TextStyle( - color: Colors.grey[600], - fontSize: 13, + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.school, + color: Color(0xFF82C9BD), + size: 24, + ), ), - ), - ], + const SizedBox(height: 12), + Text( + className, + style: const TextStyle( + color: Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Código: $classCode', + style: TextStyle( + color: Colors.grey[600], + fontSize: 13, + ), + ), + ], + ), ), ); }