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
@@ -921,6 +921,41 @@
</ScrollViewer>
</ContentPage>
<!-- Tab: WebUntis-iCal-Abgleich (Nutzer-Feedback) -->
<ContentPage Header="Stundenplan-Abgleich">
<ScrollViewer>
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
<TextBlock Text="WebUntis-Stundenplan-Abgleich" FontSize="16" FontWeight="SemiBold"/>
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
Text="Ruft periodisch den persönlichen iCal-Export von WebUntis ab, um Vertretungen, Ausfälle und Raumänderungen automatisch zu erkennen und in den Stundenplan zu übernehmen. Der Link enthält ein Zugangs-Token und wird verschlüsselt gespeichert."/>
<StackPanel Spacing="8" IsVisible="{Binding !UntisIsConfigured}">
<StackPanel Spacing="4">
<TextBlock Text="iCal-URL" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding UntisIcalUrlInput}" PasswordChar="●"
PlaceholderText="https://…/WebUntis/ical_export?..."/>
</StackPanel>
<TextBlock Text="{Binding UntisUrlError}" Foreground="Red" FontSize="12"
IsVisible="{Binding UntisUrlError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<Button Content="Speichern und aktivieren" Command="{Binding UntisSaveUrlCommand}" HorizontalAlignment="Left"/>
</StackPanel>
<StackPanel Spacing="8" IsVisible="{Binding UntisIsConfigured}">
<TextBlock Text="iCal-URL hinterlegt (verschlüsselt gespeichert)." FontSize="13" FontWeight="SemiBold"/>
<TextBlock Text="{Binding UntisStatusDisplay}" FontSize="12" Opacity="0.7"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Jetzt abrufen" Command="{Binding UntisFetchNowCommand}"
IsEnabled="{Binding !UntisFetchBusy}"/>
<Button Content="Zuordnung prüfen…" Command="{Binding UntisReviewMappingCommand}"/>
<Button Content="Entfernen" Command="{Binding UntisRemoveCommand}"/>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</ContentPage>
<!-- Tab: Darstellung (12.4) -->
<ContentPage Header="Darstellung">
<ScrollViewer>
@@ -1,8 +1,12 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using LehrerApp.Core.Interfaces;
using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels;
using LehrerApp.Desktop.ViewModels.Planning;
using LehrerApp.Desktop.ViewModels.Settings;
using LehrerApp.Desktop.Views.Planning;
using LehrerApp.Desktop.Views.Shared;
using Microsoft.Extensions.DependencyInjection;
@@ -26,9 +30,26 @@ public partial class SettingsView : UserControl
vm.OnPickRecoveryFile = PickRecoveryFile;
vm.OnConfirmRecoveryRestore = ShowRecoveryRestoreConfirmDialog;
vm.OnThemeChanged = App.ApplyTheme;
vm.OnReviewUntisMapping = ShowUntisMappingReviewDialog;
}
}
private async Task ShowUntisMappingReviewDialog()
{
var owner = TopLevel.GetTopLevel(this) as Window;
var untisSync = App.Services.GetService<UntisSyncService>();
if (owner is null || untisSync is null) return;
var groups = App.Services.GetRequiredService<IGroupRepository>().GetAll();
var mappings = App.Services.GetRequiredService<LehrerApp.Core.Interfaces.IUntisSlotMappingRepository>();
var subjects = App.Services.GetRequiredService<LehrerApp.Core.Interfaces.ISubjectRepository>();
var dialog = new UntisMappingReviewDialog
{
DataContext = new UntisMappingReviewDialogViewModel(untisSync, mappings, groups, subjects),
};
await dialog.ShowDialog<bool>(owner);
}
private async Task<bool> SaveRecoveryFile(string content)
{
var topLevel = TopLevel.GetTopLevel(this);