Historico de quizzes e inicio de atualização da IA para leitura de pdfs de matemática (incompleto)
This commit is contained in:
@@ -328,7 +328,7 @@ class _TutorChatPageState extends State<TutorChatPage>
|
||||
void _addWelcomeMessage() {
|
||||
final welcomeMessage = {
|
||||
'content':
|
||||
'''**Olá! Sou o Vico, o teu Assistente IA oficial do Teach it.**
|
||||
'''**Olá! Sou o Vico, o teu Assistente IA oficial do Learn It.**
|
||||
|
||||
Estou aqui para te ajudar a aprender de forma confiante e motivadora!
|
||||
|
||||
|
||||
@@ -13,22 +13,30 @@ class JoinClassPage extends ConsumerStatefulWidget {
|
||||
|
||||
class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
final _codeController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_codeController.dispose();
|
||||
_nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _joinClass() async {
|
||||
final code = _codeController.text.trim().toUpperCase();
|
||||
final customName = _nameController.text.trim();
|
||||
|
||||
if (code.isEmpty) {
|
||||
_showError('Insere o código da disciplina');
|
||||
return;
|
||||
}
|
||||
|
||||
if (customName.isEmpty) {
|
||||
_showError('Insere o nome da disciplina');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
@@ -105,6 +113,7 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
currentUser.displayName ??
|
||||
currentUser.email?.split('@')[0] ??
|
||||
'Aluno',
|
||||
'customClassName': customName,
|
||||
'joinedAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
|
||||
@@ -201,7 +210,7 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Entrar numa Disciplina',
|
||||
'Adicionar uma Disciplina',
|
||||
style: TextStyle(
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
@@ -345,7 +354,14 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
_buildInstructionItem(
|
||||
context,
|
||||
'3.',
|
||||
'Clicar em "Entrar na Disciplina" para confirmar',
|
||||
'Escrever o nome da disciplina',
|
||||
colorScheme,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildInstructionItem(
|
||||
context,
|
||||
'4.',
|
||||
'Clicar em "Adicionar uma Disciplina" para confirmar',
|
||||
colorScheme,
|
||||
),
|
||||
],
|
||||
@@ -407,6 +423,53 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Campo de nome da disciplina
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.outline.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withOpacity(0.2)
|
||||
: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: _nameController,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Nome da disciplina',
|
||||
hintStyle: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withOpacity(
|
||||
0.5,
|
||||
),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: const EdgeInsets.all(20),
|
||||
prefixIcon: Icon(
|
||||
Icons.edit,
|
||||
color: colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
minWidth: 48,
|
||||
minHeight: 48,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Botão de entrar
|
||||
@@ -445,7 +508,7 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Entrar na Disciplina',
|
||||
'Adicionar uma Disciplina',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -540,26 +603,30 @@ class _JoinClassPageState extends ConsumerState<JoinClassPage> {
|
||||
children: [
|
||||
Icon(Icons.help_outline, color: colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Ajuda - Código da Disciplina',
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Ajuda - Código da Disciplina',
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'O código da disciplina é um código único de 6 caracteres que o teu professor cria para cada disciplina.',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Se não tens o código, contacta o teu professor.',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'O código da disciplina é um código único de 6 caracteres que o teu professor cria para cada disciplina.',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Se não tens o código, contacta o teu professor.',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
|
||||
@@ -200,7 +200,7 @@ class QuickAccessWidget extends StatelessWidget {
|
||||
|
||||
Widget _buildJoinClassCard(BuildContext context) {
|
||||
return DashboardActionCard(
|
||||
title: 'Entrar numa Disciplina',
|
||||
title: 'Adicionar uma Disciplina',
|
||||
subtitle: 'Junta-te a uma disciplina com o código',
|
||||
icon: Icons.group_add,
|
||||
layout: DashboardActionCardLayout.horizontal,
|
||||
@@ -264,7 +264,7 @@ class QuickAccessWidget extends StatelessWidget {
|
||||
},
|
||||
),
|
||||
_QuickAccessItem(
|
||||
title: 'Entrar numa Disciplina',
|
||||
title: 'Adicionar uma Disciplina',
|
||||
subtitle: 'Junta-te a uma disciplina com o código',
|
||||
icon: Icons.group_add,
|
||||
onTap: () {
|
||||
|
||||
@@ -126,6 +126,7 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
final enrollmentData = enrollmentDoc.data() as Map<String, dynamic>;
|
||||
final classId = enrollmentData['classId'] as String? ?? '';
|
||||
final enrollmentId = enrollmentDoc.id;
|
||||
final customClassName = enrollmentData['customClassName'] as String?;
|
||||
|
||||
if (classId.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -163,11 +164,12 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
}
|
||||
|
||||
final classData = snapshot.data!.data() as Map<String, dynamic>;
|
||||
final className = classData['name'] as String? ?? 'Sem nome';
|
||||
final className =
|
||||
customClassName ?? (classData['name'] as String? ?? 'Sem nome');
|
||||
final classCode = classData['code'] as String? ?? '----';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => _showRemoveClassDialog(context, enrollmentId, className),
|
||||
onTap: () => _showEditNameDialog(context, enrollmentId, className),
|
||||
child: Container(
|
||||
width: 200,
|
||||
constraints: const BoxConstraints(minHeight: 150),
|
||||
@@ -187,19 +189,29 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.school,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
Row(
|
||||
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: Icon(
|
||||
Icons.school,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
@@ -291,6 +303,7 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
final enrollmentData = enrollmentDoc.data() as Map<String, dynamic>;
|
||||
final classId = enrollmentData['classId'] as String? ?? '';
|
||||
final enrollmentId = enrollmentDoc.id;
|
||||
final customClassName = enrollmentData['customClassName'] as String?;
|
||||
|
||||
if (classId.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
@@ -312,7 +325,8 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
}
|
||||
|
||||
final classData = snapshot.data!.data() as Map<String, dynamic>;
|
||||
final className = classData['name'] as String? ?? 'Sem nome';
|
||||
final className =
|
||||
customClassName ?? (classData['name'] as String? ?? 'Sem nome');
|
||||
final classCode = classData['code'] as String? ?? '----';
|
||||
|
||||
return Card(
|
||||
@@ -351,13 +365,30 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 20,
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () =>
|
||||
_showEditNameDialog(context, enrollmentId, className),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () =>
|
||||
_showRemoveClassDialog(context, enrollmentId, className),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () =>
|
||||
_showRemoveClassDialog(context, enrollmentId, className),
|
||||
onTap: () => _showEditNameDialog(context, enrollmentId, className),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -493,7 +524,7 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erro ao sair da disciplina: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -503,4 +534,49 @@ class _StudentClassesListWidgetState extends State<StudentClassesListWidget> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showEditNameDialog(
|
||||
BuildContext context,
|
||||
String enrollmentId,
|
||||
String currentName,
|
||||
) {
|
||||
final controller = TextEditingController(text: currentName);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Editar nome da disciplina'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(hintText: 'Nome da disciplina'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final newName = controller.text.trim();
|
||||
if (newName.isNotEmpty) {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('enrollments')
|
||||
.doc(enrollmentId)
|
||||
.update({'customClassName': newName});
|
||||
if (mounted) {
|
||||
Navigator.pop(context);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Nome atualizado com sucesso'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Salvar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +349,7 @@ class _ContentManagementPageState extends State<ContentManagementPage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface.withOpacity(0.8),
|
||||
@@ -396,13 +396,13 @@ class _ContentManagementPageState extends State<ContentManagementPage> {
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
margin: const EdgeInsets.only(right: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface.withOpacity(0.8),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: _filteredClassIds.length > 3,
|
||||
isScrollable: true,
|
||||
dividerColor: Colors.transparent,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicator: BoxDecoration(
|
||||
@@ -419,16 +419,18 @@ class _ContentManagementPageState extends State<ContentManagementPage> {
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
tabs: _filteredClassIds
|
||||
.map(
|
||||
(classId) => Tab(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(_classNames[classId] ?? classId),
|
||||
),
|
||||
tabs: List.generate(_filteredClassIds.length, (index) {
|
||||
final classId = _filteredClassIds[index];
|
||||
return Tab(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: index == 0 ? 4 : 12,
|
||||
right: index == _filteredClassIds.length - 1 ? 4 : 12,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
child: Text(_classNames[classId] ?? classId),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -157,7 +157,7 @@ class _TeacherMaterialsPageState extends State<TeacherMaterialsPage> {
|
||||
),
|
||||
if (_filteredClasses.isNotEmpty)
|
||||
TabBar(
|
||||
isScrollable: _filteredClasses.length > 3,
|
||||
isScrollable: true,
|
||||
indicatorColor: const Color(0xFFF68D2D),
|
||||
labelColor: const Color(0xFFF68D2D),
|
||||
unselectedLabelColor: Theme.of(
|
||||
|
||||
@@ -45,6 +45,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
// generating state
|
||||
String? _generatingForId;
|
||||
|
||||
// Multi-select for history
|
||||
Set<String> _selectedQuizIds = {};
|
||||
bool _isSelectionMode = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -366,16 +370,68 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if a material is mathematics-based
|
||||
bool _isMathematicsSubject(Map<String, String> material) {
|
||||
final matName = (material['name'] ?? '').toLowerCase();
|
||||
final classId = material['classId'];
|
||||
String className = '';
|
||||
|
||||
// Get class name if classId is available
|
||||
if (classId != null && _materialClassNames.containsKey(classId)) {
|
||||
className = _materialClassNames[classId]!.toLowerCase();
|
||||
}
|
||||
|
||||
// Keywords for mathematics
|
||||
final mathKeywords = [
|
||||
'matemática',
|
||||
'math',
|
||||
'álgebra',
|
||||
'geometria',
|
||||
'cálculo',
|
||||
'estatística',
|
||||
'trigonometria',
|
||||
'função',
|
||||
'equação',
|
||||
'fração',
|
||||
'raiz',
|
||||
'potência',
|
||||
'derivada',
|
||||
'integral',
|
||||
'número',
|
||||
'gráfico',
|
||||
'fórmula',
|
||||
'matriz',
|
||||
'vetor',
|
||||
'probabilidade',
|
||||
'percentagem',
|
||||
'ângulo',
|
||||
'triângulo',
|
||||
'quadrado',
|
||||
'círculo',
|
||||
'volume',
|
||||
'área',
|
||||
'perímetro',
|
||||
];
|
||||
|
||||
// Check if material name or class name contains math keywords
|
||||
final combinedText = '$matName $className';
|
||||
return mathKeywords.any((keyword) => combinedText.contains(keyword));
|
||||
}
|
||||
|
||||
Future<void> _generateQuiz(Map<String, String> material) async {
|
||||
setState(() => _generatingForId = material['id']);
|
||||
try {
|
||||
final matId = material['id']!;
|
||||
final matName = material['name'] ?? 'Material';
|
||||
final isMathematics = _isMathematicsSubject(material);
|
||||
|
||||
// Buscar contexto do PDF
|
||||
final context = await MaterialsRAGService.getRelevantChunks(
|
||||
userQuery: 'conteúdo geral resumo tópicos principais',
|
||||
userQuery: 'todos os exercícios todos os tópicos completo',
|
||||
selectedMaterialIds: [matId],
|
||||
maxChunks: 20, // Aumentar para cobrir todo o documento
|
||||
filterTableData:
|
||||
isMathematics, // Filtrar dados de tabela para matemática
|
||||
);
|
||||
|
||||
if (context.isEmpty) {
|
||||
@@ -384,7 +440,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
}
|
||||
|
||||
// Gerar quiz via Ollama em formato JSON estruturado
|
||||
final numQuestions = 5 + Random().nextInt(16); // 5..20
|
||||
final numQuestions = isMathematics
|
||||
? 10 +
|
||||
Random().nextInt(11) // 10..20 para matemática
|
||||
: 5 + Random().nextInt(16); // 5..20 para outras matérias
|
||||
final prompt =
|
||||
'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n'
|
||||
'$context\n\n'
|
||||
@@ -394,7 +453,10 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
'[{"q":"Pergunta aqui","opts":["A) opção","B) opção","C) opção","D) opção"],"ans":0,"exp":"Explicação breve da resposta correcta"},...]\n'
|
||||
'ans é o índice (0-3) da opção correcta.';
|
||||
|
||||
final raw = await RAGAIService.generateQuiz(prompt);
|
||||
final raw = await RAGAIService.generateQuiz(
|
||||
prompt,
|
||||
isMathematics: isMathematics,
|
||||
);
|
||||
final questions = _parseQuizJson(raw);
|
||||
|
||||
if (questions.isEmpty) {
|
||||
@@ -704,6 +766,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
quizId: quizId,
|
||||
questions: questions,
|
||||
materialName: name,
|
||||
onQuizCompleted: () {
|
||||
_loadHistory();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -798,6 +863,9 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
title: title,
|
||||
questions: questions,
|
||||
historyDocId: historyDocId,
|
||||
onQuizCompleted: () {
|
||||
_loadHistory();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -881,13 +949,17 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
);
|
||||
}
|
||||
|
||||
void _showQuizFromHistory(String title, String rawJson) {
|
||||
void _showQuizFromHistory(
|
||||
String title,
|
||||
String rawJson, {
|
||||
String? historyDocId,
|
||||
}) {
|
||||
final questions = _parseQuizJson(rawJson);
|
||||
if (questions.isEmpty) {
|
||||
_showSnack('Não foi possível carregar este quiz.');
|
||||
return;
|
||||
}
|
||||
_showInteractiveQuiz(title, questions);
|
||||
_showInteractiveQuiz(title, questions, historyDocId: historyDocId);
|
||||
}
|
||||
|
||||
void _showSnack(String msg) {
|
||||
@@ -1760,11 +1832,152 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
return groups;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getAiGeneratedQuizzes() {
|
||||
return _history.where((q) => q['teacherQuizId'] == null).toList();
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getTeacherQuizzes() {
|
||||
return _history.where((q) => q['teacherQuizId'] != null).toList();
|
||||
}
|
||||
|
||||
Future<void> _deleteQuizFromHistory(Map<String, dynamic> item) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Eliminar Quiz'),
|
||||
content: Text(
|
||||
'Tem certeza que deseja eliminar "${item['title'] ?? 'Quiz'}"?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Eliminar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
await FirebaseFirestore.instance
|
||||
.collection('quizHistory')
|
||||
.doc(user.uid)
|
||||
.collection('quizzes')
|
||||
.doc(item['id'])
|
||||
.delete();
|
||||
|
||||
setState(() {
|
||||
_history.removeWhere((q) => q['id'] == item['id']);
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Quiz eliminado com sucesso!'),
|
||||
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<void> _deleteSelectedQuizzes() async {
|
||||
if (_selectedQuizIds.isEmpty) return;
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Eliminar Quizzes'),
|
||||
content: Text(
|
||||
'Tem certeza que deseja eliminar ${_selectedQuizIds.length} quiz${_selectedQuizIds.length != 1 ? 'zes' : ''}?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Eliminar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
final batch = FirebaseFirestore.instance.batch();
|
||||
for (final quizId in _selectedQuizIds) {
|
||||
final ref = FirebaseFirestore.instance
|
||||
.collection('quizHistory')
|
||||
.doc(user.uid)
|
||||
.collection('quizzes')
|
||||
.doc(quizId);
|
||||
batch.delete(ref);
|
||||
}
|
||||
|
||||
await batch.commit();
|
||||
|
||||
setState(() {
|
||||
_history.removeWhere((q) => _selectedQuizIds.contains(q['id']));
|
||||
_selectedQuizIds.clear();
|
||||
_isSelectionMode = false;
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Quizzes eliminados com sucesso!'),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error deleting quizzes: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Erro ao eliminar: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHistoryTab(ColorScheme cs) {
|
||||
if (_loadingHistory) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (_history.isEmpty) {
|
||||
|
||||
final aiQuizzes = _getAiGeneratedQuizzes();
|
||||
final teacherQuizzes = _getTeacherQuizzes();
|
||||
|
||||
if (aiQuizzes.isEmpty && teacherQuizzes.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
@@ -1788,130 +2001,263 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
);
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Histórico',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
_isSelectionMode ? Icons.close : Icons.checklist,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_isSelectionMode = !_isSelectionMode;
|
||||
_selectedQuizIds.clear();
|
||||
});
|
||||
},
|
||||
tooltip: _isSelectionMode
|
||||
? 'Cancelar seleção'
|
||||
: 'Selecionar múltiplos',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: TabBar(
|
||||
labelColor: cs.onPrimary,
|
||||
unselectedLabelColor: cs.onSurfaceVariant,
|
||||
indicator: BoxDecoration(
|
||||
color: cs.primary,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: 'Gerados por IA'),
|
||||
Tab(text: 'Do Professor'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
_buildHistorySection(cs, aiQuizzes, isTeacherQuizzes: false),
|
||||
_buildHistorySection(
|
||||
cs,
|
||||
teacherQuizzes,
|
||||
isTeacherQuizzes: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistorySection(
|
||||
ColorScheme cs,
|
||||
List<Map<String, dynamic>> items, {
|
||||
required bool isTeacherQuizzes,
|
||||
}) {
|
||||
if (items.isEmpty) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.quiz_outlined,
|
||||
size: 64,
|
||||
color: cs.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
isTeacherQuizzes
|
||||
? 'Sem quizzes do professor'
|
||||
: 'Sem quizzes gerados por IA',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isTeacherQuizzes) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildHistoryList(cs, items, isTeacherQuizzes: isTeacherQuizzes),
|
||||
if (_isSelectionMode && _selectedQuizIds.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _deleteSelectedQuizzes,
|
||||
backgroundColor: Colors.red,
|
||||
icon: const Icon(Icons.delete),
|
||||
label: Text('Eliminar (${_selectedQuizIds.length})'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final groups = _groupHistoryByDiscipline();
|
||||
final filteredItems = items.where((item) {
|
||||
final cid = item['classId'] as String?;
|
||||
return groups.containsKey(cid) && cid != null;
|
||||
}).toList();
|
||||
|
||||
if (filteredItems.isEmpty) {
|
||||
return Stack(
|
||||
children: [
|
||||
_buildHistoryList(cs, items, isTeacherQuizzes: isTeacherQuizzes),
|
||||
if (_isSelectionMode && _selectedQuizIds.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _deleteSelectedQuizzes,
|
||||
backgroundColor: Colors.red,
|
||||
icon: const Icon(Icons.delete),
|
||||
label: Text('Eliminar (${_selectedQuizIds.length})'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final realDisciplineIds = groups.keys
|
||||
.where((k) => k != '__geral__' && _historyClassNames.containsKey(k))
|
||||
.toList();
|
||||
|
||||
if (realDisciplineIds.isEmpty) {
|
||||
return _buildHistoryList(cs, _history);
|
||||
}
|
||||
|
||||
if (_selectedHistoryDisciplineId != null) {
|
||||
final items = groups[_selectedHistoryDisciplineId] ?? [];
|
||||
final dName =
|
||||
_historyClassNames[_selectedHistoryDisciplineId] ??
|
||||
_selectedHistoryDisciplineId!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
return Stack(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 8, 16, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||
onPressed: () =>
|
||||
setState(() => _selectedHistoryDisciplineId = null),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
dName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${items.length} quiz${items.length != 1 ? 'zes' : ''}',
|
||||
style: TextStyle(fontSize: 13, color: cs.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
_buildHistoryList(cs, items, isTeacherQuizzes: isTeacherQuizzes),
|
||||
if (_isSelectionMode && _selectedQuizIds.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _deleteSelectedQuizzes,
|
||||
backgroundColor: Colors.red,
|
||||
icon: const Icon(Icons.delete),
|
||||
label: Text('Eliminar (${_selectedQuizIds.length})'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(child: _buildHistoryList(cs, items)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: realDisciplineIds.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final dId = realDisciplineIds[i];
|
||||
final dName = _historyClassNames[dId] ?? dId;
|
||||
final count = groups[dId]!.length;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => setState(() => _selectedHistoryDisciplineId = dId),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.separated(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
itemCount: realDisciplineIds.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final dId = realDisciplineIds[i];
|
||||
final dName = _historyClassNames[dId] ?? dId;
|
||||
final count = groups[dId]!.length;
|
||||
return InkWell(
|
||||
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),
|
||||
onTap: () => setState(() => _selectedHistoryDisciplineId = 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.history_edu, 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,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$count quiz${count != 1 ? 'zes' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
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' : ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(Icons.chevron_right, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
Icon(Icons.chevron_right, color: cs.onSurfaceVariant),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_isSelectionMode && _selectedQuizIds.isNotEmpty)
|
||||
Positioned(
|
||||
bottom: 16,
|
||||
right: 16,
|
||||
child: FloatingActionButton.extended(
|
||||
onPressed: _deleteSelectedQuizzes,
|
||||
backgroundColor: Colors.red,
|
||||
icon: const Icon(Icons.delete),
|
||||
label: Text('Eliminar (${_selectedQuizIds.length})'),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHistoryList(ColorScheme cs, List<Map<String, dynamic>> items) {
|
||||
Widget _buildHistoryList(
|
||||
ColorScheme cs,
|
||||
List<Map<String, dynamic>> items, {
|
||||
bool isTeacherQuizzes = false,
|
||||
}) {
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: items.length,
|
||||
@@ -1928,11 +2274,22 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
dateStr =
|
||||
'${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
final quizId = item['id'] as String?;
|
||||
final isSelected =
|
||||
_isSelectionMode &&
|
||||
quizId != null &&
|
||||
_selectedQuizIds.contains(quizId);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: cs.surface,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.15)),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? cs.primary
|
||||
: cs.outline.withValues(alpha: 0.15),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: cs.shadow.withValues(alpha: 0.05),
|
||||
@@ -1946,15 +2303,30 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
leading: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.quiz, color: cs.primary, size: 22),
|
||||
),
|
||||
leading: _isSelectionMode
|
||||
? Checkbox(
|
||||
value: isSelected,
|
||||
onChanged: (_) {
|
||||
setState(() {
|
||||
if (quizId != null) {
|
||||
if (_selectedQuizIds.contains(quizId)) {
|
||||
_selectedQuizIds.remove(quizId);
|
||||
} else {
|
||||
_selectedQuizIds.add(quizId);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: cs.primary.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(Icons.quiz, color: cs.primary, size: 22),
|
||||
),
|
||||
title: Text(
|
||||
matName,
|
||||
maxLines: 2,
|
||||
@@ -1979,21 +2351,60 @@ class _QuizListPageState extends State<QuizListPage>
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: Icon(
|
||||
Icons.chevron_right,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 18,
|
||||
),
|
||||
onTap: () {
|
||||
final rawJson =
|
||||
item['quizJson'] as String? ?? item['quizText'] as String?;
|
||||
if (rawJson != null && rawJson.isNotEmpty) {
|
||||
_showQuizFromHistory(matName, rawJson);
|
||||
} else {
|
||||
// Quiz sem JSON guardado (ex: quiz do professor) — mostrar resultado
|
||||
_showHistoryResultDialog(item, matName);
|
||||
}
|
||||
},
|
||||
trailing: _isSelectionMode
|
||||
? null
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: cs.onSurfaceVariant,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.red,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => _deleteQuizFromHistory(item),
|
||||
tooltip: 'Eliminar',
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: _isSelectionMode
|
||||
? () {
|
||||
if (quizId != null) {
|
||||
setState(() {
|
||||
if (_selectedQuizIds.contains(quizId)) {
|
||||
_selectedQuizIds.remove(quizId);
|
||||
} else {
|
||||
_selectedQuizIds.add(quizId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
: () {
|
||||
if (isTeacherQuizzes) {
|
||||
// Teacher quizzes: only show results, don't allow retake
|
||||
_showHistoryResultDialog(item, matName);
|
||||
} else {
|
||||
// AI-generated quizzes: allow retake
|
||||
final rawJson =
|
||||
item['quizJson'] as String? ??
|
||||
item['quizText'] as String?;
|
||||
if (rawJson != null && rawJson.isNotEmpty) {
|
||||
_showQuizFromHistory(
|
||||
matName,
|
||||
rawJson,
|
||||
historyDocId: item['id'] as String?,
|
||||
);
|
||||
} else {
|
||||
_showHistoryResultDialog(item, matName);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -2022,10 +2433,12 @@ class _InteractiveQuizSheet extends StatefulWidget {
|
||||
final String title;
|
||||
final List<_QuizQuestion> questions;
|
||||
final String? historyDocId;
|
||||
final VoidCallback? onQuizCompleted;
|
||||
const _InteractiveQuizSheet({
|
||||
required this.title,
|
||||
required this.questions,
|
||||
this.historyDocId,
|
||||
this.onQuizCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -2038,6 +2451,7 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> {
|
||||
bool _submitted = false;
|
||||
bool _saving = false;
|
||||
DateTime? _startTime;
|
||||
bool _hasSaved = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -2046,6 +2460,50 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> {
|
||||
_startTime = DateTime.now();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Save score if quiz is closed without submitting
|
||||
if (!_submitted && !_hasSaved && widget.historyDocId != null) {
|
||||
_savePartialScore().then((_) {
|
||||
if (widget.onQuizCompleted != null) {
|
||||
widget.onQuizCompleted!();
|
||||
}
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _savePartialScore() async {
|
||||
if (_hasSaved) return;
|
||||
_hasSaved = true;
|
||||
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user != null && widget.historyDocId != null) {
|
||||
// Count how many questions were answered
|
||||
final answeredCount = _chosen.where((c) => c != -1).length;
|
||||
|
||||
if (answeredCount > 0) {
|
||||
await FirebaseFirestore.instance
|
||||
.collection('quizHistory')
|
||||
.doc(user.uid)
|
||||
.collection('quizzes')
|
||||
.doc(widget.historyDocId)
|
||||
.update({
|
||||
'score': _score,
|
||||
'totalQuestions': widget.questions.length,
|
||||
'completedAt': FieldValue.serverTimestamp(),
|
||||
});
|
||||
Logger.info(
|
||||
'Saved partial quiz score: $_score/${widget.questions.length}',
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error saving partial quiz score: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _selectOption(int idx) {
|
||||
if (_submitted) return;
|
||||
setState(() => _chosen[_current] = idx);
|
||||
@@ -2072,6 +2530,7 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> {
|
||||
setState(() {
|
||||
_submitted = true;
|
||||
_saving = true;
|
||||
_hasSaved = true;
|
||||
});
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
@@ -2106,6 +2565,11 @@ class _InteractiveQuizSheetState extends State<_InteractiveQuizSheet> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call callback to refresh history
|
||||
if (widget.onQuizCompleted != null) {
|
||||
widget.onQuizCompleted!();
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error saving quiz result: $e');
|
||||
} finally {
|
||||
@@ -2497,11 +2961,13 @@ class _TeacherQuizInteractiveSheet extends StatefulWidget {
|
||||
final String quizId;
|
||||
final List<_QuizQuestion> questions;
|
||||
final String? materialName;
|
||||
final VoidCallback? onQuizCompleted;
|
||||
const _TeacherQuizInteractiveSheet({
|
||||
required this.title,
|
||||
required this.quizId,
|
||||
required this.questions,
|
||||
this.materialName,
|
||||
this.onQuizCompleted,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -2607,6 +3073,11 @@ class _TeacherQuizInteractiveSheetState
|
||||
await GamificationService.recordStudyTime(user.uid, elapsedMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
// Call callback to refresh history
|
||||
if (widget.onQuizCompleted != null) {
|
||||
widget.onQuizCompleted!();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error submitting teacher quiz result: $e');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -20,10 +19,14 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
bool _loading = true;
|
||||
String _userRole = '';
|
||||
|
||||
// Disciplina seleccionada (null = vista de disciplinas)
|
||||
String? _selectedDisciplineId;
|
||||
// Turma seleccionada (null = vista de turmas)
|
||||
String? _selectedClassId;
|
||||
Map<String, String> _classNames = {}; // classId → name
|
||||
|
||||
// Search and filter
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
DateTime? _selectedDate;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -31,6 +34,24 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
_loadQuizHistory();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _selectDate() async {
|
||||
final DateTime? picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDate ?? DateTime.now(),
|
||||
firstDate: DateTime(2020),
|
||||
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => _selectedDate = picked);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadUserRole() async {
|
||||
final user = AuthService.currentUser;
|
||||
if (user != null) {
|
||||
@@ -302,7 +323,7 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
return result ?? false;
|
||||
}
|
||||
|
||||
Map<String, List<Map<String, dynamic>>> _groupByDiscipline() {
|
||||
Map<String, List<Map<String, dynamic>>> _groupByClass() {
|
||||
final Map<String, List<Map<String, dynamic>>> groups = {};
|
||||
for (final quiz in _quizHistory) {
|
||||
final quizClassIds = (quiz['classIds'] as List?)?.cast<String>() ?? [];
|
||||
@@ -316,15 +337,69 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
return groups;
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _getFilteredQuizzes() {
|
||||
final searchQuery = _searchController.text.toLowerCase();
|
||||
|
||||
return _quizHistory.where((quiz) {
|
||||
// Filter by date if selected
|
||||
if (_selectedDate != null) {
|
||||
final quizDate = quiz['createdAt'] as Timestamp?;
|
||||
if (quizDate == null) return false;
|
||||
final quizDateTime = quizDate.toDate();
|
||||
if (quizDateTime.year != _selectedDate!.year ||
|
||||
quizDateTime.month != _selectedDate!.month ||
|
||||
quizDateTime.day != _selectedDate!.day) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by search query (class name or quiz title)
|
||||
if (searchQuery.isNotEmpty) {
|
||||
final title = (quiz['title'] as String? ?? '').toLowerCase();
|
||||
final quizClassIds = (quiz['classIds'] as List?)?.cast<String>() ?? [];
|
||||
bool matchesSearch = title.contains(searchQuery);
|
||||
|
||||
for (final classId in quizClassIds) {
|
||||
if (_classNames.containsKey(classId)) {
|
||||
final className = _classNames[classId]!.toLowerCase();
|
||||
if (className.contains(searchQuery)) {
|
||||
matchesSearch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
Map<String, List<Map<String, dynamic>>> _getFilteredGroups() {
|
||||
final filteredQuizzes = _getFilteredQuizzes();
|
||||
final Map<String, List<Map<String, dynamic>>> groups = {};
|
||||
for (final quiz in filteredQuizzes) {
|
||||
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,
|
||||
canPop: _selectedClassId == null,
|
||||
onPopInvokedWithResult: (didPop, _) {
|
||||
if (!didPop && _selectedDisciplineId != null) {
|
||||
setState(() => _selectedDisciplineId = null);
|
||||
if (!didPop && _selectedClassId != null) {
|
||||
setState(() => _selectedClassId = null);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
@@ -339,8 +414,8 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
if (_userRole == 'teacher' && _selectedDisciplineId != null) {
|
||||
setState(() => _selectedDisciplineId = null);
|
||||
if (_userRole == 'teacher' && _selectedClassId != null) {
|
||||
setState(() => _selectedClassId = null);
|
||||
return;
|
||||
}
|
||||
if (Navigator.of(context).canPop()) {
|
||||
@@ -354,6 +429,44 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
onPressed: _selectDate,
|
||||
tooltip: 'Filtrar por data',
|
||||
),
|
||||
if (_selectedDate != null)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
setState(() => _selectedDate = null);
|
||||
},
|
||||
tooltip: 'Limpar filtro',
|
||||
),
|
||||
],
|
||||
bottom: _selectedClassId == null
|
||||
? PreferredSize(
|
||||
preferredSize: const Size.fromHeight(60),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Pesquisar turma...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) => setState(() {}),
|
||||
),
|
||||
),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -365,15 +478,15 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
),
|
||||
child: _loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: _quizHistory.isEmpty
|
||||
: _getFilteredQuizzes().isEmpty
|
||||
? _buildEmptyState()
|
||||
: _userRole == 'teacher'
|
||||
? _buildTeacherBody(cs)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _quizHistory.length,
|
||||
itemCount: _getFilteredQuizzes().length,
|
||||
itemBuilder: (context, index) {
|
||||
final quiz = _quizHistory[index];
|
||||
final quiz = _getFilteredQuizzes()[index];
|
||||
return _buildQuizCard(quiz)
|
||||
.animate()
|
||||
.slideX(duration: const Duration(milliseconds: 300))
|
||||
@@ -386,16 +499,16 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
}
|
||||
|
||||
Widget _buildTeacherBody(ColorScheme cs) {
|
||||
final groups = _groupByDiscipline();
|
||||
final groups = _searchController.text.isNotEmpty || _selectedDate != null
|
||||
? _getFilteredGroups()
|
||||
: _groupByClass();
|
||||
|
||||
// Vista de quizzes de uma disciplina
|
||||
if (_selectedDisciplineId != null) {
|
||||
final quizzes = groups[_selectedDisciplineId] ?? [];
|
||||
final disciplineName =
|
||||
_classNames[_selectedDisciplineId] ??
|
||||
(_selectedDisciplineId == '__geral__'
|
||||
? 'Geral'
|
||||
: _selectedDisciplineId!);
|
||||
// Vista de quizzes de uma turma
|
||||
if (_selectedClassId != null) {
|
||||
final quizzes = groups[_selectedClassId] ?? [];
|
||||
final className =
|
||||
_classNames[_selectedClassId] ??
|
||||
(_selectedClassId == '__geral__' ? 'Geral' : _selectedClassId!);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -405,12 +518,12 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_back, color: cs.onSurface),
|
||||
onPressed: () => setState(() => _selectedDisciplineId = null),
|
||||
onPressed: () => setState(() => _selectedClassId = null),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
disciplineName,
|
||||
className,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@@ -445,19 +558,19 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
);
|
||||
}
|
||||
|
||||
// Vista de disciplinas
|
||||
final disciplineIds = groups.keys.toList();
|
||||
// Vista de turmas
|
||||
final classIds = groups.keys.toList();
|
||||
return ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: disciplineIds.length,
|
||||
itemCount: classIds.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;
|
||||
final cId = classIds[i];
|
||||
final cName = _classNames[cId] ?? (cId == '__geral__' ? 'Geral' : cId);
|
||||
final count = groups[cId]!.length;
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => setState(() => _selectedDisciplineId = dId),
|
||||
onTap: () => setState(() => _selectedClassId = cId),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
@@ -489,7 +602,7 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
dName,
|
||||
cName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@@ -631,7 +744,7 @@ class _QuizManagementPageState extends State<QuizManagementPage> {
|
||||
),
|
||||
if (_userRole == 'teacher' &&
|
||||
quiz['classIds'] != null &&
|
||||
_selectedDisciplineId == null) ...[
|
||||
_selectedClassId == null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
|
||||
@@ -164,15 +164,67 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect if a material is mathematics-based
|
||||
bool _isMathematicsSubject(Map<String, String> material) {
|
||||
final matName = (material['name'] ?? '').toLowerCase();
|
||||
final classId = material['classId'];
|
||||
String className = '';
|
||||
|
||||
// Get class name if classId is available
|
||||
if (classId != null && _classNamesMap.containsKey(classId)) {
|
||||
className = _classNamesMap[classId]!.toLowerCase();
|
||||
}
|
||||
|
||||
// Keywords for mathematics
|
||||
final mathKeywords = [
|
||||
'matemática',
|
||||
'math',
|
||||
'álgebra',
|
||||
'geometria',
|
||||
'cálculo',
|
||||
'estatística',
|
||||
'trigonometria',
|
||||
'função',
|
||||
'equação',
|
||||
'fração',
|
||||
'raiz',
|
||||
'potência',
|
||||
'derivada',
|
||||
'integral',
|
||||
'número',
|
||||
'gráfico',
|
||||
'fórmula',
|
||||
'matriz',
|
||||
'vetor',
|
||||
'probabilidade',
|
||||
'percentagem',
|
||||
'ângulo',
|
||||
'triângulo',
|
||||
'quadrado',
|
||||
'círculo',
|
||||
'volume',
|
||||
'área',
|
||||
'perímetro',
|
||||
];
|
||||
|
||||
// Check if material name or class name contains math keywords
|
||||
final combinedText = '$matName $className';
|
||||
return mathKeywords.any((keyword) => combinedText.contains(keyword));
|
||||
}
|
||||
|
||||
Future<void> _generateQuiz(Map<String, String> material) async {
|
||||
setState(() => _generatingForId = material['id']);
|
||||
try {
|
||||
final matId = material['id']!;
|
||||
final matName = material['name'] ?? 'Material';
|
||||
final isMathematics = _isMathematicsSubject(material);
|
||||
|
||||
final pdfContext = await MaterialsRAGService.getRelevantChunks(
|
||||
userQuery: 'conteúdo geral resumo tópicos principais',
|
||||
userQuery: 'todos os exercícios todos os tópicos completo',
|
||||
selectedMaterialIds: [matId],
|
||||
maxChunks: 20, // Aumentar para cobrir todo o documento
|
||||
filterTableData:
|
||||
isMathematics, // Filtrar dados de tabela para matemática
|
||||
);
|
||||
|
||||
if (pdfContext.isEmpty) {
|
||||
@@ -180,7 +232,10 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
return;
|
||||
}
|
||||
|
||||
final numQuestions = 5 + Random().nextInt(16); // 5..20
|
||||
final numQuestions = isMathematics
|
||||
? 10 +
|
||||
Random().nextInt(11) // 10..20 para matemática
|
||||
: 5 + Random().nextInt(16); // 5..20 para outras matérias
|
||||
final prompt =
|
||||
'Usa APENAS o seguinte contexto para criar um quiz. Não uses conhecimento externo.\n\n'
|
||||
'$pdfContext\n\n'
|
||||
@@ -190,7 +245,10 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
'[{"q":"Pergunta aqui","opts":["A) opção","B) opção","C) opção","D) opção"],"ans":0,"exp":"Explicação breve da resposta correcta"},...}]\n'
|
||||
'ans é o índice (0-3) da opção correcta.';
|
||||
|
||||
final raw = await RAGAIService.generateQuiz(prompt);
|
||||
final raw = await RAGAIService.generateQuiz(
|
||||
prompt,
|
||||
isMathematics: isMathematics,
|
||||
);
|
||||
final questions = _parseQuizJson(raw);
|
||||
|
||||
if (questions.isEmpty) {
|
||||
@@ -358,6 +416,73 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
return groups;
|
||||
}
|
||||
|
||||
Future<void> _deleteQuiz(String quizId, String quizTitle) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Eliminar Quiz'),
|
||||
content: Text('Tem certeza que deseja eliminar o quiz "$quizTitle"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancelar'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||
child: const Text('Eliminar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
try {
|
||||
final user = FirebaseAuth.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
// Delete from teacherQuizzes
|
||||
await FirebaseFirestore.instance
|
||||
.collection('teacherQuizzes')
|
||||
.doc(quizId)
|
||||
.delete();
|
||||
|
||||
// Also delete from student history
|
||||
final historySnapshot = await FirebaseFirestore.instance
|
||||
.collection('quizHistory')
|
||||
.where('quizId', isEqualTo: quizId)
|
||||
.get();
|
||||
|
||||
for (final doc in historySnapshot.docs) {
|
||||
await doc.reference.delete();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_history.removeWhere((item) => item['id'] == quizId);
|
||||
});
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Quiz eliminado com sucesso!'),
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildHistoryQuizTile(Map<String, dynamic> item, ColorScheme cs) {
|
||||
final name = (item['materialName'] as String? ?? 'Material')
|
||||
.replaceAll('.pdf', '')
|
||||
@@ -409,7 +534,19 @@ class _TeacherQuizPageState extends State<TeacherQuizPage>
|
||||
style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant),
|
||||
)
|
||||
: null,
|
||||
trailing: Icon(Icons.bar_chart, color: cs.onSurfaceVariant),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.bar_chart, color: cs.onSurfaceVariant),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete_outline, color: Colors.red),
|
||||
onPressed: () =>
|
||||
_deleteQuiz(item['id'], item['materialName'] ?? 'Quiz'),
|
||||
tooltip: 'Eliminar Quiz',
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _showResultsPopup(item),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user