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:
co-authored by
Claude Opus 5
parent
389f876cfa
commit
7930890d09
@@ -31,6 +31,7 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
|||||||
late int numDay;
|
late int numDay;
|
||||||
|
|
||||||
List<ServiceEntity>? _services;
|
List<ServiceEntity>? _services;
|
||||||
|
bool _loadFailed = false;
|
||||||
|
|
||||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||||
|
|
||||||
@@ -54,13 +55,17 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _loadServices() {
|
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
|
serviceRepository
|
||||||
.getServicesForProfessionalforCalendar(
|
.getPublicCalendar(widget.userProfessional.professionalInfo.recordId)
|
||||||
widget.userProfessional.myUser.id)
|
|
||||||
.then((services) {
|
.then((services) {
|
||||||
setState(() {
|
if (!mounted) return;
|
||||||
_services = services;
|
setState(() => _services = services);
|
||||||
});
|
}).catchError((e) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _loadFailed = true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +169,32 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _rangesItems(ScheduleEntity? schedule) {
|
List<Widget> _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) {
|
if (schedule == null) {
|
||||||
return [
|
return [
|
||||||
const Padding(
|
const Padding(
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ class ProfessionalEntity extends Equatable {
|
|||||||
final PaymentMethodEntity paymentMethods;
|
final PaymentMethodEntity paymentMethods;
|
||||||
final int slotDurationMinutes;
|
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({
|
const ProfessionalEntity({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.identification,
|
required this.identification,
|
||||||
@@ -40,6 +45,7 @@ class ProfessionalEntity extends Equatable {
|
|||||||
required this.schedules,
|
required this.schedules,
|
||||||
required this.paymentMethods,
|
required this.paymentMethods,
|
||||||
this.slotDurationMinutes = 120,
|
this.slotDurationMinutes = 120,
|
||||||
|
this.recordId = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
static ProfessionalEntity fromDocument(Map<String, dynamic> doc) {
|
static ProfessionalEntity fromDocument(Map<String, dynamic> doc) {
|
||||||
@@ -64,6 +70,7 @@ class ProfessionalEntity extends Equatable {
|
|||||||
paymentMethods: PaymentMethodEntity.fromDocument(doc['payment_methods']),
|
paymentMethods: PaymentMethodEntity.fromDocument(doc['payment_methods']),
|
||||||
slotDurationMinutes:
|
slotDurationMinutes:
|
||||||
(doc['slot_duration_minutes'] as num?)?.toInt() ?? 120,
|
(doc['slot_duration_minutes'] as num?)?.toInt() ?? 120,
|
||||||
|
recordId: doc['id']?.toString() ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +93,7 @@ class ProfessionalEntity extends Equatable {
|
|||||||
Schedules? schedules,
|
Schedules? schedules,
|
||||||
PaymentMethodEntity? paymentMethods,
|
PaymentMethodEntity? paymentMethods,
|
||||||
int? slotDurationMinutes,
|
int? slotDurationMinutes,
|
||||||
|
String? recordId,
|
||||||
}) {
|
}) {
|
||||||
return ProfessionalEntity(
|
return ProfessionalEntity(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -108,6 +116,7 @@ class ProfessionalEntity extends Equatable {
|
|||||||
schedules: schedules ?? this.schedules,
|
schedules: schedules ?? this.schedules,
|
||||||
paymentMethods: paymentMethods ?? this.paymentMethods,
|
paymentMethods: paymentMethods ?? this.paymentMethods,
|
||||||
slotDurationMinutes: slotDurationMinutes ?? this.slotDurationMinutes,
|
slotDurationMinutes: slotDurationMinutes ?? this.slotDurationMinutes,
|
||||||
|
recordId: recordId ?? this.recordId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +163,7 @@ class ProfessionalEntity extends Equatable {
|
|||||||
schedules,
|
schedules,
|
||||||
paymentMethods,
|
paymentMethods,
|
||||||
slotDurationMinutes,
|
slotDurationMinutes,
|
||||||
|
recordId,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -195,6 +195,7 @@ class ApiProfessionalRepository {
|
|||||||
: PaymentMethodEntity.empty,
|
: PaymentMethodEntity.empty,
|
||||||
slotDurationMinutes:
|
slotDurationMinutes:
|
||||||
(json['slot_duration_minutes'] as num?)?.toInt() ?? 120,
|
(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'));
|
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) {
|
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
|
||||||
return _stream('/services/me/history');
|
return _stream('/services/me/history');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user