Files
LearnIT/lib/features/analytics/presentation/widgets/class_ranking_widget.dart

433 lines
14 KiB
Dart

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;
}
}