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/lesson_period_schedule.dart'; import 'data/timetable_appointment_factory.dart'; import 'details/appointment_details_dispatcher.dart'; import 'widgets/custom_workweek_calendar.dart'; import 'widgets/special_regions_builder.dart'; enum _CalendarAction { addEvent, viewEvents } class Timetable extends StatefulWidget { const Timetable({super.key}); @override State createState() => _TimetableState(); } class _TimetableState extends State { final GlobalKey _calendarKey = GlobalKey(); List? _cachedAppointments; int? _lastDataVersion; DateTime _initialDisplayDate() => DateTime.now().add(const Duration(days: 2)); void _jumpToToday() { _calendarKey.currentState?.jumpToDate(_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())); } } List _appointments(TimetableState state) { if (_cachedAppointments != null && _lastDataVersion == state.dataVersion) { return _cachedAppointments!; } _lastDataVersion = state.dataVersion; final settings = context.read(); return _cachedAppointments = TimetableAppointmentFactory( lessons: state.getAllKnownLessons().toList(), customEvents: state.customEvents?.events ?? const [], rooms: state.rooms!, subjects: state.subjects!, settings: settings.val().timetableSettings, now: DateTime.now(), ).build(); } bool _isCrossedOut(Appointment appointment) { final id = appointment.id; if (id is WebuntisAppointment) return id.lesson.code == 'cancelled'; return false; } @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(); final schedule = LessonPeriodSchedule.fromState(state); final appointments = _appointments(state); final regions = SpecialRegionsBuilder( holidays: state.schoolHolidays!, schedule: schedule, colorScheme: Theme.of(context).colorScheme, disabledColor: Theme.of(context).disabledColor, ).build(); 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), onAppointmentTap: (apt) => AppointmentDetailsDispatcher.show(context, bloc, apt), onWeekChanged: (start, end) => bloc.changeWeek(start, end), isCrossedOut: _isCrossedOut, onCreateEvent: _onCreateEventAt, ); } void _onCreateEventAt(DateTime start, DateTime end) { showDialog( context: context, builder: (_) => CustomEventEditDialog(initialStart: start, initialEnd: end), barrierDismissible: false, ); } }