WIP (unstable): WebUntis-iCal-Abgleich für Vertretungen/Ausfälle

Erkennt Vertretungen, Ausfälle und Zusatzaufsichten aus dem persönlichen
WebUntis-iCal-Feed und schreibt sie automatisch als SubstitutionEntry.
Bekannter offener Bug: es tauchen weiterhin falsche Vertretungen für
Stunden auf, die real unverändert sind — wird in einem Folge-Commit
untersucht, deshalb vorerst auf diesem Branch statt main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 13:30:15 +02:00
co-authored by Claude Sonnet 5
parent e65a729f97
commit 4eb4d0a946
30 changed files with 3333 additions and 8 deletions
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Settings;
using System.Collections.ObjectModel;
@@ -62,6 +63,8 @@ public partial class TimetableViewModel : ObservableObject
private readonly SchoolYearService _schoolYear;
private readonly ISupervisionDutyRepository _supervisionDuties;
private readonly ISubstitutionEntryRepository _substitutions;
private readonly IUntisSlotMappingRepository _untisMappings;
private readonly WebUntisSettingsService _untisSettings;
public ObservableCollection<TimetableCellItem> Cells { get; } = [];
public ObservableCollection<WeekCellItem> WeekItems { get; } = [];
@@ -86,6 +89,13 @@ public partial class TimetableViewModel : ObservableObject
[ObservableProperty] private int _weekOffset;
public bool IsCurrentWeek => WeekOffset == 0;
// ── WebUntis-Abweichung (Nutzer-Feedback: "oder der Stundenplan gar nicht mehr passt") ──────
// Kein erneuter iCal-Abruf hier - vergleicht nur den lokal bereits bestätigten
// Zuordnungsstand (UntisSlotMapping, siehe UntisMappingReviewDialog) gegen die aktuellen
// TimetableSlots. Bleibt komplett verborgen, solange der Abgleich nicht aktiviert ist.
[ObservableProperty] private bool _hasUntisMismatch;
[ObservableProperty] private string _untisMismatchLabel = "";
public Func<TimetableCellItem, Task>? OnEditSlot { get; set; }
public Action<Guid>? OnNavigateToGroup { get; set; }
public Func<Task>? OnAddSubstitution { get; set; }
@@ -97,12 +107,14 @@ public partial class TimetableViewModel : ObservableObject
ISubjectRepository subjects, ILessonRepository lessons, IExamRepository exams,
ISchoolHolidayRepository schoolHolidays, SchoolCalendarSettingsService calendarSettings,
PublicHolidayService publicHolidays, SchoolYearService schoolYear,
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions)
ISupervisionDutyRepository supervisionDuties, ISubstitutionEntryRepository substitutions,
IUntisSlotMappingRepository untisMappings, WebUntisSettingsService untisSettings)
{
_slots = slots; _groups = groups; _subjects = subjects; _lessons = lessons; _exams = exams;
_schoolHolidays = schoolHolidays; _calendarSettings = calendarSettings;
_publicHolidays = publicHolidays; _schoolYear = schoolYear;
_supervisionDuties = supervisionDuties; _substitutions = substitutions;
_untisMappings = untisMappings; _untisSettings = untisSettings;
Load();
}
@@ -143,8 +155,33 @@ public partial class TimetableViewModel : ObservableObject
BuildToday(today);
BuildHoursWarnings();
BuildUpcomingExams(today);
LoadUntisMismatch();
}
private void LoadUntisMismatch()
{
if (!_untisSettings.Enabled || !_untisSettings.IsConfigured) { HasUntisMismatch = false; return; }
// CoveredPeriods statt nur PeriodNumber: bei einer von WebUntis zu einem Termin
// zusammengefassten Doppelstunde bestätigt eine einzige Zuordnung mehrere TimetableSlots
// auf einmal (siehe UntisSlotMapping.CoveredPeriods-Dokumentation).
var confirmedKeys = _untisMappings.GetAll()
.Where(m => m.Confirmed && m.Kind == SubstitutionKind.Lesson && m.GroupId is not null)
.SelectMany(m => (m.CoveredPeriods.Count > 0 ? m.CoveredPeriods : m.PeriodNumber is { } p ? [p] : [])
.Select(period => (m.Weekday, period, GroupId: m.GroupId!.Value)))
.ToHashSet();
var mismatchCount = _slots.GetAll()
.Count(s => !confirmedKeys.Contains((s.Weekday, s.PeriodNumber, s.GroupId)));
HasUntisMismatch = mismatchCount > 0;
UntisMismatchLabel = mismatchCount == 1
? "1 Stundenplan-Eintrag ohne bestätigte WebUntis-Zuordnung."
: $"{mismatchCount} Stundenplan-Einträge ohne bestätigte WebUntis-Zuordnung.";
}
[RelayCommand]
private void ReviewUntisMismatch() => OnNavigateToSettings?.Invoke(SettingsTab.WebUntis);
// ── Anstehende Klausurtermine (4.4.3) ────────────────────────────────────
/// <summary>