Files
LearnIT/lib/features/classes/presentation/pages/join_class_page.dart

644 lines
25 KiB
Dart

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/services/auth_service.dart';
/// Página para o aluno entrar numa disciplina usando o código
class JoinClassPage extends ConsumerStatefulWidget {
const JoinClassPage({super.key});
@override
ConsumerState<JoinClassPage> createState() => _JoinClassPageState();
}
class _JoinClassPageState extends ConsumerState<JoinClassPage> {
final _codeController = TextEditingController();
final _nameController = TextEditingController();
bool _isLoading = false;
@override
void dispose() {
_codeController.dispose();
_nameController.dispose();
super.dispose();
}
Future<void> _joinClass() async {
final code = _codeController.text.trim().toUpperCase();
final customName = _nameController.text.trim();
if (code.isEmpty) {
_showError('Insere o código da disciplina');
return;
}
if (customName.isEmpty) {
_showError('Insere o nome da disciplina');
return;
}
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 disciplinas por código.');
return;
}
// Ler schoolClassId autorizado do aluno (definido no registo)
final studentSchoolClassId = await AuthService.getStudentSchoolClassId(
currentUser.uid,
);
// Procurar disciplina pelo código
final classQuery = await FirebaseFirestore.instance
.collection('classes')
.where('code', isEqualTo: code)
.limit(1)
.get();
if (classQuery.docs.isEmpty) {
setState(() => _isLoading = false);
_showError('Código de disciplina inválido');
return;
}
final classDoc = classQuery.docs.first;
final classId = classDoc.id;
final classSchoolClassId = classDoc.data()['schoolClassId'] as String?;
// Verificar se o aluno está autorizado a entrar nesta disciplina
// O schoolClassId do aluno deve corresponder ao schoolClassId da disciplina
if (studentSchoolClassId == null ||
classSchoolClassId == null ||
studentSchoolClassId != classSchoolClassId) {
setState(() => _isLoading = false);
_showError(
'Não tens permissão para entrar nesta disciplina.\n'
'O teu professor ainda não te adicionou a esta disciplina.',
);
return;
}
// Verificar se já está inscrito nesta disciplina
final existingEnrollment = await FirebaseFirestore.instance
.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 disciplina');
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',
'customClassName': customName,
'joinedAt': FieldValue.serverTimestamp(),
});
setState(() => _isLoading = false);
// Mostrar sucesso
if (mounted) {
final colorScheme = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 8),
const Text('Entraste na disciplina com sucesso!'),
],
),
backgroundColor: colorScheme.primary,
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
);
// Voltar para a home
Navigator.of(context).pop();
}
} catch (e) {
setState(() => _isLoading = false);
_showError('Erro ao entrar na disciplina: $e');
}
}
void _showError(String message) {
final colorScheme = Theme.of(context).colorScheme;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(message)),
],
),
backgroundColor: colorScheme.error,
duration: const Duration(seconds: 3),
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final isDark = theme.brightness == Brightness.dark;
return Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
colorScheme.surfaceContainerHighest,
colorScheme.surface,
colorScheme.surfaceContainerLow,
]
: const [
Color(0xFFD4E8E8),
Color(0xFFE8D4C0),
Color(0xFFD8E0E8),
],
),
),
child: SafeArea(
child: Column(
children: [
// Custom AppBar
Container(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
IconButton(
icon: Icon(
Icons.arrow_back,
color: colorScheme.onSurface,
),
onPressed: () => Navigator.of(context).pop(),
),
Expanded(
child: Text(
'Adicionar uma Disciplina',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
// Body content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 20),
// Header illustration card
Center(
child: Card(
elevation: isDark ? 4 : 8,
shadowColor: isDark
? Colors.black.withOpacity(0.4)
: Colors.black.withOpacity(0.1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: isDark
? [
colorScheme.primary.withOpacity(0.1),
colorScheme.secondary.withOpacity(0.05),
]
: [
colorScheme.primary.withOpacity(0.05),
colorScheme.secondary.withOpacity(0.02),
],
),
),
child: Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: colorScheme.primary.withOpacity(
0.2,
),
width: 2,
),
),
child: Icon(
Icons.group_add,
color: colorScheme.primary,
size: 40,
),
),
const SizedBox(height: 24),
Text(
'Insere o código da disciplina',
style: theme.textTheme.headlineSmall
?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'O professor partilhou contigo um código de 6 caracteres.',
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
const SizedBox(height: 32),
// Instructions card
Card(
elevation: isDark ? 2 : 4,
shadowColor: isDark
? Colors.black.withOpacity(0.3)
: Colors.black.withOpacity(0.08),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: colorScheme.primary,
size: 20,
),
const SizedBox(width: 8),
Text(
'Como funciona',
style: theme.textTheme.titleMedium
?.copyWith(
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: 12),
_buildInstructionItem(
context,
'1.',
'Pedir ao professor o código da disciplina',
colorScheme,
),
const SizedBox(height: 8),
_buildInstructionItem(
context,
'2.',
'Inserir o código no campo abaixo',
colorScheme,
),
const SizedBox(height: 8),
_buildInstructionItem(
context,
'3.',
'Escrever o nome da disciplina',
colorScheme,
),
const SizedBox(height: 8),
_buildInstructionItem(
context,
'4.',
'Clicar em "Adicionar uma Disciplina" para confirmar',
colorScheme,
),
],
),
),
),
const SizedBox(height: 32),
// Campo de código
Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _codeController,
textCapitalization: TextCapitalization.characters,
maxLength: 6,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 8,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: 'XXXXXX',
hintStyle: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 8,
color: colorScheme.onSurfaceVariant.withOpacity(
0.5,
),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(24),
counterText: '',
prefixIcon: Icon(
Icons.vpn_key,
color: colorScheme.primary,
size: 24,
),
prefixIconConstraints: const BoxConstraints(
minWidth: 48,
minHeight: 48,
),
),
),
),
const SizedBox(height: 24),
// Campo de nome da disciplina
Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: colorScheme.outline.withOpacity(0.3),
width: 1,
),
boxShadow: [
BoxShadow(
color: isDark
? Colors.black.withOpacity(0.2)
: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _nameController,
style: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurface,
),
decoration: InputDecoration(
hintText: 'Nome da disciplina',
hintStyle: theme.textTheme.bodyLarge?.copyWith(
color: colorScheme.onSurfaceVariant.withOpacity(
0.5,
),
),
border: InputBorder.none,
contentPadding: const EdgeInsets.all(20),
prefixIcon: Icon(
Icons.edit,
color: colorScheme.primary,
size: 24,
),
prefixIconConstraints: const BoxConstraints(
minWidth: 48,
minHeight: 48,
),
),
),
),
const SizedBox(height: 32),
// Botão de entrar
SizedBox(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isLoading ? null : _joinClass,
style: ElevatedButton.styleFrom(
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 2,
shadowColor: colorScheme.primary.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
disabledBackgroundColor: colorScheme.primary
.withOpacity(0.5),
),
child: _isLoading
? SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: colorScheme.onPrimary,
strokeWidth: 2,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.login,
size: 20,
color: colorScheme.onPrimary,
),
const SizedBox(width: 8),
Text(
'Adicionar uma Disciplina',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: colorScheme.onPrimary,
),
),
],
),
),
),
const SizedBox(height: 16),
// Help text
Center(
child: TextButton.icon(
onPressed: () {
_showHelpDialog(context, colorScheme);
},
icon: Icon(
Icons.help_outline,
size: 16,
color: colorScheme.primary,
),
label: Text(
'Precisas de ajuda?',
style: TextStyle(
color: colorScheme.primary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildInstructionItem(
BuildContext context,
String number,
String text,
ColorScheme colorScheme,
) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: colorScheme.primary,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
number,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
text,
style: theme.textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
);
}
void _showHelpDialog(BuildContext context, ColorScheme colorScheme) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Theme.of(context).cardColor,
title: Row(
children: [
Icon(Icons.help_outline, color: colorScheme.primary),
const SizedBox(width: 8),
Expanded(
child: Text(
'Ajuda - Código da Disciplina',
style: TextStyle(color: colorScheme.onSurface),
),
),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'O código da disciplina é um código único de 6 caracteres que o teu professor cria para cada disciplina.',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
const SizedBox(height: 12),
Text(
'Se não tens o código, contacta o teu professor.',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'Entendido',
style: TextStyle(color: colorScheme.primary),
),
),
],
),
);
}
}