placeholders removidos e todos os dados reais colocados, com conquistas e tudo

This commit is contained in:
2026-05-17 17:29:47 +01:00
parent 6ba5c837ce
commit 49a7a6fe02
17 changed files with 4688 additions and 142 deletions

View File

@@ -0,0 +1,485 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/theme/app_theme_extension.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/user_stats.dart';
import '../../../../core/models/achievement.dart';
/// Página de conquistas para alunos
class StudentAchievementsPage extends StatefulWidget {
const StudentAchievementsPage({super.key});
@override
State<StudentAchievementsPage> createState() => _StudentAchievementsPageState();
}
class _StudentAchievementsPageState extends State<StudentAchievementsPage> {
UserStats? _userStats;
List<Achievement> _allAchievements = [];
List<Achievement> _unlockedAchievements = [];
List<Achievement> _lockedAchievements = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadAchievements();
}
Future<void> _loadAchievements() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
final [stats, allAchievements] = await Future.wait([
GamificationService.getUserStats(user.uid),
GamificationService.getAvailableAchievements(),
]);
final userStats = stats as UserStats?;
final achievements = allAchievements as List<Achievement>;
if (userStats != null) {
final unlockedIds = userStats.unlockedAchievements
.map((ua) => ua.achievementId)
.toSet();
final unlocked = achievements
.where((a) => unlockedIds.contains(a.id))
.toList();
final locked = achievements
.where((a) => !unlockedIds.contains(a.id))
.toList();
if (mounted) {
setState(() {
_userStats = userStats;
_allAchievements = achievements;
_unlockedAchievements = unlocked;
_lockedAchievements = locked;
_loading = false;
});
}
}
} catch (e) {
print('Error loading achievements: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final themeExtras = AppThemeExtras.of(context);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
cs.primary.withValues(alpha: 0.05),
cs.surface,
],
),
),
child: SafeArea(
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(24),
child: Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => context.go('/student-dashboard'),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Minhas Conquistas',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${_unlockedAchievements.length}/${_allAchievements.length} desbloqueadas',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 14,
),
),
],
),
),
],
),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [cs.primary, cs.primary.withValues(alpha: 0.8)],
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
),
// Debug buttons (remover em produção)
if (!const bool.fromEnvironment('dart.vm.product')) ...[
Container(
margin: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () async {
final user = AuthService.currentUser;
if (user != null) {
await GamificationService.initializeGamificationData(user.uid);
_loadAchievements();
}
},
child: const Text('Inicializar Dados'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () async {
final user = AuthService.currentUser;
if (user != null) {
await GamificationService.simulateQuizCompletion(
user.uid,
score: 8,
totalQuestions: 10,
materialName: 'Matemática Básica',
);
_loadAchievements();
}
},
child: const Text('Simular Quiz'),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton(
onPressed: () async {
final user = AuthService.currentUser;
if (user != null) {
await GamificationService.debugUserStats(user.uid);
}
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
child: const Text('Debug Stats'),
),
),
],
),
),
],
// Content
Expanded(
child: _loading
? const Center(child: CircularProgressIndicator())
: DefaultTabController(
length: 2,
child: Column(
children: [
// Tabs
Container(
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: TabBar(
labelColor: cs.onPrimary,
unselectedLabelColor: cs.onSurfaceVariant,
indicator: BoxDecoration(
color: cs.primary,
borderRadius: BorderRadius.circular(10),
),
indicatorSize: TabBarIndicatorSize.tab,
tabs: const [
Tab(
icon: Icon(Icons.emoji_events),
text: 'Desbloqueadas',
),
Tab(
icon: Icon(Icons.lock_outline),
text: 'Bloqueadas',
),
],
),
),
// Tab Content
Expanded(
child: TabBarView(
children: [
_buildUnlockedAchievements(),
_buildLockedAchievements(),
],
),
),
],
),
),
),
],
),
),
),
);
}
Widget _buildUnlockedAchievements() {
if (_unlockedAchievements.isEmpty) {
return _buildEmptyState(
icon: Icons.emoji_events_outlined,
title: 'Nenhuma conquista desbloqueada',
subtitle: 'Complete quizzes e mantenha seu streak para desbloquear conquistas!',
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _unlockedAchievements.length,
itemBuilder: (context, index) {
final achievement = _unlockedAchievements[index];
return _buildAchievementCard(achievement, isUnlocked: true)
.animate()
.slideX(duration: const Duration(milliseconds: 300))
.then(delay: Duration(milliseconds: index * 50));
},
);
}
Widget _buildLockedAchievements() {
if (_lockedAchievements.isEmpty) {
return _buildEmptyState(
icon: Icons.emoji_events,
title: 'Parabéns!',
subtitle: 'Você desbloqueou todas as conquistas disponíveis!',
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _lockedAchievements.length,
itemBuilder: (context, index) {
final achievement = _lockedAchievements[index];
return _buildAchievementCard(achievement, isUnlocked: false)
.animate()
.slideX(duration: const Duration(milliseconds: 300))
.then(delay: Duration(milliseconds: index * 50));
},
);
}
Widget _buildAchievementCard(Achievement achievement, {required bool isUnlocked}) {
final cs = Theme.of(context).colorScheme;
final color = _getRarityColor(achievement.rarity);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isUnlocked ? cs.surface : cs.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isUnlocked
? color.withValues(alpha: 0.3)
: cs.outline.withValues(alpha: 0.2),
width: isUnlocked ? 2 : 1,
),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: isUnlocked ? 0.1 : 0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
// Icon
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: isUnlocked
? color.withValues(alpha: 0.2)
: cs.outline.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(24),
),
child: Icon(
_getIconData(achievement.icon),
color: isUnlocked ? color : cs.outline,
size: 24,
),
),
const SizedBox(width: 16),
// Content
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
achievement.name,
style: TextStyle(
color: isUnlocked ? cs.onSurface : cs.onSurfaceVariant,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
achievement.description,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getRarityColor(achievement.rarity).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
achievement.rarity.toUpperCase(),
style: TextStyle(
color: _getRarityColor(achievement.rarity),
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 8),
Text(
'+${achievement.points} pts',
style: TextStyle(
color: cs.primary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
),
// Status
if (isUnlocked)
Icon(
Icons.check_circle,
color: Colors.green,
size: 24,
)
else
Icon(
Icons.lock,
color: cs.outline,
size: 24,
),
],
),
);
}
Widget _buildEmptyState({
required IconData icon,
required String title,
required String subtitle,
}) {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: cs.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
title,
style: TextStyle(
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
subtitle,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Color _getRarityColor(String rarity) {
switch (rarity) {
case 'common': return Colors.grey;
case 'rare': return Colors.blue;
case 'epic': return Colors.purple;
case 'legendary': return Colors.orange;
default: return Colors.grey;
}
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'emoji_events': return Icons.emoji_events;
case 'school': return Icons.school;
case 'local_fire_department': return Icons.local_fire_department;
case 'schedule': return Icons.schedule;
case 'trending_up': return Icons.trending_up;
case 'star': return Icons.star;
case 'military_tech': return Icons.military_tech;
case 'workspace_premium': return Icons.workspace_premium;
case 'psychology': return Icons.psychology;
case 'lightbulb': return Icons.lightbulb;
case 'whatshot': return Icons.whatshot;
case 'stars': return Icons.stars;
default: return Icons.emoji_events;
}
}
}

View File

