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(
|
GoRoute(
|
||||||
path: login,
|
path: login,
|
||||||
name: 'login',
|
name: 'login',
|
||||||
builder: (context, state) => const LoginPage(),
|
builder: (context, state) =>
|
||||||
|
LoginPage(selectedRole: state.uri.queryParameters['role']),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Signup
|
// Signup
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: signup,
|
path: signup,
|
||||||
name: 'signup',
|
name: 'signup',
|
||||||
builder: (context, state) => const SignupPage(),
|
builder: (context, state) =>
|
||||||
|
SignupPage(selectedRole: state.uri.queryParameters['role']),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Student Dashboard
|
// Student Dashboard
|
||||||
|
|||||||
@@ -13,13 +13,29 @@ class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Criar documento do usuário na Firestore após signup
|
/// 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 {
|
try {
|
||||||
print('DEBUG: Criando documento users/$uid com role: $role');
|
print('DEBUG: Criando documento users/$uid com role: $role');
|
||||||
await _firestore.collection('users').doc(uid).set({
|
final Map<String, dynamic> data = {
|
||||||
'role': role,
|
'role': role,
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
'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');
|
print('DEBUG: Documento criado com sucesso');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('DEBUG: Erro ao criar documento: $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
|
/// Ler role do usuário na Firestore
|
||||||
static Future<String?> getUserRole(String uid) async {
|
static Future<String?> getUserRole(String uid) async {
|
||||||
try {
|
try {
|
||||||
@@ -56,6 +119,8 @@ class AuthService {
|
|||||||
required String password,
|
required String password,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
String? role,
|
String? role,
|
||||||
|
String? classId,
|
||||||
|
String? schoolClassId,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
print('DEBUG: Tentando criar conta para email: $email');
|
print('DEBUG: Tentando criar conta para email: $email');
|
||||||
@@ -76,9 +141,15 @@ class AuthService {
|
|||||||
print('DEBUG: Display name atualizado para: $displayName');
|
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) {
|
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
|
// Verificar se o email foi verificado
|
||||||
|
|||||||
@@ -81,12 +81,22 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
final actualRole = await AuthService.getUserRole(uid);
|
final actualRole = await AuthService.getUserRole(uid);
|
||||||
print('DEBUG: Role real do usuário na Firestore: $actualRole');
|
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;
|
final selectedRole = widget.selectedRole;
|
||||||
if (selectedRole != null &&
|
if (selectedRole == null) {
|
||||||
actualRole != null &&
|
await AuthService.signOut();
|
||||||
selectedRole != actualRole) {
|
if (mounted) {
|
||||||
// Role não corresponde - mostrar erro
|
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(() {
|
setState(() {
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
});
|
});
|
||||||
@@ -171,8 +181,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
// Fazer logout para limpar a sessão
|
context.go('/role-selection');
|
||||||
AuthService.signOut();
|
|
||||||
},
|
},
|
||||||
child: Text(
|
child: Text(
|
||||||
'Voltar',
|
'Voltar',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -22,12 +23,39 @@ class _SignupPageState extends State<SignupPage> {
|
|||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _obscurePassword = true;
|
bool _obscurePassword = true;
|
||||||
late String _selectedRole;
|
late String _selectedRole;
|
||||||
|
String? _selectedSchoolClassId;
|
||||||
|
List<Map<String, String>> _availableClasses = [];
|
||||||
|
bool _isLoadingClasses = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Usar role passado da tela anterior ou default 'student'
|
// Usar role passado da tela anterior ou default 'student'
|
||||||
_selectedRole = widget.selectedRole ?? '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
|
@override
|
||||||
@@ -38,6 +66,99 @@ class _SignupPageState extends State<SignupPage> {
|
|||||||
super.dispose();
|
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 {
|
Future<void> _handleSignup() async {
|
||||||
if (!_formKey.currentState!.validate()) return;
|
if (!_formKey.currentState!.validate()) return;
|
||||||
|
|
||||||
@@ -60,6 +181,9 @@ class _SignupPageState extends State<SignupPage> {
|
|||||||
password: password,
|
password: password,
|
||||||
displayName: name,
|
displayName: name,
|
||||||
role: _selectedRole,
|
role: _selectedRole,
|
||||||
|
schoolClassId: _selectedRole == 'student'
|
||||||
|
? _selectedSchoolClassId
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
print('DEBUG: Signup Firebase bem-sucedido, navegando para dashboard');
|
print('DEBUG: Signup Firebase bem-sucedido, navegando para dashboard');
|
||||||
@@ -295,6 +419,12 @@ class _SignupPageState extends State<SignupPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Seletor de turma (apenas para alunos)
|
||||||
|
if (_selectedRole == 'student') ...[
|
||||||
|
_buildClassSelector(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
// Email field
|
// Email field
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import '../../../../core/services/auth_service.dart';
|
||||||
|
|
||||||
/// Página para visualizar os alunos de uma turma específica
|
/// Página para visualizar os alunos de uma turma específica
|
||||||
class ClassStudentsPage extends StatelessWidget {
|
class ClassStudentsPage extends StatefulWidget {
|
||||||
final String classId;
|
final String classId;
|
||||||
final String className;
|
final String className;
|
||||||
|
|
||||||
@@ -13,8 +14,110 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
required this.className,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF8F9FA),
|
backgroundColor: const Color(0xFFF8F9FA),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -28,7 +131,7 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
className,
|
widget.className,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -49,15 +152,13 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
body: StreamBuilder<QuerySnapshot>(
|
body: StreamBuilder<QuerySnapshot>(
|
||||||
stream: FirebaseFirestore.instance
|
stream: FirebaseFirestore.instance
|
||||||
.collection('enrollments')
|
.collection('enrollments')
|
||||||
.where('classId', isEqualTo: classId)
|
.where('classId', isEqualTo: widget.classId)
|
||||||
.orderBy('joinedAt', descending: true)
|
.orderBy('joinedAt', descending: true)
|
||||||
.snapshots(),
|
.snapshots(),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: Color(0xFF82C9BD)),
|
||||||
color: Color(0xFF82C9BD),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,18 +167,11 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.error_outline, size: 48, color: Colors.grey[400]),
|
||||||
Icons.error_outline,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.grey[400],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Erro ao carregar alunos',
|
'Erro ao carregar alunos',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -91,18 +185,11 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.people_outline, size: 64, color: Colors.grey[300]),
|
||||||
Icons.people_outline,
|
|
||||||
size: 64,
|
|
||||||
color: Colors.grey[300],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'Nenhum aluno entrou nesta turma ainda.',
|
'Nenhum aluno entrou nesta turma ainda.',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey[600], fontSize: 16),
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 16,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -110,10 +197,7 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Partilha o código da turma para os alunos se juntarem.',
|
'Partilha o código da turma para os alunos se juntarem.',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey[500], fontSize: 13),
|
||||||
color: Colors.grey[500],
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -126,8 +210,10 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
itemCount: enrollments.length,
|
itemCount: enrollments.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final enrollment = enrollments[index].data() as Map<String, dynamic>;
|
final enrollment =
|
||||||
final studentName = enrollment['studentName'] as String? ?? 'Aluno sem nome';
|
enrollments[index].data() as Map<String, dynamic>;
|
||||||
|
final studentName =
|
||||||
|
enrollment['studentName'] as String? ?? 'Aluno sem nome';
|
||||||
final joinedAt = enrollment['joinedAt'] as Timestamp?;
|
final joinedAt = enrollment['joinedAt'] as Timestamp?;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -174,10 +260,7 @@ class ClassStudentsPage extends StatelessWidget {
|
|||||||
joinedAt != null
|
joinedAt != null
|
||||||
? 'Entrou em ${_formatDate(joinedAt.toDate())}'
|
? 'Entrou em ${_formatDate(joinedAt.toDate())}'
|
||||||
: 'Data desconhecida',
|
: 'Data desconhecida',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey[600], fontSize: 13),
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -31,6 +31,27 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
try {
|
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
|
// Procurar turma pelo código
|
||||||
final classQuery = await FirebaseFirestore.instance
|
final classQuery = await FirebaseFirestore.instance
|
||||||
.collection('classes')
|
.collection('classes')
|
||||||
@@ -46,11 +67,14 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
|||||||
|
|
||||||
final classDoc = classQuery.docs.first;
|
final classDoc = classQuery.docs.first;
|
||||||
final classId = classDoc.id;
|
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);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +96,10 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
|||||||
await FirebaseFirestore.instance.collection('enrollments').add({
|
await FirebaseFirestore.instance.collection('enrollments').add({
|
||||||
'classId': classId,
|
'classId': classId,
|
||||||
'studentId': currentUser.uid,
|
'studentId': currentUser.uid,
|
||||||
'studentName': currentUser.displayName ?? currentUser.email?.split('@')[0] ?? 'Aluno',
|
'studentName':
|
||||||
|
currentUser.displayName ??
|
||||||
|
currentUser.email?.split('@')[0] ??
|
||||||
|
'Aluno',
|
||||||
'joinedAt': FieldValue.serverTimestamp(),
|
'joinedAt': FieldValue.serverTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,10 +190,7 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
|||||||
Center(
|
Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'O professor partilhou contigo um código de 6 caracteres.',
|
'O professor partilhou contigo um código de 6 caracteres.',
|
||||||
style: TextStyle(
|
style: TextStyle(color: Colors.grey[600], fontSize: 14),
|
||||||
color: Colors.grey[600],
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -219,7 +243,9 @@ class _JoinClassPageState extends State<JoinClassPage> {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
disabledBackgroundColor: const Color(0xFF82C9BD).withOpacity(0.5),
|
disabledBackgroundColor: const Color(
|
||||||
|
0xFF82C9BD,
|
||||||
|
).withOpacity(0.5),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? 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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadUserData();
|
_checkRoleAndLoadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _checkRoleAndLoadData() async {
|
||||||
void didChangeDependencies() {
|
final user = AuthService.currentUser;
|
||||||
super.didChangeDependencies();
|
if (user == null) {
|
||||||
// Reload user data when dependencies change (e.g., after navigation)
|
if (mounted) context.go('/role-selection');
|
||||||
_loadUserData();
|
return;
|
||||||
|
}
|
||||||
|
final role = await AuthService.getUserRole(user.uid);
|
||||||
|
if (role != 'student') {
|
||||||
|
if (mounted) {
|
||||||
|
context.go('/role-selection');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _loadUserData();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadUserData() async {
|
Future<void> _loadUserData() async {
|
||||||
|
|||||||
@@ -19,13 +19,23 @@ class _TeacherDashboardPageState extends State<TeacherDashboardPage> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadUserData();
|
_checkRoleAndLoadData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Future<void> _checkRoleAndLoadData() async {
|
||||||
void didChangeDependencies() {
|
final user = AuthService.currentUser;
|
||||||
super.didChangeDependencies();
|
if (user == null) {
|
||||||
_loadUserData();
|
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 {
|
Future<void> _loadUserData() async {
|
||||||
|
|||||||
@@ -288,6 +288,8 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
|
|||||||
label,
|
label,
|
||||||
style: TextStyle(color: color.withOpacity(0.8), fontSize: 10),
|
style: TextStyle(color: color.withOpacity(0.8), fontSize: 10),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -34,18 +34,22 @@ class TeacherHeroWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Visão Geral da Turma',
|
'Visão Geral da Turma',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
fontSize: 24,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Acompanhe o progresso dos seus alunos',
|
'Acompanhe o progresso dos seus alunos',
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
fontSize: 16,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -108,12 +112,16 @@ class TeacherHeroWidget extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Flexible(
|
||||||
'Progresso Médio da Turma',
|
child: Text(
|
||||||
style: TextStyle(
|
'Progresso Médio da Turma',
|
||||||
color: Colors.white,
|
maxLines: 2,
|
||||||
fontSize: 18,
|
overflow: TextOverflow.ellipsis,
|
||||||
fontWeight: FontWeight.bold,
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
@@ -283,8 +291,10 @@ class TeacherHeroWidget extends StatelessWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12),
|
style: const TextStyle(color: Colors.white, fontSize: 11),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -53,9 +53,6 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
|||||||
// Secondary Actions Row
|
// Secondary Actions Row
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
// Manage Students Card
|
|
||||||
Expanded(child: _buildManageStudentsCard(context)),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
// View Analytics Card
|
// View Analytics Card
|
||||||
Expanded(child: _buildViewAnalyticsCard(context)),
|
Expanded(child: _buildViewAnalyticsCard(context)),
|
||||||
],
|
],
|
||||||
@@ -71,273 +68,214 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUploadContentCard(BuildContext context) {
|
Widget _buildUploadContentCard(BuildContext context) {
|
||||||
return Container(
|
return ClipRRect(
|
||||||
constraints: const BoxConstraints(minHeight: 135, maxHeight: 160),
|
borderRadius: BorderRadius.circular(16),
|
||||||
decoration: BoxDecoration(
|
child:
|
||||||
gradient: LinearGradient(
|
Container(
|
||||||
begin: Alignment.topLeft,
|
height: 150,
|
||||||
end: Alignment.bottomRight,
|
decoration: BoxDecoration(
|
||||||
colors: [
|
gradient: LinearGradient(
|
||||||
Theme.of(context).colorScheme.primary,
|
begin: Alignment.topLeft,
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
end: Alignment.bottomRight,
|
||||||
],
|
colors: [
|
||||||
),
|
Theme.of(context).colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(16),
|
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
||||||
boxShadow: [
|
],
|
||||||
BoxShadow(
|
),
|
||||||
color: Theme.of(context).colorScheme.primary.withOpacity(0.3),
|
borderRadius: BorderRadius.circular(16),
|
||||||
blurRadius: 15,
|
boxShadow: [
|
||||||
offset: const Offset(0, 8),
|
BoxShadow(
|
||||||
),
|
color: Theme.of(
|
||||||
],
|
context,
|
||||||
),
|
).colorScheme.primary.withOpacity(0.3),
|
||||||
child: Material(
|
blurRadius: 15,
|
||||||
color: Colors.transparent,
|
offset: const Offset(0, 8),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
child: Material(
|
||||||
),
|
color: Colors.transparent,
|
||||||
),
|
child: InkWell(
|
||||||
)
|
borderRadius: BorderRadius.circular(16),
|
||||||
.animate()
|
onTap: () => Navigator.push(
|
||||||
.scale(
|
context,
|
||||||
duration: const Duration(milliseconds: 600),
|
MaterialPageRoute(
|
||||||
curve: Curves.elasticOut,
|
builder: (_) => const TeacherMaterialsPage(),
|
||||||
)
|
),
|
||||||
.then(delay: const Duration(milliseconds: 100));
|
),
|
||||||
|
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) {
|
Widget _buildCreateQuizCard(BuildContext context) {
|
||||||
return Container(
|
return ClipRRect(
|
||||||
height: 150,
|
borderRadius: BorderRadius.circular(16),
|
||||||
decoration: BoxDecoration(
|
child:
|
||||||
color: Theme.of(context).colorScheme.surface,
|
Container(
|
||||||
borderRadius: BorderRadius.circular(16),
|
height: 150,
|
||||||
border: Border.all(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
width: 1,
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
border: Border.all(
|
||||||
boxShadow: [
|
color: Theme.of(
|
||||||
BoxShadow(
|
context,
|
||||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
).colorScheme.outline.withOpacity(0.2),
|
||||||
blurRadius: 10,
|
width: 1,
|
||||||
offset: const Offset(0, 4),
|
),
|
||||||
),
|
boxShadow: [
|
||||||
],
|
BoxShadow(
|
||||||
),
|
color: Theme.of(
|
||||||
child: Material(
|
context,
|
||||||
color: Colors.transparent,
|
).colorScheme.shadow.withOpacity(0.05),
|
||||||
child: InkWell(
|
blurRadius: 10,
|
||||||
borderRadius: BorderRadius.circular(16),
|
offset: const Offset(0, 4),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
child: Material(
|
||||||
),
|
color: Colors.transparent,
|
||||||
),
|
child: InkWell(
|
||||||
)
|
borderRadius: BorderRadius.circular(16),
|
||||||
.animate()
|
onTap: () => context.go('/teacher/quiz/create'),
|
||||||
.scale(
|
child: Padding(
|
||||||
duration: const Duration(milliseconds: 600),
|
padding: const EdgeInsets.all(12),
|
||||||
curve: Curves.elasticOut,
|
child: Column(
|
||||||
)
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
.then(delay: const Duration(milliseconds: 200));
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
}
|
children: [
|
||||||
|
Container(
|
||||||
Widget _buildManageStudentsCard(BuildContext context) {
|
padding: const EdgeInsets.all(8),
|
||||||
return Container(
|
decoration: BoxDecoration(
|
||||||
height: 120,
|
color: Theme.of(
|
||||||
decoration: BoxDecoration(
|
context,
|
||||||
color: Theme.of(context).colorScheme.surface,
|
).colorScheme.secondary.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(10),
|
||||||
border: Border.all(
|
),
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
child: Icon(
|
||||||
width: 1,
|
Icons.quiz,
|
||||||
),
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
boxShadow: [
|
size: 20,
|
||||||
BoxShadow(
|
),
|
||||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
),
|
||||||
blurRadius: 10,
|
Column(
|
||||||
offset: const Offset(0, 4),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
],
|
children: [
|
||||||
),
|
Text(
|
||||||
child: Material(
|
'Criar Quiz',
|
||||||
color: Colors.transparent,
|
style: TextStyle(
|
||||||
child: InkWell(
|
color: Theme.of(
|
||||||
borderRadius: BorderRadius.circular(16),
|
context,
|
||||||
onTap: () => context.go('/teacher/students'),
|
).colorScheme.onSurface,
|
||||||
child: Padding(
|
fontSize: 14,
|
||||||
padding: const EdgeInsets.all(16),
|
fontWeight: FontWeight.bold,
|
||||||
child: Column(
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
const SizedBox(height: 2),
|
||||||
children: [
|
Text(
|
||||||
Container(
|
'Avaliações interativas',
|
||||||
padding: const EdgeInsets.all(8),
|
maxLines: 1,
|
||||||
decoration: BoxDecoration(
|
overflow: TextOverflow.ellipsis,
|
||||||
color: Theme.of(
|
style: TextStyle(
|
||||||
context,
|
color: Theme.of(
|
||||||
).colorScheme.primary.withOpacity(0.1),
|
context,
|
||||||
borderRadius: BorderRadius.circular(10),
|
).colorScheme.onSurfaceVariant,
|
||||||
),
|
fontSize: 11,
|
||||||
child: Icon(
|
),
|
||||||
Icons.people,
|
),
|
||||||
color: Theme.of(context).colorScheme.primary,
|
],
|
||||||
size: 20,
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
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),
|
||||||
.animate()
|
curve: Curves.elasticOut,
|
||||||
.scale(
|
)
|
||||||
duration: const Duration(milliseconds: 600),
|
.then(delay: const Duration(milliseconds: 200)),
|
||||||
curve: Curves.elasticOut,
|
);
|
||||||
)
|
|
||||||
.then(delay: const Duration(milliseconds: 300));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildViewAnalyticsCard(BuildContext context) {
|
Widget _buildViewAnalyticsCard(BuildContext context) {
|
||||||
@@ -420,183 +358,312 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCreateClassCard(BuildContext context) {
|
Widget _buildCreateClassCard(BuildContext context) {
|
||||||
return Container(
|
return ClipRRect(
|
||||||
constraints: const BoxConstraints(minHeight: 135, maxHeight: 160),
|
borderRadius: BorderRadius.circular(16),
|
||||||
decoration: BoxDecoration(
|
child:
|
||||||
color: Theme.of(context).colorScheme.surface,
|
Container(
|
||||||
borderRadius: BorderRadius.circular(16),
|
height: 150,
|
||||||
border: Border.all(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.outline.withOpacity(0.2),
|
color: Theme.of(context).colorScheme.surface,
|
||||||
width: 1,
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
border: Border.all(
|
||||||
boxShadow: [
|
color: Theme.of(
|
||||||
BoxShadow(
|
context,
|
||||||
color: Theme.of(context).colorScheme.shadow.withOpacity(0.05),
|
).colorScheme.outline.withOpacity(0.2),
|
||||||
blurRadius: 10,
|
width: 1,
|
||||||
offset: const Offset(0, 4),
|
),
|
||||||
),
|
boxShadow: [
|
||||||
],
|
BoxShadow(
|
||||||
),
|
color: Theme.of(
|
||||||
child: Material(
|
context,
|
||||||
color: Colors.transparent,
|
).colorScheme.shadow.withOpacity(0.05),
|
||||||
child: InkWell(
|
blurRadius: 10,
|
||||||
borderRadius: BorderRadius.circular(16),
|
offset: const Offset(0, 4),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
child: Material(
|
||||||
),
|
color: Colors.transparent,
|
||||||
),
|
child: InkWell(
|
||||||
)
|
borderRadius: BorderRadius.circular(16),
|
||||||
.animate()
|
onTap: _isCreatingClass
|
||||||
.scale(
|
? null
|
||||||
duration: const Duration(milliseconds: 600),
|
: () => _showCreateClassDialog(context),
|
||||||
curve: Curves.elasticOut,
|
child: Padding(
|
||||||
)
|
padding: const EdgeInsets.all(12),
|
||||||
.then(delay: const Duration(milliseconds: 150));
|
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) {
|
void _showCreateClassDialog(BuildContext context) {
|
||||||
final TextEditingController nameController = TextEditingController();
|
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(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext dialogContext) {
|
builder: (BuildContext dialogContext) {
|
||||||
return AlertDialog(
|
return StatefulBuilder(
|
||||||
shape: RoundedRectangleBorder(
|
builder: (context, setDialogState) {
|
||||||
borderRadius: BorderRadius.circular(16),
|
// Atualizar estado do dialog quando as classes carregarem
|
||||||
),
|
if (isLoadingClasses) {
|
||||||
title: Text(
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
'Criar Nova Turma',
|
if (dialogContext.mounted) setDialogState(() {});
|
||||||
style: TextStyle(
|
});
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
}
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
return AlertDialog(
|
||||||
),
|
shape: RoundedRectangleBorder(
|
||||||
content: Column(
|
borderRadius: BorderRadius.circular(16),
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
title: Text(
|
||||||
children: [
|
'Criar Nova Disciplina',
|
||||||
Text(
|
|
||||||
'Digite o nome da turma:',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
fontSize: 14,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
content: Column(
|
||||||
TextField(
|
mainAxisSize: MainAxisSize.min,
|
||||||
controller: nameController,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
decoration: InputDecoration(
|
children: [
|
||||||
hintText: 'Ex: Matemática 9º Ano',
|
Text(
|
||||||
filled: true,
|
'Nome da disciplina:',
|
||||||
fillColor: Theme.of(
|
style: TextStyle(
|
||||||
context,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
).colorScheme.surfaceContainerHighest,
|
fontSize: 14,
|
||||||
border: OutlineInputBorder(
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
borderSide: BorderSide.none,
|
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
const SizedBox(height: 12),
|
||||||
borderRadius: BorderRadius.circular(12),
|
TextField(
|
||||||
borderSide: BorderSide.none,
|
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(
|
const SizedBox(height: 16),
|
||||||
borderRadius: BorderRadius.circular(12),
|
Text(
|
||||||
borderSide: BorderSide(
|
'Turma:',
|
||||||
color: Theme.of(context).colorScheme.primary,
|
style: TextStyle(
|
||||||
width: 2,
|
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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
ElevatedButton(
|
||||||
],
|
onPressed: () {
|
||||||
),
|
final className = nameController.text.trim();
|
||||||
actions: [
|
if (className.isNotEmpty) {
|
||||||
TextButton(
|
Navigator.of(dialogContext).pop();
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
_createClass(
|
||||||
child: Text(
|
className,
|
||||||
'Cancelar',
|
schoolClassId: selectedSchoolClassId,
|
||||||
style: TextStyle(
|
);
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
}
|
||||||
|
},
|
||||||
|
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(() {
|
setState(() {
|
||||||
_isCreatingClass = true;
|
_isCreatingClass = true;
|
||||||
});
|
});
|
||||||
@@ -627,12 +694,17 @@ class _TeacherQuickActionsWidgetState extends State<TeacherQuickActionsWidget> {
|
|||||||
final classCode = _generateClassCode();
|
final classCode = _generateClassCode();
|
||||||
final firestore = FirebaseFirestore.instance;
|
final firestore = FirebaseFirestore.instance;
|
||||||
|
|
||||||
await firestore.collection('classes').add({
|
final Map<String, dynamic> classData = {
|
||||||
'name': className,
|
'name': className,
|
||||||
'teacherId': currentUser.uid,
|
'teacherId': currentUser.uid,
|
||||||
'code': classCode,
|
'code': classCode,
|
||||||
'createdAt': FieldValue.serverTimestamp(),
|
'createdAt': FieldValue.serverTimestamp(),
|
||||||
});
|
};
|
||||||
|
if (schoolClassId != null && schoolClassId.isNotEmpty) {
|
||||||
|
classData['schoolClassId'] = schoolClassId;
|
||||||
|
}
|
||||||
|
|
||||||
|
await firestore.collection('classes').add(classData);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ class SettingsNotifier extends StateNotifier<SettingsState> {
|
|||||||
SettingsNotifier()
|
SettingsNotifier()
|
||||||
: super(
|
: super(
|
||||||
SettingsState(
|
SettingsState(
|
||||||
themeMode: ThemeService.getThemeMode(),
|
themeMode: ThemeMode.light,
|
||||||
isDarkModeAvailable: ThemeService.isDarkModeAvailable(),
|
isDarkModeAvailable: ThemeService.isDarkModeAvailable(),
|
||||||
),
|
),
|
||||||
);
|
) {
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
/// Set theme mode
|
/// Set theme mode
|
||||||
Future<void> setThemeMode(ThemeMode themeMode) async {
|
Future<void> setThemeMode(ThemeMode themeMode) async {
|
||||||
@@ -60,10 +62,8 @@ class SettingsNotifier extends StateNotifier<SettingsState> {
|
|||||||
Future<void> resetSettings() async {
|
Future<void> resetSettings() async {
|
||||||
state = state.copyWith(isLoading: true);
|
state = state.copyWith(isLoading: true);
|
||||||
await ThemeService.resetTheme();
|
await ThemeService.resetTheme();
|
||||||
state = state.copyWith(
|
final themeMode = await ThemeService.getStoredThemeMode();
|
||||||
themeMode: ThemeService.getThemeMode(),
|
state = state.copyWith(themeMode: themeMode, isLoading: false);
|
||||||
isLoading: false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import '../lib/core/services/content_service.dart';
|
|
||||||
import '../lib/core/services/vector_service.dart';
|
import '../lib/core/services/vector_service.dart';
|
||||||
import '../lib/core/services/rag_service.dart';
|
|
||||||
import '../lib/core/services/rag_ai_service.dart';
|
import '../lib/core/services/rag_ai_service.dart';
|
||||||
|
import '../lib/core/services/rag_service.dart' show TutorMode;
|
||||||
import '../lib/core/models/content_chunk.dart';
|
import '../lib/core/models/content_chunk.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
@@ -64,81 +63,6 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('RAGService - Mode Instructions', () {
|
|
||||||
print('🔍 Testing RAG service mode instructions...');
|
|
||||||
|
|
||||||
// Test different modes
|
|
||||||
final explanationMode = RAGService._getModeInstructions(
|
|
||||||
TutorMode.explanation,
|
|
||||||
);
|
|
||||||
final tutorMode = RAGService._getModeInstructions(TutorMode.tutor);
|
|
||||||
final explorationMode = RAGService._getModeInstructions(
|
|
||||||
TutorMode.exploration,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(explanationMode, contains('explicações detalhadas'));
|
|
||||||
expect(tutorMode, contains('método socrático'));
|
|
||||||
expect(explorationMode, contains('exploração'));
|
|
||||||
|
|
||||||
print('✅ Mode instructions generated correctly');
|
|
||||||
print(' Explanation: "${explanationMode.substring(0, 50)}..."');
|
|
||||||
print(' Tutor: "${tutorMode.substring(0, 50)}..."');
|
|
||||||
print(' Exploration: "${explorationMode.substring(0, 50)}..."');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('RAGService - Context Building', () {
|
|
||||||
print('🔍 Testing context window building...');
|
|
||||||
|
|
||||||
// Create mock content chunks
|
|
||||||
final chunks = [
|
|
||||||
ContentChunk(
|
|
||||||
id: 'test1',
|
|
||||||
contentId: 'content1',
|
|
||||||
text:
|
|
||||||
'A fotossíntese é o processo biológico que converte luz solar em energia química.',
|
|
||||||
subject: 'Biologia',
|
|
||||||
concept: 'Fotossíntese',
|
|
||||||
unit: 'Processos Biológicos',
|
|
||||||
difficulty: 0.6,
|
|
||||||
grade: 10,
|
|
||||||
embedding: List.filled(384, 0.1),
|
|
||||||
sourceDocument: 'Biologia Manual.pdf',
|
|
||||||
metadata: {},
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
ContentChunk(
|
|
||||||
id: 'test2',
|
|
||||||
contentId: 'content1',
|
|
||||||
text:
|
|
||||||
'Durante a fotossíntese, as plantas absorvem CO2 e liberam oxigénio.',
|
|
||||||
subject: 'Biologia',
|
|
||||||
concept: 'Fotossíntese',
|
|
||||||
unit: 'Processos Biológicos',
|
|
||||||
difficulty: 0.7,
|
|
||||||
grade: 10,
|
|
||||||
embedding: List.filled(384, 0.2),
|
|
||||||
sourceDocument: 'Biologia Manual.pdf',
|
|
||||||
metadata: {},
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
final context = RAGService._buildContextWindow(
|
|
||||||
chunks,
|
|
||||||
'O que é fotossíntese?',
|
|
||||||
TutorMode.explanation,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(context, contains('CONTEÚDO EDUCACIONAL RELEVANTE'));
|
|
||||||
expect(context, contains('Fotossíntese'));
|
|
||||||
expect(context, contains('Biologia'));
|
|
||||||
expect(context, contains('INSTRUÇÕES DE TUTORIA'));
|
|
||||||
|
|
||||||
print('✅ Context window built successfully');
|
|
||||||
print(' Context length: ${context.length} characters');
|
|
||||||
print(' Contains ${chunks.length} source chunks');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('RAGAIService - Service Availability', () async {
|
test('RAGAIService - Service Availability', () async {
|
||||||
print('🔍 Testing Ollama service availability...');
|
print('🔍 Testing Ollama service availability...');
|
||||||
|
|
||||||
@@ -200,19 +124,11 @@ void main() {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Step 3: Build context
|
// Step 3: Test AI service if available
|
||||||
final context = RAGService._buildContextWindow(
|
|
||||||
mockChunks,
|
|
||||||
userQuery,
|
|
||||||
mode,
|
|
||||||
);
|
|
||||||
print(' ✅ Context built (${context.length} chars)');
|
|
||||||
|
|
||||||
// Step 4: Test AI service if available
|
|
||||||
try {
|
try {
|
||||||
final ragResponse = await RAGAIService.generateRAGResponse(
|
final ragResponse = await RAGAIService.generateRAGResponse(
|
||||||
userQuery: userQuery,
|
userQuery: userQuery,
|
||||||
context: context,
|
context: userQuery,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
sources: mockChunks,
|
sources: mockChunks,
|
||||||
);
|
);
|
||||||
@@ -221,23 +137,8 @@ void main() {
|
|||||||
print(' Answer: "${ragResponse.answer.substring(0, 100)}..."');
|
print(' Answer: "${ragResponse.answer.substring(0, 100)}..."');
|
||||||
print(' Confidence: ${ragResponse.confidence.toStringAsFixed(2)}');
|
print(' Confidence: ${ragResponse.confidence.toStringAsFixed(2)}');
|
||||||
print(' Sources: ${ragResponse.sources.length}');
|
print(' Sources: ${ragResponse.sources.length}');
|
||||||
print(
|
|
||||||
' Related concepts: ${ragResponse.relatedConcepts.join(', ')}',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print(' ⚠️ AI service not available, using mock response');
|
print(' ⚠️ AI service not available: $e');
|
||||||
|
|
||||||
// Create mock response
|
|
||||||
final mockResponse = RAGService._createRAGResponse(
|
|
||||||
query: userQuery,
|
|
||||||
context: context,
|
|
||||||
mode: mode,
|
|
||||||
sources: mockChunks,
|
|
||||||
);
|
|
||||||
|
|
||||||
print(' ✅ Mock RAG response created');
|
|
||||||
print(' Answer: "${mockResponse.answer.substring(0, 100)}..."');
|
|
||||||
print(' Confidence: ${mockResponse.confidence.toStringAsFixed(2)}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
print('✅ RAG pipeline simulation completed successfully');
|
print('✅ RAG pipeline simulation completed successfully');
|
||||||
|
|||||||
Reference in New Issue
Block a user