541 lines
18 KiB
Dart
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);
|
|
}
|
|
}
|
|
}
|