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;
|
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 {
|
getCities() async {
|
||||||
try {
|
try {
|
||||||
// GET /locations/countries returns nested {regions: [{cities: [...]}]}
|
// 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.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/auth_provider.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/router/router.dart';
|
||||||
import 'package:prosapp_web_app/services/api_service.dart';
|
import 'package:prosapp_web_app/services/api_service.dart';
|
||||||
import 'package:prosapp_web_app/services/maps_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
|
// Throttle reverse geocode on camera idle
|
||||||
DateTime _lastGeocode = DateTime(0);
|
DateTime _lastGeocode = DateTime(0);
|
||||||
|
|
||||||
|
// Ciudad detectada y disponibilidad
|
||||||
|
String _detectedCity = '';
|
||||||
|
bool _cityAvailable = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -121,10 +126,18 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
final results = data['results'] as List?;
|
final results = data['results'] as List?;
|
||||||
if (results != null && results.isNotEmpty) {
|
if (results != null && results.isNotEmpty) {
|
||||||
final address = results[0]['formatted_address'] as String;
|
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) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentAddress = address;
|
_currentAddress = address;
|
||||||
_searchController.text = address;
|
_searchController.text = address;
|
||||||
|
_detectedCity = city;
|
||||||
|
_cityAvailable = available;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +146,16 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
if (mounted) setState(() => _geocoding = false);
|
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 {
|
Future<void> _geocodeAndMoveMap(String address) async {
|
||||||
if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return;
|
if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return;
|
||||||
try {
|
try {
|
||||||
@@ -579,10 +602,48 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
|
|
||||||
const SizedBox(height: 12),
|
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(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _requesting ? null : _requestService,
|
onPressed: (_requesting || !_cityAvailable) ? null : _requestService,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFF42A4EF),
|
backgroundColor: const Color(0xFF42A4EF),
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|||||||
Reference in New Issue
Block a user