tela de verificação de turma, possibilidade de alunos entrarem em turmas

This commit is contained in:
2026-05-12 18:59:22 +01:00
parent b7988eb608
commit ad400a9c37
8 changed files with 905 additions and 51 deletions

View File

@@ -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));
}
}

View File

@@ -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<QuerySnapshot>(
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<String, dynamic>;
final classId = enrollmentData['classId'] as String? ?? '';
if (classId.isEmpty) {
return const SizedBox.shrink();
}
return FutureBuilder<DocumentSnapshot>(
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<String, dynamic>;
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,
),
),
],
),
);
},
);
}
}

View File

@@ -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<String, dynamic>;
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,
),
),
],
),
),
);
}