diff --git a/frontend/lib/ui/screens/documents/widgets/recycle_bin_modal.dart b/frontend/lib/ui/screens/documents/widgets/recycle_bin_modal.dart index 7851e0a..66f36f3 100644 --- a/frontend/lib/ui/screens/documents/widgets/recycle_bin_modal.dart +++ b/frontend/lib/ui/screens/documents/widgets/recycle_bin_modal.dart @@ -5,6 +5,7 @@ import 'package:frontend/ui/providers/document_provider.dart'; import 'package:frontend/ui/shared/dialogs/error_dialog.dart'; import 'package:frontend/ui/shared/dialogs/success_dialog.dart'; import 'package:frontend/ui/shared/inputs/app_search_bar.dart'; +import 'package:frontend/ui/shared/modals/custom_modal.dart'; class RecycleBinModal extends ConsumerStatefulWidget { const RecycleBinModal({super.key}); @@ -41,43 +42,14 @@ class _RecycleBinModalState extends ConsumerState { final isMobile = screenW < 700; final trashAsync = ref.watch(trashDocumentsProvider); - return Dialog( - backgroundColor: AppColors.surfaceWhite, - surfaceTintColor: Colors.transparent, - insetPadding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 32, - vertical: isMobile ? 24 : 40, - ), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800, maxHeight: 800), + return CustomModal( + title: 'Recycle Bin', + icon: Icons.delete_outline, + maxWidth: 800, + content: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 800), child: Column( children: [ - // Header - Padding( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 16), - child: Row( - children: [ - const Icon(Icons.delete_outline, size: 24, color: AppColors.textPrimary), - const SizedBox(width: 12), - const Text( - 'Recycle Bin', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.textPrimary, - ), - ), - const Spacer(), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - const Divider(height: 1), - // Controls Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), 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 bafec06..8f4e3c4 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,7 @@ import '../../../../domain/entities/setup_models.dart'; import '../../../shared/dialogs/success_dialog.dart'; import '../../../shared/dialogs/info_dialog.dart'; import '../../documents/widgets/document_preview_modal.dart'; +import '../../../shared/modals/custom_modal.dart'; // --------------------------------------------------------------- // Auto-capitalises the first letter of every word (works on paste) @@ -589,18 +590,17 @@ class _AddEditStudentModalState extends ConsumerState double maxDialogWidth = isMobile ? 380 : 620; - return Dialog( - insetPadding: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 24, - vertical: 12 + viewInsets.bottom * 0.05, - ), - backgroundColor: AppColors.surfaceWhite, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppSizes.radiusLarge), - ), - child: ConstrainedBox( + return CustomModal( + title: _showOcrStep + ? 'Auto-Fill with OCR' + : (widget.student != null ? 'Update Student Record' : 'Student Details'), + icon: _showOcrStep + ? Icons.document_scanner_outlined + : (widget.student != null ? Icons.edit : Icons.person_add), + maxWidth: maxDialogWidth, + onClose: _showOcrStep ? () => Navigator.of(context).pop() : _confirmClose, + content: ConstrainedBox( constraints: BoxConstraints( - maxWidth: maxDialogWidth, maxHeight: dialogHeight, ), child: Padding( @@ -639,28 +639,6 @@ class _AddEditStudentModalState extends ConsumerState mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Flexible( - child: Text( - 'Auto-Fill with OCR', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: AppColors.primaryGreen, - ), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - const Divider(height: 32), - if (_errorMessage != null) ...[ _ErrorBanner(message: _errorMessage!), const SizedBox(height: AppSizes.p16), @@ -758,31 +736,6 @@ class _AddEditStudentModalState extends ConsumerState mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - // ---- Header ---- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - isEdit - ? 'Update Student Record' - : 'Student Details', - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.primaryGreen, - ), - overflow: TextOverflow.ellipsis, - ), - ), - IconButton( - icon: const Icon(Icons.close, color: AppColors.textSecondary), - onPressed: _confirmClose, - ), - ], - ), - const Divider(height: 16), - // ---- Tab Bar ---- TabBar( controller: _tabController, diff --git a/frontend/lib/ui/screens/users/users_screen.dart b/frontend/lib/ui/screens/users/users_screen.dart index 4b24950..74c29d8 100644 --- a/frontend/lib/ui/screens/users/users_screen.dart +++ b/frontend/lib/ui/screens/users/users_screen.dart @@ -414,7 +414,24 @@ class _UsersScreenState extends ConsumerState { maxWidth: double.infinity, ), ), - + if (_searchQuery.isNotEmpty || _roleFilter != 'all') ...[ + const SizedBox(width: AppSizes.p16), + OutlinedButton.icon( + onPressed: () { + _searchController.clear(); + setState(() { + _roleFilter = 'all'; + _searchQuery = ''; + }); + }, + icon: const Icon(Icons.filter_alt_off, size: 18), + label: const Text('Reset'), + style: OutlinedButton.styleFrom( + foregroundColor: Colors.grey.shade700, + side: BorderSide(color: Colors.grey.shade300), + ), + ), + ] ], ), ), @@ -756,127 +773,106 @@ class _AddEditUserModalState extends ConsumerState { @override Widget build(BuildContext context) { final currentUser = ref.watch(authProvider).value; - return Dialog( - backgroundColor: AppColors.surfaceWhite, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppSizes.radiusLarge)), - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 560), - child: Padding( - padding: const EdgeInsets.all(AppSizes.p24), - child: Form( - key: _formKey, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Header - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row(children: [ - Icon(_isEdit ? Icons.edit : Icons.person_add, color: AppColors.primaryGreen), - const SizedBox(width: 8), - Text( - _isEdit ? 'Edit System User' : 'Create New User', - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: AppColors.textPrimary), - ), - ]), - IconButton(icon: const Icon(Icons.close), onPressed: () => Navigator.of(context).pop()), - ], + return CustomModal( + title: _isEdit ? 'Edit System User' : 'Create New User', + icon: _isEdit ? Icons.edit : Icons.person_add, + maxWidth: 560, + content: SingleChildScrollView( + padding: const EdgeInsets.all(AppSizes.p24), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name fields + _field('First Name', Icons.badge, _firstNameCtrl, required: true), + const SizedBox(height: AppSizes.p12), + _field('Middle Name', Icons.badge, _middleNameCtrl), + const SizedBox(height: AppSizes.p12), + _field('Last Name', Icons.badge, _lastNameCtrl, required: true), + const SizedBox(height: AppSizes.p12), + _field('Ext. (Jr, Sr, III)', Icons.text_fields, _extCtrl), + const SizedBox(height: AppSizes.p12), + + // Username + _field('Username', Icons.person, _usernameCtrl, required: !_isEdit, readOnly: _isEdit), + const SizedBox(height: AppSizes.p12), + + // Role dropdown + if (_isEdit && widget.user?.id == currentUser?.id) + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(AppSizes.radiusMedium), + border: Border.all(color: Colors.grey.shade300), ), - const Divider(height: 28), + child: Row(children: [ + Icon(Icons.shield, color: _roleColor(widget.user!.role), size: 20), + const SizedBox(width: 12), + Text(widget.user!.role.toUpperCase(), style: TextStyle(fontWeight: FontWeight.bold, color: _roleColor(widget.user!.role))), + const SizedBox(width: 8), + const Icon(Icons.lock, color: Colors.grey, size: 14), + const SizedBox(width: 4), + const Text('(Your own role cannot be changed)', style: TextStyle(fontSize: 12, color: Colors.grey)), + ]), + ) + else + DropdownButtonFormField( + value: _selectedRole, + decoration: const InputDecoration(prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textSecondary)), + items: const [ + DropdownMenuItem(value: 'teacher', child: Text('Teacher')), + DropdownMenuItem(value: 'admin', child: Text('Admin')), + ], + onChanged: (val) => setState(() => _selectedRole = val!), + ), + const SizedBox(height: AppSizes.p12), - // Name fields - _field('First Name', Icons.badge, _firstNameCtrl, required: true), - const SizedBox(height: AppSizes.p12), - _field('Middle Name', Icons.badge, _middleNameCtrl), - const SizedBox(height: AppSizes.p12), - _field('Last Name', Icons.badge, _lastNameCtrl, required: true), - const SizedBox(height: AppSizes.p12), - _field('Ext. (Jr, Sr, III)', Icons.text_fields, _extCtrl), - const SizedBox(height: AppSizes.p12), + // Contact + _field('Email Address', Icons.email_outlined, _emailCtrl, validator: AppValidators.validateEmail), + const SizedBox(height: AppSizes.p12), + _field('Phone Number (Starts with 09)', Icons.phone_outlined, _phoneCtrl, validator: AppValidators.validatePhone), + const SizedBox(height: AppSizes.p12), - // Username - _field('Username', Icons.person, _usernameCtrl, required: !_isEdit, readOnly: _isEdit), - const SizedBox(height: AppSizes.p12), - - // Role dropdown - if (_isEdit && widget.user?.id == currentUser?.id) - Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(AppSizes.radiusMedium), - border: Border.all(color: Colors.grey.shade300), + if (!_isEdit) + Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: AppColors.primaryGreen.withValues(alpha: 0.07), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: AppColors.primaryGreen.withValues(alpha: 0.3)), + ), + child: const Row(children: [ + Icon(Icons.auto_awesome, color: AppColors.primaryGreen, size: 16), + SizedBox(width: 8), + Expanded( + child: Text( + 'A secure temporary password will be auto-generated and shown once after creation.', + style: TextStyle(fontSize: 12, color: AppColors.primaryGreen, fontWeight: FontWeight.w600), ), - child: Row(children: [ - Icon(Icons.shield, color: _roleColor(widget.user!.role), size: 20), - const SizedBox(width: 12), - Text(widget.user!.role.toUpperCase(), style: TextStyle(fontWeight: FontWeight.bold, color: _roleColor(widget.user!.role))), - const SizedBox(width: 8), - const Icon(Icons.lock, color: Colors.grey, size: 14), - const SizedBox(width: 4), - const Text('(Your own role cannot be changed)', style: TextStyle(fontSize: 12, color: Colors.grey)), - ]), - ) - else - DropdownButtonFormField( - value: _selectedRole, - decoration: const InputDecoration(prefixIcon: Icon(Icons.shield_outlined, color: AppColors.textSecondary)), - items: const [ - DropdownMenuItem(value: 'teacher', child: Text('Teacher')), - DropdownMenuItem(value: 'admin', child: Text('Admin')), - ], - onChanged: (val) => setState(() => _selectedRole = val!), ), - const SizedBox(height: AppSizes.p12), + ]), + ), - // Contact - _field('Email Address', Icons.email_outlined, _emailCtrl, validator: AppValidators.validateEmail), - const SizedBox(height: AppSizes.p12), - _field('Phone Number (Starts with 09)', Icons.phone_outlined, _phoneCtrl, validator: AppValidators.validatePhone), - const SizedBox(height: AppSizes.p12), - - if (!_isEdit) - Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: AppColors.primaryGreen.withValues(alpha: 0.07), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: AppColors.primaryGreen.withValues(alpha: 0.3)), - ), - child: const Row(children: [ - Icon(Icons.auto_awesome, color: AppColors.primaryGreen, size: 16), - SizedBox(width: 8), - Expanded( - child: Text( - 'A secure temporary password will be auto-generated and shown once after creation.', - style: TextStyle(fontSize: 12, color: AppColors.primaryGreen, fontWeight: FontWeight.w600), - ), - ), - ]), + const SizedBox(height: AppSizes.p24), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCEL')), + const SizedBox(width: AppSizes.p16), + SizedBox( + width: 150, + child: PrimaryButton( + label: _isEdit ? 'UPDATE' : 'CREATE', + isLoading: _isLoading, + onPressed: _handleSave, ), - - const SizedBox(height: AppSizes.p12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton(onPressed: () => Navigator.pop(context), child: const Text('CANCEL')), - const SizedBox(width: AppSizes.p16), - SizedBox( - width: 150, - child: PrimaryButton( - label: _isEdit ? 'UPDATE' : 'CREATE', - isLoading: _isLoading, - onPressed: _handleSave, - ), - ), - ], ), ], ), - ), + ], ), ), ), diff --git a/frontend/lib/ui/shared/modals/custom_modal.dart b/frontend/lib/ui/shared/modals/custom_modal.dart index 1409923..2ce0058 100644 --- a/frontend/lib/ui/shared/modals/custom_modal.dart +++ b/frontend/lib/ui/shared/modals/custom_modal.dart @@ -7,6 +7,7 @@ class CustomModal extends StatelessWidget { final Widget content; final List? headerActions; final double maxWidth; + final VoidCallback? onClose; const CustomModal({ super.key, @@ -15,6 +16,7 @@ class CustomModal extends StatelessWidget { required this.content, this.headerActions, this.maxWidth = 620, + this.onClose, }); static Future show({ @@ -24,6 +26,7 @@ class CustomModal extends StatelessWidget { required Widget content, List? headerActions, double maxWidth = 620, + VoidCallback? onClose, }) { return showDialog( context: context, @@ -34,6 +37,7 @@ class CustomModal extends StatelessWidget { content: content, headerActions: headerActions, maxWidth: maxWidth, + onClose: onClose, ), ); } @@ -84,7 +88,7 @@ class CustomModal extends StatelessWidget { icon: const Icon(Icons.close, color: Colors.white, size: 20), padding: EdgeInsets.zero, constraints: const BoxConstraints(), - onPressed: () => Navigator.of(context).pop(), + onPressed: onClose ?? () => Navigator.of(context).pop(), ), ], ),