import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; import 'package:url_launcher/url_launcher.dart'; import '../../../../core/theme/app_theme_extension.dart'; import 'pdf_viewer_page.dart'; /// Página de Gerenciamento de Conteúdo /// Mostra todos os materiais disponíveis separados por disciplinas (turmas) class ContentManagementPage extends StatefulWidget { const ContentManagementPage({super.key}); @override State createState() => _ContentManagementPageState(); } class _ContentManagementPageState extends State { final FirebaseFirestore _firestore = FirebaseFirestore.instance; Map>> _materialsByClass = {}; Map _classNames = {}; bool _isLoading = true; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); List _filteredClassIds = []; int _selectedTabIndex = 0; @override void initState() { super.initState(); _loadMaterials(); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _filterClasses(String query) { setState(() { _searchQuery = query.toLowerCase(); if (_searchQuery.isEmpty) { _filteredClassIds = _classNames.keys.toList(); } else { _filteredClassIds = _classNames.entries .where((entry) => entry.value.toLowerCase().contains(_searchQuery)) .map((entry) => entry.key) .toList(); } _selectedTabIndex = 0; }); } Future _loadMaterials() async { final currentUser = FirebaseAuth.instance.currentUser; if (currentUser == null) { if (mounted) setState(() => _isLoading = false); return; } try { // Buscar turmas onde o aluno está inscrito final enrollSnapshot = await _firestore .collection('enrollments') .where('studentId', isEqualTo: currentUser.uid) .get(); final enrolledClassIds = enrollSnapshot.docs .map((doc) => doc.data()['classId'] as String?) .whereType() .toSet(); if (enrolledClassIds.isEmpty) { if (mounted) setState(() => _isLoading = false); return; } // Buscar nomes das turmas final classNames = {}; final classDocs = await Future.wait( enrolledClassIds.map( (id) => _firestore.collection('classes').doc(id).get(), ), ); for (final doc in classDocs.where((d) => d.exists)) { classNames[doc.id] = doc.data()?['name'] as String? ?? doc.id; } // Buscar materiais dessas turmas (uma por uma para evitar índice) final materialsByClass = >>{}; for (final classId in enrolledClassIds) { final materialsSnapshot = await _firestore .collection('materials') .where('classId', isEqualTo: classId) .get(); for (final doc in materialsSnapshot.docs) { final material = doc.data(); material['id'] = doc.id; if (classNames.containsKey(classId)) { materialsByClass.putIfAbsent(classId, () => []).add(material); } } } if (mounted) { setState(() { _materialsByClass = materialsByClass; _classNames = classNames; _filteredClassIds = classNames.keys.toList(); _isLoading = false; }); } } catch (e) { print('Error loading materials: $e'); if (mounted) setState(() => _isLoading = false); } } Future _viewInApp(String url, String fileName) async { if (url.isEmpty) { _showErrorSnackBar('URL do ficheiro não disponível'); return; } Navigator.push( context, MaterialPageRoute( builder: (context) => PdfViewerPage(url: url, fileName: fileName), ), ); } Future _openFile(String? url, String fileName) async { if (url == null || url.isEmpty) { _showErrorSnackBar('URL do ficheiro não disponível'); return; } try { 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) { _showErrorSnackBar('Erro ao abrir ficheiro: $e'); } } Future _deleteMaterial( String docId, String fileName, String? url, ) async { // Students cannot delete materials _showErrorSnackBar('Apenas professores podem eliminar materiais'); } void _showDeleteConfirmation(String docId, String fileName, String? url) { showDialog( context: context, builder: (dialogContext) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), title: const Text( 'Eliminar Material', style: TextStyle(fontWeight: FontWeight.bold), ), content: Text( 'Tens a certeza que queres eliminar "$fileName"?\nEsta ação não pode ser desfeita.', ), actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Cancelar'), ), ElevatedButton( onPressed: () { Navigator.of(dialogContext).pop(); _deleteMaterial(docId, fileName, url); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: const Text('Eliminar'), ), ], ), ); } void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.check_circle, color: Colors.white), const SizedBox(width: 12), Expanded(child: Text(message)), ], ), backgroundColor: const Color(0xFF10B981), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 2), ), ); } void _showErrorSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Row( children: [ const Icon(Icons.error_outline, color: Colors.white), const SizedBox(width: 12), Expanded(child: Text(message)), ], ), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), duration: const Duration(seconds: 3), ), ); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final themeExtras = AppThemeExtras.of(context); if (_isLoading) { return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: themeExtras.dashboardBackgroundGradient, stops: themeExtras.dashboardGradientStops, ), ), child: const Center(child: CircularProgressIndicator()), ), ); } if (_materialsByClass.isEmpty) { return Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: themeExtras.dashboardBackgroundGradient, stops: themeExtras.dashboardGradientStops, ), ), child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 120, decoration: BoxDecoration( color: cs.surface.withOpacity(0.3), borderRadius: BorderRadius.circular(24), ), child: Icon( Icons.folder_open, color: cs.onSurfaceVariant, size: 64, ), ) .animate() .fadeIn(duration: const Duration(milliseconds: 400)) .scale(), const SizedBox(height: 24), Text( 'Nenhum material disponível', style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ).animate().fadeIn(delay: const Duration(milliseconds: 200)), const SizedBox(height: 8), Text( 'Junta-te a uma disciplina para ver materiais', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), ).animate().fadeIn(delay: const Duration(milliseconds: 300)), ], ), ), ), ); } final defaultTabController = DefaultTabController( length: _filteredClassIds.length, initialIndex: _selectedTabIndex, child: Scaffold( body: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: themeExtras.dashboardBackgroundGradient, stops: themeExtras.dashboardGradientStops, ), ), child: Column( children: [ SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( children: [ Row( 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, ), ), ), ], ), const SizedBox(height: 16), 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, ), ), onChanged: _filterClasses, ), ), ], ), ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 16), decoration: BoxDecoration( color: cs.surface.withOpacity(0.8), borderRadius: BorderRadius.circular(16), ), child: TabBar( isScrollable: _filteredClassIds.length > 3, dividerColor: Colors.transparent, indicatorSize: TabBarIndicatorSize.tab, indicator: BoxDecoration( color: cs.primary, borderRadius: BorderRadius.circular(12), ), labelColor: cs.onPrimary, unselectedLabelColor: cs.onSurfaceVariant, labelStyle: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), unselectedLabelStyle: const TextStyle( fontWeight: FontWeight.normal, fontSize: 14, ), tabs: _filteredClassIds .map( (classId) => Tab( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Text(_classNames[classId] ?? classId), ), ), ) .toList(), ), ), const SizedBox(height: 16), Expanded( child: _filteredClassIds.isEmpty ? Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 120, height: 120, decoration: BoxDecoration( color: cs.surface.withOpacity(0.3), borderRadius: BorderRadius.circular(24), ), child: Icon( Icons.search_off, color: cs.onSurfaceVariant, size: 64, ), ) .animate() .fadeIn( duration: const Duration(milliseconds: 400), ) .scale(), const SizedBox(height: 24), Text( 'Nenhuma turma encontrada', style: TextStyle( color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.bold, ), ).animate().fadeIn( delay: const Duration(milliseconds: 200), ), const SizedBox(height: 8), Text( 'Tenta uma pesquisa diferente', style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, ), ).animate().fadeIn( delay: const Duration(milliseconds: 300), ), ], ), ) : TabBarView( children: _filteredClassIds.map((classId) { final materials = _materialsByClass[classId] ?? []; if (materials.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 100, height: 100, decoration: BoxDecoration( color: cs.surface.withOpacity(0.3), borderRadius: BorderRadius.circular( 20, ), ), child: Icon( Icons.folder_open, color: cs.onSurfaceVariant, size: 48, ), ) .animate() .fadeIn( duration: const Duration( milliseconds: 400, ), ) .scale(), const SizedBox(height: 20), Text( 'Nenhum material nesta turma', style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.bold, ), ).animate().fadeIn( delay: const Duration(milliseconds: 200), ), ], ), ); } return ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: materials.length, separatorBuilder: (_, __) => const SizedBox(height: 12), itemBuilder: (context, index) { final material = materials[index]; return _buildMaterialCard(material, cs) .animate() .fadeIn( delay: Duration(milliseconds: 50 * index), duration: const Duration(milliseconds: 300), ) .slideY( begin: 0.1, end: 0, delay: Duration(milliseconds: 50 * index), ); }, ); }).toList(), ), ), ], ), ), ), ); return defaultTabController; } Widget _buildMaterialCard(Map material, ColorScheme cs) { final fileName = material['fileName'] ?? 'Ficheiro sem nome'; final url = material['url'] as String?; final createdAt = material['createdAt'] as Timestamp?; final extension = path.extension(fileName).toLowerCase(); 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; } else { iconData = Icons.insert_drive_file; iconColor = const Color(0xFF82C9BD); } String formattedDate = 'Data desconhecida'; if (createdAt != null) { formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(createdAt.toDate()); } return Container( decoration: BoxDecoration( color: cs.surface.withOpacity(0.8), borderRadius: BorderRadius.circular(16), border: Border.all(color: cs.outline.withOpacity(0.15), width: 1), boxShadow: [ BoxShadow( color: cs.shadow.withOpacity(0.08), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(16), onTap: url != null ? () => _openFile(url, fileName) : null, child: Padding( padding: const EdgeInsets.all(16), child: Row( children: [ Container( width: 56, height: 56, decoration: BoxDecoration( color: iconColor.withOpacity(0.15), borderRadius: BorderRadius.circular(14), border: Border.all( color: iconColor.withOpacity(0.3), width: 1, ), ), child: Icon(iconData, color: iconColor, size: 28), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( fileName, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 15, color: cs.onSurface, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 6), Row( children: [ Icon( Icons.calendar_today_outlined, size: 12, color: cs.onSurfaceVariant, ), const SizedBox(width: 4), Text( formattedDate, style: TextStyle( color: cs.onSurfaceVariant, fontSize: 12, ), ), ], ), ], ), ), if (url != null) Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: cs.primary.withOpacity(0.12), borderRadius: BorderRadius.circular(12), border: Border.all( color: cs.primary.withOpacity(0.3), width: 1, ), ), child: IconButton( icon: Icon( Icons.visibility_outlined, color: cs.primary, size: 22, ), tooltip: 'Ver na app', onPressed: () => _viewInApp(url, fileName), ), ), const SizedBox(width: 8), Container( width: 44, height: 44, decoration: BoxDecoration( color: cs.secondary.withOpacity(0.12), borderRadius: BorderRadius.circular(12), border: Border.all( color: cs.secondary.withOpacity(0.3), width: 1, ), ), child: IconButton( icon: Icon( Icons.download_outlined, color: cs.secondary, size: 22, ), tooltip: 'Transferir', onPressed: () => _openFile(url, fileName), ), ), ], ), ], ), ), ), ), ); } }