diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 4ae4541..86cde64 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -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). diff --git a/frontend/lib/ui/providers/student_provider.dart b/frontend/lib/ui/providers/student_provider.dart index ca39977..bc350d0 100644 --- a/frontend/lib/ui/providers/student_provider.dart +++ b/frontend/lib/ui/providers/student_provider.dart @@ -229,10 +229,11 @@ final studentActiveFilterCountProvider = Provider((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, diff --git a/frontend/lib/ui/screens/documents/widgets/student_profile_modal.dart b/frontend/lib/ui/screens/documents/widgets/student_profile_modal.dart index 3fc5991..3712d0e 100644 --- a/frontend/lib/ui/screens/documents/widgets/student_profile_modal.dart +++ b/frontend/lib/ui/screens/documents/widgets/student_profile_modal.dart @@ -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 { _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 _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, + ), + ), + ], + ), + ), + ), + ), ], ), ], diff --git a/frontend/lib/ui/screens/students/students_screen.dart b/frontend/lib/ui/screens/students/students_screen.dart index e2383ab..103d8c8 100644 --- a/frontend/lib/ui/screens/students/students_screen.dart +++ b/frontend/lib/ui/screens/students/students_screen.dart @@ -194,9 +194,9 @@ class _StudentsScreenState extends ConsumerState { (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 { 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 { ); 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 { )); } - 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 { } 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 { _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 { 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 { 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 { ); } + 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 { ); } 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 || diff --git a/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart b/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart index f85c1a0..c517ea9 100644 --- a/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart +++ b/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart @@ -87,12 +87,13 @@ class _StudentFilterDialogState extends ConsumerState { '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 { _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 { _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 { : '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 { section: _pendingSection == 'All Sections' ? '' : _pendingSection, - status: widget.isEnrolledTab - ? 'Enrolled' - : (_pendingStatus == 'All Status' - ? '' - : _pendingStatus), + status: _pendingStatus == 'All Status' + ? '' + : _pendingStatus, is4Ps: is4psVal, sortBy: _pendingSortBy, sortOrder: _pendingSortOrder, diff --git a/frontend/lib/ui/shared/widgets/app_pagination.dart b/frontend/lib/ui/shared/widgets/app_pagination.dart index dc37c7f..453aca4 100644 --- a/frontend/lib/ui/shared/widgets/app_pagination.dart +++ b/frontend/lib/ui/shared/widgets/app_pagination.dart @@ -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),