import 'dart:convert'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import '../../../../core/services/materials_rag_service.dart'; import '../../../../core/services/rag_ai_service.dart'; import '../../../../core/utils/logger.dart'; // ─── Modelo de dados (partilhado internamente) ──────────────────────────────── class _QuizQuestion { String question; List options; int correctIndex; String explanation; _QuizQuestion({ required this.question, required this.options, required this.correctIndex, required this.explanation, }); Map toJson() => { 'q': question, 'opts': options, 'ans': correctIndex, 'exp': explanation, }; static _QuizQuestion? fromMap(dynamic e) { if (e is! Map) return null; final q = e['q'] as String?; final opts = (e['opts'] as List?)?.cast(); final ans = e['ans'] as int?; final exp = e['exp'] as String? ?? ''; if (q == null || opts == null || ans == null) return null; if (opts.length < 2 || ans < 0 || ans >= opts.length) return null; return _QuizQuestion( question: q, options: opts, correctIndex: ans, explanation: exp, ); } } // ─── Página principal professor ─────────────────────────────────────────────── class TeacherQuizPage extends StatefulWidget { const TeacherQuizPage({super.key}); @override State createState() => _TeacherQuizPageState(); } class _TeacherQuizPageState extends State with SingleTickerProviderStateMixin { late TabController _tabController; List> _materials = []; List> _history = []; List> _teacherClasses = []; bool _loadingMaterials = true; bool _loadingHistory = true; String? _generatingForId; // Disciplina seleccionada no histórico (null = vista de disciplinas) String? _selectedHistoryDisciplineId; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _loadMaterials(); _loadHistory(); _loadTeacherClasses(); } @override void dispose() { _tabController.dispose(); super.dispose(); } Future _loadTeacherClasses() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) return; final snap = await FirebaseFirestore.instance .collection('classes') .where('teacherId', isEqualTo: uid) .orderBy('createdAt', descending: true) .get(); final classes = snap.docs .map((d) => {'id': d.id, 'name': d.data()['name'] as String? ?? d.id}) .toList(); if (mounted) setState(() => _teacherClasses = classes); } catch (e) { Logger.error('Error loading teacher classes: $e'); } } Future _loadMaterials() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) { if (mounted) setState(() => _loadingMaterials = false); return; } final snap = await FirebaseFirestore.instance .collection('materials') .where('teacherId', isEqualTo: uid) .orderBy('createdAt', descending: true) .get(); final mats = snap.docs .where( (d) => (d.data()['fileName'] as String? ?? '') .toLowerCase() .endsWith('.pdf'), ) .map( (d) => { 'id': d.id, 'name': d.data()['fileName'] as String? ?? 'Material', }, ) .toList(); if (mounted) setState(() { _materials = mats; _loadingMaterials = false; }); } catch (e) { Logger.error('Teacher quiz load materials: $e'); if (mounted) setState(() => _loadingMaterials = false); } } Future _loadHistory() async { try { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) { if (mounted) setState(() => _loadingHistory = false); return; } final snap = await FirebaseFirestore.instance .collection('teacherQuizzes') .where('teacherId', isEqualTo: uid) .orderBy('createdAt', descending: true) .limit(30) .get(); final list = snap.docs.map((d) => {'id': d.id, ...d.data()}).toList(); if (mounted) setState(() { _history = list; _loadingHistory = false; }); } catch (e) { Logger.error('Teacher quiz load history: $e'); if (mounted) setState(() => _loadingHistory = false); } } /// Detect if a material is mathematics-based bool _isMathematicsSubject(Map 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)); } void _showQuizCreationOptions(Map material) { final cs = Theme.of(context).colorScheme; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Container( decoration: BoxDecoration( color: cs.surface, borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), ), padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle bar Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: cs.onSurfaceVariant.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 24), Text( 'Criar Quiz', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, color: cs.onSurface, ), ), const SizedBox(height: 8), Text( 'Escolhe como queres criar o quiz para este material:', style: TextStyle( fontSize: 14, color: cs.onSurfaceVariant, ), ), const SizedBox(height: 24), // Option 1: AI _buildOptionCard( context: context, icon: Icons.auto_awesome, title: 'Gerar com IA', subtitle: 'O AI analisa o material e cria perguntas automaticamente', color: cs.primary, onTap: () { Navigator.pop(context); _generateQuiz(material); }, ), const SizedBox(height: 12), // Option 2: Manual _buildOptionCard( context: context, icon: Icons.edit_note, title: 'Criar Manualmente', subtitle: 'Cria as perguntas e respostas do zero', color: cs.secondary, onTap: () { Navigator.pop(context); _createManualQuiz(material); }, ), const SizedBox(height: 24), ], ), ), ); } Widget _buildOptionCard({ required BuildContext context, required IconData icon, required String title, required String subtitle, required Color color, required VoidCallback onTap, }) { final cs = Theme.of(context).colorScheme; return Material( color: Colors.transparent, borderRadius: BorderRadius.circular(16), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( border: Border.all(color: color.withValues(alpha: 0.3)), borderRadius: BorderRadius.circular(16), color: color.withValues(alpha: 0.05), ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: color, size: 28), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: cs.onSurface, ), ), const SizedBox(height: 4), Text( subtitle, style: TextStyle( fontSize: 13, color: cs.onSurfaceVariant, ), ), ], ), ), Icon( Icons.arrow_forward_ios, color: color, size: 16, ), ], ), ), ), ); } Future _createManualQuiz(Map material) async { final matName = material['name'] ?? 'Material'; final matId = material['id']!; // Abrir editor com lista vazia de perguntas if (mounted) { final published = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => _QuizEditorPage( materialName: matName, materialId: matId, questions: const [], // Lista vazia para criação manual availableClasses: _teacherClasses, ), ), ); if (published == true) { await _loadHistory(); } } } Future _generateQuiz(Map 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: '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) { if (mounted) _showSnack('Não foi possível aceder ao conteúdo do PDF.'); return; } 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' 'Cria um quiz com EXACTAMENTE $numQuestions perguntas de escolha múltipla sobre o conteúdo acima. Não repitas perguntas.\n' 'Responde SOMENTE com JSON válido, sem texto adicional, sem markdown, sem blocos de código.\n' 'Formato exacto:\n' '[{"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, isMathematics: isMathematics, ); final questions = _parseQuizJson(raw); if (questions.isEmpty) { if (mounted) _showSnack('Não foi possível gerar o quiz. Tenta novamente.'); return; } // Abrir editor — o professor pode rever/editar antes de publicar if (mounted) { final published = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => _QuizEditorPage( materialName: matName, materialId: matId, questions: questions, availableClasses: _teacherClasses, ), ), ); if (published == true) { await _loadHistory(); } } } catch (e) { Logger.error('Error generating teacher quiz: $e'); if (mounted) _showSnack('Erro ao gerar quiz. Tenta novamente.'); } finally { if (mounted) setState(() => _generatingForId = null); } } List<_QuizQuestion> _parseQuizJson(String raw) { try { final start = raw.indexOf('['); final end = raw.lastIndexOf(']'); if (start == -1 || end == -1 || end <= start) return []; final jsonStr = raw.substring(start, end + 1); final decoded = jsonDecode(jsonStr); if (decoded is! List) return []; return decoded .map<_QuizQuestion?>(_QuizQuestion.fromMap) .whereType<_QuizQuestion>() .toList(); } catch (e) { Logger.error('Quiz JSON parse error: $e'); return []; } } void _showSnack(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating), ); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, appBar: AppBar( title: const Text('Criar Quiz'), backgroundColor: cs.surface, foregroundColor: cs.onSurface, elevation: 0, leading: IconButton( icon: const Icon(Icons.arrow_back), onPressed: () { if (_selectedHistoryDisciplineId != null) { setState(() => _selectedHistoryDisciplineId = null); } else { context.go('/teacher-dashboard'); } }, ), bottom: TabBar( controller: _tabController, labelColor: cs.primary, unselectedLabelColor: cs.onSurfaceVariant, indicatorColor: cs.primary, tabs: const [ Tab(text: 'Gerar Quiz'), Tab(text: 'Histórico'), ], ), ), body: TabBarView( controller: _tabController, children: [_buildMaterialsTab(cs), _buildHistoryTab(cs)], ), ); } Widget _buildMaterialsTab(ColorScheme cs) { if (_loadingMaterials) return const Center(child: CircularProgressIndicator()); if (_materials.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.folder_open, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4), ), const SizedBox(height: 16), Text( 'Sem PDFs carregados.', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), ), const SizedBox(height: 8), Text( 'Faz upload de um PDF nos teus materiais primeiro.', textAlign: TextAlign.center, style: TextStyle( color: cs.onSurfaceVariant.withValues(alpha: 0.7), fontSize: 13, ), ), ], ), ), ); } return ListView.separated( padding: const EdgeInsets.all(16), itemCount: _materials.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, i) { final mat = _materials[i]; final isGenerating = _generatingForId == mat['id']; final name = (mat['name'] ?? 'Material') .replaceAll('.pdf', '') .replaceAll('_', ' '); return _MaterialCard( name: name, isGenerating: isGenerating, onTap: isGenerating ? null : () => _showQuizCreationOptions(mat), cs: cs, ); }, ); } Map get _classNamesMap { return {for (final c in _teacherClasses) c['id']!: c['name'] ?? c['id']!}; } Map>> _groupHistoryByDiscipline() { final classNames = _classNamesMap; final Map>> groups = {}; for (final quiz in _history) { final quizClassIds = (quiz['classIds'] as List?)?.cast() ?? []; String? groupId = quizClassIds.cast().firstWhere( (cid) => cid != null && classNames.containsKey(cid), orElse: () => null, ); groupId ??= quizClassIds.isNotEmpty ? quizClassIds.first : '__geral__'; groups.putIfAbsent(groupId, () => []).add(quiz); } return groups; } Future _deleteQuiz(String quizId, String quizTitle) async { final confirmed = await showDialog( 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 item, ColorScheme cs) { final name = (item['materialName'] as String? ?? 'Material') .replaceAll('.pdf', '') .replaceAll('_', ' '); final ts = item['createdAt']; String dateStr = ''; if (ts is Timestamp) { final dt = ts.toDate(); 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')}'; } return Container( 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: ListTile( contentPadding: const EdgeInsets.symmetric(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), ), title: Text( name, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface, ), ), subtitle: dateStr.isNotEmpty ? Text( dateStr, style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), ) : null, 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), ), ); } Widget _buildHistoryTab(ColorScheme cs) { if (_loadingHistory) return const Center(child: CircularProgressIndicator()); if (_history.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.history, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4), ), const SizedBox(height: 16), Text( 'Ainda não criaste nenhum quiz.', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 16), ), ], ), ), ); } final groups = _groupHistoryByDiscipline(); final classNames = _classNamesMap; // Vista de quizzes de uma disciplina if (_selectedHistoryDisciplineId != null) { final quizzes = groups[_selectedHistoryDisciplineId] ?? []; final disciplineName = classNames[_selectedHistoryDisciplineId] ?? (_selectedHistoryDisciplineId == '__geral__' ? 'Geral' : _selectedHistoryDisciplineId!); 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(() => _selectedHistoryDisciplineId = 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.separated( padding: const EdgeInsets.all(16), itemCount: quizzes.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, i) => _buildHistoryQuizTile(quizzes[i], cs), ), ), ], ); } // 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(() => _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.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), ], ), ), ); }, ); } void _showResultsPopup(Map quizDoc) { final quizId = quizDoc['id'] as String; final name = (quizDoc['materialName'] as String? ?? 'Material') .replaceAll('.pdf', '') .replaceAll('_', ' '); showDialog( context: context, builder: (_) => _StudentResultsDialog(quizId: quizId, quizTitle: name), ); } } // ─── Card de material reutilizável ──────────────────────────────────────────── class _MaterialCard extends StatelessWidget { final String name; final bool isGenerating; final VoidCallback? onTap; final ColorScheme cs; const _MaterialCard({ required this.name, required this.isGenerating, required this.onTap, required this.cs, }); @override Widget build(BuildContext context) { return Container( 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: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), leading: Container( width: 44, height: 44, decoration: BoxDecoration( color: cs.secondary.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), ), child: Icon(Icons.picture_as_pdf, color: cs.secondary, size: 22), ), title: Text( name, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface, ), ), subtitle: Text( 'Gerar quiz com IA', style: TextStyle(fontSize: 12, color: cs.onSurfaceVariant), ), trailing: isGenerating ? SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, color: cs.primary, ), ) : Icon(Icons.auto_awesome, color: cs.primary, size: 26), onTap: onTap, ), ); } } // ─── Editor de quiz ────────────────────────────────────────────────────────── class _QuizEditorPage extends StatefulWidget { final String materialName; final String materialId; final List<_QuizQuestion> questions; final List> availableClasses; const _QuizEditorPage({ required this.materialName, required this.materialId, required this.questions, required this.availableClasses, }); @override State<_QuizEditorPage> createState() => _QuizEditorPageState(); } class _QuizEditorPageState extends State<_QuizEditorPage> { late List<_QuizQuestion> _questions; final Set _selectedClassIds = {}; bool _saving = false; @override void initState() { super.initState(); // cópia editável _questions = widget.questions .map( (q) => _QuizQuestion( question: q.question, options: List.from(q.options), correctIndex: q.correctIndex, explanation: q.explanation, ), ) .toList(); // selecionar todas as disciplinas por defeito for (final c in widget.availableClasses) { if (c['id'] != null) _selectedClassIds.add(c['id']!); } } void _addBlankQuestion() { setState(() { _questions.add( _QuizQuestion( question: '', options: ['A) ', 'B) ', 'C) ', 'D) '], correctIndex: 0, explanation: '', ), ); }); } Future _publish() async { setState(() => _saving = true); try { final uid = FirebaseAuth.instance.currentUser?.uid; if (uid == null) return; final jsonStr = jsonEncode(_questions.map((q) => q.toJson()).toList()); await FirebaseFirestore.instance.collection('teacherQuizzes').add({ 'teacherId': uid, 'materialId': widget.materialId, 'materialName': widget.materialName, 'quizJson': jsonStr, 'classIds': _selectedClassIds.toList(), 'createdAt': FieldValue.serverTimestamp(), }); if (mounted) Navigator.of(context).pop(true); } catch (e) { Logger.error('Error publishing teacher quiz: $e'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Erro ao publicar: $e'), behavior: SnackBarBehavior.floating, ), ); } } finally { if (mounted) setState(() => _saving = false); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, appBar: AppBar( title: Text( widget.materialName.replaceAll('.pdf', '').replaceAll('_', ' '), maxLines: 2, overflow: TextOverflow.ellipsis, ), backgroundColor: cs.surface, foregroundColor: cs.onSurface, elevation: 0, actions: [ _saving ? const Padding( padding: EdgeInsets.only(right: 16), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ) : TextButton.icon( onPressed: (widget.availableClasses.isEmpty || _selectedClassIds.isNotEmpty) ? _publish : null, icon: const Icon(Icons.publish), label: const Text('Publicar'), ), ], ), body: ListView.builder( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), itemCount: _questions.length + 2, // +1 disciplinas header +1 add button itemBuilder: (context, i) { if (i == 0) return _buildClassSelector(cs); if (i == _questions.length + 1) { return Padding( padding: const EdgeInsets.only(top: 8), child: OutlinedButton.icon( onPressed: _addBlankQuestion, icon: const Icon(Icons.add), label: const Text('Adicionar Pergunta'), style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), ), ); } final qIdx = i - 1; return Padding( padding: const EdgeInsets.only(bottom: 16), child: _QuestionEditor( index: qIdx, question: _questions[qIdx], cs: cs, onChanged: () => setState(() {}), onDelete: _questions.length > 1 ? () => setState(() => _questions.removeAt(qIdx)) : null, ), ); }, ), ); } Widget _buildClassSelector(ColorScheme cs) { if (widget.availableClasses.isEmpty) return const SizedBox(height: 0); return Container( margin: const EdgeInsets.only(bottom: 16), 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.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.groups, color: cs.primary, size: 20), const SizedBox(width: 8), Text( 'Disciplinas com acesso', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: cs.onSurface, ), ), ], ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: widget.availableClasses.map((c) { final id = c['id']!; final name = c['name'] ?? id; final selected = _selectedClassIds.contains(id); return FilterChip( label: Text( name, style: TextStyle( fontSize: 13, color: selected ? cs.onPrimary : cs.onSurface, ), ), selected: selected, onSelected: (v) => setState( () => v ? _selectedClassIds.add(id) : _selectedClassIds.remove(id), ), selectedColor: cs.primary, checkmarkColor: cs.onPrimary, backgroundColor: cs.surfaceContainerHighest.withValues( alpha: 0.5, ), side: BorderSide( color: selected ? cs.primary : cs.outline.withValues(alpha: 0.3), ), ); }).toList(), ), if (_selectedClassIds.isEmpty) Padding( padding: const EdgeInsets.only(top: 8), child: Text( 'Seleciona pelo menos uma disciplina.', style: TextStyle(fontSize: 12, color: cs.error), ), ), ], ), ); } } // ─── Editor de uma pergunta ─────────────────────────────────────────────────── class _QuestionEditor extends StatefulWidget { final int index; final _QuizQuestion question; final ColorScheme cs; final VoidCallback onChanged; final VoidCallback? onDelete; const _QuestionEditor({ required this.index, required this.question, required this.cs, required this.onChanged, this.onDelete, }); @override State<_QuestionEditor> createState() => _QuestionEditorState(); } class _QuestionEditorState extends State<_QuestionEditor> { late TextEditingController _qCtrl; late TextEditingController _expCtrl; late List _optCtrl; bool _expanded = true; @override void initState() { super.initState(); _qCtrl = TextEditingController(text: widget.question.question); _expCtrl = TextEditingController(text: widget.question.explanation); _optCtrl = widget.question.options .map((o) => TextEditingController(text: o)) .toList(); } @override void dispose() { _qCtrl.dispose(); _expCtrl.dispose(); for (final c in _optCtrl) c.dispose(); super.dispose(); } void _save() { widget.question.question = _qCtrl.text; widget.question.explanation = _expCtrl.text; for (int i = 0; i < _optCtrl.length; i++) { widget.question.options[i] = _optCtrl[i].text; } widget.onChanged(); } @override Widget build(BuildContext context) { final cs = widget.cs; return Container( 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.05), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( children: [ // Header expansível InkWell( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), onTap: () => setState(() => _expanded = !_expanded), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Container( width: 28, height: 28, decoration: BoxDecoration( color: cs.primary.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Center( child: Text( '${widget.index + 1}', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: cs.primary, ), ), ), ), const SizedBox(width: 10), Expanded( child: Text( widget.question.question.isEmpty ? 'Pergunta ${widget.index + 1}' : widget.question.question, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: cs.onSurface, ), ), ), if (widget.onDelete != null) IconButton( icon: Icon( Icons.delete_outline, color: cs.error, size: 18, ), padding: EdgeInsets.zero, constraints: const BoxConstraints(), onPressed: widget.onDelete, tooltip: 'Remover', ), const SizedBox(width: 4), Icon( _expanded ? Icons.expand_less : Icons.expand_more, color: cs.onSurfaceVariant, ), ], ), ), ), if (_expanded) ...[ const Divider(height: 1), Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Pergunta _label('Pergunta', cs), const SizedBox(height: 6), _textField(_qCtrl, cs, maxLines: 3), const SizedBox(height: 14), // Opções _label('Opções', cs), const SizedBox(height: 6), ...List.generate(widget.question.options.length, (i) { final isCorrect = widget.question.correctIndex == i; return Padding( padding: const EdgeInsets.only(bottom: 8), child: Row( children: [ GestureDetector( onTap: () { setState(() => widget.question.correctIndex = i); widget.onChanged(); }, child: Container( width: 28, height: 28, margin: const EdgeInsets.only(right: 8), decoration: BoxDecoration( shape: BoxShape.circle, color: isCorrect ? cs.primary : cs.surfaceContainerHighest, border: Border.all( color: isCorrect ? cs.primary : cs.outline.withValues(alpha: 0.3), ), ), child: isCorrect ? Icon( Icons.check, size: 16, color: cs.onPrimary, ) : Center( child: Text( String.fromCharCode(65 + i), style: TextStyle( fontSize: 12, color: cs.onSurfaceVariant, ), ), ), ), ), Expanded( child: _textField( _optCtrl[i], cs, hint: 'Opção ${String.fromCharCode(65 + i)}', ), ), ], ), ); }), Text( 'Toca no círculo para marcar a correcta', style: TextStyle(fontSize: 11, color: cs.onSurfaceVariant), ), const SizedBox(height: 14), // Explicação _label('Explicação (quando erra)', cs), const SizedBox(height: 6), _textField( _expCtrl, cs, maxLines: 3, hint: 'Explica porque esta é a resposta correcta...', ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: TextButton.icon( onPressed: _save, icon: const Icon(Icons.save_outlined, size: 18), label: const Text('Guardar'), ), ), ], ), ), ], ], ), ); } Widget _label(String text, ColorScheme cs) => Text( text, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: cs.onSurfaceVariant, ), ); Widget _textField( TextEditingController ctrl, ColorScheme cs, { int maxLines = 1, String? hint, }) => TextField( controller: ctrl, maxLines: maxLines, style: TextStyle(fontSize: 14, color: cs.onSurface), decoration: InputDecoration( hintText: hint, hintStyle: TextStyle( color: cs.onSurfaceVariant.withValues(alpha: 0.5), fontSize: 13, ), filled: true, fillColor: cs.surfaceContainerHighest.withValues(alpha: 0.5), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide(color: cs.primary, width: 1.5), ), ), ); } // ─── Popup de resultados por aluno ──────────────────────────────────────────── class _StudentResultsDialog extends StatefulWidget { final String quizId; final String quizTitle; const _StudentResultsDialog({required this.quizId, required this.quizTitle}); @override State<_StudentResultsDialog> createState() => _StudentResultsDialogState(); } class _StudentResultsDialogState extends State<_StudentResultsDialog> { List> _results = []; bool _loading = true; @override void initState() { super.initState(); _load(); } Future _load() async { try { final snap = await FirebaseFirestore.instance .collection('teacherQuizzes') .doc(widget.quizId) .collection('submissions') .orderBy('submittedAt', descending: true) .get(); final results = >[]; for (final doc in snap.docs) { final data = doc.data(); // Tentar resolver nome do aluno final studentId = data['studentId'] as String?; String studentName = data['studentName'] as String? ?? 'Aluno'; if (studentId != null && studentName == 'Aluno') { try { final userDoc = await FirebaseFirestore.instance .collection('users') .doc(studentId) .get(); if (userDoc.exists) { studentName = userDoc.data()?['displayName'] as String? ?? userDoc.data()?['name'] as String? ?? studentName; } } catch (_) {} } results.add({...data, 'studentName': studentName}); } if (mounted) setState(() { _results = results; _loading = false; }); } catch (e) { Logger.error('Error loading quiz submissions: $e'); if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 40), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 480, maxHeight: 520), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Container( padding: const EdgeInsets.fromLTRB(20, 20, 12, 16), decoration: BoxDecoration( color: cs.primary.withValues(alpha: 0.08), borderRadius: const BorderRadius.vertical( top: Radius.circular(20), ), ), child: Row( children: [ Icon(Icons.bar_chart, color: cs.primary), const SizedBox(width: 10), Expanded( child: Text( widget.quizTitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, color: cs.onSurface, ), ), ), IconButton( icon: Icon(Icons.close, color: cs.onSurfaceVariant), onPressed: () => Navigator.of(context).pop(), ), ], ), ), const Divider(height: 1), // Body Flexible( child: _loading ? const Center( child: Padding( padding: EdgeInsets.all(32), child: CircularProgressIndicator(), ), ) : _results.isEmpty ? Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( 'Nenhum aluno submeteu este quiz ainda.', textAlign: TextAlign.center, style: TextStyle(color: cs.onSurfaceVariant), ), ), ) : ListView.separated( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), shrinkWrap: true, itemCount: _results.length, separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (_, i) { final r = _results[i]; final score = r['score'] as int? ?? 0; final total = r['total'] as int? ?? 1; final pct = (score / total * 100).round(); final Color c = pct >= 80 ? const Color(0xFF10B981) : pct >= 50 ? const Color(0xFFF59E0B) : const Color(0xFFEF4444); final ts = r['submittedAt']; String dateStr = ''; if (ts is Timestamp) { final dt = ts.toDate(); dateStr = '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}'; } return Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), decoration: BoxDecoration( color: c.withValues(alpha: 0.07), borderRadius: BorderRadius.circular(12), border: Border.all(color: c.withValues(alpha: 0.2)), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( r['studentName'] as String, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: cs.onSurface, ), ), if (dateStr.isNotEmpty) Text( dateStr, style: TextStyle( fontSize: 11, color: cs.onSurfaceVariant, ), ), ], ), ), Text( '$score/$total', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 16, color: c, ), ), const SizedBox(width: 6), Text( '($pct%)', style: TextStyle(fontSize: 12, color: c), ), ], ), ); }, ), ), ], ), ), ); } }