diff --git a/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs b/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs index 905b23c..99ddc4f 100644 --- a/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs +++ b/LehrerApp.Desktop/Services/WebUntisIntegrationService.cs @@ -22,7 +22,7 @@ public sealed record UntisTimetablePeriodDto(int Id, int Date, int StartTime, in IReadOnlyList Subjects, IReadOnlyList Rooms); public sealed record UntisStudentAddressDto(string? Email, string? Mobile, string? Phone, string? City, string? PostCode, string? Street); -public sealed record UntisStudentDto(int UntisId, int ExternKey, string ClassName, string? Name, string? LongName, +public sealed record UntisStudentDto(int UntisId, int? ExternKey, string ClassName, string? Name, string? LongName, string? ForeName, string DisplayName, string? Gender, int? BirthDate, string? BirthDateRaw, int? EntryDate, string? EntryDateRaw, int? ExitDate, string? ExitDateRaw, string? Text, string? MedicalReportDuty, string? Schulpflicht, string? Majority, UntisStudentAddressDto Address, string? AttributeIL); diff --git a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs index 048f1d8..d9e0b20 100644 --- a/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/ClassTeacher/ClassTeacherOverviewViewModel.cs @@ -38,7 +38,7 @@ public sealed record ClassTeacherRosterRow(string StudentName, int? ExternKey, b return students .Select(s => { - var absence = absenceByKey.GetValueOrDefault(s.ExternKey) + var absence = (s.ExternKey is { } key ? absenceByKey.GetValueOrDefault(key) : null) ?? absenceByName.GetValueOrDefault(s.DisplayName); return new ClassTeacherRosterRow(s.DisplayName, s.ExternKey, absence is not null, absence is null ? null : $"{absence.TotalAbsentPeriods} Stunde(n) — {absence.StatusLabel}", diff --git a/LehrerApp.WebUntis.Tests/WebUntisStudentReportParserTests.cs b/LehrerApp.WebUntis.Tests/WebUntisStudentReportParserTests.cs index 167fb17..6b61a24 100644 --- a/LehrerApp.WebUntis.Tests/WebUntisStudentReportParserTests.cs +++ b/LehrerApp.WebUntis.Tests/WebUntisStudentReportParserTests.cs @@ -51,6 +51,19 @@ public sealed class WebUntisStudentReportParserTests Assert.Equal(20270731, student.ExitDate); } + [Fact] + public void Parse_ErlaubtFehlendenExternKey() + { + // Reale Schuldaten enthalten Schüler*innen ohne gepflegten externen Schlüssel (z.B. frisch + // angelegt) - das darf nicht den Abruf der gesamten Klassenliste zum Absturz bringen. + const string report = "id\texternKey\tklasse.name\r\n17\t\t10a\r\n"; + + var student = Assert.Single(WebUntisStudentReportParser.Parse(report)); + + Assert.Equal(17, student.UntisId); + Assert.Null(student.ExternKey); + } + [Fact] public void Parse_LehntUngueltigePflichtIdAb() { diff --git a/LehrerApp.WebUntis/WebUntisModels.cs b/LehrerApp.WebUntis/WebUntisModels.cs index 7193e8e..7077964 100644 --- a/LehrerApp.WebUntis/WebUntisModels.cs +++ b/LehrerApp.WebUntis/WebUntisModels.cs @@ -41,9 +41,12 @@ public sealed record UntisStudentAddress( string? PostCode, string? Street); +// ExternKey optional: reale Schuldaten enthalten Schüler*innen (z.B. frisch angelegt/übernommen) +// ohne gepflegten externen Schlüssel - ein fehlender Wert bei einer/einem soll nicht den Abruf der +// gesamten Klassenliste zum Absturz bringen (siehe WebUntisStudentReportParser). public sealed record UntisStudent( int UntisId, - int ExternKey, + int? ExternKey, string ClassName, string? Name, string? LongName, diff --git a/LehrerApp.WebUntis/WebUntisStudentReportParser.cs b/LehrerApp.WebUntis/WebUntisStudentReportParser.cs index 709df18..b01b05d 100644 --- a/LehrerApp.WebUntis/WebUntisStudentReportParser.cs +++ b/LehrerApp.WebUntis/WebUntisStudentReportParser.cs @@ -23,7 +23,7 @@ public static class WebUntisStudentReportParser values[headers[index]] = index < row.Count ? row[index].Trim() : null; var untisId = RequiredInt(Get(values, "id"), "id"); - var externalKey = RequiredInt(Get(values, "externKey"), "externKey"); + var externalKey = OptionalInt(Get(values, "externKey")); var name = Optional(Get(values, "name")); var lastName = Optional(Get(values, "longName")); var firstName = Optional(Get(values, "foreName")); @@ -73,6 +73,11 @@ public static class WebUntisStudentReportParser ? parsed : throw new InvalidDataException($"Ungültige Zahl in Spalte \"{field}\"."); + private static int? OptionalInt(string? value) => + int.TryParse(value?.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) + ? parsed + : null; + private static int? GermanDate(string? value) { if (!DateOnly.TryParseExact(value?.Trim(), ["dd.MM.yyyy", "yyyyMMdd"], CultureInfo.InvariantCulture, diff --git a/TODO.md b/TODO.md index ce0eb56..0f28ba7 100644 --- a/TODO.md +++ b/TODO.md @@ -1328,6 +1328,15 @@ eigenen Unterricht abfragt und deshalb mit den regulären Lehrkraft-Rechten funk Schülerdetail; ein Detail-Drill-down pro Schüler*in (Heatmap gefehlter Einzelstunden, Verspätung/vorzeitiges Verlassen je Woche); eine KI-gestützte Zusammenfassung/Bewertung der Einträge (pro Schüler*in oder gesamt) mit Namens-Anonymisierung/Synonymen vor dem Versand. + **Echter Fehler gefunden und behoben:** Die Übersicht rief für das Roster + `WebUntisIntegrationService.GetStudentsAsync` auf (den bestehenden, schon länger genutzten + Schülerlisten-Bericht) — `WebUntisStudentReportParser` behandelte die Spalte `externKey` bisher als + Pflichtfeld (`RequiredInt`) und ließ den kompletten Abruf mit "Ungültige Zahl in Spalte + \"externKey\"" scheitern, sobald auch nur eine/einer der Schüler*innen der Klasse keinen + gepflegten externen Schlüssel hat (in der Praxis vorkommend, z. B. frisch angelegt). `externKey` + ist jetzt optional (`UntisStudent.ExternKey`/`UntisStudentDto.ExternKey` → `int?`) — betraf auch + den bestehenden WebUntis-Klassenimport in der Kursansicht, der das aber schon immer über + `FirstNotEmpty` beim Weiterimport toleriert hatte und deshalb nicht auffiel. ### 4.4 Wochen-/Tagesansicht - [x] **4.4.1** Kalenderansicht über alle Gruppen: Woche und Tag — siehe Nachtrag zu 4.3