feat(ui): update recent activities, student modals, documents filters, and settings theme button
This commit is contained in:
1 parent
41d5fb3459
commit
bec1e4fba7
12 files changed
+237
-103
No files matched your search
@@ -39,7 +39,7 @@ class _RecentActivitiesScreenState
|
||||
selectedDayHighlightColor: AppColors.primaryGreen,
|
||||
),
|
||||
dialogSize: const Size(325, 400),
|
||||
value: [if (_fromDate != null) _fromDate!, if (_toDate != null) _toDate!],
|
||||
value: [_fromDate, _toDate].whereType<DateTime>().toList(),
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
);
|
||||
if (values != null && values.isNotEmpty) {
|
||||
@@ -223,7 +223,7 @@ class _RecentActivitiesScreenState
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: ['All Entities', 'student', 'document']
|
||||
children: ['All Entities', 'student', 'enrollment', 'document', 'user']
|
||||
.map((v) {
|
||||
final isSelected = pendingEntity == v;
|
||||
return ChoiceChip(
|
||||
@@ -421,7 +421,7 @@ class _RecentActivitiesScreenState
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: activities.length,
|
||||
separatorBuilder: (_, _s) => const SizedBox(height: 12),
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, i) {
|
||||
final a = activities[i];
|
||||
|
||||
@@ -623,6 +623,4 @@ class _RecentActivitiesScreenState
|
||||
return Icons.description;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(String raw) => pht.formatDateTime(raw);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart'
|
||||
show defaultTargetPlatform, TargetPlatform;
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -27,7 +26,6 @@ import '../../../core/network/api_constants.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'widgets/recycle_bin_modal.dart';
|
||||
import 'widgets/bulk_operations_bar.dart';
|
||||
import 'widgets/documents_header.dart';
|
||||
import '../../../domain/entities/document_model.dart';
|
||||
|
||||
class DocumentsScreen extends ConsumerStatefulWidget {
|
||||
@@ -1348,13 +1346,18 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
const defaultStatuses = [
|
||||
'All Statuses',
|
||||
'Pending',
|
||||
'Verified',
|
||||
'Draft',
|
||||
'Completed',
|
||||
'Archived',
|
||||
];
|
||||
final statusItems = statusesAsync.when(
|
||||
data: (s) => (defaultStatuses.toSet()..addAll(s)).toList(),
|
||||
data: (s) => (defaultStatuses.toSet()
|
||||
..addAll(s.where((item) {
|
||||
final lower = item.toLowerCase();
|
||||
return lower != 'verified' &&
|
||||
lower != 'draft' &&
|
||||
lower != 'pending';
|
||||
})))
|
||||
.toList(),
|
||||
loading: () => defaultStatuses,
|
||||
error: (_, st) => defaultStatuses,
|
||||
);
|
||||
|
||||
@@ -98,7 +98,11 @@ class _StudentProfileDialogShellState
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 620),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 620,
|
||||
minWidth: widget.isMobile ? 0 : 540,
|
||||
minHeight: 460,
|
||||
),
|
||||
color: isDark ? AppColors.darkPageBackground : AppColors.pageBackground,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -314,7 +318,7 @@ class StudentProfileModalBody extends ConsumerWidget {
|
||||
|
||||
return studentAsync.when(
|
||||
loading: () => const SizedBox(
|
||||
height: 200,
|
||||
height: 400,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primaryGreen),
|
||||
),
|
||||
|
||||
@@ -1080,6 +1080,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
backgroundColor:
|
||||
AppColors
|
||||
.primaryGreen,
|
||||
foregroundColor:
|
||||
isDark ? Colors.white : Colors.black,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
@@ -1138,6 +1140,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
backgroundColor:
|
||||
AppColors
|
||||
.primaryGreen,
|
||||
foregroundColor:
|
||||
isDark ? Colors.white : Colors.black,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
vertical: 14,
|
||||
|
||||
@@ -800,9 +800,10 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
ErrorBanner(message: _errorMessage!),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -897,8 +898,16 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
|
||||
.where((y) => y.status.toLowerCase() == 'active')
|
||||
.toList();
|
||||
final defaultYearId = (active.isNotEmpty ? active.last : (years.isNotEmpty ? years.last : null))?.id;
|
||||
final currentYearId = _selectedAcademicYearId ?? defaultYearId;
|
||||
|
||||
if (_selectedAcademicYearId == null && defaultYearId != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && _selectedAcademicYearId == null) {
|
||||
setState(() => _selectedAcademicYearId = defaultYearId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final currentYearId = _selectedAcademicYearId ?? defaultYearId;
|
||||
final selectedYearExists = years.any((y) => y.id == currentYearId);
|
||||
final effectiveValue = selectedYearExists ? currentYearId : null;
|
||||
|
||||
@@ -915,11 +924,11 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
|
||||
.toList(),
|
||||
onChanged: (val) => setState(() {
|
||||
_selectedAcademicYearId = val;
|
||||
_selectedGradeLevel = 7;
|
||||
_selectedGradeLevel = _selectedGradeLevel ?? 7;
|
||||
_selectedSectionId = null;
|
||||
_trackStrand = null;
|
||||
}),
|
||||
validator: (v) => v == null ? 'Academic year is required.' : null,
|
||||
validator: (v) => (v == null && effectiveValue == null) ? 'Academic year is required.' : null,
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
@@ -968,11 +977,12 @@ class _AddStudentModalState extends ConsumerState<AddStudentModal> {
|
||||
final activeYears = yearsAsync.value?.where((y) => y.status.toLowerCase() == 'active').toList();
|
||||
final defaultYearId = (activeYears != null && activeYears.isNotEmpty ? activeYears.last : (yearsAsync.value != null && yearsAsync.value!.isNotEmpty ? yearsAsync.value!.last : null))?.id;
|
||||
final effectiveYearId = _selectedAcademicYearId ?? defaultYearId;
|
||||
final effectiveGrade = _selectedGradeLevel ?? 7;
|
||||
|
||||
final filtered = sections
|
||||
.where((sec) =>
|
||||
sec.academicYearId == effectiveYearId &&
|
||||
sec.gradeLevel == _selectedGradeLevel)
|
||||
sec.gradeLevel == effectiveGrade)
|
||||
.toList();
|
||||
final matches = filtered.where((s) => s.id == _selectedSectionId);
|
||||
final initialSectionName =
|
||||
|
||||
@@ -563,51 +563,114 @@ class _EditEnrollmentModalState extends ConsumerState<EditEnrollmentModal> {
|
||||
],
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.end,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: AppSizes.p12,
|
||||
runSpacing: AppSizes.p8,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
padding: const EdgeInsets.only(top: 20, bottom: 8),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isMobile = constraints.maxWidth < 450;
|
||||
if (isMobile) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_currentEnrollment == null) ...[
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey('add_more_enrollment_button'),
|
||||
onPressed: _isLoading ? null : _handleAddMoreEnrollment,
|
||||
icon: const Icon(Icons.add_circle_outline, size: 16),
|
||||
label: const Text(
|
||||
'Add More Enrollment',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
label: 'SAVE',
|
||||
isLoading: _isLoading,
|
||||
onPressed: _handleSave,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => Navigator.of(context).pop(),
|
||||
child: const Text(
|
||||
'CANCEL',
|
||||
style: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey('add_more_enrollment_button'),
|
||||
onPressed: _isLoading ? null : _handleAddMoreEnrollment,
|
||||
icon: const Icon(Icons.add_circle_outline, size: 16),
|
||||
label: const Text(
|
||||
'Add More Enrollment',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
const SizedBox(width: 12),
|
||||
if (_currentEnrollment == null) ...[
|
||||
OutlinedButton.icon(
|
||||
key: const ValueKey('add_more_enrollment_button'),
|
||||
onPressed: _isLoading ? null : _handleAddMoreEnrollment,
|
||||
icon: const Icon(Icons.add_circle_outline, size: 16),
|
||||
label: const Text(
|
||||
'Add More Enrollment',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
SizedBox(
|
||||
width: 130,
|
||||
child: PrimaryButton(
|
||||
label: 'SAVE',
|
||||
isLoading: _isLoading,
|
||||
onPressed: _handleSave,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: PrimaryButton(
|
||||
label: 'SAVE',
|
||||
isLoading: _isLoading,
|
||||
onPressed: _handleSave,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -607,9 +607,10 @@ class _EditStudentModalState extends ConsumerState<EditStudentModal> {
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
ErrorBanner(message: _errorMessage!),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
],
|
||||
Builder(
|
||||
builder: (ctx) {
|
||||
|
||||
@@ -437,9 +437,9 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
|
||||
|
||||
_buildDivider(isDark),
|
||||
|
||||
// 8. Items per Page
|
||||
// 8. Students per Page
|
||||
_buildFilterSection(
|
||||
label: 'Items per Page',
|
||||
label: 'Students per Page',
|
||||
hasActiveFilter: _pendingLimit != 20,
|
||||
onReset: () => setState(() => _pendingLimit = 20),
|
||||
child: _buildFilterChipGroup(
|
||||
|
||||
@@ -218,7 +218,7 @@ class _ExtensionNameFieldState extends State<ExtensionNameField> {
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemCount: options.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (ctx, index) {
|
||||
final opt = options.elementAt(index);
|
||||
final isNa = opt == 'N/A';
|
||||
@@ -259,43 +259,47 @@ class DobInputFormatter extends TextInputFormatter {
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
String text = newValue.text.trim();
|
||||
final isoMatch = RegExp(r'^(\d{4})[\/\-\.](\d{1,2})[\/\-\.](\d{1,2})$').firstMatch(text);
|
||||
if (isoMatch != null) {
|
||||
final yyyy = isoMatch.group(1)!;
|
||||
final mm = isoMatch.group(2)!.padLeft(2, '0');
|
||||
final dd = isoMatch.group(3)!.padLeft(2, '0');
|
||||
text = '$mm$dd$yyyy';
|
||||
} else {
|
||||
final sepMatch = RegExp(r'^(\d{1,2})[\/\-\.](\d{1,2})[\/\-\.](\d{2,4})$').firstMatch(text);
|
||||
if (sepMatch != null) {
|
||||
int m = int.parse(sepMatch.group(1)!);
|
||||
int d = int.parse(sepMatch.group(2)!);
|
||||
int y = int.parse(sepMatch.group(3)!);
|
||||
if (y < 100) y += (y <= 30 ? 2000 : 1900);
|
||||
if (m > 12 && d <= 12) {
|
||||
final tmp = m;
|
||||
m = d;
|
||||
d = tmp;
|
||||
}
|
||||
final mm = m.toString().padLeft(2, '0');
|
||||
final dd = d.toString().padLeft(2, '0');
|
||||
text = '$mm$dd$y';
|
||||
if (newValue.text.isEmpty) {
|
||||
return newValue;
|
||||
}
|
||||
|
||||
// Handle backspace / deletion
|
||||
if (newValue.text.length < oldValue.text.length) {
|
||||
// If user hit backspace on a hyphen, step back one more character
|
||||
if (oldValue.selection.baseOffset > 1 &&
|
||||
oldValue.text[oldValue.selection.baseOffset - 1] == '-' &&
|
||||
newValue.selection.baseOffset == oldValue.selection.baseOffset - 1) {
|
||||
final cursor = oldValue.selection.baseOffset - 1;
|
||||
final digitsBefore = oldValue.text.substring(0, cursor).replaceAll(RegExp(r'[^\d]'), '');
|
||||
final truncatedDigitsBefore = digitsBefore.isNotEmpty ? digitsBefore.substring(0, digitsBefore.length - 1) : '';
|
||||
final digitsAfter = oldValue.text.substring(cursor + 1).replaceAll(RegExp(r'[^\d]'), '');
|
||||
final newDigits = truncatedDigitsBefore + digitsAfter;
|
||||
return _formatDigits(newDigits, cursor - 1);
|
||||
}
|
||||
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
|
||||
return _formatDigits(digits, newValue.selection.baseOffset);
|
||||
}
|
||||
|
||||
final digits = text.replaceAll(RegExp(r'[^\d]'), '');
|
||||
final d = digits.length > 8 ? digits.substring(0, 8) : digits;
|
||||
final digits = newValue.text.replaceAll(RegExp(r'[^\d]'), '');
|
||||
final cleanDigits = digits.length > 8 ? digits.substring(0, 8) : digits;
|
||||
return _formatDigits(cleanDigits, null);
|
||||
}
|
||||
|
||||
static TextEditingValue _formatDigits(String digits, int? desiredOffset) {
|
||||
final buf = StringBuffer();
|
||||
for (int i = 0; i < d.length; i++) {
|
||||
buf.write(d[i]);
|
||||
if (i == 1 || i == 3) buf.write('-');
|
||||
for (int i = 0; i < digits.length; i++) {
|
||||
if (i == 2 || i == 4) {
|
||||
buf.write('-');
|
||||
}
|
||||
buf.write(digits[i]);
|
||||
}
|
||||
final result = buf.toString();
|
||||
final text = buf.toString();
|
||||
final offset = desiredOffset != null
|
||||
? desiredOffset.clamp(0, text.length)
|
||||
: text.length;
|
||||
return TextEditingValue(
|
||||
text: result,
|
||||
selection: TextSelection.collapsed(offset: result.length),
|
||||
text: text,
|
||||
selection: TextSelection.collapsed(offset: offset),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user