diff --git a/lib/screens/user/user_calendar_screen.dart b/lib/screens/user/user_calendar_screen.dart index 97a1205..f10a657 100644 --- a/lib/screens/user/user_calendar_screen.dart +++ b/lib/screens/user/user_calendar_screen.dart @@ -31,6 +31,7 @@ class UserCalendarScreenState extends State { late int numDay; List? _services; + bool _loadFailed = false; CalendarFormat _calendarFormat = CalendarFormat.month; @@ -54,13 +55,17 @@ class UserCalendarScreenState extends State { } void _loadServices() { + setState(() => _loadFailed = false); + // Public calendar of *this* professional. The old call returned the + // caller's own agenda, so every slot looked free. serviceRepository - .getServicesForProfessionalforCalendar( - widget.userProfessional.myUser.id) + .getPublicCalendar(widget.userProfessional.professionalInfo.recordId) .then((services) { - setState(() { - _services = services; - }); + if (!mounted) return; + setState(() => _services = services); + }).catchError((e) { + if (!mounted) return; + setState(() => _loadFailed = true); }); } @@ -164,6 +169,32 @@ class UserCalendarScreenState extends State { } List _rangesItems(ScheduleEntity? schedule) { + if (_loadFailed) { + // Slots are hidden rather than shown as free: booking blind is how you + // end up with two people in the same hour. + return [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16), + child: Column( + children: [ + const Icon(Icons.wifi_off_outlined, size: 32, color: Colors.grey), + const SizedBox(height: 10), + const Text('No se pudo cargar la disponibilidad', + textAlign: TextAlign.center, + style: TextStyle(fontWeight: FontWeight.w600)), + const SizedBox(height: 4), + const Text( + 'No mostramos horarios para evitar que reserves sobre una cita ya agendada.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 13, color: Colors.grey)), + const SizedBox(height: 12), + OutlinedButton( + onPressed: _loadServices, child: const Text('Reintentar')), + ], + ), + ), + ]; + } if (schedule == null) { return [ const Padding( diff --git a/packages/professional_repository/lib/src/entities/professional_entity.dart b/packages/professional_repository/lib/src/entities/professional_entity.dart index 368fc7a..e701781 100644 --- a/packages/professional_repository/lib/src/entities/professional_entity.dart +++ b/packages/professional_repository/lib/src/entities/professional_entity.dart @@ -21,6 +21,11 @@ class ProfessionalEntity extends Equatable { final PaymentMethodEntity paymentMethods; final int slotDurationMinutes; + /// Row id in the `professionals` table. Distinct from [id], which is the + /// owning user's id. Endpoints such as /services/public-calendar are keyed + /// by this one. + final String recordId; + const ProfessionalEntity({ required this.id, required this.identification, @@ -40,6 +45,7 @@ class ProfessionalEntity extends Equatable { required this.schedules, required this.paymentMethods, this.slotDurationMinutes = 120, + this.recordId = '', }); static ProfessionalEntity fromDocument(Map doc) { @@ -64,6 +70,7 @@ class ProfessionalEntity extends Equatable { paymentMethods: PaymentMethodEntity.fromDocument(doc['payment_methods']), slotDurationMinutes: (doc['slot_duration_minutes'] as num?)?.toInt() ?? 120, + recordId: doc['id']?.toString() ?? '', ); } @@ -86,6 +93,7 @@ class ProfessionalEntity extends Equatable { Schedules? schedules, PaymentMethodEntity? paymentMethods, int? slotDurationMinutes, + String? recordId, }) { return ProfessionalEntity( id: id ?? this.id, @@ -108,6 +116,7 @@ class ProfessionalEntity extends Equatable { schedules: schedules ?? this.schedules, paymentMethods: paymentMethods ?? this.paymentMethods, slotDurationMinutes: slotDurationMinutes ?? this.slotDurationMinutes, + recordId: recordId ?? this.recordId, ); } @@ -154,6 +163,7 @@ class ProfessionalEntity extends Equatable { schedules, paymentMethods, slotDurationMinutes, + recordId, ]; @override diff --git a/packages/professional_repository/lib/src/repositories/api_professional_repository.dart b/packages/professional_repository/lib/src/repositories/api_professional_repository.dart index 4d34db4..62e6316 100644 --- a/packages/professional_repository/lib/src/repositories/api_professional_repository.dart +++ b/packages/professional_repository/lib/src/repositories/api_professional_repository.dart @@ -195,6 +195,7 @@ class ApiProfessionalRepository { : PaymentMethodEntity.empty, slotDurationMinutes: (json['slot_duration_minutes'] as num?)?.toInt() ?? 120, + recordId: json['id']?.toString() ?? '', ); } diff --git a/packages/service_repository/lib/src/repositories/api_service_repository.dart b/packages/service_repository/lib/src/repositories/api_service_repository.dart index 2e85e9d..c8d3824 100644 --- a/packages/service_repository/lib/src/repositories/api_service_repository.dart +++ b/packages/service_repository/lib/src/repositories/api_service_repository.dart @@ -229,6 +229,19 @@ class ApiServiceRepository { return _parseList(await _get('/services/professional/calendar')); } + /// Another professional's booked slots, for a client browsing availability. + /// + /// [professionalRecordId] is the row id of the `professionals` table + /// (`ProfessionalEntity.recordId`), not the owning user's id. Clients used to + /// call the endpoint above, which returns the *caller's* own agenda, so every + /// slot looked free and two people could book the same hour. + Future> getPublicCalendar( + String professionalRecordId) async { + final body = await _get('/services/public-calendar/$professionalRecordId'); + final raw = body is Map ? (body['services'] as List? ?? []) : []; + return raw.map((e) => _fromApi(e as Map)).toList(); + } + Stream> getServicesHistoryForUser(String userId) { return _stream('/services/me/history'); }