correção de bugs, creação propria para turmas, e preparação para criar quizzes
This commit is contained in:
@@ -58,14 +58,16 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: login,
|
||||
name: 'login',
|
||||
builder: (context, state) => const LoginPage(),
|
||||
builder: (context, state) =>
|
||||
LoginPage(selectedRole: state.uri.queryParameters['role']),
|
||||
),
|
||||
|
||||
// Signup
|
||||
GoRoute(
|
||||
path: signup,
|
||||
name: 'signup',
|
||||
builder: (context, state) => const SignupPage(),
|
||||
builder: (context, state) =>
|
||||
SignupPage(selectedRole: state.uri.queryParameters['role']),
|
||||
),
|
||||
|
||||
// Student Dashboard
|
||||
|
||||
@@ -13,13 +13,29 @@ class AuthService {
|
||||
}
|
||||
|
||||
/// Criar documento do usuário na Firestore após signup
|
||||
static Future<void> createUserRole(String uid, String role) async {
|
||||
static Future<void> createUserRole(
|
||||
String uid,
|
||||
String role, {
|
||||
String? classId,
|
||||
String? schoolClassId,
|
||||
String? displayName,
|
||||
}) async {
|
||||
try {
|
||||
print('DEBUG: Criando documento users/$uid com role: $role');
|
||||
await _firestore.collection('users').doc(uid).set({
|
||||
final Map<String, dynamic> data = {
|
||||
'role': role,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
};
|
||||
if (classId != null && classId.isNotEmpty) {
|
||||
data['classId'] = classId;
|
||||
}
|
||||
if (schoolClassId != null && schoolClassId.isNotEmpty) {
|
||||
data['schoolClassId'] = schoolClassId;
|
||||
}
|
||||
if (displayName != null && displayName.isNotEmpty) {
|
||||
data['displayName'] = displayName;
|
||||
}
|
||||
await _firestore.collection('users').doc(uid).set(data);
|
||||
print('DEBUG: Documento criado com sucesso');
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro ao criar documento: $e');
|
||||
@@ -27,6 +43,53 @@ class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Criar inscrição do aluno na turma escolhida
|
||||
static Future<void> createEnrollment({
|
||||
required String studentId,
|
||||
required String classId,
|
||||
required String studentName,
|
||||
}) async {
|
||||
try {
|
||||
print(
|
||||
'DEBUG: Criando enrollment para student=$studentId, class=$classId',
|
||||
);
|
||||
final existing = await _firestore
|
||||
.collection('enrollments')
|
||||
.where('studentId', isEqualTo: studentId)
|
||||
.where('classId', isEqualTo: classId)
|
||||
.limit(1)
|
||||
.get();
|
||||
if (existing.docs.isNotEmpty) {
|
||||
print('DEBUG: Enrollment já existe, ignorando');
|
||||
return;
|
||||
}
|
||||
await _firestore.collection('enrollments').add({
|
||||
'classId': classId,
|
||||
'studentId': studentId,
|
||||
'studentName': studentName,
|
||||
'joinedAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
print('DEBUG: Enrollment criado com sucesso');
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro ao criar enrollment: $e');
|
||||
throw Exception('Erro ao associar aluno à turma');
|
||||
}
|
||||
}
|
||||
|
||||
/// Ler classId do aluno na Firestore
|
||||
static Future<String?> getStudentClassId(String uid) async {
|
||||
try {
|
||||
final doc = await _firestore.collection('users').doc(uid).get();
|
||||
if (doc.exists) {
|
||||
return doc.data()?['classId'] as String?;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro ao ler classId: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ler role do usuário na Firestore
|
||||
static Future<String?> getUserRole(String uid) async {
|
||||
try {
|
||||
@@ -56,6 +119,8 @@ class AuthService {
|
||||
required String password,
|
||||
String? displayName,
|
||||
String? role,
|
||||
String? classId,
|
||||
String? schoolClassId,
|
||||
}) async {
|
||||
try {
|
||||
print('DEBUG: Tentando criar conta para email: $email');
|
||||
@@ -76,9 +141,15 @@ class AuthService {
|
||||
print('DEBUG: Display name atualizado para: $displayName');
|
||||
}
|
||||
|
||||
// Criar documento na Firestore com role
|
||||
// Criar documento na Firestore com role (e classId se aluno)
|
||||
if (role != null && result.user != null) {
|
||||
await createUserRole(result.user!.uid, role);
|
||||
await createUserRole(
|
||||
result.user!.uid,
|
||||
role,
|
||||
classId: classId,
|
||||
schoolClassId: schoolClassId,
|
||||
displayName: displayName,
|
||||
);
|
||||
}
|
||||
|
||||
// Verificar se o email foi verificado
|
||||
|
||||
@@ -81,12 +81,22 @@ class _LoginPageState extends State<LoginPage> {
|
||||
final actualRole = await AuthService.getUserRole(uid);
|
||||
print('DEBUG: Role real do usuário na Firestore: $actualRole');
|
||||
|
||||
// Validar se o role selecionado corresponde ao role real
|
||||
// Se não há role selecionado, redirecionar para role-selection
|
||||
final selectedRole = widget.selectedRole;
|
||||
if (selectedRole != null &&
|
||||
actualRole != null &&
|
||||
selectedRole != actualRole) {
|
||||
// Role não corresponde - mostrar erro
|
||||
if (selectedRole == null) {
|
||||
await AuthService.signOut();
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
context.go('/role-selection');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Validar se o role selecionado corresponde ao role real
|
||||
if (actualRole != null && selectedRole != actualRole) {
|
||||
// Fazer logout imediato antes de mostrar erro
|
||||
await AuthService.signOut();
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
@@ -171,8 +181,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
// Fazer logout para limpar a sessão
|
||||
AuthService.signOut();
|
||||
context.go('/role-selection');
|
||||
},
|
||||
child: Text(
|
||||
'Voltar',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -22,12 +23,39 @@ class _SignupPageState extends State<SignupPage> {
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
late String _selectedRole;
|
||||
String? _selectedSchoolClassId;
|
||||
List<Map<String, String>> _availableClasses = [];
|
||||
bool _isLoadingClasses = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Usar role passado da tela anterior ou default 'student'
|
||||
_selectedRole = widget.selectedRole ?? 'student';
|
||||
if (_selectedRole == 'student') {
|
||||
_loadAvailableClasses();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadAvailableClasses() async {
|
||||
setState(() => _isLoadingClasses = true);
|
||||
try {
|
||||
final snapshot = await FirebaseFirestore.instance
|
||||
.collection('school_classes')
|
||||
.where('active', isEqualTo: true)
|
||||
.orderBy('year')
|
||||
.orderBy('section')
|
||||
.get();
|
||||
setState(() {
|
||||
_availableClasses = snapshot.docs.map((doc) {
|
||||
final data = doc.data();
|
||||
return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)};
|
||||
}).toList();
|
||||
_isLoadingClasses = false;
|
||||
});
|
||||
} catch (e) {
|
||||
setState(() => _isLoadingClasses = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -38,6 +66,99 @@ class _SignupPageState extends State<SignupPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildClassSelector(BuildContext context) {
|
||||
if (_isLoadingClasses) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'A carregar turmas...',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_availableClasses.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
'Nenhuma turma disponível. Contacta o teu professor.',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return DropdownButtonFormField<String>(
|
||||
value: _selectedSchoolClassId,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Turma',
|
||||
labelStyle: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: BorderSide(color: Theme.of(context).colorScheme.error),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).brightness == Brightness.dark
|
||||
? Theme.of(context).colorScheme.surfaceContainerHighest
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
dropdownColor: Theme.of(context).colorScheme.surface,
|
||||
hint: Text(
|
||||
'Seleciona a tua turma',
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
items: _availableClasses
|
||||
.map(
|
||||
(c) => DropdownMenuItem<String>(
|
||||
value: c['id'],
|
||||
child: Text(c['name']!),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) => setState(() => _selectedSchoolClassId = value),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Seleciona a tua turma';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleSignup() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
@@ -60,6 +181,9 @@ class _SignupPageState extends State<SignupPage> {
|
||||
password: password,
|
||||
displayName: name,
|
||||
role: _selectedRole,
|
||||
schoolClassId: _selectedRole == 'student'
|
||||
? _selectedSchoolClassId
|
||||
: null,
|
||||
);
|
||||
|
||||
print('DEBUG: Signup Firebase bem-sucedido, navegando para dashboard');
|
||||
@@ -295,6 +419,12 @@ class _SignupPageState extends State<SignupPage> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Seletor de turma (apenas para alunos)
|
||||
if (_selectedRole == 'student') ...[
|
||||
_buildClassSelector(context),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
|
||||
/// Página para visualizar os alunos de uma turma específica
|
||||
class ClassStudentsPage extends StatelessWidget {
|
||||
class ClassStudentsPage extends StatefulWidget {
|
||||
final String classId;
|
||||
final String className;
|
||||
|
||||
@@ -13,8 +14,110 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
required this.className,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ClassStudentsPage> createState() => _ClassStudentsPageState();
|
||||
}
|
||||
|
||||
class _ClassStudentsPageState extends State<ClassStudentsPage> {
|
||||
bool _isCheckingAccess = true;
|
||||
bool _accessGranted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_verifyOwnership();
|
||||
}
|
||||
|
||||
Future<void> _verifyOwnership() async {
|
||||
final currentUser = AuthService.currentUser;
|
||||
if (currentUser == null) {
|
||||
setState(() {
|
||||
_isCheckingAccess = false;
|
||||
_accessGranted = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final role = await AuthService.getUserRole(currentUser.uid);
|
||||
if (role != 'teacher') {
|
||||
setState(() {
|
||||
_isCheckingAccess = false;
|
||||
_accessGranted = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final classDoc = await FirebaseFirestore.instance
|
||||
.collection('classes')
|
||||
.doc(widget.classId)
|
||||
.get();
|
||||
|
||||
final teacherId = classDoc.data()?['teacherId'] as String?;
|
||||
setState(() {
|
||||
_isCheckingAccess = false;
|
||||
_accessGranted = classDoc.exists && teacherId == currentUser.uid;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_isCheckingAccess) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Color(0xFFF8F9FA),
|
||||
body: Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF82C9BD)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!_accessGranted) {
|
||||
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(
|
||||
'Acesso Negado',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
body: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.lock_outline,
|
||||
size: 64,
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'Sem permissão',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Só podes ver os alunos das tuas próprias turmas.',
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF8F9FA),
|
||||
appBar: AppBar(
|
||||
@@ -28,7 +131,7 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
className,
|
||||
widget.className,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
@@ -49,15 +152,13 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
body: StreamBuilder<QuerySnapshot>(
|
||||
stream: FirebaseFirestore.instance
|
||||
.collection('enrollments')
|
||||
.where('classId', isEqualTo: classId)
|
||||
.where('classId', isEqualTo: widget.classId)
|
||||
.orderBy('joinedAt', descending: true)
|
||||
.snapshots(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
child: CircularProgressIndicator(color: Color(0xFF82C9BD)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,18 +167,11 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -91,18 +185,11 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people_outline,
|
||||
size: 64,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
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,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -110,10 +197,7 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
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,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@@ -126,8 +210,10 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
itemCount: enrollments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final enrollment = enrollments[index].data() as Map<String, dynamic>;
|
||||
final studentName = enrollment['studentName'] as String? ?? 'Aluno sem nome';
|
||||
final enrollment =
|
||||
enrollments[index].data() as Map<String, dynamic>;
|
||||
final studentName =
|
||||
enrollment['studentName'] as String? ?? 'Aluno sem nome';
|
||||
final joinedAt = enrollment['joinedAt'] as Timestamp?;
|
||||
|
||||
return Container(
|
||||
@@ -174,10 +260,7 @@ class ClassStudentsPage extends StatelessWidget {
|
||||
joinedAt != null
|
||||
? 'Entrou em ${_formatDate(joinedAt.toDate())}'
|
||||
: 'Data desconhecida',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 13,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -31,6 +31,27 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
||||
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 turmas por código.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ler classId autorizado do aluno (definido no registo)
|
||||
final authorizedClassId = await AuthService.getStudentClassId(
|
||||
currentUser.uid,
|
||||
);
|
||||
|
||||
// Procurar turma pelo código
|
||||
final classQuery = await FirebaseFirestore.instance
|
||||
.collection('classes')
|
||||
@@ -46,11 +67,14 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
||||
|
||||
final classDoc = classQuery.docs.first;
|
||||
final classId = classDoc.id;
|
||||
final currentUser = AuthService.currentUser;
|
||||
|
||||
if (currentUser == null) {
|
||||
// Verificar se o aluno está autorizado a entrar nesta turma
|
||||
if (authorizedClassId == null || authorizedClassId != classId) {
|
||||
setState(() => _isLoading = false);
|
||||
_showError('Erro: Utilizador não autenticado');
|
||||
_showError(
|
||||
'Não tens permissão para entrar nesta turma.\n'
|
||||
'O teu professor ainda não te adicionou a esta disciplina.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +96,10 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
||||
await FirebaseFirestore.instance.collection('enrollments').add({
|
||||
'classId': classId,
|
||||
'studentId': currentUser.uid,
|
||||
'studentName': currentUser.displayName ?? currentUser.email?.split('@')[0] ?? 'Aluno',
|
||||
'studentName':
|
||||
currentUser.displayName ??
|
||||
currentUser.email?.split('@')[0] ??
|
||||
'Aluno',
|
||||
'joinedAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
@@ -163,10 +190,7 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
||||
Center(
|
||||
child: Text(
|
||||
'O professor partilhou contigo um código de 6 caracteres.',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 14,
|
||||
),
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
@@ -219,7 +243,9 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
disabledBackgroundColor: const Color(0xFF82C9BD).withOpacity(0.5),
|
||||
disabledBackgroundColor: const Color(
|
||||
0xFF82C9BD,
|
||||
).withOpacity(0.5),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../dashboard/presentation/widgets/teacher_classes_list_widget.dart';
|
||||
|
||||
/// Página dedicada para o professor ver todas as suas turmas
|
||||
/// Reutiliza o TeacherClassesListWidget existente
|
||||
class TeacherAllClassesPage extends StatelessWidget {
|
||||
const TeacherAllClassesPage({super.key});
|
||||
|
||||
@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(
|
||||
'As Minhas Turmas',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: const SingleChildScrollView(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
TeacherClassesListWidget(),
|
||||
SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,14 +19,23 @@ class _StudentDashboardPageState extends State<StudentDashboardPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserData();
|
||||
_checkRoleAndLoadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Reload user data when dependencies change (e.g., after navigation)
|
||||
_loadUserData();
|
||||
Future<void> _checkRoleAndLoadData() async {
|
||||
final user = AuthService.currentUser;
|
||||
if (user == null) {
|
||||
if (mounted) context.go('/role-selection');
|
||||
return;
|
||||
}
|
||||
final role = await AuthService.getUserRole(user.uid);
|
||||
if (role != 'student') {
|
||||
if (mounted) {
|
||||
context.go('/role-selection');
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _loadUserData();
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
|
||||
@@ -19,13 +19,23 @@ class _TeacherDashboardPageState extends State<TeacherDashboardPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserData();
|
||||
_checkRoleAndLoadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_loadUserData();
|
||||
Future<void> _checkRoleAndLoadData() async {
|
||||
final user = AuthService.currentUser;
|
||||
if (user == null) {
|
||||
if (mounted) context.go('/role-selection');
|
||||
return;
|
||||
}
|
||||
final role = await AuthService.getUserRole(user.uid);
|
||||
if (role != 'teacher') {
|
||||
if (mounted) {
|
||||
context.go('/role-selection');
|
||||
}
|
||||
return;
|
||||
}
|
||||
await _loadUserData();
|
||||
}
|
||||
|
||||
Future<void> _loadUserData() async {
|
||||
|
||||
@@ -288,6 +288,8 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
|
||||
label,
|
||||
style: TextStyle(color: color.withOpacity(0.8), fontSize: 10),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -34,18 +34,22 @@ class TeacherHeroWidget extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
'Visão Geral da Turma',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 24,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Acompanhe o progresso dos seus alunos',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 16,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -108,12 +112,16 @@ class TeacherHeroWidget extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Progresso Médio da Turma',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
const Flexible(
|
||||
child: Text(
|
||||
'Progresso Médio da Turma',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -283,8 +291,10 @@ class TeacherHeroWidget extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -53,9 +53,6 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
||||
// Secondary Actions Row
|
||||
Row(
|
||||
children: [
|
||||
// Manage Students Card
|
||||
Expanded(child: _buildManageStudentsCard(context)),
|
||||
const SizedBox(width: 16),
|
||||
// View Analytics Card
|
||||
Expanded(child: _buildViewAnalyticsCard(context)),
|
||||
],
|
||||
@@ -71,273 +68,214 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
||||
}
|
||||
|
||||
Widget _buildUploadContentCard(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 135, maxHeight: 160),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const TeacherMaterialsPage()),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.upload_file,
|
||||
color: Colors.white,
|
||||
size: 22,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'NOVO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Upload Conteúdo',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'PDFs, textos, imagens',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child:
|
||||
Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Theme.of(context).colorScheme.primary,
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 100));
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => const TeacherMaterialsPage(),
|
||||
),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.upload_file,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.secondary,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Text(
|
||||
'NOVO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
Text(
|
||||
'Upload Conteúdo',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'PDFs, textos, imagens',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 100)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCreateQuizCard(BuildContext context) {
|
||||
return Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => context.go('/teacher/quiz/create'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.quiz,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Criar Quiz',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Avaliações interativas',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child:
|
||||
Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.shadow.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Widget _buildManageStudentsCard(BuildContext context) {
|
||||
return Container(
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => context.go('/teacher/students'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.people,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => context.go('/teacher/quiz/create'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.secondary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.quiz,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Criar Quiz',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Avaliações interativas',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Gerir Alunos',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Acesso e permissões',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 300));
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 200)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildViewAnalyticsCard(BuildContext context) {
|
||||
@@ -420,183 +358,312 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
||||
}
|
||||
|
||||
Widget _buildCreateClassCard(BuildContext context) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(minHeight: 135, maxHeight: 160),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: _isCreatingClass
|
||||
? null
|
||||
: () => _showCreateClassDialog(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: _isCreatingClass
|
||||
? SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.school,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Criar Turma',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Gerar código de acesso',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child:
|
||||
Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.shadow.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 150));
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: _isCreatingClass
|
||||
? null
|
||||
: () => _showCreateClassDialog(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: _isCreatingClass
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.school,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Criar Turma',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Gerar código de acesso',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 150)),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreateClassDialog(BuildContext context) {
|
||||
final TextEditingController nameController = TextEditingController();
|
||||
String? selectedSchoolClassId;
|
||||
List<Map<String, String>> schoolClasses = [];
|
||||
bool isLoadingClasses = true;
|
||||
|
||||
// Carregar school_classes antes de mostrar o dialog
|
||||
FirebaseFirestore.instance
|
||||
.collection('school_classes')
|
||||
.where('active', isEqualTo: true)
|
||||
.orderBy('year')
|
||||
.orderBy('section')
|
||||
.get()
|
||||
.then((snapshot) {
|
||||
schoolClasses = snapshot.docs.map((doc) {
|
||||
final data = doc.data();
|
||||
return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)};
|
||||
}).toList();
|
||||
isLoadingClasses = false;
|
||||
})
|
||||
.catchError((_) {
|
||||
isLoadingClasses = false;
|
||||
});
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: Text(
|
||||
'Criar Nova Turma',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Digite o nome da turma:',
|
||||
return StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
// Atualizar estado do dialog quando as classes carregarem
|
||||
if (isLoadingClasses) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
if (dialogContext.mounted) setDialogState(() {});
|
||||
});
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
title: Text(
|
||||
'Criar Nova Disciplina',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Ex: Matemática 9º Ano',
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Nome da disciplina:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Ex: Matemática, Física...',
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Turma:',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
isLoadingClasses
|
||||
? Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'A carregar turmas...',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: DropdownButtonFormField<String>(
|
||||
value: selectedSchoolClassId,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
),
|
||||
hint: Text(
|
||||
'Seleciona a turma',
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
items: schoolClasses
|
||||
.map(
|
||||
(c) => DropdownMenuItem<String>(
|
||||
value: c['id'],
|
||||
child: Text(c['name']!),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) => setDialogState(
|
||||
() => selectedSchoolClassId = value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(
|
||||
'Cancelar',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: Text(
|
||||
'Cancelar',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final className = nameController.text.trim();
|
||||
if (className.isNotEmpty) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
_createClass(
|
||||
className,
|
||||
schoolClassId: selectedSchoolClassId,
|
||||
);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('Criar'),
|
||||
),
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final className = nameController.text.trim();
|
||||
if (className.isNotEmpty) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
_createClass(className);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: const Text('Criar'),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -613,7 +680,7 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createClass(String className) async {
|
||||
Future<void> _createClass(String className, {String? schoolClassId}) async {
|
||||
setState(() {
|
||||
_isCreatingClass = true;
|
||||
});
|
||||
@@ -627,12 +694,17 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
||||
final classCode = _generateClassCode();
|
||||
final firestore = FirebaseFirestore.instance;
|
||||
|
||||
await firestore.collection('classes').add({
|
||||
final Map<String, dynamic> classData = {
|
||||
'name': className,
|
||||
'teacherId': currentUser.uid,
|
||||
'code': classCode,
|
||||
'createdAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
};
|
||||
if (schoolClassId != null && schoolClassId.isNotEmpty) {
|
||||
classData['schoolClassId'] = schoolClassId;
|
||||
}
|
||||
|
||||
await firestore.collection('classes').add(classData);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
|
||||
@@ -32,10 +32,12 @@ class SettingsNotifier extends StateNotifier<SettingsState> {
|
||||
SettingsNotifier()
|
||||
: super(
|
||||
SettingsState(
|
||||
themeMode: ThemeService.getThemeMode(),
|
||||
themeMode: ThemeMode.light,
|
||||
isDarkModeAvailable: ThemeService.isDarkModeAvailable(),
|
||||
),
|
||||
);
|
||||
) {
|
||||
loadSettings();
|
||||
}
|
||||
|
||||
/// Set theme mode
|
||||
Future<void> setThemeMode(ThemeMode themeMode) async {
|
||||
@@ -60,10 +62,8 @@ class SettingsNotifier extends StateNotifier<SettingsState> {
|
||||
Future<void> resetSettings() async {
|
||||
state = state.copyWith(isLoading: true);
|
||||
await ThemeService.resetTheme();
|
||||
state = state.copyWith(
|
||||
themeMode: ThemeService.getThemeMode(),
|
||||
isLoading: false,
|
||||
);
|
||||
final themeMode = await ThemeService.getStoredThemeMode();
|
||||
state = state.copyWith(themeMode: themeMode, isLoading: false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user