fix(agenda): one slot generator for professional and patient
The two calendars each built their own list of hours. The patient's
enforced a 3-hour booking lead time; the professional's did not, so at
15:00 an 08:00-18:00 agenda reported "3 libres" (15, 16, 17) that no
patient could actually take.
Both now go through SlotGenerator. The professional still sees the
near-term hours (blocking the next hour is legitimate) but they are
labelled "Sin reserva" and excluded from the "libres" count, so the
number on his agenda means what the patient sees.
Also in this pass:
- endOf() clamps the slot end at 23:59; a 120-minute slot booked at
23:00 was sending "25:00" to the backend.
- ScheduleEntity.copyWith can set an hour back to null, so returning a
day to jornada continua no longer keeps the split hours around.
- Removed the Firebase-era schedule map ('habilitado', 'range1Hour1')
together with the dead ProfessionalEntity.fromDocument that fed it.
- Settings loads guard on mounted and swallow failures instead of
calling setState after dispose.
- "Pedir cita" says what is missing instead of doing nothing.
Tests: 9 passing, including the clamp and the empty/inverted ranges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
3327220557
commit
0a8e5d11a2
@@ -4,9 +4,8 @@ import 'package:injector/injector.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:professional_repository/professional_repository.dart';
|
||||
import 'package:prosappco/blocs/professional_list_bloc/professional_list_bloc.dart';
|
||||
import 'package:prosappco/utils/time_of_day_extension.dart';
|
||||
import 'package:prosappco/utils/service_day.dart';
|
||||
import 'package:prosappco/utils/time_of_day_utils.dart';
|
||||
import 'package:prosappco/utils/slot_generator.dart';
|
||||
import 'package:service_repository/service_repository.dart';
|
||||
import 'package:setting_repository/setting_repository.dart';
|
||||
import 'package:table_calendar/table_calendar.dart';
|
||||
@@ -47,11 +46,13 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
settingRepository.getSettings().then(
|
||||
(value) => setState(() {
|
||||
settings = value;
|
||||
}),
|
||||
);
|
||||
settingRepository.getSettings().then((value) {
|
||||
if (!mounted) return;
|
||||
setState(() => settings = value);
|
||||
}).catchError((_) {
|
||||
// Settings only gate optional features; failing to read them must not
|
||||
// break the screen, but it must not setState after dispose either.
|
||||
});
|
||||
}
|
||||
|
||||
void _loadServices() {
|
||||
@@ -169,6 +170,23 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
||||
}
|
||||
|
||||
List<Widget> _rangesItems(ScheduleEntity? schedule) {
|
||||
if (_services == null && !_loadFailed) {
|
||||
// Treating "unknown" as busy is the safe default, but without this the
|
||||
// patient saw every hour in red and concluded the professional was full.
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 40),
|
||||
child: Center(
|
||||
child: Column(children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 12),
|
||||
Text('Consultando disponibilidad…',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey)),
|
||||
]),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
if (_loadFailed) {
|
||||
// Slots are hidden rather than shown as free: booking blind is how you
|
||||
// end up with two people in the same hour.
|
||||
@@ -212,55 +230,23 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
||||
];
|
||||
}
|
||||
|
||||
if (schedule.continuousDay) {
|
||||
if (schedule.range1Hour1 == null ||
|
||||
schedule.range2Hour2 == null ||
|
||||
schedule.range1Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
List<TimeOfDay> ranges = TimeOfDayUtils.genRanges(
|
||||
schedule.range1Hour1!,
|
||||
schedule.range2Hour2!,
|
||||
stepMinutes: widget.userProfessional.professionalInfo.slotDurationMinutes,
|
||||
);
|
||||
|
||||
return rangesItemList(ranges, _services, today);
|
||||
} else {
|
||||
if (schedule.range1Hour1 == null ||
|
||||
schedule.range1Hour2 == null ||
|
||||
schedule.range2Hour1 == null ||
|
||||
schedule.range2Hour2 == null ||
|
||||
schedule.range1Hour1!.compareTo(schedule.range1Hour2!) >= 0 ||
|
||||
schedule.range2Hour1!.compareTo(schedule.range2Hour2!) >= 0) {
|
||||
return [
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
List<TimeOfDay> ranges1 = TimeOfDayUtils.genRanges(
|
||||
schedule.range1Hour1!,
|
||||
schedule.range1Hour2!,
|
||||
stepMinutes: widget.userProfessional.professionalInfo.slotDurationMinutes,
|
||||
);
|
||||
List<TimeOfDay> ranges2 = TimeOfDayUtils.genRanges(
|
||||
schedule.range2Hour1!,
|
||||
schedule.range2Hour2!,
|
||||
stepMinutes: widget.userProfessional.professionalInfo.slotDurationMinutes,
|
||||
);
|
||||
|
||||
// One generator for both calendars: what the professional counts as free
|
||||
// in his agenda is exactly what shows up here.
|
||||
final ranges = SlotGenerator.forDay(
|
||||
schedule: schedule,
|
||||
day: today,
|
||||
slotDurationMinutes:
|
||||
widget.userProfessional.professionalInfo.slotDurationMinutes,
|
||||
);
|
||||
if (ranges.isEmpty) {
|
||||
return [
|
||||
...rangesItemList(ranges1, _services, today),
|
||||
...rangesItemList(ranges2, _services, today),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 20),
|
||||
child: Text("No hay horarios disponibles"),
|
||||
)
|
||||
];
|
||||
}
|
||||
return rangesItemList(ranges, _services, today);
|
||||
}
|
||||
|
||||
bool _isHora1Ocupada(
|
||||
@@ -293,7 +279,7 @@ class UserCalendarScreenState extends State<UserCalendarScreen> {
|
||||
);
|
||||
|
||||
if (selectedDateTime
|
||||
.isBefore(currentDateTime.add(const Duration(hours: 3)))) {
|
||||
.isBefore(currentDateTime.add(kBookingLeadTime))) {
|
||||
return Card(
|
||||
elevation: 4,
|
||||
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
|
||||
Reference in New Issue
Block a user