feat: Untis-Hub-Kadenzkopplung, anpassbarer Statusvorschlag + KI-/MCP-Unterstuetzung beim Fehlzeitenabgleich (Nutzer-Feedback)
CI / build-and-test (push) Canceled after 0s

- UntisHubService.RecordRun: ein abgeschlossener Langzeit-Fehlzeitenabgleich schliesst die
  kurzfristige Kadenz derselben Gruppe automatisch mit ab (nicht umgekehrt).
- Fehlzeitenabgleich-Dialog: neue "Uebernahme als"-ComboBox statt starrem Zielstatus, vorbelegt
  mit dem berechneten Vorschlag, aber frei aenderbar.
- Neuer ai-backend-Endpunkt untis-status.php + AiPlanningService.RequestUntisStatusSuggestionsAsync:
  gebuendelter, anonymisierter KI-Statusvorschlag (nur Positions-Id + Rohsignale, nie Name/Klasse/
  Datum), mit hartem Id-Mengen-Abgleich gegen Verwechslung.
- Neue MCP-Tools (UntisComparisonTools): get_untis_hub_status, get_untis_absence_rows/
  apply_untis_absence_status (anonymer Weg ueber ENr-Zuordnung) sowie
  get_named_untis_absence_pattern als bewusste, eng begrenzte Ausnahme (Name+Fehlzeiten fuer
  explizit angegebene Schueler-IDs, mit Bestaetigung ohne Sitzungsfreigabe - dafuer
  IMcpConfirmationService.ConfirmAsync um allowSessionTrust erweitert).
- MapStatus/ENr-Zuordnung aus dem ViewModel in das neue, geteilte UntisLessonAbsenceHelper
  gezogen, damit Dialog und MCP-Tool nie unterschiedliche Statusvorschlaege berechnen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 22:55:33 +02:00
