Files
adminandClaude Sonnet 5 bfe214ecf2
CI / build-and-test (push) Canceled after 0s
feat: Untis-Hub-Kadenzkopplung, anpassbarer Statusvorschlag + KI-/MCP-Unterstuetzung beim Fehlzeitenabgleich (Nutzer-Feedback)
- 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>
2026-09-12 22:55:33 +02:00

91 lines
4.5 KiB
C#

using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.ViewModels.Students;
using LehrerApp.Desktop.Views;
using LehrerApp.Desktop.Views.Groups;
using LehrerApp.Desktop.Views.Students;
using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Services;
/// <summary>Führt einen der vier bestehenden WebUntis-Abgleiche (unverändert, über ihre bestehenden
/// Dialoge) aus und vermerkt das Ergebnis im <see cref="UntisHubService"/> - genutzt sowohl vom
/// Untis-Hub-Dialog ("Jetzt prüfen" je Zeile) als auch von den WebUntis-Menüpunkten in
/// <c>MainWindow</c>, damit beide Einstiegspunkte denselben Fälligkeitsstand pflegen.</summary>
public static class UntisHubActions
{
public static async Task RunFehlzeitenAsync(Window owner, LearningGroup group, UntisHubJobKind kind,
DateOnly start, DateOnly end, UntisHubService hub)
{
var vm = new WebUntisLessonAbsenceComparisonViewModel(group,
App.Services.GetRequiredService<WebUntisIntegrationService>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IParticipationSessionRepository>(),
App.Services.GetRequiredService<IParticipationRepository>(),
App.Services.GetRequiredService<AiPlanningService>(),
App.Services.GetRequiredService<AiSettingsService>())
{ StartDate = start.ToDateTime(TimeOnly.MinValue), EndDate = end.ToDateTime(TimeOnly.MinValue) };
var loaded = TrackLoad(vm, v => v.Busy);
await new WebUntisLessonAbsenceComparisonDialog { DataContext = vm }.ShowDialog(owner);
if (loaded()) hub.RecordRun(kind, group.Id, vm.Status);
}
public static async Task RunKlassenbuchAsync(Window owner, UntisHubService hub)
{
var vm = new WebUntisDocumentationComparisonViewModel(
App.Services.GetRequiredService<WebUntisIntegrationService>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IGroupRepository>(),
App.Services.GetRequiredService<IDocumentationRepository>(),
App.Services.GetRequiredService<SchoolYearService>());
var loaded = TrackLoad(vm, v => v.Busy);
await new WebUntisDocumentationComparisonDialog { DataContext = vm }.ShowDialog(owner);
if (loaded()) hub.RecordRun(UntisHubJobKind.Klassenbuchabgleich, null, vm.Status);
}
public static async Task RunHausaufgabenAsync(Window owner, UntisHubService hub)
{
var vm = new WebUntisHomeworkComparisonViewModel(
App.Services.GetRequiredService<WebUntisIntegrationService>(),
App.Services.GetRequiredService<IStudentRepository>(),
App.Services.GetRequiredService<IGroupRepository>(),
App.Services.GetRequiredService<ISubjectRepository>(),
App.Services.GetRequiredService<IGroupMembershipRepository>(),
App.Services.GetRequiredService<IParticipationSessionRepository>(),
App.Services.GetRequiredService<IParticipationRepository>(),
App.Services.GetRequiredService<SchoolYearService>());
var loaded = TrackLoad(vm, v => v.Busy);
await new WebUntisHomeworkComparisonDialog { DataContext = vm }.ShowDialog(owner);
if (loaded()) hub.RecordRun(UntisHubJobKind.Hausaufgabenabgleich, null, vm.Status);
}
public static async Task RunOffenePeriodsAsync(Window owner, UntisHubService hub)
{
var dialog = new OpenUntisPeriodsDialog();
await dialog.ShowDialog(owner);
hub.RecordRun(UntisHubJobKind.OffenePeriods, null, dialog.LastStatus);
}
/// <summary>Erkennt nicht-invasiv (ohne die Vergleichs-ViewModels zu ändern), ob im Dialog
/// tatsächlich ein Ladeversuch stattfand: <c>Busy</c> wechselt in <c>Load()</c> immer erst auf
/// true und im <c>finally</c>-Block zurück auf false, egal ob erfolgreich oder mit Fehler
/// abgebrochen - genau dieser Übergang wird hier beobachtet.</summary>
private static Func<bool> TrackLoad<T>(T vm, Func<T, bool> isBusy) where T : ObservableObject
{
var loaded = false;
var wasBusy = false;
vm.PropertyChanged += (_, e) =>
{
if (e.PropertyName != "Busy") return;
var busy = isBusy(vm);
if (wasBusy && !busy) loaded = true;
wasBusy = busy;
};
return () => loaded;
}
}