fix: clients read the professional's real agenda, not their own

/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 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-08-24 21:29:31 -05:00
co-authored by Claude Opus 5
parent 389f876cfa
commit 7930890d09
4 changed files with 60 additions and 5 deletions
@@ -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<String, dynamic> 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
@@ -195,6 +195,7 @@ class ApiProfessionalRepository {
: PaymentMethodEntity.empty,
slotDurationMinutes:
(json['slot_duration_minutes'] as num?)?.toInt() ?? 120,
recordId: json['id']?.toString() ?? '',
);
}
@@ -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<List<ServiceEntity>> getPublicCalendar(
String professionalRecordId) async {
final body = await _get('/services/public-calendar/$professionalRecordId');
final raw = body is Map ? (body['services'] as List? ?? []) : <dynamic>[];
return raw.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
}
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
return _stream('/services/me/history');
}