implemented current schoolyear API and dynamic timetable scroll boundaries, added handling for out-of-range errors to narrow accessible dates, optimized holiday region rendering by collapsing overlaps, and refined holiday tile UI

This commit is contained in:
2026-05-14 15:07:48 +02:00
parent 2cb8321d07
commit 582eff8750
13 changed files with 368 additions and 51 deletions
+48 -7
View File
@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../extensions/date_time.dart';
import '../../../routing/app_routes.dart';
import '../../../state/app/infrastructure/loadable_state/view/loadable_state_consumer.dart';
import '../../../state/app/modules/settings/bloc/settings_cubit.dart';
@@ -13,6 +12,7 @@ import 'custom_events/custom_event_edit_dialog.dart';
import 'data/arbitrary_appointment.dart';
import 'data/lesson_period_schedule.dart';
import 'data/timetable_appointment_factory.dart';
import 'data/webuntis_time.dart';
import 'details/appointment_details_dispatcher.dart';
import 'widgets/custom_workweek_calendar.dart';
import 'widgets/special_regions_builder.dart';
@@ -147,18 +147,20 @@ class _TimetableState extends State<Timetable> {
disabledColor: Theme.of(context).disabledColor,
).build();
// Scroll bounds follow the Webuntis school-year API: the calendar lets
// the user navigate every week the server has data for. A two-week
// fallback is used only while the school-year payload hasn't loaded yet
// (first launch / offline), so the calendar still mounts.
final (minDate, maxDate) = _scrollBounds(state);
return CustomWorkWeekCalendar(
key: _calendarKey,
schedule: schedule,
appointments: appointments,
timeRegions: regions,
initialDate: _initialDisplayDate(),
minDate: DateTime.now()
.subtract(const Duration(days: 14))
.nextWeekday(DateTime.sunday),
maxDate: DateTime.now()
.add(const Duration(days: 7))
.nextWeekday(DateTime.saturday),
minDate: minDate,
maxDate: maxDate,
onAppointmentTap: (apt) =>
AppointmentDetailsDispatcher.show(context, bloc, apt),
onWeekChanged: (start, end) => bloc.changeWeek(start, end),
@@ -175,4 +177,43 @@ class _TimetableState extends State<Timetable> {
barrierDismissible: false,
);
}
/// Returns the (minDate, maxDate) the user is allowed to scroll between.
/// Starts from the Webuntis school year (or a tight window when that
/// hasn't loaded yet) and tightens by anything the bloc has learned
/// from `-7004 no allowed date` errors during scroll — so the user
/// can't slide off into territory Webuntis would refuse anyway.
///
/// minDate is snapped *forward* to the next Monday because the calendar's
/// internal `_mondayOf()` would otherwise pull a mid-week minDate back
/// into the just-rejected week. maxDate is passed through unsnapped —
/// `_mondayOf()` correctly walks back to the Monday of its own week,
/// which is the last fully-allowed week.
(DateTime, DateTime) _scrollBounds(TimetableState state) {
final year = state.schoolyear?.result;
final DateTime baseMin;
final DateTime baseMax;
if (year != null) {
baseMin = WebuntisTime.parse(year.startDate, 0);
baseMax = WebuntisTime.parse(year.endDate, 0);
} else {
final now = DateTime.now();
baseMin = now.subtract(const Duration(days: 14));
baseMax = now.add(const Duration(days: 7));
}
final effectiveMin = state.accessibleStartDate != null
? (state.accessibleStartDate!.isAfter(baseMin)
? state.accessibleStartDate!
: baseMin)
: baseMin;
final effectiveMax = state.accessibleEndDate != null
? (state.accessibleEndDate!.isBefore(baseMax)
? state.accessibleEndDate!
: baseMax)
: baseMax;
final daysToMonday =
(DateTime.monday - effectiveMin.weekday) % DateTime.daysPerWeek;
final mondayMin = effectiveMin.add(Duration(days: daysToMonday));
return (mondayMin, effectiveMax);
}
}