import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncfusion_flutter_calendar/calendar.dart'; import '../../../extensions/dateTime.dart'; import '../../../state/app/infrastructure/loadableState/view/loadable_state_consumer.dart'; import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../state/app/modules/settings/bloc/settings_cubit.dart'; import 'custom_events/custom_event_edit_dialog.dart'; import 'custom_events/custom_events_view.dart'; import 'data/arbitrary_appointment.dart'; import 'data/timetable_appointment_factory.dart'; import 'details/appointment_details_dispatcher.dart'; import 'widgets/appointment_tile.dart'; import 'widgets/lesson_appointment_source.dart'; import 'widgets/special_regions_builder.dart'; import 'widgets/time_region_tile.dart'; enum _CalendarAction { addEvent, viewEvents } class Timetable extends StatefulWidget { const Timetable({super.key}); @override State createState() => _TimetableState(); } class _TimetableState extends State { final CalendarController _controller = CalendarController(); late Timer _highlightTicker; LessonAppointmentSource? _cachedSource; int? _lastDataVersion; @override void initState() { super.initState(); _controller.displayDate = _initialDisplayDate(); _highlightTicker = Timer.periodic(const Duration(seconds: 30), (_) { if (mounted) setState(() => _cachedSource = null); }); } @override void dispose() { _highlightTicker.cancel(); super.dispose(); } DateTime _initialDisplayDate() => DateTime.now().add(const Duration(days: 2)); void _jumpToToday() { _controller.displayDate = _initialDisplayDate(); } void _onAction(_CalendarAction action) { switch (action) { case _CalendarAction.addEvent: showDialog( context: context, builder: (_) => const CustomEventEditDialog(), barrierDismissible: false, ); case _CalendarAction.viewEvents: Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CustomEventsView())); } } LessonAppointmentSource _appointmentSource(TimetableState state) { if (_cachedSource != null && _lastDataVersion == state.dataVersion) { return _cachedSource!; } _lastDataVersion = state.dataVersion; final settings = context.read(); final appointments = TimetableAppointmentFactory( lessons: state.getAllKnownLessons().toList(), customEvents: state.customEvents?.events ?? const [], rooms: state.rooms!, subjects: state.subjects!, settings: settings.val().timetableSettings, colorScheme: Theme.of(context).colorScheme, now: DateTime.now(), ).build(); return _cachedSource = LessonAppointmentSource(appointments); } @override Widget build(BuildContext context) { final bloc = context.read(); return Scaffold( appBar: AppBar( title: const Text('Stunden & Vertretungsplan'), actions: [ IconButton(icon: const Icon(Icons.home_outlined), onPressed: _jumpToToday), PopupMenuButton<_CalendarAction>( icon: const Icon(Icons.edit_calendar_outlined), onSelected: _onAction, itemBuilder: (_) => const [ PopupMenuItem( value: _CalendarAction.addEvent, child: ListTile(title: Text('Kalendereintrag hinzufügen'), leading: Icon(Icons.add)), ), PopupMenuItem( value: _CalendarAction.viewEvents, child: ListTile( title: Text('Kalendereinträge anzeigen'), leading: Icon(Icons.perm_contact_calendar_outlined), ), ), ], ), ], ), body: LoadableStateConsumer( child: (state, _) => _calendar(state, bloc), ), ); } Widget _calendar(TimetableState state, TimetableBloc bloc) { if (!state.hasReferenceData) return const SizedBox.shrink(); return SfCalendar( timeZone: 'W. Europe Standard Time', view: CalendarView.workWeek, dataSource: _appointmentSource(state), maxDate: DateTime.now().add(const Duration(days: 7)).nextWeekday(DateTime.saturday), minDate: DateTime.now().subtract(const Duration(days: 14)).nextWeekday(DateTime.sunday), controller: _controller, onViewChanged: (details) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; bloc.changeWeek(details.visibleDates.first, details.visibleDates.last); }); }, onTap: (tap) { if (tap.appointments == null || tap.appointments!.isEmpty) return; AppointmentDetailsDispatcher.show(context, bloc, tap.appointments!.first); }, firstDayOfWeek: DateTime.monday, specialRegions: SpecialRegionsBuilder( holidays: state.schoolHolidays!, colorScheme: Theme.of(context).colorScheme, disabledColor: Theme.of(context).disabledColor, ).build(), timeSlotViewSettings: const TimeSlotViewSettings( startHour: 7.5, endHour: 16.5, timeInterval: Duration(minutes: 30), timeFormat: 'HH:mm', dayFormat: 'EE', timeIntervalHeight: 40, ), timeRegionBuilder: (_, details) => TimeRegionTile(details: details), appointmentBuilder: (_, details) => AppointmentTile( details: details, crossedOut: _isCrossedOut(details), ), headerHeight: 0, selectionDecoration: const BoxDecoration(), allowAppointmentResize: false, allowDragAndDrop: false, allowViewNavigation: false, ); } bool _isCrossedOut(CalendarAppointmentDetails details) { final appointment = details.appointments.first; final id = appointment.id; if (id is WebuntisAppointment) return id.lesson.code == 'cancelled'; return false; } }