feat: server-side search with debounce for professionals list
- getProfessionals() now accepts optional search param sent to ?search= - updateCity only triggers the initial load (city no longer used as filter) - Search field debounces 450ms before firing API call - TextEditingController added so clear button also resets the field - Client-side filtering removed (API now handles it and returns top 7) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
9da159f287
commit
e0982995e6
@@ -5,25 +5,23 @@ import 'package:prosapp_web_app/services/api_service.dart';
|
||||
class ProfessionalsProvider extends ChangeNotifier {
|
||||
List<UsuarioProfesional> professionals = [];
|
||||
bool isLoading = true;
|
||||
String? _city;
|
||||
bool _initialized = false;
|
||||
final _api = ApiService.instance;
|
||||
|
||||
void updateCity(String? city) {
|
||||
// Always load on first auth event; afterwards only reload when city changes.
|
||||
if (!_initialized || _city != city) {
|
||||
// Only used to trigger the initial load when auth becomes available.
|
||||
if (!_initialized) {
|
||||
_initialized = true;
|
||||
_city = city;
|
||||
getProfessionals();
|
||||
}
|
||||
}
|
||||
|
||||
getProfessionals() async {
|
||||
Future<void> getProfessionals({String? search}) async {
|
||||
isLoading = true;
|
||||
notifyListeners();
|
||||
try {
|
||||
final path = (_city != null && _city!.isNotEmpty)
|
||||
? '/professionals?city=${Uri.encodeComponent(_city!)}'
|
||||
final path = (search != null && search.trim().isNotEmpty)
|
||||
? '/professionals?search=${Uri.encodeComponent(search.trim())}'
|
||||
: '/professionals';
|
||||
final res = await _api.get(path);
|
||||
final list = (res is Map ? res['data'] : res) as List;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
||||
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
||||
@@ -14,19 +15,35 @@ class ProfessionalsView extends StatefulWidget {
|
||||
|
||||
class _ProfessionalsViewState extends State<ProfessionalsView> {
|
||||
String _search = '';
|
||||
Timer? _debounce;
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounce?.cancel();
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String value) {
|
||||
setState(() => _search = value);
|
||||
_debounce?.cancel();
|
||||
_debounce = Timer(const Duration(milliseconds: 450), () {
|
||||
context.read<ProfessionalsProvider>().getProfessionals(search: value);
|
||||
});
|
||||
}
|
||||
|
||||
void _clearSearch() {
|
||||
_controller.clear();
|
||||
_onSearchChanged('');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final provider = context.watch<ProfessionalsProvider>();
|
||||
final isDark = context.watch<ThemeProvider>().isDark;
|
||||
|
||||
final filtered = provider.professionals.where((p) {
|
||||
if (_search.isEmpty) return true;
|
||||
final q = _search.toLowerCase();
|
||||
return p.user.name.toLowerCase().contains(q) ||
|
||||
p.professionalInfo.profession.toLowerCase().contains(q) ||
|
||||
(p.user.city ?? '').toLowerCase().contains(q);
|
||||
}).toList();
|
||||
final filtered = provider.professionals;
|
||||
|
||||
final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||
final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB);
|
||||
@@ -39,7 +56,8 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 14),
|
||||
child: TextField(
|
||||
onChanged: (v) => setState(() => _search = v),
|
||||
controller: _controller,
|
||||
onChanged: _onSearchChanged,
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white : const Color(0xFF111827),
|
||||
fontSize: 14,
|
||||
@@ -52,7 +70,7 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
||||
suffixIcon: _search.isNotEmpty
|
||||
? IconButton(
|
||||
icon: Icon(Icons.close, size: 18, color: hintColor),
|
||||
onPressed: () => setState(() => _search = ''),
|
||||
onPressed: _clearSearch,
|
||||
)
|
||||
: null,
|
||||
filled: true,
|
||||
|
||||
Reference in New Issue
Block a user