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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
735d8c2ba0
commit
646e44d126
@@ -69,8 +69,15 @@ class ServicesProvider extends ChangeNotifier {
|
||||
final data = await _api.get('/services/$serviceId');
|
||||
final map = data as Map<String, dynamic>;
|
||||
final servicio = Service.fromJson(map, map['id'] as String);
|
||||
final userData = await _api.get('/users/${servicio.professionalId}');
|
||||
final user = Usuario.fromDocument(userData as Map<String, dynamic>);
|
||||
// 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<String, dynamic>?;
|
||||
final userDoc = profDoc?['users'] as Map<String, dynamic>?;
|
||||
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<String, dynamic>;
|
||||
final servicio = Service.fromJson(map, map['id'] as String);
|
||||
final userData = await _api.get('/users/${servicio.userId}');
|
||||
final user = Usuario.fromDocument(userData as Map<String, dynamic>);
|
||||
// findById embeds users (the patient) directly — use it instead of a second /users/:id call
|
||||
final userDoc = map['users'] as Map<String, dynamic>?;
|
||||
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;
|
||||
|
||||
@@ -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<DashboardView> {
|
||||
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<DashboardView> {
|
||||
_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<DashboardView> {
|
||||
_professional = prof;
|
||||
_selectedDay = result[1];
|
||||
_selectedHour = result[2];
|
||||
_bookAsDelivery = prof.professionalInfo.locationPreferences == LocationPreferences.delivery;
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -344,18 +347,16 @@ class _DashboardViewState extends State<DashboardView> {
|
||||
}
|
||||
|
||||
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<DashboardView> {
|
||||
'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<DashboardView> {
|
||||
),
|
||||
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<DashboardView> {
|
||||
}
|
||||
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<DashboardView> {
|
||||
|
||||
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<DashboardView> {
|
||||
),
|
||||
|
||||
// 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<DashboardView> {
|
||||
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,
|
||||
|
||||
@@ -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<ServiceView> createState() => _ServiceViewState();
|
||||
}
|
||||
|
||||
class _ServiceViewState extends State<ServiceView> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final sp = Provider.of<ServicesProvider>(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<SettingsProvider>(context);
|
||||
final servicesProvider =
|
||||
Provider.of<ServicesProvider>(context, listen: false);
|
||||
|
||||
if (type == 'user') {
|
||||
servicesProvider.getServiceForUser(serviceId);
|
||||
}
|
||||
|
||||
if (type == 'professional') {
|
||||
servicesProvider.getServiceForProfessional(serviceId);
|
||||
}
|
||||
|
||||
return Consumer<ServicesProvider>(
|
||||
builder: (context, servicesProvider, child) {
|
||||
|
||||
Reference in New Issue
Block a user