feat(navigation): integrate student search, multi-select, and filter actions into Android AppBar header
This commit is contained in:
1 parent
3073790f56
commit
8ae1ab6f0b
3 files changed
+171
-6
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(() {
|
||||
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 ──
|
||||
|
||||
Reference in new issue
Block a user