diff --git a/LehrerApp.Core/Models/Participation.cs b/LehrerApp.Core/Models/Participation.cs
index af32d6a..45be265 100644
--- a/LehrerApp.Core/Models/Participation.cs
+++ b/LehrerApp.Core/Models/Participation.cs
@@ -32,9 +32,18 @@ public class ParticipationEntry
// ParticipationCountSuggestion.
public int RaisedHandCount { get; set; }
public int CalledOnCount { get; set; }
+ ///
+ /// Tagesflagge (Nutzer-Feedback): markiert eine session-bezogene Bewertung als besonders
+ /// herausragend oder besonders schwach, unabhängig von der Richtung ("egal in welche
+ /// Richtung, herausragend festhalten"). Rein deskriptiv — fließt nirgends in eine Berechnung
+ /// (Mitarbeitsnote 3.2, Aufrufgerechtigkeit) ein, nur zur Erinnerung/Dokumentation.
+ ///
+ public DayHighlightKind? DayHighlight { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
+public enum DayHighlightKind { Standout, Sleepy, Rough }
+
///
/// Hausaufgabenstatus einer Sitzung; null bedeutet, dass für diesen Termin keine
/// Hausaufgabe erfasst wurde. MissingOpen kann später in MissingOverdue oder SubmittedLate
diff --git a/LehrerApp.Core/Services/ReportGradeTargetCalculator.cs b/LehrerApp.Core/Services/ReportGradeTargetCalculator.cs
new file mode 100644
index 0000000..ecce664
--- /dev/null
+++ b/LehrerApp.Core/Services/ReportGradeTargetCalculator.cs
@@ -0,0 +1,48 @@
+using LehrerApp.Core.Models;
+
+namespace LehrerApp.Core.Services;
+
+/// Welcher der drei Leistungsbereiche (siehe GradingScheme) bei der Zielnoten-Frage der
+/// gesuchte/unbekannte ist.
+public enum GradeBucketKind { Exams, Participation, Other }
+
+public readonly record struct TargetGradeResult(double RequiredAverage, bool IsAchievable);
+
+///
+/// Beantwortet die Zielnoten-Frage "Was brauche ich noch, um Zeugnisnote X zu erreichen?"
+/// (Nutzer-Feedback) — kehrt um: löst nach
+/// dem Durchschnitt eines gewählten Bereichs auf, der zusammen mit den bekannten Durchschnitten
+/// der übrigen Bereiche (gewichtet nach ) genau die Zielnote ergibt.
+/// Bereiche ohne bekannten Durchschnitt (z.B. noch keine Mitarbeitsnote im Halbjahr) fließen
+/// nicht in die bekannte Seite der Gleichung ein — exakt dieselbe Normierung wie
+/// bei fehlenden Bereichen.
+///
+public static class ReportGradeTargetCalculator
+{
+ public static TargetGradeResult? SolveRequiredAverage(double target, GradeBucketKind solveFor,
+ double? examsAverage, double? participationAverage, double? otherAverage,
+ GradingScheme scheme, GradingSystem system)
+ {
+ var solvePercent = solveFor switch
+ {
+ GradeBucketKind.Exams => scheme.ExamsPercent,
+ GradeBucketKind.Participation => scheme.ParticipationPercent,
+ _ => scheme.OtherPercent,
+ };
+ // Dieser Bereich fließt laut Gewichtungsschema gar nicht in die Zeugnisnote ein —
+ // keine Zielnoten-Frage für ihn beantwortbar.
+ if (solvePercent <= 0) return null;
+
+ var known = new List<(double Avg, double Percent)>();
+ if (solveFor != GradeBucketKind.Exams && examsAverage is { } e) known.Add((e, scheme.ExamsPercent));
+ if (solveFor != GradeBucketKind.Participation && participationAverage is { } p) known.Add((p, scheme.ParticipationPercent));
+ if (solveFor != GradeBucketKind.Other && otherAverage is { } o) known.Add((o, scheme.OtherPercent));
+
+ var totalPercent = known.Sum(k => k.Percent) + solvePercent;
+ var knownContribution = known.Sum(k => k.Avg * k.Percent);
+ var required = (target * totalPercent - knownContribution) / solvePercent;
+
+ var (min, max) = system == GradingSystem.Grades1To6 ? (1.0, 6.0) : (0.0, 15.0);
+ return new TargetGradeResult(required, required >= min && required <= max);
+ }
+}
diff --git a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
index fca4c2d..12683d0 100644
--- a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
@@ -103,6 +103,47 @@ public sealed class QuickInputViewModelTests
Assert.Null(rows[0].GetRating("quality"));
}
+ [Fact]
+ public void SetDayHighlight_SpeichertAufDerZeileUndAktualisiertDieAnzeige()
+ {
+ var aspects = new List
+ {
+ new(new ParticipationAspect { Key = "quality", Label = "Qualität" }),
+ };
+ var rows = new List
+ {
+ new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
+ };
+ var vm = new QuickInputViewModel(rows, aspects);
+
+ vm.SetDayHighlight(DayHighlightKind.Standout);
+
+ Assert.Equal(DayHighlightKind.Standout, rows[0].DayHighlight);
+ Assert.Equal("👑", vm.CurrentStudentDayHighlightSymbol);
+ Assert.Equal("Spitzentag", vm.CurrentStudentDayHighlightLabel);
+ }
+
+ [Fact]
+ public void ZurueckZuVorherigemSchueler_ZeigtDessenTagesflaggeWiederAn()
+ {
+ var aspects = new List
+ {
+ new(new ParticipationAspect { Key = "quality", Label = "Qualität" }),
+ };
+ var rows = new List
+ {
+ new(Guid.NewGuid(), "Anna", new ParticipationEntry(), aspects, []),
+ new(Guid.NewGuid(), "Ben", new ParticipationEntry(), aspects, []),
+ };
+ var vm = new QuickInputViewModel(rows, aspects);
+
+ vm.SetDayHighlight(DayHighlightKind.Sleepy); // Anna
+ vm.NextStudent(); // zu Ben, keine Flagge
+ vm.PreviousStudent(); // zurück zu Anna
+
+ Assert.Equal(DayHighlightKind.Sleepy, vm.CurrentStudentDayHighlight);
+ }
+
[Fact]
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
{
diff --git a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
index 46c06f7..2f748be 100644
--- a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
@@ -472,6 +472,7 @@ public sealed class SeatingPlanViewModelTests
vm.SetRatingByNumber(5);
vm.ApplyAttendanceShortcut(1, clear: false);
vm.ApplyHomeworkShortcut(7, clear: false);
+ vm.ApplyDayHighlightShortcut(1, clear: false);
var session = Assert.Single(sessions.GetByGroup(groupId));
Assert.Equal(DateOnly.FromDateTime(DateTime.Today), session.Date);
@@ -481,6 +482,27 @@ public sealed class SeatingPlanViewModelTests
Assert.Equal(AttendanceStatus.Present, entry.Attendance);
Assert.Equal(HomeworkStatus.MissingOpen, entry.Homework);
Assert.True(entry.HomeworkMissing);
+ Assert.Equal(DayHighlightKind.Standout, entry.DayHighlight);
+ }
+
+ [Fact]
+ public void SitzplatzBewertung_TagesflaggeKannWiederGeloeschtWerden()
+ {
+ var groupId = Guid.NewGuid();
+ var studentId = Guid.NewGuid();
+ var sessions = new FakeSessions([]);
+ var entries = new FakeEntries();
+ var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
+ groupId, studentId, "Beispiel, Anna", canEdit: true);
+
+ vm.ApplyDayHighlightShortcut(3, clear: false); // aus Versehen "Schlechter Tag" gesetzt
+ vm.ApplyDayHighlightShortcut(null, clear: true);
+
+ var session = Assert.Single(sessions.GetByGroup(groupId));
+ var entry = entries.GetBySessionAndStudent(session.Id, studentId)!;
+ Assert.Null(entry.DayHighlight);
+ Assert.Equal("Keine Markierung", vm.DayHighlightLabel);
+ Assert.All(vm.DayHighlightChoices, c => Assert.Equal(c.Kind is null, c.IsSelected));
}
[Fact]
diff --git a/LehrerApp.Desktop.Tests/StudentPerformanceOverviewViewModelTests.cs b/LehrerApp.Desktop.Tests/StudentPerformanceOverviewViewModelTests.cs
new file mode 100644
index 0000000..b2b6328
--- /dev/null
+++ b/LehrerApp.Desktop.Tests/StudentPerformanceOverviewViewModelTests.cs
@@ -0,0 +1,146 @@
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using LehrerApp.Desktop.ViewModels.Groups;
+using Xunit;
+
+namespace LehrerApp.Desktop.Tests;
+
+/// Tests für den Schüler-Leistungsüberblick (Nutzer-Feedback): Klausuren/Mitarbeit/Sonstige
+/// im Überblick, berechnete Zeugnisnote, Zielnoten- und Was-wäre-wenn-Rechner sowie der
+/// Bewerter-/Schülermodus-Unterschied (Kursdurchschnitt, Bearbeitbarkeit).
+public sealed class StudentPerformanceOverviewViewModelTests
+{
+ private static readonly Guid GroupId = Guid.NewGuid();
+ private static readonly Guid StudentId = Guid.NewGuid();
+ private static readonly Guid OtherStudentId = Guid.NewGuid();
+
+ private static StudentPerformanceOverviewViewModel BuildViewModel(
+ FakeExams? exams = null, FakeResults? results = null, FakeGrades? grades = null,
+ FakeSchemes? schemes = null, GradingSystem system = GradingSystem.Grades1To6)
+ {
+ var scheme = new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
+ var schemesRepo = schemes ?? new FakeSchemes();
+ if (schemes is null) schemesRepo.SetForGroup(GroupId, scheme);
+
+ return new StudentPerformanceOverviewViewModel(
+ grades ?? new FakeGrades(), exams ?? new FakeExams([]), results ?? new FakeResults(),
+ new FakeMemberships([]), schemesRepo, new GradingService(),
+ GroupId, StudentId, GroupType.Class, system, "Beispiel, Anna", "Testkurs", "2025/26");
+ }
+
+ [Fact]
+ public void ExamRows_ZeigtEigeneNoteUndKursdurchschnitt()
+ {
+ var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
+ var exams = new FakeExams([exam]);
+ var results = new FakeResults();
+ results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
+ results.Add(new ExamResult { ExamId = exam.Id, StudentId = OtherStudentId, Grade = "4" });
+
+ var vm = BuildViewModel(exams, results);
+
+ var row = Assert.Single(vm.ExamRows);
+ Assert.Equal("2", row.OwnGradeDisplay);
+ Assert.Equal("3.0", row.ClassAverageDisplay);
+ }
+
+ [Fact]
+ public void ExamHistory_ZeigtEigenenVerlaufUndMarkiertNotenabfall()
+ {
+ var exam1 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
+ var exam2 = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K2", Date = new DateOnly(2025, 9, 24) };
+ var exams = new FakeExams([exam1, exam2]);
+ var results = new FakeResults();
+ results.Add(new ExamResult { ExamId = exam1.Id, StudentId = StudentId, Grade = "2" });
+ results.Add(new ExamResult { ExamId = exam2.Id, StudentId = StudentId, Grade = "5" }); // Abfall + mangelhaft
+
+ var vm = BuildViewModel(exams, results);
+
+ Assert.Equal(2, vm.ExamHistory.Points.Count);
+ Assert.False(vm.ExamHistory.Points[0].IsWarning);
+ Assert.True(vm.ExamHistory.Points[1].IsWarning);
+ Assert.Contains("Abfall", vm.ExamHistory.Points[1].WarningText);
+ Assert.Contains("Versetzungsgefährdung", vm.ExamHistory.Points[1].WarningText);
+ Assert.True(vm.ExamHistory.Points[1].BarHeight < vm.ExamHistory.Points[0].BarHeight);
+ }
+
+ [Fact]
+ public void AktuelleZeugnisnote_KombiniertAlleDreiBereiche()
+ {
+ var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
+ var exams = new FakeExams([exam]);
+ var results = new FakeResults();
+ results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
+ var grades = new FakeGrades();
+ grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
+ Date = new DateOnly(2025, 9, 5), Value = "3" });
+ grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Other,
+ Date = new DateOnly(2025, 9, 5), Value = "1" });
+
+ var vm = BuildViewModel(exams, results, grades);
+
+ // (2*50 + 3*40 + 1*10) / 100 = 2.3 -> kaufmännisch 2
+ Assert.Equal("Note 2", vm.CurrentReportGradeDisplay);
+ }
+
+ [Fact]
+ public void Zielnote_BerechnetNoetigenKlausurschnitt()
+ {
+ var grades = new FakeGrades();
+ grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
+ Date = new DateOnly(2025, 9, 5), Value = "3" });
+ grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Other,
+ Date = new DateOnly(2025, 9, 5), Value = "2" });
+ var vm = BuildViewModel(grades: grades);
+
+ vm.TargetGradeText = "2";
+ vm.TargetBucketName = "Klausuren";
+
+ // Ziel 2,0 = (x*50 + 3*40 + 2*10)/100 => 200 = 50x+120+20 => x = 1,2
+ Assert.Contains("1.2", vm.TargetResultDisplay);
+ }
+
+ [Fact]
+ public void WasWaereWenn_ZeigtZeugnisnoteMitZusaetzlicherKlausurAn()
+ {
+ var exam = new Exam { Id = Guid.NewGuid(), GroupId = GroupId, Title = "K1", Date = new DateOnly(2025, 9, 10) };
+ var exams = new FakeExams([exam]);
+ var results = new FakeResults();
+ results.Add(new ExamResult { ExamId = exam.Id, StudentId = StudentId, Grade = "2" });
+ var grades = new FakeGrades();
+ grades.Add(new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
+ Date = new DateOnly(2025, 9, 5), Value = "2" });
+
+ var vm = BuildViewModel(exams, results, grades);
+
+ vm.WhatIfExamGradeText = "6";
+
+ // Klausuren neu: (2+6)/2=4 (50%), Mitarbeit 2 (40%, Sonstige fehlt) -> (4*50+2*40)/90 = 3,11 -> kaufm. 3
+ Assert.Contains("3", vm.WhatIfResultDisplay);
+ }
+
+ [Fact]
+ public void SaveCommand_AufMitarbeitszeile_SpeichertUndAktualisiertDurchschnitt()
+ {
+ var grades = new FakeGrades();
+ var grade = new Grade { StudentId = StudentId, GroupId = GroupId, Category = GradeCategory.Participation,
+ Date = new DateOnly(2025, 9, 5), Value = "3" };
+ grades.Add(grade);
+ var vm = BuildViewModel(grades: grades);
+
+ var row = Assert.Single(vm.ParticipationRows);
+ row.Value = "1";
+ row.SaveCommand.Execute(null);
+
+ Assert.Equal("1.0", vm.ParticipationAverageDisplay);
+ Assert.Equal("1", grades.GetByStudentAndGroup(StudentId, GroupId).Single().Value);
+ }
+
+ [Fact]
+ public void OhneWerteImZeitraum_ZeigtHinweisStattZeugnisnote()
+ {
+ var vm = BuildViewModel();
+
+ Assert.Contains("Noch nicht berechenbar", vm.CurrentReportGradeDisplay);
+ }
+}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
index 793b85a..29cc665 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
@@ -54,6 +54,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
public Func? OnManageStudentGrades { get; set; }
public Func? OnCollectiveGrade { get; set; }
public Func? OnReportGrades { get; set; }
+ public Func? OnShowPerformanceOverview { get; set; }
public GradeOverviewTabViewModel(IGradeRepository grades, IExamRepository exams,
IExamResultRepository results, IStudentRepository students,
@@ -107,6 +108,14 @@ public partial class GradeOverviewTabViewModel : ObservableObject
Recompute();
}
+ [RelayCommand]
+ private async Task ShowPerformanceOverview()
+ {
+ if (SelectedRow is null || OnShowPerformanceOverview is null) return;
+ await OnShowPerformanceOverview(SelectedRow);
+ Recompute();
+ }
+
[RelayCommand]
private async Task CollectiveGrade()
{
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
index 8f01c2c..c3eacf0 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
@@ -158,6 +158,7 @@ public partial class ParticipationTabViewModel : ObservableObject
row.OnCompetencyRatingChanged = (sid, code, val) => SaveCompetencyRating(sessionId, sid, code, val);
row.HomeworkChangedCallback = (sid, val) => SaveHomework(sessionId, sid, val);
row.AttendanceChangedCallback = (sid, val) => SaveAttendance(sessionId, sid, val);
+ row.DayHighlightChangedCallback = (sid, val) => SaveDayHighlight(sessionId, sid, val);
StudentRows.Add(row);
}
QuickInputCommand.NotifyCanExecuteChanged();
@@ -212,6 +213,15 @@ public partial class ParticipationTabViewModel : ObservableObject
_entries.Save(entry);
}
+ private void SaveDayHighlight(Guid sessionId, Guid studentId, DayHighlightKind? value)
+ {
+ if (IsReadOnly) return;
+ var entry = _entries.GetBySessionAndStudent(sessionId, studentId)
+ ?? new ParticipationEntry { SessionId = sessionId, StudentId = studentId };
+ entry.DayHighlight = value;
+ _entries.Save(entry);
+ }
+
[RelayCommand]
private void ToggleCompetencyTags() => CompetencyTagsVisible = !CompetencyTagsVisible;
@@ -389,6 +399,7 @@ public partial class ParticipationStudentRow : ObservableObject
[ObservableProperty] private HomeworkStatus? _homework;
[ObservableProperty] private AttendanceStatus? _attendance;
+ [ObservableProperty] private DayHighlightKind? _dayHighlight;
public string AttendanceLabel => AttendanceDisplay.ShortLabel(Attendance);
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
@@ -405,6 +416,7 @@ public partial class ParticipationStudentRow : ObservableObject
public Action? OnCompetencyRatingChanged { get; set; }
public Action? HomeworkChangedCallback { get; set; }
public Action? AttendanceChangedCallback { get; set; }
+ public Action? DayHighlightChangedCallback { get; set; }
public ParticipationStudentRow(Guid id, string name, ParticipationEntry entry,
List aspects, List competencyCodes)
@@ -414,6 +426,7 @@ public partial class ParticipationStudentRow : ObservableObject
_aspectDefs = aspects;
_homework = HomeworkDisplay.Effective(entry);
_attendance = entry.Attendance;
+ _dayHighlight = entry.DayHighlight;
foreach (var a in aspects)
{
@@ -463,6 +476,12 @@ public partial class ParticipationStudentRow : ObservableObject
HomeworkChangedCallback?.Invoke(StudentId, value);
}
+ public void SetDayHighlight(DayHighlightKind? value)
+ {
+ DayHighlight = value;
+ DayHighlightChangedCallback?.Invoke(StudentId, value);
+ }
+
[RelayCommand]
private void CycleAttendance()
{
@@ -614,6 +633,27 @@ public static class AttendanceDisplay
};
}
+// ── Tagesflagge (Nutzer-Feedback) ──────────────────────────────────────────────
+
+public static class DayHighlightDisplay
+{
+ public static string Symbol(DayHighlightKind? kind) => kind switch
+ {
+ DayHighlightKind.Standout => "👑",
+ DayHighlightKind.Sleepy => "😴",
+ DayHighlightKind.Rough => "⚡",
+ _ => "",
+ };
+
+ public static string Label(DayHighlightKind? kind) => kind switch
+ {
+ DayHighlightKind.Standout => "Spitzentag",
+ DayHighlightKind.Sleepy => "Schlaftag",
+ DayHighlightKind.Rough => "Schlechter Tag",
+ _ => "Keine Markierung",
+ };
+}
+
// ── Eine Bewertungszelle ──────────────────────────────────────────────────────
public partial class RatingCell : ObservableObject
@@ -800,12 +840,31 @@ public partial class QuickInputViewModel : ObservableObject
[ObservableProperty] private string _progressText = "";
[ObservableProperty] private bool _currentStudentIsAbsent;
[ObservableProperty] private string _currentStudentAttendanceLabel = "";
+ [ObservableProperty] private DayHighlightKind? _currentStudentDayHighlight;
/// Dimmt Name/Aspektliste, wenn der aktuelle Schüler abwesend ist — kein Blockieren der
/// Eingabe (manche Bewertungssysteme wollen trotzdem einen Eintrag, z.B. "0 Punkte"), nur ein
/// visueller Hinweis, dass eine Bewertung hier normalerweise keinen Sinn ergibt.
public double CurrentStudentContentOpacity => CurrentStudentIsAbsent ? 0.4 : 1.0;
+ /// Tagesflagge (Nutzer-Feedback): Fallback für Kurse ohne Sitzplan — im Sitzplatz-Dialog gibt
+ /// es dieselbe Auswahl bereits über SeatAssessmentViewModel.DayHighlightChoices.
+ public string CurrentStudentDayHighlightSymbol => DayHighlightDisplay.Symbol(CurrentStudentDayHighlight);
+ public string CurrentStudentDayHighlightLabel => DayHighlightDisplay.Label(CurrentStudentDayHighlight);
+
+ public void SetDayHighlight(DayHighlightKind? value)
+ {
+ if (_rows.Count == 0) return;
+ _rows[StudentIndex].SetDayHighlight(value);
+ CurrentStudentDayHighlight = value;
+ }
+
+ partial void OnCurrentStudentDayHighlightChanged(DayHighlightKind? value)
+ {
+ OnPropertyChanged(nameof(CurrentStudentDayHighlightSymbol));
+ OnPropertyChanged(nameof(CurrentStudentDayHighlightLabel));
+ }
+
public ObservableCollection AspectRows { get; } = [];
/// Wechselt je nach Typ des aktuell gewählten Aspekts (3.1.3) — Scale3/Binary haben andere
@@ -824,7 +883,8 @@ public partial class QuickInputViewModel : ObservableObject
_ => "1–5 bewerten",
};
return $"{ratingHint} · Q/W/E/R/T Aspekt wählen · Leertaste/↓ nächster Aspekt · ↑ vorheriger Aspekt · " +
- "Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · Esc schließen";
+ "Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · " +
+ "⇧1/2/3 Tagesflagge, ⇧0 löschen · Esc schließen";
}
}
@@ -852,6 +912,7 @@ public partial class QuickInputViewModel : ObservableObject
ProgressText = $"{index + 1} / {_rows.Count}";
CurrentStudentIsAbsent = row.IsAbsent;
CurrentStudentAttendanceLabel = row.AttendanceTooltip;
+ CurrentStudentDayHighlight = row.DayHighlight;
AspectRows.Clear();
foreach (var (a, i) in _aspects.Select((a, i) => (a, i)))
diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
index 7d21db3..e89ccc3 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
@@ -547,11 +547,13 @@ public partial class SeatCellViewModel : ObservableObject
[ObservableProperty] private double _lessonOpacity = 1.0;
[ObservableProperty] private string _attendanceBadge = "";
[ObservableProperty] private string _homeworkBadge = "";
+ [ObservableProperty] private string _dayHighlightBadge = "";
[ObservableProperty] private int _raisedHandCount;
[ObservableProperty] private int _calledOnCount;
public ObservableCollection SituationTags { get; } = [];
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
+ public bool HasDayHighlightBadge => DayHighlightBadge.Length > 0;
public bool ShowLessonOverview => IsOccupied && !CanEdit;
[ObservableProperty] private bool _canRecordLesson;
public bool IsOccupied => SelectedOption.StudentId.HasValue;
@@ -644,6 +646,7 @@ public partial class SeatCellViewModel : ObservableObject
var attendance = entry?.Attendance;
AttendanceBadge = attendance is null ? "" : AttendanceDisplay.ShortLabel(attendance);
HomeworkBadge = entry is null ? "" : HomeworkDisplay.Symbol(HomeworkDisplay.Effective(entry));
+ DayHighlightBadge = DayHighlightDisplay.Symbol(entry?.DayHighlight);
RaisedHandCount = entry?.RaisedHandCount ?? 0;
CalledOnCount = entry?.CalledOnCount ?? 0;
LessonOpacity = attendance is not null and not AttendanceStatus.Present
@@ -652,6 +655,7 @@ public partial class SeatCellViewModel : ObservableObject
foreach (var choice in SituationTags) choice.IsSelected = selected.Contains(choice.Text);
OnPropertyChanged(nameof(HasAttendanceBadge));
OnPropertyChanged(nameof(HasHomeworkBadge));
+ OnPropertyChanged(nameof(HasDayHighlightBadge));
}
}
@@ -677,6 +681,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
[ObservableProperty] private int _selectedAspectIndex;
[ObservableProperty] private string _attendanceLabel = "Noch nicht kontrolliert";
[ObservableProperty] private string _homeworkLabel = "Keine Hausaufgabe aufgegeben";
+ [ObservableProperty] private string _dayHighlightLabel = "Keine Markierung";
public string StudentName { get; }
public string SessionDisplay { get; }
@@ -687,6 +692,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
public ObservableCollection AspectRows { get; } = [];
public ObservableCollection AttendanceChoices { get; } = [];
public ObservableCollection HomeworkChoices { get; } = [];
+ public ObservableCollection DayHighlightChoices { get; } = [];
public SeatAssessmentViewModel(IParticipationSessionRepository sessions,
IParticipationRepository entries, IParticipationAspectRepository aspects,
@@ -729,6 +735,7 @@ public partial class SeatAssessmentViewModel : ObservableObject
BuildAttendanceChoices();
BuildHomeworkChoices();
+ BuildDayHighlightChoices();
RefreshStatusChoices();
SelectAspect(0);
}
@@ -761,6 +768,17 @@ public partial class SeatAssessmentViewModel : ObservableObject
HomeworkChoices.Add(new("·", "Keine aufgegeben", "⌥X", null, SetHomework));
}
+ private void BuildDayHighlightChoices()
+ {
+ DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Standout),
+ DayHighlightDisplay.Label(DayHighlightKind.Standout), "⇧1", DayHighlightKind.Standout, SetDayHighlight));
+ DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Sleepy),
+ DayHighlightDisplay.Label(DayHighlightKind.Sleepy), "⇧2", DayHighlightKind.Sleepy, SetDayHighlight));
+ DayHighlightChoices.Add(new(DayHighlightDisplay.Symbol(DayHighlightKind.Rough),
+ DayHighlightDisplay.Label(DayHighlightKind.Rough), "⇧3", DayHighlightKind.Rough, SetDayHighlight));
+ DayHighlightChoices.Add(new("·", "Keine Markierung", "⇧X", null, SetDayHighlight));
+ }
+
public void SelectAspect(int index)
{
if (index < 0 || index >= AspectRows.Count) return;
@@ -827,6 +845,17 @@ public partial class SeatAssessmentViewModel : ObservableObject
if (clear || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8) SetHomework(status);
}
+ public void ApplyDayHighlightShortcut(int? digit, bool clear)
+ {
+ if (!CanEdit) return;
+ var kind = clear ? null : digit switch
+ {
+ 1 => DayHighlightKind.Standout, 2 => DayHighlightKind.Sleepy, 3 => DayHighlightKind.Rough,
+ _ => (DayHighlightKind?)null,
+ };
+ if (clear || digit is 1 or 2 or 3) SetDayHighlight(kind);
+ }
+
private void ApplyRating(string key, int? value)
{
if (!CanEdit || _entry is null) return;
@@ -857,13 +886,23 @@ public partial class SeatAssessmentViewModel : ObservableObject
RefreshStatusChoices();
}
+ private void SetDayHighlight(DayHighlightKind? kind)
+ {
+ if (!CanEdit || _entry is null) return;
+ _entry.DayHighlight = kind;
+ _entries.Save(_entry);
+ RefreshStatusChoices();
+ }
+
private void RefreshStatusChoices()
{
AttendanceLabel = AttendanceDisplay.Label(_entry?.Attendance);
HomeworkLabel = HomeworkDisplay.Label(_entry is null ? null : HomeworkDisplay.Effective(_entry));
+ DayHighlightLabel = DayHighlightDisplay.Label(_entry?.DayHighlight);
foreach (var choice in AttendanceChoices) choice.IsSelected = choice.Status == _entry?.Attendance;
var homework = _entry is null ? null : HomeworkDisplay.Effective(_entry);
foreach (var choice in HomeworkChoices) choice.IsSelected = choice.Status == homework;
+ foreach (var choice in DayHighlightChoices) choice.IsSelected = choice.Kind == _entry?.DayHighlight;
}
}
@@ -966,6 +1005,17 @@ public partial class SeatHomeworkChoice(string symbol, string label, string shor
[RelayCommand] private void Apply() => apply(Status);
}
+public partial class SeatDayHighlightChoice(string symbol, string label, string shortcut,
+ DayHighlightKind? kind, Action apply) : ObservableObject
+{
+ public string Symbol { get; } = symbol;
+ public string Label { get; } = label;
+ public string Shortcut { get; } = shortcut;
+ public DayHighlightKind? Kind { get; } = kind;
+ [ObservableProperty] private bool _isSelected;
+ [RelayCommand] private void Apply() => apply(Kind);
+}
+
public partial class SeatingPlanDialogViewModel : ObservableObject
{
private readonly ISeatingPlanRepository _plans;
diff --git a/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs
new file mode 100644
index 0000000..907a9a1
--- /dev/null
+++ b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs
@@ -0,0 +1,310 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using LehrerApp.Core.Interfaces;
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using LehrerApp.Desktop.ViewModels.Students;
+using System.Collections.ObjectModel;
+using System.Globalization;
+
+namespace LehrerApp.Desktop.ViewModels.Groups;
+
+// ── Schüler-Leistungsüberblick + Zielnoten-Rechner (Nutzer-Feedback) ─────────────
+//
+// Zeigt die Leistungen eines einzelnen Schülers in einem Kurs im Überblick — Klausuren,
+// Mitarbeit- und sonstige Noten, die daraus berechnete Zeugnisnote sowie zwei Rechner
+// ("Zielnote": was brauche ich noch? / "Was-wäre-wenn": wie wirkt sich eine zusätzliche
+// Klausurnote aus?). Bewusst pro Kurs statt fächerübergreifend, da Gewichtungsschema und
+// Notensystem am Kurs hängen. Zwei Modi über IsTeacherMode: im Bewertermodus zusätzlich
+// Kursdurchschnitt je Klausur und Bearbeitbarkeit der Mitarbeit-/Sonstige-Noten; im
+// Schülermodus ausschließlich die eigenen Werte, rein lesend — gedacht, um den Bildschirm im
+// Gespräch umzudrehen. Exam-Noten bleiben bewusst nur lesbar: eine Korrektur läuft weiterhin
+// über den bestehenden ExamGradingDialog, der die Punkte-Struktur konsistent hält — direktes
+// Überschreiben von Exam.Grade hier könnte mit ExamResult.Points/TotalPoints auseinanderlaufen.
+public partial class StudentPerformanceOverviewViewModel : ObservableObject
+{
+ private readonly IGradeRepository _grades;
+ private readonly IExamRepository _exams;
+ private readonly IExamResultRepository _results;
+ private readonly IGroupMembershipRepository _memberships;
+ private readonly IGradingSchemeRepository _schemes;
+ private readonly GradingService _grading;
+
+ private readonly Guid _groupId;
+ private readonly Guid _studentId;
+ private readonly GroupType _groupType;
+ private readonly GradingSystem _gradingSystem;
+ private readonly string _schoolYear;
+
+ private GradingScheme _scheme = new();
+ private double? _examsAverage;
+ private double? _participationAverage;
+ private double? _otherAverage;
+ private List<(string Grade, double Weight)> _examGradesRaw = [];
+ private List<(string Grade, double Weight)> _participationGradesRaw = [];
+ private List<(string Grade, double Weight)> _otherGradesRaw = [];
+
+ public string StudentName { get; }
+ public string GroupLabel { get; }
+
+ [ObservableProperty] private ParticipationPeriodOption _selectedPeriod;
+ [ObservableProperty] private bool _isTeacherMode = true;
+ [ObservableProperty] private string _schemeSummary = "";
+ [ObservableProperty] private string? _examsAverageDisplay;
+ [ObservableProperty] private string? _participationAverageDisplay;
+ [ObservableProperty] private string? _otherAverageDisplay;
+ [ObservableProperty] private string _currentReportGradeDisplay = "";
+
+ [ObservableProperty] private string _targetGradeText = "";
+ [ObservableProperty] private GradeBucketKind _targetBucket = GradeBucketKind.Exams;
+ [ObservableProperty] private string _targetResultDisplay = "";
+
+ [ObservableProperty] private string _whatIfExamGradeText = "";
+ [ObservableProperty] private string _whatIfResultDisplay = "";
+
+ public List PeriodOptions { get; } =
+ [
+ new(ParticipationPeriod.FullYear, "Gesamtes Schuljahr"),
+ new(ParticipationPeriod.H1, "1. Halbjahr"),
+ new(ParticipationPeriod.H2, "2. Halbjahr"),
+ ];
+
+ public string[] TargetBucketOptions { get; } = ["Klausuren", "Mitarbeit", "Sonstige"];
+
+ public string TargetBucketName
+ {
+ get => TargetBucket switch
+ {
+ GradeBucketKind.Participation => "Mitarbeit",
+ GradeBucketKind.Other => "Sonstige",
+ _ => "Klausuren",
+ };
+ set => TargetBucket = value switch
+ {
+ "Mitarbeit" => GradeBucketKind.Participation,
+ "Sonstige" => GradeBucketKind.Other,
+ _ => GradeBucketKind.Exams,
+ };
+ }
+
+ public ObservableCollection ExamRows { get; } = [];
+ /// Eigener Verlauf (Nutzer-Feedback): dieselbe Balken-Sparkline wie die bestehende
+ /// Notenentwicklung im Schülerdetail (2.5), hier nur auf die Klausuren dieses Kurses
+ /// beschränkt statt aller Lerngruppen — passt zum Kursdurchschnitt-Vergleich je Klausur.
+ public StudentGradeHistoryGroup ExamHistory { get; } = new("Klausurverlauf");
+ public ObservableCollection ParticipationRows { get; } = [];
+ public ObservableCollection OtherRows { get; } = [];
+
+ public StudentPerformanceOverviewViewModel(IGradeRepository grades, IExamRepository exams,
+ IExamResultRepository results, IGroupMembershipRepository memberships,
+ IGradingSchemeRepository schemes, GradingService grading,
+ Guid groupId, Guid studentId, GroupType groupType, GradingSystem gradingSystem,
+ string studentName, string groupLabel, string schoolYear)
+ {
+ _grades = grades; _exams = exams; _results = results; _memberships = memberships;
+ _schemes = schemes; _grading = grading;
+ _groupId = groupId; _studentId = studentId; _groupType = groupType;
+ _gradingSystem = gradingSystem; _schoolYear = schoolYear;
+ StudentName = studentName; GroupLabel = groupLabel;
+
+ _selectedPeriod = PeriodOptions[0];
+ Recompute();
+ }
+
+ partial void OnSelectedPeriodChanged(ParticipationPeriodOption value) => Recompute();
+
+ public string ModeButtonLabel => IsTeacherMode ? "Modus: Bewerter" : "Modus: Schüler";
+
+ partial void OnIsTeacherModeChanged(bool value) => OnPropertyChanged(nameof(ModeButtonLabel));
+
+ [RelayCommand]
+ private void ToggleMode() => IsTeacherMode = !IsTeacherMode;
+
+ private GradingScheme ResolveScheme() =>
+ _schemes.GetByGroup(_groupId)
+ ?? _schemes.GetDefaultForType(_groupType)
+ ?? new GradingScheme { ExamsPercent = 50, ParticipationPercent = 40, OtherPercent = 10 };
+
+ private void Recompute()
+ {
+ _scheme = ResolveScheme();
+ SchemeSummary = $"Klausuren {_scheme.ExamsPercent:0.#} % · Mitarbeit {_scheme.ParticipationPercent:0.#} % · " +
+ $"Sonstige {_scheme.OtherPercent:0.#} %";
+
+ var (periodFrom, periodTo) = GroupMembershipService.SchoolYearPeriod(_schoolYear, SelectedPeriod.Period switch
+ {
+ ParticipationPeriod.H1 => SchoolYearPeriodKind.H1,
+ ParticipationPeriod.H2 => SchoolYearPeriodKind.H2,
+ _ => SchoolYearPeriodKind.FullYear,
+ });
+ var membership = _memberships.GetByGroup(_groupId).FirstOrDefault(m => m.StudentId == _studentId);
+
+ // ── Klausuren ─────────────────────────────────────────────────────
+ ExamRows.Clear();
+ ExamHistory.Points.Clear();
+ _examGradesRaw = [];
+ int? previousNoteEquivalent = null;
+ foreach (var exam in _exams.GetByGroup(_groupId)
+ .Where(e => e.Date >= periodFrom && e.Date <= periodTo)
+ .Where(e => membership is null || GroupMembershipService.IsActiveOn(membership, e.Date))
+ .Where(e => !e.Niveau.HasValue || membership?.Niveau == e.Niveau)
+ .OrderBy(e => e.Date))
+ {
+ var allResults = _results.GetByExam(exam.Id);
+ var own = allResults.FirstOrDefault(r => r.StudentId == _studentId);
+ string? ownGrade = own is { Absent: false, Grade: not null } ? own.Grade : null;
+ if (ownGrade is not null) _examGradesRaw.Add((ownGrade, 1.0));
+
+ var classGrades = allResults.Where(r => !r.Absent && r.Grade is not null)
+ .Select(r => (r.Grade!, 1.0)).ToList();
+ var classAverage = classGrades.Count > 0 ? _grading.WeightedAverage(classGrades) : (double?)null;
+
+ ExamRows.Add(new StudentExamRow(exam.Title, exam.Date, ownGrade,
+ classAverage.HasValue ? FormatGrade(classAverage.Value) : "–"));
+
+ // Gleiche Balken-Sparkline wie die bestehende Notenentwicklung (2.5): Rohwert auf die
+ // 1..6-Notenäquivalent-Achse abbilden, damit Grades1To6 und Points0To15 dieselbe
+ // Balkenhöhen-Formel nutzen können.
+ int? noteEquivalent = ownGrade is not null && int.TryParse(ownGrade, out var raw)
+ ? (_gradingSystem == GradingSystem.Grades1To6 ? raw : int.Parse(PointsNoteMapping.PointsToNote(raw)))
+ : null;
+ var isFailing = noteEquivalent is >= 5;
+ var isDrop = noteEquivalent.HasValue && previousNoteEquivalent.HasValue
+ && noteEquivalent.Value - previousNoteEquivalent.Value >= 1;
+ var warnings = new List();
+ if (isDrop) warnings.Add("Abfall um ≥ 1 Note");
+ if (isFailing) warnings.Add("Versetzungsgefährdung");
+ ExamHistory.Points.Add(new GradeHistoryPoint(exam.Date, exam.Title, ownGrade ?? "–",
+ noteEquivalent, warnings.Count > 0, string.Join(" · ", warnings)));
+ if (noteEquivalent.HasValue) previousNoteEquivalent = noteEquivalent;
+ }
+ _examsAverage = _examGradesRaw.Count > 0 ? _grading.WeightedAverage(_examGradesRaw) : null;
+
+ // ── Mitarbeit & Sonstige ──────────────────────────────────────────
+ var allGrades = _grades.GetByStudentAndGroup(_studentId, _groupId)
+ .Where(g => g.Date >= periodFrom && g.Date <= periodTo)
+ .Where(g => membership is null || GroupMembershipService.IsActiveOn(membership, g.Date))
+ .OrderBy(g => g.Date).ToList();
+
+ ParticipationRows.Clear();
+ _participationGradesRaw = [];
+ foreach (var g in allGrades.Where(g => g.Category == GradeCategory.Participation))
+ {
+ _participationGradesRaw.Add((g.Value, g.Weight));
+ ParticipationRows.Add(new StudentSimpleGradeRow(g, SaveGrade));
+ }
+ _participationAverage = _participationGradesRaw.Count > 0 ? _grading.WeightedAverage(_participationGradesRaw) : null;
+
+ OtherRows.Clear();
+ _otherGradesRaw = [];
+ foreach (var g in allGrades.Where(g => g.Category != GradeCategory.Participation))
+ {
+ _otherGradesRaw.Add((g.Value, g.Weight));
+ OtherRows.Add(new StudentSimpleGradeRow(g, SaveGrade));
+ }
+ _otherAverage = _otherGradesRaw.Count > 0 ? _grading.WeightedAverage(_otherGradesRaw) : null;
+
+ ExamsAverageDisplay = _examsAverage.HasValue ? FormatGrade(_examsAverage.Value) : null;
+ ParticipationAverageDisplay = _participationAverage.HasValue ? FormatGrade(_participationAverage.Value) : null;
+ OtherAverageDisplay = _otherAverage.HasValue ? FormatGrade(_otherAverage.Value) : null;
+
+ var reportGrade = _grading.CalculateReportGrade(_examGradesRaw, _participationGradesRaw, _otherGradesRaw,
+ _scheme, _gradingSystem, RoundingRule.Commercial);
+ CurrentReportGradeDisplay = reportGrade is null
+ ? "Noch nicht berechenbar — keine Werte im gewählten Zeitraum."
+ : $"Note {reportGrade}";
+
+ RecomputeTarget();
+ RecomputeWhatIf();
+ }
+
+ private void SaveGrade(Grade grade)
+ {
+ _grades.Save(grade);
+ Recompute();
+ }
+
+ partial void OnTargetGradeTextChanged(string value) => RecomputeTarget();
+ partial void OnTargetBucketChanged(GradeBucketKind value)
+ {
+ OnPropertyChanged(nameof(TargetBucketName));
+ RecomputeTarget();
+ }
+
+ private void RecomputeTarget()
+ {
+ if (!TryParseGrade(TargetGradeText, out var target))
+ {
+ TargetResultDisplay = "";
+ return;
+ }
+
+ var result = ReportGradeTargetCalculator.SolveRequiredAverage(target, TargetBucket,
+ _examsAverage, _participationAverage, _otherAverage, _scheme, _gradingSystem);
+
+ TargetResultDisplay = result switch
+ {
+ null => $"{TargetBucketName} fließt laut Gewichtungsschema nicht in die Zeugnisnote ein.",
+ { IsAchievable: true } r => $"Nötiger Schnitt in {TargetBucketName}: {FormatGrade(r.RequiredAverage)}",
+ { IsAchievable: false } r =>
+ $"Mit den übrigen Bereichen rechnerisch nicht mehr erreichbar (bräuchte {FormatGrade(r.RequiredAverage)}).",
+ };
+ }
+
+ partial void OnWhatIfExamGradeTextChanged(string value) => RecomputeWhatIf();
+
+ private void RecomputeWhatIf()
+ {
+ if (string.IsNullOrWhiteSpace(WhatIfExamGradeText))
+ {
+ WhatIfResultDisplay = "";
+ return;
+ }
+
+ var hypothetical = new List<(string Grade, double Weight)>(_examGradesRaw) { (WhatIfExamGradeText.Trim(), 1.0) };
+ var result = _grading.CalculateReportGrade(hypothetical, _participationGradesRaw, _otherGradesRaw,
+ _scheme, _gradingSystem, RoundingRule.Commercial);
+ WhatIfResultDisplay = result is null
+ ? "Ungültige Note."
+ : $"Zeugnisnote mit dieser zusätzlichen Klausurnote: {result}";
+ }
+
+ private static bool TryParseGrade(string text, out double value) =>
+ double.TryParse(text.Trim().Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out value);
+
+ private static string FormatGrade(double value) => value.ToString("0.0", CultureInfo.InvariantCulture);
+}
+
+public class StudentExamRow(string title, DateOnly date, string? ownGrade, string classAverageDisplay)
+{
+ public string Title { get; } = title;
+ public string DateDisplay { get; } = date.ToString("dd.MM.yyyy");
+ public string OwnGradeDisplay { get; } = ownGrade ?? "–";
+ public string ClassAverageDisplay { get; } = classAverageDisplay;
+}
+
+public partial class StudentSimpleGradeRow : ObservableObject
+{
+ private readonly Grade _grade;
+ private readonly Action _save;
+
+ public string CategoryLabel { get; }
+ public string DateDisplay { get; }
+ [ObservableProperty] private string _value;
+
+ public StudentSimpleGradeRow(Grade grade, Action save)
+ {
+ _grade = grade;
+ _save = save;
+ CategoryLabel = GradeCategoryDisplay.Label(grade.Category);
+ DateDisplay = grade.Date.ToString("dd.MM.yyyy");
+ _value = grade.Value;
+ }
+
+ [RelayCommand]
+ private void Save()
+ {
+ _grade.Value = Value.Trim();
+ _save(_grade);
+ }
+}
diff --git a/LehrerApp.Desktop/Views/Groups/GradeOverviewTabView.axaml b/LehrerApp.Desktop/Views/Groups/GradeOverviewTabView.axaml
index 1541205..2366e7a 100644
--- a/LehrerApp.Desktop/Views/Groups/GradeOverviewTabView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/GradeOverviewTabView.axaml
@@ -19,6 +19,9 @@
+