import 'package:flutter/material.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import '../../../../api/webuntis/queries/get_holidays/get_holidays_response.dart'; import '../../../../extensions/date_time.dart'; import '../data/calendar_layout.dart'; import '../data/lesson_period_schedule.dart'; import '../data/webuntis_time.dart'; import 'time_region_tile.dart'; class SpecialRegionsBuilder { final GetHolidaysResponse holidays; final LessonPeriodSchedule schedule; final ColorScheme colorScheme; final Color disabledColor; SpecialRegionsBuilder({ required this.holidays, required this.schedule, required this.colorScheme, required this.disabledColor, }); List build() { final rangeStart = DateTime.now() .subtract(const Duration(days: 14)) .nextWeekday(DateTime.monday); // Far enough out to cover comfortable scrolling without rebuilding; // Syncfusion only paints regions that intersect the visible window so // the extra entries don't hurt rendering cost. final rangeEnd = DateTime.now().add(const Duration(days: 180)); final holidayRegions = _buildHolidayRegions().toList(); final holidayDays = holidayRegions .map((r) => _dayKey(r.startTime)) .toSet(); // Materialise one TimeRegion per break per non-holiday day. Tried // `recurrenceRule: FREQ=DAILY` with `recurrenceExceptionDates` first, // but Syncfusion's recurrence exception matching is unreliable in // practice — the break overlay kept showing on holiday days. Generating // explicit per-day regions and just skipping holidays is robust. final breakPeriods = schedule.periods.where((p) => p.isBreak).toList(); final breakRegions = []; for ( var day = rangeStart; !day.isAfter(rangeEnd); day = day.add(const Duration(days: 1)) ) { if (holidayDays.contains(_dayKey(day))) continue; for (final p in breakPeriods) { final start = day.copyWith( hour: p.start.hour, minute: p.start.minute, ); breakRegions.add(_breakRegion(start, p.duration)); } } return [...holidayRegions, ...breakRegions]; } static String _dayKey(DateTime d) => '${d.year}-${d.month}-${d.day}'; Iterable _buildHolidayRegions() => holidays.result.expand(( holiday, ) { final startDay = WebuntisTime.parse(holiday.startDate, 0); final endDay = WebuntisTime.parse(holiday.endDate, 0); // Webuntis treats endDate inclusively (last day of the break) — the // `+ 1` covers single-day public holidays (where startDate == endDate) // and the final day of a multi-day vacation, both of which would // otherwise be skipped. final dayCount = endDay.difference(startDay).inDays + 1; final days = List.generate( dayCount, (i) => startDay.add(Duration(days: i)), ); final gridStartHour = kCalendarStartHour.floor(); final gridStartMinute = ((kCalendarStartHour - gridStartHour) * 60).round(); final gridEndHour = kCalendarEndHour.floor(); final gridEndMinute = ((kCalendarEndHour - gridEndHour) * 60).round(); return days.map( (day) => TimeRegion( startTime: day.copyWith(hour: gridStartHour, minute: gridStartMinute), endTime: day.copyWith(hour: gridEndHour, minute: gridEndMinute), text: '$kTimeRegionHolidayPrefix${holiday.name}', color: disabledColor.withAlpha(50), ), ); }); TimeRegion _breakRegion(DateTime start, Duration duration) => TimeRegion( startTime: start, endTime: start.add(duration), text: kTimeRegionCenterIcon, color: colorScheme.primary.withAlpha(50), iconData: Icons.restaurant, ); }