@@ -0,0 +1,343 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../../core/theme/app_theme_extension.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/class_stats.dart';
import '../../../../core/models/achievement.dart';
import '../widgets/class_analytics_card.dart';
import '../widgets/class_ranking_widget.dart';
import '../widgets/create_achievement_dialog.dart';
/// Analytics page for teachers with class breakdowns and rankings
class AnalyticsPage extends StatefulWidget {
const AnalyticsPage({super.key});
@override
State<AnalyticsPage> createState() => _AnalyticsPageState();
}
class _AnalyticsPageState extends State<AnalyticsPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<ClassStats> _classStats = [];
bool _loading = true;
String? _selectedClassId;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_loadClassStats();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
Future<void> _loadClassStats() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
// Obter turmas do professor
final classesSnapshot = await FirebaseFirestore.instance
.collection('classes')
.where('teacherId', isEqualTo: user.uid)
.get();
final classStatsList = <ClassStats>[];
for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id;
final stats = await GamificationService.getClassStats(classId);
if (stats != null) {
classStatsList.add(stats);
}
}
if (mounted) {
setState(() {
_classStats = classStatsList;
_loading = false;
});
}
} catch (e) {
print('Error loading class stats: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
final themeExtras = AppThemeExtras.of(context);
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: themeExtras.dashboardBackgroundGradient,
stops: themeExtras.dashboardGradientStops,
),
),
child: SafeArea(
child: Column(
children: [
// Header
Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () => context.go('/teacher-dashboard'),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Analytics',
style: TextStyle(
color: Colors.white,
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Acompanhe o desempenho das turmas',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 16,
),
),
],
),
),
IconButton(
icon: const Icon(Icons.add, color: Colors.white),
onPressed: _showCreateAchievementDialog,
tooltip: 'Criar Conquista',
),
],
),
const SizedBox(height: 20),
TabBar(
controller: _tabController,
labelColor: Colors.white,
unselectedLabelColor: Colors.white.withValues(alpha: 0.7),
indicatorColor: Colors.white,
indicatorWeight: 2,
tabs: const [
Tab(text: 'Turmas'),
Tab(text: 'Rankings'),
],
),
],
),
),
// Content
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildClassesTab(),
_buildRankingsTab(),
],
),
),
],
),
),
),
);
}
Widget _buildClassesTab() {
if (_loading) {
return const Center(child: CircularProgressIndicator(color: Colors.white));
}
if (_classStats.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.analytics_outlined,
size: 64,
color: Colors.white.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'Nenhuma turma encontrada',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 18,
),
),
const SizedBox(height: 8),
Text(
'Crie turmas para ver as analytics aqui',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14,
),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Overview Cards
Row(
children: [
Expanded(
child: _buildOverviewCard(
'Total de Alunos',
'${_classStats.fold(0, (sum, stats) => sum + stats.totalStudents)}',
Icons.people,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildOverviewCard(
'Alunos Ativos',
'${_classStats.fold(0, (sum, stats) => sum + stats.activeStudents)}',
Icons.trending_up,
Colors.green,
),
),
],
),
const SizedBox(height: 20),
// Class Cards
..._classStats.map((stats) => Padding(
padding: const EdgeInsets.only(bottom: 16),
child: ClassAnalyticsCard(
classStats: stats,
onTap: () => _showClassRanking(stats),
),
)),
],
),
);
}
Widget _buildRankingsTab() {
if (_selectedClassId == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.leaderboard,
size: 64,
color: Colors.white.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'Selecione uma turma',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 18,
),
),
const SizedBox(height: 8),
Text(
'Clique em uma turma na aba "Turmas" para ver o ranking',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14,
),
),
],
),
);
}
return ClassRankingWidget(classId: _selectedClassId!);
}
Widget _buildOverviewCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
),
child: Column(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(height: 12),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
title,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
).animate().scale(duration: 600.ms, curve: Curves.elasticOut);
}
void _showClassRanking(ClassStats stats) {
setState(() {
_selectedClassId = stats.classId;
});
_tabController.animateTo(1); // Mudar para aba de rankings
}
void _showCreateAchievementDialog() {
showDialog(
context: context,
builder: (context) => CreateAchievementDialog(
onAchievementCreated: (achievement) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Conquista "${achievement.name}" criada com sucesso!'),
backgroundColor: Colors.green,
),
);
},
),
);
}
}

View File

