From 7930890d09d64e6fdbf88bbc4c0bed499a5a599e Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:29:31 -0500 Subject: [PATCH] fix: clients read the professional's real agenda, not their own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /services/public-calendar/{professionalId} exists after all — the web has been using it all along. My earlier probes missed it because they guessed the URL shape, and because it is keyed by the professionals row id rather than the owning user id, which ProfessionalEntity was discarding. Adds ProfessionalEntity.recordId and points the client calendar at the public endpoint. Until now a client browsing a professional's availability was served their own agenda, so every slot looked free and two people could book the same hour. A failed load now hides the slots and offers a retry instead of showing them as available. Co-Authored-By: Claude Opus 5 --- lib/screens/user/user_calendar_screen.dart | 41 ++++++++++++++++--- .../lib/src/entities/professional_entity.dart | 10 +++++ .../api_professional_repository.dart | 1 + .../repositories/api_service_repository.dart | 13 ++++++ 4 files changed, 60 insertions(+), 5 deletions(-) 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'); }