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
@@ -36,8 +36,9 @@ public enum SettingsTab
Privacy = 10,
Sync = 11,
Ai = 12,
Appearance = 13,
Trash = 14,
WebUntis = 13,
Appearance = 14,
Trash = 15,
}
// ── Haupt-ViewModel ───────────────────────────────────────────────────────────
@@ -191,6 +192,16 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] private bool _aiIsLoggedIn;
[ObservableProperty] private string _aiBalanceDisplay = "";
// ── WebUntis-iCal-Abgleich (Nutzer-Feedback) ──────────────────────────────
[ObservableProperty] private bool _untisEnabled;
[ObservableProperty] private bool _untisIsConfigured;
[ObservableProperty] private string _untisIcalUrlInput = "";
[ObservableProperty] private string _untisUrlError = "";
[ObservableProperty] private string _untisStatusDisplay = "";
[ObservableProperty] private bool _untisFetchBusy;
public Func<Task>? OnReviewUntisMapping { get; set; }
// ── Synchronisation (Kapitel 10) ──────────────────────────────────────────
[ObservableProperty] private string _syncServerUrl = "";
@@ -268,6 +279,8 @@ public partial class SettingsViewModel : ObservableObject
private readonly ISupervisionDutyRepository _supervisionDuties;
private readonly AiSettingsService _aiSettings;
private readonly AiPlanningService _aiPlanning;
private readonly WebUntisSettingsService _untisSettings;
private readonly UntisSyncService? _untisSync;
private readonly SyncSettingsService _syncSettings;
private readonly SyncAuthService _syncAuth;
private readonly EventQueue _eventQueue;
@@ -289,10 +302,12 @@ public partial class SettingsViewModel : ObservableObject
SchoolCalendarSettingsService calendarSettings, PeriodScheduleService periodSchedule,
ISupervisionDutyRepository supervisionDuties, LetterTemplateService letterTemplates,
AiSettingsService aiSettings, AiPlanningService aiPlanning,
WebUntisSettingsService untisSettings,
SyncSettingsService syncSettings, SyncAuthService syncAuth, EventQueue eventQueue,
AppLogger logger, SyncKeyStatus syncKeyStatus, SyncKeyRecoveryService syncKeyRecovery,
AppearanceSettingsService appearance, TrashViewModel trashTab,
SnapshotService? snapshotService = null, SyncEngine? syncEngine = null)
SnapshotService? snapshotService = null, SyncEngine? syncEngine = null,
UntisSyncService? untisSync = null)
{
_logger = logger;
_syncKeyRecovery = syncKeyRecovery;
@@ -320,6 +335,8 @@ public partial class SettingsViewModel : ObservableObject
_letterTemplates = letterTemplates;
_aiSettings = aiSettings;
_aiPlanning = aiPlanning;
_untisSettings = untisSettings;
_untisSync = untisSync;
_syncSettings = syncSettings;
_syncAuth = syncAuth;
_eventQueue = eventQueue;
@@ -342,6 +359,7 @@ public partial class SettingsViewModel : ObservableObject
LoadSupervisionDuties();
LoadLetterTemplates();
LoadAiSettings();
LoadUntisSettings();
LoadSyncSettings();
LoadSyncConflicts();
}
@@ -485,6 +503,60 @@ public partial class SettingsViewModel : ObservableObject
AiBalanceDisplay = "";
}
// ── WebUntis-iCal-Abgleich: Laden / Speichern / Entfernen / Jetzt abrufen ────
//
// Wie bei Sync deckt ein Neustart das Registrieren von UntisSyncService ab
// (AppBootstrapper registriert es nur einmalig beim Start, wenn URL+Enabled vorliegen).
private void LoadUntisSettings()
{
UntisEnabled = _untisSettings.Enabled;
UntisIsConfigured = _untisSettings.IsConfigured;
UntisStatusDisplay = _untisSettings.LastSyncAt is { } at
? $"Letzter Abgleich: {at.ToLocalTime():dd.MM.yyyy HH:mm} — {_untisSettings.LastSyncStatus}"
: "Noch kein Abgleich durchgeführt.";
}
[RelayCommand]
private void UntisSaveUrl()
{
UntisUrlError = "";
if (string.IsNullOrWhiteSpace(UntisIcalUrlInput)) { UntisUrlError = "iCal-URL erforderlich."; return; }
if (!Uri.TryCreate(UntisIcalUrlInput, UriKind.Absolute, out _)) { UntisUrlError = "Ungültige URL."; return; }
_untisSettings.SetIcalUrl(UntisIcalUrlInput.Trim());
_untisSettings.SetEnabled(true);
UntisIcalUrlInput = "";
AppBootstrapper.RestartApplication();
}
[RelayCommand]
private void UntisRemove()
{
_untisSettings.ClearIcalUrl();
LoadUntisSettings();
AppBootstrapper.RestartApplication();
}
[RelayCommand]
private async Task UntisFetchNow()
{
if (_untisSync is null) { UntisStatusDisplay = "Abgleich nicht aktiv — App neu starten."; return; }
UntisFetchBusy = true;
try
{
await _untisSync.PollAsync();
LoadUntisSettings();
}
finally { UntisFetchBusy = false; }
}
[RelayCommand]
private async Task UntisReviewMapping()
{
if (OnReviewUntisMapping is not null) await OnReviewUntisMapping();
}
// ── Synchronisation: Laden / Anmelden / Abmelden / Verbindungstest ───────
//
// Server-URL, Zugangsdaten und Token werden erst nach erfolgreichem Login zusammen