últimas mudanças

This commit is contained in:
2026-06-11 23:59:58 +01:00
parent 3533d3436b
commit 34d7ae8afc
4 changed files with 248 additions and 206 deletions

View File

@@ -93,6 +93,20 @@ class _LoginPageState extends State<LoginPage> {
return; return;
} }
// Se role não existe na Firestore (null), permitir login e criar documento
if (actualRole == null) {
print(
'DEBUG: Role não encontrado na Firestore, criando documento com role selecionado: $selectedRole',
);
try {
await AuthService.createUserRole(uid, selectedRole);
print('DEBUG: Role criado com sucesso');
} catch (e) {
print('DEBUG: Erro ao criar role: $e');
// Continuar mesmo se falhar, pois o usuário já está autenticado
}
}
// Validar se o role selecionado corresponde ao role real // Validar se o role selecionado corresponde ao role real
if (actualRole != null && selectedRole != actualRole) { if (actualRole != null && selectedRole != actualRole) {
// Fazer logout imediato antes de mostrar erro // Fazer logout imediato antes de mostrar erro
@@ -118,6 +132,9 @@ class _LoginPageState extends State<LoginPage> {
return; return;
} }
// Usar selectedRole se actualRole for null (caso acabamos de criar)
final finalRole = actualRole ?? selectedRole;
// Save session based on remember me preference // Save session based on remember me preference
await SessionService.saveSession( await SessionService.saveSession(
rememberMe: _rememberMe, rememberMe: _rememberMe,
@@ -137,7 +154,7 @@ class _LoginPageState extends State<LoginPage> {
); );
// Redirecionar baseado no role real // Redirecionar baseado no role real
if (actualRole == 'teacher') { if (finalRole == 'teacher') {
context.go('/teacher-dashboard'); context.go('/teacher-dashboard');
} else { } else {
context.go('/student-dashboard'); context.go('/student-dashboard');
@@ -523,7 +540,9 @@ class _LoginPageState extends State<LoginPage> {
// Signup link // Signup link
GestureDetector( GestureDetector(
onTap: () { onTap: () {
context.go('/signup'); context.go(
'/signup?role=${widget.selectedRole}',
);
}, },
child: Text( child: Text(
'Não tem conta? Criar aqui', 'Não tem conta? Criar aqui',

View File

@@ -41,12 +41,14 @@ class _SignupPageState extends State<SignupPage> {
Future<void> _loadAvailableClasses() async { Future<void> _loadAvailableClasses() async {
setState(() => _isLoadingClasses = true); setState(() => _isLoadingClasses = true);
try { try {
print('DEBUG: Loading school_classes from Firestore');
final snapshot = await FirebaseFirestore.instance final snapshot = await FirebaseFirestore.instance
.collection('school_classes') .collection('school_classes')
.where('active', isEqualTo: true) .where('active', isEqualTo: true)
.orderBy('year') .orderBy('year')
.orderBy('section') .orderBy('section')
.get(); .get();
print('DEBUG: Loaded ${snapshot.docs.length} school classes');
setState(() { setState(() {
_availableClasses = snapshot.docs.map((doc) { _availableClasses = snapshot.docs.map((doc) {
final data = doc.data(); final data = doc.data();
@@ -55,6 +57,7 @@ class _SignupPageState extends State<SignupPage> {
_isLoadingClasses = false; _isLoadingClasses = false;
}); });
} catch (e) { } catch (e) {
print('DEBUG: Error loading school_classes: $e');
setState(() => _isLoadingClasses = false); setState(() => _isLoadingClasses = false);
} }
} }

View File

@@ -1,9 +1,11 @@
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter/services.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../../../../core/services/auth_service.dart'; import '../../../../core/services/auth_service.dart';
import '../../../../core/theme/app_colors.dart';
import '../../../../core/theme/app_theme_extension.dart'; import '../../../../core/theme/app_theme_extension.dart';
/// Página para visualizar os alunos de uma turma específica /// Página para visualizar os alunos de uma turma específica
@@ -202,9 +204,7 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
controller: textController, controller: textController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Nome da Turma', labelText: 'Nome da Turma',
border: OutlineInputBorder( border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
borderRadius: BorderRadius.circular(12),
),
prefixIcon: const Icon(Icons.school), prefixIcon: const Icon(Icons.school),
), ),
autofocus: true, autofocus: true,
@@ -254,10 +254,14 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error.withValues(alpha: 0.1), color: Theme.of(
context,
).colorScheme.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.error.withValues(alpha: 0.3), color: Theme.of(
context,
).colorScheme.error.withValues(alpha: 0.3),
), ),
), ),
child: Row( child: Row(
@@ -420,12 +424,7 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
Widget _buildAppBar(ColorScheme cs) { Widget _buildAppBar(ColorScheme cs) {
return Container( return Container(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 16, right: 16, top: 52, bottom: 16),
left: 16,
right: 16,
top: 52,
bottom: 16,
),
child: Column( child: Column(
children: [ children: [
// Top Row with Back and Actions // Top Row with Back and Actions
@@ -459,11 +458,20 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
// Delete Button // Delete Button
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.2), color:
(Theme.of(context).brightness == Brightness.dark
? DarkBrandColors.primaryOrange
: AppColors.primaryOrange)
.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: IconButton( child: IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.white), icon: Icon(
Icons.delete_outline,
color: Theme.of(context).brightness == Brightness.dark
? DarkBrandColors.primaryOrange
: AppColors.primaryOrange,
),
onPressed: _showDeleteClassDialog, onPressed: _showDeleteClassDialog,
tooltip: 'Eliminar turma', tooltip: 'Eliminar turma',
), ),
@@ -587,79 +595,85 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
children: [ children: [
const SizedBox(height: 40), const SizedBox(height: 40),
Container( Container(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.surface, color: cs.surface,
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: cs.shadow.withValues(alpha: 0.08), color: cs.shadow.withValues(alpha: 0.08),
blurRadius: 20, blurRadius: 20,
offset: const Offset(0, 8), offset: const Offset(0, 8),
),
],
),
child: Column(
children: [
Icon(
Icons.people_outline,
size: 80,
color: cs.primary.withValues(alpha: 0.5),
),
const SizedBox(height: 24),
Text(
'Nenhum aluno entrou nesta turma ainda.',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'Partilha o código da turma para os alunos se juntarem.',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: cs.primary.withValues(alpha: 0.3),
), ),
), ],
child: Row( ),
mainAxisSize: MainAxisSize.min, child: Column(
children: [ children: [
Icon(Icons.copy, color: cs.primary, size: 18), Icon(
const SizedBox(width: 8), Icons.people_outline,
Text( size: 80,
'Código: $_classCode', color: cs.primary.withValues(alpha: 0.5),
style: TextStyle( ),
color: cs.primary, const SizedBox(height: 24),
fontSize: 16, Text(
fontWeight: FontWeight.bold, 'Nenhum aluno entrou nesta turma ainda.',
style: TextStyle(
color: cs.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'Partilha o código da turma para os alunos se juntarem.',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
GestureDetector(
onTap: _copyCodeToClipboard,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
decoration: BoxDecoration(
color: cs.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: cs.primary.withValues(alpha: 0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.copy, color: cs.primary, size: 18),
const SizedBox(width: 8),
Text(
'Código: $_classCode',
style: TextStyle(
color: cs.primary,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
), ),
), ),
], ),
), ],
), ),
], )
), .animate()
).animate().fadeIn( .fadeIn(
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
curve: Curves.easeOut, curve: Curves.easeOut,
).then(delay: const Duration(milliseconds: 100)), )
.then(delay: const Duration(milliseconds: 100)),
], ],
), ),
); );
@@ -679,11 +693,7 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
margin: const EdgeInsets.only(bottom: 16, left: 8), margin: const EdgeInsets.only(bottom: 16, left: 8),
child: Row( child: Row(
children: [ children: [
Icon( Icon(Icons.people, color: cs.onSurface, size: 20),
Icons.people,
color: cs.onSurface,
size: 20,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'Alunos Matriculados', 'Alunos Matriculados',
@@ -746,118 +756,144 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
int index, int index,
) { ) {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 12), margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: cs.surface, color: cs.surface,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: cs.shadow.withValues(alpha: 0.06), color: cs.shadow.withValues(alpha: 0.06),
blurRadius: 12, blurRadius: 12,
offset: const Offset(0, 4), offset: const Offset(0, 4),
),
],
), ),
], child: Material(
), color: Colors.transparent,
child: Material( borderRadius: BorderRadius.circular(16),
color: Colors.transparent, child: InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: InkWell( onTap: null,
borderRadius: BorderRadius.circular(16), child: Padding(
onTap: null, padding: const EdgeInsets.all(16),
child: Padding( child: Row(
padding: const EdgeInsets.all(16), children: [
child: Row( // Avatar
children: [ Container(
// Avatar width: 52,
Container( height: 52,
width: 52, decoration: BoxDecoration(
height: 52, gradient: LinearGradient(
decoration: BoxDecoration( begin: Alignment.topLeft,
gradient: LinearGradient( end: Alignment.bottomRight,
begin: Alignment.topLeft, colors: [
end: Alignment.bottomRight, cs.primary.withValues(alpha: 0.8),
colors: [ cs.primary.withValues(alpha: 0.4),
cs.primary.withValues(alpha: 0.8), ],
cs.primary.withValues(alpha: 0.4),
],
),
borderRadius: BorderRadius.circular(14),
),
child: Center(
child: Text(
studentName.isNotEmpty
? studentName[0].toUpperCase()
: '?',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
// Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
studentName,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
), ),
maxLines: 1, borderRadius: BorderRadius.circular(14),
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(height: 4), child: Center(
Text( child: Text(
joinedAt != null studentName.isNotEmpty
? 'Entrou em ${_formatDate(joinedAt.toDate())}' ? studentName[0].toUpperCase()
: 'Data desconhecida', : '?',
style: TextStyle( style: const TextStyle(
color: cs.onSurfaceVariant, color: Colors.white,
fontSize: 13, fontSize: 22,
fontWeight: FontWeight.bold,
),
), ),
), ),
],
),
),
// Delete Button
Container(
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: Icon(Icons.delete_outline, color: cs.error, size: 20),
onPressed: () => _showRemoveStudentDialog(
context,
enrollmentId,
studentName,
), ),
tooltip: 'Remover aluno', const SizedBox(width: 16),
padding: const EdgeInsets.all(10), // Info
constraints: const BoxConstraints(), Expanded(
), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
studentName,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
joinedAt != null
? 'Entrou em ${_formatDate(joinedAt.toDate())}'
: 'Data desconhecida',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
// Delete Button
Container(
decoration: BoxDecoration(
color: cs.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(10),
),
child: IconButton(
icon: Icon(
Icons.delete_outline,
color: cs.error,
size: 20,
),
onPressed: () => _showRemoveStudentDialog(
context,
enrollmentId,
studentName,
),
tooltip: 'Remover aluno',
padding: const EdgeInsets.all(10),
constraints: const BoxConstraints(),
),
),
],
), ),
], ),
), ),
), ),
), )
), .animate()
).animate().fadeIn( .fadeIn(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
curve: Curves.easeOut, curve: Curves.easeOut,
).then(delay: Duration(milliseconds: index * 50)); )
.then(delay: Duration(milliseconds: index * 50));
} }
String _formatDate(DateTime date) { String _formatDate(DateTime date) {
return DateFormat('dd/MM/yyyy').format(date); return DateFormat('dd/MM/yyyy').format(date);
} }
void _copyCodeToClipboard() {
Clipboard.setData(ClipboardData(text: _classCode ?? ''));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.check_circle, color: Colors.white),
const SizedBox(width: 12),
const Text('Código copiado!'),
],
),
backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
duration: const Duration(seconds: 2),
),
);
}
Future<void> _showRemoveStudentDialog( Future<void> _showRemoveStudentDialog(
BuildContext context, BuildContext context,
String enrollmentId, String enrollmentId,
@@ -869,7 +905,10 @@ class _ClassStudentsPageState extends State<ClassStudentsPage> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row( title: Row(
children: [ children: [
Icon(Icons.person_remove, color: Theme.of(context).colorScheme.error), Icon(
Icons.person_remove,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text('Remover Aluno'), const Text('Remover Aluno'),
], ],

View File

@@ -56,11 +56,6 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
return; return;
} }
// Ler schoolClassId autorizado do aluno (definido no registo)
final studentSchoolClassId = await AuthService.getStudentSchoolClassId(
currentUser.uid,
);
// Procurar disciplina pelo código // Procurar disciplina pelo código
final classQuery = await FirebaseFirestore.instance final classQuery = await FirebaseFirestore.instance
.collection('classes') .collection('classes')
@@ -76,20 +71,6 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
final classDoc = classQuery.docs.first; final classDoc = classQuery.docs.first;
final classId = classDoc.id; 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 // Verificar se já está inscrito nesta disciplina
final existingEnrollment = await FirebaseFirestore.instance final existingEnrollment = await FirebaseFirestore.instance