Files
TIS_RMS/frontend/lib/domain/repositories/student_repository.dart
T

541 lines
18 KiB
Dart

import 'package:dio/dio.dart';
import 'dart:async';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:socket_io_client/socket_io_client.dart' as IO;
import '../../core/network/api_constants.dart';
import '../entities/student_model.dart';
/// Paginated student list returned from the API.
class StudentPage {
final List<StudentModel> students;
final int total;
final int page;
final int limit;
final int totalPages;
const StudentPage({
required this.students,
required this.total,
required this.page,
required this.limit,
required this.totalPages,
});
factory StudentPage.fromJson(Map<String, dynamic> json) {
final pagination = json['pagination'] as Map<String, dynamic>;
return StudentPage(
students: (json['students'] as List)
.map((s) => StudentModel.fromJson(s as Map<String, dynamic>))
.toList(),
total: (pagination['total'] as num).toInt(),
page: (pagination['page'] as num).toInt(),
limit: (pagination['limit'] as num).toInt(),
totalPages: (pagination['totalPages'] as num).toInt(),
);
}
}
/// A single enrollment record extracted from an SF9/SF10 OCR scan.
class OcrEnrollmentRecord {
final String gradeLevel;
final String section;
final String schoolYear;
final String adviserName;
final String semester;
const OcrEnrollmentRecord({
required this.gradeLevel,
required this.section,
required this.schoolYear,
this.adviserName = '',
this.semester = '',
});
factory OcrEnrollmentRecord.fromJson(Map<String, dynamic> json) {
return OcrEnrollmentRecord(
gradeLevel: (json['gradeLevel'] as String?) ?? '',
section: (json['section'] as String?) ?? '',
schoolYear: (json['schoolYear'] as String?) ?? '',
adviserName: (json['adviserName'] as String?) ?? '',
semester: (json['semester'] as String?) ?? '',
);
}
}
class StudentRepository {
final Dio _dio = ApiConstants.createDio();
final FlutterSecureStorage _storage = const FlutterSecureStorage();
IO.Socket? _socket;
final StreamController<void> _studentUpdateController =
StreamController.broadcast();
Stream<void> get onStudentChanged => _studentUpdateController.stream;
StudentRepository() {
_initRealTimeSocket();
}
Future<Options> _getAuthOptions() async {
final token = await _storage.read(key: 'jwt_token');
return Options(headers: {'Authorization': 'Bearer $token'});
}
// ----------------------------------------------------------------
// Initialize WebSocket for Real-Time LAN Sync
// ----------------------------------------------------------------
void _initRealTimeSocket() async {
final token = await _storage.read(key: 'jwt_token');
// Assumes your ApiConstants.baseUrl looks like 'http://192.168.1.10:3000/api'
// Sockets usually connect to the root domain 'http://192.168.1.10:3000'
final socketUrl = ApiConstants.baseUrl.replaceAll('/api', '');
_socket = IO.io(
socketUrl,
IO.OptionBuilder()
.setTransports(['websocket'])
.setAuth({
'token': token,
}) // Pass token for secure backend verification
.disableAutoConnect()
.build(),
);
_socket?.connect();
_socket?.onConnect((_) {
print('Real-time sync connected to LAN server');
});
// Listen for database changes broadcasted by the Node.js server
_socket?.on('student_added', (_) => _studentUpdateController.add(null));
_socket?.on('student_updated', (_) => _studentUpdateController.add(null));
_socket?.on('student_deleted', (_) => _studentUpdateController.add(null));
}
void dispose() {
_socket?.dispose();
_studentUpdateController.close();
}
// ----------------------------------------------------------------
// Fetch paginated students with optional search + filters
// ----------------------------------------------------------------
Future<StudentPage> getStudents({
String search = '',
int page = 1,
int limit = 10,
String gradeLevel = '',
String status = '',
String section = '',
String schoolYear = '',
String is4ps = '',
String lrn = '',
String sortBy = '',
String sortOrder = '',
}) async {
try {
final options = await _getAuthOptions();
final response = await _dio.get(
'/students',
queryParameters: {
if (search.trim().isNotEmpty) 'search': search.trim(),
if (gradeLevel.trim().isNotEmpty) 'gradeLevel': gradeLevel.trim(),
if (status.trim().isNotEmpty) 'status': status.trim(),
if (section.trim().isNotEmpty) 'section': section.trim(),
if (schoolYear.trim().isNotEmpty) 'schoolYear': schoolYear.trim(),
if (is4ps.trim().isNotEmpty) 'is4ps': is4ps.trim(),
if (lrn.trim().isNotEmpty) 'lrn': lrn.trim(),
if (sortBy.trim().isNotEmpty) 'sortBy': sortBy.trim(),
if (sortOrder.trim().isNotEmpty) 'sortOrder': sortOrder.trim(),
'page': page,
'limit': limit,
},
options: options,
);
return StudentPage.fromJson(response.data as Map<String, dynamic>);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to fetch students.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Get single student by ID
// ----------------------------------------------------------------
Future<StudentModel> getStudentById(int id) async {
try {
final options = await _getAuthOptions();
final response = await _dio.get('/students/$id', options: options);
return StudentModel.fromJson(response.data as Map<String, dynamic>);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to fetch student.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Create student — returns the newly created ID
// ----------------------------------------------------------------
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 {
try {
final options = await _getAuthOptions();
final response = await _dio.post(
'/students',
data: {
'lrn': lrn.trim(),
'firstName': firstName.trim(),
'middleName': middleName?.trim(),
'lastName': lastName.trim(),
'extension': extension?.trim(),
'sex': sex,
'birthDate': birthDate != null ?
'${birthDate.year}-${birthDate.month.toString().padLeft(2, '0')}-${birthDate.day.toString().padLeft(2, '0')}' : null,
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
'is4ps': is4ps,
},
options: options,
);
return (response.data['id'] as num).toInt();
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to create student.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Update student
// ----------------------------------------------------------------
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 {
try {
final options = await _getAuthOptions();
await _dio.put(
'/students/$id',
data: {
'lrn': lrn.trim(),
'firstName': firstName.trim(),
'middleName': middleName?.trim(),
'lastName': lastName.trim(),
'extension': extension?.trim(),
'sex': sex,
'birthDate': birthDate != null ?
'${birthDate.year}-${birthDate.month.toString().padLeft(2, '0')}-${birthDate.day.toString().padLeft(2, '0')}' : null,
'status': status,
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
'is4ps': is4ps,
},
options: options,
);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to update student.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Set student inactive (Soft delete)
// ----------------------------------------------------------------
Future<void> setStudentInactive(int id, StudentModel student) async {
try {
final options = await _getAuthOptions();
await _dio.put(
'/students/$id',
data: {
'lrn': student.lrn,
'firstName': student.firstName,
'middleName': student.middleName,
'lastName': student.lastName,
'extension': student.extension,
'sex': student.sex,
'birthDate': student.birthDate?.toIso8601String().split('T').first,
'status':
'Inactive', // Changed status to Inactive instead of deleting
'academicYearId': student.latestEnrollment?.academicYearId ?? 0,
'gradeLevel': student.latestGradeLevel ?? 0,
'sectionId': student.latestEnrollment?.sectionId ?? 0,
'trackStrand': student.latestEnrollment?.trackStrand,
'is4ps': student.is4ps,
},
options: options,
);
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ??
'Failed to update student to inactive.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Bulk Enroll Students
// ----------------------------------------------------------------
Future<void> bulkEnroll({
required List<int> studentIds,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
try {
final options = await _getAuthOptions();
await _dio.post(
'/students/bulk-enroll',
data: {
'studentIds': studentIds,
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
},
options: options,
);
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ?? 'Failed to bulk enroll students.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Bulk Graduate Students
// ----------------------------------------------------------------
Future<void> bulkGraduate(List<int> studentIds) async {
try {
final options = await _getAuthOptions();
await _dio.put(
'/students/bulk-graduate',
data: {'studentIds': studentIds},
options: options,
);
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ?? 'Failed to bulk graduate students.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Bulk Change Status (Used for Dropped, Transferred, etc)
// ----------------------------------------------------------------
Future<void> bulkChangeStatus(
List<StudentModel> students,
String newStatus,
) async {
if (students.isEmpty) return;
try {
final options = await _getAuthOptions();
final studentIds = students.map((s) => s.id).toList();
await _dio.post(
'/students/bulk-status',
data: {'studentIds': studentIds, 'status': newStatus},
options: options,
);
} on DioException catch (e) {
throw Exception(
e.response?.data['message'] ?? 'Failed to perform bulk status change.',
);
}
}
// ----------------------------------------------------------------
// Update Enrollment
// ----------------------------------------------------------------
Future<void> updateEnrollment({
required int enrollmentId,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
try {
final options = await _getAuthOptions();
await _dio.put(
'/students/enrollments/$enrollmentId',
data: {
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
},
options: options,
);
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ?? 'Failed to update enrollment.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Delete Enrollment
// ----------------------------------------------------------------
Future<void> deleteEnrollment(int enrollmentId) async {
try {
final options = await _getAuthOptions();
await _dio.delete(
'/students/enrollments/$enrollmentId',
options: options,
);
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ?? 'Failed to delete enrollment.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Add Enrollment
// ----------------------------------------------------------------
Future<void> addEnrollment({
required int studentId,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
try {
final options = await _getAuthOptions();
await _dio.post(
'/students/$studentId/enrollments',
data: {
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
},
options: options,
);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to add enrollment.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Bulk Enroll Students
// ----------------------------------------------------------------
Future<void> bulkEnrollStudents({
required List<int> studentIds,
required int academicYearId,
required int gradeLevel,
required int sectionId,
String? trackStrand,
}) async {
try {
final options = await _getAuthOptions();
await _dio.post(
'/students/bulk-enroll',
data: {
'studentIds': studentIds,
'academicYearId': academicYearId,
'gradeLevel': gradeLevel,
'sectionId': sectionId,
'trackStrand': trackStrand,
},
options: options,
);
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to bulk enroll students.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Verify LRNs
// ----------------------------------------------------------------
Future<List<Map<String, dynamic>>> verifyLrns(List<String> lrns) async {
try {
final options = await _getAuthOptions();
final response = await _dio.post(
'/students/verify-lrns',
data: {'lrns': lrns},
options: options,
);
final List<dynamic> verified = response.data['verified'] ?? [];
return verified.map((v) => v as Map<String, dynamic>).toList();
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to verify LRNs.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Bulk Create Students from OCR Import
// ----------------------------------------------------------------
/// Returns { created, skipped, failed, results }
Future<Map<String, dynamic>> bulkCreateStudents(
List<Map<String, dynamic>> students,
) async {
try {
final options = await _getAuthOptions();
final response = await _dio.post(
'/students/bulk-ocr-import',
data: {'students': students},
options: options,
);
return response.data as Map<String, dynamic>;
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to bulk import students.';
throw Exception(msg);
}
}
// ----------------------------------------------------------------
// Manual SF9/SF10 OCR Scan Enrollment
// ----------------------------------------------------------------
Future<List<OcrEnrollmentRecord>> scanEnrollmentFromSF(int studentId) async {
try {
final options = await _getAuthOptions();
final response = await _dio.post(
'/students/$studentId/ocr-enrollment',
options: options,
);
final data = response.data as Map<String, dynamic>;
final records = (data['data'] as Map<String, dynamic>?)?['records'] as List?;
return (records ?? [])
.map((r) => OcrEnrollmentRecord.fromJson(r as Map<String, dynamic>))
.toList();
} on DioException catch (e) {
final msg = e.response?.data?['message'] ?? 'Failed to scan SF10/SF9 for enrollment.';
throw Exception(msg);
}
}
}