fix: corregir flujo completo de servicio y geocodificación
- 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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
8296b5a3ea
commit
735d8c2ba0
@@ -65,8 +65,9 @@ class Service {
|
|||||||
userScored: doc['user_scored'] as bool? ?? false,
|
userScored: doc['user_scored'] as bool? ?? false,
|
||||||
address: doc['address'] as String? ?? '',
|
address: doc['address'] as String? ?? '',
|
||||||
aditionalAddress: (doc['additional_address'] ?? doc['aditional_address']) as String? ?? '',
|
aditionalAddress: (doc['additional_address'] ?? doc['aditional_address']) as String? ?? '',
|
||||||
latitude: (doc['latitude'] as num?)?.toDouble() ?? 0.0,
|
// Prisma Decimal fields serialize as JSON strings — same pattern as average_score
|
||||||
longitude: (doc['longitude'] as num?)?.toDouble() ?? 0.0,
|
latitude: double.tryParse(doc['latitude']?.toString() ?? '') ?? 0.0,
|
||||||
|
longitude: double.tryParse(doc['longitude']?.toString() ?? '') ?? 0.0,
|
||||||
day: (doc['day']?.toString() ?? '').split('T').first,
|
day: (doc['day']?.toString() ?? '').split('T').first,
|
||||||
createdAt: doc['created_at']?.toString() ?? '',
|
createdAt: doc['created_at']?.toString() ?? '',
|
||||||
description: doc['description'] as String? ?? '',
|
description: doc['description'] as String? ?? '',
|
||||||
|
|||||||
@@ -10,10 +10,20 @@ enum ServiceStatus {
|
|||||||
selfBooked // 6
|
selfBooked // 6
|
||||||
}
|
}
|
||||||
|
|
||||||
int enumToIntService(ServiceStatus state) {
|
// Maps Flutter enum → backend string (matches ServiceStatus enum in the backend DTO)
|
||||||
return state.index;
|
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) {
|
String enumToStringService(ServiceStatus state) =>
|
||||||
return ServiceStatus.values[value];
|
_serviceStatusStrings[state] ?? 'pending';
|
||||||
}
|
|
||||||
|
int enumToIntService(ServiceStatus state) => state.index;
|
||||||
|
|
||||||
|
ServiceStatus intToEnumService(int value) => ServiceStatus.values[value];
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ServicesProvider extends ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> changeServiceStatus(String serviceId, ServiceStatus newStatus) async {
|
Future<void> changeServiceStatus(String serviceId, ServiceStatus newStatus) async {
|
||||||
try {
|
try {
|
||||||
await _api.patch('/services/$serviceId/status', {'status': enumToIntService(newStatus)});
|
await _api.patch('/services/$serviceId/status', {'status': enumToStringService(newStatus)});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error al actualizar el estado: $e');
|
print('Error al actualizar el estado: $e');
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ import 'dart:js' as js;
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:prosapp_web_app/services/api_service.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<Map<String, dynamic>> addressComponents;
|
||||||
|
const GeocodeResult({required this.formattedAddress, required this.addressComponents});
|
||||||
|
}
|
||||||
|
|
||||||
class MapsService {
|
class MapsService {
|
||||||
static bool _loaded = false;
|
static bool _loaded = false;
|
||||||
static Completer<bool>? _completer;
|
static Completer<bool>? _completer;
|
||||||
@@ -38,6 +45,59 @@ class MapsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uses the already-loaded Maps JS SDK Geocoder — no separate Geocoding API enablement needed.
|
||||||
|
static Future<GeocodeResult?> reverseGeocode(double lat, double lng) async {
|
||||||
|
final completer = Completer<GeocodeResult?>();
|
||||||
|
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 = <Map<String, dynamic>>[];
|
||||||
|
// 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 = <String>[];
|
||||||
|
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) {
|
static void _injectScript(String apiKey) {
|
||||||
// Verifica si ya está cargado
|
// Verifica si ya está cargado
|
||||||
final existing = js.context.callMethod('eval', [
|
final existing = js.context.callMethod('eval', [
|
||||||
|
|||||||
@@ -182,35 +182,25 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reverseGeocode(LatLng pos) async {
|
Future<void> _reverseGeocode(LatLng pos) async {
|
||||||
if (_mapsApiKey == null || _mapsApiKey!.isEmpty) return;
|
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
if (now.difference(_lastGeocode).inMilliseconds < 800) return;
|
if (now.difference(_lastGeocode).inMilliseconds < 800) return;
|
||||||
_lastGeocode = now;
|
_lastGeocode = now;
|
||||||
|
|
||||||
setState(() => _geocoding = true);
|
if (mounted) setState(() => _geocoding = true);
|
||||||
try {
|
try {
|
||||||
final res = await http.get(Uri.parse(
|
// Use Maps JS SDK Geocoder — works with the same API key as the map,
|
||||||
'https://maps.googleapis.com/maps/api/geocode/json'
|
// no separate Geocoding API billing required
|
||||||
'?latlng=${pos.latitude},${pos.longitude}'
|
final result = await MapsService.reverseGeocode(pos.latitude, pos.longitude);
|
||||||
'&key=$_mapsApiKey&language=es',
|
if (result != null && mounted) {
|
||||||
));
|
final city = _extractCity(result.addressComponents);
|
||||||
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);
|
|
||||||
|
|
||||||
final citiesProvider = Provider.of<CitiesProvider>(context, listen: false);
|
final citiesProvider = Provider.of<CitiesProvider>(context, listen: false);
|
||||||
final available = city.isEmpty || citiesProvider.isCityAvailable(city);
|
final available = city.isEmpty || citiesProvider.isCityAvailable(city);
|
||||||
final userCity = user?.city ?? '';
|
final userCity = user?.city ?? '';
|
||||||
final mismatch = userCity.isNotEmpty && city.isNotEmpty && !_citiesMatch(city, userCity);
|
final mismatch = userCity.isNotEmpty && city.isNotEmpty && !_citiesMatch(city, userCity);
|
||||||
|
|
||||||
if (mounted) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentAddress = address;
|
_currentAddress = result.formattedAddress;
|
||||||
_searchController.text = address;
|
_searchController.text = result.formattedAddress;
|
||||||
_detectedCity = city;
|
_detectedCity = city;
|
||||||
_cityAvailable = available;
|
_cityAvailable = available;
|
||||||
_cityMismatch = mismatch;
|
_cityMismatch = mismatch;
|
||||||
@@ -223,8 +213,6 @@ class _DashboardViewState extends State<DashboardView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
if (mounted) setState(() => _geocoding = false);
|
if (mounted) setState(() => _geocoding = false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user