feat(students): adapt filter dialog for enrolled vs all tabs

- Hide school year and status filters in student filter dialog on Enrolled tab\n- Remove 'Enrolled' status filter option on All Students tab\n- Sync studentViewTabProvider and studentActiveFilterCountProvider across desktop and mobile layouts
This commit is contained in:
ssbg04 committed 2026-10-01 23:49:48 +08:00
1 parent 5952de21b2
commit f30faeecff
5 files changed
+128 -80

No files matched your search

+4
View File
@@ -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
@@ -757,7 +757,11 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
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()),
@@ -206,8 +206,29 @@ class StudentQueryNotifier extends Notifier<StudentQueryParams> {
final studentMultiSelectProvider = StateProvider<bool>((ref) => false);
final studentSelectedIdsProvider = StateProvider<List<int>>((ref) => []);
// ============================================================
// View Tab Provider (Enrolled vs All Students)
// ============================================================
enum StudentViewTab { enrolled, all }
final studentViewTabProvider =
StateProvider<StudentViewTab>((ref) => StudentViewTab.enrolled);
final studentActiveFilterCountProvider = Provider<int>((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,
@@ -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<StudentsScreen> {
ProviderSubscription<String>? _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<StudentsScreen> {
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<StudentsScreen> {
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<StudentsScreen> {
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<StudentsScreen> {
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<StudentsScreen> {
// ----------------------------------------------------------------
// VIEW TAB: Enrolled (current year) vs All Students
// ----------------------------------------------------------------
Future<void> _applyViewTab(_StudentViewTab tab) async {
Future<void> _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<StudentsScreen> {
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<StudentsScreen> {
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<bool>(studentMultiSelectProvider, (previous, next) {
if (_showMultiSelect != next) {
@@ -1046,7 +1042,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
ref.listen<AsyncValue<List<AcademicYearModel>>>(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<StudentsScreen> {
// 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<StudentsScreen> {
}
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<StudentsScreen> {
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<StudentsScreen> {
isDark: isDark,
isMobile: isMobile,
isMobileOrAndroid: isMobileOrAndroid,
onTap: () => _applyViewTab(_StudentViewTab.all),
onTap: () => _applyViewTab(StudentViewTab.all),
),
],
),
@@ -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<void> 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<StudentFilterDialog> {
'Completed',
'Pending',
];
static const _statusItems = [
static const _allTabStatusItems = [
'All Status',
'Enrolled',
'Graduated',
'Transferred',
'Dropped',
@@ -77,6 +82,9 @@ 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;
@@ -226,7 +234,9 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
),
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<StudentFilterDialog> {
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<StudentFilterDialog> {
_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<StudentFilterDialog> {
),
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<StudentFilterDialog> {
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,