diff --git a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart index b0c75d1..ab20de2 100644 --- a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart @@ -22,9 +22,13 @@ class TeacherDashboardPage extends StatefulWidget { State createState() => _TeacherDashboardPageState(); } -class _TeacherDashboardPageState extends State { +class _TeacherDashboardPageState extends State + with AutomaticKeepAliveClientMixin { String _userName = 'Professor'; + @override + bool get wantKeepAlive => true; + @override void initState() { super.initState(); @@ -36,6 +40,12 @@ class _TeacherDashboardPageState extends State { } } + Future _refreshDashboard() async { + // Clear cached data to force refresh + TeacherDashboardPage.clearCachedUserName(); + await _loadUserData(); + } + Future _checkRoleAndLoadData() async { final user = AuthService.currentUser; if (user == null) { @@ -94,6 +104,7 @@ class _TeacherDashboardPageState extends State { @override Widget build(BuildContext context) { + super.build(context); final themeExtras = AppThemeExtras.of(context); final headerColor = themeExtras.dashboardHeaderTextColor; @@ -107,83 +118,86 @@ class _TeacherDashboardPageState extends State { stops: themeExtras.dashboardGradientStops, ), ), - child: SafeArea( - top: false, - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only( - left: 24.0, - right: 24.0, - bottom: 28.0, - top: 52.0, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header with logout and settings - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Bem-vindo, $_userName!', - style: TextStyle( - color: headerColor, - fontSize: 28, - fontWeight: FontWeight.bold, + child: RefreshIndicator( + onRefresh: _refreshDashboard, + child: SafeArea( + top: false, + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only( + left: 24.0, + right: 24.0, + bottom: 28.0, + top: 52.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with logout and settings + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Bem-vindo, $_userName!', + style: TextStyle( + color: headerColor, + fontSize: 28, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 4), - Text( - 'Painel de Gestão de Conteúdo', - style: TextStyle( - color: headerColor, - fontSize: 16, - fontWeight: FontWeight.w300, + const SizedBox(height: 4), + Text( + 'Painel de Gestão de Conteúdo', + style: TextStyle( + color: headerColor, + fontSize: 16, + fontWeight: FontWeight.w300, + ), ), - ), - ], + ], + ), ), - ), - IconButton( - icon: Icon(Icons.settings, color: headerColor), - onPressed: () { - AppRouter.goToSettings(context); - }, - tooltip: 'Configurações', - ), - IconButton( - icon: Icon(Icons.logout, color: headerColor), - onPressed: () async { - await AuthService.signOut(); - if (mounted) { - context.go('/login'); - } - }, - tooltip: 'Sair', - ), - ], - ), - const SizedBox(height: 32), + IconButton( + icon: Icon(Icons.settings, color: headerColor), + onPressed: () { + AppRouter.goToSettings(context); + }, + tooltip: 'Configurações', + ), + IconButton( + icon: Icon(Icons.logout, color: headerColor), + onPressed: () async { + await AuthService.signOut(); + if (mounted) { + context.go('/login'); + } + }, + tooltip: 'Sair', + ), + ], + ), + const SizedBox(height: 32), - // Hero Section - Class Overview - TeacherHeroWidget(userName: _userName), + // Hero Section - Class Overview + TeacherHeroWidget(userName: _userName), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Quick Actions Section - const TeacherQuickActionsWidget(), + // Quick Actions Section + const TeacherQuickActionsWidget(), - const SizedBox(height: 24), + const SizedBox(height: 24), - // Classes List Section - const TeacherClassesListWidget(), + // Classes List Section + const TeacherClassesListWidget(), - const SizedBox(height: 40), - ], + const SizedBox(height: 40), + ], + ), ), ), ), diff --git a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart index 8ca30c3..9c29cca 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_hero_widget.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; -import '../../../../core/theme/app_theme_extension.dart'; import '../../../../core/services/gamification_service.dart'; import '../../../../core/models/class_stats.dart'; import '../../../../core/services/auth_service.dart'; @@ -9,16 +8,23 @@ import '../../../../core/services/auth_service.dart'; /// Hero section for teacher dashboard showing class overview class TeacherHeroWidget extends StatefulWidget { final String userName; + final VoidCallback? onRefresh; - const TeacherHeroWidget({super.key, required this.userName}); + const TeacherHeroWidget({super.key, required this.userName, this.onRefresh}); @override State createState() => _TeacherHeroWidgetState(); } -class _TeacherHeroWidgetState extends State { +class _TeacherHeroWidgetState extends State + with AutomaticKeepAliveClientMixin { List _classStats = []; + List _cachedClassStats = []; bool _loading = true; + bool _isFirstLoad = true; + + @override + bool get wantKeepAlive => true; @override void initState() { @@ -26,11 +32,24 @@ class _TeacherHeroWidgetState extends State { _loadClassStats(); } - Future _loadClassStats() async { + Future _loadClassStats({bool forceRefresh = false}) async { try { final user = AuthService.currentUser; if (user == null) return; + // Show cached data immediately if available and not forcing refresh + if (_cachedClassStats.isNotEmpty && !forceRefresh) { + setState(() { + _classStats = _cachedClassStats; + _loading = false; + }); + return; // Don't reload if we have cached data + } + + setState(() { + _loading = _isFirstLoad; + }); + // Obter turmas do professor final classesSnapshot = await FirebaseFirestore.instance .collection('classes') @@ -41,10 +60,9 @@ class _TeacherHeroWidgetState extends State { for (final classDoc in classesSnapshot.docs) { final classId = classDoc.id; - // Forçar atualização para obter dados mais recentes final stats = await GamificationService.getClassStats( classId, - forceRefresh: true, + forceRefresh: forceRefresh, ); if (stats != null) { classStatsList.add(stats); @@ -54,7 +72,9 @@ class _TeacherHeroWidgetState extends State { if (mounted) { setState(() { _classStats = classStatsList; + _cachedClassStats = classStatsList; _loading = false; + _isFirstLoad = false; }); } } catch (e) { @@ -62,19 +82,27 @@ class _TeacherHeroWidgetState extends State { if (mounted) { setState(() { _loading = false; + _isFirstLoad = false; }); } } } + /// Public method to refresh data + Future refresh() async { + await _loadClassStats(forceRefresh: true); + } + int get totalStudents => _classStats.fold(0, (sum, stats) => sum + stats.totalStudents); int get activeQuizzes => _classStats.fold(0, (sum, stats) => sum + stats.activeQuizzes); int get uploadedContent => _classStats.fold(0, (sum, stats) => sum + stats.totalContent); - int get studentsNeedingSupport => - _classStats.fold(0, (sum, stats) => sum + stats.studentsNeedingSupport.length); + int get studentsNeedingSupport => _classStats.fold( + 0, + (sum, stats) => sum + stats.studentsNeedingSupport.length, + ); double get classAverageProgress { if (_classStats.isEmpty) return 0.0; final totalProgress = _classStats.fold( @@ -98,6 +126,7 @@ class _TeacherHeroWidgetState extends State { } Widget build(BuildContext context) { + super.build(context); if (_loading) { return Container( margin: const EdgeInsets.only(bottom: 24), @@ -189,72 +218,6 @@ class _TeacherHeroWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Overall Progress - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Flexible( - child: Text( - 'Progresso Médio da Turma', - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: Colors.white, - fontSize: 15, - fontWeight: FontWeight.bold, - ), - ), - ), - Builder( - builder: (context) { - final displayValue = (classAverageProgress * 100) - .toInt(); - print('=== RENDER DEBUG ==='); - print('classAverageProgress: $classAverageProgress'); - print('displayValue: $displayValue%'); - print('=== END RENDER DEBUG ==='); - return Text( - '$displayValue%', - style: const TextStyle( - color: Colors.white, - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ); - }, - ), - ], - ), - const SizedBox(height: 16), - - // Progress Bar - GestureDetector( - onTap: () => _showProgressExplanation(context), - child: Container( - height: 12, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.3), - borderRadius: BorderRadius.circular(6), - ), - child: FractionallySizedBox( - alignment: Alignment.centerLeft, - widthFactor: classAverageProgress, - child: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - AppThemeExtras.of(context).heroProgressStart, - AppThemeExtras.of(context).heroProgressEnd, - ], - ), - borderRadius: BorderRadius.circular(6), - ), - ), - ), - ), - ), - const SizedBox(height: 20), - // Stats Grid IntrinsicHeight( child: Row( @@ -275,14 +238,6 @@ class _TeacherHeroWidgetState extends State { label: 'Conteúdos', ), ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - icon: Icons.warning_amber, - value: '$studentsNeedingSupport', - label: 'Precisam Apoio', - ), - ), ], ), ), @@ -489,24 +444,4 @@ class _TeacherHeroWidgetState extends State { ], ); } - - void _showProgressExplanation(BuildContext context) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Progresso Médio da Turma'), - content: const Text( - 'O progresso médio da turma é calculado com base no domínio dos conceitos por cada aluno. ' - 'Cada aluno tem um nível de domínio para cada conceito (0-100%), e o progresso médio ' - 'é a média de todos esses níveis de domínio em toda a turma.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Entendi'), - ), - ], - ), - ); - } } diff --git a/lib/features/materials/presentation/pages/content_management_page.dart b/lib/features/materials/presentation/pages/content_management_page.dart index 0a4278a..4424686 100644 --- a/lib/features/materials/presentation/pages/content_management_page.dart +++ b/lib/features/materials/presentation/pages/content_management_page.dart @@ -325,80 +325,83 @@ class _ContentManagementPageState extends State { ), child: Column( children: [ - SafeArea( - top: false, - child: Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 20, - top: 52, - ), - child: Column( - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IconButton( - icon: Icon(Icons.arrow_back, color: cs.onSurface), - onPressed: () => Navigator.pop(context), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Gerenciamento de Conteúdo', - style: TextStyle( - color: cs.onSurface, - fontSize: 24, - fontWeight: FontWeight.bold, + Container( + decoration: BoxDecoration(color: const Color(0xFF82C9BD)), + child: SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 20, + top: 52, + ), + child: Column( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + icon: Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Gerenciamento de Conteúdo', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), ), ), - ), - ], - ), - const SizedBox(height: 8), - Container( - decoration: BoxDecoration( - color: cs.surface.withOpacity(0.8), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: cs.outline.withOpacity(0.2), - width: 1, - ), + ], ), - child: TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: 'Procurar turmas...', - hintStyle: TextStyle(color: cs.onSurfaceVariant), - prefixIcon: Icon( - Icons.search, - color: cs.onSurfaceVariant, - ), - suffixIcon: _searchQuery.isNotEmpty - ? IconButton( - icon: Icon( - Icons.clear, - color: cs.onSurfaceVariant, - ), - onPressed: () { - _searchController.clear(); - _filterClasses(''); - }, - ) - : null, - filled: true, - fillColor: Colors.transparent, - border: InputBorder.none, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + const SizedBox(height: 8), + Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withOpacity(0.3), + width: 1, ), ), - onChanged: _filterClasses, + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Procurar turmas...', + hintStyle: TextStyle(color: Colors.grey[600]), + prefixIcon: Icon( + Icons.search, + color: Colors.grey[600], + ), + suffixIcon: _searchQuery.isNotEmpty + ? IconButton( + icon: Icon( + Icons.clear, + color: Colors.grey[600], + ), + onPressed: () { + _searchController.clear(); + _filterClasses(''); + }, + ) + : null, + filled: true, + fillColor: Colors.transparent, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: _filterClasses, + ), ), - ), - ], + ], + ), ), ), ), @@ -570,22 +573,38 @@ class _ContentManagementPageState extends State { final fileName = material['fileName'] ?? 'Ficheiro sem nome'; final url = material['url'] as String?; final createdAt = material['createdAt'] as Timestamp?; + final mimeType = material['mimeType'] as String?; - final extension = path.extension(fileName).toLowerCase(); + // Determine icon based on MIME type, fallback to extension IconData iconData; Color iconColor; - if (extension == '.pdf') { - iconData = Icons.picture_as_pdf; - iconColor = Colors.red; - } else if (extension == '.jpg' || - extension == '.jpeg' || - extension == '.png') { - iconData = Icons.image; - iconColor = Colors.blue; + if (mimeType != null) { + if (mimeType == 'application/pdf') { + iconData = Icons.picture_as_pdf; + iconColor = Colors.red; + } else if (mimeType.startsWith('image/')) { + iconData = Icons.image; + iconColor = Colors.blue; + } else { + iconData = Icons.insert_drive_file; + iconColor = const Color(0xFF82C9BD); + } } else { - iconData = Icons.insert_drive_file; - iconColor = const Color(0xFF82C9BD); + // Fallback to extension for old files without MIME type + final extension = path.extension(fileName).toLowerCase(); + if (extension == '.pdf') { + iconData = Icons.picture_as_pdf; + iconColor = Colors.red; + } else if (extension == '.jpg' || + extension == '.jpeg' || + extension == '.png') { + iconData = Icons.image; + iconColor = Colors.blue; + } else { + iconData = Icons.insert_drive_file; + iconColor = const Color(0xFF82C9BD); + } } String formattedDate = 'Data desconhecida'; @@ -633,15 +652,16 @@ class _ContentManagementPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - fileName, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - color: cs.onSurface, + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + fileName, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 15, + color: cs.onSurface, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Row( diff --git a/lib/features/materials/presentation/pages/teacher_materials_page.dart b/lib/features/materials/presentation/pages/teacher_materials_page.dart index b6a3e8a..0161b98 100644 --- a/lib/features/materials/presentation/pages/teacher_materials_page.dart +++ b/lib/features/materials/presentation/pages/teacher_materials_page.dart @@ -11,6 +11,7 @@ import 'package:path/path.dart' as path; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/services/auth_service.dart'; +import 'pdf_viewer_page.dart'; /// Página de Materiais do Professor /// Tela dedicada para upload e gestão de materiais para a IA @@ -78,10 +79,12 @@ class _TeacherMaterialsPageState extends State { return Scaffold( appBar: AppBar( title: const Text( - 'Materiais da Disciplina', + 'Gerenciamento de conteudo', style: TextStyle(fontWeight: FontWeight.bold), ), elevation: 0, + backgroundColor: const Color(0xFF82C9BD), + foregroundColor: Colors.white, ), body: Container( decoration: BoxDecoration( @@ -116,10 +119,12 @@ class _TeacherMaterialsPageState extends State { child: Scaffold( appBar: AppBar( title: const Text( - 'Materiais da Disciplina', + 'Gerenciamento de conteudo', style: TextStyle(fontWeight: FontWeight.bold), ), elevation: 0, + backgroundColor: const Color(0xFF82C9BD), + foregroundColor: Colors.white, bottom: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight + 60), child: Column( @@ -296,17 +301,32 @@ class _TeacherMaterialsPageState extends State { final fileName = material['fileName'] ?? 'Ficheiro sem nome'; final createdAt = material['createdAt'] as Timestamp?; - final extension = path.extension(fileName).toLowerCase(); - final fileType = extension == '.pdf' - ? 'pdf' - : (extension == '.jpg' || - extension == '.jpeg' || - extension == '.png') - ? 'image' - : 'other'; + final mimeType = material['mimeType'] as String?; + final url = material['url'] as String?; + + // Determine file type from MIME type, fallback to extension + String fileType; + if (mimeType != null) { + if (mimeType == 'application/pdf') { + fileType = 'pdf'; + } else if (mimeType.startsWith('image/')) { + fileType = 'image'; + } else { + fileType = 'other'; + } + } else { + // Fallback to extension for old files without MIME type + final extension = path.extension(fileName).toLowerCase(); + fileType = extension == '.pdf' + ? 'pdf' + : (extension == '.jpg' || + extension == '.jpeg' || + extension == '.png') + ? 'image' + : 'other'; + } final docId = materials[index].id; - final url = material['url'] as String?; return _buildMaterialCard( docId: docId, @@ -699,7 +719,16 @@ class _TeacherMaterialsPageState extends State { .child('materials') .child(cleanFileName); - await ref.putFile(File(filePath)); + // Set content type based on file type parameter + final metadata = SettableMetadata( + contentType: fileType == 'pdf' + ? 'application/pdf' + : fileType == 'image' + ? 'image/jpeg' + : 'application/octet-stream', + ); + + await ref.putFile(File(filePath), metadata); final downloadUrl = await ref.getDownloadURL(); // Criar documento no Firestore @@ -707,6 +736,11 @@ class _TeacherMaterialsPageState extends State { 'teacherId': uid, 'fileName': cleanFileName, 'url': downloadUrl, + 'mimeType': fileType == 'pdf' + ? 'application/pdf' + : fileType == 'image' + ? 'image/jpeg' + : 'application/octet-stream', 'createdAt': FieldValue.serverTimestamp(), }; if (classId != null && classId.isNotEmpty) { @@ -835,31 +869,6 @@ class _TeacherMaterialsPageState extends State { ); } - Future _openFile(String? url, String fileName) async { - if (url == null || url.isEmpty) { - _showErrorSnackBar('URL do ficheiro não disponível'); - return; - } - - try { - print('Opening file: $fileName'); - print('URL: $url'); - final uri = Uri.parse(url); - - final launched = await launchUrl( - uri, - mode: LaunchMode.externalApplication, - ); - - if (!launched) { - _showErrorSnackBar('Não foi possível abrir o ficheiro'); - } - } catch (e) { - print('Error opening file: $e'); - _showErrorSnackBar('Erro ao abrir ficheiro: $e'); - } - } - Future _downloadFile(String? url, String fileName) async { if (url == null || url.isEmpty) { _showErrorSnackBar('URL do ficheiro não disponível'); @@ -887,6 +896,20 @@ class _TeacherMaterialsPageState extends State { } } + Future _viewInApp(String? url, String fileName) async { + if (url == null || url.isEmpty) { + _showErrorSnackBar('URL do ficheiro não disponível'); + return; + } + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PdfViewerPage(url: url, fileName: fileName), + ), + ); + } + Widget _buildMaterialCard({ required String docId, required String fileName, @@ -938,15 +961,16 @@ class _TeacherMaterialsPageState extends State { ), child: Icon(iconData, color: iconColor, size: 28), ), - title: Text( - fileName, - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, - color: Theme.of(context).colorScheme.onSurface, + title: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Text( + fileName, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Theme.of(context).colorScheme.onSurface, + ), ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), subtitle: Text( formattedDate, @@ -962,7 +986,7 @@ class _TeacherMaterialsPageState extends State { icon: const Icon(Icons.visibility_outlined, color: Colors.blue), tooltip: 'Ver', constraints: const BoxConstraints(minWidth: 40, minHeight: 40), - onPressed: () => _openFile(url, fileName), + onPressed: () => _viewInApp(url, fileName), ), IconButton( icon: const Icon(Icons.download_outlined, color: Colors.green), diff --git a/pubspec.lock b/pubspec.lock index 8748a41..9a90773 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1141,13 +1141,13 @@ packages: source: hosted version: "1.17.0" mime: - dependency: transitive + dependency: "direct main" description: name: mime - sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.0.6" mockito: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 097ca46..162d31c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -91,6 +91,7 @@ dependencies: file_selector: ^1.0.3 image_picker: ^1.0.4 syncfusion_flutter_pdf: ^33.2.6 + mime: ^1.0.5 # Markdown rendering flutter_markdown: ^0.6.23