From 646e44d126f6c79e265dd2352e4ba301fe071425 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:33:33 -0500 Subject: [PATCH] fix: location preference bug and service detail 404 - When professional accepts 'both' modes, add office/delivery toggle so the patient can explicitly choose instead of defaulting to delivery. Reset _bookAsDelivery when a professional is selected; office-only professionals still force office, delivery-only force delivery. - Fix 'sin servicio' on ServiceView: getServiceForUser was calling /users/:professionalId using the professionals-table UUID (not user UUID), returning 404 and setting service=null. Use the embedded professionals.users data from findById instead. - Convert ServiceView to StatefulWidget; fetch in initState to avoid re-fetching on every parent rebuild. - Remove client-side FCM notification from _requestService; backend create() now notifies the professional directly with the server key. Co-Authored-By: Claude Sonnet 4.6 --- lib/providers/services_provider.dart | 20 ++++- lib/ui/views/dashboard_view.dart | 129 ++++++++++++++++++++------- lib/ui/views/service_view.dart | 28 +++--- 3 files changed, 132 insertions(+), 45 deletions(-) diff --git a/lib/providers/services_provider.dart b/lib/providers/services_provider.dart index 7ac7a41..cdacfcc 100644 --- a/lib/providers/services_provider.dart +++ b/lib/providers/services_provider.dart @@ -69,8 +69,15 @@ class ServicesProvider extends ChangeNotifier { final data = await _api.get('/services/$serviceId'); final map = data as Map; final servicio = Service.fromJson(map, map['id'] as String); - final userData = await _api.get('/users/${servicio.professionalId}'); - final user = Usuario.fromDocument(userData as Map); + // findById embeds professionals.users — use it instead of a second /users/:id call + // (professional_id is a professionals-table UUID, not a user UUID) + final profDoc = map['professionals'] as Map?; + final userDoc = profDoc?['users'] as Map?; + final user = userDoc != null + ? Usuario.fromDocument(userDoc) + : Usuario(id: servicio.professionalId, email: null, phone: null, + name: '?', nickname: null, city: null, picture: null, + birthday: null, gender: null, proState: ProState.inactive, token: null); service = ServicioProfesional(user: user, service: servicio); } catch (e) { service = null; @@ -86,8 +93,13 @@ class ServicesProvider extends ChangeNotifier { final data = await _api.get('/services/$serviceId'); final map = data as Map; final servicio = Service.fromJson(map, map['id'] as String); - final userData = await _api.get('/users/${servicio.userId}'); - final user = Usuario.fromDocument(userData as Map); + // findById embeds users (the patient) directly — use it instead of a second /users/:id call + final userDoc = map['users'] as Map?; + final user = userDoc != null + ? Usuario.fromDocument(userDoc) + : Usuario(id: servicio.userId, email: null, phone: null, + name: '?', nickname: null, city: null, picture: null, + birthday: null, gender: null, proState: ProState.inactive, token: null); service = ServicioProfesional(user: user, service: servicio); } catch (e) { service = null; diff --git a/lib/ui/views/dashboard_view.dart b/lib/ui/views/dashboard_view.dart index 890c130..27fb7af 100644 --- a/lib/ui/views/dashboard_view.dart +++ b/lib/ui/views/dashboard_view.dart @@ -8,7 +8,6 @@ import 'package:intl/intl.dart'; import 'package:prosapp_web_app/models/schedules_entity.dart'; import 'package:prosapp_web_app/models/service.dart'; import 'package:prosapp_web_app/models/location_preferences.dart'; -import 'package:prosapp_web_app/models/service_location_preferences.dart'; 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'; @@ -21,7 +20,6 @@ import 'package:prosapp_web_app/services/api_service.dart'; import 'package:prosapp_web_app/services/maps_service.dart'; import 'package:prosapp_web_app/services/navigation_service.dart'; import 'package:prosapp_web_app/services/notifications_service.dart'; -import 'package:prosapp_web_app/utils/local_notifications.dart'; import 'package:prosapp_web_app/utils/network_utility.dart'; import 'package:prosapp_web_app/utils/time_of_day_extension.dart'; import 'package:provider/provider.dart'; @@ -56,6 +54,9 @@ class _DashboardViewState extends State { DateTime? _selectedDay; TimeOfDay? _selectedHour; bool _requesting = false; + // For professionals that accept both office and delivery, the patient chooses. + // true = delivery, false = office. Meaningless when professional only accepts one. + bool _bookAsDelivery = false; // Throttle reverse geocode on camera idle DateTime _lastGeocode = DateTime(0); @@ -310,6 +311,7 @@ class _DashboardViewState extends State { _professional = prof; _selectedDay = result[0] as DateTime; _selectedHour = result[1] as TimeOfDay; + _bookAsDelivery = prof.professionalInfo.locationPreferences == LocationPreferences.delivery; }); } } catch (_) {} @@ -329,6 +331,7 @@ class _DashboardViewState extends State { _professional = prof; _selectedDay = result[1]; _selectedHour = result[2]; + _bookAsDelivery = prof.professionalInfo.locationPreferences == LocationPreferences.delivery; }); } catch (_) {} } @@ -344,18 +347,16 @@ class _DashboardViewState extends State { } final profPrefs = _professional!.professionalInfo.locationPreferences; - final isOfficeOnly = profPrefs == LocationPreferences.office; + // Determine if this booking is for office or delivery + final bool isOfficeBooking = profPrefs == LocationPreferences.office || + (profPrefs == LocationPreferences.both && !_bookAsDelivery); // Dirección solo requerida cuando el profesional hace domicilios - if (!isOfficeOnly && _currentAddress.isEmpty) { + if (!isOfficeBooking && _currentAddress.isEmpty) { NotificationsService.showSnackBarError('Mueve el mapa para seleccionar tu dirección'); return; } - final serviceLocation = isOfficeOnly - ? ServiceLocationPreferences.office - : ServiceLocationPreferences.delivery; - setState(() => _requesting = true); try { final slot = _selectedHour!; @@ -369,21 +370,14 @@ class _DashboardViewState extends State { 'day': '${_selectedDay!.year}-${pad(_selectedDay!.month)}-${pad(_selectedDay!.day)}', 'range1_hour1': '${pad(slot.hour)}:${pad(slot.minute)}', 'range1_hour2': '${pad(endSlot.hour)}:${pad(endSlot.minute)}', - 'address': isOfficeOnly ? _professional!.professionalInfo.address : _currentAddress, - 'latitude': isOfficeOnly ? _professional!.professionalInfo.latitude : _mapCenter.latitude, - 'longitude': isOfficeOnly ? _professional!.professionalInfo.longitude : _mapCenter.longitude, + 'address': isOfficeBooking ? _professional!.professionalInfo.address : _currentAddress, + 'latitude': isOfficeBooking ? _professional!.professionalInfo.latitude : _mapCenter.latitude, + 'longitude': isOfficeBooking ? _professional!.professionalInfo.longitude : _mapCenter.longitude, // @IsEnum(['office','delivery']) expects the string value, not an integer - 'location_preference': isOfficeOnly ? 'office' : 'delivery', + 'location_preference': isOfficeBooking ? 'office' : 'delivery', }; await ApiService.instance.post('/services', payload); NotificationsService.showSnackbar('Servicio solicitado exitosamente'); - if (_professional!.user.token != null) { - LocalNotifications.sendPushNotification( - _professional!.user.token!, - 'Nuevo servicio', - 'Tienes una nueva solicitud de servicio pendiente', - ); - } setState(() { _professional = null; _selectedDay = null; @@ -656,11 +650,88 @@ class _DashboardViewState extends State { ), const SizedBox(height: 10), - // Dirección: solo relevante cuando el profesional hace domicilios + // Tipo de servicio (solo cuando el profesional acepta ambas modalidades) + if (_professional?.professionalInfo.locationPreferences == LocationPreferences.both) ...[ + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => setState(() => _bookAsDelivery = false), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: !_bookAsDelivery + ? const Color(0xFF42A4EF) + : (isDark ? const Color(0xFF1E293B) : Colors.grey[100]), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: !_bookAsDelivery + ? const Color(0xFF42A4EF) + : cardBorder, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.store_outlined, size: 14, + color: !_bookAsDelivery ? Colors.white : subtextColor), + const SizedBox(width: 5), + Text('Consultorio', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: !_bookAsDelivery ? Colors.white : subtextColor)), + ], + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: () => setState(() => _bookAsDelivery = true), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: _bookAsDelivery + ? const Color(0xFF42A4EF) + : (isDark ? const Color(0xFF1E293B) : Colors.grey[100]), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _bookAsDelivery + ? const Color(0xFF42A4EF) + : cardBorder, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.home_outlined, size: 14, + color: _bookAsDelivery ? Colors.white : subtextColor), + const SizedBox(width: 5), + Text('Domicilio', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: _bookAsDelivery ? Colors.white : subtextColor)), + ], + ), + ), + ), + ), + ], + ), + const SizedBox(height: 10), + ], + + // Dirección: consultorio del profesional o dirección del cliente Builder(builder: (context) { final prefs = _professional?.professionalInfo.locationPreferences; - final isOffice = prefs == LocationPreferences.office; - if (isOffice) { + final isOfficeBooking = prefs == LocationPreferences.office || + (prefs == LocationPreferences.both && !_bookAsDelivery); + if (isOfficeBooking) { // Mostrar dirección del consultorio del profesional final profAddress = _professional?.professionalInfo.address ?? ''; if (profAddress.isNotEmpty) { @@ -692,7 +763,7 @@ class _DashboardViewState extends State { } return const SizedBox.shrink(); } - // Domicilio o ambos: mostrar dirección del cliente + // Domicilio: mostrar dirección del cliente if (_currentAddress.isNotEmpty) { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -805,9 +876,8 @@ class _DashboardViewState extends State { const SizedBox(height: 12), - // Aviso ciudad no disponible - if (!_cityAvailable && _detectedCity.isNotEmpty && - _professional?.professionalInfo.locationPreferences != LocationPreferences.office) + // Aviso ciudad no disponible (solo aplica cuando el servicio es a domicilio) + if (!_cityAvailable && _detectedCity.isNotEmpty && _bookAsDelivery) Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 10), @@ -845,8 +915,7 @@ class _DashboardViewState extends State { ), // Warning: dirección fuera de la ciudad del usuario (solo en modo domicilio) - if (_cityMismatch && _detectedCity.isNotEmpty && - _professional?.professionalInfo.locationPreferences != LocationPreferences.office) + if (_cityMismatch && _detectedCity.isNotEmpty && _bookAsDelivery) Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 10), @@ -888,9 +957,9 @@ class _DashboardViewState extends State { child: ElevatedButton( onPressed: _requesting ? null - : (_professional?.professionalInfo.locationPreferences == LocationPreferences.office + : ((!_bookAsDelivery || (_cityAvailable && !_cityMismatch)) ? _requestService - : (!_cityAvailable || _cityMismatch ? null : _requestService)), + : null), style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF42A4EF), foregroundColor: Colors.white, diff --git a/lib/ui/views/service_view.dart b/lib/ui/views/service_view.dart index 2a5d533..6bf066f 100644 --- a/lib/ui/views/service_view.dart +++ b/lib/ui/views/service_view.dart @@ -16,7 +16,7 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:prosapp_web_app/utils/local_notifications.dart'; -class ServiceView extends StatelessWidget { +class ServiceView extends StatefulWidget { final String type; final String serviceId; @@ -26,19 +26,25 @@ class ServiceView extends StatelessWidget { required this.serviceId, }); + @override + State createState() => _ServiceViewState(); +} + +class _ServiceViewState extends State { + @override + void initState() { + super.initState(); + final sp = Provider.of(context, listen: false); + if (widget.type == 'user') { + sp.getServiceForUser(widget.serviceId); + } else if (widget.type == 'professional') { + sp.getServiceForProfessional(widget.serviceId); + } + } + @override Widget build(BuildContext context) { final settingsProvider = Provider.of(context); - final servicesProvider = - Provider.of(context, listen: false); - - if (type == 'user') { - servicesProvider.getServiceForUser(serviceId); - } - - if (type == 'professional') { - servicesProvider.getServiceForProfessional(serviceId); - } return Consumer( builder: (context, servicesProvider, child) {