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'; import '../../../state/app/modules/timetable/bloc/timetable_bloc.dart'; import '../../../state/app/modules/timetable/bloc/timetable_state.dart'; import '../../../storage/timetable_settings.dart'; 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 '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; TimetableSettings? _lastTimetableSettings; 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: AppRoutes.openCustomEvents(context); } } List _appointments(TimetableState state) { final timetableSettings = context .watch() .val() .timetableSettings; if (_cachedAppointments != null && _lastDataVersion == state.dataVersion && identical(_lastTimetableSettings, timetableSettings)) { return _cachedAppointments!; } _lastDataVersion = state.dataVersion; _lastTimetableSettings = timetableSettings; return _cachedAppointments = TimetableAppointmentFactory( lessons: state.getAllKnownLessons().toList(), customEvents: state.customEvents?.events ?? const [], rooms: state.rooms!, subjects: state.subjects!, settings: timetableSettings, now: DateTime.now(), ).build(); } bool _isCrossedOut(Appointment appointment) { final id = appointment.id; if (id is WebuntisAppointment) return id.lesson.code == 'cancelled'; return false; } bool _isOnInitialWeek(TimetableState state) { final target = _initialDisplayDate(); final targetMonday = target.subtract(Duration(days: target.weekday - 1)); final mondayOnly = DateTime( targetMonday.year, targetMonday.month, targetMonday.day, ); return state.startDate == mondayOnly; } @override Widget build(BuildContext context) { final bloc = context.read(); final loadableState = context.watch().state; final innerState = loadableState.data; final atToday = innerState != null && _isOnInitialWeek(innerState); return Scaffold( appBar: AppBar( title: const Text('Stunden & Vertretungsplan'), actions: [ IconButton( icon: const Icon(Icons.home_outlined), onPressed: atToday ? null : _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, ); } }