diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 829dc4f..5ad9375 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; @@ -24,33 +25,67 @@ class EmployeeSelectionBottomSheet extends StatefulWidget { class _EmployeeSelectionBottomSheetState extends State { final TextEditingController _searchController = TextEditingController(); + final RxBool _isSearching = false.obs; - final RxList _searchResults = [].obs; + final RxList _allResults = [].obs; + late RxList _selectedEmployees; + Timer? _debounce; + @override void initState() { super.initState(); _selectedEmployees = RxList.from(widget.initiallySelected); - _searchEmployees(''); + + _performSearch(''); } @override void dispose() { + _debounce?.cancel(); _searchController.dispose(); super.dispose(); } - Future _searchEmployees(String query) async { + // ------------------------------------------------------ + // 🔥 Optimized debounce-based search + // ------------------------------------------------------ + void _onSearchChanged(String query) { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 300), () { + _performSearch(query.trim()); + }); + } + + Future _performSearch(String query) async { _isSearching.value = true; + final data = await ApiService.searchEmployeesBasic(searchString: query); + final results = (data as List) .map((e) => EmployeeModel.fromJson(e as Map)) .toList(); - _searchResults.assignAll(results); + + // ------------------------------------------------------ + // 🔥 Auto-move selected employees to top + // ------------------------------------------------------ + results.sort((a, b) { + final aSel = _selectedEmployees.contains(a) ? 0 : 1; + final bSel = _selectedEmployees.contains(b) ? 0 : 1; + + if (aSel != bSel) return aSel.compareTo(bSel); + return a.name.toLowerCase().compareTo(b.name.toLowerCase()); + }); + + _allResults.assignAll(results); + _isSearching.value = false; } + // ------------------------------------------------------ + // Handle tap & checkbox + // ------------------------------------------------------ void _toggleEmployee(EmployeeModel emp) { if (widget.multipleSelection) { if (_selectedEmployees.contains(emp)) { @@ -61,9 +96,14 @@ class _EmployeeSelectionBottomSheetState } else { _selectedEmployees.assignAll([emp]); } - _selectedEmployees.refresh(); // important for Obx rebuild + + // Re-sort list after each toggle + _performSearch(_searchController.text.trim()); } + // ------------------------------------------------------ + // Submit selection + // ------------------------------------------------------ void _handleSubmit() { if (widget.multipleSelection) { Navigator.of(context).pop(_selectedEmployees.toList()); @@ -73,11 +113,14 @@ class _EmployeeSelectionBottomSheetState } } + // ------------------------------------------------------ + // Search bar widget + // ------------------------------------------------------ Widget _searchBar() => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextField( controller: _searchController, - onChanged: _searchEmployees, + onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Search employees...', filled: true, @@ -88,7 +131,7 @@ class _EmployeeSelectionBottomSheetState icon: const Icon(Icons.close, color: Colors.grey), onPressed: () { _searchController.clear(); - _searchEmployees(''); + _performSearch(''); }, ) : null, @@ -102,60 +145,52 @@ class _EmployeeSelectionBottomSheetState ), ); + // ------------------------------------------------------ + // Employee list (optimized) + // ------------------------------------------------------ Widget _employeeList() => Expanded( child: Obx(() { - if (_isSearching.value) { - return const Center(child: CircularProgressIndicator()); - } - - if (_searchResults.isEmpty) { - return const Center(child: Text("No employees found")); - } + final results = _allResults; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), - itemCount: _searchResults.length, + itemCount: results.length, itemBuilder: (context, index) { - final emp = _searchResults[index]; + final emp = results[index]; + final isSelected = _selectedEmployees.contains(emp); - return Obx(() { - final isSelected = _selectedEmployees.contains(emp); - return ListTile( - leading: CircleAvatar( - backgroundColor: Colors.blueAccent, - child: Text( - (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') - .toUpperCase(), - style: const TextStyle(color: Colors.white), - ), + return ListTile( + leading: CircleAvatar( + backgroundColor: Colors.blueAccent, + child: Text( + (emp.firstName.isNotEmpty ? emp.firstName[0] : 'U') + .toUpperCase(), + style: const TextStyle(color: Colors.white), ), - title: Text('${emp.firstName} ${emp.lastName}'), - subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) { - FocusScope.of(context).unfocus(); // hide keyboard - _toggleEmployee(emp); - }, - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), + ), + title: Text('${emp.firstName} ${emp.lastName}'), + subtitle: Text(emp.email), + trailing: Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, ), - onTap: () { - FocusScope.of(context).unfocus(); - _toggleEmployee(emp); - }, - contentPadding: - const EdgeInsets.symmetric(horizontal: 0, vertical: 4), - ); - }); + ), + onTap: () => _toggleEmployee(emp), + contentPadding: + const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); }, ); }), ); + // ------------------------------------------------------ + // Build bottom sheet + // ------------------------------------------------------ @override Widget build(BuildContext context) { return BaseBottomSheet( @@ -164,10 +199,12 @@ class _EmployeeSelectionBottomSheetState onSubmit: _handleSubmit, child: SizedBox( height: MediaQuery.of(context).size.height * 0.7, - child: Column(children: [ - _searchBar(), - _employeeList(), - ]), + child: Column( + children: [ + _searchBar(), + _employeeList(), + ], + ), ), ); }