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:
Lizandro Guarnizo
2026-06-27 08:38:01 -05:00
co-authored by Claude Sonnet 4.6
parent 842a67cdea
commit 9c61a35747
2 changed files with 80 additions and 1 deletions
+18
View File
@@ -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: [...]}]}
+62 -1
View File
@@ -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,