@@ -0,0 +1,317 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../../core/models/class_stats.dart';
import '../../../../core/theme/app_theme_extension.dart';
/// Card displaying analytics for a specific class
class ClassAnalyticsCard extends StatelessWidget {
final ClassStats classStats;
final VoidCallback onTap;
const ClassAnalyticsCard({
super.key,
required this.classStats,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
cs.primary.withValues(alpha: 0.9),
cs.primary.withValues(alpha: 0.7),
],
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: cs.shadow.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
classStats.className,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${classStats.activeStudents} de ${classStats.totalStudents} alunos ativos',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 14,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.trending_up, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
'${(classStats.averageProgress * 100).toInt()}%',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
const SizedBox(height: 20),
// Progress Bar
Container(
height: 8,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
),
child: FractionallySizedBox(
alignment: Alignment.centerLeft,
widthFactor: classStats.averageProgress.clamp(0.0, 1.0),
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppThemeExtras.of(context).heroProgressStart,
AppThemeExtras.of(context).heroProgressEnd,
],
),
borderRadius: BorderRadius.circular(4),
),
),
),
),
const SizedBox(height: 20),
// Stats Grid
Row(
children: [
Expanded(
child: _buildStatCard(
icon: Icons.quiz,
value: '${classStats.activeQuizzes}',
label: 'Quizzes Ativos',
context: context,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.description,
value: '${classStats.totalContent}',
label: 'Conteúdos',
context: context,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
icon: Icons.warning,
value: '${classStats.studentsNeedingSupport.length}',
label: 'Precisam Apoio',
context: context,
isWarning: classStats.studentsNeedingSupport.isNotEmpty,
),
),
],
),
// Students needing support preview
if (classStats.studentsNeedingSupport.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.priority_high,
color: Colors.orange.withValues(alpha: 0.8),
size: 16,
),
const SizedBox(width: 6),
Text(
'Alunos que precisam de atenção:',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.9),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 8),
...classStats.studentsNeedingSupport.take(3).map((student) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.orange,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
student.studentName,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11,
),
),
),
Text(
'${student.averageScore.toInt()}%',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
],
),
)),
if (classStats.studentsNeedingSupport.length > 3)
Text(
'+${classStats.studentsNeedingSupport.length - 3} alunos',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.6),
fontSize: 10,
),
),
],
),
),
],
// Click indicator
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Ver ranking detalhado',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 12,
),
),
const SizedBox(width: 4),
Icon(
Icons.arrow_forward,
color: Colors.white.withValues(alpha: 0.8),
size: 12,
),
],
),
],
),
),
),
),
).animate().scale(duration: 600.ms, curve: Curves.elasticOut);
}
Widget _buildStatCard({
required IconData icon,
required String value,
required String label,
required BuildContext context,
bool isWarning = false,
}) {
final cs = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
children: [
Icon(
icon,
color: isWarning ? Colors.orange : Colors.white,
size: 20,
),
const SizedBox(height: 6),
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 10,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}

View File

@@ -0,0 +1,432 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/class_stats.dart';
import '../../../../core/theme/app_theme_extension.dart';
/// Widget displaying student ranking for a specific class
class ClassRankingWidget extends StatefulWidget {
final String classId;
const ClassRankingWidget({
super.key,
required this.classId,
});
@override
State<ClassRankingWidget> createState() => _ClassRankingWidgetState();
}
class _ClassRankingWidgetState extends State<ClassRankingWidget> {
List<StudentRanking> _rankings = [];
ClassStats? _classStats;
bool _loading = true;
String _searchQuery = '';
@override
void initState() {
super.initState();
_loadRankingData();
}
Future<void> _loadRankingData() async {
try {
final results = await Future.wait([
GamificationService.getClassRanking(widget.classId),
GamificationService.getClassStats(widget.classId),
]);
final rankings = results[0] as List<StudentRanking>;
final classStats = results[1] as ClassStats?;
if (mounted) {
setState(() {
_rankings = rankings;
_classStats = classStats;
_loading = false;
});
}
} catch (e) {
print('Error loading ranking data: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
List<StudentRanking> get _filteredRankings {
if (_searchQuery.isEmpty) return _rankings;
return _rankings.where((student) =>
student.studentName.toLowerCase().contains(_searchQuery.toLowerCase()) ||
student.studentEmail.toLowerCase().contains(_searchQuery.toLowerCase())
).toList();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
if (_loading) {
return const Center(
child: CircularProgressIndicator(color: Colors.white),
);
}
return Container(
margin: const EdgeInsets.all(24),
child: Column(
children: [
// Header with class info
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [cs.primary, cs.primary.withValues(alpha: 0.8)],
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_classStats?.className ?? 'Carregando...',
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'${_rankings.length} alunos • Progresso médio: ${((_classStats?.averageProgress ?? 0) * 100).toInt()}%',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 14,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(16),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.leaderboard, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
'Ranking',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
],
),
),
const SizedBox(height: 20),
// Search bar
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
),
child: Row(
children: [
Icon(Icons.search, color: Colors.white.withValues(alpha: 0.7)),
const SizedBox(width: 12),
Expanded(
child: TextField(
onChanged: (value) {
setState(() {
_searchQuery = value;
});
},
style: const TextStyle(color: Colors.white),
decoration: InputDecoration(
hintText: 'Buscar aluno...',
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
),
border: InputBorder.none,
),
),
),
if (_searchQuery.isNotEmpty)
IconButton(
icon: Icon(Icons.clear, color: Colors.white.withValues(alpha: 0.7)),
onPressed: () {
setState(() {
_searchQuery = '';
});
},
),
],
),
),
const SizedBox(height: 20),
// Ranking list
Expanded(
child: _filteredRankings.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: EdgeInsets.zero,
itemCount: _filteredRankings.length,
itemBuilder: (context, index) {
final student = _filteredRankings[index];
final rankPosition = _rankings.indexOf(student) + 1;
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildStudentRankingCard(student, rankPosition),
);
},
),
),
],
),
);
}
Widget _buildEmptyState() {
if (_searchQuery.isNotEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.white.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'Nenhum aluno encontrado',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 18,
),
),
const SizedBox(height: 8),
Text(
'Tente buscar com outros termos',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14,
),
),
],
),
);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.people_outline,
size: 64,
color: Colors.white.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
'Nenhum aluno na turma',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 18,
),
),
const SizedBox(height: 8),
Text(
'Os alunos aparecerão aqui quando se matricularem',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 14,
),
),
],
),
);
}
Widget _buildStudentRankingCard(StudentRanking student, int rankPosition) {
final cs = Theme.of(context).colorScheme;
// Determinar cor baseada na posição
Color rankColor;
IconData rankIcon;
if (rankPosition == 1) {
rankColor = Colors.amber;
rankIcon = Icons.emoji_events;
} else if (rankPosition == 2) {
rankColor = Colors.grey.withValues(alpha: 0.8);
rankIcon = Icons.workspace_premium;
} else if (rankPosition == 3) {
rankColor = Colors.brown.withValues(alpha: 0.8);
rankIcon = Icons.military_tech;
} else {
rankColor = Colors.white.withValues(alpha: 0.7);
rankIcon = Icons.numbers;
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.2)),
),
child: Row(
children: [
// Rank position
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: rankColor.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: rankColor.withValues(alpha: 0.5)),
),
child: Center(
child: rankPosition <= 3
? Icon(rankIcon, color: rankColor, size: 20)
: Text(
'$rankPosition',
style: TextStyle(
color: rankColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 16),
// Student info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
student.studentName,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 2),
Text(
student.studentEmail,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 12,
),
),
],
),
),
// Stats
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
// Overall score
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getScoreColor(student.overallScore).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${student.overallScore.toInt()}%',
style: TextStyle(
color: _getScoreColor(student.overallScore),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 4),
// Quiz completion
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.quiz,
color: Colors.white.withValues(alpha: 0.7),
size: 12,
),
const SizedBox(width: 4),
Text(
'${student.completedQuizzes}/${student.totalQuizzes}',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.7),
fontSize: 11,
),
),
],
),
const SizedBox(height: 2),
// Streak
if (student.currentStreak > 0)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.local_fire_department,
color: Colors.orange.withValues(alpha: 0.8),
size: 12,
),
const SizedBox(width: 4),
Text(
'${student.currentStreak}d',
style: TextStyle(
color: Colors.orange.withValues(alpha: 0.8),
fontSize: 11,
),
),
],
),
],
),
],
),
).animate().slideX(duration: 300.ms, curve: Curves.easeOut);
}
Color _getScoreColor(double score) {
if (score >= 80) return Colors.green;
if (score >= 60) return Colors.blue;
if (score >= 40) return Colors.orange;
return Colors.red;
}
}

View File

