feat: validacion ciudad disponible en dashboard
- CitiesProvider.isCityAvailable() y findMatchingCity() con comparacion flexible - Al mover el mapa extrae la ciudad del geocodificado inverso - Si la ciudad no esta en la lista muestra aviso amarillo y bloquea solicitud - Boton de solicitar deshabilitado cuando ciudad no esta disponible Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
842a67cdea
commit
9c61a35747
@@ -18,6 +18,24 @@ class CitiesProvider extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Busca una ciudad que coincida con el nombre geocodificado (comparación flexible)
|
||||
City? findMatchingCity(String geocodedName) {
|
||||
if (geocodedName.isEmpty || cities.isEmpty) return null;
|
||||
final normalized = geocodedName.toLowerCase().trim();
|
||||
// Intento exacto primero
|
||||
for (final c in cities) {
|
||||
if (c.cityName.toLowerCase() == normalized) return c;
|
||||
}
|
||||
// Coincidencia parcial: el nombre geocodificado contiene el nombre de ciudad
|
||||
for (final c in cities) {
|
||||
final cn = c.cityName.toLowerCase();
|
||||
if (normalized.contains(cn) || cn.contains(normalized.split(',').first.trim())) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool isCityAvailable(String geocodedName) => findMatchingCity(geocodedName) != null;
|
||||
|
||||
getCities() async {
|
||||
try {
|
||||
// GET /locations/countries returns nested {regions: [{cities: [...]}]}
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:prosapp_web_app/models/service_status.dart';
|
||||
import 'package:prosapp_web_app/models/usuario.dart';
|
||||
import 'package:prosapp_web_app/models/usuario_profesional.dart';
|
||||
import 'package:prosapp_web_app/providers/auth_provider.dart';
|
||||
import 'package:prosapp_web_app/providers/cities_provider.dart';
|
||||
import 'package:prosapp_web_app/router/router.dart';
|
||||
import 'package:prosapp_web_app/services/api_service.dart';
|
||||
import 'package:prosapp_web_app/services/maps_service.dart';
|
||||
@@ -56,6 +57,10 @@ class _DashboardViewState extends State<DashboardView> {
|
||||
// Throttle reverse geocode on camera idle
|
||||
DateTime _lastGeocode = DateTime(0);
|
||||
|
||||
// Ciudad detectada y disponibilidad
|
||||
String _detectedCity = '';
|
||||
bool _cityAvailable = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -121,10 +126,18 @@ class _DashboardViewState extends State<DashboardView> {
|
||||
final results = data['results'] as List?;
|
||||
if (results != null && results.isNotEmpty) {
|
||||
final address = results[0]['formatted_address'] as String;
|
||||
final components = results[0]['address_components'] as List? ?? [];
|
||||
final city = _extractCity(components);
|
||||
|
||||
final citiesProvider = Provider.of<CitiesProvider>(context, listen: false);
|
||||
final available = city.isEmpty || citiesProvider.isCityAvailable(city);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_currentAddress = address;
|
||||
_searchController.text = address;
|
||||
_detectedCity = city;
|
||||
_cityAvailable = available;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -133,6 +146,16 @@ class _DashboardViewState extends State<DashboardView> {
|
||||
if (mounted) setState(() => _geocoding = false);
|
||||
}
|
||||
|
||||
String _extractCity(List components) {
|
||||
for (final c in components) {
|
||||
final types = List<String>.from(c['types'] ?? []);
|
||||
if (types.contains('locality') || types.contains('administrative_area_level_2')) {
|
||||
return c['long_name'] as String? ?? '';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> _geocodeAndMoveMap(String address) async {
|
||||
if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return;
|
||||
try {
|
||||
@@ -579,10 +602,48 @@ class _DashboardViewState extends State<DashboardView> {
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Aviso ciudad no disponible
|
||||
if (!_cityAvailable && _detectedCity.isNotEmpty)
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF3CD),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFFFC107)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.location_off_outlined, color: Color(0xFF856404), size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Servicio no disponible en tu ciudad',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF856404),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Actualmente no operamos en $_detectedCity. Mueve el mapa a una ciudad disponible.',
|
||||
style: const TextStyle(color: Color(0xFF856404), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _requesting ? null : _requestService,
|
||||
onPressed: (_requesting || !_cityAvailable) ? null : _requestService,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF42A4EF),
|
||||
foregroundColor: Colors.white,
|
||||
|
||||
Reference in New Issue
Block a user