From e39fc5593dbdaf6288912f2b819cced67b6037b4 Mon Sep 17 00:00:00 2001 From: ssbg04 Date: Fri, 19 Jun 2026 00:34:51 +0800 Subject: [PATCH] Regit --- .../ui/layouts/android_bottom_nav_layout.dart | 222 +++++++----------- .../settings/teacher_management_screen.dart | 2 + .../widgets/add_edit_student_modal.dart | 118 +++++++++- .../lib/ui/shared/dialogs/logout_dialog.dart | 4 +- .../ui/shared/inputs/custom_text_field.dart | 7 + 5 files changed, 211 insertions(+), 142 deletions(-) diff --git a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart index b6c2bf4..8016dc5 100644 --- a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart +++ b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart @@ -67,93 +67,9 @@ class _AndroidBottomNavLayoutState extends ConsumerState } } - void _onNavTapped(int index, List> allowedPrimary, List> allowedSecondary) { - if (index < allowedPrimary.length) { - final label = allowedPrimary[index]['label'] as String; - - // ✅ NEW: Update global state instead of local setState - ref.read(activeTabProvider.notifier).setTab(label); - _reloadTabContent(label); - } else { - _showMoreBottomSheet(allowedPrimary, allowedSecondary); - } - } - - void _showMoreBottomSheet(List> allowedPrimary, List> allowedSecondary) { - showModalBottomSheet( - context: context, - backgroundColor: AppColors.surfaceWhite, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(AppSizes.radiusLarge)), - ), - builder: (sheetContext) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: AppSizes.p12), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // Drag Handle - Container( - width: 40, - height: 5, - margin: const EdgeInsets.only(bottom: AppSizes.p16), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(AppSizes.radiusSmall), - ), - ), - // Build Secondary Tabs - ...allowedSecondary.asMap().entries.map((entry) { - int secondaryIndex = entry.key; - var tab = entry.value; - - return ListTile( - leading: Container( - padding: const EdgeInsets.all(AppSizes.p8), - decoration: BoxDecoration( - color: AppColors.primaryGreen.withOpacity(0.1), - borderRadius: BorderRadius.circular(AppSizes.radiusMedium), - ), - child: Icon(tab['icon'] as IconData, color: AppColors.primaryGreen), - ), - title: Text( - tab['label'] as String, - style: const TextStyle(fontWeight: FontWeight.w600, color: AppColors.textPrimary), - ), - trailing: const Icon(Icons.chevron_right, color: AppColors.textMuted), - onTap: () { - Navigator.pop(sheetContext); - final label = tab['label'] as String; - // ✅ NEW: Update global state when a "More" option is clicked - ref.read(activeTabProvider.notifier).setTab(label); - _reloadTabContent(label); - }, - ); - }), - const Divider(), - // Logout Option in the More Menu - ListTile( - leading: const Padding( - padding: EdgeInsets.all(AppSizes.p8), - child: Icon(Icons.exit_to_app, color: AppColors.error), - ), - title: const Text( - 'Logout', - style: TextStyle(fontWeight: FontWeight.w600, color: AppColors.error), - ), - onTap: () { - // DO NOT pop the bottom sheet here. - // Just show the dialog directly! - showLogoutConfirmationDialog(context); - }, - ), - ], - ), - ), - ); - }, - ); + void _onNavTapped(String label) { + ref.read(activeTabProvider.notifier).setTab(label); + _reloadTabContent(label); } @override @@ -180,28 +96,6 @@ class _AndroidBottomNavLayoutState extends ConsumerState _visitedIndices.add(currentIndex); - int selectedBottomNavIndex = allowedPrimary.indexWhere((t) => t['label'] == activeTab); - if (selectedBottomNavIndex == -1) { - // If the active tab is NOT in the primary list (e.g., Settings), highlight the "More" tab - selectedBottomNavIndex = allowedPrimary.length; - } - - List bottomNavItems = allowedPrimary.map((tab) { - return BottomNavigationBarItem( - icon: Icon(tab['icon'] as IconData), - label: tab['label'] as String, - ); - }).toList(); - - if (allowedSecondary.isNotEmpty) { - bottomNavItems.add( - const BottomNavigationBarItem( - icon: Icon(Icons.more_horiz), - label: 'More', - ), - ); - } - return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { @@ -238,6 +132,69 @@ class _AndroidBottomNavLayoutState extends ConsumerState }, child: Scaffold( backgroundColor: AppColors.pageBackground, + appBar: AppBar( + backgroundColor: AppColors.primaryGreen, + foregroundColor: Colors.white, + iconTheme: const IconThemeData(color: Colors.white), + title: Text( + tabs[currentIndex]['label'] as String, + style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white), + ), + elevation: 0, + ), + drawer: Drawer( + child: Column( + children: [ + UserAccountsDrawerHeader( + decoration: const BoxDecoration(color: AppColors.primaryGreen), + accountName: Text( + 'TIS RMS', + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18, color: Colors.white), + ), + accountEmail: Text( + 'Role: ${widget.userRole.toUpperCase()}', + style: const TextStyle(color: Colors.white70), + ), + currentAccountPicture: const CircleAvatar( + backgroundColor: Colors.transparent, + backgroundImage: AssetImage('assets/images/logo.png'), + ), + ), + Expanded( + child: ListView( + padding: EdgeInsets.zero, + children: [ + if (allowedPrimary.isNotEmpty) ...[ + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text('MAIN', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.bold, fontSize: 12)), + ), + ...allowedPrimary.map((tab) => _buildDrawerItem(tab, currentIndex, tabs)), + ], + if (allowedSecondary.isNotEmpty) ...[ + const Divider(), + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text('OTHER', style: TextStyle(color: AppColors.textSecondary, fontWeight: FontWeight.bold, fontSize: 12)), + ), + ...allowedSecondary.map((tab) => _buildDrawerItem(tab, currentIndex, tabs)), + ], + ], + ), + ), + const Divider(), + ListTile( + leading: const Icon(Icons.exit_to_app, color: AppColors.error), + title: const Text('Logout', style: TextStyle(fontWeight: FontWeight.bold, color: AppColors.error)), + onTap: () { + Navigator.pop(context); // Close drawer first + showLogoutConfirmationDialog(context); + }, + ), + const SizedBox(height: 16), + ], + ), + ), body: IndexedStack( index: currentIndex, children: tabs.asMap().entries.map((entry) { @@ -246,30 +203,31 @@ class _AndroidBottomNavLayoutState extends ConsumerState : const SizedBox.shrink(); }).toList(), ), - bottomNavigationBar: Container( - decoration: BoxDecoration( - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.1), - blurRadius: 10, - offset: const Offset(0, -5), - ), - ], - ), - child: BottomNavigationBar( - currentIndex: selectedBottomNavIndex, - onTap: (i) => _onNavTapped(i, allowedPrimary, allowedSecondary), - backgroundColor: AppColors.primaryGreen, - selectedItemColor: Colors.white, - unselectedItemColor: Colors.white70, - showUnselectedLabels: true, - type: BottomNavigationBarType.fixed, - selectedLabelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12), - unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 11), - items: bottomNavItems, - ), - ), ), ); } + + Widget _buildDrawerItem(Map tab, int currentIndex, List> allTabs) { + final label = tab['label'] as String; + final icon = tab['icon'] as IconData; + final index = allTabs.indexWhere((t) => t['label'] == label); + final isSelected = index == currentIndex; + + return ListTile( + leading: Icon(icon, color: isSelected ? AppColors.primaryGreen : AppColors.textSecondary), + title: Text( + label, + style: TextStyle( + fontWeight: isSelected ? FontWeight.bold : FontWeight.normal, + color: isSelected ? AppColors.primaryGreen : AppColors.textSecondary, + ), + ), + selected: isSelected, + selectedTileColor: AppColors.primaryGreen.withOpacity(0.1), + onTap: () { + Navigator.pop(context); // Close drawer + _onNavTapped(label); + }, + ); + } } \ No newline at end of file diff --git a/frontend/lib/ui/screens/settings/teacher_management_screen.dart b/frontend/lib/ui/screens/settings/teacher_management_screen.dart index 343e70c..607e09e 100644 --- a/frontend/lib/ui/screens/settings/teacher_management_screen.dart +++ b/frontend/lib/ui/screens/settings/teacher_management_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/constants/app_colors.dart'; import '../../../core/constants/app_sizes.dart'; @@ -876,6 +877,7 @@ class _GradeLevelFormModalState extends ConsumerState { controller: _levelController, prefixIcon: Icons.numbers, keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], validator: (v) { if (v == null || v.trim().isEmpty) return 'Level is required'; if (int.tryParse(v) == null) return 'Must be a valid integer'; diff --git a/frontend/lib/ui/screens/students/widgets/add_edit_student_modal.dart b/frontend/lib/ui/screens/students/widgets/add_edit_student_modal.dart index 94ff69c..a7b3f6f 100644 --- a/frontend/lib/ui/screens/students/widgets/add_edit_student_modal.dart +++ b/frontend/lib/ui/screens/students/widgets/add_edit_student_modal.dart @@ -17,6 +17,29 @@ import '../../../shared/dialogs/success_dialog.dart'; import '../../../shared/dialogs/info_dialog.dart'; import 'package:syncfusion_flutter_pdfviewer/pdfviewer.dart'; +// --------------------------------------------------------------- +// Auto-capitalises the first letter of every word (works on paste) +// --------------------------------------------------------------- +class _UpperCaseWordsFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final text = newValue.text; + if (text.isEmpty) return newValue; + final capitalized = text.replaceAllMapped( + RegExp(r'(^|\s)(\S)'), + (m) => '${m[1]}${m[2]!.toUpperCase()}', + ); + return newValue.copyWith( + text: capitalized, + selection: newValue.selection, + composing: TextRange.empty, + ); + } +} + class AddEditStudentModal extends ConsumerStatefulWidget { final StudentModel? student; @@ -89,6 +112,63 @@ class _AddEditStudentModalState extends ConsumerState super.dispose(); } + // ---------------------------------------------------------------- + // EXIT CONFIRMATION + // ---------------------------------------------------------------- + bool get _hasAnyData => + _lrnController.text.isNotEmpty || + _firstNameController.text.isNotEmpty || + _middleNameController.text.isNotEmpty || + _lastNameController.text.isNotEmpty || + _selectedDob != null; + + Future _confirmClose() async { + // If editing, or nothing entered yet — close immediately + if (widget.student != null || !_hasAnyData) { + if (mounted) Navigator.of(context).pop(); + return; + } + final shouldClose = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + title: const Row( + children: [ + Icon(Icons.warning_amber_rounded, color: Colors.orange), + SizedBox(width: 8), + Text('Discard Changes?'), + ], + ), + content: const Text( + 'You have unsaved data. Are you sure you want to close without saving?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), + child: const Text( + 'KEEP EDITING', + style: TextStyle( + color: AppColors.primaryGreen, + fontWeight: FontWeight.bold, + ), + ), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), + child: const Text( + 'DISCARD', + style: TextStyle( + color: Colors.redAccent, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + if (shouldClose == true && mounted) Navigator.of(context).pop(); + } + // ---------------------------------------------------------------- // OCR PROCESSING // ---------------------------------------------------------------- @@ -379,6 +459,7 @@ class _AddEditStudentModalState extends ConsumerState gradeLevelsAsync, sectionsAsync, isMobile, + viewInsets.bottom, ), ), const VerticalDivider(width: 32), @@ -394,6 +475,7 @@ class _AddEditStudentModalState extends ConsumerState gradeLevelsAsync, sectionsAsync, isMobile, + viewInsets.bottom, )), ), ), @@ -606,6 +688,7 @@ class _AddEditStudentModalState extends ConsumerState AsyncValue> gradeLevelsAsync, AsyncValue> sectionsAsync, bool isMobile, + double keyboardInset, ) { final compactTheme = Theme.of(context).copyWith( inputDecorationTheme: Theme.of(context).inputDecorationTheme.copyWith( @@ -646,7 +729,7 @@ class _AddEditStudentModalState extends ConsumerState ), IconButton( icon: const Icon(Icons.close, color: AppColors.textSecondary), - onPressed: () => Navigator.of(context).pop(), + onPressed: _confirmClose, ), ], ), @@ -717,6 +800,8 @@ class _AddEditStudentModalState extends ConsumerState Expanded( child: TextFormField( controller: _firstNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], validator: (v) => _validateRequired( v, 'First name', @@ -733,6 +818,8 @@ class _AddEditStudentModalState extends ConsumerState Expanded( child: TextFormField( controller: _middleNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], decoration: const InputDecoration( labelText: 'Middle Name (optional)', prefixIcon: Icon( @@ -750,6 +837,8 @@ class _AddEditStudentModalState extends ConsumerState flex: 3, child: TextFormField( controller: _lastNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], validator: (v) => _validateRequired(v, 'Last name'), decoration: const InputDecoration( @@ -764,6 +853,8 @@ class _AddEditStudentModalState extends ConsumerState Expanded( child: TextFormField( controller: _extController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], decoration: const InputDecoration( labelText: 'Ext.', hintText: 'Jr / III', @@ -780,6 +871,8 @@ class _AddEditStudentModalState extends ConsumerState children: [ TextFormField( controller: _firstNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], validator: (v) => _validateRequired(v, 'First name'), decoration: const InputDecoration( @@ -789,6 +882,8 @@ class _AddEditStudentModalState extends ConsumerState const SizedBox(height: AppSizes.p12), TextFormField( controller: _middleNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], decoration: const InputDecoration( labelText: 'Middle Name', ), @@ -796,6 +891,8 @@ class _AddEditStudentModalState extends ConsumerState const SizedBox(height: AppSizes.p12), TextFormField( controller: _lastNameController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], validator: (v) => _validateRequired(v, 'Last name'), decoration: const InputDecoration( @@ -805,6 +902,8 @@ class _AddEditStudentModalState extends ConsumerState const SizedBox(height: AppSizes.p12), TextFormField( controller: _extController, + textCapitalization: TextCapitalization.words, + inputFormatters: [_UpperCaseWordsFormatter()], decoration: const InputDecoration( labelText: 'Extension (Jr / III)', ), @@ -1046,12 +1145,13 @@ class _AddEditStudentModalState extends ConsumerState // ---- Actions ---- if (isMobile) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ + if (keyboardInset == 0) // Hide when keyboard is open + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ PrimaryButton( label: isEdit ? 'UPDATE' : 'SAVE', isLoading: _isLoading, @@ -1092,7 +1192,7 @@ class _AddEditStudentModalState extends ConsumerState TextButton( onPressed: _isLoading ? null - : () => Navigator.of(context).pop(), + : _confirmClose, child: const Text( 'CANCEL', style: TextStyle( @@ -1131,7 +1231,7 @@ class _AddEditStudentModalState extends ConsumerState TextButton( onPressed: _isLoading ? null - : () => Navigator.of(context).pop(), + : _confirmClose, child: const Text( 'CANCEL', style: TextStyle( diff --git a/frontend/lib/ui/shared/dialogs/logout_dialog.dart b/frontend/lib/ui/shared/dialogs/logout_dialog.dart index f6db68c..b37abb8 100644 --- a/frontend/lib/ui/shared/dialogs/logout_dialog.dart +++ b/frontend/lib/ui/shared/dialogs/logout_dialog.dart @@ -33,8 +33,10 @@ Future showLogoutConfirmationDialog(BuildContext context) async { // 1. Close the dialog immediately for responsiveness navigator.pop(); - // 2. Remove the saved password for security (keep username intact) + // 2. Remove all saved credentials for security final prefs = await SharedPreferences.getInstance(); + await prefs.remove('rememberMe'); + await prefs.remove('saved_username'); await prefs.remove('saved_password'); // 3. Navigate FIRST to LoginScreen and destroy routing history. diff --git a/frontend/lib/ui/shared/inputs/custom_text_field.dart b/frontend/lib/ui/shared/inputs/custom_text_field.dart index 01428b3..01b60ba 100644 --- a/frontend/lib/ui/shared/inputs/custom_text_field.dart +++ b/frontend/lib/ui/shared/inputs/custom_text_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class CustomTextField extends StatelessWidget { final String hintText; @@ -14,6 +15,8 @@ class CustomTextField extends StatelessWidget { final TextInputType? keyboardType; final TextInputAction? textInputAction; final void Function(String)? onSubmitted; + final int? maxLength; + final List? inputFormatters; const CustomTextField({ super.key, @@ -30,6 +33,8 @@ class CustomTextField extends StatelessWidget { this.keyboardType, this.textInputAction, this.onSubmitted, + this.maxLength, + this.inputFormatters, }); @override @@ -41,7 +46,9 @@ class CustomTextField extends StatelessWidget { onChanged: onChanged, validator: validator, maxLines: maxLines, + maxLength: maxLength, keyboardType: keyboardType, + inputFormatters: inputFormatters, textInputAction: textInputAction, onFieldSubmitted: onSubmitted, decoration: InputDecoration(