WIP (unstable): Klassenlehrer-Tab (fremde Klassenbucheinträge + Fehlzeiten der Klasse)
Neuer Tab in der Gruppendetailansicht (LearningGroup.IsClassTeacher-Flag), zeigt Klassenbucheinträge anderer Lehrkräfte sowie Fehlzeiten der ganzen Klasse über alle Fächer, pro Schüler*in und Tag zusammengefasst. Beides rein lesend, kein Übernehmen in lokale Daten. Fehlzeiten-Bericht (AbsencePerStudent) adressiert die Klasse über "KL"+getKlassen-ID, automatisch aufgelöst - noch nicht mit echtem WebUntis-Zugang gegengeprüft (siehe TODO.md). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,12 @@ public sealed record UntisLessonAbsenceDto(string StudentName, int Date, int Abs
|
||||
string? Reason, int? ExternKey, bool ExternKeyInParentheses, string? HandledOn, bool Counts);
|
||||
public sealed record UntisClassRegisterEventDto(string ClassName, int Date, string? Subject,
|
||||
string StudentName, string? CategoryName, string? CategoryGroup, string? Text);
|
||||
public sealed record UntisForeignClassRegisterEventDto(string ClassName, int Date, string? Subject,
|
||||
string StudentName, string? TeacherUsername, string? CategoryName, string? CategoryGroup, string? Text);
|
||||
public sealed record UntisClassAbsenceEntryDto(string StudentName, int? ExternKey, string ClassName, int Date,
|
||||
int AbsentPeriods, int AbsentMinutes, string? TeacherUsernames, string? Subject, string? AbsenceReason,
|
||||
string? Note, int? EntryId, string? HandledOn, bool Counts, string? ExcuseNote, int? PeriodNumber,
|
||||
string? Status, bool CountsAsFullDay);
|
||||
|
||||
/// <summary>Direkter WebUntis-Client des Desktops. Personenbezogene Antworten und der
|
||||
/// unverschlüsselte CSV-Report passieren zu keinem Zeitpunkt den LehrerApp-Server.</summary>
|
||||
@@ -135,6 +141,65 @@ public sealed class WebUntisIntegrationService(HttpClient http, WebUntisSettings
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// Spiegelbild von GetOwnClassRegisterEventsAsync für das "Klassenlehrer"-Feature (siehe TODO.md):
|
||||
// dieselben "-alle-"-Rohdaten, aber Einträge anderer Lehrkräfte statt der eigenen, zusätzlich auf
|
||||
// die übergebene Klasse eingeschränkt (der "-alle-"-Bericht liefert sonst auch andere Klassen mit,
|
||||
// sobald eine Lehrkraft mehrere Klassen unterrichtet).
|
||||
public Task<IReadOnlyList<UntisForeignClassRegisterEventDto>> GetForeignClassRegisterEventsAsync(
|
||||
string className, DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var ownUsername = settings.GetApiCredentials()?.Username;
|
||||
var entries = await client.GetClassRegisterEventsReportAsync(Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisForeignClassRegisterEventDto>)entries
|
||||
.Where(x => string.Equals(x.ClassName, className, StringComparison.OrdinalIgnoreCase)
|
||||
// Nicht einfach "!=" gegen ownUsername: wäre ownUsername unbekannt (null), würde
|
||||
// ein ebenfalls fehlender TeacherUsername sonst fälschlich als "eigen" erkannt
|
||||
// (Equals(null,null)==true) und rausgefiltert statt als fremd angezeigt zu werden.
|
||||
&& !(ownUsername is not null
|
||||
&& string.Equals(x.TeacherUsername, ownUsername, StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(x => new UntisForeignClassRegisterEventDto(x.ClassName, x.Date, x.Subject, x.StudentName,
|
||||
x.TeacherUsername, x.CategoryName, x.CategoryGroup, x.Text))
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// Fehlzeiten der ganzen Klasse über alle Fächer, Ergänzung zu GetForeignClassRegisterEventsAsync
|
||||
// für das "Klassenlehrer"-Feature. Der Bericht adressiert die Klasse über "KL"+getKlassen-ID statt
|
||||
// eines Namens, siehe ResolveClassIdAsync.
|
||||
public Task<IReadOnlyList<UntisClassAbsenceEntryDto>> GetClassAbsencesAsync(string className,
|
||||
DateOnly start, DateOnly end, CancellationToken token = default) => ExecuteAsync(async client =>
|
||||
{
|
||||
var classId = await ResolveClassIdAsync(client, className, token);
|
||||
var entries = await client.GetClassAbsencesAsync(classId, Date(start), Date(end), token);
|
||||
return (IReadOnlyList<UntisClassAbsenceEntryDto>)entries.Select(x => new UntisClassAbsenceEntryDto(
|
||||
x.StudentName, x.ExternKey, x.ClassName, x.Date, x.AbsentPeriods, x.AbsentMinutes,
|
||||
x.TeacherUsernames, x.Subject, x.AbsenceReason, x.Note, x.EntryId, x.HandledOn, x.Counts,
|
||||
x.ExcuseNote, x.PeriodNumber, x.Status, x.CountsAsFullDay))
|
||||
.ToList();
|
||||
}, token);
|
||||
|
||||
// "KL" + getKlassen-ID, dasselbe Element-Kürzel-Schema wie kl/te/su/ro bei anderen Berichten
|
||||
// (siehe Entities(...) unten). Unbestätigt (kein offizieller Report-Parameter dokumentiert), aber
|
||||
// aus einem echten Browser-Request rückgeschlossen - schlägt die Zuordnung fehl, liefert der
|
||||
// Bericht erkennbar leere/falsche Daten statt eines stillen Fehlers.
|
||||
private static async Task<string> ResolveClassIdAsync(WebUntisClient client, string className,
|
||||
CancellationToken token)
|
||||
{
|
||||
var years = await client.GetSchoolYearsAsync(token);
|
||||
var today = Date(DateOnly.FromDateTime(DateTime.Today));
|
||||
var year = years.FirstOrDefault(y => y.StartDate <= today && y.EndDate >= today)
|
||||
?? years.OrderByDescending(y => y.StartDate).FirstOrDefault()
|
||||
?? throw new WebUntisIntegrationException("WebUntis hat kein Schuljahr geliefert.");
|
||||
var classes = await client.GetClassesAsync(year.UntisId, token);
|
||||
var matches = classes.Where(c => string.Equals(c.Name, className, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
return matches.Count switch
|
||||
{
|
||||
1 => $"KL{matches[0].UntisId}",
|
||||
0 => throw new WebUntisIntegrationException($"WebUntis kennt keine Klasse namens \"{className}\"."),
|
||||
_ => throw new WebUntisIntegrationException(
|
||||
$"Mehrere WebUntis-Klassen heißen \"{className}\" - bitte manuell prüfen."),
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<T> ExecuteAsync<T>(Func<WebUntisClient, Task<T>> operation, CancellationToken token)
|
||||
{
|
||||
try { return await operation(await GetClientAsync(token)); }
|
||||
|
||||
Reference in New Issue
Block a user