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:
1 parent
5952de21b2
commit
f30faeecff
5 files changed
+128
-80
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user