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

@@ -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<JoinClassPage> createState() => _JoinClassPageState();
}
class _JoinClassPageState extends State<JoinClassPage> {
final _codeController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
Future<void> _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,
),
),
),
),
],
),
),
);
}
}