migrated custom timetable events to Marianum-Connect and refactored push notification handling

This commit is contained in:
2026-07-12 14:31:48 +02:00
parent c444ed54a5
commit 91a6216f66
28 changed files with 474 additions and 212 deletions
@@ -63,6 +63,10 @@ class TalkSection extends StatelessWidget {
Haptics.selection();
final enabled = e ?? false;
settings.val(write: true).notificationSettings.enabled = enabled;
// Turning off does NOT unregister: the device stays subscribed so
// silent sync pushes keep arriving; the message handler and iOS
// NSE suppress only the visible notification (via the mirrored
// flag). Enabling (re-)registers and ensures the OS permission.
if (enabled) {
final messenger = ScaffoldMessenger.of(context);
unawaited(() async {
@@ -82,8 +86,6 @@ class TalkSection extends StatelessWidget {
);
}
}());
} else {
unawaited(PushRegistration().unregister());
}
},
),
@@ -57,6 +57,10 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
text: widget.existingEvent?.description ?? widget.initialDescription,
);
late String _rrule = widget.existingEvent?.rrule ?? '';
// Für neue Termine standardmäßig an, bestehende behalten ihren Wert. Migrierte
// mhsl-Termine tragen ausgeschaltet (Modell-Default), da sie nie ein Serien-
// Ferien-Flag hatten.
late bool _skipHolidays = widget.existingEvent?.skipHolidays ?? true;
late CustomTimetableColors _color = CustomTimetableColors.values.firstWhere(
(e) => e.name == widget.existingEvent?.color,
orElse: () => TimetableColors.defaultColor,
@@ -150,6 +154,7 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
endDate: endDate,
color: _color.name,
rrule: _rrule,
skipHolidays: _rrule.trim().isEmpty ? false : _skipHolidays,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
@@ -297,6 +302,18 @@ class _CustomEventEditDialogState extends State<CustomEventEditDialog> {
},
),
),
if (_rrule.trim().isNotEmpty) ...[
const Divider(),
SwitchListTile(
secondary: const Icon(Icons.beach_access_outlined),
title: const Text('In den Ferien ausblenden'),
subtitle: const Text(
'An Ferientagen wird der Termin nicht angezeigt',
),
value: _skipHolidays,
onChanged: (v) => setState(() => _skipHolidays = v),
),
],
],
),
),
@@ -1,5 +1,6 @@
import 'package:syncfusion_flutter_calendar/calendar.dart';
import '../../../../api/marianumconnect/models/mc_holiday.dart';
import '../../../../api/marianumconnect/queries/timetable_get_subjects/timetable_get_subjects_response.dart';
import '../../../../api/marianumconnect/queries/timetable_get_week/timetable_get_week_response.dart';
import '../../../../api/mhsl/custom_timetable_event/custom_timetable_event.dart';
@@ -16,6 +17,7 @@ class TimetableAppointmentFactory {
final List<McTimetableEntry> lessons;
final List<CustomTimetableEvent> customEvents;
final List<McSubject> subjects;
final List<McHoliday> holidays;
final TimetableSettings settings;
final DateTime now;
@@ -25,6 +27,7 @@ class TimetableAppointmentFactory {
required this.subjects,
required this.settings,
required this.now,
this.holidays = const [],
});
List<Appointment> build() {
@@ -98,6 +101,14 @@ class TimetableAppointmentFactory {
),
)
.toList();
// Ferien ausblenden ist nur für Serien sinnvoll; Syncfusion matcht
// Ausnahmen über recurrenceExceptionDates. Überzählige Ferientage, an denen
// gar keine Occurrence liegt, ignoriert es folgenlos — daher genügt es, für
// jeden Ferientag ein Ausnahmedatum zur Terminzeit einzustreuen, ohne die
// RRULE selbst expandieren zu müssen.
if (event.skipHolidays && parsed.rule.isNotEmpty) {
exceptionDates.addAll(_holidayExceptionDates(event));
}
return Appointment(
id: CustomAppointment(event),
startTime: event.startDate,
@@ -128,6 +139,36 @@ class TimetableAppointmentFactory {
);
}
List<DateTime> _holidayExceptionDates(CustomTimetableEvent event) {
final result = <DateTime>[];
for (final holiday in holidays) {
var day = DateTime(
holiday.startDate.year,
holiday.startDate.month,
holiday.startDate.day,
);
final last = DateTime(
holiday.endDate.year,
holiday.endDate.month,
holiday.endDate.day,
);
while (!day.isAfter(last)) {
result.add(
DateTime(
day.year,
day.month,
day.day,
event.startDate.hour,
event.startDate.minute,
event.startDate.second,
),
);
day = DateTime(day.year, day.month, day.day + 1);
}
}
return result;
}
/// All-day convention: a `CustomTimetableEvent` is treated as all-day when
/// its `startDate` and `endDate` both land on midnight of the same day.
/// Keeps the backend schema unchanged — the editor stores all-day events as
@@ -79,6 +79,7 @@ class TimetableCalendarViewState extends State<TimetableCalendarView> {
lessons: state.getAllKnownLessons().toList(),
customEvents: widget.customEvents,
subjects: state.subjects?.result ?? const [],
holidays: state.schoolHolidays?.result ?? const [],
settings: timetableSettings,
now: DateTime.now(),
).build();