feat(system): implement socket.io real-time sync, admin passwords, ocr adjustments, and shortcut fixes

This commit is contained in:
ssbg04 committed 2026-10-02 10:17:44 +08:00
1 parent f30faeecff
commit b04981d7fb
46 files changed
+3878 -176

No files matched your search

+58 -2
View File
@@ -930,7 +930,7 @@ Files backup/restore - mirror mode or zip -->
- Clicking the "Enrolled" tab automatically clears the search field and restores the active year enrolled filter.
- Also cleared view-tab filters when search is triggered from the mobile search dialog in android_bottom_nav_layout.dart. -->
# ANDROID APP
<!-- # ANDROID APP
- [DONE] hamburger nav bar slide too much touch area on the left
- In `android_bottom_nav_layout.dart`, reduced `drawerEdgeDragWidth` from `MediaQuery.of(context).size.width * 0.45` down to a strict `28.0` edge touch area to prevent accidental drawer opening when interacting with screen content.
- [DONE] report screen in deped tab export pdf cannot download the generated pdf, because of permission denied in android app
@@ -962,7 +962,63 @@ Files backup/restore - mirror mode or zip -->
- [DONE] make the about, support, system information in settings responsive
- System/Client Description Header now adapts using `LayoutBuilder` (<460px): renders icon + title in a row and full-width description below on mobile screens, avoiding horizontal squishing.
- System Information items (`_buildAboutInfoRow`) dynamically switch from a 2-column fixed-label layout to stacked label & selectable value on compact viewports (<460px), preventing text compression.
- Developer Contact container adapts on viewports <500px: stacks label and email with a clean indentation to prevent horizontal overflows. Action buttons seamlessly wrap with `Wrap`.
- Developer Contact container adapts on viewports <500px: stacks label and email with a clean indentation to prevent horizontal overflows. Action buttons seamlessly wrap with `Wrap`. -->
<!-- # SETTINGS SCREEN
- [DONE] android app cannot open links in about, support & information buttons cannot redirect to browser
- Added `<queries>` intent declarations for `https`, `http`, and `mailto` schemes to `AndroidManifest.xml` (required on Android 11+ / API 30+ for `url_launcher` visibility).
- Enhanced external link handling in `settings_screen.dart` with dual launch mode fallback (`LaunchMode.externalApplication` -> `LaunchMode.platformDefault`) and clipboard copy fallback with user feedback snackbars if no browser or email client responds.
# STUDENT SCREEN
- [DONE] in android list remove the bottom space
- Removed obsolete 76px bottom spacer on Android when `totalPages <= 1` in `students_screen.dart`.
- Adjusted Android card list bottom padding from 24px to 8px, eliminating the dead whitespace at the bottom of the list.
- [DONE] move the filter button into the header tabs
- Removed the filter button from the Android top AppBar in `android_bottom_nav_layout.dart`.
- Integrated the filter button into the header tabs row in `students_screen.dart` (alongside the view switcher tabs) with active filter badge and direct tap target.
# DOCUMENT AND ARCHIVE SCREEN
- [DONE] make the filter realtime from the backend
- Added `GET /api/documents/types` endpoint on backend (`documentController.getDocumentTypes` and `routes/documents.js`) returning distinct active document types and requirements categorized into JHS, SHS, and General in real time directly from the database.
- Enhanced `documentController.getAllDocuments` and `archiveController.getArchivedDocuments` to handle category prefixes, raw names, and clean names seamlessly when filtering.
- Added `realtimeDocumentTypesProvider` in Flutter and updated `studentFoldersProvider` to perform real-time backend search queries.
- Updated `documents_screen.dart` and `archives_screen.dart` PopupMenuButton to invalidate and pull fresh types on open, displaying real-time JHS, SHS, and Other document categories with instant reactive queries. -->
<!-- # STATIC WEBSITE
- [DONE] for a system documentation how to use/ guide
- Created dedicated standalone directory [documentation/](file:///f:/SumbrerongBato/tis_rms_server/documentation) completely decoupled from frontend and backend builds.
- Implemented high-aesthetic responsive user guide [documentation/index.html](file:///f:/SumbrerongBato/tis_rms_server/documentation/index.html) with modern typography (`Inter`), Emerald green (`#00B074`) & Slate dark/light theme, and offline-capable architecture.
- Structured modular sections: System Overview, RBAC Roles & Security Matrix, Interactive Virtual Tour, Modules 1–8 (Dashboard, Students & Multi-Select, Virtual Folders & Document Taxonomy, Retention & Real-time Archives, Batch Print Queue & Duplex Merging, DepEd SF10 Excel-to-PDF Engine, Users & Immutable RA 10173 Audit Trail, Settings & Mandatory Checklists), Keyboard Shortcuts, and Connection Diagnostics & FAQ.
- Integrated dual-platform mockup frames: Windows desktop titlebars and Android tablet/notch frames with live tab switcher.
- Sourced and centralized all real system assets under `documentation/assets/images/windows/`, `documentation/assets/images/android/`, `documentation/assets/images/features/`, and `documentation/assets/videos/`.
- Built interactive JavaScript engine [documentation/app.js](file:///f:/SumbrerongBato/tis_rms_server/documentation/app.js): interactive frame-by-frame virtual tour reel with HUD overlay and timeline progress, real-time client search with `Ctrl+K` shortcut, image zoom Lightbox modal, theme switcher with `localStorage` persistence, and scrollspy navigation.
- Verified in browser with subagent: zero console errors, smooth tab transitions, working zoom lightbox, live search, and dark mode toggle. -->
<!-- # USER SCREEN -> PASSWORD UPON CREATE AN USER [DONE]
- [x] dont make the password `username123` for new admin user, make it randomized
- Implemented 10-character cryptographically secure randomized password generator for new admin accounts in `userController.js`.
- Exposed and highlighted generated temporary password in user creation success dialog in `users_screen.dart`.
# STUDENT SCREEN -> add student using OCR [DONE]
- [x] dont upload the document that used for OCR in their folder
- Removed document auto-upload logic from `add_student_modal.dart` and `bulk_ocr_import_dialog.dart` after creating students.
# KEYBOARD SHORTCUTS [DONE]
- [x] Ctrl + N in student screen not working (wired to `_openModal()` in `students_screen.dart`)
- [x] Ctrl + U in document screen not working (wired to `UploadOcrModal.show(...)` in `documents_screen.dart`)
- [x] Ctrl + P in document and archive screen not working (wired to `PrintQueueModal.show(context)` in `documents_screen.dart` and `archives_screen.dart`)
- [x] remove Ctrl + B for backup (removed from documentation shortcuts table and FAQ)
- [x] remove Ctrl + A for filter (removed from documentation shortcuts table) -->
# HISTORY SCREEN
- PRINTED_HISTORY card has a overflowed for date and time
# Documentation website
- fix FAQs make sure its on the current system features
- connect the documentation based on the current system features
- dont speficy the android is for tablet just android
- redesign the entire documentation make it minimalist no extra design just make it normal documentation
---
@@ -93,5 +93,18 @@
<data
android:mimeType="text/plain"/>
</intent>
<!-- Required for url_launcher on Android 11+ (API 30+) -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
</manifest>
@@ -253,6 +253,24 @@ class DocumentRepository {
}
}
Future<Map<String, List<String>>> getDocumentTypes() async {
try {
final options = await _getAuthOptions();
final response = await _dio.get('/documents/types', options: options);
final data = response.data as Map<String, dynamic>;
return {
'jhs': (data['jhs'] as List? ?? []).map((e) => e.toString()).toList(),
'shs': (data['shs'] as List? ?? []).map((e) => e.toString()).toList(),
'general':
(data['general'] as List? ?? []).map((e) => e.toString()).toList(),
};
} on DioException catch (e) {
final msg =
e.response?.data?['message'] ?? 'Failed to fetch document types.';
throw Exception(msg);
}
}
Future<DocumentPage> getDocuments({
String search = '',
int page = 1,
@@ -28,7 +28,6 @@ import '../providers/navigation_provider.dart';
import '../providers/connected_users_provider.dart';
import '../screens/capstone_members/capstone_members_screen.dart';
import '../shared/dialogs/disconnected_dialog.dart';
import '../screens/students/widgets/student_filter_dialog.dart';
import '../shared/inputs/app_search_bar.dart';
import '../shared/menus/profile_dropdown_menu.dart';
import '../shared/widgets/notification_icon_button.dart';
@@ -748,32 +747,6 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
);
},
),
// 2. Filter Icon with Badge
Consumer(
builder: (context, ref, _) {
final activeCount = ref.watch(studentActiveFilterCountProvider);
final query = ref.watch(studentQueryProvider);
return Tooltip(
message: 'Filter Students',
child: IconButton(
onPressed: () =>
StudentFilterDialog.show(
context,
query: query,
isEnrolledTab: ref.read(studentViewTabProvider) == StudentViewTab.enrolled,
),
icon: Badge(
isLabelVisible: activeCount > 0,
label: Text(activeCount.toString()),
child: const Icon(
Icons.tune_rounded,
size: 22,
),
),
),
);
},
),
// 3. Add Student Icon (High-Attraction Elevated Primary CTA)
if (widget.userRole != 'teacher')
Tooltip(
@@ -151,11 +151,20 @@ final studentDocumentsProvider = FutureProvider.family
final studentFoldersProvider = FutureProvider.autoDispose<List<FolderModel>>((
ref,
) async {
final query = ref.watch(documentQueryProvider);
final repo = ref.read(documentRepositoryProvider);
// Fetch top-level folders (no parentId filter = all root folders)
return repo.getFolders();
return repo.getFolders(search: query.search);
});
// ============================================================
// Realtime document types provider — live from backend database
// ============================================================
final realtimeDocumentTypesProvider =
FutureProvider.autoDispose<Map<String, List<String>>>((ref) async {
final repo = ref.read(documentRepositoryProvider);
return repo.getDocumentTypes();
});
// ============================================================
// Document statuses provider — fetches distinct statuses from backend
// ============================================================
@@ -745,6 +745,9 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
const SingleActivator(LogicalKeyboardKey.keyP, control: true): () {
PrintQueueModal.show(context);
},
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1196,6 +1199,10 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
tooltip: 'Filter by Document Type',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
onOpened: () {
ref.invalidate(realtimeDocumentTypesProvider);
ref.invalidate(documentRequirementsProvider);
},
icon: Badge(
isLabelVisible: _getActiveFilterCount() > 0,
label: Text(_getActiveFilterCount().toString()),
@@ -1216,25 +1223,30 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
_applyFilters();
},
itemBuilder: (context) {
final requirementsAsync = ref.read(documentRequirementsProvider);
final jhsReqs = requirementsAsync.maybeWhen(
data: (reqs) => reqs
final realtimeData = ref.read(realtimeDocumentTypesProvider).asData?.value;
final requirementsData = ref.read(documentRequirementsProvider).asData?.value ?? [];
List<String> jhsReqs = [];
List<String> shsReqs = [];
List<String> generalReqs = [];
if (realtimeData != null) {
jhsReqs = List<String>.from(realtimeData['jhs'] ?? []);
shsReqs = List<String>.from(realtimeData['shs'] ?? []);
generalReqs = List<String>.from(realtimeData['general'] ?? []);
} else {
jhsReqs = requirementsData
.where((r) => r.category == 'JHS')
.map((r) => r.name)
.map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
..sort(),
orElse: () => <String>[],
);
final shsReqs = requirementsAsync.maybeWhen(
data: (reqs) => reqs
..sort();
shsReqs = requirementsData
.where((r) => r.category == 'SHS')
.map((r) => r.name)
.map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
..sort(),
orElse: () => <String>[],
);
..sort();
}
// Update cached lists for _applyFilters
if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs;
@@ -1317,6 +1329,18 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
}
}
if (generalReqs.isNotEmpty) {
items.add(const PopupMenuDivider(height: 8));
items.add(PopupMenuItem<String>(
enabled: false,
height: 28,
child: buildHeader('OTHER DOCUMENTS'),
));
for (final doc in generalReqs) {
items.add(buildItem(doc, doc, isSubItem: true));
}
}
return items;
},
),
@@ -700,6 +700,17 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
const SingleActivator(LogicalKeyboardKey.keyU, control: true): () {
UploadOcrModal.show(
context,
prefilledStudentId:
_openedFolderStudentId ?? widget.initialStudentId,
prefilledLrn: ref.read(openedFolderProvider)?.lrn,
);
},
const SingleActivator(LogicalKeyboardKey.keyP, control: true): () {
PrintQueueModal.show(context);
},
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1229,6 +1240,10 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
tooltip: 'Filter by Document Type',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
onOpened: () {
ref.invalidate(realtimeDocumentTypesProvider);
ref.invalidate(documentRequirementsProvider);
},
icon: Badge(
isLabelVisible: _getActiveFilterCount() > 0,
label: Text(_getActiveFilterCount().toString()),
@@ -1247,24 +1262,30 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
_applyFilters();
},
itemBuilder: (context) {
final jhsReqs = requirementsAsync.maybeWhen(
data: (reqs) => reqs
final realtimeData = ref.read(realtimeDocumentTypesProvider).asData?.value;
final requirementsData = ref.read(documentRequirementsProvider).asData?.value ?? [];
List<String> jhsReqs = [];
List<String> shsReqs = [];
List<String> generalReqs = [];
if (realtimeData != null) {
jhsReqs = List<String>.from(realtimeData['jhs'] ?? []);
shsReqs = List<String>.from(realtimeData['shs'] ?? []);
generalReqs = List<String>.from(realtimeData['general'] ?? []);
} else {
jhsReqs = requirementsData
.where((r) => r.category == 'JHS')
.map((r) => r.name as String)
.map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
..sort(),
orElse: () => <String>[],
);
final shsReqs = requirementsAsync.maybeWhen(
data: (reqs) => reqs
..sort();
shsReqs = requirementsData
.where((r) => r.category == 'SHS')
.map((r) => r.name as String)
.map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
..sort(),
orElse: () => <String>[],
);
..sort();
}
// Update cached lists for _applyFilters
if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs;
@@ -1347,6 +1368,18 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
}
}
if (generalReqs.isNotEmpty) {
items.add(const PopupMenuDivider(height: 8));
items.add(PopupMenuItem<String>(
enabled: false,
height: 28,
child: buildHeader('OTHER DOCUMENTS'),
));
for (final doc in generalReqs) {
items.add(buildItem(doc, doc, isSubItem: true));
}
}
return items;
},
),
@@ -2317,9 +2317,25 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
OutlinedButton.icon(
onPressed: () async {
final uri = Uri.parse('https://github.com/ssbg04/TIS_RMS/issues');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
const url = 'https://github.com/ssbg04/TIS_RMS/issues';
final uri = Uri.parse(url);
bool launched = false;
try {
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched) {
launched = await launchUrl(uri, mode: LaunchMode.platformDefault);
}
} catch (_) {}
if (!launched) {
await Clipboard.setData(const ClipboardData(text: url));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Issue tracker link copied to clipboard: https://github.com/ssbg04/TIS_RMS/issues'),
backgroundColor: AppColors.primaryGreen,
),
);
}
}
},
icon: const Icon(Icons.bug_report_outlined, size: 15),
@@ -2334,9 +2350,25 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
),
OutlinedButton.icon(
onPressed: () async {
final uri = Uri.parse('https://github.com/ssbg04/TIS_RMS');
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
const url = 'https://github.com/ssbg04/TIS_RMS';
final uri = Uri.parse(url);
bool launched = false;
try {
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
if (!launched) {
launched = await launchUrl(uri, mode: LaunchMode.platformDefault);
}
} catch (_) {}
if (!launched) {
await Clipboard.setData(const ClipboardData(text: url));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('GitHub repository link copied to clipboard: https://github.com/ssbg04/TIS_RMS'),
backgroundColor: AppColors.primaryGreen,
),
);
}
}
},
icon: const Icon(Icons.code_rounded, size: 15),
@@ -1066,6 +1066,9 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
const SingleActivator(LogicalKeyboardKey.keyN, control: true): () {
_openModal();
},
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1232,7 +1235,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
height: (MediaQuery.of(context).size.width < 700 ||
defaultTargetPlatform ==
TargetPlatform.android)
? 76
? 0
: 16,
),
orElse: () => const SizedBox.shrink(),
@@ -1340,6 +1343,30 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
const Spacer(),
// Right: Filter Button for Android in header tabs
if (isAndroid) ...[
IconButton(
onPressed: () => StudentFilterDialog.show(
context,
query: query,
isEnrolledTab: _viewTab == StudentViewTab.enrolled,
),
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
icon: Badge(
isLabelVisible: activeCount > 0,
label: Text(activeCount.toString()),
child: Icon(
Icons.tune_rounded,
size: 20,
color: activeCount > 0
? AppColors.primaryGreen
: (isDark ? AppColors.darkTextPrimary : Colors.black87),
),
),
),
],
// Right: Action buttons (desktop / non-android)
if (!isAndroid) ...[
// Search IconButton
@@ -1677,7 +1704,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
top: isDesktop ? 10 : 14,
bottom: isDesktop ? 20 : 24,
bottom: isDesktop ? 20 : (defaultTargetPlatform == TargetPlatform.android ? 8 : 16),
),
itemCount: students.length,
separatorBuilder: (ctx, index) => SizedBox(
@@ -10,7 +10,6 @@ import '../../../../domain/repositories/student_repository.dart';
import '../../../providers/setup_provider.dart';
import '../../../providers/ocr_provider.dart';
import '../../../providers/student_provider.dart';
import '../../../providers/document_provider.dart';
import '../../documents/widgets/document_preview_modal.dart';
import '../../../shared/inputs/document_source_picker.dart';
import 'ocr_enrollment_validation_modal.dart';
@@ -59,8 +58,6 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
final List<OcrEnrollmentPrefill> _ocrSavedEnrollments = [];
File? _ocrScannedFile;
String _ocrScannedFileName = '';
String? _selectedOcrDocType;
static const _extSuggestions = [
'JR.',
@@ -297,7 +294,6 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
setState(() {
_errorMessage = null;
_ocrScannedFile = file;
_ocrScannedFileName = fileName;
});
String? docType = _detectDocType(fileName);
@@ -308,7 +304,6 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
if (docType == null || !mounted) return;
setState(() {
_selectedOcrDocType = docType;
_isLoading = true;
});
@@ -334,7 +329,6 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
_selectedDob = _parseFlexibleDob(ocrResult.dob);
}
_ocrScannedFile = file;
_selectedOcrDocType = docType;
_currentStep = 1;
});
@@ -425,7 +419,7 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
setState(() => _isLoading = true);
try {
final notifier = ref.read(studentMutationProvider.notifier);
final studentId = await notifier.createStudent(
await notifier.createStudent(
lrn: _lrnController.text.trim(),
firstName: _firstNameController.text.trim(),
middleName: _middleNameController.text.trim().isEmpty
@@ -444,56 +438,9 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
is4ps: _is4ps,
);
// After the student's directory is created, upload the scanned
// SF9/SF10 document into it.
String? uploadError;
if (_ocrScannedFile != null) {
try {
final bytes = await _ocrScannedFile!.readAsBytes();
final docType = _selectedOcrDocType ?? 'SF9';
final originalName = _ocrScannedFileName.isEmpty
? _ocrScannedFile!.path.split(RegExp(r'[\\/]')).last
: _ocrScannedFileName;
final ext = originalName.contains('.')
? originalName.split('.').last
: 'pdf';
final lrn = _lrnController.text.trim();
final renamedFileName = '${docType}_$lrn.$ext';
await ref.read(documentRepositoryProvider).uploadDocumentBytes(
studentId: studentId,
documentType: docType,
fileName: renamedFileName,
bytes: bytes,
);
} catch (e) {
final raw = e.toString();
uploadError = raw.startsWith('Exception: ') ? raw.substring(11) : raw;
}
}
if (!mounted) return;
ref.invalidate(studentPageProvider);
if (mounted) setState(() => _isLoading = false);
if (uploadError != null) {
await showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Student Created - Upload Failed'),
content: Text(
'The student was created successfully, but the scanned '
'document could not be uploaded to their folder.\n\n'
'$uploadError',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
}
if (!mounted) return;
Navigator.of(context).pop(true);
} catch (e) {
@@ -810,8 +757,6 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
onPressed: () => setState(() {
_errorMessage = null;
_ocrScannedFile = null;
_ocrScannedFileName = '';
_selectedOcrDocType = null;
_ocrSavedEnrollments.clear();
_lrnController.text = '308035';
_firstNameController.clear();
@@ -9,7 +9,6 @@ import 'package:desktop_drop/desktop_drop.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../domain/entities/setup_models.dart';
import '../../../providers/document_provider.dart';
import '../../../providers/ocr_provider.dart';
import '../../../providers/student_provider.dart';
import '../../../providers/setup_provider.dart';
@@ -773,49 +772,7 @@ class _BulkOcrImportDialogState extends ConsumerState<BulkOcrImportDialog> {
.read(studentMutationProvider.notifier)
.bulkCreateStudents(payload);
// Auto-upload scanned documents for newly created students
final rawResults = result['results'];
if (rawResults is List) {
for (final r in rawResults) {
if (r is Map && r['status'] == 'created' && r['id'] != null) {
final studentId = r['id'] as int;
final studentLrn = (r['lrn'] ?? '').toString();
// Find corresponding _OcrItem with local scanned file
final matchingItem = validItems.cast<_OcrItem?>().firstWhere(
(item) =>
item != null &&
item.lrn.trim() == studentLrn.trim() &&
!item.filePath.startsWith('csv_row_'),
orElse: () => null,
);
if (matchingItem != null) {
final localFile = File(matchingItem.filePath);
if (await localFile.exists()) {
try {
final bytes = await localFile.readAsBytes();
final docType = matchingItem.docType ?? 'SF9';
final originalName = matchingItem.fileName;
final ext = originalName.contains('.')
? originalName.split('.').last
: 'pdf';
// Renaming convention according to docType: e.g. SF9_308035123456.pdf or SF10_308035123456.pdf
final renamedFileName = '${docType}_${matchingItem.lrn}.$ext';
await ref.read(documentRepositoryProvider).uploadDocumentBytes(
studentId: studentId,
documentType: docType,
fileName: renamedFileName,
bytes: bytes,
);
} catch (upErr) {
debugPrint('Failed to auto-upload OCR doc for student $studentId: $upErr');
}
}
}
}
}
}
setState(() {
_importResult = result;
@@ -2006,7 +2006,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
}
final username = _usernameCtrl.text.trim();
await ref.read(usersProvider.notifier).createUser(
final tempPassword = await ref.read(usersProvider.notifier).createUser(
username: username,
firstName: _firstNameCtrl.text.trim(),
middleName: _middleNameCtrl.text.trim(),
@@ -2022,6 +2022,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
_showUserCreatedSuccessDialog(
context,
username: username,
temporaryPassword: tempPassword,
email: _emailCtrl.text.trim(),
);
} catch (e) {
@@ -2401,6 +2402,7 @@ Widget _buildEmailDomainSuggestions(
void _showUserCreatedSuccessDialog(
BuildContext ctx, {
required String username,
required String temporaryPassword,
String? email,
}) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
@@ -2426,6 +2428,13 @@ void _showUserCreatedSuccessDialog(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_credentialRow(dialogCtx, 'Username', username),
const SizedBox(height: 10),
_credentialRow(
dialogCtx,
'Temporary Password',
temporaryPassword,
highlight: true,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.28+28
version: 1.0.31+31
environment:
sdk: ^3.11.5