feat(system): implement socket.io real-time sync, admin passwords, ocr adjustments, and shortcut fixes
This commit is contained in:
1 parent
f30faeecff
commit
b04981d7fb
46 files changed
+3878
-176
No files matched your search
+58
-2
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user