feat: archives file menu, copy (1) naming, excel to pdf, and OCR enrollment validation
- archives: per-file menu (preview/print/download) via icon, right-click, long-press - documents: copy renames to "name (1).ext" with incrementing collision handling - documents: convert xls/xlsx/csv to multi-sheet PDF saved in student folder (excel kept) - students: OCR enrollment scan is review-only with 7-12 grade gate and manual validation modal - add student: linear OCR progress, SF9/SF10 grade 7-12 validation with auto-fill enrollment
This commit is contained in:
1 parent
2ac3a791ce
commit
c5998468b7
13 files changed
+1297
-235
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import '../../core/network/api_constants.dart';
|
||||
@@ -321,6 +322,34 @@ class DocumentRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Uploads in-memory bytes (e.g. a converted PDF) as a new document in the
|
||||
/// student's folder.
|
||||
Future<void> uploadDocumentBytes({
|
||||
required int studentId,
|
||||
required String documentType,
|
||||
int? requirementId,
|
||||
required String fileName,
|
||||
required Uint8List bytes,
|
||||
}) async {
|
||||
try {
|
||||
final options = await _getAuthOptions();
|
||||
final formData = FormData.fromMap({
|
||||
'studentId': studentId,
|
||||
'documentType': documentType,
|
||||
if (requirementId != null) 'requirementId': requirementId,
|
||||
'document': MultipartFile.fromBytes(
|
||||
bytes,
|
||||
filename: fileName,
|
||||
contentType: DioMediaType('application', 'pdf'),
|
||||
),
|
||||
});
|
||||
await _dio.post('/documents/upload', data: formData, options: options);
|
||||
} on DioException catch (e) {
|
||||
final msg = e.response?.data?['message'] ?? 'Failed to upload document.';
|
||||
throw Exception(msg);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> bulkDeleteDocuments(List<int> ids) async {
|
||||
try {
|
||||
final options = await _getAuthOptions();
|
||||
|
||||
@@ -35,6 +35,33 @@ class StudentPage {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
@@ -441,14 +468,18 @@ class StudentRepository {
|
||||
// ----------------------------------------------------------------
|
||||
// Manual SF9/SF10 OCR Scan Enrollment
|
||||
// ----------------------------------------------------------------
|
||||
Future<Map<String, dynamic>> scanEnrollmentFromSF(int studentId) async {
|
||||
Future<List<OcrEnrollmentRecord>> scanEnrollmentFromSF(int studentId) async {
|
||||
try {
|
||||
final options = await _getAuthOptions();
|
||||
final response = await _dio.post(
|
||||
'/students/$studentId/ocr-enrollment',
|
||||
options: options,
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
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);
|
||||
|
||||
Reference in new issue
Block a user