@@ -0,0 +1,633 @@
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/achievement.dart';
/// Dialog for creating custom achievements
class CreateAchievementDialog extends StatefulWidget {
final Function(Achievement) onAchievementCreated;
const CreateAchievementDialog({
super.key,
required this.onAchievementCreated,
});
@override
State<CreateAchievementDialog> createState() => _CreateAchievementDialogState();
}
class _CreateAchievementDialogState extends State<CreateAchievementDialog> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _descriptionController = TextEditingController();
final _valueController = TextEditingController();
String _selectedCategory = 'quiz';
String _selectedRequirementType = 'quiz_completion';
String _selectedOperator = '>=';
String _selectedIcon = 'star';
String _selectedRarity = 'common';
int _points = 10;
final List<String> _categories = [
'quiz',
'study_time',
'streak',
'concept',
'general',
];
final List<String> _requirementTypes = [
'quiz_completion',
'quiz_score',
'study_time',
'streak_days',
'concepts_mastered',
];
final List<String> _operators = ['>=', '==', '>'];
final List<String> _icons = [
'star',
'emoji_events',
'school',
'local_fire_department',
'schedule',
'trending_up',
'military_tech',
'workspace_premium',
'psychology',
'lightbulb',
];
final List<String> _rarities = [
'common',
'rare',
'epic',
'legendary',
];
final Map<String, int> _rarityPoints = {
'common': 10,
'rare': 25,
'epic': 50,
'legendary': 100,
};
@override
void dispose() {
_nameController.dispose();
_descriptionController.dispose();
_valueController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Dialog(
backgroundColor: cs.surface,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Container(
width: MediaQuery.of(context).size.width * 0.8,
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.75,
minWidth: 280,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [cs.primary, cs.primary.withValues(alpha: 0.8)],
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
const Icon(Icons.emoji_events, color: Colors.white, size: 28),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Criar Nova Conquista',
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white),
onPressed: () => Navigator.of(context).pop(),
),
],
),
),
// Form
Expanded(
child: Form(
key: _formKey,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Basic Info
_buildSectionTitle('Informações Básicas'),
const SizedBox(height: 16),
_buildTextField(
controller: _nameController,
label: 'Nome da Conquista',
hint: 'Ex: Mestre dos Quizzes',
validator: (value) {
if (value == null || value.isEmpty) {
return 'Campo obrigatório';
}
return null;
},
),
const SizedBox(height: 16),
_buildTextField(
controller: _descriptionController,
label: 'Descrição',
hint: 'Ex: Complete 10 quizzes com 100% de acerto',
maxLines: 2,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Campo obrigatório';
}
return null;
},
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildDropdownField<String>(
label: 'Categoria',
value: _selectedCategory,
items: _categories,
onChanged: (value) {
setState(() {
_selectedCategory = value!;
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildDropdownField<String>(
label: 'Ícone',
value: _selectedIcon,
items: _icons,
onChanged: (value) {
setState(() {
_selectedIcon = value!;
});
},
),
),
],
),
const SizedBox(height: 16),
// Requirements
_buildSectionTitle('Requisitos'),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildDropdownField<String>(
label: 'Tipo de Requisito',
value: _selectedRequirementType,
items: _requirementTypes,
onChanged: (value) {
setState(() {
_selectedRequirementType = value!;
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: _buildDropdownField<String>(
label: 'Operador',
value: _selectedOperator,
items: _operators,
onChanged: (value) {
setState(() {
_selectedOperator = value!;
});
},
),
),
],
),
const SizedBox(height: 16),
_buildTextField(
controller: _valueController,
label: 'Valor',
hint: 'Ex: 10',
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Campo obrigatório';
}
if (int.tryParse(value) == null) {
return 'Digite um número válido';
}
return null;
},
),
const SizedBox(height: 16),
// Reward
_buildSectionTitle('Recompensa'),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildDropdownField<String>(
label: 'Raridade',
value: _selectedRarity,
items: _rarities,
onChanged: (value) {
setState(() {
_selectedRarity = value!;
_points = _rarityPoints[value]!;
});
},
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Pontos',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 12,
),
),
const SizedBox(height: 4),
Text(
'$_points',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
],
),
const SizedBox(height: 16),
// Preview
_buildSectionTitle('Preview'),
const SizedBox(height: 16),
_buildAchievementPreview(),
],
),
),
),
),
// Actions
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: cs.surfaceContainerHighest,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(20),
bottomRight: Radius.circular(20),
),
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
side: BorderSide(color: cs.outline),
),
child: Text('Cancelar', style: TextStyle(color: cs.onSurface)),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: _createAchievement,
style: ElevatedButton.styleFrom(
backgroundColor: cs.primary,
foregroundColor: cs.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('Criar Conquista'),
),
),
],
),
),
],
),
),
);
}
Widget _buildSectionTitle(String title) {
final cs = Theme.of(context).colorScheme;
return Text(
title,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
);
}
Widget _buildTextField({
required TextEditingController controller,
required String label,
required String hint,
int maxLines = 1,
TextInputType? keyboardType,
String? Function(String?)? validator,
}) {
final cs = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
TextFormField(
controller: controller,
decoration: InputDecoration(
hintText: hint,
hintStyle: TextStyle(color: cs.onSurfaceVariant),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.primary, width: 2),
),
contentPadding: const EdgeInsets.all(16),
),
maxLines: maxLines,
keyboardType: keyboardType,
validator: validator,
),
],
);
}
Widget _buildDropdownField<T>({
required String label,
required T value,
required List<T> items,
required Function(T?) onChanged,
}) {
final cs = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
color: cs.onSurface,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<T>(
isExpanded: true,
value: value,
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.outline),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: cs.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
),
items: items.map((item) {
return DropdownMenuItem(
value: item,
child: Text(
item.toString().split('_').map((word) =>
word[0].toUpperCase() + word.substring(1)
).join(' '),
style: const TextStyle(fontSize: 12),
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
onChanged: onChanged,
),
],
);
}
Widget _buildAchievementPreview() {
final cs = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
cs.primary.withValues(alpha: 0.1),
cs.primary.withValues(alpha: 0.05),
],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: cs.primary.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: _getRarityColor().withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(24),
border: Border.all(color: _getRarityColor().withValues(alpha: 0.5)),
),
child: Icon(
_getIconData(),
color: _getRarityColor(),
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_nameController.text.isEmpty ? 'Nome da Conquista' : _nameController.text,
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
_descriptionController.text.isEmpty
? 'Descrição da conquista'
: _descriptionController.text,
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
),
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getRarityColor().withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$_points pts',
style: TextStyle(
color: _getRarityColor(),
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}
IconData _getIconData() {
switch (_selectedIcon) {
case 'emoji_events': return Icons.emoji_events;
case 'school': return Icons.school;
case 'local_fire_department': return Icons.local_fire_department;
case 'schedule': return Icons.schedule;
case 'trending_up': return Icons.trending_up;
case 'military_tech': return Icons.military_tech;
case 'workspace_premium': return Icons.workspace_premium;
case 'psychology': return Icons.psychology;
case 'lightbulb': return Icons.lightbulb;
default: return Icons.star;
}
}
Color _getRarityColor() {
switch (_selectedRarity) {
case 'common': return Colors.grey;
case 'rare': return Colors.blue;
case 'epic': return Colors.purple;
case 'legendary': return Colors.orange;
default: return Colors.grey;
}
}
Future<void> _createAchievement() async {
if (!_formKey.currentState!.validate()) return;
try {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
final achievement = Achievement(
id: '', // Will be generated by Firestore
name: _nameController.text,
description: _descriptionController.text,
icon: _selectedIcon,
category: _selectedCategory,
requirements: AchievementRequirement(
type: _selectedRequirementType,
value: int.parse(_valueController.text),
operator: _selectedOperator,
),
points: _points,
rarity: _selectedRarity,
isActive: true,
createdAt: DateTime.now(),
createdBy: user.uid,
);
final achievementId = await GamificationService.createCustomAchievement(
teacherId: user.uid,
name: achievement.name,
description: achievement.description,
icon: achievement.icon,
category: achievement.category,
requirements: achievement.requirements,
points: achievement.points,
rarity: achievement.rarity,
);
final createdAchievement = achievement.copyWith(id: achievementId);
widget.onAchievementCreated(createdAchievement);
Navigator.of(context).pop();
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Erro ao criar conquista: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

View File

@@ -3,13 +3,75 @@ import 'package:flutter/material.dart';
import '../../../../core/theme/app_theme_extension.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/user_stats.dart';
import '../../../../core/models/achievement.dart';
/// Profile section with user info and achievements
class ProfileSectionWidget extends StatelessWidget {
class ProfileSectionWidget extends StatefulWidget {
const ProfileSectionWidget({super.key});
@override
State<ProfileSectionWidget> createState() => _ProfileSectionWidgetState();
}
class _ProfileSectionWidgetState extends State<ProfileSectionWidget> {
UserStats? _userStats;
List<Achievement> _recentAchievements = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadUserData();
}
Future<void> _loadUserData() async {
try {
final user = AuthService.currentUser;
if (user != null) {
final results = await Future.wait([
GamificationService.getUserStats(user.uid),
GamificationService.getAvailableAchievements(),
]);
final stats = results[0] as UserStats?;
final achievements = results[1] as List<Achievement>;
// Obter conquistas desbloqueadas recentemente
final unlockedAchievementIds = stats?.unlockedAchievements
.map((ua) => ua.achievementId)
.toSet() ?? {};
final recentUnlocked = achievements
.where((a) => unlockedAchievementIds.contains(a.id))
.take(4)
.toList();
if (mounted) {
setState(() {
_userStats = stats;
_recentAchievements = recentUnlocked;
_loading = false;
});
}
}
} catch (e) {
print('Error loading user data: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
final user = AuthService.currentUser;
final userName = user?.displayName ?? 'Estudante';
final userEmail = user?.email ?? '';
@@ -124,36 +186,55 @@ class ProfileSectionWidget extends StatelessWidget {
),
const SizedBox(height: 12),
// Achievement Badges
Row(
children: [
_buildAchievementBadge(
icon: Icons.local_fire_department,
label: '7 dias',
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 12),
_buildAchievementBadge(
icon: Icons.school,
label: '3 conceitos',
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
_buildAchievementBadge(
icon: Icons.speed,
label: 'Rápido',
color: Theme.of(
// Achievement List (Teacher-style design)
if (_recentAchievements.isNotEmpty) ...[
..._recentAchievements.map((achievement) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildAchievementItem(
context,
).colorScheme.primary.withOpacity(0.8),
achievement.name,
achievement.points,
_getRarityColor(achievement.rarity),
_getIconData(achievement.icon),
),
);
}),
] else ...[
// Streak item
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildAchievementItem(
context,
'${_userStats?.currentStreak ?? 0} dias seguidos',
(_userStats?.currentStreak ?? 0) * 5,
Theme.of(context).colorScheme.secondary,
Icons.local_fire_department,
),
const SizedBox(width: 12),
_buildAchievementBadge(
icon: Icons.star,
label: '100%',
color: Theme.of(context).colorScheme.tertiary,
),
// Concepts item
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildAchievementItem(
context,
'${_userStats?.masteredConcepts.length ?? 0} conceitos dominados',
(_userStats?.masteredConcepts.length ?? 0) * 10,
Theme.of(context).colorScheme.primary,
Icons.school,
),
],
),
),
if (_userStats != null && _userStats!.totalStudyTime > 0)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildAchievementItem(
context,
'${(_userStats!.totalStudyTime / 60).toStringAsFixed(1)}h estudadas',
(_userStats!.totalStudyTime ~/ 60) * 3,
Theme.of(context).colorScheme.tertiary,
Icons.schedule,
),
),
],
const SizedBox(height: 20),
// Recent Activity Summary
@@ -183,7 +264,7 @@ class ProfileSectionWidget extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Ótimo progresso!',
_getProgressMessage(),
style: TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: 14,
@@ -192,7 +273,7 @@ class ProfileSectionWidget extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
'Você está 15% acima da média esta semana',
_getProgressComparison(),
style: TextStyle(
color: Theme.of(
context,
@@ -217,32 +298,103 @@ class ProfileSectionWidget extends StatelessWidget {
.then(delay: const Duration(milliseconds: 400));
}
Widget _buildAchievementBadge({
required IconData icon,
required String label,
required Color color,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3), width: 1),
),
child: Column(
children: [
Icon(icon, color: color, size: 16),
const SizedBox(height: 4),
Text(
label,
Widget _buildAchievementItem(
BuildContext context,
String name,
int points,
Color color,
IconData icon,
) {
return Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Icon(icon, color: color, size: 16),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
name,
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 14,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$points pts',
style: TextStyle(
color: color,
fontSize: 10,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
);
}
IconData _getIconData(String iconName) {
switch (iconName) {
case 'emoji_events': return Icons.emoji_events;
case 'school': return Icons.school;
case 'local_fire_department': return Icons.local_fire_department;
case 'schedule': return Icons.schedule;
case 'trending_up': return Icons.trending_up;
case 'military_tech': return Icons.military_tech;
case 'workspace_premium': return Icons.workspace_premium;
case 'psychology': return Icons.psychology;
case 'lightbulb': return Icons.lightbulb;
case 'star': return Icons.star;
case 'speed': return Icons.speed;
default: return Icons.star;
}
}
Color _getRarityColor(String rarity) {
switch (rarity) {
case 'common': return Colors.grey;
case 'rare': return Colors.blue;
case 'epic': return Colors.purple;
case 'legendary': return Colors.orange;
default: return Colors.grey;
}
}
String _getProgressMessage() {
if (_userStats == null) return 'Continue estudando!';
final streak = _userStats!.currentStreak;
final studyTime = _userStats!.totalStudyTime;
if (streak >= 7) return 'Incrível streak! 🔥';
if (studyTime >= 300) return 'Dedicação exemplar! 📚';
if (streak >= 3) return 'Bom progresso! 📈';
return 'Continue estudando! 💪';
}
String _getProgressComparison() {
if (_userStats == null) return 'Comece sua jornada de estudos';
final weeklyTime = _userStats!.weeklyStudyTime;
final concepts = _userStats!.masteredConcepts.length;
if (weeklyTime >= 180) return 'Você está 15% acima da média esta semana';
if (concepts >= 3) return 'Domine $concepts conceitos esta semana';
if (weeklyTime >= 60) return 'Bom tempo de estudo esta semana';
return 'Continue assim para subir no ranking!';
}
}

View File

@@ -2,30 +2,81 @@ import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../../core/theme/app_theme_extension.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/user_stats.dart';
import '../../../../core/services/auth_service.dart';
/// Progress tracking hero section for student dashboard
class ProgressHeroWidget extends StatelessWidget {
class ProgressHeroWidget extends StatefulWidget {
final String userName;
final double overallProgress;
final List<String> masteredConcepts;
final int studyTimeMinutes;
final int streakDays;
const ProgressHeroWidget({
super.key,
required this.userName,
this.overallProgress = 0.65,
this.masteredConcepts = const [
'Fundamentos de Programação',
'Algoritmos Básicos',
'Estruturas de Dados',
],
this.studyTimeMinutes = 245,
this.streakDays = 7,
});
@override
State<ProgressHeroWidget> createState() => _ProgressHeroWidgetState();
}
class _ProgressHeroWidgetState extends State<ProgressHeroWidget> {
@override
Widget build(BuildContext context) {
return FutureBuilder<UserStats?>(
future: _loadUserStats(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return _buildLoadingState();
}
if (snapshot.hasError) {
return _buildErrorState();
}
final userStats = snapshot.data;
return _buildContent(userStats);
},
);
}
Future<UserStats?> _loadUserStats() async {
try {
final user = AuthService.currentUser;
if (user != null) {
return await GamificationService.getUserStats(user.uid);
}
return null;
} catch (e) {
print('Error loading user stats: $e');
return null;
}
}
double _calculateOverallProgress(UserStats? userStats) {
if (userStats == null || userStats.masteredConcepts.isEmpty) {
return 0.0;
}
final totalMastery = userStats.masteredConcepts
.map((c) => c.masteryLevel)
.reduce((a, b) => a + b);
return totalMastery / (userStats.masteredConcepts.length * 100);
}
Widget _buildLoadingState() {
return const Center(child: CircularProgressIndicator());
}
Widget _buildErrorState() {
return const Center(child: Text('Erro ao carregar dados'));
}
Widget _buildContent(UserStats? userStats) {
final streakDays = userStats?.currentStreak ?? 0;
final overallProgress = _calculateOverallProgress(userStats);
final masteredConcepts = userStats?.masteredConcepts.map((c) => c.conceptName).toList() ?? [];
final studyTimeMinutes = userStats?.totalStudyTime ?? 0;
return Container(
margin: const EdgeInsets.only(bottom: 24),
child: Column(
@@ -48,7 +99,7 @@ class ProgressHeroWidget extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'Continue assim, $userName!',
'Continue assim, ${widget.userName}!',
style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant,
fontSize: 16,

View File

@@ -22,23 +22,34 @@ class QuickAccessWidget extends StatelessWidget {
),
),
const SizedBox(height: 16),
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: _buildTutorIACard(context),
Column(
children: [
IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 2,
child: _buildTutorIACard(context),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: _buildQuizCard(context),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: _buildAchievementsCard(context),
),
],
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: _buildQuizCard(context),
),
],
),
),
],
),
const SizedBox(height: 16),
_buildQuizManagementCard(context),
const SizedBox(height: 16),
_buildJoinClassCard(context),
],
)
@@ -85,6 +96,42 @@ class QuickAccessWidget extends StatelessWidget {
.then(delay: const Duration(milliseconds: 200));
}
Widget _buildAchievementsCard(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return DashboardActionCardSurface(
title: 'Conquistas',
subtitle: 'Ver medals',
icon: Icons.emoji_events,
minHeight: 150,
iconColor: Colors.amber,
onTap: () => context.go('/student/achievements'),
)
.animate()
.scale(
duration: const Duration(milliseconds: 600),
curve: Curves.elasticOut,
)
.then(delay: const Duration(milliseconds: 200));
}
Widget _buildQuizManagementCard(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return DashboardActionCardSurface(
title: 'Gerenciar Quizzes',
subtitle: 'Ver histórico ou eliminar',
icon: Icons.manage_history,
minHeight: 80,
iconColor: cs.tertiary,
onTap: () => context.go('/quiz-management'),
)
.animate()
.slideY(
duration: const Duration(milliseconds: 800),
curve: Curves.easeOut,
)
.then(delay: const Duration(milliseconds: 200));
}
Widget _buildJoinClassCard(BuildContext context) {
return DashboardActionCard(
title: 'Entrar numa Turma',

View File

@@ -1,15 +1,95 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import '../../../../core/theme/app_theme_extension.dart';
import 'package:flutter_animate/flutter_animate.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../core/services/gamification_service.dart';
import '../../../../core/models/class_stats.dart';
/// Analytics preview section for teacher dashboard
class TeacherAnalyticsPreviewWidget extends StatelessWidget {
class TeacherAnalyticsPreviewWidget extends StatefulWidget {
const TeacherAnalyticsPreviewWidget({super.key});
@override
State<TeacherAnalyticsPreviewWidget> createState() => _TeacherAnalyticsPreviewWidgetState();
}
class _TeacherAnalyticsPreviewWidgetState extends State<TeacherAnalyticsPreviewWidget> {
List<ClassStats> _classStats = [];
List<StudentRanking> _topStudents = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadAnalyticsData();
}
Future<void> _loadAnalyticsData() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
// Obter turmas do professor
final classesSnapshot = await FirebaseFirestore.instance
.collection('classes')
.where('teacherId', isEqualTo: user.uid)
.get();
final classStatsList = <ClassStats>[];
for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id;
final stats = await GamificationService.getClassStats(classId);
if (stats != null) {
classStatsList.add(stats);
}
}
// Obter melhores alunos de todas as turmas
final allTopStudents = <StudentRanking>[];
for (final classStats in classStatsList) {
final ranking = await GamificationService.getClassRanking(classStats.classId);
allTopStudents.addAll(ranking.take(3)); // Top 3 de cada turma
}
// Ordenar por score e pegar os melhores
allTopStudents.sort((a, b) => b.overallScore.compareTo(a.overallScore));
final topStudents = allTopStudents.take(4).toList();
if (mounted) {
setState(() {
_classStats = classStatsList;
_topStudents = topStudents;
_loading = false;
});
}
} catch (e) {
print('Error loading analytics data: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
int get totalStudents => _classStats.fold(0, (sum, stats) => sum + stats.totalStudents);
int get activeStudents => _classStats.fold(0, (sum, stats) => sum + stats.activeStudents);
double get averageProgress {
if (_classStats.isEmpty) return 0.0;
final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress);
return totalProgress / _classStats.length;
}
int get studentsNeedingSupport => _classStats.fold(0, (sum, stats) => sum + stats.studentsNeedingSupport.length);
@override
Widget build(BuildContext context) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
final user = AuthService.currentUser;
final userName = user?.displayName ?? 'Professor';
final userEmail = user?.email ?? '';
@@ -109,24 +189,24 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
_buildQuickStat(
icon: Icons.check_circle,
label: 'Alunos Ativos',
value: '18/24',
value: '$activeStudents/$totalStudents',
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
_buildQuickStat(
icon: Icons.warning_amber,
label: 'Precisam Apoio',
value: '3',
value: '$studentsNeedingSupport',
color: Theme.of(context).colorScheme.secondary,
),
const SizedBox(width: 12),
_buildQuickStat(
icon: Icons.emoji_events,
label: 'Média Turma',
value: '72%',
value: '${(averageProgress * 100).toInt()}%',
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.8),
).colorScheme.primary.withValues(alpha: 0.8),
),
],
),
@@ -154,33 +234,17 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
const SizedBox(height: 12),
// Student List Preview
_buildStudentPerformanceItem(
context,
'Ana Silva',
95,
Theme.of(context).colorScheme.tertiary,
),
const SizedBox(height: 8),
_buildStudentPerformanceItem(
context,
'João Costa',
88,
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
_buildStudentPerformanceItem(
context,
'Maria Santos',
82,
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
_buildStudentPerformanceItem(
context,
'Pedro Lima',
45,
Theme.of(context).colorScheme.secondary,
),
..._topStudents.map((student) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _buildStudentPerformanceItem(
context,
student.studentName,
student.overallScore.toInt(),
_getScoreColor(student.overallScore),
),
);
}),
const SizedBox(height: 20),
@@ -218,7 +282,7 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
'12 conteúdos verificados • 2 pendentes de revisão',
'${_classStats.fold(0, (sum, stats) => sum + stats.totalContent)} conteúdos • $studentsNeedingSupport alunos precisam de apoio',
style: TextStyle(
color: Theme.of(
context,
@@ -336,4 +400,11 @@ class TeacherAnalyticsPreviewWidget extends StatelessWidget {
],
);
}
Color _getScoreColor(double score) {
if (score >= 80) return Theme.of(context).colorScheme.tertiary;
if (score >= 60) return Theme.of(context).colorScheme.primary;
if (score >= 40) return Theme.of(context).colorScheme.secondary;
return Theme.of(context).colorScheme.error;
}
}

