feat(navigation): integrate student search, multi-select, and filter actions into Android AppBar header

This commit is contained in:
ssbg04 committed 2026-08-31 22:51:06 +08:00
1 parent 3073790f56
commit 8ae1ab6f0b
3 files changed
+173 -8

No files matched your search

@@ -26,6 +26,8 @@ import '../providers/navigation_provider.dart';
import '../providers/connected_users_provider.dart';
import '../screens/capstone_members/capstone_members_screen.dart';
import '../shared/dialogs/disconnected_dialog.dart';
import '../screens/students/widgets/student_filter_dialog.dart';
import '../shared/inputs/app_search_bar.dart';
// Dummy screen for placeholders
class PlaceholderScreen extends StatelessWidget {
@@ -82,6 +84,46 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
});
}
void _showStudentSearchDialog(BuildContext context, WidgetRef ref) {
final currentSearch = ref.read(studentQueryProvider).search;
final searchController = TextEditingController(text: currentSearch);
final searchFocusNode = FocusNode();
WidgetsBinding.instance.addPostFrameCallback((_) {
searchFocusNode.requestFocus();
});
showDialog(
context: context,
barrierColor: Colors.black54,
builder: (ctx) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
return Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, kToolbarHeight + 24, 16, 0),
child: Material(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
elevation: 4,
borderRadius: BorderRadius.circular(12),
child: AppSearchBar(
controller: searchController,
focusNode: searchFocusNode,
collapsible: false,
hint: 'Search by LRN or name...',
maxWidth: 600,
onSubmitted: (value) {
Navigator.of(ctx).pop();
ref.read(studentQueryProvider.notifier).setSearch(value);
},
),
),
),
);
},
);
}
void _initTabs() {
final primaryTabsConfig = [
{
@@ -332,6 +374,87 @@ class _AndroidBottomNavLayoutState extends ConsumerState<AndroidBottomNavLayout>
),
elevation: 0,
surfaceTintColor: Colors.transparent,
actions: [
if (activeTab == 'Students') ...[
// 1. Search Icon
Consumer(
builder: (context, ref, _) {
final query = ref.watch(studentQueryProvider);
return Tooltip(
message: query.search.isNotEmpty
? 'Clear Search'
: 'Search Students',
child: IconButton(
icon: Icon(
query.search.isNotEmpty
? Icons.close
: Icons.search,
size: 22,
),
onPressed: () {
if (query.search.isNotEmpty) {
ref.read(studentQueryProvider.notifier).setSearch('');
} else {
_showStudentSearchDialog(context, ref);
}
},
),
);
},
),
// 2. Multi-Select Toggle (for non-teachers)
if (widget.userRole != 'teacher')
Consumer(
builder: (context, ref, _) {
final isMultiSelect = ref.watch(studentMultiSelectProvider);
return Tooltip(
message: isMultiSelect
? 'Exit Multi-Select'
: 'Multi-Select',
child: IconButton(
icon: Icon(
Icons.checklist_rounded,
size: 22,
color: isMultiSelect
? AppColors.primaryGreen
: null,
),
onPressed: () {
final next = !isMultiSelect;
ref.read(studentMultiSelectProvider.notifier).state = next;
if (!next) {
ref.read(studentSelectedIdsProvider.notifier).state = [];
}
},
),
);
},
),
// 3. Filter Icon with Badge
Consumer(
builder: (context, ref, _) {
final activeCount = ref.watch(studentActiveFilterCountProvider);
final query = ref.watch(studentQueryProvider);
return Tooltip(
message: 'Filter Students',
child: IconButton(
onPressed: () =>
StudentFilterDialog.show(context, query: query),
icon: Badge(
isLabelVisible: activeCount > 0,
label: Text(activeCount.toString()),
child: const Icon(
Icons.tune_rounded,
size: 22,
),
),
),
);
},
),
const SizedBox(width: 4),
],
],
),
drawer: Drawer(
backgroundColor: Theme.of(context).colorScheme.surface,
@@ -716,8 +839,8 @@ class _PageSkeletonLoaderState extends State<_PageSkeletonLoader>
child: ListView.separated(
itemCount: 5,
physics: const NeverScrollableScrollPhysics(),
separatorBuilder: (_, __) => const SizedBox(height: 16),
itemBuilder: (_, __) => Container(
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (_, _) => Container(
height: 60,
decoration: BoxDecoration(
color: secondaryColor,
@@ -200,6 +200,25 @@ class StudentQueryNotifier extends Notifier<StudentQueryParams> {
void reset() => state = const StudentQueryParams();
}
// ============================================================
// Multi-Select & Filter State Providers
// ============================================================
final studentMultiSelectProvider = StateProvider<bool>((ref) => false);
final studentSelectedIdsProvider = StateProvider<List<int>>((ref) => []);
final studentActiveFilterCountProvider = Provider<int>((ref) {
final query = ref.watch(studentQueryProvider);
return [
query.gradeLevel.isNotEmpty,
query.section.isNotEmpty,
query.status.isNotEmpty,
query.schoolYear.isNotEmpty,
query.is4Ps.isNotEmpty,
query.sortBy.isNotEmpty,
query.limit != 20,
].where((v) => v).length;
});
// ============================================================
// Async data provider — re-fetches when query changes
// ============================================================
@@ -369,10 +369,14 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
message: 'Cancel selection',
child: IconButton(
icon: Icon(Icons.close, color: buttonColor),
onPressed: () => setState(() {
_selectedStudentIds.clear();
_showMultiSelect = false;
}),
onPressed: () {
setState(() {
_selectedStudentIds.clear();
_showMultiSelect = false;
});
ref.read(studentMultiSelectProvider.notifier).state = false;
ref.read(studentSelectedIdsProvider.notifier).state = [];
},
),
),
const SizedBox(width: 8),
@@ -792,6 +796,21 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
query.limit != 20,
].where((v) => v).length;
final multiSelectFromProvider = ref.watch(studentMultiSelectProvider);
if (multiSelectFromProvider != _showMultiSelect) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && multiSelectFromProvider != _showMultiSelect) {
setState(() {
_showMultiSelect = multiSelectFromProvider;
if (!multiSelectFromProvider) _selectedStudentIds.clear();
});
}
});
}
final isMobileOrAndroid = MediaQuery.of(context).size.width < 800 ||
defaultTargetPlatform == TargetPlatform.android;
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
@@ -811,6 +830,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_showMultiSelect = false;
_selectedStudentIds.clear();
});
ref.read(studentMultiSelectProvider.notifier).state = false;
ref.read(studentSelectedIdsProvider.notifier).state = [];
return;
}
ref.read(activeTabProvider.notifier).setTab('Dashboard');
@@ -849,7 +870,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
_buildInlineMultiSelectHeader(
pageAsync.value?.students ?? [],
)
else
else if (!isMobileOrAndroid)
Padding(
padding: const EdgeInsets.only(
left: AppSizes.p24,
@@ -862,7 +883,9 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
ref,
activeCount,
),
),
)
else
const SizedBox(height: 12),
const SizedBox(height: AppSizes.p24),
// ── Data Table / Cards ──