placeholders removidos e todos os dados reais colocados, com conquistas e tudo
This commit is contained in:
334
lib/core/models/achievement.dart
Normal file
334
lib/core/models/achievement.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
/// Model para definição de conquistas
|
||||
class Achievement {
|
||||
final String id;
|
||||
final String name;
|
||||
final String description;
|
||||
final String icon;
|
||||
final String category; // 'streak', 'study_time', 'quiz', 'concept'
|
||||
final AchievementRequirement requirements;
|
||||
final int points;
|
||||
final String rarity; // 'common', 'rare', 'epic', 'legendary'
|
||||
final bool isActive;
|
||||
final DateTime createdAt;
|
||||
final String? createdBy; // teacherId se criada por professor
|
||||
|
||||
const Achievement({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.icon,
|
||||
required this.category,
|
||||
required this.requirements,
|
||||
required this.points,
|
||||
required this.rarity,
|
||||
required this.isActive,
|
||||
required this.createdAt,
|
||||
this.createdBy,
|
||||
});
|
||||
|
||||
factory Achievement.fromFirestore(Map<String, dynamic> data, String id) {
|
||||
return Achievement(
|
||||
id: id,
|
||||
name: data['name'] ?? '',
|
||||
description: data['description'] ?? '',
|
||||
icon: data['icon'] ?? 'star',
|
||||
category: data['category'] ?? 'general',
|
||||
requirements: AchievementRequirement.fromFirestore(data['requirements'] ?? {}),
|
||||
points: data['points'] ?? 0,
|
||||
rarity: data['rarity'] ?? 'common',
|
||||
isActive: data['isActive'] ?? true,
|
||||
createdAt: (data['createdAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
createdBy: data['createdBy'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
'icon': icon,
|
||||
'category': category,
|
||||
'requirements': requirements.toFirestore(),
|
||||
'points': points,
|
||||
'rarity': rarity,
|
||||
'isActive': isActive,
|
||||
'createdAt': Timestamp.fromDate(createdAt),
|
||||
if (createdBy != null) 'createdBy': createdBy,
|
||||
};
|
||||
}
|
||||
|
||||
Achievement copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? icon,
|
||||
String? category,
|
||||
AchievementRequirement? requirements,
|
||||
int? points,
|
||||
String? rarity,
|
||||
bool? isActive,
|
||||
String? createdBy,
|
||||
}) {
|
||||
return Achievement(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
icon: icon ?? this.icon,
|
||||
category: category ?? this.category,
|
||||
requirements: requirements ?? this.requirements,
|
||||
points: points ?? this.points,
|
||||
rarity: rarity ?? this.rarity,
|
||||
isActive: isActive ?? this.isActive,
|
||||
createdAt: createdAt,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Requisitos para desbloquear uma conquista
|
||||
class AchievementRequirement {
|
||||
final String type; // 'streak_days', 'study_time', 'quiz_score', 'concepts_mastered', 'quiz_completion'
|
||||
final num value;
|
||||
final String operator; // '>=', '==', '>'
|
||||
final Map<String, dynamic>? additionalParams;
|
||||
|
||||
const AchievementRequirement({
|
||||
required this.type,
|
||||
required this.value,
|
||||
required this.operator,
|
||||
this.additionalParams,
|
||||
});
|
||||
|
||||
factory AchievementRequirement.fromFirestore(Map<String, dynamic> data) {
|
||||
return AchievementRequirement(
|
||||
type: data['type'] ?? '',
|
||||
value: data['value'] ?? 0,
|
||||
operator: data['operator'] ?? '>=',
|
||||
additionalParams: data['additionalParams'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'type': type,
|
||||
'value': value,
|
||||
'operator': operator,
|
||||
if (additionalParams != null) 'additionalParams': additionalParams,
|
||||
};
|
||||
}
|
||||
|
||||
bool checkCondition(num currentValue) {
|
||||
switch (operator) {
|
||||
case '>=':
|
||||
return currentValue >= value;
|
||||
case '==':
|
||||
return currentValue == value;
|
||||
case '>':
|
||||
return currentValue > value;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Conquistas predefinidas do sistema
|
||||
class SystemAchievements {
|
||||
static List<Achievement> get defaultAchievements => [
|
||||
Achievement(
|
||||
id: 'first_quiz',
|
||||
name: 'Primeiro Passo',
|
||||
description: 'Complete seu primeiro quiz',
|
||||
icon: 'emoji_events',
|
||||
category: 'quiz',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 1,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 10,
|
||||
rarity: 'common',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'week_streak',
|
||||
name: 'Semana de Dedicação',
|
||||
description: 'Mantenha uma streak de 7 dias',
|
||||
icon: 'local_fire_department',
|
||||
category: 'streak',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'streak_days',
|
||||
value: 7,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 50,
|
||||
rarity: 'rare',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'study_marathon',
|
||||
name: 'Maratona de Estudos',
|
||||
description: 'Estude por 100 minutos em um dia',
|
||||
icon: 'schedule',
|
||||
category: 'study_time',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'study_time',
|
||||
value: 100,
|
||||
operator: '>=',
|
||||
additionalParams: {'period': 'daily'},
|
||||
),
|
||||
points: 30,
|
||||
rarity: 'rare',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'first_quiz',
|
||||
name: 'Primeiro Passo',
|
||||
description: 'Complete seu primeiro quiz',
|
||||
icon: 'emoji_events',
|
||||
category: 'quiz',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 1,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 10,
|
||||
rarity: 'common',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'perfect_score',
|
||||
name: 'Perfeição',
|
||||
description: 'Obtenha 100% em um quiz',
|
||||
icon: 'star',
|
||||
category: 'quiz',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_score',
|
||||
value: 100,
|
||||
operator: '==',
|
||||
),
|
||||
points: 25,
|
||||
rarity: 'rare',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'concept_master',
|
||||
name: 'Mestre de Conceitos',
|
||||
description: 'Domine 5 conceitos',
|
||||
icon: 'school',
|
||||
category: 'concept',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'concepts_mastered',
|
||||
value: 5,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 40,
|
||||
rarity: 'epic',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'month_streak',
|
||||
name: 'Lendário',
|
||||
description: 'Mantenha uma streak de 30 dias',
|
||||
icon: 'whatshot',
|
||||
category: 'streak',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'streak_days',
|
||||
value: 30,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 200,
|
||||
rarity: 'legendary',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
// Conquistas genéricas de número de quizzes
|
||||
Achievement(
|
||||
id: 'quiz_novice_5',
|
||||
name: 'Iniciante',
|
||||
description: 'Complete 5 quizzes',
|
||||
icon: 'emoji_events',
|
||||
category: 'quiz_count',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 5,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 15,
|
||||
rarity: 'common',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'quiz_intermediate_10',
|
||||
name: 'Estudante Dedicao',
|
||||
description: 'Complete 10 quizzes',
|
||||
icon: 'school',
|
||||
category: 'quiz_count',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 10,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 30,
|
||||
rarity: 'common',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'quiz_advanced_25',
|
||||
name: 'Mestre dos Quizzes',
|
||||
description: 'Complete 25 quizzes',
|
||||
icon: 'military_tech',
|
||||
category: 'quiz_count',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 25,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 75,
|
||||
rarity: 'rare',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'quiz_expert_50',
|
||||
name: 'Especialista',
|
||||
description: 'Complete 50 quizzes',
|
||||
icon: 'workspace_premium',
|
||||
category: 'quiz_count',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 50,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 150,
|
||||
rarity: 'epic',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
Achievement(
|
||||
id: 'quiz_legend_100',
|
||||
name: 'Lenda dos Quizzes',
|
||||
description: 'Complete 100 quizzes',
|
||||
icon: 'stars',
|
||||
category: 'quiz_count',
|
||||
requirements: AchievementRequirement(
|
||||
type: 'quiz_completion',
|
||||
value: 100,
|
||||
operator: '>=',
|
||||
),
|
||||
points: 300,
|
||||
rarity: 'legendary',
|
||||
isActive: true,
|
||||
createdAt: DateTime.now(),
|
||||
),
|
||||
];
|
||||
}
|
||||
195
lib/core/models/class_stats.dart
Normal file
195
lib/core/models/class_stats.dart
Normal file
@@ -0,0 +1,195 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
/// Model para estatísticas da turma (professor)
|
||||
class ClassStats {
|
||||
final String classId;
|
||||
final String teacherId;
|
||||
final String className;
|
||||
final int totalStudents;
|
||||
final int activeStudents;
|
||||
final double averageProgress;
|
||||
final int totalQuizzes;
|
||||
final int activeQuizzes;
|
||||
final int totalContent;
|
||||
final List<WeeklyStats> weeklyStats;
|
||||
final List<StudentNeedingSupport> studentsNeedingSupport;
|
||||
|
||||
const ClassStats({
|
||||
required this.classId,
|
||||
required this.teacherId,
|
||||
required this.className,
|
||||
required this.totalStudents,
|
||||
required this.activeStudents,
|
||||
required this.averageProgress,
|
||||
required this.totalQuizzes,
|
||||
required this.activeQuizzes,
|
||||
required this.totalContent,
|
||||
required this.weeklyStats,
|
||||
required this.studentsNeedingSupport,
|
||||
});
|
||||
|
||||
factory ClassStats.fromFirestore(Map<String, dynamic> data, String classId) {
|
||||
return ClassStats(
|
||||
classId: classId,
|
||||
teacherId: data['teacherId'] ?? '',
|
||||
className: data['className'] ?? '',
|
||||
totalStudents: data['totalStudents'] ?? 0,
|
||||
activeStudents: data['activeStudents'] ?? 0,
|
||||
averageProgress: (data['averageProgress'] ?? 0).toDouble(),
|
||||
totalQuizzes: data['totalQuizzes'] ?? 0,
|
||||
activeQuizzes: data['activeQuizzes'] ?? 0,
|
||||
totalContent: data['totalContent'] ?? 0,
|
||||
weeklyStats: (data['weeklyStats'] as List<dynamic>?)
|
||||
?.map((w) => WeeklyStats.fromFirestore(w))
|
||||
.toList() ??
|
||||
[],
|
||||
studentsNeedingSupport: (data['studentsNeedingSupport'] as List<dynamic>?)
|
||||
?.map((s) => StudentNeedingSupport.fromFirestore(s))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'teacherId': teacherId,
|
||||
'className': className,
|
||||
'totalStudents': totalStudents,
|
||||
'activeStudents': activeStudents,
|
||||
'averageProgress': averageProgress,
|
||||
'totalQuizzes': totalQuizzes,
|
||||
'activeQuizzes': activeQuizzes,
|
||||
'totalContent': totalContent,
|
||||
'weeklyStats': weeklyStats.map((w) => w.toFirestore()).toList(),
|
||||
'studentsNeedingSupport': studentsNeedingSupport.map((s) => s.toFirestore()).toList(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Estatísticas semanais da turma
|
||||
class WeeklyStats {
|
||||
final DateTime weekStart;
|
||||
final int activeStudents;
|
||||
final double averageScore;
|
||||
final int totalStudyTime;
|
||||
|
||||
const WeeklyStats({
|
||||
required this.weekStart,
|
||||
required this.activeStudents,
|
||||
required this.averageScore,
|
||||
required this.totalStudyTime,
|
||||
});
|
||||
|
||||
factory WeeklyStats.fromFirestore(Map<String, dynamic> data) {
|
||||
return WeeklyStats(
|
||||
weekStart: (data['weekStart'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
activeStudents: data['activeStudents'] ?? 0,
|
||||
averageScore: (data['averageScore'] ?? 0).toDouble(),
|
||||
totalStudyTime: data['totalStudyTime'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'weekStart': Timestamp.fromDate(weekStart),
|
||||
'activeStudents': activeStudents,
|
||||
'averageScore': averageScore,
|
||||
'totalStudyTime': totalStudyTime,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Aluno que precisa de apoio
|
||||
class StudentNeedingSupport {
|
||||
final String studentId;
|
||||
final String studentName;
|
||||
final String reason; // 'low_scores', 'inactivity', 'struggling_concept'
|
||||
final DateTime lastActivity;
|
||||
final double averageScore;
|
||||
|
||||
const StudentNeedingSupport({
|
||||
required this.studentId,
|
||||
required this.studentName,
|
||||
required this.reason,
|
||||
required this.lastActivity,
|
||||
required this.averageScore,
|
||||
});
|
||||
|
||||
factory StudentNeedingSupport.fromFirestore(Map<String, dynamic> data) {
|
||||
return StudentNeedingSupport(
|
||||
studentId: data['studentId'] ?? '',
|
||||
studentName: data['studentName'] ?? '',
|
||||
reason: data['reason'] ?? '',
|
||||
lastActivity: (data['lastActivity'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
averageScore: (data['averageScore'] ?? 0).toDouble(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'studentId': studentId,
|
||||
'studentName': studentName,
|
||||
'reason': reason,
|
||||
'lastActivity': Timestamp.fromDate(lastActivity),
|
||||
'averageScore': averageScore,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Ranking de alunos em uma turma
|
||||
class StudentRanking {
|
||||
final String studentId;
|
||||
final String studentName;
|
||||
final String studentEmail;
|
||||
final double overallScore;
|
||||
final int completedQuizzes;
|
||||
final int totalQuizzes;
|
||||
final double quizCompletionRate;
|
||||
final int studyTimeMinutes;
|
||||
final int currentStreak;
|
||||
final DateTime lastActivity;
|
||||
|
||||
const StudentRanking({
|
||||
required this.studentId,
|
||||
required this.studentName,
|
||||
required this.studentEmail,
|
||||
required this.overallScore,
|
||||
required this.completedQuizzes,
|
||||
required this.totalQuizzes,
|
||||
required this.quizCompletionRate,
|
||||
required this.studyTimeMinutes,
|
||||
required this.currentStreak,
|
||||
required this.lastActivity,
|
||||
});
|
||||
|
||||
double get quizCompletionPercentage => quizCompletionRate * 100;
|
||||
|
||||
factory StudentRanking.fromFirestore(Map<String, dynamic> data, String studentId) {
|
||||
return StudentRanking(
|
||||
studentId: studentId,
|
||||
studentName: data['studentName'] ?? '',
|
||||
studentEmail: data['studentEmail'] ?? '',
|
||||
overallScore: (data['overallScore'] ?? 0).toDouble(),
|
||||
completedQuizzes: data['completedQuizzes'] ?? 0,
|
||||
totalQuizzes: data['totalQuizzes'] ?? 0,
|
||||
quizCompletionRate: (data['quizCompletionRate'] ?? 0).toDouble(),
|
||||
studyTimeMinutes: data['studyTimeMinutes'] ?? 0,
|
||||
currentStreak: data['currentStreak'] ?? 0,
|
||||
lastActivity: (data['lastActivity'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'studentName': studentName,
|
||||
'studentEmail': studentEmail,
|
||||
'overallScore': overallScore,
|
||||
'completedQuizzes': completedQuizzes,
|
||||
'totalQuizzes': totalQuizzes,
|
||||
'quizCompletionRate': quizCompletionRate,
|
||||
'studyTimeMinutes': studyTimeMinutes,
|
||||
'currentStreak': currentStreak,
|
||||
'lastActivity': Timestamp.fromDate(lastActivity),
|
||||
};
|
||||
}
|
||||
}
|
||||
151
lib/core/models/user_stats.dart
Normal file
151
lib/core/models/user_stats.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
|
||||
/// Model para estatísticas do usuário (aluno)
|
||||
class UserStats {
|
||||
final String userId;
|
||||
final int currentStreak;
|
||||
final int longestStreak;
|
||||
final int totalStudyTime; // em minutos
|
||||
final DateTime? lastActivityDate;
|
||||
final int weeklyStudyTime; // minutos esta semana
|
||||
final int monthlyStudyTime; // minutos este mês
|
||||
final int completedQuizzes; // total de quizzes completos
|
||||
final List<MasteredConcept> masteredConcepts;
|
||||
final List<UnlockedAchievement> unlockedAchievements;
|
||||
|
||||
const UserStats({
|
||||
required this.userId,
|
||||
required this.currentStreak,
|
||||
required this.longestStreak,
|
||||
required this.totalStudyTime,
|
||||
required this.weeklyStudyTime,
|
||||
required this.monthlyStudyTime,
|
||||
required this.completedQuizzes,
|
||||
required this.masteredConcepts,
|
||||
required this.unlockedAchievements,
|
||||
this.lastActivityDate,
|
||||
});
|
||||
|
||||
factory UserStats.fromFirestore(Map<String, dynamic> data, String userId) {
|
||||
return UserStats(
|
||||
userId: userId,
|
||||
currentStreak: data['currentStreak'] ?? 0,
|
||||
longestStreak: data['longestStreak'] ?? 0,
|
||||
totalStudyTime: data['totalStudyTime'] ?? 0,
|
||||
lastActivityDate: (data['lastActivityDate'] as Timestamp?)?.toDate(),
|
||||
weeklyStudyTime: data['weeklyStudyTime'] ?? 0,
|
||||
monthlyStudyTime: data['monthlyStudyTime'] ?? 0,
|
||||
completedQuizzes: data['completedQuizzes'] ?? 0,
|
||||
masteredConcepts: (data['masteredConcepts'] as List<dynamic>?)
|
||||
?.map((c) => MasteredConcept.fromFirestore(c))
|
||||
.toList() ??
|
||||
[],
|
||||
unlockedAchievements: (data['unlockedAchievements'] as List<dynamic>?)
|
||||
?.map((a) => UnlockedAchievement.fromFirestore(a))
|
||||
.toList() ??
|
||||
[],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
final data = {
|
||||
'currentStreak': currentStreak,
|
||||
'longestStreak': longestStreak,
|
||||
'totalStudyTime': totalStudyTime,
|
||||
'weeklyStudyTime': weeklyStudyTime,
|
||||
'monthlyStudyTime': monthlyStudyTime,
|
||||
'completedQuizzes': completedQuizzes,
|
||||
'masteredConcepts': masteredConcepts.map((c) => c.toFirestore()).toList(),
|
||||
'unlockedAchievements': unlockedAchievements.map((a) => a.toFirestore()).toList(),
|
||||
};
|
||||
|
||||
if (lastActivityDate != null) {
|
||||
data['lastActivityDate'] = Timestamp.fromDate(lastActivityDate!);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
UserStats copyWith({
|
||||
int? currentStreak,
|
||||
int? longestStreak,
|
||||
int? totalStudyTime,
|
||||
DateTime? lastActivityDate,
|
||||
int? weeklyStudyTime,
|
||||
int? monthlyStudyTime,
|
||||
int? completedQuizzes,
|
||||
List<MasteredConcept>? masteredConcepts,
|
||||
List<UnlockedAchievement>? unlockedAchievements,
|
||||
}) {
|
||||
return UserStats(
|
||||
userId: userId,
|
||||
currentStreak: currentStreak ?? this.currentStreak,
|
||||
longestStreak: longestStreak ?? this.longestStreak,
|
||||
totalStudyTime: totalStudyTime ?? this.totalStudyTime,
|
||||
lastActivityDate: lastActivityDate ?? this.lastActivityDate,
|
||||
weeklyStudyTime: weeklyStudyTime ?? this.weeklyStudyTime,
|
||||
monthlyStudyTime: monthlyStudyTime ?? this.monthlyStudyTime,
|
||||
completedQuizzes: completedQuizzes ?? this.completedQuizzes,
|
||||
masteredConcepts: masteredConcepts ?? this.masteredConcepts,
|
||||
unlockedAchievements: unlockedAchievements ?? this.unlockedAchievements,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Conceito dominado pelo aluno
|
||||
class MasteredConcept {
|
||||
final String conceptName;
|
||||
final DateTime masteredAt;
|
||||
final int masteryLevel; // 0-100
|
||||
|
||||
const MasteredConcept({
|
||||
required this.conceptName,
|
||||
required this.masteredAt,
|
||||
required this.masteryLevel,
|
||||
});
|
||||
|
||||
factory MasteredConcept.fromFirestore(Map<String, dynamic> data) {
|
||||
return MasteredConcept(
|
||||
conceptName: data['conceptName'] ?? '',
|
||||
masteredAt: (data['masteredAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
masteryLevel: data['masteryLevel'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'conceptName': conceptName,
|
||||
'masteredAt': Timestamp.fromDate(masteredAt),
|
||||
'masteryLevel': masteryLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Conquista desbloqueada pelo aluno
|
||||
class UnlockedAchievement {
|
||||
final String achievementId;
|
||||
final DateTime unlockedAt;
|
||||
final Map<String, dynamic> metadata;
|
||||
|
||||
const UnlockedAchievement({
|
||||
required this.achievementId,
|
||||
required this.unlockedAt,
|
||||
required this.metadata,
|
||||
});
|
||||
|
||||
factory UnlockedAchievement.fromFirestore(Map<String, dynamic> data) {
|
||||
return UnlockedAchievement(
|
||||
achievementId: data['achievementId'] ?? '',
|
||||
unlockedAt: (data['unlockedAt'] as Timestamp?)?.toDate() ?? DateTime.now(),
|
||||
metadata: data['metadata'] ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toFirestore() {
|
||||
return {
|
||||
'achievementId': achievementId,
|
||||
'unlockedAt': Timestamp.fromDate(unlockedAt),
|
||||
'metadata': metadata,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import '../../features/quiz/presentation/pages/teacher_quiz_page.dart';
|
||||
import '../../features/profile/presentation/pages/profile_page.dart';
|
||||
import '../../features/splash/presentation/pages/splash_page.dart';
|
||||
import '../../features/auth/presentation/pages/role_selection_page.dart';
|
||||
import '../../features/analytics/presentation/pages/analytics_page.dart';
|
||||
import '../../features/achievements/presentation/pages/student_achievements_page.dart';
|
||||
import '../../features/quiz/presentation/pages/quiz_management_page.dart';
|
||||
|
||||
/// App Router Configuration
|
||||
class AppRouter {
|
||||
@@ -29,6 +32,9 @@ class AppRouter {
|
||||
static const String quiz = '/quiz/:quizId';
|
||||
static const String profile = '/profile';
|
||||
static const String settings = '/settings';
|
||||
static const String teacherAnalytics = '/teacher/analytics';
|
||||
static const String studentAchievements = '/student/achievements';
|
||||
static const String quizManagement = '/quiz-management';
|
||||
|
||||
// Nested route paths (without leading slash)
|
||||
static const String tutorNested = 'tutor';
|
||||
@@ -141,6 +147,27 @@ class AppRouter {
|
||||
builder: (context, state) => const TutorChatPageSimple(),
|
||||
),
|
||||
|
||||
// Teacher Analytics Route
|
||||
GoRoute(
|
||||
path: teacherAnalytics,
|
||||
name: 'teacherAnalytics',
|
||||
builder: (context, state) => const AnalyticsPage(),
|
||||
),
|
||||
|
||||
// Student Achievements Route
|
||||
GoRoute(
|
||||
path: studentAchievements,
|
||||
name: 'studentAchievements',
|
||||
builder: (context, state) => const StudentAchievementsPage(),
|
||||
),
|
||||
|
||||
// Quiz Management Route
|
||||
GoRoute(
|
||||
path: quizManagement,
|
||||
name: 'quizManagement',
|
||||
builder: (context, state) => const QuizManagementPage(),
|
||||
),
|
||||
|
||||
// Quiz List Route (independent — student access)
|
||||
GoRoute(
|
||||
path: quizList,
|
||||
|
||||
734
lib/core/services/gamification_service.dart
Normal file
734
lib/core/services/gamification_service.dart
Normal file
@@ -0,0 +1,734 @@
|
||||
import 'package:cloud_firestore/cloud_firestore.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../models/user_stats.dart';
|
||||
import '../models/class_stats.dart';
|
||||
import '../models/achievement.dart';
|
||||
import '../utils/logger.dart';
|
||||
|
||||
/// Serviço para gerenciar gamificação e conquistas
|
||||
class GamificationService {
|
||||
static final FirebaseFirestore _firestore = FirebaseFirestore.instance;
|
||||
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
/// Atualizar streak diário do usuário
|
||||
static Future<void> updateDailyStreak(String userId) async {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
final userStatsDoc = await userStatsRef.get();
|
||||
|
||||
if (!userStatsDoc.exists) {
|
||||
// Criar estatísticas iniciais
|
||||
await userStatsRef.set({
|
||||
'userId': userId,
|
||||
'currentStreak': 1,
|
||||
'longestStreak': 1,
|
||||
'totalStudyTime': 0,
|
||||
'lastActivityDate': Timestamp.now(),
|
||||
'weeklyStudyTime': 0,
|
||||
'monthlyStudyTime': 0,
|
||||
'masteredConcepts': [],
|
||||
'unlockedAchievements': [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
||||
final now = DateTime.now();
|
||||
final lastActivity = userStats.lastActivityDate;
|
||||
|
||||
int newStreak = userStats.currentStreak;
|
||||
int newLongestStreak = userStats.longestStreak;
|
||||
|
||||
Logger.info('=== UPDATE DAILY STREAK DEBUG ===');
|
||||
Logger.info('Last activity: $lastActivity');
|
||||
Logger.info('Current streak before update: $newStreak');
|
||||
|
||||
if (lastActivity == null) {
|
||||
// Primeira atividade - iniciar streak
|
||||
Logger.info('First activity detected - setting streak to 1');
|
||||
newStreak = 1;
|
||||
newLongestStreak = 1;
|
||||
} else {
|
||||
// Normalizar para início do dia para comparação correta
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final lastDay = DateTime(lastActivity.year, lastActivity.month, lastActivity.day);
|
||||
final difference = today.difference(lastDay).inDays;
|
||||
|
||||
if (difference == 0) {
|
||||
// Já ativou hoje, não alterar streak mas atualiza timestamp
|
||||
// Corrigir streak se estiver em 0 (erro de dados)
|
||||
if (newStreak == 0) {
|
||||
newStreak = 1;
|
||||
newLongestStreak = 1;
|
||||
Logger.info('Correcting invalid streak (0) to 1');
|
||||
await userStatsRef.update({
|
||||
'currentStreak': 1,
|
||||
'longestStreak': 1,
|
||||
'lastActivityDate': Timestamp.now(),
|
||||
});
|
||||
} else {
|
||||
Logger.info('Already active today, streak unchanged: $newStreak');
|
||||
await userStatsRef.update({
|
||||
'lastActivityDate': Timestamp.now(),
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else if (difference == 1) {
|
||||
Logger.info('Consecutive activity detected, incrementing streak');
|
||||
// Atividade consecutiva
|
||||
newStreak++;
|
||||
newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak;
|
||||
} else {
|
||||
// Quebrou o streak
|
||||
newStreak = 1;
|
||||
newLongestStreak = newStreak > newLongestStreak ? newStreak : newLongestStreak;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('Updating streak to: $newStreak');
|
||||
await userStatsRef.update({
|
||||
'currentStreak': newStreak,
|
||||
'longestStreak': newLongestStreak,
|
||||
'lastActivityDate': Timestamp.now(),
|
||||
});
|
||||
|
||||
Logger.info('Streak updated successfully');
|
||||
// Verificar conquistas de streak
|
||||
await _checkStreakAchievements(userId, newStreak);
|
||||
Logger.info('=== END UPDATE DAILY STREAK DEBUG ===');
|
||||
} catch (e) {
|
||||
Logger.error('Error updating daily streak: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Registrar tempo de estudo
|
||||
static Future<void> recordStudyTime(String userId, int minutes) async {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
final userStatsDoc = await userStatsRef.get();
|
||||
|
||||
if (!userStatsDoc.exists) {
|
||||
await _createInitialUserStats(userId);
|
||||
}
|
||||
|
||||
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
||||
final newTotalTime = userStats.totalStudyTime + minutes;
|
||||
|
||||
await userStatsRef.update({
|
||||
'totalStudyTime': newTotalTime,
|
||||
'weeklyStudyTime': FieldValue.increment(minutes),
|
||||
'monthlyStudyTime': FieldValue.increment(minutes),
|
||||
});
|
||||
|
||||
// Verificar conquistas de tempo de estudo
|
||||
await _checkStudyTimeAchievements(userId, newTotalTime);
|
||||
} catch (e) {
|
||||
Logger.error('Error recording study time: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Registrar atividade de quiz
|
||||
static Future<void> recordQuizActivity(String userId, {
|
||||
required int score,
|
||||
required int totalQuestions,
|
||||
required String materialName,
|
||||
}) async {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
|
||||
// Atualizar streak
|
||||
await updateDailyStreak(userId);
|
||||
|
||||
// Registrar tempo de estudo (estimado)
|
||||
await recordStudyTime(userId, 15); // 15 minutos por quiz
|
||||
|
||||
// Verificar conquistas de quiz
|
||||
await _checkQuizAchievements(userId, score, totalQuestions);
|
||||
|
||||
// Incrementar contador de quizzes completos
|
||||
await userStatsRef.update({
|
||||
'completedQuizzes': FieldValue.increment(1),
|
||||
});
|
||||
Logger.info('Incremented completed quizzes count');
|
||||
|
||||
// Atualizar conceitos dominados se score >= 50%
|
||||
Logger.info('Checking if score qualifies for mastered concept: ${score / totalQuestions >= 0.5}');
|
||||
if (score / totalQuestions >= 0.5) {
|
||||
Logger.info('Adding mastered concept: $materialName with score: $score');
|
||||
await _addMasteredConcept(userId, materialName, score);
|
||||
} else {
|
||||
Logger.info('Score too low for mastered concept: $score/$totalQuestions');
|
||||
}
|
||||
|
||||
Logger.info('Quiz activity recorded for user $userId: $score/$totalQuestions');
|
||||
} catch (e) {
|
||||
Logger.error('Error recording quiz activity: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Obter estatísticas do usuário
|
||||
static Future<UserStats?> getUserStats(String userId) async {
|
||||
try {
|
||||
final doc = await _firestore.collection('users').doc(userId).get();
|
||||
|
||||
if (!doc.exists) {
|
||||
// Criar estatísticas iniciais se não existirem
|
||||
await _initializeUserStats(userId);
|
||||
return await getUserStats(userId); // Chamada recursiva após inicialização
|
||||
}
|
||||
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
|
||||
// Garantir que completedQuizzes exista
|
||||
if (!data.containsKey('completedQuizzes')) {
|
||||
await _firestore.collection('users').doc(userId).update({
|
||||
'completedQuizzes': 0,
|
||||
});
|
||||
data['completedQuizzes'] = 0;
|
||||
}
|
||||
|
||||
return UserStats.fromFirestore(data, userId);
|
||||
} catch (e) {
|
||||
Logger.error('Error getting user stats: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obter estatísticas da turma
|
||||
static Future<ClassStats?> getClassStats(String classId) async {
|
||||
try {
|
||||
final classStatsDoc = await _firestore.collection('classStats').doc(classId).get();
|
||||
if (!classStatsDoc.exists) {
|
||||
return await _calculateClassStats(classId);
|
||||
}
|
||||
return ClassStats.fromFirestore(classStatsDoc.data()!, classId);
|
||||
} catch (e) {
|
||||
Logger.error('Error getting class stats: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obter ranking de alunos da turma
|
||||
static Future<List<StudentRanking>> getClassRanking(String classId) async {
|
||||
try {
|
||||
// Primeiro, obter todos os alunos matriculados na turma
|
||||
final enrollmentsSnapshot = await _firestore
|
||||
.collection('enrollments')
|
||||
.where('classId', isEqualTo: classId)
|
||||
.get();
|
||||
|
||||
if (enrollmentsSnapshot.docs.isEmpty) {
|
||||
Logger.info('No students enrolled in class $classId');
|
||||
return [];
|
||||
}
|
||||
|
||||
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc['studentId'] as String).toList();
|
||||
final rankings = <StudentRanking>[];
|
||||
|
||||
// Para cada aluno, obter suas estatísticas
|
||||
for (final studentId in studentIds) {
|
||||
try {
|
||||
final userStats = await getUserStats(studentId);
|
||||
if (userStats != null) {
|
||||
// Obter informações do usuário
|
||||
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
||||
final userData = userDoc.data() as Map<String, dynamic>?;
|
||||
|
||||
// Calcular estatísticas para o ranking
|
||||
// Usar contador real de quizzes completos
|
||||
final completedQuizzes = userStats.completedQuizzes;
|
||||
final totalQuizzes = completedQuizzes + 5; // Estimativa de quizzes disponíveis
|
||||
final quizCompletionRate = totalQuizzes > 0 ? completedQuizzes / totalQuizzes : 0.0;
|
||||
|
||||
// Calcular score geral baseado em múltiplos fatores
|
||||
final overallScore = _calculateOverallScore(userStats, quizCompletionRate);
|
||||
|
||||
rankings.add(StudentRanking(
|
||||
studentId: studentId,
|
||||
studentName: userData?['displayName'] ?? 'Aluno $studentId',
|
||||
studentEmail: userData?['email'] ?? '',
|
||||
overallScore: overallScore,
|
||||
completedQuizzes: completedQuizzes,
|
||||
totalQuizzes: totalQuizzes,
|
||||
quizCompletionRate: quizCompletionRate,
|
||||
currentStreak: userStats.currentStreak,
|
||||
studyTimeMinutes: userStats.totalStudyTime,
|
||||
lastActivity: userStats.lastActivityDate ?? DateTime.now(),
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error getting stats for student $studentId: $e');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Ordenar por score geral
|
||||
rankings.sort((a, b) => b.overallScore.compareTo(a.overallScore));
|
||||
|
||||
return rankings;
|
||||
} catch (e) {
|
||||
Logger.error('Error getting class ranking: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Calcular score geral para ranking
|
||||
static double _calculateOverallScore(UserStats userStats, double quizCompletionRate) {
|
||||
double score = 0.0;
|
||||
|
||||
// Peso de 40% para completion rate de quizzes
|
||||
score += quizCompletionRate * 40;
|
||||
|
||||
// Peso de 20% para streak (máximo 20 pontos)
|
||||
score += (userStats.currentStreak / 30.0 * 20).clamp(0.0, 20.0);
|
||||
|
||||
// Peso de 20% para tempo de estudo (máximo 20 pontos para 10 horas)
|
||||
score += (userStats.totalStudyTime / 600.0 * 20).clamp(0.0, 20.0);
|
||||
|
||||
// Peso de 20% para conceitos dominados (máximo 20 pontos para 10 conceitos)
|
||||
score += (userStats.masteredConcepts.length / 10.0 * 20).clamp(0.0, 20.0);
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/// Criar conquista personalizada (professor)
|
||||
static Future<String> createCustomAchievement({
|
||||
required String teacherId,
|
||||
required String name,
|
||||
required String description,
|
||||
required String icon,
|
||||
required String category,
|
||||
required AchievementRequirement requirements,
|
||||
required int points,
|
||||
required String rarity,
|
||||
}) async {
|
||||
try {
|
||||
final achievementRef = await _firestore.collection('achievements').add({
|
||||
'name': name,
|
||||
'description': description,
|
||||
'icon': icon,
|
||||
'category': category,
|
||||
'requirements': requirements.toFirestore(),
|
||||
'points': points,
|
||||
'rarity': rarity,
|
||||
'isActive': true,
|
||||
'createdAt': Timestamp.now(),
|
||||
'createdBy': teacherId,
|
||||
});
|
||||
|
||||
Logger.info('Custom achievement created: ${achievementRef.id}');
|
||||
return achievementRef.id;
|
||||
} catch (e) {
|
||||
Logger.error('Error creating custom achievement: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Obter conquistas disponíveis
|
||||
static Future<List<Achievement>> getAvailableAchievements({String? teacherId}) async {
|
||||
try {
|
||||
// Sempre incluir conquistas do sistema
|
||||
List<Achievement> achievements = List.from(SystemAchievements.defaultAchievements);
|
||||
|
||||
// Adicionar conquistas personalizadas do professor
|
||||
Query query = _firestore.collection('achievements').where('isActive', isEqualTo: true);
|
||||
|
||||
if (teacherId != null) {
|
||||
query = query.where('createdBy', isEqualTo: teacherId);
|
||||
}
|
||||
|
||||
final snapshot = await query.get();
|
||||
achievements.addAll(
|
||||
snapshot.docs
|
||||
.map((doc) => Achievement.fromFirestore(doc.data() as Map<String, dynamic>, doc.id))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Logger.info('Total achievements loaded: ${achievements.length}');
|
||||
return achievements;
|
||||
} catch (e) {
|
||||
Logger.error('Error getting available achievements: $e');
|
||||
return SystemAchievements.defaultAchievements;
|
||||
}
|
||||
}
|
||||
|
||||
/// Método público para inicializar dados de gamificação (para testes)
|
||||
static Future<void> initializeGamificationData(String userId) async {
|
||||
try {
|
||||
final userStats = await getUserStats(userId);
|
||||
if (userStats == null) {
|
||||
await _createInitialUserStats(userId);
|
||||
Logger.info('Gamification data initialized for user $userId');
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error initializing gamification data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Método público para simular quiz completion (para testes)
|
||||
static Future<void> simulateQuizCompletion(String userId, {
|
||||
required int score,
|
||||
required int totalQuestions,
|
||||
required String materialName,
|
||||
}) async {
|
||||
Logger.info('=== SIMULATING QUIZ COMPLETION ===');
|
||||
Logger.info('User: $userId');
|
||||
Logger.info('Score: $score/$totalQuestions');
|
||||
Logger.info('Material: $materialName');
|
||||
|
||||
await recordQuizActivity(
|
||||
userId,
|
||||
score: score,
|
||||
totalQuestions: totalQuestions,
|
||||
materialName: materialName,
|
||||
);
|
||||
|
||||
// Verificar estado após simulação
|
||||
await debugUserStats(userId);
|
||||
}
|
||||
|
||||
/// Método de debugging para verificar estado completo do usuário
|
||||
static Future<void> debugUserStats(String userId) async {
|
||||
try {
|
||||
Logger.info('=== DEBUGGING USER STATS ===');
|
||||
|
||||
final userStats = await getUserStats(userId);
|
||||
if (userStats == null) {
|
||||
Logger.error('User stats not found for $userId');
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.info('Current Streak: ${userStats.currentStreak}');
|
||||
Logger.info('Longest Streak: ${userStats.longestStreak}');
|
||||
Logger.info('Total Study Time: ${userStats.totalStudyTime}');
|
||||
Logger.info('Weekly Study Time: ${userStats.weeklyStudyTime}');
|
||||
Logger.info('Monthly Study Time: ${userStats.monthlyStudyTime}');
|
||||
Logger.info('Mastered Concepts: ${userStats.masteredConcepts.length}');
|
||||
Logger.info('Unlocked Achievements: ${userStats.unlockedAchievements.length}');
|
||||
|
||||
for (final concept in userStats.masteredConcepts) {
|
||||
Logger.info(' - Concept: ${concept.conceptName}, Level: ${concept.masteryLevel}');
|
||||
}
|
||||
|
||||
for (final achievement in userStats.unlockedAchievements) {
|
||||
Logger.info(' - Achievement: ${achievement.achievementId}, Unlocked: ${achievement.unlockedAt}');
|
||||
}
|
||||
|
||||
Logger.info('=== END DEBUG ===');
|
||||
} catch (e) {
|
||||
Logger.error('Error debugging user stats: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Métodos privados
|
||||
|
||||
static Future<void> _createInitialUserStats(String userId) async {
|
||||
await _firestore.collection('users').doc(userId).set({
|
||||
'userId': userId,
|
||||
'currentStreak': 0,
|
||||
'longestStreak': 0,
|
||||
'totalStudyTime': 0,
|
||||
'lastActivityDate': null, // null para que primeira atividade inicie streak
|
||||
'weeklyStudyTime': 0,
|
||||
'monthlyStudyTime': 0,
|
||||
'masteredConcepts': [],
|
||||
'unlockedAchievements': [],
|
||||
});
|
||||
}
|
||||
|
||||
static Future<void> _addMasteredConcept(String userId, String conceptName, int score) async {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
final userStatsDoc = await userStatsRef.get();
|
||||
|
||||
if (!userStatsDoc.exists) return;
|
||||
|
||||
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
||||
|
||||
// Verificar se conceito já está dominado
|
||||
final existingConcept = userStats.masteredConcepts
|
||||
.where((c) => c.conceptName == conceptName)
|
||||
.firstOrNull;
|
||||
|
||||
if (existingConcept != null) {
|
||||
// Atualizar nível de maestria se score maior
|
||||
if (score > existingConcept.masteryLevel) {
|
||||
final updatedConcepts = userStats.masteredConcepts.map((c) {
|
||||
if (c.conceptName == conceptName) {
|
||||
return MasteredConcept(
|
||||
conceptName: conceptName,
|
||||
masteredAt: DateTime.now(),
|
||||
masteryLevel: score,
|
||||
);
|
||||
}
|
||||
return c;
|
||||
}).toList();
|
||||
|
||||
await userStatsRef.update({
|
||||
'masteredConcepts': updatedConcepts.map((c) => c.toFirestore()).toList(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Adicionar novo conceito
|
||||
final newConcept = MasteredConcept(
|
||||
conceptName: conceptName,
|
||||
masteredAt: DateTime.now(),
|
||||
masteryLevel: score,
|
||||
);
|
||||
|
||||
await userStatsRef.update({
|
||||
'masteredConcepts': FieldValue.arrayUnion([newConcept.toFirestore()]),
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error adding mastered concept: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ClassStats> _calculateClassStats(String classId) async {
|
||||
try {
|
||||
// Obter informações da turma
|
||||
final classDoc = await _firestore.collection('classes').doc(classId).get();
|
||||
if (!classDoc.exists) {
|
||||
throw Exception('Class not found');
|
||||
}
|
||||
|
||||
final className = classDoc.data()?['name'] ?? 'Unknown Class';
|
||||
final teacherId = classDoc.data()?['teacherId'] ?? '';
|
||||
|
||||
// Obter alunos matriculados
|
||||
final enrollmentsSnapshot = await _firestore
|
||||
.collection('enrollments')
|
||||
.where('classId', isEqualTo: classId)
|
||||
.get();
|
||||
|
||||
final studentIds = enrollmentsSnapshot.docs.map((doc) => doc.data()['studentId'] as String).toList();
|
||||
|
||||
// Calcular estatísticas
|
||||
int activeStudents = 0;
|
||||
double totalProgress = 0;
|
||||
List<StudentNeedingSupport> needingSupport = [];
|
||||
|
||||
for (final studentId in studentIds) {
|
||||
final userStats = await getUserStats(studentId);
|
||||
if (userStats != null) {
|
||||
// Verificar se está ativo (atividade nos últimos 7 dias)
|
||||
int daysSinceLastActivity = 999; // Valor alto para inatividade
|
||||
if (userStats.lastActivityDate != null) {
|
||||
daysSinceLastActivity = DateTime.now().difference(userStats.lastActivityDate!).inDays;
|
||||
if (daysSinceLastActivity <= 7) {
|
||||
activeStudents++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calcular progresso baseado em conceitos dominados
|
||||
final progress = userStats.masteredConcepts.isEmpty
|
||||
? 0.0
|
||||
: userStats.masteredConcepts.map((c) => c.masteryLevel).reduce((a, b) => a + b) / userStats.masteredConcepts.length / 100;
|
||||
totalProgress += progress;
|
||||
|
||||
// Verificar se precisa de apoio
|
||||
if (progress < 0.5 || daysSinceLastActivity > 14) {
|
||||
final userDoc = await _firestore.collection('users').doc(studentId).get();
|
||||
final userName = userDoc.data()?['displayName'] ?? 'Unknown';
|
||||
|
||||
needingSupport.add(StudentNeedingSupport(
|
||||
studentId: studentId,
|
||||
studentName: userName,
|
||||
reason: progress < 0.5 ? 'low_scores' : 'inactivity',
|
||||
lastActivity: userStats.lastActivityDate ?? DateTime.now(),
|
||||
averageScore: progress * 100,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final averageProgress = studentIds.isEmpty ? 0.0 : totalProgress / studentIds.length;
|
||||
|
||||
// Obter estatísticas de quizzes
|
||||
final quizzesSnapshot = await _firestore
|
||||
.collection('teacherQuizzes')
|
||||
.where('classIds', arrayContains: classId)
|
||||
.get();
|
||||
|
||||
final classStats = ClassStats(
|
||||
classId: classId,
|
||||
teacherId: teacherId,
|
||||
className: className,
|
||||
totalStudents: studentIds.length,
|
||||
activeStudents: activeStudents,
|
||||
averageProgress: averageProgress,
|
||||
totalQuizzes: quizzesSnapshot.docs.length,
|
||||
activeQuizzes: quizzesSnapshot.docs.length, // Simplificado
|
||||
totalContent: 0, // Implementar depois
|
||||
weeklyStats: [],
|
||||
studentsNeedingSupport: needingSupport,
|
||||
);
|
||||
|
||||
// Salvar estatísticas calculadas
|
||||
await _firestore.collection('classStats').doc(classId).set(classStats.toFirestore());
|
||||
|
||||
return classStats;
|
||||
} catch (e) {
|
||||
Logger.error('Error calculating class stats: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkStreakAchievements(String userId, int streakDays) async {
|
||||
final achievements = await getAvailableAchievements();
|
||||
final streakAchievements = achievements.where((a) => a.category == 'streak');
|
||||
|
||||
for (final achievement in streakAchievements) {
|
||||
if (achievement.requirements.checkCondition(streakDays)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkStudyTimeAchievements(String userId, int totalMinutes) async {
|
||||
final achievements = await getAvailableAchievements();
|
||||
final studyAchievements = achievements.where((a) => a.category == 'study_time');
|
||||
|
||||
for (final achievement in studyAchievements) {
|
||||
if (achievement.requirements.checkCondition(totalMinutes)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _checkQuizAchievements(String userId, int score, int totalQuestions) async {
|
||||
Logger.info('=== CHECKING QUIZ ACHIEVEMENTS ===');
|
||||
Logger.info('Score: $score/$totalQuestions');
|
||||
|
||||
final achievements = await getAvailableAchievements();
|
||||
final userStats = await getUserStats(userId);
|
||||
|
||||
if (userStats == null) {
|
||||
Logger.error('User stats null for achievement checking');
|
||||
return;
|
||||
}
|
||||
|
||||
final percentage = (score / totalQuestions) * 100;
|
||||
// Usar contador real de quizzes completos
|
||||
final completedQuizzes = userStats.completedQuizzes;
|
||||
|
||||
Logger.info('Percentage: $percentage%');
|
||||
Logger.info('Completed quizzes: $completedQuizzes');
|
||||
Logger.info('Mastered concepts: ${userStats.masteredConcepts.length}');
|
||||
Logger.info('Available achievements: ${achievements.length}');
|
||||
|
||||
for (final achievement in achievements) {
|
||||
if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_score' &&
|
||||
achievement.requirements.checkCondition(percentage)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
} else if (achievement.category == 'quiz_count' && achievement.requirements.type == 'quiz_completion' &&
|
||||
achievement.requirements.checkCondition(completedQuizzes)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
} else if (achievement.category == 'quiz' && achievement.requirements.type == 'quiz_completion' &&
|
||||
achievement.requirements.checkCondition(1)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
} else if (achievement.category == 'concept' && achievement.requirements.type == 'concepts_mastered' &&
|
||||
achievement.requirements.checkCondition(userStats.masteredConcepts.length)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
} else {
|
||||
Logger.info('Achievement not matched: ${achievement.id} - category: ${achievement.category}, type: ${achievement.requirements.type}');
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info('=== END CHECKING QUIZ ACHIEVEMENTS ===');
|
||||
// Verificar conquistas genéricas de número de quizzes
|
||||
await _checkQuizCountAchievements(userId, completedQuizzes);
|
||||
}
|
||||
|
||||
static Future<void> _checkQuizCountAchievements(String userId, int completedQuizzes) async {
|
||||
final achievements = await getAvailableAchievements();
|
||||
final quizCountAchievements = achievements.where((a) => a.category == 'quiz_count');
|
||||
|
||||
for (final achievement in quizCountAchievements) {
|
||||
if (achievement.requirements.type == 'quiz_completion' &&
|
||||
achievement.requirements.checkCondition(completedQuizzes)) {
|
||||
await _unlockAchievement(userId, achievement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _unlockAchievement(String userId, String achievementId) async {
|
||||
try {
|
||||
Logger.info('=== ATTEMPTING TO UNLOCK ACHIEVEMENT ===');
|
||||
Logger.info('Achievement ID: $achievementId');
|
||||
Logger.info('User ID: $userId');
|
||||
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
final userStatsDoc = await userStatsRef.get();
|
||||
|
||||
if (!userStatsDoc.exists) {
|
||||
Logger.error('User stats document does not exist for user $userId');
|
||||
return;
|
||||
}
|
||||
|
||||
final userStats = UserStats.fromFirestore(userStatsDoc.data()!, userId);
|
||||
Logger.info('Current unlocked achievements count: ${userStats.unlockedAchievements.length}');
|
||||
|
||||
// Verificar se já desbloqueou
|
||||
final alreadyUnlocked = userStats.unlockedAchievements
|
||||
.any((a) => a.achievementId == achievementId);
|
||||
|
||||
Logger.info('Already unlocked: $alreadyUnlocked');
|
||||
|
||||
if (!alreadyUnlocked) {
|
||||
final unlockedAchievement = UnlockedAchievement(
|
||||
achievementId: achievementId,
|
||||
unlockedAt: DateTime.now(),
|
||||
metadata: {},
|
||||
);
|
||||
|
||||
Logger.info('Adding achievement to Firestore...');
|
||||
await userStatsRef.update({
|
||||
'unlockedAchievements': FieldValue.arrayUnion([unlockedAchievement.toFirestore()]),
|
||||
});
|
||||
|
||||
Logger.info('Achievement unlocked successfully: $achievementId for user $userId');
|
||||
} else {
|
||||
Logger.info('Achievement $achievementId already unlocked for user $userId');
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.error('Error unlocking achievement: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Inicializar estatísticas do usuário
|
||||
static Future<void> _initializeUserStats(String userId) async {
|
||||
try {
|
||||
final userStatsRef = _firestore.collection('users').doc(userId);
|
||||
|
||||
// Verificar se já existe
|
||||
final doc = await userStatsRef.get();
|
||||
if (doc.exists) {
|
||||
// Apenas atualizar com completedQuizzes se não existir
|
||||
final data = doc.data() as Map<String, dynamic>;
|
||||
if (!data.containsKey('completedQuizzes')) {
|
||||
await userStatsRef.update({
|
||||
'completedQuizzes': 0,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Criar documento inicial
|
||||
await userStatsRef.set({
|
||||
'completedQuizzes': 0,
|
||||
'currentStreak': 0,
|
||||
'longestStreak': 0,
|
||||
'totalStudyTime': 0,
|
||||
'weeklyStudyTime': 0,
|
||||
'monthlyStudyTime': 0,
|
||||
'masteredConcepts': [],
|
||||
'unlockedAchievements': [],
|
||||
});
|
||||
}
|
||||
|
||||
Logger.info('User stats initialized for user $userId');
|
||||
} catch (e) {
|
||||
Logger.error('Error initializing user stats: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user