diff --git a/lib/features/materials/presentation/pages/teacher_materials_page.dart b/lib/features/materials/presentation/pages/teacher_materials_page.dart index 0003d5c..d311f24 100644 --- a/lib/features/materials/presentation/pages/teacher_materials_page.dart +++ b/lib/features/materials/presentation/pages/teacher_materials_page.dart @@ -27,8 +27,38 @@ class _TeacherMaterialsPageState extends State { ); final ImagePicker _imagePicker = ImagePicker(); bool _isUploading = false; + List> _classes = []; + List> _filteredClasses = []; + String _searchQuery = ''; - Stream _getMaterialsStream() { + @override + void initState() { + super.initState(); + _loadClasses(); + } + + Future _loadClasses() async { + final currentUser = AuthService.currentUser; + if (currentUser == null) return; + + final snapshot = await _firestore + .collection('classes') + .where('teacherId', isEqualTo: currentUser.uid) + .orderBy('createdAt', descending: true) + .get(); + + if (mounted) { + setState(() { + _classes = snapshot.docs.map((doc) { + final data = doc.data(); + return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)}; + }).toList(); + _filteredClasses = List.from(_classes); + }); + } + } + + Stream _getMaterialsStream(String classId) { final currentUser = AuthService.currentUser; if (currentUser == null) { return const Stream.empty(); @@ -37,64 +67,142 @@ class _TeacherMaterialsPageState extends State { return _firestore .collection('materials') .where('teacherId', isEqualTo: currentUser.uid) - .orderBy('createdAt', descending: true) + .where('classId', isEqualTo: classId) .snapshots(); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text( - 'Materiais da Disciplina', - style: TextStyle(fontWeight: FontWeight.bold), + if (_classes.isEmpty) { + return Scaffold( + appBar: AppBar( + title: const Text( + 'Materiais da Disciplina', + style: TextStyle(fontWeight: FontWeight.bold), + ), + elevation: 0, ), - elevation: 0, - ), - floatingActionButton: _isUploading - ? FloatingActionButton.extended( - onPressed: null, - backgroundColor: const Color(0xFFF68D2D).withOpacity(0.6), - icon: const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Colors.white, - ), - ), - label: const Text( - 'A enviar...', - style: TextStyle(color: Colors.white), - ), - ) - : FloatingActionButton.extended( - onPressed: _showUploadOptions, - backgroundColor: const Color(0xFFF68D2D), - icon: const Icon(Icons.add, color: Colors.white), - label: const Text( - 'Adicionar', - style: TextStyle(color: Colors.white), - ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: Theme.of(context).brightness == Brightness.dark + ? [ + Theme.of(context).colorScheme.primary.withOpacity(0.3), + Theme.of(context).colorScheme.background, + ] + : [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)], + stops: const [0.0, 0.4], + ), + ), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_open, size: 64, color: Colors.grey), + SizedBox(height: 16), + Text('Ainda não tens turmas criadas.'), + ], ), - - body: Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: Theme.of(context).brightness == Brightness.dark - ? [ - Theme.of(context).colorScheme.primary.withOpacity(0.3), - Theme.of(context).colorScheme.background, - ] - : [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)], - stops: const [0.0, 0.4], ), ), - child: SafeArea( - child: StreamBuilder( - stream: _getMaterialsStream(), + ); + } + + return DefaultTabController( + length: _filteredClasses.length, + child: Scaffold( + appBar: AppBar( + title: const Text( + 'Materiais da Disciplina', + style: TextStyle(fontWeight: FontWeight.bold), + ), + elevation: 0, + bottom: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight + 60), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: TextField( + decoration: InputDecoration( + hintText: 'Procurar turma...', + prefixIcon: const Icon(Icons.search), + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onChanged: (value) { + setState(() { + _searchQuery = value.toLowerCase(); + _filteredClasses = _classes.where((c) { + final name = (c['name'] as String).toLowerCase(); + return name.contains(_searchQuery); + }).toList(); + }); + }, + ), + ), + if (_filteredClasses.isNotEmpty) + TabBar( + isScrollable: _filteredClasses.length > 3, + indicatorColor: const Color(0xFFF68D2D), + labelColor: const Color(0xFFF68D2D), + unselectedLabelColor: Theme.of( + context, + ).colorScheme.onSurface, + tabAlignment: TabAlignment.start, + padding: const EdgeInsets.symmetric(horizontal: 8), + tabs: _filteredClasses + .map((c) => Tab(text: c['name'] as String)) + .toList(), + ), + ], + ), + ), + ), + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: Theme.of(context).brightness == Brightness.dark + ? [ + Theme.of(context).colorScheme.primary.withOpacity(0.3), + Theme.of(context).colorScheme.background, + ] + : [const Color(0xFF82C9BD), const Color(0xFFF8F9FA)], + stops: const [0.0, 0.4], + ), + ), + child: TabBarView( + children: _filteredClasses.map((classData) { + final classId = classData['id'] as String; + return _buildClassTab(classId: classId); + }).toList(), + ), + ), + ), + ); + } + + Widget _buildClassTab({required String classId}) { + return SafeArea( + child: Stack( + children: [ + StreamBuilder( + stream: _getMaterialsStream(classId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center( @@ -128,6 +236,18 @@ class _TeacherMaterialsPageState extends State { final materials = snapshot.data?.docs ?? []; + // Sort by createdAt descending on client side + materials.sort((a, b) { + final aData = a.data() as Map?; + final bData = b.data() as Map?; + final aTime = aData?['createdAt'] as Timestamp?; + final bTime = bData?['createdAt'] as Timestamp?; + if (aTime == null && bTime == null) return 0; + if (aTime == null) return 1; + if (bTime == null) return -1; + return bTime.compareTo(aTime); + }); + if (materials.isEmpty) { return Center( child: Column( @@ -167,7 +287,6 @@ class _TeacherMaterialsPageState extends State { materials[index].data() as Map; final fileName = material['fileName'] ?? 'Ficheiro sem nome'; final createdAt = material['createdAt'] as Timestamp?; - // Inferir tipo pela extensão do filename final extension = path.extension(fileName).toLowerCase(); final fileType = extension == '.pdf' ? 'pdf' @@ -179,7 +298,6 @@ class _TeacherMaterialsPageState extends State { final docId = materials[index].id; final url = material['url'] as String?; - final classId = material['classId'] as String?; return _buildMaterialCard( docId: docId, @@ -193,12 +311,42 @@ class _TeacherMaterialsPageState extends State { ); }, ), - ), + Positioned( + right: 16, + bottom: 16, + child: _isUploading + ? FloatingActionButton.extended( + onPressed: null, + backgroundColor: const Color(0xFFF68D2D).withOpacity(0.6), + icon: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ), + label: const Text( + 'A enviar...', + style: TextStyle(color: Colors.white), + ), + ) + : FloatingActionButton.extended( + onPressed: () => _showUploadOptions(classId), + backgroundColor: const Color(0xFFF68D2D), + icon: const Icon(Icons.add, color: Colors.white), + label: const Text( + 'Adicionar', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], ), ); } - void _showUploadOptions() { + void _showUploadOptions(String classId) { final cs = Theme.of(context).colorScheme; showModalBottomSheet( context: context, @@ -242,7 +390,7 @@ class _TeacherMaterialsPageState extends State { subtitle: 'Selecionar ficheiro PDF', onTap: () { Navigator.pop(context); - _selectPDF(); + _selectPDF(classId); }, ), const SizedBox(height: 12), @@ -253,7 +401,7 @@ class _TeacherMaterialsPageState extends State { subtitle: 'Escolher foto existente', onTap: () { Navigator.pop(context); - _selectImageFromGallery(); + _selectImageFromGallery(classId); }, ), const SizedBox(height: 12), @@ -264,7 +412,7 @@ class _TeacherMaterialsPageState extends State { subtitle: 'Tirar foto nova', onTap: () { Navigator.pop(context); - _takePhotoWithCamera(); + _takePhotoWithCamera(classId); }, ), const SizedBox(height: 20), @@ -332,7 +480,105 @@ class _TeacherMaterialsPageState extends State { ); } - Future _selectPDF() async { + Future _showRenameDialog(String originalName) async { + if (!mounted) return null; + + final wantRename = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: const Text( + 'Renomear Ficheiro', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: const Text('Deseja renomear o ficheiro?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(false), + child: const Text('Não'), + ), + ElevatedButton( + onPressed: () => Navigator.of(dialogContext).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF68D2D), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text('Sim'), + ), + ], + ), + ); + + if (wantRename != true || !mounted) return originalName; + + final newName = await showDialog( + context: context, + builder: (dialogContext) { + return StatefulBuilder( + builder: (context, setDialogState) { + String textValue = originalName; + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text( + 'Novo Nome', + style: TextStyle(fontWeight: FontWeight.bold), + ), + content: TextField( + decoration: InputDecoration( + hintText: originalName, + filled: true, + fillColor: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + autofocus: true, + onChanged: (value) => textValue = value, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(null), + child: const Text('Cancelar'), + ), + ElevatedButton( + onPressed: () { + final name = textValue.trim(); + if (name.isNotEmpty) { + Navigator.of(dialogContext).pop(name); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFFF68D2D), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + child: const Text('Confirmar'), + ), + ], + ); + }, + ); + }, + ); + + return newName?.isNotEmpty == true ? newName : originalName; + } + + Future _selectPDF(String classId) async { try { const XTypeGroup pdfTypeGroup = XTypeGroup( label: 'PDFs', @@ -343,10 +589,15 @@ class _TeacherMaterialsPageState extends State { final XFile? file = await openFile(acceptedTypeGroups: [pdfTypeGroup]); if (file == null) return; - await _pickClassAndUpload( + final originalName = path.basename(file.path); + final finalName = await _showRenameDialog(originalName); + if (finalName == null) return; + + await _uploadFile( filePath: file.path, - fileName: path.basename(file.path), + fileName: finalName, fileType: 'pdf', + classId: classId, ); } catch (e) { if (mounted) { @@ -355,7 +606,7 @@ class _TeacherMaterialsPageState extends State { } } - Future _selectImageFromGallery() async { + Future _selectImageFromGallery(String classId) async { try { final XFile? image = await _imagePicker.pickImage( source: ImageSource.gallery, @@ -365,10 +616,15 @@ class _TeacherMaterialsPageState extends State { ); if (image == null) return; - await _pickClassAndUpload( + final originalName = path.basename(image.path); + final finalName = await _showRenameDialog(originalName); + if (finalName == null) return; + + await _uploadFile( filePath: image.path, - fileName: path.basename(image.path), + fileName: finalName, fileType: 'image', + classId: classId, ); } catch (e) { if (mounted) { @@ -377,7 +633,7 @@ class _TeacherMaterialsPageState extends State { } } - Future _takePhotoWithCamera() async { + Future _takePhotoWithCamera(String classId) async { try { final XFile? photo = await _imagePicker.pickImage( source: ImageSource.camera, @@ -387,10 +643,15 @@ class _TeacherMaterialsPageState extends State { ); if (photo == null) return; - await _pickClassAndUpload( + final originalName = path.basename(photo.path); + final finalName = await _showRenameDialog(originalName); + if (finalName == null) return; + + await _uploadFile( filePath: photo.path, - fileName: path.basename(photo.path), + fileName: finalName, fileType: 'image', + classId: classId, ); } catch (e) { if (mounted) { @@ -399,142 +660,6 @@ class _TeacherMaterialsPageState extends State { } } - Future _pickClassAndUpload({ - required String filePath, - required String fileName, - required String fileType, - }) async { - final currentUser = AuthService.currentUser; - if (currentUser == null) { - _showErrorSnackBar('Utilizador não autenticado'); - return; - } - - // Carregar as disciplinas do professor - List> teacherClasses = []; - try { - final snapshot = await _firestore - .collection('classes') - .where('teacherId', isEqualTo: currentUser.uid) - .orderBy('createdAt', descending: true) - .get(); - teacherClasses = snapshot.docs.map((doc) { - final data = doc.data(); - return {'id': doc.id, 'name': (data['name'] as String? ?? doc.id)}; - }).toList(); - } catch (_) {} - - if (!mounted) return; - - // Se o professor não tem disciplinas, fazer upload sem associar disciplina - if (teacherClasses.isEmpty) { - await _uploadFile( - filePath: filePath, - fileName: fileName, - fileType: fileType, - ); - return; - } - - // Mostrar diálogo de seleção de disciplina - String? selectedClassId = await showDialog( - context: context, - builder: (dialogContext) { - String? picked; - return StatefulBuilder( - builder: (context, setDialogState) => AlertDialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - title: const Text( - 'Escolher Disciplina', - style: TextStyle(fontWeight: FontWeight.bold), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Seleciona a disciplina que terá acesso a este material:', - style: TextStyle(fontSize: 14), - ), - const SizedBox(height: 16), - DropdownButtonFormField( - value: picked, - isExpanded: true, - decoration: InputDecoration( - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide.none, - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: Theme.of(context).colorScheme.primary, - width: 2, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - hint: const Text('Seleciona a disciplina'), - items: teacherClasses - .map( - (c) => DropdownMenuItem( - value: c['id'], - child: Text(c['name']!), - ), - ) - .toList(), - onChanged: (value) => setDialogState(() => picked = value), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(null), - child: const Text('Cancelar'), - ), - ElevatedButton( - onPressed: picked == null - ? null - : () => Navigator.of(dialogContext).pop(picked), - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFFF68D2D), - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - child: const Text('Confirmar'), - ), - ], - ), - ); - }, - ); - - // Se cancelou o diálogo, não fazer upload - if (selectedClassId == null) return; - - await _uploadFile( - filePath: filePath, - fileName: fileName, - fileType: fileType, - classId: selectedClassId, - ); - } - Future _uploadFile({ required String filePath, required String fileName, @@ -700,18 +825,6 @@ class _TeacherMaterialsPageState extends State { ); } - Future _getClassName(String classId) async { - try { - final doc = await _firestore.collection('classes').doc(classId).get(); - if (doc.exists) { - return doc.data()?['name'] as String?; - } - return null; - } catch (_) { - return null; - } - } - Widget _buildMaterialCard({ required String docId, required String fileName, @@ -773,58 +886,13 @@ class _TeacherMaterialsPageState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, ), - subtitle: classId != null - ? FutureBuilder( - future: _getClassName(classId), - builder: (context, snap) { - final className = snap.data; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - formattedDate, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 13, - ), - ), - if (className != null) ...[ - const SizedBox(height: 2), - Row( - children: [ - Icon( - Icons.school_outlined, - size: 12, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - className, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - fontSize: 12, - fontWeight: FontWeight.w500, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ], - ); - }, - ) - : Text( - formattedDate, - style: TextStyle( - color: Theme.of(context).colorScheme.onSurfaceVariant, - fontSize: 13, - ), - ), + subtitle: Text( + formattedDate, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: 13, + ), + ), trailing: IconButton( icon: const Icon(Icons.delete_outline, color: Colors.red), tooltip: 'Eliminar',