co-authored by Claude Sonnet 5
parent d979a84e7b
commit bfe214ecf2
25 changed files with 900 additions and 62 deletions
@@ -161,7 +161,9 @@ public partial class GroupDetailView : UserControl
App.Services.GetRequiredService<WebUntisIntegrationService>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IParticipationSessionRepository>(),
App.Services.GetRequiredService<IParticipationRepository>());
App.Services.GetRequiredService<IParticipationRepository>(),
App.Services.GetRequiredService<AiPlanningService>(),
App.Services.GetRequiredService<AiSettingsService>());
await new WebUntisLessonAbsenceComparisonDialog { DataContext = dialogVm }.ShowDialog(owner);
vm.ParticipationTab.RefreshCurrentGrid();
}
@@ -4,12 +4,12 @@
xmlns:conv="clr-namespace:LehrerApp.Desktop.Converters"
x:Class="LehrerApp.Desktop.Views.Groups.WebUntisLessonAbsenceComparisonDialog"
x:DataType="vm:WebUntisLessonAbsenceComparisonViewModel"
Title="Fehlzeiten je Unterricht mit WebUntis abgleichen" Width="1000" Height="640"
MinWidth="800" MinHeight="450" WindowStartupLocation="CenterOwner">
Title="Fehlzeiten je Unterricht mit WebUntis abgleichen" Width="1150" Height="640"
MinWidth="900" MinHeight="450" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto,Auto" Margin="24" RowSpacing="8">
<StackPanel Grid.Row="0" Spacing="4">
<TextBlock Text="Fehlzeiten je Unterricht mit WebUntis abgleichen" Classes="dialogtitle"/>
<TextBlock Text="Nur markierte Zeilen mit einer vorhandenen lokalen Kursstunde werden übernommen. Zeilen ohne automatische Zuordnung bitte manuell einem Kursmitglied zuweisen."
<TextBlock Text="Nur markierte Zeilen mit einer vorhandenen lokalen Kursstunde werden übernommen. Zeilen ohne automatische Zuordnung bitte manuell einem Kursmitglied zuweisen. „Übernahme als“ ist mit dem aus WebUntis abgeleiteten Status vorbelegt, aber vor dem Übernehmen änderbar."
FontSize="12" Opacity="0.65" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Grid.Row="1" Orientation="Horizontal" Spacing="8">
@@ -17,20 +17,24 @@
<TextBlock Text="bis" VerticalAlignment="Center"/>
<CalendarDatePicker SelectedDate="{Binding EndDate, Converter={x:Static conv:DateTimeOffsetToDateTimeConverter.Instance}}"/>
<Button Content="Fehlzeiten laden" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>
<Button Content="KI-Vorschlag für Übernahme laden" Command="{Binding SuggestStatusWithAiCommand}"
IsEnabled="{Binding !AiSuggestBusy}"
ToolTip.Tip="Schickt für jede geladene Zeile nur eine technische Positions-Id sowie Grund/Minuten/Bearbeitet-Kennzeichen ans KI-Backend - nie Namen, Klasse oder Datum. Setzt nur die &quot;Übernahme als&quot;-Auswahl vor, übernimmt nichts von selbst."/>
</StackPanel>
<Grid Grid.Row="2" ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="4,0">
<Grid Grid.Row="2" ColumnDefinitions="Auto,1.1*,1.1*,80,80,1.1*,1.1*,1*" ColumnSpacing="8" Margin="4,0">
<TextBlock Grid.Column="1" Text="Name (WebUntis)" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="2" Text="Zuordnung" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="3" Text="Datum" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="4" Text="Zeit" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="5" Text="WebUntis-Status" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="6" Text="Lokaler Status" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="6" Text="Übernahme als" FontSize="11" Opacity="0.6"/>
<TextBlock Grid.Column="7" Text="Lokaler Status" FontSize="11" Opacity="0.6"/>
</Grid>
<ScrollViewer Grid.Row="3">
<ItemsControl ItemsSource="{Binding Rows}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:WebUntisLessonAbsenceRow">
<Grid ColumnDefinitions="Auto,1.2*,1.2*,80,80,1.1*,1.1*" ColumnSpacing="8" Margin="0,3">
<Grid ColumnDefinitions="Auto,1.1*,1.1*,80,80,1.1*,1.1*,1*" ColumnSpacing="8" Margin="0,3">
<CheckBox Grid.Column="0" IsChecked="{Binding Selected}" IsEnabled="{Binding CanApply}"/>
<TextBlock Grid.Column="1" Text="{Binding UntisStudentName}" VerticalAlignment="Center" TextWrapping="Wrap"/>
<ComboBox Grid.Column="2" ItemsSource="{Binding Candidates}" SelectedItem="{Binding AssignedStudent}"
@@ -39,7 +43,9 @@
<TextBlock Grid.Column="3" Text="{Binding DateLabel}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="4" Text="{Binding TimeLabel}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="5" Text="{Binding UntisStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
<TextBlock Grid.Column="6" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
<ComboBox Grid.Column="6" ItemsSource="{Binding StatusOptions}" SelectedItem="{Binding SelectedStatusName}"
HorizontalAlignment="Stretch"/>
<TextBlock Grid.Column="7" Text="{Binding LocalStatus}" TextWrapping="Wrap" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
@@ -16,7 +16,7 @@
<ScrollViewer Grid.Row="1" MaxHeight="380" Margin="24,18">
<TextBlock Text="{Binding Message}" TextWrapping="Wrap" FontSize="14" LineHeight="20"/>
</ScrollViewer>
<StackPanel Grid.Row="2" Margin="24,0,24,14" Spacing="6">
<StackPanel Grid.Row="2" Margin="24,0,24,14" Spacing="6" IsVisible="{Binding AllowSessionTrust}">
<CheckBox x:Name="TrustOperationCheck" FontSize="12"
Content="Diese Art von Aktion für den Rest der Sitzung nicht mehr nachfragen"/>
<CheckBox x:Name="TrustAllCheck" FontSize="12"
@@ -11,4 +11,11 @@ public class McpConfirmDialogInfo
public string Title { get; init; } = "";
public string Message { get; init; } = "";
public string ConfirmText { get; init; } = "Übernehmen";
/// <summary>False blendet beide "nicht mehr nachfragen"-Kontrollkästchen aus (Nutzer-Entscheidung
/// zu <c>get_named_untis_absence_pattern</c>: eine namentliche Fehlzeitenauskunft muss jedes Mal
/// einzeln bestätigt werden) - AvaloniaMcpConfirmationService ignoriert eine Sitzungsfreigabe für
/// diesen Aufruf ohnehin serverseitig, das Ausblenden verhindert zusätzlich die falsche
/// Erwartung, ein angekreuztes Kästchen hätte hier irgendeine Wirkung.</summary>
public bool AllowSessionTrust { get; init; } = true;
}