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:
ssbg04 committed 2026-08-08 00:57:04 +08:00
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);