Skip to content

Commit edf4471

Browse files
committed
✨ feat: Se implementó el final de semestre
1 parent f65ff4e commit edf4471

8 files changed

Lines changed: 605 additions & 29 deletions

File tree

lib/data/models/enrollment_model.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,39 @@ import 'package:cloud_firestore/cloud_firestore.dart';
22

33
class EnrollmentModel {
44
final String id;
5+
final String uid; // <-- AÑADIDO
56
final String subject;
67
final String professor;
78
final String schedule;
89
final String salon;
910
final String status;
11+
final String academy; // <-- AÑADIDO
1012
final DateTime assignedAt;
11-
final double? finalGrade; // <-- Nueva propiedad opcional
13+
final double? finalGrade;
1214

1315
EnrollmentModel({
1416
required this.id,
17+
required this.uid, // <-- AÑADIDO
1518
required this.subject,
1619
required this.professor,
1720
required this.schedule,
1821
required this.salon,
1922
required this.status,
23+
required this.academy, // <-- AÑADIDO
2024
required this.assignedAt,
21-
this.finalGrade, // <-- opcional
25+
this.finalGrade,
2226
});
2327

24-
// Factory constructor to create an instance from a Firestore document
2528
factory EnrollmentModel.fromMap(Map<String, dynamic> data, String documentId) {
2629
return EnrollmentModel(
2730
id: documentId,
31+
uid: data['uid'] ?? 'N/A', // <-- AÑADIDO
2832
subject: data['subject'] ?? 'N/A',
2933
professor: data['professor'] ?? 'N/A',
3034
schedule: data['schedule'] ?? 'N/A',
3135
salon: data['salon'] ?? 'N/A',
3236
status: data['status'] ?? 'N/A',
37+
academy: data['academy'] ?? 'N/A', // <-- AÑADIDO
3338
assignedAt: (data['assigned_at'] as Timestamp?)?.toDate() ?? DateTime.now(),
3439
finalGrade: (data['final_grade'] != null) ? (data['final_grade'] as num).toDouble() : null,
3540
);
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import 'package:flutter/foundation.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:cloud_firestore/cloud_firestore.dart';
4+
import '../../../data/models/enrollment_model.dart';
5+
import '../../../data/models/user_model.dart';
6+
import '../../../data/repositories/auth_repository.dart';
7+
8+
// Para la generación de Excel
9+
import 'package:universal_html/html.dart' as html;
10+
import 'package:excel/excel.dart';
11+
12+
// Definiendo un tipo para las estadísticas
13+
typedef Stats = ({int accredited, int notAccredited});
14+
15+
class SemesterReportViewModel extends ChangeNotifier {
16+
final FirebaseFirestore _db = FirebaseFirestore.instance;
17+
final AuthRepository _authRepo;
18+
19+
bool _isLoading = false;
20+
String? _loadingMessage;
21+
String? _errorMessage;
22+
23+
// --- DATOS PARA GRÁFICOS ---
24+
Map<String, Stats> _accreditedBySubject = {};
25+
Map<String, Stats> _accreditedByAcademy = {};
26+
27+
bool get isLoading => _isLoading;
28+
String? get loadingMessage => _loadingMessage;
29+
String? get errorMessage => _errorMessage;
30+
31+
Map<String, Stats> get accreditedBySubject => _accreditedBySubject;
32+
Map<String, Stats> get accreditedByAcademy => _accreditedByAcademy;
33+
34+
SemesterReportViewModel({required AuthRepository authRepo}) : _authRepo = authRepo;
35+
36+
void _setLoading(bool loading, [String? message]) {
37+
_isLoading = loading;
38+
_loadingMessage = message;
39+
notifyListeners();
40+
}
41+
42+
// --- FUNCIÓN PRIVADA PARA OBTENER DATOS ---
43+
Future<({List<UserModel> students, Map<String, List<EnrollmentModel>> enrollments})> _fetchReportData() async {
44+
_setLoading(true, "Obteniendo datos de alumnos...");
45+
// ROL TUTORIAS: Obtiene todos los alumnos sin filtrar por academia
46+
final studentsSnapshot = await _db.collection('users')
47+
.where('role', isEqualTo: 'student')
48+
.get();
49+
50+
final students = studentsSnapshot.docs.map((doc) => UserModel.fromMap(doc.data(), doc.id)).toList();
51+
final studentIds = students.map((s) => s.id).toList();
52+
53+
if (studentIds.isEmpty) {
54+
return (students: <UserModel>[], enrollments: <String, List<EnrollmentModel>>{});
55+
}
56+
57+
_setLoading(true, "Obteniendo inscripciones...");
58+
final Map<String, List<EnrollmentModel>> allEnrollments = {};
59+
for (int i = 0; i < studentIds.length; i += 10) {
60+
final chunk = studentIds.sublist(i, i + 10 > studentIds.length ? studentIds.length : i + 10);
61+
final enrollmentsSnapshot = await _db.collection('enrollments').where('uid', whereIn: chunk).get();
62+
63+
for (final doc in enrollmentsSnapshot.docs) {
64+
final enrollment = EnrollmentModel.fromMap(doc.data(), doc.id);
65+
allEnrollments.putIfAbsent(enrollment.uid, () => []).add(enrollment);
66+
}
67+
}
68+
return (students: students, enrollments: allEnrollments);
69+
}
70+
71+
// --- LÓGICA PARA GRÁFICOS ---
72+
Future<void> processChartData() async {
73+
_setLoading(true, "Procesando datos para gráficos...");
74+
_errorMessage = null;
75+
76+
try {
77+
final data = await _fetchReportData();
78+
if (data.students.isEmpty) {
79+
_accreditedBySubject = {};
80+
_accreditedByAcademy = {};
81+
notifyListeners();
82+
return;
83+
}
84+
85+
final tempBySubject = <String, Stats>{};
86+
final tempByAcademy = <String, Stats>{};
87+
88+
for (final student in data.students) {
89+
final studentEnrollments = data.enrollments[student.id] ?? [];
90+
for (final enrollment in studentEnrollments) {
91+
if (enrollment.status == 'ACREDITADO' || enrollment.status == 'NO_ACREDITADO') {
92+
// Conteo por materia
93+
final subjectStat = tempBySubject.putIfAbsent(enrollment.subject, () => (accredited: 0, notAccredited: 0));
94+
if (enrollment.status == 'ACREDITADO') {
95+
tempBySubject[enrollment.subject] = (accredited: subjectStat.accredited + 1, notAccredited: subjectStat.notAccredited);
96+
} else {
97+
tempBySubject[enrollment.subject] = (accredited: subjectStat.accredited, notAccredited: subjectStat.notAccredited + 1);
98+
}
99+
100+
// Conteo por academia
101+
final academyStat = tempByAcademy.putIfAbsent(enrollment.academy, () => (accredited: 0, notAccredited: 0));
102+
if (enrollment.status == 'ACREDITADO') {
103+
tempByAcademy[enrollment.academy] = (accredited: academyStat.accredited + 1, notAccredited: academyStat.notAccredited);
104+
} else {
105+
tempByAcademy[enrollment.academy] = (accredited: academyStat.accredited, notAccredited: academyStat.notAccredited + 1);
106+
}
107+
}
108+
}
109+
}
110+
_accreditedBySubject = tempBySubject;
111+
_accreditedByAcademy = tempByAcademy;
112+
113+
} catch (e) {
114+
_errorMessage = e.toString().replaceAll("Exception: ", "");
115+
} finally {
116+
_setLoading(false);
117+
}
118+
}
119+
120+
121+
// --- LÓGICA PARA EXCEL ---
122+
Future<void> generateExcelReport() async {
123+
_setLoading(true, "Generando reporte...");
124+
_errorMessage = null;
125+
126+
try {
127+
final data = await _fetchReportData();
128+
if (data.students.isEmpty) {
129+
throw Exception("No hay alumnos para reportar.");
130+
}
131+
132+
_setLoading(true, "Creando archivo Excel...");
133+
final excel = Excel.createExcel();
134+
final Sheet sheet = excel[excel.getDefaultSheet()!];
135+
136+
final headers = ['Boleta', 'Nombre del Alumno', 'Materia', 'Estatus', 'Calificación'];
137+
sheet.appendRow(headers.map((h) => TextCellValue(h)).toList());
138+
139+
for (final student in data.students) {
140+
final studentEnrollments = data.enrollments[student.id] ?? [];
141+
if (studentEnrollments.isEmpty) {
142+
sheet.appendRow([TextCellValue(student.boleta), TextCellValue(student.name), TextCellValue('-'), TextCellValue('Sin materias'), TextCellValue('-')]);
143+
} else {
144+
for (final enrollment in studentEnrollments) {
145+
sheet.appendRow([
146+
TextCellValue(student.boleta),
147+
TextCellValue(student.name),
148+
TextCellValue(enrollment.subject),
149+
TextCellValue(enrollment.status),
150+
TextCellValue(enrollment.finalGrade?.toString() ?? 'N/A'),
151+
]);
152+
}
153+
}
154+
}
155+
156+
final fileBytes = excel.save();
157+
if (fileBytes != null && kIsWeb) {
158+
final blob = html.Blob([fileBytes], 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
159+
final url = html.Url.createObjectUrlFromBlob(blob);
160+
final anchor = html.AnchorElement(href: url)..setAttribute("download", "Reporte_Fin_Semestre.xlsx")..click();
161+
html.Url.revokeObjectUrl(url);
162+
} else if (!kIsWeb) {
163+
throw Exception("La descarga de archivos solo está soportada en la versión web.");
164+
}
165+
166+
} catch (e) {
167+
_errorMessage = e.toString().replaceAll("Exception: ", "");
168+
} finally {
169+
_setLoading(false);
170+
}
171+
}
172+
173+
// --- LÓGICA DE LIMPIEZA (CORREGIDA) ---
174+
Future<String?> deleteAllStudents() async {
175+
_setLoading(true, "Eliminando TODOS los alumnos...");
176+
_errorMessage = null;
177+
178+
try {
179+
final batch = _db.batch();
180+
final snapshot = await _db.collection('users').where('role', isEqualTo: 'student').get();
181+
182+
if (snapshot.docs.isEmpty) {
183+
return "No hay alumnos para eliminar.";
184+
}
185+
186+
for (final doc in snapshot.docs) {
187+
batch.delete(doc.reference);
188+
}
189+
190+
await batch.commit();
191+
192+
return "${snapshot.docs.length} alumnos han sido eliminados.";
193+
} catch (e) {
194+
_errorMessage = "Error al eliminar alumnos: $e";
195+
return null;
196+
} finally {
197+
_setLoading(false);
198+
}
199+
}
200+
}

lib/features/dashboard/viewmodels/student_detail_viewmodel.dart

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,10 @@ class StudentDetailViewModel extends ChangeNotifier {
4040
notifyListeners();
4141

4242
try {
43-
// 0. Obtener academias del usuario actual (el que está viendo la pantalla)
43+
// 0. Obtener usuario actual y sus academias
4444
final currentUser = await _authRepo.getCurrentUserData();
45-
if (currentUser == null || currentUser.academies.isEmpty) {
46-
throw Exception("No se pudieron determinar las academias del usuario actual para filtrar.");
47-
}
48-
final userAcademies = currentUser.academies;
45+
// SI EL USUARIO NO TIENE ACADEMIAS, SE TRATARÁ COMO LISTA VACÍA, NO LANZARÁ ERROR
46+
final userAcademies = currentUser?.academies ?? [];
4947

5048
// 1. Recargar datos del alumno
5149
final studentDoc = await _db.collection('users').doc(studentId).get();
@@ -55,7 +53,16 @@ class StudentDetailViewModel extends ChangeNotifier {
5553
throw Exception("No se pudo encontrar al estudiante.");
5654
}
5755

58-
// 2. Cargar inscripciones (Materias), filtrando por LAS ACADEMIAS DEL PROFESOR
56+
// Si el usuario no tiene academias, no hay nada que mostrar.
57+
if (userAcademies.isEmpty) {
58+
_enrollments = [];
59+
_groupedEvidences.clear();
60+
_isLoading = false;
61+
notifyListeners();
62+
return;
63+
}
64+
65+
// 2. Cargar inscripciones (Materias) FILTRADAS por las academias del usuario
5966
final enrollmentsSnapshot = await _db
6067
.collection('enrollments')
6168
.where('uid', isEqualTo: studentId)
@@ -102,6 +109,7 @@ class StudentDetailViewModel extends ChangeNotifier {
102109
}
103110
}
104111

112+
105113
Future<String?> reviewEvidence({
106114
required String evidenceId,
107115
required bool isApproved,
@@ -145,7 +153,6 @@ class StudentDetailViewModel extends ChangeNotifier {
145153
notifyListeners();
146154

147155
try {
148-
// Llamada al repositorio para calificar la materia individual
149156
await _authRepo.assignSubjectGrade(
150157
studentId: student.id,
151158
enrollmentId: enrollmentId,

0 commit comments

Comments
 (0)