import 'package:flutter/material.dart'; import 'package:professional_repository/professional_repository.dart'; import 'package:prosappco/utils/time_of_day_extension.dart'; import 'package:prosappco/utils/time_of_day_utils.dart'; /// Minimum notice a patient has to give before an appointment. /// /// It used to live only in the patient's calendar, so the professional's own /// agenda generated a different list: at 15:00 a professional working /// 08:00-18:00 with hourly slots saw "3 libres" (15:00, 16:00, 17:00) while /// the patient could only book 18:00. Both screens now go through /// [SlotGenerator], so the two lists always agree. const Duration kBookingLeadTime = Duration(hours: 3); /// The single source of truth for "which times exist on this day". class SlotGenerator { const SlotGenerator._(); /// Times offered on [day] for [schedule]. /// /// [notBefore] drops the slots that are already in the past (or inside the /// lead time). Pass it as `DateTime.now().add(kBookingLeadTime)` for the /// patient flow and `DateTime.now()` for the professional's own agenda, /// where the professional may still block the next hour. static List forDay({ required ScheduleEntity? schedule, required DateTime day, required int slotDurationMinutes, DateTime? notBefore, }) { final raw = _rawSlots(schedule, slotDurationMinutes); if (notBefore == null) return raw; return raw.where((t) { final at = DateTime(day.year, day.month, day.day, t.hour, t.minute); return !at.isBefore(notBefore); }).toList(); } /// Where a slot starting at [start] ends. /// /// Clamped to 23:59: a 120-minute slot starting at 23:00 used to produce /// "25:00", which is not a real time and the backend stored verbatim. static TimeOfDay endOf(TimeOfDay start, int slotDurationMinutes) { final total = start.hour * 60 + start.minute + slotDurationMinutes; if (total >= 24 * 60) return const TimeOfDay(hour: 23, minute: 59); return TimeOfDay(hour: total ~/ 60, minute: total % 60); } static List _rawSlots(ScheduleEntity? s, int stepMinutes) { if (s == null || !s.enabled) return []; if (s.continuousDay) { if (s.range1Hour1 == null || s.range2Hour2 == null) return []; if (s.range1Hour1!.compareTo(s.range2Hour2!) >= 0) return []; return TimeOfDayUtils.genRanges(s.range1Hour1!, s.range2Hour2!, stepMinutes: stepMinutes); } if (s.range1Hour1 == null || s.range1Hour2 == null || s.range2Hour1 == null || s.range2Hour2 == null) { return []; } if (s.range1Hour1!.compareTo(s.range1Hour2!) >= 0 || s.range2Hour1!.compareTo(s.range2Hour2!) >= 0) { return []; } return [ ...TimeOfDayUtils.genRanges(s.range1Hour1!, s.range1Hour2!, stepMinutes: stepMinutes), ...TimeOfDayUtils.genRanges(s.range2Hour1!, s.range2Hour2!, stepMinutes: stepMinutes), ]; } }