Complete requested UI tasks
This commit is contained in:
1 parent
f2e52a22e5
commit
f20a7078e6
4 files changed
+131
-206
No files matched your search
@@ -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<RecycleBinModal> {
|
||||
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),
|
||||
|
||||
@@ -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<AddEditStudentModal>
|
||||
|
||||
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<AddEditStudentModal>
|
||||
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<AddEditStudentModal>
|
||||
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,
|
||||
|
||||
@@ -414,7 +414,24 @@ class _UsersScreenState extends ConsumerState<UsersScreen> {
|
||||
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<AddEditUserModal> {
|
||||
@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<String>(
|
||||
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<String>(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ class CustomModal extends StatelessWidget {
|
||||
final Widget content;
|
||||
final List<Widget>? 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<T?> show<T>({
|
||||
@@ -24,6 +26,7 @@ class CustomModal extends StatelessWidget {
|
||||
required Widget content,
|
||||
List<Widget>? headerActions,
|
||||
double maxWidth = 620,
|
||||
VoidCallback? onClose,
|
||||
}) {
|
||||
return showDialog<T>(
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user