679 lines
22 KiB
Dart
679 lines
22 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_animate/flutter_animate.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
import '../../../../core/services/auth_service.dart';
|
|
import '../../../../core/utils/logger.dart';
|
|
|
|
/// Página para gerenciar quizzes (eliminar quizzes feitos)
|
|
class QuizManagementPage extends StatefulWidget {
|
|
const QuizManagementPage({super.key});
|
|
|
|
@override
|
|
State<QuizManagementPage> createState() => _QuizManagementPageState();
|
|
}
|
|
|
|
class _QuizManagementPageState extends State<QuizManagementPage> {
|
|
List<Map<String, dynamic>> _quizHistory = [];
|
|
bool _loading = true;
|
|
String _userRole = '';
|
|
|
|
// Disciplina seleccionada (null = vista de disciplinas)
|
|
String? _selectedDisciplineId;
|
|
Map<String, String> _classNames = {}; // classId → name
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadUserRole();
|
|
_loadQuizHistory();
|
|
}
|
|
|
|
Future<void> _loadUserRole() async {
|
|
final user = AuthService.currentUser;
|
|
if (user != null) {
|
|
final userDoc = await FirebaseFirestore.instance
|
|
.collection('users')
|
|
.doc(user.uid)
|
|
.get();
|
|
|
|
if (userDoc.exists) {
|
|
setState(() {
|
|
_userRole = userDoc.data()?['role'] ?? '';
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadQuizHistory() async {
|
|
try {
|
|
final user = AuthService.currentUser;
|
|
if (user == null) return;
|
|
|
|
Query query;
|
|
|
|
if (_userRole == 'teacher') {
|
|
// Professor: ver todos os quizzes criados
|
|
query = FirebaseFirestore.instance
|
|
.collection('teacherQuizzes')
|
|
.where('teacherId', isEqualTo: user.uid)
|
|
.orderBy('createdAt', descending: true);
|
|
} else {
|
|
// Aluno: ver quizzes criados pelo próprio aluno + conceitos dominados
|
|
final results = await Future.wait([
|
|
FirebaseFirestore.instance
|
|
.collection('userStats')
|
|
.doc(user.uid)
|
|
.get(),
|
|
FirebaseFirestore.instance
|
|
.collection('quizHistory')
|
|
.doc(user.uid)
|
|
.collection('quizzes')
|
|
.orderBy('createdAt', descending: true)
|
|
.get(),
|
|
]);
|
|
|
|
final userStatsSnapshot = results[0] as DocumentSnapshot;
|
|
final studentQuizzesSnapshot = results[1] as QuerySnapshot;
|
|
|
|
List<Map<String, dynamic>> quizList = [];
|
|
|
|
// Adicionar quizzes criados pelo aluno
|
|
for (final doc in studentQuizzesSnapshot.docs) {
|
|
final data = Map<String, dynamic>.from(doc.data() as Map);
|
|
data['id'] = doc.id;
|
|
data['title'] = data['materialName'] ?? 'Quiz sem nome';
|
|
data['type'] = 'created';
|
|
quizList.add(data);
|
|
}
|
|
|
|
// Adicionar conceitos dominados
|
|
if (userStatsSnapshot.exists) {
|
|
final stats = userStatsSnapshot.data() as Map<String, dynamic>?;
|
|
if (stats != null) {
|
|
final masteredConcepts =
|
|
(stats['masteredConcepts'] as List<dynamic>?)
|
|
?.map((c) => Map<String, dynamic>.from(c as Map))
|
|
.toList() ??
|
|
[];
|
|
|
|
for (final concept in masteredConcepts) {
|
|
quizList.add({
|
|
'id': concept['conceptName'] ?? '',
|
|
'title': concept['conceptName'] ?? 'Conceito sem nome',
|
|
'score': concept['masteryLevel'] ?? 0,
|
|
'totalQuestions': 100, // Simulado
|
|
'completedAt': concept['masteredAt'] ?? Timestamp.now(),
|
|
'type': 'concept',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
_quizHistory = quizList;
|
|
_loading = false;
|
|
});
|
|
return;
|
|
}
|
|
|
|
final snapshot = await query.get();
|
|
final quizzes = snapshot.docs
|
|
.map((doc) {
|
|
final data = Map<String, dynamic>.from(doc.data() as Map);
|
|
data['id'] = doc.id;
|
|
return data;
|
|
})
|
|
.cast<Map<String, dynamic>>()
|
|
.toList();
|
|
|
|
// Obter nomes das disciplinas
|
|
final classIdSet = <String>{};
|
|
for (final q in quizzes) {
|
|
final ids = (q['classIds'] as List?)?.cast<String>() ?? [];
|
|
classIdSet.addAll(ids);
|
|
}
|
|
final classNamesMap = <String, String>{};
|
|
if (classIdSet.isNotEmpty) {
|
|
final docs = await Future.wait(
|
|
classIdSet.map(
|
|
(id) =>
|
|
FirebaseFirestore.instance.collection('classes').doc(id).get(),
|
|
),
|
|
);
|
|
for (final doc in docs.where((d) => d.exists)) {
|
|
classNamesMap[doc.id] = doc.data()?['name'] as String? ?? doc.id;
|
|
}
|
|
}
|
|
|
|
setState(() {
|
|
_quizHistory = quizzes;
|
|
_classNames = classNamesMap;
|
|
_loading = false;
|
|
});
|
|
} catch (e) {
|
|
Logger.error('Error loading quiz history: $e');
|
|
setState(() {
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteQuiz(String quizId, String quizTitle, String type) async {
|
|
try {
|
|
String confirmMessage;
|
|
String successMessage;
|
|
|
|
if (_userRole == 'teacher') {
|
|
confirmMessage =
|
|
'Tem certeza que deseja eliminar o quiz "$quizTitle"? Esta ação não pode ser desfeita.';
|
|
successMessage = 'Quiz eliminado com sucesso!';
|
|
} else {
|
|
if (type == 'created') {
|
|
confirmMessage =
|
|
'Tem certeza que deseja eliminar seu quiz "$quizTitle"?';
|
|
successMessage = 'Quiz eliminado com sucesso!';
|
|
} else {
|
|
confirmMessage =
|
|
'Tem certeza que deseja remover o conceito "$quizTitle" do seu histórico?';
|
|
successMessage = 'Conceito removido com sucesso!';
|
|
}
|
|
}
|
|
|
|
final confirmed = await _showDeleteConfirmation(
|
|
quizTitle,
|
|
confirmMessage,
|
|
);
|
|
if (!confirmed) return;
|
|
|
|
if (_userRole == 'teacher') {
|
|
// Professor: eliminar quiz criado
|
|
await FirebaseFirestore.instance
|
|
.collection('teacherQuizzes')
|
|
.doc(quizId)
|
|
.delete();
|
|
|
|
// Também eliminar do histórico de alunos
|
|
final historySnapshot = await FirebaseFirestore.instance
|
|
.collection('quizHistory')
|
|
.where('quizId', isEqualTo: quizId)
|
|
.get();
|
|
|
|
for (final doc in historySnapshot.docs) {
|
|
await doc.reference.delete();
|
|
}
|
|
} else {
|
|
// Aluno: remover quiz criado ou conceito dominado
|
|
if (type == 'created') {
|
|
// Remover quiz criado pelo aluno
|
|
final user = AuthService.currentUser;
|
|
if (user != null) {
|
|
await FirebaseFirestore.instance
|
|
.collection('quizHistory')
|
|
.doc(user.uid)
|
|
.collection('quizzes')
|
|
.doc(quizId)
|
|
.delete();
|
|
}
|
|
} else {
|
|
// Remover conceito dominado
|
|
final user = AuthService.currentUser;
|
|
if (user != null) {
|
|
final userStatsDoc = await FirebaseFirestore.instance
|
|
.collection('userStats')
|
|
.doc(user.uid)
|
|
.get();
|
|
|
|
if (userStatsDoc.exists) {
|
|
final userStats = userStatsDoc.data() as Map<String, dynamic>;
|
|
final masteredConcepts =
|
|
(userStats['masteredConcepts'] as List<dynamic>?)
|
|
?.map((c) => Map<String, dynamic>.from(c as Map))
|
|
.toList() ??
|
|
[];
|
|
|
|
// Encontrar o conceito específico para remover
|
|
final conceptToRemove = masteredConcepts.firstWhere(
|
|
(c) => c['conceptName'] == quizId,
|
|
orElse: () => {},
|
|
);
|
|
|
|
if (conceptToRemove.isNotEmpty) {
|
|
await FirebaseFirestore.instance
|
|
.collection('userStats')
|
|
.doc(user.uid)
|
|
.update({
|
|
'masteredConcepts': FieldValue.arrayRemove([
|
|
conceptToRemove,
|
|
]),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_loadQuizHistory(); // Recarregar lista
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(successMessage),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
Logger.error('Error deleting quiz: $e');
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Erro ao eliminar: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<bool> _showDeleteConfirmation(String quizTitle, String message) async {
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(
|
|
_userRole == 'teacher' ? 'Eliminar Quiz' : 'Confirmar Eliminação',
|
|
),
|
|
content: Text(message),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
child: const Text('Cancelar'),
|
|
),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
|
|
child: const Text('Eliminar'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
return result ?? false;
|
|
}
|
|
|
|
Map<String, List<Map<String, dynamic>>> _groupByDiscipline() {
|
|
final Map<String, List<Map<String, dynamic>>> groups = {};
|
|
for (final quiz in _quizHistory) {
|
|
final quizClassIds = (quiz['classIds'] as List?)?.cast<String>() ?? [];
|
|
String? groupId = quizClassIds.cast<String?>().firstWhere(
|
|
(cid) => cid != null && _classNames.containsKey(cid),
|
|
orElse: () => null,
|
|
);
|
|
groupId ??= quizClassIds.isNotEmpty ? quizClassIds.first : '__geral__';
|
|
groups.putIfAbsent(groupId, () => []).add(quiz);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
|
|
return PopScope(
|
|
canPop: _selectedDisciplineId == null,
|
|
onPopInvokedWithResult: (didPop, _) {
|
|
if (!didPop && _selectedDisciplineId != null) {
|
|
setState(() => _selectedDisciplineId = null);
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: cs.surface,
|
|
appBar: AppBar(
|
|
title: Text(
|
|
_userRole == 'teacher' ? 'Gerenciar Quizzes' : 'Meu Histórico',
|
|
),
|
|
backgroundColor: cs.surface,
|
|
foregroundColor: cs.onSurface,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () {
|
|
if (_userRole == 'teacher' && _selectedDisciplineId != null) {
|
|
setState(() => _selectedDisciplineId = null);
|
|
return;
|
|
}
|
|
if (Navigator.of(context).canPop()) {
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
context.go(
|
|
_userRole == 'teacher'
|
|
? '/teacher-dashboard'
|
|
: '/student-dashboard',
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
body: Container(
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [cs.primary.withValues(alpha: 0.05), cs.surface],
|
|
),
|
|
),
|
|
child: _loading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: _quizHistory.isEmpty
|
|
? _buildEmptyState()
|
|
: _userRole == 'teacher'
|
|
? _buildTeacherBody(cs)
|
|
: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: _quizHistory.length,
|
|
itemBuilder: (context, index) {
|
|
final quiz = _quizHistory[index];
|
|
return _buildQuizCard(quiz)
|
|
.animate()
|
|
.slideX(duration: const Duration(milliseconds: 300))
|
|
.then(delay: Duration(milliseconds: index * 50));
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTeacherBody(ColorScheme cs) {
|
|
final groups = _groupByDiscipline();
|
|
|
|
// Vista de quizzes de uma disciplina
|
|
if (_selectedDisciplineId != null) {
|
|
final quizzes = groups[_selectedDisciplineId] ?? [];
|
|
final disciplineName =
|
|
_classNames[_selectedDisciplineId] ??
|
|
(_selectedDisciplineId == '__geral__'
|
|
? 'Geral'
|
|
: _selectedDisciplineId!);
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
|
|
child: Row(
|
|
children: [
|
|
IconButton(
|
|
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
|
onPressed: () => setState(() => _selectedDisciplineId = null),
|
|
),
|
|
const SizedBox(width: 4),
|
|
Expanded(
|
|
child: Text(
|
|
disciplineName,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: cs.onSurface,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
'${quizzes.length} quiz${quizzes.length != 1 ? 'zes' : ''}',
|
|
style: TextStyle(fontSize: 13, color: cs.onSurfaceVariant),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const Divider(height: 1),
|
|
Expanded(
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: quizzes.length,
|
|
itemBuilder: (context, index) {
|
|
final quiz = quizzes[index];
|
|
return _buildQuizCard(quiz)
|
|
.animate()
|
|
.slideX(duration: const Duration(milliseconds: 300))
|
|
.then(delay: Duration(milliseconds: index * 50));
|
|
},
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// Vista de disciplinas
|
|
final disciplineIds = groups.keys.toList();
|
|
return ListView.separated(
|
|
padding: const EdgeInsets.all(16),
|
|
itemCount: disciplineIds.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
|
itemBuilder: (context, i) {
|
|
final dId = disciplineIds[i];
|
|
final dName = _classNames[dId] ?? (dId == '__geral__' ? 'Geral' : dId);
|
|
final count = groups[dId]!.length;
|
|
return InkWell(
|
|
borderRadius: BorderRadius.circular(16),
|
|
onTap: () => setState(() => _selectedDisciplineId = dId),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: BoxDecoration(
|
|
color: cs.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: cs.outline.withValues(alpha: 0.15)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: cs.shadow.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: cs.primary.withValues(alpha: 0.1),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Icon(Icons.school, color: cs.primary, size: 26),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
dName,
|
|
maxLines: 2,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.bold,
|
|
color: cs.onSurface,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
'$count quiz${count != 1 ? 'zes' : ''} criado${count != 1 ? 's' : ''}',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: cs.onSurfaceVariant,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Icon(Icons.chevron_right, color: cs.onSurfaceVariant),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildEmptyState() {
|
|
final cs = Theme.of(context).colorScheme;
|
|
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(32),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
_userRole == 'teacher'
|
|
? Icons.quiz_outlined
|
|
: Icons.history_edu_outlined,
|
|
size: 64,
|
|
color: cs.onSurfaceVariant,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
_userRole == 'teacher'
|
|
? 'Nenhum quiz criado'
|
|
: 'Nenhum quiz no histórico',
|
|
style: TextStyle(
|
|
color: cs.onSurface,
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
_userRole == 'teacher'
|
|
? 'Crie seu primeiro quiz para começar!'
|
|
: 'Complete alguns quizzes para ver seu histórico aqui.',
|
|
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuizCard(Map<String, dynamic> quiz) {
|
|
final cs = Theme.of(context).colorScheme;
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 12),
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: cs.surface,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: cs.outline.withValues(alpha: 0.2)),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: cs.shadow.withValues(alpha: 0.1),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
quiz['title'] ?? 'Quiz sem título',
|
|
style: TextStyle(
|
|
color: cs.onSurface,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
if (_userRole == 'student' && quiz['score'] != null) ...[
|
|
Text(
|
|
'Pontuação: ${quiz['score']}/${quiz['totalQuestions'] ?? '?'}',
|
|
style: TextStyle(
|
|
color: cs.primary,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
Text(
|
|
_userRole == 'teacher' || quiz['type'] == 'created'
|
|
? 'Criado em: ${_formatDate(quiz['createdAt'])}'
|
|
: 'Completado em: ${_formatDate(quiz['completedAt'])}',
|
|
style: TextStyle(
|
|
color: cs.onSurfaceVariant,
|
|
fontSize: 12,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => _deleteQuiz(
|
|
quiz['id'],
|
|
quiz['title'] ?? 'Quiz',
|
|
quiz['type'] ?? 'unknown',
|
|
),
|
|
icon: Icon(Icons.delete_outline, color: Colors.red),
|
|
tooltip: _userRole == 'teacher' ? 'Eliminar Quiz' : 'Remover',
|
|
),
|
|
],
|
|
),
|
|
if (_userRole == 'teacher' &&
|
|
quiz['classIds'] != null &&
|
|
_selectedDisciplineId == null) ...[
|
|
const SizedBox(height: 8),
|
|
Wrap(
|
|
spacing: 4,
|
|
children: (quiz['classIds'] as List<dynamic>).map((classId) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 4,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: cs.primaryContainer,
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Text(
|
|
'Disciplina: $classId',
|
|
style: TextStyle(
|
|
color: cs.onPrimaryContainer,
|
|
fontSize: 10,
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatDate(dynamic date) {
|
|
if (date == null) return 'Data desconhecida';
|
|
|
|
DateTime dateTime;
|
|
if (date is Timestamp) {
|
|
dateTime = date.toDate();
|
|
} else if (date is DateTime) {
|
|
dateTime = date;
|
|
} else {
|
|
return 'Data desconhecida';
|
|
}
|
|
|
|
return '${dateTime.day}/${dateTime.month}/${dateTime.year}';
|
|
}
|
|
}
|