feat(ui): update recent activities, student modals, documents filters, and settings theme button

This commit is contained in:
ssbg04 committed 2026-08-18 23:13:08 +08:00
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),
);
}
}