Files
Client/lib/view/pages/timetable/widgets/special_regions_builder.dart
T

124 lines
4.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/marianumconnect/queries/timetable_get_holidays/timetable_get_holidays_response.dart';
import '../../../../extensions/date_time.dart';
import '../data/calendar_layout.dart';
import '../data/lesson_period_schedule.dart';
import 'time_region_tile.dart';
class SpecialRegionsBuilder {
final TimetableGetHolidaysResponse holidays;
final LessonPeriodSchedule schedule;
final ColorScheme colorScheme;
final Color disabledColor;
SpecialRegionsBuilder({
required this.holidays,
required this.schedule,
required this.colorScheme,
required this.disabledColor,
});
List<TimeRegion> build() {
final rangeStart = DateTime.now()
.subtractDays(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().addDays(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 = <TimeRegion>[];
for (var day = rangeStart; !day.isAfter(rangeEnd); day = day.addDays(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<TimeRegion> _buildHolidayRegions() {
// Multiple holiday entries can cover the same day (e.g. a public holiday
// falling inside a vacation period). Collapse them per-day so we emit
// exactly one TimeRegion per day and the overlapping labels don't render
// on top of each other.
final byDay = <String, _HolidayDay>{};
for (final holiday in holidays.result) {
final startDay = DateTime(
holiday.startDate.year,
holiday.startDate.month,
holiday.startDate.day,
);
final endDay = DateTime(
holiday.endDate.year,
holiday.endDate.month,
holiday.endDate.day,
);
// 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;
for (var i = 0; i < dayCount; i++) {
final day = startDay.addDays(i);
final key = _dayKey(day);
byDay.putIfAbsent(key, () => _HolidayDay(day, [])).names.add(
holiday.shortName,
);
}
}
final gridStartHour = kCalendarStartHour.floor();
final gridStartMinute = ((kCalendarStartHour - gridStartHour) * 60).round();
final gridEndHour = kCalendarEndHour.floor();
final gridEndMinute = ((kCalendarEndHour - gridEndHour) * 60).round();
return byDay.values.map(
(entry) => TimeRegion(
startTime: entry.day.copyWith(
hour: gridStartHour,
minute: gridStartMinute,
),
endTime: entry.day.copyWith(
hour: gridEndHour,
minute: gridEndMinute,
),
text: '$kTimeRegionHolidayPrefix${entry.names.join(" / ")}',
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,
);
}
class _HolidayDay {
final DateTime day;
final List<String> names;
_HolidayDay(this.day, this.names);
}