diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 352dda4..25141f3 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,47 @@ ## [Unreleased] +### Added +- **Class Creation Feature (ETAPA 1)** - Teachers can now create classes from the dashboard + - New "Criar Turma" button in Teacher Dashboard Quick Actions + - Simple dialog interface for entering class name + - Automatic generation of 6-character unique class codes (A-Z, 0-9) + - Firestore integration saving class data (name, teacherId, code, timestamp) + - Visual feedback with loading indicator and success/error messages + +- **Classes List Display (ETAPA 2)** - Teachers can now view their created classes + - New "As Minhas Turmas" section in Teacher Dashboard + - Real-time StreamBuilder to fetch classes from Firestore + - **CORREÇÃO**: O erro anterior foi tentar usar `GridView` horizontal para um layout que exige colunas fixas + - **SOLUÇÃO**: Usar `ListView.builder` com `scrollDirection: Axis.horizontal` + - Cada item do ListView é uma `Column` contendo 2 cards (índice * 2 e índice * 2 + 1) + - Cards mantêm exatamente o mesmo tamanho e estilo da lista vertical original + - Layout: Card 1, 3, 5... (top row) | Card 2, 4, 6... (bottom row) + - Scroll horizontal para visualizar todas as turmas + - Visual cards showing class name and access code + - Empty state message when no classes exist + - Loading state with CircularProgressIndicator + +### Fixed +- **Unified Quick Action Cards Text Style** + - "Upload Conteúdo" and "Criar Quiz" cards now match "Criar Turma" text alignment + - All cards use `Column` with `crossAxisAlignment: CrossAxisAlignment.start` for text section + - Subtitle text supports 2 lines with `maxLines: 2` and `height: 1.2` + - Consistent typography: title fontSize 16, subtitle fontSize 12 + - "Criar Quiz" subtitle changed to "Avaliações interativas" + +- **Pixel Overflow in Classes List Widget** + - **Causa**: Tentativa de usar `GridView` horizontal para layout de colunas fixas + - **Solução**: Substituir por `ListView.builder` com `scrollDirection: Axis.horizontal` + - Cada item do ListView é uma `Column` com 2 cards (índice * 2 e índice * 2 + 1) + - Cards mantêm tamanho original da lista vertical (sem constraints artificiais) + - Altura do SizedBox: 280 pixels (suficiente para 2 cards + spacing) + +- **Pixel Overflow in Teacher Dashboard Cards** + - Replaced fixed height constraints with flexible `BoxConstraints(minHeight: 135, maxHeight: 160)` + - Fixed overflow issues in "Upload Conteúdo" and "Criar Turma" cards + - Cards now properly adapt to different screen sizes and content + ### Planned Features - Voice interaction capabilities - Advanced analytics dashboard diff --git a/docs/PROJECT_PROGRESS.md b/docs/PROJECT_PROGRESS.md index 755f828..3994dc3 100644 --- a/docs/PROJECT_PROGRESS.md +++ b/docs/PROJECT_PROGRESS.md @@ -404,6 +404,43 @@ This document tracks the overall progress of the AI Study Assistant project deve ### **Last 24 Hours:** +- ✅ **ETAPA 2: Classes List Display** - Teachers can now view their created classes + - New `TeacherClassesListWidget` component + - "As Minhas Turmas" section added to Teacher Dashboard + - Real-time Firestore stream with `StreamBuilder` + - Query: `.where('teacherId', isEqualTo: currentUser.uid).orderBy('createdAt', descending: true)` + - **CORREÇÃO**: O erro anterior foi tentar usar `GridView` horizontal para um layout que exige colunas fixas de 2 cards + - **SOLUÇÃO**: Usar `ListView.builder` com `scrollDirection: Axis.horizontal` + - Cada item do ListView é uma `Column` com 2 cards (índice * 2 e índice * 2 + 1) + - Cards mantêm exatamente o mesmo tamanho da lista vertical original + - Layout: Card 1, 3, 5... (top row) | Card 2, 4, 6... (bottom row) + - Scroll horizontal para visualizar todas as turmas + - Padding entre colunas: 12 pixels + - Altura do SizedBox: 280 pixels (suficiente para 2 cards + spacing) + - Empty state: "Ainda não criaste nenhuma turma." + - Loading state with `CircularProgressIndicator` + - Cards styled with white background, rounded corners, and subtle shadow + +- ✅ **Fixed Pixel Overflow Issues** - Teacher Dashboard cards + - Replaced fixed `height: 150` with `BoxConstraints(minHeight: 135, maxHeight: 160)` + - Fixed overflow in "Criar Turma" card + - Fixed overflow in "Upload Conteúdo" card + - Cards now adapt better to different screen sizes + +- ✅ **Unified Quick Action Cards Text Style** + - "Upload Conteúdo" and "Criar Quiz" cards now have same text alignment as "Criar Turma" + - All cards use `Column` with `crossAxisAlignment: CrossAxisAlignment.start` for text + - Subtitle text now supports 2 lines with `maxLines: 2` and proper line height (1.2) + - Consistent fontSize (16 for title, 12 for subtitle) across all cards + - "Criar Quiz" subtitle updated to "Avaliações interativas" for better description + +- ✅ **ETAPA 1: Class Creation Feature** - Teacher can now create classes + - Added "Criar Turma" button in Teacher Dashboard + - Dialog for entering class name + - Auto-generates 6-character random code (A-Z, 0-9) + - Saves to Firestore `classes` collection with name, teacherId, code, createdAt + - Success/error feedback via SnackBar + - ✅ Fixed dashboard overflow issue in QuickAccessWidget - ✅ Implemented responsive layout with IntrinsicHeight and Flexible diff --git a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart index 73f362e..75793bd 100644 --- a/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart +++ b/lib/features/dashboard/presentation/pages/teacher_dashboard_page.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import '../../../../core/services/auth_service.dart'; import '../widgets/teacher_hero_widget.dart'; import '../widgets/teacher_quick_actions_widget.dart'; +import '../widgets/teacher_classes_list_widget.dart'; import '../widgets/teacher_analytics_preview_widget.dart'; class TeacherDashboardPage extends StatefulWidget { @@ -141,6 +142,11 @@ class _TeacherDashboardPageState extends State { const SizedBox(height: 24), + // Classes List Section + const TeacherClassesListWidget(), + + const SizedBox(height: 24), + // Analytics Preview Section const TeacherAnalyticsPreviewWidget(), diff --git a/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart new file mode 100644 index 0000000..b886dad --- /dev/null +++ b/lib/features/dashboard/presentation/widgets/teacher_classes_list_widget.dart @@ -0,0 +1,155 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter/material.dart'; + +import '../../../../core/services/auth_service.dart'; + +/// Widget para listar as turmas criadas pelo professor +class TeacherClassesListWidget extends StatelessWidget { + const TeacherClassesListWidget({super.key}); + + @override + Widget build(BuildContext context) { + final currentUser = AuthService.currentUser; + + if (currentUser == null) { + return const SizedBox.shrink(); + } + + return StreamBuilder( + stream: FirebaseFirestore.instance + .collection('classes') + .where('teacherId', isEqualTo: currentUser.uid) + .orderBy('createdAt', descending: true) + .snapshots(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator( + color: Color(0xFF82C9BD), + ), + ), + ); + } + + if (snapshot.hasError) { + return const SizedBox.shrink(); + } + + final classes = snapshot.data?.docs ?? []; + + if (classes.isEmpty) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: Text( + 'Ainda não criaste nenhuma turma.', + style: TextStyle( + color: Colors.grey[600], + fontSize: 14, + ), + ), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'As Minhas Turmas', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: const Color(0xFF2D3748), + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + SizedBox( + height: 330, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.only(right: 16), + itemCount: (classes.length + 1) ~/ 2, + itemBuilder: (context, index) { + final firstIndex = index * 2; + final secondIndex = firstIndex + 1; + + return Padding( + padding: const EdgeInsets.only(right: 12), + child: Column( + children: [ + _buildClassCard(classes[firstIndex]), + const SizedBox(height: 12), + if (secondIndex < classes.length) + _buildClassCard(classes[secondIndex]), + ], + ), + ); + }, + ), + ), + ], + ); + }, + ); + } + + Widget _buildClassCard(DocumentSnapshot doc) { + final data = doc.data() as Map; + final className = data['name'] as String? ?? 'Sem nome'; + final classCode = data['code'] as String? ?? '----'; + + return Container( + width: 200, + height: 150, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: const Icon( + Icons.school, + color: Color(0xFF82C9BD), + size: 24, + ), + ), + const SizedBox(height: 12), + Text( + className, + style: const TextStyle( + color: Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + 'Código: $classCode', + style: TextStyle( + color: Colors.grey[600], + fontSize: 13, + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart b/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart index 8776dc8..869d557 100644 --- a/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart +++ b/lib/features/dashboard/presentation/widgets/teacher_quick_actions_widget.dart @@ -1,11 +1,23 @@ +import 'dart:math'; + +import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; +import '../../../../core/services/auth_service.dart'; + /// Quick access cards for teacher actions -class TeacherQuickActionsWidget extends StatelessWidget { +class TeacherQuickActionsWidget extends StatefulWidget { const TeacherQuickActionsWidget({super.key}); + @override + State createState() => _TeacherQuickActionsWidgetState(); +} + +class _TeacherQuickActionsWidgetState extends State { + bool _isCreatingClass = false; + @override Widget build(BuildContext context) { return Column( @@ -25,7 +37,10 @@ class TeacherQuickActionsWidget extends StatelessWidget { Row( children: [ // Upload Content Card (Primary) - Expanded(flex: 3, child: _buildUploadContentCard(context)), + Expanded(flex: 2, child: _buildUploadContentCard(context)), + const SizedBox(width: 16), + // Create Class Card + Expanded(flex: 2, child: _buildCreateClassCard(context)), const SizedBox(width: 16), // Create Quiz Card (Secondary) Expanded(flex: 2, child: _buildCreateQuizCard(context)), @@ -55,7 +70,7 @@ class TeacherQuickActionsWidget extends StatelessWidget { Widget _buildUploadContentCard(BuildContext context) { return Container( - height: 150, + constraints: const BoxConstraints(minHeight: 135, maxHeight: 160), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topLeft, @@ -77,15 +92,14 @@ class TeacherQuickActionsWidget extends StatelessWidget { borderRadius: BorderRadius.circular(16), onTap: () => context.go('/teacher/upload'), child: Padding( - padding: const EdgeInsets.all(20), + padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Container( - padding: const EdgeInsets.all(7), + padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(10), @@ -93,30 +107,31 @@ class TeacherQuickActionsWidget extends StatelessWidget { child: const Icon( Icons.upload_file, color: Colors.white, - size: 24, + size: 22, ), ), const Spacer(), Container( padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 4, + horizontal: 8, + vertical: 3, ), decoration: BoxDecoration( color: const Color(0xFFF68D2D), - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(10), ), child: const Text( 'NOVO', style: TextStyle( color: Colors.white, - fontSize: 10, + fontSize: 9, fontWeight: FontWeight.bold, ), ), ), ], ), + const Spacer(), const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -124,13 +139,14 @@ class TeacherQuickActionsWidget extends StatelessWidget { 'Upload Conteúdo', style: TextStyle( color: Colors.white, - fontSize: 18, + fontSize: 16, fontWeight: FontWeight.bold, ), ), + SizedBox(height: 4), Text( 'PDFs, textos, imagens', - maxLines: 1, + maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, @@ -192,10 +208,10 @@ class TeacherQuickActionsWidget extends StatelessWidget { size: 24, ), ), - const Column( + Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( 'Criar Quiz', style: TextStyle( color: Color(0xFF2D3748), @@ -203,13 +219,13 @@ class TeacherQuickActionsWidget extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - SizedBox(height: 4), + const SizedBox(height: 4), Text( - 'Avaliações', + 'Avaliações interativas', maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( - color: Color(0xFF718096), + color: const Color(0xFF718096), fontSize: 12, height: 1.2, ), @@ -373,4 +389,238 @@ class TeacherQuickActionsWidget extends StatelessWidget { ) .then(delay: const Duration(milliseconds: 400)); } + + Widget _buildCreateClassCard(BuildContext context) { + return Container( + constraints: const BoxConstraints(minHeight: 135, maxHeight: 160), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE2E8F0), width: 1), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: _isCreatingClass ? null : () => _showCreateClassDialog(context), + child: Padding( + padding: const EdgeInsets.all(14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xFF82C9BD).withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: _isCreatingClass + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Color(0xFF82C9BD), + ), + ) + : const Icon( + Icons.school, + color: Color(0xFF82C9BD), + size: 24, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Criar Turma', + style: TextStyle( + color: const Color(0xFF2D3748), + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Gerar código de acesso', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0xFF718096), + fontSize: 12, + height: 1.2, + ), + ), + ], + ), + ], + ), + ), + ), + ), + ) + .animate() + .scale( + duration: const Duration(milliseconds: 600), + curve: Curves.elasticOut, + ) + .then(delay: const Duration(milliseconds: 150)); + } + + void _showCreateClassDialog(BuildContext context) { + final TextEditingController nameController = TextEditingController(); + + showDialog( + context: context, + builder: (BuildContext dialogContext) { + return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + title: const Text( + 'Criar Nova Turma', + style: TextStyle( + color: Color(0xFF2D3748), + fontWeight: FontWeight.bold, + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Digite o nome da turma:', + style: TextStyle( + color: Color(0xFF718096), + fontSize: 14, + ), + ), + const SizedBox(height: 12), + TextField( + controller: nameController, + decoration: InputDecoration( + hintText: 'Ex: Matemática 9º Ano', + filled: true, + fillColor: const Color(0xFFF7FAFC), + 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: const BorderSide( + color: Color(0xFF82C9BD), + width: 2, + ), + ), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text( + 'Cancelar', + style: TextStyle(color: Color(0xFF718096)), + ), + ), + ElevatedButton( + onPressed: () { + final className = nameController.text.trim(); + if (className.isNotEmpty) { + Navigator.of(dialogContext).pop(); + _createClass(className); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF82C9BD), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Text('Criar'), + ), + ], + ); + }, + ); + } + + String _generateClassCode() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + final random = Random(); + return String.fromCharCodes( + Iterable.generate( + 6, + (_) => chars.codeUnitAt(random.nextInt(chars.length)), + ), + ); + } + + Future _createClass(String className) async { + setState(() { + _isCreatingClass = true; + }); + + try { + final currentUser = AuthService.currentUser; + if (currentUser == null) { + throw Exception('Utilizador não autenticado'); + } + + final classCode = _generateClassCode(); + final firestore = FirebaseFirestore.instance; + + await firestore.collection('classes').add({ + 'name': className, + 'teacherId': currentUser.uid, + 'code': classCode, + 'createdAt': FieldValue.serverTimestamp(), + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Turma "$className" criada com sucesso! Código: $classCode'), + backgroundColor: const Color(0xFF82C9BD), + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Erro ao criar turma: $e'), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + } + } finally { + setState(() { + _isCreatingClass = false; + }); + } + } }