Files
TIS_RMS/frontend/lib/ui/providers/student_provider.dart
T

556 lines
16 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../../core/network/api_constants.dart';
import '../../domain/repositories/student_repository.dart';
import '../../domain/entities/student_model.dart';
import '../../domain/entities/dashboard_models.dart' show RecentActivity;
// ============================================================
// Academic Years Model
// ============================================================
class AcademicYear {
final int id;
final String yearRange;
final String status;
AcademicYear({
required this.id,
required this.yearRange,
required this.status,
});
factory AcademicYear.fromJson(Map<String, dynamic> json) {
return AcademicYear(
id: json['id'] as int,
yearRange: json['year_range'] as String,
status: json['status'] as String? ?? 'active',
);
}
}
// ============================================================
// Repository provider
// ============================================================
final studentRepositoryProvider = Provider<StudentRepository>((ref) {
return StudentRepository();
});
// ============================================================
// Academic Years Provider
// ============================================================
final academicYearsProvider = FutureProvider.autoDispose<List<AcademicYear>>((
ref,
) async {
final storage = const FlutterSecureStorage();
final token = await storage.read(key: 'jwt_token');
final dio = ApiConstants.createDio();
try {
final response = await dio.get(
'/setup/academic-years',
options: Options(headers: {'Authorization': 'Bearer $token'}),
);
return (response.data as List)
.map((y) => AcademicYear.fromJson(y as Map<String, dynamic>))
.toList();
} catch (e) {
return [];
}
});
// ============================================================
// Student Detail Provider
// ============================================================
final studentDetailProvider = FutureProvider.family
.autoDispose<StudentModel, int>((ref, studentId) async {
final repo = ref.read(studentRepositoryProvider);
return repo.getStudentById(studentId);
});
// ============================================================
// Student Activities / Audit Trail Provider
// ============================================================
final studentActivitiesProvider = FutureProvider.family
.autoDispose<List<RecentActivity>, int>((ref, studentId) async {
final repo = ref.read(studentRepositoryProvider);
return repo.getStudentActivities(studentId);
});
// ============================================================
// Query params state — drives what the list shows
// ============================================================
class StudentQueryParams {
final String search;
final int page;
final int limit;
final String gradeLevel; // '' = All
final String status; // '' = All
final String section; // '' = All
final String schoolYear; // '' = All
final String is4Ps; // '' = All, 'true' = Yes, 'false' = No
final String lrn; // '' = All
final String sortBy; // '' = None, 'lrn', 'name', 'grade_section', 'doc_status'
final String sortOrder; // '' = None, 'asc', 'desc'
const StudentQueryParams({
this.search = '',
this.page = 1,
this.limit = 20,
this.gradeLevel = '',
this.status = '',
this.section = '',
this.schoolYear = '',
this.is4Ps = '',
this.lrn = '',
this.sortBy = '',
this.sortOrder = '',
});
StudentQueryParams copyWith({
String? search,
int? page,
int? limit,
String? gradeLevel,
String? status,
String? section,
String? schoolYear,
String? is4Ps,
String? lrn,
String? sortBy,
String? sortOrder,
}) {
return StudentQueryParams(
search: search ?? this.search,
page: page ?? this.page,
limit: limit ?? this.limit,
gradeLevel: gradeLevel ?? this.gradeLevel,
status: status ?? this.status,
section: section ?? this.section,
schoolYear: schoolYear ?? this.schoolYear,
is4Ps: is4Ps ?? this.is4Ps,
lrn: lrn ?? this.lrn,
sortBy: sortBy ?? this.sortBy,
sortOrder: sortOrder ?? this.sortOrder,
);
}
}
// ============================================================
// Notifier — manages the live query state
// ============================================================
final studentQueryProvider =
NotifierProvider<StudentQueryNotifier, StudentQueryParams>(
StudentQueryNotifier.new,
);
class StudentQueryNotifier extends Notifier<StudentQueryParams> {
@override
StudentQueryParams build() => const StudentQueryParams();
void setSearch(String value) {
// Reset to page 1 whenever search changes
state = state.copyWith(search: value, page: 1);
}
void setPage(int page) => state = state.copyWith(page: page);
void setLimit(int limit) => state = state.copyWith(limit: limit, page: 1);
void setGradeLevel(String grade) =>
state = state.copyWith(gradeLevel: grade, section: '', page: 1);
void setStatus(String status) =>
state = state.copyWith(status: status, page: 1);
void setSection(String section) =>
state = state.copyWith(section: section, page: 1);
void setSchoolYear(String schoolYear) => state = state.copyWith(
schoolYear: schoolYear,
gradeLevel: '',
section: '',
page: 1,
);
void setIs4Ps(String is4Ps) => state = state.copyWith(is4Ps: is4Ps, page: 1);
void setLrn(String lrn) => state = state.copyWith(lrn: lrn, page: 1);
void setSort(String sortBy, String sortOrder) => state = state.copyWith(
sortBy: sortBy,
sortOrder: sortOrder,
page: 1,
);
void setFilters({
String? schoolYear,
String? gradeLevel,
String? section,
String? status,
String? is4Ps,
String? sortBy,
String? sortOrder,
int? limit,
}) {
state = state.copyWith(
schoolYear: schoolYear ?? state.schoolYear,
gradeLevel: gradeLevel ?? state.gradeLevel,
section: section ?? state.section,
status: status ?? state.status,
is4Ps: is4Ps ?? state.is4Ps,
sortBy: sortBy ?? state.sortBy,
sortOrder: sortOrder ?? state.sortOrder,
limit: limit ?? state.limit,
page: 1,
);
}
void reset() => state = const StudentQueryParams();
}
// ============================================================
// Multi-Select & Filter State Providers
// ============================================================
final studentMultiSelectProvider = StateProvider<bool>((ref) => false);
final studentSelectedIdsProvider = StateProvider<List<int>>((ref) => []);
// ============================================================
// View Tab Provider (Enrolled vs All Students)
// ============================================================
enum StudentViewTab { enrolled, all }
final studentViewTabProvider =
StateProvider<StudentViewTab>((ref) => StudentViewTab.enrolled);
final studentActiveFilterCountProvider = Provider<int>((ref) {
final query = ref.watch(studentQueryProvider);
final tab = ref.watch(studentViewTabProvider);
if (tab == StudentViewTab.enrolled) {
// In enrolled tab, status and schoolYear are inherent to the tab, not user overrides
return [
query.gradeLevel.isNotEmpty,
query.section.isNotEmpty,
query.is4Ps.isNotEmpty,
query.sortBy.isNotEmpty,
query.limit != 20,
].where((v) => v).length;
}
return [
query.gradeLevel.isNotEmpty,
query.section.isNotEmpty,
query.status.isNotEmpty,
query.schoolYear.isNotEmpty,
query.is4Ps.isNotEmpty,
query.sortBy.isNotEmpty,
query.limit != 20,
].where((v) => v).length;
});
// ============================================================
// Async data provider — re-fetches when query changes
// ============================================================
final studentPageProvider = FutureProvider.autoDispose<StudentPage>((
ref,
) async {
final query = ref.watch(studentQueryProvider);
final repo = ref.read(studentRepositoryProvider);
final sub = repo.onStudentChanged.listen((_) {
ref.invalidateSelf();
});
ref.onDispose(() => sub.cancel());
return repo.getStudents(
search: query.search,
page: query.page,
limit: query.limit,
gradeLevel: query.gradeLevel,
status: query.status,
section: query.section,
schoolYear: query.schoolYear,
is4ps: query.is4Ps,
lrn: query.lrn,
sortBy: query.sortBy,
sortOrder: query.sortOrder,
);
});
// ============================================================
// Mutation notifier — handles Create / Update / Delete
// ============================================================
final studentMutationProvider =
AsyncNotifierProvider<StudentMutationNotifier, void>(
StudentMutationNotifier.new,
);
class StudentMutationNotifier extends AsyncNotifier<void> {
@override
FutureOr<void> build() {}
Future<int> createStudent({
required String lrn,
required String firstName,
String? middleName,
required String lastName,
String? extension,
required String sex,
required DateTime? birthDate,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
bool is4ps = false,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
final id = await repo.createStudent(
lrn: lrn,
firstName: firstName,
middleName: middleName,
lastName: lastName,
extension: extension,
sex: sex,
birthDate: birthDate,
academicYearId: academicYearId,
gradeLevel: gradeLevel,
sectionId: sectionId,
trackStrand: trackStrand,
is4ps: is4ps,
);
state = const AsyncData(null);
// Invalidate to refresh the list
ref.invalidate(studentPageProvider);
return id;
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> updateStudent({
required int id,
required String lrn,
required String firstName,
String? middleName,
required String lastName,
String? extension,
required String sex,
required DateTime? birthDate,
int academicYearId = 0,
int gradeLevel = 0,
int sectionId = 0,
String? trackStrand,
String status = 'Enrolled',
bool is4ps = false,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.updateStudent(
id: id,
lrn: lrn,
firstName: firstName,
middleName: middleName,
lastName: lastName,
extension: extension,
sex: sex,
birthDate: birthDate,
status: status,
academicYearId: academicYearId,
gradeLevel: gradeLevel,
sectionId: sectionId,
trackStrand: trackStrand,
is4ps: is4ps,
);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
ref.invalidate(studentDetailProvider(id));
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> setStudentInactive(int id, StudentModel student) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.setStudentInactive(id, student);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
ref.invalidate(studentDetailProvider(id));
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> bulkEnroll({
required List<int> studentIds,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.bulkEnroll(
studentIds: studentIds,
academicYearId: academicYearId,
gradeLevel: gradeLevel,
sectionId: sectionId,
trackStrand: trackStrand,
);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
for (final id in studentIds) {
ref.invalidate(studentDetailProvider(id));
}
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> bulkGraduate(List<int> studentIds) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.bulkGraduate(studentIds);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
for (final id in studentIds) {
ref.invalidate(studentDetailProvider(id));
}
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> bulkChangeStatus(
List<StudentModel> students,
String newStatus,
) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.bulkChangeStatus(students, newStatus);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
for (final student in students) {
ref.invalidate(studentDetailProvider(student.id));
}
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> updateEnrollment({
required int studentId,
required int enrollmentId,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.updateEnrollment(
enrollmentId: enrollmentId,
academicYearId: academicYearId,
gradeLevel: gradeLevel,
sectionId: sectionId,
trackStrand: trackStrand,
);
state = const AsyncData(null);
ref.invalidate(studentDetailProvider(studentId));
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> deleteEnrollment({
required int studentId,
required int enrollmentId,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.deleteEnrollment(enrollmentId);
state = const AsyncData(null);
ref.invalidate(studentDetailProvider(studentId));
ref.invalidate(studentPageProvider);
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<void> addEnrollment({
required int studentId,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
await repo.addEnrollment(
studentId: studentId,
academicYearId: academicYearId,
gradeLevel: gradeLevel,
sectionId: sectionId,
trackStrand: trackStrand,
);
state = const AsyncData(null);
ref.invalidate(studentDetailProvider(studentId));
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
/// Submits a batch of student records to POST /students/bulk-ocr-import.
/// Returns the raw result map: { created, skipped, failed, results }
Future<Map<String, dynamic>> bulkCreateStudents(
List<Map<String, dynamic>> students,
) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
final result = await repo.bulkCreateStudents(students);
state = const AsyncData(null);
ref.invalidate(studentPageProvider);
return result;
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
Future<List<OcrEnrollmentRecord>> scanEnrollmentFromSF(int studentId) async {
state = const AsyncLoading();
try {
final repo = ref.read(studentRepositoryProvider);
final result = await repo.scanEnrollmentFromSF(studentId);
state = const AsyncData(null);
return result;
} catch (e, st) {
state = AsyncError(e, st);
rethrow;
}
}
}