Complete requested UI tasks

This commit is contained in:
ssbg04 committed 2026-07-03 22:56:49 +08:00
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,
+108 -112
View File
@@ -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(),
),
],
),