View File

@@ -1,27 +1,89 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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';
/// Hero section for teacher dashboard showing class overview
class TeacherHeroWidget extends StatelessWidget {
class TeacherHeroWidget extends StatefulWidget {
final String userName;
final int totalStudents;
final int activeQuizzes;
final int uploadedContent;
final double classAverageProgress;
const TeacherHeroWidget({
super.key,
required this.userName,
this.totalStudents = 24,
this.activeQuizzes = 3,
this.uploadedContent = 12,
this.classAverageProgress = 0.72,
});
@override
State<TeacherHeroWidget> createState() => _TeacherHeroWidgetState();
}
class _TeacherHeroWidgetState extends State<TeacherHeroWidget> {
List<ClassStats> _classStats = [];
bool _loading = true;
@override
void initState() {
super.initState();
_loadClassStats();
}
Future<void> _loadClassStats() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
// Obter turmas do professor
final classesSnapshot = await FirebaseFirestore.instance
.collection('classes')
.where('teacherId', isEqualTo: user.uid)
.get();
final classStatsList = <ClassStats>[];
for (final classDoc in classesSnapshot.docs) {
final classId = classDoc.id;
final stats = await GamificationService.getClassStats(classId);
if (stats != null) {
classStatsList.add(stats);
}
}
if (mounted) {
setState(() {
_classStats = classStatsList;
_loading = false;
});
}
} catch (e) {
print('Error loading class stats: $e');
if (mounted) {
setState(() {
_loading = false;
});
}
}
}
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);
double get classAverageProgress {
if (_classStats.isEmpty) return 0.0;
final totalProgress = _classStats.fold(0.0, (sum, stats) => sum + stats.averageProgress);
return totalProgress / _classStats.length;
}
Widget build(BuildContext context) {
if (_loading) {
return Container(
margin: const EdgeInsets.only(bottom: 24),
child: const Center(child: CircularProgressIndicator()),
);
}
return Container(
margin: const EdgeInsets.only(bottom: 24),
child: Column(
@@ -231,26 +293,7 @@ class TeacherHeroWidget extends StatelessWidget {
],
),
const SizedBox(height: 12),
_buildActivityItem(
context,
'15 alunos completaram o quiz de Derivadas',
'Hoje, 14:30',
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 8),
_buildActivityItem(
context,
'Novo conteúdo: Regra da Cadeia',
'Ontem, 09:15',
Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 8),
_buildActivityItem(
context,
'3 alunos precisam de apoio em Limites',
'Ontem, 16:45',
Theme.of(context).colorScheme.error,
),
..._buildRecentActivities(),
],
),
)
@@ -302,6 +345,67 @@ class TeacherHeroWidget extends StatelessWidget {
);
}
List<Widget> _buildRecentActivities() {
final activities = <Widget>[];
if (_classStats.isEmpty) {
activities.add(_buildActivityItem(
context,
'Nenhuma atividade recente',
'Comece criando turmas e conteúdos',
Theme.of(context).colorScheme.onSurfaceVariant,
));
return activities;
}
// Adicionar atividades baseadas nas estatísticas das turmas
for (final stats in _classStats.take(3)) {
if (stats.activeQuizzes > 0) {
activities.add(_buildActivityItem(
context,
'${stats.activeQuizzes} quizzes ativos em ${stats.className}',
'Recente',
Theme.of(context).colorScheme.primary,
));
activities.add(const SizedBox(height: 8));
}
if (stats.studentsNeedingSupport.isNotEmpty) {
activities.add(_buildActivityItem(
context,
'${stats.studentsNeedingSupport.length} alunos precisam de apoio em ${stats.className}',
'Ver analytics',
Theme.of(context).colorScheme.error,
));
activities.add(const SizedBox(height: 8));
}
if (stats.totalContent > 0) {
activities.add(_buildActivityItem(
context,
'${stats.totalContent} conteúdos disponíveis em ${stats.className}',
'Atualizado',
Theme.of(context).colorScheme.secondary,
));
activities.add(const SizedBox(height: 8));
}
}
// Remover o último SizedBox se existir
if (activities.isNotEmpty && activities.last is SizedBox) {
activities.removeLast();
}
return activities.isEmpty ? [
_buildActivityItem(
context,
'Nenhuma atividade recente',
'Comece criando turmas e conteúdos',
Theme.of(context).colorScheme.onSurfaceVariant,
)
] : activities;
}
Widget _buildActivityItem(
BuildContext context,
String text,
@@ -333,7 +437,7 @@ class TeacherHeroWidget extends StatelessWidget {
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.7),
).colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
fontSize: 12,
),
),

