feat(students): update student screen tabs, profile actions, and pagination styling

This commit is contained in:
ssbg04 committed 2026-10-11 21:33:03 +08:00
1 parent 45a53183ee
commit c73a927c0c
6 files changed
+545 -188

No files matched your search

+11 -9
View File
@@ -1113,16 +1113,18 @@ Files backup/restore - mirror mode or zip -->
# STUDENT SCREEN
- enrolled tab:
- when no students it has clear filter button, remove it, then in the student list table, add add button at the bottom to add student and nagivation for all students tab to enroll a students]
- when I update the student status to inactive, dropped, transferred, graduated stays on this tab, add in filter for inactive, dropped, transferred, graducation
- rename the tab as the active academic year
- [x] when no students it has clear filter button, remove it, then in the student list table, add add button at the bottom to add student and nagivation for all students tab to enroll a students
- [x] when I update the student status to inactive, dropped, transferred, graduated stays on this tab, add in filter for inactive, dropped, transferred, graduation
- [x] rename the tab as the active academic year
- student profile:
- prevent or remove the buttons for not enrolled students upload docs
- history add load more and load less, it only show last 10 entries, and resets to default when not active tab
- remove the verified badge for files on records
- add button to navigate to document screen inside of their folder
- pagination on windows app
- use white font for dark theme and black for white theme for previous, next, and pagination numbers and add background like the numbers
- [x] prevent or remove the buttons for not enrolled students upload docs
- [x] history add load more and load less, it only show last 10 entries, and resets to default when not active tab
- [x] remove the verified badge for files on records
- [x] add button to navigate to document screen inside of their folder
# Pagination on windows app
- [x] use white font for dark theme and black for white theme for previous, next, and pagination numbers and add background like the numbers
# ARCHIVE SCREEN
- [x] Removed Recycle Bin, delete, restore, upload new version, and copy buttons from the Archive Screen (including folder context menus, document context menus, document preview modal, and bulk operations bar).
@@ -229,10 +229,11 @@ final studentActiveFilterCountProvider = Provider<int>((ref) {
final tab = ref.watch(studentViewTabProvider);
if (tab == StudentViewTab.enrolled) {
// In enrolled tab, status and schoolYear are inherent to the tab, not user overrides
// In enrolled tab, status is a user filter; schoolYear is inherent to the tab
return [
query.gradeLevel.isNotEmpty,
query.section.isNotEmpty,
query.status.isNotEmpty,
query.is4Ps.isNotEmpty,
query.sortBy.isNotEmpty,
query.limit != 20,
@@ -12,6 +12,8 @@ import '../../../../domain/repositories/document_repository.dart'
import '../../../../domain/entities/document_requirement_model.dart';
import '../../../providers/student_provider.dart';
import '../../../providers/document_provider.dart';
import '../../../providers/navigation_provider.dart';
import '../../../providers/archives_provider.dart';
import 'document_preview_modal.dart';
import 'upload_ocr_modal.dart';
import 'print_queue_modal.dart';
@@ -368,6 +370,51 @@ class StudentProfileModalBody extends ConsumerStatefulWidget {
class _StudentProfileModalBodyState
extends ConsumerState<StudentProfileModalBody> {
_StudentProfileTab _selectedTab = _StudentProfileTab.overview;
int _historyLimit = 10;
void _navigateToDocumentsFolder(BuildContext context, StudentModel student) {
Navigator.of(context).pop();
if (student.status == 'Enrolled') {
ref.read(activeTabProvider.notifier).setTab('Documents');
Future.microtask(() {
ref.read(openedFolderProvider.notifier).setFolder(
OpenedFolderData(
id: student.id,
name: student.listDisplayName,
lrn: student.lrn,
),
);
});
} else {
if (widget.userRole.toLowerCase() == 'teacher') {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Inactive Record'),
content: Text(
'${student.listDisplayName} is currently marked as ${student.status}. Archived records are only accessible by Administrators.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('OK'),
),
],
),
);
} else {
ref.read(activeTabProvider.notifier).setTab('Archives');
Future.microtask(() {
ref.read(openedArchiveFolderProvider.notifier).setFolder(
OpenedArchiveFolderData(
id: student.id,
name: student.listDisplayName,
),
);
});
}
}
}
Future<void> _handleAddToPrintQueue(DocumentModel doc) async {
try {
@@ -513,7 +560,10 @@ class _StudentProfileModalBodyState
icon: Icons.person_outline_rounded,
isSelected: _selectedTab == _StudentProfileTab.overview,
isDark: isDark,
onTap: () => setState(() => _selectedTab = _StudentProfileTab.overview),
onTap: () => setState(() {
_selectedTab = _StudentProfileTab.overview;
_historyLimit = 10;
}),
),
),
const SizedBox(width: 4),
@@ -524,7 +574,10 @@ class _StudentProfileModalBodyState
count: docCount,
isSelected: _selectedTab == _StudentProfileTab.documents,
isDark: isDark,
onTap: () => setState(() => _selectedTab = _StudentProfileTab.documents),
onTap: () => setState(() {
_selectedTab = _StudentProfileTab.documents;
_historyLimit = 10;
}),
),
),
const SizedBox(width: 4),
@@ -791,11 +844,13 @@ class _StudentProfileModalBodyState
_buildRequirementsStatus(
context,
missing,
onUploadRequirement: (req) => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
),
onUploadRequirement: student.status == 'Enrolled'
? (req) => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
)
: null,
)
else
Container(
@@ -846,6 +901,21 @@ class _StudentProfileModalBodyState
spacing: 6,
runSpacing: 4,
children: [
OutlinedButton.icon(
onPressed: () => _navigateToDocumentsFolder(context, student),
icon: const Icon(Icons.folder_open_rounded, size: 14),
label: Text(
student.status == 'Enrolled' ? 'Open Folder' : 'Open Archive',
style: const TextStyle(fontSize: 11.5),
),
style: OutlinedButton.styleFrom(
visualDensity: VisualDensity.compact,
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
),
OutlinedButton.icon(
onPressed: () => PrintQueueModal.show(context),
icon: const Icon(Icons.print_outlined, size: 14),
@@ -864,23 +934,24 @@ class _StudentProfileModalBodyState
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
),
ElevatedButton.icon(
onPressed: () => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
if (student.status == 'Enrolled')
ElevatedButton.icon(
onPressed: () => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
),
icon: const Icon(Icons.upload_file_rounded, size: 14),
label:
const Text('Upload File', style: TextStyle(fontSize: 11.5)),
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.compact,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
),
icon: const Icon(Icons.upload_file_rounded, size: 14),
label:
const Text('Upload File', style: TextStyle(fontSize: 11.5)),
style: ElevatedButton.styleFrom(
visualDensity: VisualDensity.compact,
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
),
),
],
);
@@ -962,11 +1033,13 @@ class _StudentProfileModalBodyState
_buildRequirementsStatus(
context,
missing,
onUploadRequirement: (req) => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
),
onUploadRequirement: student.status == 'Enrolled'
? (req) => UploadOcrModal.show(
context,
prefilledStudentId: student.id,
prefilledLrn: student.lrn,
)
: null,
)
else
const Center(
@@ -1159,11 +1232,6 @@ class _StudentProfileModalBodyState
) {
final bool isMobile = MediaQuery.of(context).size.width < 500;
final isArchived = doc.status.toLowerCase() == 'archived';
final statusColor = isArchived ? Colors.grey : AppColors.success;
final statusIcon = isArchived
? Icons.archive_outlined
: Icons.check_circle_rounded;
final statusLabel = isArchived ? 'Archived' : 'Verified';
return Container(
margin: const EdgeInsets.only(bottom: 8),
@@ -1208,32 +1276,34 @@ class _StudentProfileModalBodyState
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(4),
border:
Border.all(color: statusColor.withValues(alpha: 0.3)),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 9, color: statusColor),
const SizedBox(width: 3),
Text(
statusLabel,
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.bold,
color: statusColor,
if (isArchived) ...[
const SizedBox(width: 6),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
decoration: BoxDecoration(
color: Colors.grey.withValues(alpha: isDark ? 0.2 : 0.1),
borderRadius: BorderRadius.circular(4),
border:
Border.all(color: Colors.grey.withValues(alpha: 0.3)),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.archive_outlined, size: 9, color: Colors.grey),
SizedBox(width: 3),
Text(
'Archived',
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
),
],
],
),
),
),
],
],
),
const SizedBox(height: 3),
@@ -1472,10 +1542,59 @@ class _StudentProfileModalBodyState
);
}
final displayed = activities.take(_historyLimit).toList();
final hasMore = activities.length > _historyLimit;
final canLoadLess = _historyLimit > 10;
return Column(
children: activities
.map((a) => _buildActivityItemTile(context, a, isDark))
.toList(),
children: [
...displayed
.map((a) => _buildActivityItemTile(context, a, isDark)),
if (hasMore || canLoadLess) ...[
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (hasMore)
OutlinedButton.icon(
onPressed: () => setState(() {
_historyLimit += 10;
}),
icon: const Icon(Icons.expand_more_rounded, size: 16),
label: Text(
'Load More (${activities.length - _historyLimit} left)',
style: const TextStyle(fontSize: 12),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
padding: const EdgeInsets.symmetric(
horizontal: 14, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
),
if (hasMore && canLoadLess) const SizedBox(width: 10),
if (canLoadLess)
TextButton.icon(
onPressed: () => setState(() {
_historyLimit = 10;
}),
icon: const Icon(Icons.expand_less_rounded, size: 16),
label: const Text('Load Less',
style: TextStyle(fontSize: 12)),
style: TextButton.styleFrom(
foregroundColor: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
),
],
),
],
],
);
},
),
@@ -1701,46 +1820,84 @@ class _StudentProfileModalBodyState
_buildStatusBadge(student.status),
],
),
if (widget.onEditDetails != null) ...[
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: InkWell(
onTap: widget.onEditDetails,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 7),
decoration: BoxDecoration(
color: AppColors.primaryGreen
.withValues(alpha: isDark ? 0.20 : 0.10),
const SizedBox(height: 10),
Row(
children: [
if (widget.onEditDetails != null)
Expanded(
child: InkWell(
onTap: widget.onEditDetails,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.primaryGreen
.withValues(alpha: isDark ? 0.45 : 0.35),
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.edit_rounded,
size: 13, color: AppColors.primaryGreen),
const SizedBox(width: 6),
const Text(
'Edit Details',
style: TextStyle(
color: AppColors.primaryGreen,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 7),
decoration: BoxDecoration(
color: AppColors.primaryGreen
.withValues(alpha: isDark ? 0.20 : 0.10),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppColors.primaryGreen
.withValues(alpha: isDark ? 0.45 : 0.35),
width: 1,
),
),
],
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.edit_rounded,
size: 13, color: AppColors.primaryGreen),
SizedBox(width: 6),
Text(
'Edit Details',
style: TextStyle(
color: AppColors.primaryGreen,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
],
),
),
),
),
if (widget.onEditDetails != null) const SizedBox(width: 8),
Expanded(
child: InkWell(
onTap: () => _navigateToDocumentsFolder(context, student),
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 7),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface2 : const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
width: 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.folder_open_rounded,
size: 14,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary),
const SizedBox(width: 6),
Text(
student.status == 'Enrolled' ? 'View Folder' : 'View Archive',
style: TextStyle(
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
fontSize: 12,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
),
),
],
),
),
),
),
),
],
],
),
] else ...[
Row(
children: [
@@ -1839,6 +1996,45 @@ class _StudentProfileModalBodyState
),
),
],
const SizedBox(height: 6),
Tooltip(
message: student.status == 'Enrolled'
? 'Open Folder in Documents'
: 'Open Folder in Archives',
child: InkWell(
onTap: () => _navigateToDocumentsFolder(context, student),
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface2 : const Color(0xFFF1F5F9),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.folder_open_rounded,
size: 12,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary),
const SizedBox(width: 4),
Text(
student.status == 'Enrolled' ? 'View Folder' : 'View Archive',
style: TextStyle(
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
),
],
),
),
),
),
],
),
],
@@ -194,9 +194,9 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
(y) => y.status.toLowerCase() == 'active',
orElse: () => years.isNotEmpty ? years.first : AcademicYearModel(id: 0, yearRange: '', status: ''),
);
// Apply enrolled status + current school year
// Apply active school year without hardcoding status so all statuses in this SY stay visible
ref.read(studentQueryProvider.notifier).setFilters(
status: 'Enrolled',
status: '',
schoolYear: active.yearRange.isNotEmpty ? active.yearRange : '',
gradeLevel: '',
section: '',
@@ -217,7 +217,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
void _onSearchSubmitted(String query) {
_debounce?.cancel();
if (widget.userRole != 'teacher' && query.trim().isNotEmpty && _viewTab != StudentViewTab.all) {
if (query.trim().isNotEmpty && _viewTab != StudentViewTab.all) {
if (mounted) setState(() => _viewTab = StudentViewTab.all);
ref.read(studentViewTabProvider.notifier).state = StudentViewTab.all;
ref.read(studentQueryProvider.notifier).setFilters(
@@ -1065,7 +1065,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
);
if (active.yearRange.isNotEmpty && currentQuery.schoolYear != active.yearRange) {
ref.read(studentQueryProvider.notifier).setFilters(
status: 'Enrolled',
status: currentQuery.status,
schoolYear: active.yearRange,
);
}
@@ -1549,7 +1549,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
));
}
if (_viewTab == StudentViewTab.all && query.status.isNotEmpty) {
if (query.status.isNotEmpty) {
chips.add(_buildFilterChip(
label: 'Status: ${query.status}',
onDeleted: () => ref.read(studentQueryProvider.notifier).setStatus(''),
@@ -1676,7 +1676,9 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
} catch (_) {
activeYear = years.isNotEmpty ? years.first : null;
}
final yearLabel = activeYear != null ? ' · ${activeYear.yearRange}' : '';
final tabLabel = activeYear != null && activeYear.yearRange.isNotEmpty
? (isMobile ? activeYear.yearRange : 'S.Y. ${activeYear.yearRange}')
: (isMobile ? 'Active S.Y.' : 'Active School Year');
final isEnrolled = _viewTab == StudentViewTab.enrolled;
@@ -1697,26 +1699,26 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_buildSegmentedTabItem(
icon: Icons.school_outlined,
activeIcon: Icons.school_rounded,
label: isMobile ? 'Enrolled' : 'Enrolled$yearLabel',
isSelected: widget.userRole == 'teacher' ? true : isEnrolled,
label: tabLabel,
isSelected: isEnrolled,
isDark: isDark,
isMobile: isMobile,
isMobileOrAndroid: isMobileOrAndroid,
onTap: widget.userRole == 'teacher' ? () {} : () => _applyViewTab(StudentViewTab.enrolled),
onTap: () => _applyViewTab(StudentViewTab.enrolled),
),
const SizedBox(width: 2),
_buildSegmentedTabItem(
icon: Icons.people_outline_rounded,
activeIcon: Icons.people_alt_rounded,
label: widget.userRole == 'teacher'
? (isMobile ? 'Assigned' : 'All Assigned')
: (isMobile ? 'All' : 'All Students'),
isSelected: !isEnrolled,
isDark: isDark,
isMobile: isMobile,
isMobileOrAndroid: isMobileOrAndroid,
onTap: () => _applyViewTab(StudentViewTab.all),
),
if (widget.userRole != 'teacher') ...[
const SizedBox(width: 2),
_buildSegmentedTabItem(
icon: Icons.people_outline_rounded,
activeIcon: Icons.people_alt_rounded,
label: isMobile ? 'All' : 'All Students',
isSelected: !isEnrolled,
isDark: isDark,
isMobile: isMobile,
isMobileOrAndroid: isMobileOrAndroid,
onTap: () => _applyViewTab(StudentViewTab.all),
),
],
],
),
);
@@ -1851,6 +1853,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
if (students.isEmpty) return _buildEmptyState(noSections: noSections);
final isDark = Theme.of(context).brightness == Brightness.dark;
final showBottomActions = _viewTab == StudentViewTab.enrolled && widget.userRole != 'teacher';
return RefreshIndicator(
color: AppColors.primaryGreen,
@@ -1868,11 +1871,14 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
top: isDesktop ? 10 : 14,
bottom: isDesktop ? 20 : (defaultTargetPlatform == TargetPlatform.android ? 8 : 16),
),
itemCount: students.length,
itemCount: students.length + (showBottomActions ? 1 : 0),
separatorBuilder: (ctx, index) => SizedBox(
height: isDesktop ? 8 : AppSizes.p12,
),
itemBuilder: (context, i) {
if (i == students.length) {
return _buildEnrolledBottomActions(isDark, isDesktop);
}
final s = students[i];
final isSelected = _selectedStudentIds.contains(s.id);
if (isDesktop) {
@@ -1885,6 +1891,50 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
);
}
Widget _buildEnrolledBottomActions(bool isDark, bool isDesktop) {
return Container(
margin: const EdgeInsets.only(top: 8, bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade200,
),
),
child: Wrap(
spacing: 12,
runSpacing: 8,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
OutlinedButton.icon(
onPressed: () => _openModal(),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('Add Student'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
ElevatedButton.icon(
onPressed: () => _applyViewTab(StudentViewTab.all),
icon: const Icon(Icons.people_alt_rounded, size: 18),
label: const Text('Enroll Existing Student (All Students)'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
],
),
);
}
Widget _buildDesktopCard(
BuildContext context,
StudentModel s,
@@ -2949,6 +2999,97 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
);
}
final query = ref.watch(studentQueryProvider);
final isEnrolledTab = _viewTab == StudentViewTab.enrolled;
if (isEnrolledTab) {
final years = ref.watch(academicYearsListProvider).asData?.value ?? [];
AcademicYearModel? activeYear;
try {
activeYear = years.firstWhere((y) => y.status.toLowerCase() == 'active');
} catch (_) {
activeYear = years.isNotEmpty ? years.first : null;
}
final syTitle = activeYear != null && activeYear.yearRange.isNotEmpty
? 'S.Y. ${activeYear.yearRange}'
: 'Active School Year';
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface2 : const Color(0xFFF1F5F9),
shape: BoxShape.circle,
),
child: Icon(
Icons.school_outlined,
size: 48,
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
'No Students in $syTitle',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
query.status.isNotEmpty || query.gradeLevel.isNotEmpty || query.section.isNotEmpty || query.search.isNotEmpty
? 'No students match your selected filters in $syTitle.'
: 'There are no students enrolled in $syTitle yet.',
textAlign: TextAlign.center,
style: TextStyle(
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
fontSize: 13,
),
),
if (widget.userRole != 'teacher') ...[
const SizedBox(height: 20),
Wrap(
spacing: 12,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
OutlinedButton.icon(
onPressed: () => _openModal(),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('Add Student'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
),
),
ElevatedButton.icon(
onPressed: () => _applyViewTab(StudentViewTab.all),
icon: const Icon(Icons.people_alt_rounded, size: 18),
label: const Text('Enroll Existing Student (All Students)'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
),
),
],
),
],
],
),
),
);
}
final hasActiveFilter = query.search.isNotEmpty ||
query.gradeLevel.isNotEmpty ||
query.section.isNotEmpty ||
@@ -87,12 +87,13 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
'Completed',
'Missing',
];
static const _allTabStatusItems = [
static const _statusItems = [
'All Status',
'Graduated',
'Transferred',
'Dropped',
'Enrolled',
'Inactive',
'Dropped',
'Transferred',
'Graduated',
];
static const _fourPsItems = ['All', 'Yes', 'No'];
static const _pageSizes = [10, 15, 20, 50, 100];
@@ -106,9 +107,6 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
_pendingGradeLevel = q.gradeLevel.isEmpty ? 'All Grades' : q.gradeLevel;
_pendingSection = q.section.isEmpty ? 'All Sections' : q.section;
_pendingStatus = q.status.isEmpty ? 'All Status' : q.status;
if (!widget.isEnrolledTab && _pendingStatus == 'Enrolled') {
_pendingStatus = 'All Status';
}
_pending4Ps =
q.is4Ps.isEmpty ? 'All' : (q.is4Ps == 'true' ? 'Yes' : 'No');
_pendingSortBy = q.sortBy;
@@ -398,22 +396,20 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
_buildDivider(isDark),
// 4. Status Filter - only shown for All Students, with 'Enrolled' removed
if (!widget.isEnrolledTab) ...[
_buildFilterSection(
label: 'Status',
hasActiveFilter: _pendingStatus != 'All Status',
onReset: () => setState(() {
_pendingStatus = 'All Status';
}),
child: _buildFilterChipGroup(
items: _allTabStatusItems,
selectedValue: _pendingStatus,
onSelected: (v) => setState(() => _pendingStatus = v),
),
// 4. Status Filter
_buildFilterSection(
label: 'Status',
hasActiveFilter: _pendingStatus != 'All Status',
onReset: () => setState(() {
_pendingStatus = 'All Status';
}),
child: _buildFilterChipGroup(
items: _statusItems,
selectedValue: _pendingStatus,
onSelected: (v) => setState(() => _pendingStatus = v),
),
_buildDivider(isDark),
],
),
_buildDivider(isDark),
// 5. 4Ps Beneficiary
_buildFilterSection(
@@ -524,8 +520,7 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
: 'All School Years';
_pendingGradeLevel = 'All Grades';
_pendingSection = 'All Sections';
_pendingStatus =
widget.isEnrolledTab ? 'Enrolled' : 'All Status';
_pendingStatus = 'All Status';
_pending4Ps = 'All';
_pendingSortBy = '';
_pendingSortOrder = '';
@@ -572,11 +567,9 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
section: _pendingSection == 'All Sections'
? ''
: _pendingSection,
status: widget.isEnrolledTab
? 'Enrolled'
: (_pendingStatus == 'All Status'
? ''
: _pendingStatus),
status: _pendingStatus == 'All Status'
? ''
: _pendingStatus,
is4Ps: is4psVal,
sortBy: _pendingSortBy,
sortOrder: _pendingSortOrder,
@@ -74,20 +74,32 @@ class AppPagination extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// "Previous" text button
InkWell(
onTap: canPrev ? () => onPageChanged(currentPage - 1) : null,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Text(
'Previous',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: canPrev
? (isDark ? AppColors.darkTextPrimary : AppColors.textPrimary)
: (isDark ? Colors.white24 : Colors.grey.shade400),
// "Previous" text button with background like numbers
Material(
color: Colors.transparent,
child: InkWell(
onTap: canPrev ? () => onPageChanged(currentPage - 1) : null,
borderRadius: BorderRadius.circular(8),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
),
),
child: Text(
'Previous',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: canPrev
? (isDark ? Colors.white : Colors.black)
: (isDark ? Colors.white24 : Colors.grey.shade400),
),
),
),
),
@@ -99,20 +111,32 @@ class AppPagination extends StatelessWidget {
const SizedBox(width: 8),
// "Next" text button
InkWell(
onTap: canNext ? () => onPageChanged(currentPage + 1) : null,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
child: Text(
'Next',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: canNext
? AppColors.primaryGreen
: (isDark ? Colors.white24 : Colors.grey.shade400),
// "Next" text button with background like numbers
Material(
color: Colors.transparent,
child: InkWell(
onTap: canNext ? () => onPageChanged(currentPage + 1) : null,
borderRadius: BorderRadius.circular(8),
child: Container(
height: 36,
padding: const EdgeInsets.symmetric(horizontal: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
),
),
child: Text(
'Next',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: canNext
? (isDark ? Colors.white : Colors.black)
: (isDark ? Colors.white24 : Colors.grey.shade400),
),
),
),
),
@@ -278,8 +302,8 @@ class AppPagination extends StatelessWidget {
color: isActive
? Colors.white
: (isDark
? AppColors.darkTextPrimary
: AppColors.textPrimary),
? Colors.white
: Colors.black),
),
),
),
@@ -386,8 +410,8 @@ class AppPagination extends StatelessWidget {
fontSize: 13,
fontWeight: FontWeight.bold,
color: isDark
? AppColors.darkTextPrimary
: AppColors.textPrimary,
? Colors.white
: Colors.black,
),
),
const SizedBox(width: 2),