feat(students): add LRN filter modal and column title sort dropdowns for LRN, Name, Grade & Sec, and Doc Status

This commit is contained in:
ssbg04 committed 2026-08-01 02:45:04 +08:00
1 parent bf1e27426a
commit c10b87584b
4 files changed
+227 -29

No files matched your search

@@ -100,6 +100,9 @@ class StudentRepository {
String section = '',
String schoolYear = '',
String is4ps = '',
String lrn = '',
String sortBy = '',
String sortOrder = '',
}) async {
try {
final options = await _getAuthOptions();
@@ -112,6 +115,9 @@ class StudentRepository {
if (section.trim().isNotEmpty) 'section': section.trim(),
if (schoolYear.trim().isNotEmpty) 'schoolYear': schoolYear.trim(),
if (is4ps.trim().isNotEmpty) 'is4ps': is4ps.trim(),
if (lrn.trim().isNotEmpty) 'lrn': lrn.trim(),
if (sortBy.trim().isNotEmpty) 'sortBy': sortBy.trim(),
if (sortOrder.trim().isNotEmpty) 'sortOrder': sortOrder.trim(),
'page': page,
'limit': limit,
},
@@ -80,6 +80,9 @@ class StudentQueryParams {
final String section; // '' = All
final String schoolYear; // '' = All
final String is4Ps; // '' = All, 'true' = Yes, 'false' = No
final String lrn; // '' = All
final String sortBy; // '' = None, 'lrn', 'name', 'grade_section', 'doc_status'
final String sortOrder; // '' = None, 'asc', 'desc'
const StudentQueryParams({
this.search = '',
@@ -90,6 +93,9 @@ class StudentQueryParams {
this.section = '',
this.schoolYear = '',
this.is4Ps = '',
this.lrn = '',
this.sortBy = '',
this.sortOrder = '',
});
StudentQueryParams copyWith({
@@ -101,6 +107,9 @@ class StudentQueryParams {
String? section,
String? schoolYear,
String? is4Ps,
String? lrn,
String? sortBy,
String? sortOrder,
}) {
return StudentQueryParams(
search: search ?? this.search,
@@ -111,6 +120,9 @@ class StudentQueryParams {
section: section ?? this.section,
schoolYear: schoolYear ?? this.schoolYear,
is4Ps: is4Ps ?? this.is4Ps,
lrn: lrn ?? this.lrn,
sortBy: sortBy ?? this.sortBy,
sortOrder: sortOrder ?? this.sortOrder,
);
}
}
@@ -154,6 +166,14 @@ class StudentQueryNotifier extends Notifier<StudentQueryParams> {
void setIs4Ps(String is4Ps) => state = state.copyWith(is4Ps: is4Ps, page: 1);
void setLrn(String lrn) => state = state.copyWith(lrn: lrn, page: 1);
void setSort(String sortBy, String sortOrder) => state = state.copyWith(
sortBy: sortBy,
sortOrder: sortOrder,
page: 1,
);
void reset() => state = const StudentQueryParams();
}
@@ -181,6 +201,9 @@ final studentPageProvider = FutureProvider.autoDispose<StudentPage>((
section: query.section,
schoolYear: query.schoolYear,
is4ps: query.is4Ps,
lrn: query.lrn,
sortBy: query.sortBy,
sortOrder: query.sortOrder,
);
});
@@ -40,15 +40,15 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
bool _showMultiSelect = false;
bool _isDragSelecting = false;
ProviderSubscription<String>? _tabListener;
String _docStatusSort = '';
// Pending filter state (applied only when user taps "Apply now")
String _pendingSchoolYear = 'All School Years';
String _pendingGradeLevel = 'All Grades';
String _pendingSection = 'All Sections';
String _pendingStatus = 'All Status';
String _pending4Ps = 'All'; // 'All', 'Yes', 'No'
String _pendingLrn = '';
int _pendingLimit = 15;
final TextEditingController _pendingLrnCtrl = TextEditingController();
static const _gradeLevels = ['All Grades', '7', '8', '9', '10', '11', '12'];
static const _statusItems = [
@@ -111,6 +111,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_searchController.dispose();
_searchFocusNode.dispose();
_horizontalScrollController.dispose();
_pendingLrnCtrl.dispose();
ref.read(studentQueryProvider.notifier).reset();
super.dispose();
}
@@ -669,6 +670,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
query.section.isNotEmpty,
query.status.isNotEmpty,
query.is4Ps.isNotEmpty,
query.lrn.isNotEmpty,
query.limit != 15,
].where((v) => v).length;
@@ -930,6 +932,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_pending4Ps = query.is4Ps.isEmpty
? 'All'
: (query.is4Ps == 'true' ? 'Yes' : 'No');
_pendingLrn = query.lrn;
_pendingLrnCtrl.text = query.lrn;
_pendingLimit = query.limit;
});
@@ -1106,6 +1110,42 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// LRN
_buildFilterSection(
label: 'LRN',
onReset: () => setDialogState(() {
_pendingLrn = '';
_pendingLrnCtrl.clear();
}),
child: SizedBox(
height: 38,
child: TextField(
controller: _pendingLrnCtrl,
decoration: InputDecoration(
hintText: 'Filter by LRN...',
hintStyle: const TextStyle(fontSize: 13, color: Colors.grey),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryGreen),
),
),
keyboardType: TextInputType.number,
onChanged: (v) => setDialogState(() => _pendingLrn = v.trim()),
),
),
),
const Divider(height: 1, indent: 20, endIndent: 20),
// School Year
_buildFilterSection(
label: 'School Year',
@@ -1245,6 +1285,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_pendingSection = 'All Sections';
_pendingStatus = 'All Status';
_pending4Ps = 'All';
_pendingLrn = '';
_pendingLrnCtrl.clear();
_pendingLimit = 15;
});
final n = ref.read(studentQueryProvider.notifier);
@@ -1253,6 +1295,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
n.setSection('');
n.setStatus('');
n.setIs4Ps('');
n.setLrn('');
n.setLimit(15);
},
child: const Text(
@@ -1300,6 +1343,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
if (_pending4Ps == 'Yes') is4psVal = 'true';
if (_pending4Ps == 'No') is4psVal = 'false';
n.setIs4Ps(is4psVal);
n.setLrn(_pendingLrn);
n.setLimit(_pendingLimit);
onApply();
@@ -1452,14 +1496,32 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
if (rawStudents.isEmpty) return _buildEmptyState(noSections: noSections);
List<StudentModel> students = List.from(rawStudents);
if (_docStatusSort == 'asc') {
students.sort(
(a, b) => a.missingDocumentsCount.compareTo(b.missingDocumentsCount),
);
} else if (_docStatusSort == 'desc') {
students.sort(
(a, b) => b.missingDocumentsCount.compareTo(a.missingDocumentsCount),
);
if (query.sortBy == 'lrn') {
students.sort((a, b) {
final comp = a.lrn.compareTo(b.lrn);
return query.sortOrder == 'desc' ? -comp : comp;
});
} else if (query.sortBy == 'name') {
students.sort((a, b) {
final compA = '${a.lastName} ${a.firstName}'.toLowerCase();
final compB = '${b.lastName} ${b.firstName}'.toLowerCase();
final comp = compA.compareTo(compB);
return query.sortOrder == 'desc' ? -comp : comp;
});
} else if (query.sortBy == 'grade_section') {
students.sort((a, b) {
final gComp = a.gradeLevel.compareTo(b.gradeLevel);
if (gComp != 0) {
return query.sortOrder == 'desc' ? -gComp : gComp;
}
final sComp = a.section.toLowerCase().compareTo(b.section.toLowerCase());
return query.sortOrder == 'desc' ? -sComp : sComp;
});
} else if (query.sortBy == 'doc_status') {
students.sort((a, b) {
final comp = a.missingDocumentsCount.compareTo(b.missingDocumentsCount);
return query.sortOrder == 'desc' ? -comp : comp;
});
}
return LayoutBuilder(
@@ -1545,25 +1607,112 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
},
),
),
const DataColumn2(
DataColumn2(
size: ColumnSize.M,
label: Text(
'LRN',
style: TextStyle(fontWeight: FontWeight.bold),
label: Row(
children: [
const Text(
'LRN',
style: TextStyle(fontWeight: FontWeight.bold),
),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
ref
.read(studentQueryProvider.notifier)
.setSort(val.isEmpty ? '' : 'lrn', val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(
value: '',
checked: query.sortBy == 'lrn' && query.sortOrder == '',
child: const Text('None'),
),
CheckedPopupMenuItem(
value: 'asc',
checked: query.sortBy == 'lrn' && query.sortOrder == 'asc',
child: const Text('asc'),
),
CheckedPopupMenuItem(
value: 'desc',
checked: query.sortBy == 'lrn' && query.sortOrder == 'desc',
child: const Text('dsc'),
),
],
),
],
),
),
const DataColumn2(
DataColumn2(
size: ColumnSize.L,
label: Text(
'Name',
style: TextStyle(fontWeight: FontWeight.bold),
label: Row(
children: [
const Text(
'Name',
style: TextStyle(fontWeight: FontWeight.bold),
),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
ref
.read(studentQueryProvider.notifier)
.setSort(val.isEmpty ? '' : 'name', val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(
value: '',
checked: query.sortBy == 'name' && query.sortOrder == '',
child: const Text('None'),
),
CheckedPopupMenuItem(
value: 'asc',
checked: query.sortBy == 'name' && query.sortOrder == 'asc',
child: const Text('A-Z'),
),
CheckedPopupMenuItem(
value: 'desc',
checked: query.sortBy == 'name' && query.sortOrder == 'desc',
child: const Text('Z-A'),
),
],
),
],
),
),
const DataColumn2(
DataColumn2(
size: ColumnSize.M,
label: Text(
'Grade & Sec.',
style: TextStyle(fontWeight: FontWeight.bold),
label: Row(
children: [
const Text(
'Grade & Sec.',
style: TextStyle(fontWeight: FontWeight.bold),
),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
ref
.read(studentQueryProvider.notifier)
.setSort(val.isEmpty ? '' : 'grade_section', val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(
value: '',
checked: query.sortBy == 'grade_section' && query.sortOrder == '',
child: const Text('None'),
),
CheckedPopupMenuItem(
value: 'asc',
checked: query.sortBy == 'grade_section' && query.sortOrder == 'asc',
child: const Text('7-12 & A-Z'),
),
CheckedPopupMenuItem(
value: 'desc',
checked: query.sortBy == 'grade_section' && query.sortOrder == 'desc',
child: const Text('12-7 & Z-A'),
),
],
),
],
),
),
DataColumn2(
@@ -1664,23 +1813,25 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
setState(() => _docStatusSort = val);
ref
.read(studentQueryProvider.notifier)
.setSort(val.isEmpty ? '' : 'doc_status', val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(
value: '',
checked: _docStatusSort == '',
checked: query.sortBy == 'doc_status' && query.sortOrder == '',
child: const Text('None'),
),
CheckedPopupMenuItem(
value: 'asc',
checked: _docStatusSort == 'asc',
child: const Text('Least-Greatest'),
checked: query.sortBy == 'doc_status' && query.sortOrder == 'asc',
child: const Text('Low-High Attention'),
),
CheckedPopupMenuItem(
value: 'desc',
checked: _docStatusSort == 'desc',
child: const Text('Greatest-Least'),
checked: query.sortBy == 'doc_status' && query.sortOrder == 'desc',
child: const Text('High-Low Attention'),
),
],
),