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>
121 lines
3.9 KiB
Dart
121 lines
3.9 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:professional_repository/professional_repository.dart';
|
|
import 'package:prosappco/utils/slot_generator.dart';
|
|
import 'package:prosappco/utils/time_of_day_extension.dart';
|
|
import 'package:prosappco/utils/time_of_day_utils.dart';
|
|
|
|
void main() {
|
|
_slotGeneratorTests();
|
|
group('TimeOfDayExtension.add', () {
|
|
test('carries minutes into hours', () {
|
|
expect(const TimeOfDay(hour: 8, minute: 45).add(minute: 45),
|
|
const TimeOfDay(hour: 9, minute: 30));
|
|
});
|
|
|
|
test('does not wrap past midnight, so range loops still terminate', () {
|
|
expect(const TimeOfDay(hour: 23, minute: 30).add(minute: 45).hour, 24);
|
|
});
|
|
});
|
|
|
|
group('TimeOfDayUtils.genRanges', () {
|
|
// Regression: a minute-based step used to leave the hour untouched
|
|
// ("8:90"), so isBefore() never became false and this looped forever,
|
|
// freezing the calendar screen.
|
|
test('minute-based step terminates and lands on real times', () {
|
|
final slots = TimeOfDayUtils.genRanges(
|
|
const TimeOfDay(hour: 8, minute: 0),
|
|
const TimeOfDay(hour: 18, minute: 0),
|
|
stepMinutes: 45,
|
|
);
|
|
|
|
expect(slots.length, 14);
|
|
expect(slots.first, const TimeOfDay(hour: 8, minute: 0));
|
|
expect(slots[1], const TimeOfDay(hour: 8, minute: 45));
|
|
expect(slots[2], const TimeOfDay(hour: 9, minute: 30));
|
|
expect(slots.last, const TimeOfDay(hour: 17, minute: 45));
|
|
expect(slots.every((t) => t.minute < 60), isTrue);
|
|
});
|
|
|
|
test('two-hour step keeps the historical behaviour', () {
|
|
final slots = TimeOfDayUtils.genRanges(
|
|
const TimeOfDay(hour: 8, minute: 0),
|
|
const TimeOfDay(hour: 18, minute: 0),
|
|
stepMinutes: 120,
|
|
);
|
|
|
|
expect(slots.map((t) => t.hour).toList(), [8, 10, 12, 14, 16]);
|
|
});
|
|
|
|
test('end before start yields no slots', () {
|
|
final slots = TimeOfDayUtils.genRanges(
|
|
const TimeOfDay(hour: 18, minute: 0),
|
|
const TimeOfDay(hour: 8, minute: 0),
|
|
stepMinutes: 45,
|
|
);
|
|
|
|
expect(slots, isEmpty);
|
|
});
|
|
});
|
|
}
|
|
|
|
void _slotGeneratorTests() {
|
|
group('SlotGenerator', () {
|
|
final monday = DateTime(2026, 8, 24); // lunes
|
|
|
|
ScheduleEntity continuous(TimeOfDay a, TimeOfDay b) => ScheduleEntity(
|
|
enabled: true,
|
|
continuousDay: true,
|
|
range1Hour1: a,
|
|
range1Hour2: null,
|
|
range2Hour1: null,
|
|
range2Hour2: b,
|
|
);
|
|
|
|
test('a disabled day offers nothing', () {
|
|
expect(
|
|
SlotGenerator.forDay(
|
|
schedule: continuous(
|
|
const TimeOfDay(hour: 8, minute: 0),
|
|
const TimeOfDay(hour: 18, minute: 0))
|
|
.copyWith(enabled: false),
|
|
day: monday,
|
|
slotDurationMinutes: 60,
|
|
),
|
|
isEmpty,
|
|
);
|
|
});
|
|
|
|
test('an inverted range offers nothing instead of looping forever', () {
|
|
expect(
|
|
SlotGenerator.forDay(
|
|
schedule: continuous(const TimeOfDay(hour: 18, minute: 0),
|
|
const TimeOfDay(hour: 8, minute: 0)),
|
|
day: monday,
|
|
slotDurationMinutes: 60,
|
|
),
|
|
isEmpty,
|
|
);
|
|
});
|
|
|
|
test('notBefore drops the slots already past', () {
|
|
final slots = SlotGenerator.forDay(
|
|
schedule: continuous(const TimeOfDay(hour: 8, minute: 0),
|
|
const TimeOfDay(hour: 18, minute: 0)),
|
|
day: monday,
|
|
slotDurationMinutes: 60,
|
|
notBefore: DateTime(2026, 8, 24, 15, 0),
|
|
);
|
|
expect(slots.first, const TimeOfDay(hour: 15, minute: 0));
|
|
expect(slots.length, 3); // 15, 16, 17
|
|
});
|
|
|
|
test('endOf clamps instead of producing 25:00', () {
|
|
expect(SlotGenerator.endOf(const TimeOfDay(hour: 23, minute: 0), 120),
|
|
const TimeOfDay(hour: 23, minute: 59));
|
|
expect(SlotGenerator.endOf(const TimeOfDay(hour: 9, minute: 30), 45),
|
|
const TimeOfDay(hour: 10, minute: 15));
|
|
});
|
|
});
|
|
}
|