diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 4107d2d..4f29cc3 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -943,6 +943,10 @@ Files backup/restore - mirror mode or zip --> - [DONE] remove the "Student Directory" and copy the design of top header tab on attached picture - Removed "Students Directory" page title heading and unified the header into a floating card (`height: 52`, `margin: horizontal 16/24, vertical 8`, rounded 12 with border and subtle shadow). - Left side contains segmented tab switcher (`Enrolled` & `All Students`), right side contains search button, filter button with badge, elevated `Add Student` primary CTA button, and bulk import button. +- [DONE] students tab make the filter not the same for enrolled and all, in the enrolled remove the status and school year, now in all tab remove the enrolled status + - Enrolled tab: Filter dialog hides both "School Year" and "Status" sections (locked to current active school year + Enrolled status). Resetting filters clears grade/section/4Ps/sorting/limit without unsetting the enrolled school year or status. + - All Students tab: Filter dialog displays "School Year" dropdown and "Status" filter chips with `'Enrolled'` removed (options: `'All Status'`, `'Graduated'`, `'Transferred'`, `'Dropped'`, `'Inactive'`). + - Added `studentViewTabProvider` and updated `studentActiveFilterCountProvider` so the filter badge count accurately counts active user overrides per tab. # USERS SCREEN - [DONE] use tabs for filter of all, admin, teacher, inactive -> copy the design on attached picture diff --git a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart index 4927a7c..ec96dd7 100644 --- a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart +++ b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart @@ -757,7 +757,11 @@ class _AndroidBottomNavLayoutState extends ConsumerState message: 'Filter Students', child: IconButton( onPressed: () => - StudentFilterDialog.show(context, query: query), + StudentFilterDialog.show( + context, + query: query, + isEnrolledTab: ref.read(studentViewTabProvider) == StudentViewTab.enrolled, + ), icon: Badge( isLabelVisible: activeCount > 0, label: Text(activeCount.toString()), diff --git a/frontend/lib/ui/providers/student_provider.dart b/frontend/lib/ui/providers/student_provider.dart index 110c27b..2d6f0e1 100644 --- a/frontend/lib/ui/providers/student_provider.dart +++ b/frontend/lib/ui/providers/student_provider.dart @@ -206,8 +206,29 @@ class StudentQueryNotifier extends Notifier { final studentMultiSelectProvider = StateProvider((ref) => false); final studentSelectedIdsProvider = StateProvider>((ref) => []); +// ============================================================ +// View Tab Provider (Enrolled vs All Students) +// ============================================================ +enum StudentViewTab { enrolled, all } + +final studentViewTabProvider = + StateProvider((ref) => StudentViewTab.enrolled); + final studentActiveFilterCountProvider = Provider((ref) { final query = ref.watch(studentQueryProvider); + final tab = ref.watch(studentViewTabProvider); + + if (tab == StudentViewTab.enrolled) { + // In enrolled tab, status and schoolYear are inherent to the tab, not user overrides + return [ + query.gradeLevel.isNotEmpty, + query.section.isNotEmpty, + query.is4Ps.isNotEmpty, + query.sortBy.isNotEmpty, + query.limit != 20, + ].where((v) => v).length; + } + return [ query.gradeLevel.isNotEmpty, query.section.isNotEmpty, diff --git a/frontend/lib/ui/screens/students/students_screen.dart b/frontend/lib/ui/screens/students/students_screen.dart index e886825..3b6c826 100644 --- a/frontend/lib/ui/screens/students/students_screen.dart +++ b/frontend/lib/ui/screens/students/students_screen.dart @@ -29,8 +29,6 @@ import '../../shared/dialogs/success_dialog.dart'; import '../../shared/widgets/app_pagination.dart'; import '../../shared/widgets/app_error_state.dart'; -enum _StudentViewTab { enrolled, all } - class StudentsScreen extends ConsumerStatefulWidget { final String userRole; const StudentsScreen({super.key, this.userRole = 'teacher'}); @@ -53,7 +51,7 @@ class _StudentsScreenState extends ConsumerState { ProviderSubscription? _tabListener; /// Whether the screen is showing enrolled-only (current year) or all students. - _StudentViewTab _viewTab = _StudentViewTab.enrolled; + StudentViewTab _viewTab = StudentViewTab.enrolled; final ScrollController _scrollController = ScrollController(); bool _showTopFade = false; @@ -95,10 +93,11 @@ class _StudentsScreenState extends ConsumerState { final initialQuery = ref.read(studentQueryProvider).search; if (initialQuery.isNotEmpty) { _searchController.text = initialQuery; - if (mounted) setState(() => _viewTab = _StudentViewTab.all); + if (mounted) setState(() => _viewTab = StudentViewTab.all); + ref.read(studentViewTabProvider.notifier).state = StudentViewTab.all; } else { // Apply the default enrolled-tab filter on first load - _applyViewTab(_StudentViewTab.enrolled); + _applyViewTab(StudentViewTab.enrolled); } if (ref.read(activeTabProvider) == 'Students') { _shortcutFocusNode.requestFocus(); @@ -112,7 +111,8 @@ class _StudentsScreenState extends ConsumerState { if (_searchController.text.isNotEmpty) _searchController.clear(); ref.read(studentQueryProvider.notifier).reset(); // Reset view tab to enrolled for next visit - if (mounted) setState(() => _viewTab = _StudentViewTab.enrolled); + if (mounted) setState(() => _viewTab = StudentViewTab.enrolled); + ref.read(studentViewTabProvider.notifier).state = StudentViewTab.enrolled; if (_showMultiSelect || _selectedStudentIds.isNotEmpty) { _updateSelection(() { _showMultiSelect = false; @@ -137,7 +137,8 @@ class _StudentsScreenState extends ConsumerState { if (mounted) setState(() {}); } if (currentQuery.isNotEmpty) { - if (mounted) setState(() => _viewTab = _StudentViewTab.all); + if (mounted) setState(() => _viewTab = StudentViewTab.all); + ref.read(studentViewTabProvider.notifier).state = StudentViewTab.all; } else if (mounted) { // Re-apply the enrolled filter when returning to this tab _applyViewTab(_viewTab); @@ -167,8 +168,9 @@ class _StudentsScreenState extends ConsumerState { void _onSearchChanged() { _debounce?.cancel(); final text = _searchController.text; - if (text.trim().isNotEmpty && _viewTab != _StudentViewTab.all) { - if (mounted) setState(() => _viewTab = _StudentViewTab.all); + if (text.trim().isNotEmpty && _viewTab != StudentViewTab.all) { + if (mounted) setState(() => _viewTab = StudentViewTab.all); + ref.read(studentViewTabProvider.notifier).state = StudentViewTab.all; ref.read(studentQueryProvider.notifier).setFilters( status: '', schoolYear: '', @@ -184,11 +186,12 @@ class _StudentsScreenState extends ConsumerState { // ---------------------------------------------------------------- // VIEW TAB: Enrolled (current year) vs All Students // ---------------------------------------------------------------- - Future _applyViewTab(_StudentViewTab tab) async { + Future _applyViewTab(StudentViewTab tab) async { if (!mounted) return; setState(() => _viewTab = tab); + ref.read(studentViewTabProvider.notifier).state = tab; - if (tab == _StudentViewTab.enrolled) { + if (tab == StudentViewTab.enrolled) { // Clear any search when switching back to the enrolled tab if (_searchController.text.isNotEmpty) { _searchController.clear(); @@ -228,8 +231,9 @@ class _StudentsScreenState extends ConsumerState { void _onSearchSubmitted(String query) { _debounce?.cancel(); - if (query.trim().isNotEmpty && _viewTab != _StudentViewTab.all) { - if (mounted) setState(() => _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( status: '', schoolYear: '', @@ -1015,15 +1019,7 @@ class _StudentsScreenState extends ConsumerState { Widget build(BuildContext context) { final query = ref.watch(studentQueryProvider); final pageAsync = ref.watch(studentPageProvider); - final activeCount = [ - query.schoolYear.isNotEmpty, - query.gradeLevel.isNotEmpty, - query.section.isNotEmpty, - query.status.isNotEmpty, - query.is4Ps.isNotEmpty, - query.sortBy.isNotEmpty, - query.limit != 20, - ].where((v) => v).length; + final activeCount = ref.watch(studentActiveFilterCountProvider); ref.listen(studentMultiSelectProvider, (previous, next) { if (_showMultiSelect != next) { @@ -1046,7 +1042,7 @@ class _StudentsScreenState extends ConsumerState { ref.listen>>(academicYearsListProvider, (prev, next) { if (!mounted) return; final years = next.asData?.value; - if (years != null && years.isNotEmpty && _viewTab == _StudentViewTab.enrolled) { + if (years != null && years.isNotEmpty && _viewTab == StudentViewTab.enrolled) { final currentQuery = ref.read(studentQueryProvider); if (currentQuery.search.isEmpty) { final active = years.firstWhere( @@ -1380,8 +1376,11 @@ class _StudentsScreenState extends ConsumerState { // Filter IconButton with Badge IconButton( - onPressed: () => - StudentFilterDialog.show(context, query: query), + onPressed: () => StudentFilterDialog.show( + context, + query: query, + isEnrolledTab: _viewTab == StudentViewTab.enrolled, + ), padding: const EdgeInsets.all(8), constraints: const BoxConstraints(minWidth: 36, minHeight: 36), icon: Badge( @@ -1492,7 +1491,7 @@ class _StudentsScreenState extends ConsumerState { } final yearLabel = activeYear != null ? ' ยท ${activeYear.yearRange}' : ''; - final isEnrolled = _viewTab == _StudentViewTab.enrolled; + final isEnrolled = _viewTab == StudentViewTab.enrolled; return Container( height: 36, @@ -1516,7 +1515,7 @@ class _StudentsScreenState extends ConsumerState { isDark: isDark, isMobile: isMobile, isMobileOrAndroid: isMobileOrAndroid, - onTap: () => _applyViewTab(_StudentViewTab.enrolled), + onTap: () => _applyViewTab(StudentViewTab.enrolled), ), const SizedBox(width: 2), _buildSegmentedTabItem( @@ -1527,7 +1526,7 @@ class _StudentsScreenState extends ConsumerState { isDark: isDark, isMobile: isMobile, isMobileOrAndroid: isMobileOrAndroid, - onTap: () => _applyViewTab(_StudentViewTab.all), + onTap: () => _applyViewTab(StudentViewTab.all), ), ], ), 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 14bd106..8c8916d 100644 --- a/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart +++ b/frontend/lib/ui/screens/students/widgets/student_filter_dialog.dart @@ -7,15 +7,18 @@ import '../../../providers/student_provider.dart'; class StudentFilterDialog extends ConsumerStatefulWidget { final StudentQueryParams initialQuery; + final bool isEnrolledTab; const StudentFilterDialog({ super.key, required this.initialQuery, + this.isEnrolledTab = false, }); static Future show( BuildContext context, { required StudentQueryParams query, + bool isEnrolledTab = false, }) { return showDialog( context: context, @@ -31,7 +34,10 @@ class StudentFilterDialog extends ConsumerStatefulWidget { maxWidth: 460, maxHeight: MediaQuery.of(context).size.height * 0.88, ), - child: StudentFilterDialog(initialQuery: query), + child: StudentFilterDialog( + initialQuery: query, + isEnrolledTab: isEnrolledTab, + ), ), ), ); @@ -57,9 +63,8 @@ class _StudentFilterDialogState extends ConsumerState { 'Completed', 'Pending', ]; - static const _statusItems = [ + static const _allTabStatusItems = [ 'All Status', - 'Enrolled', 'Graduated', 'Transferred', 'Dropped', @@ -77,6 +82,9 @@ 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; @@ -226,7 +234,9 @@ class _StudentFilterDialogState extends ConsumerState { ), const SizedBox(width: 10), Text( - 'Filter Students', + widget.isEnrolledTab + ? 'Filter Enrolled Students' + : 'Filter All Students', style: TextStyle( fontSize: 17, fontWeight: FontWeight.w700, @@ -266,32 +276,33 @@ class _StudentFilterDialogState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // 1. School Year (Cascades to Grade & Section) - _buildFilterSection( - label: 'School Year', - hasActiveFilter: - _pendingSchoolYear != 'All School Years', - onReset: () => setState(() { - _pendingSchoolYear = 'All School Years'; - _pendingGradeLevel = 'All Grades'; - _pendingSection = 'All Sections'; - }), - child: _buildCleanDropdown( - value: _pendingSchoolYear, - items: syItems, - onChanged: (v) { - if (v != null) { - setState(() { - _pendingSchoolYear = v; - _pendingGradeLevel = 'All Grades'; - _pendingSection = 'All Sections'; - }); - } - }, + // 1. School Year (Cascades to Grade & Section) - only shown for All Students + if (!widget.isEnrolledTab) ...[ + _buildFilterSection( + label: 'School Year', + hasActiveFilter: + _pendingSchoolYear != 'All School Years', + onReset: () => setState(() { + _pendingSchoolYear = 'All School Years'; + _pendingGradeLevel = 'All Grades'; + _pendingSection = 'All Sections'; + }), + child: _buildCleanDropdown( + value: _pendingSchoolYear, + items: syItems, + onChanged: (v) { + if (v != null) { + setState(() { + _pendingSchoolYear = v; + _pendingGradeLevel = 'All Grades'; + _pendingSection = 'All Sections'; + }); + } + }, + ), ), - ), - - _buildDivider(isDark), + _buildDivider(isDark), + ], // 2. Grade Level (Cascades to Section) _buildFilterSection( @@ -342,21 +353,22 @@ class _StudentFilterDialogState extends ConsumerState { _buildDivider(isDark), - // 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), + // 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), + ), ), - ), - - _buildDivider(isDark), + _buildDivider(isDark), + ], // 5. 4Ps Beneficiary _buildFilterSection( @@ -460,10 +472,15 @@ class _StudentFilterDialogState extends ConsumerState { ), onPressed: () { setState(() { - _pendingSchoolYear = 'All School Years'; + _pendingSchoolYear = widget.isEnrolledTab + ? (widget.initialQuery.schoolYear.isEmpty + ? 'All School Years' + : widget.initialQuery.schoolYear) + : 'All School Years'; _pendingGradeLevel = 'All Grades'; _pendingSection = 'All Sections'; - _pendingStatus = 'All Status'; + _pendingStatus = + widget.isEnrolledTab ? 'Enrolled' : 'All Status'; _pending4Ps = 'All'; _pendingSortBy = ''; _pendingSortOrder = ''; @@ -499,19 +516,22 @@ class _StudentFilterDialogState extends ConsumerState { if (_pending4Ps == 'No') is4psVal = 'false'; ref.read(studentQueryProvider.notifier).setFilters( - schoolYear: - _pendingSchoolYear == 'All School Years' + schoolYear: widget.isEnrolledTab + ? widget.initialQuery.schoolYear + : (_pendingSchoolYear == 'All School Years' ? '' - : _pendingSchoolYear, + : _pendingSchoolYear), gradeLevel: _pendingGradeLevel == 'All Grades' ? '' : _pendingGradeLevel, section: _pendingSection == 'All Sections' ? '' : _pendingSection, - status: _pendingStatus == 'All Status' - ? '' - : _pendingStatus, + status: widget.isEnrolledTab + ? 'Enrolled' + : (_pendingStatus == 'All Status' + ? '' + : _pendingStatus), is4Ps: is4psVal, sortBy: _pendingSortBy, sortOrder: _pendingSortOrder,