View File

@@ -6,6 +6,7 @@ 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/services/gamification_service.dart';
import '../../../../core/utils/logger.dart';
class QuizListPage extends StatefulWidget {
@@ -423,6 +424,7 @@ class _QuizListPageState extends State<QuizListPage>
title: name,
quizId: quizId,
questions: questions,
materialName: name,
),
);
}
@@ -1162,10 +1164,12 @@ class _TeacherQuizInteractiveSheet extends StatefulWidget {
final String title;
final String quizId;
final List<_QuizQuestion> questions;
final String? materialName;
const _TeacherQuizInteractiveSheet({
required this.title,
required this.quizId,
required this.questions,
this.materialName,
});
@override
@@ -1229,6 +1233,14 @@ class _TeacherQuizInteractiveSheetState
'total': widget.questions.length,
'submittedAt': FieldValue.serverTimestamp(),
});
// Registrar atividade no sistema de gamificação
await GamificationService.recordQuizActivity(
user.uid,
score: _score,
totalQuestions: widget.questions.length,
materialName: widget.materialName ?? 'Quiz',
);
}
} catch (e) {
Logger.error('Error submitting teacher quiz result: $e');

View File

@@ -0,0 +1,458 @@
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:go_router/go_router.dart';
import '../../../../core/services/auth_service.dart';
import '../../../../core/utils/logger.dart';
/// Página para gerenciar quizzes (eliminar quizzes feitos)
class QuizManagementPage extends StatefulWidget {
const QuizManagementPage({super.key});
@override
State<QuizManagementPage> createState() => _QuizManagementPageState();
}
class _QuizManagementPageState extends State<QuizManagementPage> {
List<Map<String, dynamic>> _quizHistory = [];
bool _loading = true;
String _userRole = '';
@override
void initState() {
super.initState();
_loadUserRole();
_loadQuizHistory();
}
Future<void> _loadUserRole() async {
final user = AuthService.currentUser;
if (user != null) {
final userDoc = await FirebaseFirestore.instance
.collection('users')
.doc(user.uid)
.get();
if (userDoc.exists) {
setState(() {
_userRole = userDoc.data()?['role'] ?? '';
});
}
}
}
Future<void> _loadQuizHistory() async {
try {
final user = AuthService.currentUser;
if (user == null) return;
Query query;
if (_userRole == 'teacher') {
// Professor: ver todos os quizzes criados
query = FirebaseFirestore.instance
.collection('teacherQuizzes')
.where('teacherId', isEqualTo: user.uid)
.orderBy('createdAt', descending: true);
} else {
// Aluno: ver quizzes criados pelo próprio aluno + conceitos dominados
final results = await Future.wait([
FirebaseFirestore.instance.collection('userStats').doc(user.uid).get(),
FirebaseFirestore.instance
.collection('quizHistory')
.doc(user.uid)
.collection('quizzes')
.orderBy('createdAt', descending: true)
.get(),
]);
final userStatsSnapshot = results[0] as DocumentSnapshot;
final studentQuizzesSnapshot = results[1] as QuerySnapshot;
List<Map<String, dynamic>> quizList = [];
// Adicionar quizzes criados pelo aluno
for (final doc in studentQuizzesSnapshot.docs) {
final data = Map<String, dynamic>.from(doc.data() as Map);
data['id'] = doc.id;
data['title'] = data['materialName'] ?? 'Quiz sem nome';
data['type'] = 'created';
quizList.add(data);
}
// Adicionar conceitos dominados
if (userStatsSnapshot.exists) {
final stats = userStatsSnapshot.data() as Map<String, dynamic>?;
if (stats != null) {
final masteredConcepts = (stats['masteredConcepts'] as List<dynamic>?)
?.map((c) => Map<String, dynamic>.from(c as Map))
.toList() ?? [];
for (final concept in masteredConcepts) {
quizList.add({
'id': concept['conceptName'] ?? '',
'title': concept['conceptName'] ?? 'Conceito sem nome',
'score': concept['masteryLevel'] ?? 0,
'totalQuestions': 100, // Simulado
'completedAt': concept['masteredAt'] ?? Timestamp.now(),
'type': 'concept',
});
}
}
}
setState(() {
_quizHistory = quizList;
_loading = false;
});
return;
}
final snapshot = await query.get();
setState(() {
_quizHistory = snapshot.docs.map((doc) {
final data = Map<String, dynamic>.from(doc.data() as Map);
data['id'] = doc.id;
return data;
}).cast<Map<String, dynamic>>().toList();
_loading = false;
});
} catch (e) {
Logger.error('Error loading quiz history: $e');
setState(() {
_loading = false;
});
}
}
Future<void> _deleteQuiz(String quizId, String quizTitle, String type) async {
try {
String confirmMessage;
String successMessage;
if (_userRole == 'teacher') {
confirmMessage = 'Tem certeza que deseja eliminar o quiz "$quizTitle"? Esta ação não pode ser desfeita.';
successMessage = 'Quiz eliminado com sucesso!';
} else {
if (type == 'created') {
confirmMessage = 'Tem certeza que deseja eliminar seu quiz "$quizTitle"?';
successMessage = 'Quiz eliminado com sucesso!';
} else {
confirmMessage = 'Tem certeza que deseja remover o conceito "$quizTitle" do seu histórico?';
successMessage = 'Conceito removido com sucesso!';
}
}
final confirmed = await _showDeleteConfirmation(quizTitle, confirmMessage);
if (!confirmed) return;
if (_userRole == 'teacher') {
// Professor: eliminar quiz criado
await FirebaseFirestore.instance
.collection('teacherQuizzes')
.doc(quizId)
.delete();
// Também eliminar do histórico de alunos
final historySnapshot = await FirebaseFirestore.instance
.collection('quizHistory')
.where('quizId', isEqualTo: quizId)
.get();
for (final doc in historySnapshot.docs) {
await doc.reference.delete();
}
} else {
// Aluno: remover quiz criado ou conceito dominado
if (type == 'created') {
// Remover quiz criado pelo aluno
final user = AuthService.currentUser;
if (user != null) {
await FirebaseFirestore.instance
.collection('quizHistory')
.doc(user.uid)
.collection('quizzes')
.doc(quizId)
.delete();
}
} else {
// Remover conceito dominado
final user = AuthService.currentUser;
if (user != null) {
final userStatsDoc = await FirebaseFirestore.instance
.collection('userStats')
.doc(user.uid)
.get();
if (userStatsDoc.exists) {
final userStats = userStatsDoc.data() as Map<String, dynamic>;
final masteredConcepts = (userStats['masteredConcepts'] as List<dynamic>?)
?.map((c) => Map<String, dynamic>.from(c as Map))
.toList() ?? [];
// Encontrar o conceito específico para remover
final conceptToRemove = masteredConcepts.firstWhere(
(c) => c['conceptName'] == quizId,
orElse: () => {},
);
if (conceptToRemove.isNotEmpty) {
await FirebaseFirestore.instance
.collection('userStats')
.doc(user.uid)
.update({
'masteredConcepts': FieldValue.arrayRemove([conceptToRemove])
});
}
}
}
}
}
_loadQuizHistory(); // Recarregar lista
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(successMessage),
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,
),
);
}
}
}
Future<bool> _showDeleteConfirmation(String quizTitle, String message) async {
final result = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(_userRole == 'teacher' ? 'Eliminar Quiz' : 'Confirmar Eliminação'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancelar'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(true),
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Eliminar'),
),
],
),
);
return result ?? false;
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
title: Text(_userRole == 'teacher' ? 'Gerenciar Quizzes' : 'Meu Histórico'),
backgroundColor: cs.surface,
foregroundColor: cs.onSurface,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => context.go(_userRole == 'teacher'
? '/teacher-dashboard'
: '/student-dashboard'),
),
),
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
cs.primary.withValues(alpha: 0.05),
cs.surface,
],
),
),
child: _loading
? const Center(child: CircularProgressIndicator())
: _quizHistory.isEmpty
? _buildEmptyState()
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _quizHistory.length,
itemBuilder: (context, index) {
final quiz = _quizHistory[index];
return _buildQuizCard(quiz)
.animate()
.slideX(duration: const Duration(milliseconds: 300))
.then(delay: Duration(milliseconds: index * 50));
},
),
),
);
}
Widget _buildEmptyState() {
final cs = Theme.of(context).colorScheme;
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_userRole == 'teacher' ? Icons.quiz_outlined : Icons.history_edu_outlined,
size: 64,
color: cs.onSurfaceVariant,
),
const SizedBox(height: 16),
Text(
_userRole == 'teacher' ? 'Nenhum quiz criado' : 'Nenhum quiz no histórico',
style: TextStyle(
color: cs.onSurface,
fontSize: 20,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_userRole == 'teacher'
? 'Crie seu primeiro quiz para começar!'
: 'Complete alguns quizzes para ver seu histórico aqui.',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 14,
),
textAlign: TextAlign.center,
),
],
),
),
);
}
Widget _buildQuizCard(Map<String, dynamic> quiz) {
final cs = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.only(bottom: 12),
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.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
quiz['title'] ?? 'Quiz sem título',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
if (_userRole == 'student' && quiz['score'] != null) ...[
Text(
'Pontuação: ${quiz['score']}/${quiz['totalQuestions'] ?? '?'}',
style: TextStyle(
color: cs.primary,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
Text(
_userRole == 'teacher' || quiz['type'] == 'created'
? 'Criado em: ${_formatDate(quiz['createdAt'])}'
: 'Completado em: ${_formatDate(quiz['completedAt'])}',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 12,
),
),
],
),
),
IconButton(
onPressed: () => _deleteQuiz(quiz['id'], quiz['title'] ?? 'Quiz', quiz['type'] ?? 'unknown'),
icon: Icon(
Icons.delete_outline,
color: Colors.red,
),
tooltip: _userRole == 'teacher' ? 'Eliminar Quiz' : 'Remover',
),
],
),
if (_userRole == 'teacher' && quiz['classIds'] != null) ...[
const SizedBox(height: 8),
Wrap(
spacing: 4,
children: (quiz['classIds'] as List<dynamic>).map((classId) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: cs.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Turma: $classId',
style: TextStyle(
color: cs.onPrimaryContainer,
fontSize: 10,
),
),
);
}).toList(),
),
],
],
),
);
}
String _formatDate(dynamic date) {
if (date == null) return 'Data desconhecida';
DateTime dateTime;
if (date is Timestamp) {
dateTime = date.toDate();
} else if (date is DateTime) {
dateTime = date;
} else {
return 'Data desconhecida';
}
return '${dateTime.day}/${dateTime.month}/${dateTime.year}';
}
}