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>
110 lines
5.6 KiB
C#
110 lines
5.6 KiB
C#
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
|
|
namespace LehrerApp.Desktop.Services;
|
|
|
|
public enum UntisHubDueState { Ok, Due, Overdue }
|
|
|
|
/// <summary>Eine Zeile im Untis-Hub: eine Job-Instanz (Fehlzeiten-Kadenz je Lerngruppe, oder einer
|
|
/// der drei dashboard-weiten Jobs) mit ihrer aktuellen Fälligkeit.</summary>
|
|
public sealed record UntisHubJobRow(
|
|
UntisHubJobKind Kind, Guid? GroupId, string GroupName,
|
|
DateTime? LastRunAt, string? LastResultSummary, UntisHubDueState DueState, string DueLabel);
|
|
|
|
/// <summary>
|
|
/// Zeigt, welche der bestehenden, rein manuell ausgelösten WebUntis-Abgleiche (Fehlzeiten pro
|
|
/// Lerngruppe, offene Stunden, Klassenbuch-/Hausaufgabenabgleich) fällig sind - ohne selbst
|
|
/// WebUntis anzufragen (Nutzer-Feedback: "nicht bei WebUntis auffallen", siehe
|
|
/// <see cref="UntisReportCacheService"/>). Die eigentlichen Abgleiche laufen weiterhin über die
|
|
/// bestehenden Vergleichsdialoge (siehe <see cref="UntisHubActions"/>); dieser Service verwaltet
|
|
/// nur die Fälligkeits-Zeitstempel dazu.
|
|
/// </summary>
|
|
public sealed class UntisHubService(
|
|
IGroupRepository groups, IUntisHubJobStateRepository jobStates, SchoolYearService schoolYears)
|
|
{
|
|
private static readonly TimeSpan FehlzeitenKurzDue = TimeSpan.FromDays(14);
|
|
private static readonly TimeSpan FehlzeitenKurzOverdue = TimeSpan.FromDays(21);
|
|
private static readonly TimeSpan FehlzeitenLangDue = TimeSpan.FromDays(60);
|
|
private static readonly TimeSpan FehlzeitenLangOverdue = TimeSpan.FromDays(90);
|
|
private static readonly TimeSpan GlobalDue = TimeSpan.FromDays(14);
|
|
private static readonly TimeSpan GlobalOverdue = TimeSpan.FromDays(21);
|
|
|
|
private static readonly (UntisHubJobKind Kind, string Label)[] GlobalJobs =
|
|
[
|
|
(UntisHubJobKind.OffenePeriods, "Offene Stunden"),
|
|
(UntisHubJobKind.Klassenbuchabgleich, "Klassenbuchabgleich"),
|
|
(UntisHubJobKind.Hausaufgabenabgleich, "Hausaufgabenabgleich"),
|
|
];
|
|
|
|
public List<UntisHubJobRow> GetRows()
|
|
{
|
|
var eligibleGroups = groups.GetBySchoolYear(schoolYears.CurrentSchoolYear())
|
|
.Where(g => g.WebUntisLessonId is not null)
|
|
.OrderBy(g => g.Name)
|
|
.ToList();
|
|
return BuildRows(eligibleGroups, jobStates.GetAll(), DateTime.UtcNow);
|
|
}
|
|
|
|
/// <summary>Der Langzeit-Fehlzeitenabgleich deckt seit Schuljahresbeginn ein Zeitfenster ab, das
|
|
/// das kurzfristige vollständig einschließt - ohne das hier mitzuziehen bliebe die kurzfristige
|
|
/// Kadenz trotz erledigtem Langzeit-Abgleich als fällig stehen (Nutzer-Feedback). Umgekehrt deckt
|
|
/// ein Kurz-Lauf das lange Fenster nicht ab, bleibt also einseitig.
|
|
public void RecordRun(UntisHubJobKind kind, Guid? groupId, string? summary)
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
Save(kind, groupId, now, summary);
|
|
if (kind == UntisHubJobKind.FehlzeitenLang)
|
|
Save(UntisHubJobKind.FehlzeitenKurz, groupId, now, "durch Abgleich seit Schuljahresbeginn mit erledigt");
|
|
}
|
|
|
|
private void Save(UntisHubJobKind kind, Guid? groupId, DateTime at, string? summary) =>
|
|
jobStates.Save(new UntisHubJobState
|
|
{
|
|
Id = jobStates.Get(kind, groupId)?.Id ?? Guid.NewGuid(),
|
|
Kind = kind, GroupId = groupId, LastRunAt = at, LastResultSummary = summary,
|
|
});
|
|
|
|
/// Reine Entscheidungslogik ohne Repository-Zugriff (gleiches Muster wie
|
|
/// <see cref="UntisReportCacheService.Plan"/>): aus den fälligkeitsrelevanten Lerngruppen und den
|
|
/// zuletzt gespeicherten Job-Zuständen wird die vollständige Hub-Zeilenliste gebaut - zwei
|
|
/// Fehlzeiten-Zeilen je Gruppe plus die drei dashboard-weiten Zeilen.
|
|
public static List<UntisHubJobRow> BuildRows(
|
|
IReadOnlyList<LearningGroup> eligibleGroups, IReadOnlyList<UntisHubJobState> states, DateTime utcNow)
|
|
{
|
|
UntisHubJobState? State(UntisHubJobKind kind, Guid? groupId) =>
|
|
states.FirstOrDefault(s => s.Kind == kind && s.GroupId == groupId);
|
|
|
|
var rows = new List<UntisHubJobRow>();
|
|
foreach (var group in eligibleGroups)
|
|
{
|
|
rows.Add(Row(UntisHubJobKind.FehlzeitenKurz, group.Id, group.Name,
|
|
State(UntisHubJobKind.FehlzeitenKurz, group.Id), FehlzeitenKurzDue, FehlzeitenKurzOverdue, utcNow));
|
|
rows.Add(Row(UntisHubJobKind.FehlzeitenLang, group.Id, group.Name,
|
|
State(UntisHubJobKind.FehlzeitenLang, group.Id), FehlzeitenLangDue, FehlzeitenLangOverdue, utcNow));
|
|
}
|
|
foreach (var (kind, label) in GlobalJobs)
|
|
rows.Add(Row(kind, null, label, State(kind, null), GlobalDue, GlobalOverdue, utcNow));
|
|
return rows;
|
|
}
|
|
|
|
private static UntisHubJobRow Row(UntisHubJobKind kind, Guid? groupId, string groupName,
|
|
UntisHubJobState? state, TimeSpan due, TimeSpan overdue, DateTime utcNow)
|
|
{
|
|
var (dueState, dueLabel) = DueStatus(state?.LastRunAt, due, overdue, utcNow);
|
|
return new UntisHubJobRow(kind, groupId, groupName, state?.LastRunAt, state?.LastResultSummary,
|
|
dueState, dueLabel);
|
|
}
|
|
|
|
private static (UntisHubDueState, string) DueStatus(
|
|
DateTime? lastRunAt, TimeSpan due, TimeSpan overdue, DateTime utcNow)
|
|
{
|
|
if (lastRunAt is null) return (UntisHubDueState.Overdue, "noch nie geprüft");
|
|
var age = utcNow - lastRunAt.Value;
|
|
var days = (int)age.TotalDays;
|
|
if (age >= overdue) return (UntisHubDueState.Overdue, $"fällig seit {days} Tagen");
|
|
if (age >= due) return (UntisHubDueState.Due, $"fällig seit {days} Tagen");
|
|
return (UntisHubDueState.Ok, days == 0 ? "gerade eben geprüft" : $"vor {days} Tag(en) geprüft");
|
|
}
|
|
}
|