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 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((ref) { return StudentRepository(); }); // ============================================================ // Academic Years Provider // ============================================================ final academicYearsProvider = FutureProvider.autoDispose>(( 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)) .toList(); } catch (e) { return []; } }); // ============================================================ // Student Detail Provider // ============================================================ final studentDetailProvider = FutureProvider.family .autoDispose((ref, studentId) async { final repo = ref.read(studentRepositoryProvider); return repo.getStudentById(studentId); }); // ============================================================ // Student Activities / Audit Trail Provider // ============================================================ final studentActivitiesProvider = FutureProvider.family .autoDispose, 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.new, ); class StudentQueryNotifier extends Notifier { @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((ref) => false); final studentSelectedIdsProvider = StateProvider>((ref) => []); // ============================================================ // View Tab Provider (Enrolled vs All Students) // ============================================================ enum StudentViewTab { enrolled, all } final studentViewTabProvider = StateProvider((ref) => StudentViewTab.enrolled); final studentActiveFilterCountProvider = Provider((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(( 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.new, ); class StudentMutationNotifier extends AsyncNotifier { @override FutureOr build() {} Future 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 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 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 bulkEnroll({ required List 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 bulkGraduate(List 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 bulkChangeStatus( List 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 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 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 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> bulkCreateStudents( List> 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> 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; } } }