From 735d8c2ba0e4d767d3a2ef72355c60a411007e99 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:15:59 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20corregir=20flujo=20completo=20de=20servi?= =?UTF-8?q?cio=20y=20geocodificaci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Service.fromJson: lat/lng con Decimal de Prisma llegaban como string y el cast 'as num?' lanzaba TypeError silencioso → lista de servicios vacía. Mismo patrón que average_score, fix: double.tryParse(?.toString()) - service_status: agregar enumToStringService que mapea el enum de Flutter al string que espera el DTO del backend ('pending','accepted', etc.). changeServiceStatus enviaba el índice entero → @IsEnum rechazaba → el profesional no podía aceptar ni rechazar solicitudes - maps_service: agregar MapsService.reverseGeocode que usa el Geocoder del Maps JS SDK ya cargado, sin requerir la Geocoding API habilitada por separado - dashboard_view: _reverseGeocode usa el JS Geocoder en vez del endpoint HTTP Co-Authored-By: Claude Sonnet 4.6 --- lib/models/service.dart | 5 ++- lib/models/service_status.dart | 22 +++++++--- lib/providers/services_provider.dart | 2 +- lib/services/maps_service.dart | 60 ++++++++++++++++++++++++++++ lib/ui/views/dashboard_view.dart | 58 +++++++++++---------------- 5 files changed, 103 insertions(+), 44 deletions(-) diff --git a/lib/models/service.dart b/lib/models/service.dart index 098f605..c62e8c2 100644 --- a/lib/models/service.dart +++ b/lib/models/service.dart @@ -65,8 +65,9 @@ class Service { userScored: doc['user_scored'] as bool? ?? false, address: doc['address'] as String? ?? '', aditionalAddress: (doc['additional_address'] ?? doc['aditional_address']) as String? ?? '', - latitude: (doc['latitude'] as num?)?.toDouble() ?? 0.0, - longitude: (doc['longitude'] as num?)?.toDouble() ?? 0.0, + // Prisma Decimal fields serialize as JSON strings — same pattern as average_score + latitude: double.tryParse(doc['latitude']?.toString() ?? '') ?? 0.0, + longitude: double.tryParse(doc['longitude']?.toString() ?? '') ?? 0.0, day: (doc['day']?.toString() ?? '').split('T').first, createdAt: doc['created_at']?.toString() ?? '', description: doc['description'] as String? ?? '', diff --git a/lib/models/service_status.dart b/lib/models/service_status.dart index 73b13cc..0bc6976 100644 --- a/lib/models/service_status.dart +++ b/lib/models/service_status.dart @@ -10,10 +10,20 @@ enum ServiceStatus { selfBooked // 6 } -int enumToIntService(ServiceStatus state) { - return state.index; -} +// Maps Flutter enum → backend string (matches ServiceStatus enum in the backend DTO) +const _serviceStatusStrings = { + ServiceStatus.pending: 'pending', + ServiceStatus.acepted: 'accepted', + ServiceStatus.denied: 'denied', + ServiceStatus.active: 'active', + ServiceStatus.cancelled: 'cancelled', + ServiceStatus.completed: 'completed', + ServiceStatus.selfBooked:'self_booked', +}; -ServiceStatus intToEnumService(int value) { - return ServiceStatus.values[value]; -} +String enumToStringService(ServiceStatus state) => + _serviceStatusStrings[state] ?? 'pending'; + +int enumToIntService(ServiceStatus state) => state.index; + +ServiceStatus intToEnumService(int value) => ServiceStatus.values[value]; diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart index e472221..7ac7a41 100644 --- a/lib/providers/services_provider.dart +++ b/lib/providers/services_provider.dart @@ -21,7 +21,7 @@ class ServicesProvider extends ChangeNotifier { Future changeServiceStatus(String serviceId, ServiceStatus newStatus) async { try { - await _api.patch('/services/$serviceId/status', {'status': enumToIntService(newStatus)}); + await _api.patch('/services/$serviceId/status', {'status': enumToStringService(newStatus)}); notifyListeners(); } catch (e) { print('Error al actualizar el estado: $e'); diff --git a/lib/services/maps_service.dart b/lib/services/maps_service.dart index 5ec79c3..8878664 100644 --- a/lib/services/maps_service.dart +++ b/lib/services/maps_service.dart @@ -4,6 +4,13 @@ import 'dart:js' as js; import 'package:flutter/foundation.dart'; import 'package:prosapp_web_app/services/api_service.dart'; +// Result of a reverse geocode via the Maps JS SDK Geocoder +class GeocodeResult { + final String formattedAddress; + final List> addressComponents; + const GeocodeResult({required this.formattedAddress, required this.addressComponents}); +} + class MapsService { static bool _loaded = false; static Completer? _completer; @@ -38,6 +45,59 @@ class MapsService { } } + // Uses the already-loaded Maps JS SDK Geocoder — no separate Geocoding API enablement needed. + static Future reverseGeocode(double lat, double lng) async { + final completer = Completer(); + final id = '_gc${DateTime.now().millisecondsSinceEpoch}'; + + try { + js.context['${id}_ok'] = js.allowInterop((String address, String compsJson) { + try { js.context.deleteProperty('${id}_ok'); } catch (_) {} + try { + final rawList = js.context.callMethod('eval', ['JSON.parse(\'${compsJson.replaceAll("'", "\\'")}\')']); + final comps = >[]; + // rawList is a JS array; iterate by index + final len = (rawList['length'] as num?)?.toInt() ?? 0; + for (var i = 0; i < len; i++) { + final item = rawList[i] as js.JsObject; + final types = []; + final tLen = (item['types']['length'] as num?)?.toInt() ?? 0; + for (var t = 0; t < tLen; t++) types.add(item['types'][t].toString()); + comps.add({'long_name': item['long_name'].toString(), 'types': types}); + } + completer.complete(GeocodeResult(formattedAddress: address, addressComponents: comps)); + } catch (_) { + completer.complete(GeocodeResult(formattedAddress: address, addressComponents: [])); + } + }); + + js.context['${id}_err'] = js.allowInterop(() { + try { js.context.deleteProperty('${id}_err'); } catch (_) {} + completer.complete(null); + }); + + js.context.callMethod('eval', [''' + (function(){ + try { + var g = new google.maps.Geocoder(); + g.geocode({location:{lat:$lat,lng:$lng},language:'es'}, function(r,s){ + if(s==='OK'&&r&&r.length>0){ + var compsStr = JSON.stringify(r[0].address_components.map(function(c){ + return {long_name:c.long_name,types:c.types}; + })); + window['${id}_ok'](r[0].formatted_address, compsStr); + } else { window['${id}_err'](); } + }); + } catch(e){ window['${id}_err'](); } + })(); + ''']); + } catch (e) { + completer.complete(null); + } + + return completer.future.timeout(const Duration(seconds: 10), onTimeout: () => null); + } + static void _injectScript(String apiKey) { // Verifica si ya está cargado final existing = js.context.callMethod('eval', [ diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart index 128f747..890c130 100644 --- a/lib/ui/views/dashboard_view.dart +++ b/lib/ui/views/dashboard_view.dart @@ -182,47 +182,35 @@ class _DashboardViewState extends State { } Future _reverseGeocode(LatLng pos) async { - if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return; final now = DateTime.now(); if (now.difference(_lastGeocode).inMilliseconds < 800) return; _lastGeocode = now; - setState(() => _geocoding = true); + if (mounted) setState(() => _geocoding = true); try { - final res = await http.get(Uri.parse( - 'https://maps.googleapis.com/maps/api/geocode/json' - '?latlng=${pos.latitude},${pos.longitude}' - '&key=$_mapsApiKey&language=es', - )); - if (res.statusCode == 200) { - final data = jsonDecode(res.body); - 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); + // Use Maps JS SDK Geocoder — works with the same API key as the map, + // no separate Geocoding API billing required + final result = await MapsService.reverseGeocode(pos.latitude, pos.longitude); + if (result != null && mounted) { + final city = _extractCity(result.addressComponents); + final citiesProvider = Provider.of(context, listen: false); + final available = city.isEmpty || citiesProvider.isCityAvailable(city); + final userCity = user?.city ?? ''; + final mismatch = userCity.isNotEmpty && city.isNotEmpty && !_citiesMatch(city, userCity); - final citiesProvider = Provider.of(context, listen: false); - final available = city.isEmpty || citiesProvider.isCityAvailable(city); - final userCity = user?.city ?? ''; - final mismatch = userCity.isNotEmpty && city.isNotEmpty && !_citiesMatch(city, userCity); - - if (mounted) { - setState(() { - _currentAddress = address; - _searchController.text = address; - _detectedCity = city; - _cityAvailable = available; - _cityMismatch = mismatch; - }); - if (!mismatch && city.isNotEmpty) { - context.read().setLocationContext( - city: city, - lat: pos.latitude, - lng: pos.longitude, - ); - } - } + setState(() { + _currentAddress = result.formattedAddress; + _searchController.text = result.formattedAddress; + _detectedCity = city; + _cityAvailable = available; + _cityMismatch = mismatch; + }); + if (!mismatch && city.isNotEmpty) { + context.read().setLocationContext( + city: city, + lat: pos.latitude, + lng: pos.longitude, + ); } } } catch (_) {}