import 'dart:async'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; class EmployeeSelectionBottomSheet extends StatefulWidget { final List initiallySelected; final bool multipleSelection; final String title; const EmployeeSelectionBottomSheet({ Key? key, this.initiallySelected = const [], this.multipleSelection = true, this.title = 'Select Employees', }) : super(key: key); @override State createState() => _EmployeeSelectionBottomSheetState(); } class _EmployeeSelectionBottomSheetState extends State { final TextEditingController _searchController = TextEditingController(); final RxBool _isSearching = false.obs; final RxList _allResults = [].obs; late RxList _selectedEmployees; Timer? _debounce; @override void initState() { super.initState(); _selectedEmployees = RxList.from(widget.initiallySelected); _performSearch(''); } @override void dispose() { _debounce?.cancel(); _searchController.dispose(); super.dispose(); } // ------------------------------------------------------ // 🔥 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(); // ------------------------------------------------------ // Auto-move selected employees to top // ------------------------------------------------------ results.sort((a, b) { if (widget.multipleSelection) { // Only move selected employees to top in multi-select final aSel = _selectedEmployees.contains(a) ? 0 : 1; final bSel = _selectedEmployees.contains(b) ? 0 : 1; if (aSel != bSel) return aSel.compareTo(bSel); } // Otherwise, keep original order (or alphabetically if needed) 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)) { _selectedEmployees.remove(emp); } else { _selectedEmployees.add(emp); } } else { _selectedEmployees.assignAll([emp]); } // Re-sort list after each toggle _performSearch(_searchController.text.trim()); } // ------------------------------------------------------ // Submit selection // ------------------------------------------------------ void _handleSubmit() { if (widget.multipleSelection) { Navigator.of(context).pop(_selectedEmployees.toList()); } else { Navigator.of(context) .pop(_selectedEmployees.isNotEmpty ? _selectedEmployees.first : null); } } // ------------------------------------------------------ // Search bar widget // ------------------------------------------------------ Widget _searchBar() => Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: TextField( controller: _searchController, onChanged: _onSearchChanged, decoration: InputDecoration( hintText: 'Search employees...', filled: true, fillColor: Colors.grey.shade100, prefixIcon: const Icon(Icons.search, color: Colors.grey), suffixIcon: _searchController.text.isNotEmpty ? IconButton( icon: const Icon(Icons.close, color: Colors.grey), onPressed: () { _searchController.clear(); _performSearch(''); }, ) : null, contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), border: OutlineInputBorder( borderRadius: BorderRadius.circular(30), borderSide: BorderSide.none, ), ), ), ); // ------------------------------------------------------ // Employee list (optimized) // ------------------------------------------------------ Widget _employeeList() => Expanded( child: Obx(() { final results = _allResults; return ListView.builder( padding: const EdgeInsets.symmetric(vertical: 4), itemCount: results.length, itemBuilder: (context, index) { final emp = results[index]; final isSelected = _selectedEmployees.contains(emp); Widget trailingWidget; if (widget.multipleSelection) { // Multiple selection → normal checkbox trailingWidget = Checkbox( value: isSelected, onChanged: (_) => _toggleEmployee(emp), fillColor: MaterialStateProperty.resolveWith( (states) => states.contains(MaterialState.selected) ? Colors.blueAccent : Colors.white, ), ); } else { // Single selection → check circle trailingWidget = isSelected ? const Icon(Icons.check_circle, color: Colors.blueAccent) : const Icon(Icons.circle_outlined, color: Colors.grey); } 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: trailingWidget, onTap: () => _toggleEmployee(emp), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), ); }, ); }), ); // ------------------------------------------------------ // Build bottom sheet // ------------------------------------------------------ @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Navigator.of(context).pop(), onSubmit: _handleSubmit, child: SizedBox( height: MediaQuery.of(context).size.height * 0.7, child: Column( children: [ _searchBar(), _employeeList(), ], ), ), ); } }