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 {
|
class ProfessionalsProvider extends ChangeNotifier {
|
||||||
List<UsuarioProfesional> professionals = [];
|
List<UsuarioProfesional> professionals = [];
|
||||||
bool isLoading = true;
|
bool isLoading = true;
|
||||||
String? _city;
|
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
final _api = ApiService.instance;
|
final _api = ApiService.instance;
|
||||||
|
|
||||||
void updateCity(String? city) {
|
void updateCity(String? city) {
|
||||||
// Always load on first auth event; afterwards only reload when city changes.
|
// Only used to trigger the initial load when auth becomes available.
|
||||||
if (!_initialized || _city != city) {
|
if (!_initialized) {
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
_city = city;
|
|
||||||
getProfessionals();
|
getProfessionals();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getProfessionals() async {
|
Future<void> getProfessionals({String? search}) async {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final path = (_city != null && _city!.isNotEmpty)
|
final path = (search != null && search.trim().isNotEmpty)
|
||||||
? '/professionals?city=${Uri.encodeComponent(_city!)}'
|
? '/professionals?search=${Uri.encodeComponent(search.trim())}'
|
||||||
: '/professionals';
|
: '/professionals';
|
||||||
final res = await _api.get(path);
|
final res = await _api.get(path);
|
||||||
final list = (res is Map ? res['data'] : res) as List;
|
final list = (res is Map ? res['data'] : res) as List;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
||||||
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
import 'package:prosapp_web_app/providers/professionals_provider.dart';
|
||||||
@@ -14,19 +15,35 @@ class ProfessionalsView extends StatefulWidget {
|
|||||||
|
|
||||||
class _ProfessionalsViewState extends State<ProfessionalsView> {
|
class _ProfessionalsViewState extends State<ProfessionalsView> {
|
||||||
String _search = '';
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final provider = context.watch<ProfessionalsProvider>();
|
final provider = context.watch<ProfessionalsProvider>();
|
||||||
final isDark = context.watch<ThemeProvider>().isDark;
|
final isDark = context.watch<ThemeProvider>().isDark;
|
||||||
|
|
||||||
final filtered = provider.professionals.where((p) {
|
final filtered = provider.professionals;
|
||||||
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 cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
final cardBg = isDark ? const Color(0xFF1E293B) : Colors.white;
|
||||||
final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB);
|
final border = isDark ? const Color(0xFF334155) : const Color(0xFFE5E7EB);
|
||||||
@@ -39,7 +56,8 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 14),
|
padding: const EdgeInsets.fromLTRB(0, 0, 0, 14),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
onChanged: (v) => setState(() => _search = v),
|
controller: _controller,
|
||||||
|
onChanged: _onSearchChanged,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: isDark ? Colors.white : const Color(0xFF111827),
|
color: isDark ? Colors.white : const Color(0xFF111827),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -52,7 +70,7 @@ class _ProfessionalsViewState extends State<ProfessionalsView> {
|
|||||||
suffixIcon: _search.isNotEmpty
|
suffixIcon: _search.isNotEmpty
|
||||||
? IconButton(
|
? IconButton(
|
||||||
icon: Icon(Icons.close, size: 18, color: hintColor),
|
icon: Icon(Icons.close, size: 18, color: hintColor),
|
||||||
onPressed: () => setState(() => _search = ''),
|
onPressed: _clearSearch,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
filled: true,
|
filled: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user