diff --git a/LehrerApp.Core/Models/Participation.cs b/LehrerApp.Core/Models/Participation.cs
index a6d8b27..af32d6a 100644
--- a/LehrerApp.Core/Models/Participation.cs
+++ b/LehrerApp.Core/Models/Participation.cs
@@ -24,6 +24,14 @@ public class ParticipationEntry
// Bleibt für bereits gespeicherte Daten erhalten. Neue Schreibvorgänge setzen beide Felder.
public bool HomeworkMissing { get; set; }
public AttendanceStatus? Attendance { get; set; }
+ // Strichliste im Sitzplan (Nutzer-Feedback): RaisedHandCount zählt jede beobachtete Meldung,
+ // CalledOnCount die Teilmenge davon, bei der der Schüler auch drangekommen ist —
+ // "drangekommen" ist immer auch eine Meldung und erhöht daher immer beide Zähler zugleich,
+ // nie CalledOnCount allein. Ergibt nebenbei eine Aufrufgerechtigkeits-Quote
+ // (CalledOnCount / RaisedHandCount) und schlägt die Quantitätsbewertung vor, siehe
+ // ParticipationCountSuggestion.
+ public int RaisedHandCount { get; set; }
+ public int CalledOnCount { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -160,3 +168,27 @@ public static class ParticipationRatingScale
_ => value,
};
}
+
+///
+/// Nutzer-Feedback: die Strichliste im Sitzplan ()
+/// schlägt direkt eine Quantitätsbewertung vor — wie oft sich jemand meldet, ist ja bereits die
+/// Quantität. Nur ein Richtwert, kein fester Automatismus: die Lehrkraft übernimmt den Vorschlag
+/// per Klick oder ignoriert ihn. Bewusst nur für (der
+/// Standardtyp des "quantity"-Aspekts) — für individuell umkonfigurierte Aspekttypen gäbe es keine
+/// sinnvolle, unmissverständliche Abbildung.
+///
+public static class ParticipationCountSuggestion
+{
+ public static int? SuggestQuantity(AspectValueType type, int raisedHandCount)
+ {
+ if (type != AspectValueType.Scale5) return null;
+ return raisedHandCount switch
+ {
+ 0 => -2,
+ 1 => -1,
+ 2 or 3 => 0,
+ 4 or 5 => 1,
+ _ => 2,
+ };
+ }
+}
diff --git a/LehrerApp.Core/Services/DashboardSettingsService.cs b/LehrerApp.Core/Services/DashboardSettingsService.cs
index f11aa93..9366a8f 100644
--- a/LehrerApp.Core/Services/DashboardSettingsService.cs
+++ b/LehrerApp.Core/Services/DashboardSettingsService.cs
@@ -15,7 +15,7 @@ public sealed class DashboardSettingsService
public static readonly string[] DefaultCardOrder =
[
"today", "tasks", "calendar", "excuses", "upcoming",
- "corrections", "unplanned", "alerts", "attendance", "support", "groups",
+ "corrections", "unplanned", "alerts", "attendance", "support", "groups", "examload",
];
private readonly string _configPath;
diff --git a/LehrerApp.Core/Services/ExamWeekLoadService.cs b/LehrerApp.Core/Services/ExamWeekLoadService.cs
new file mode 100644
index 0000000..77e0abe
--- /dev/null
+++ b/LehrerApp.Core/Services/ExamWeekLoadService.cs
@@ -0,0 +1,54 @@
+using System.Globalization;
+using LehrerApp.Core.Models;
+
+namespace LehrerApp.Core.Services;
+
+///
+/// Zählt eigene Klausuren pro ISO-Kalenderwoche über alle Lerngruppen hinweg. Nutzer-Feedback:
+/// die klassenbezogene Kollisionsprüfung (mehrere Klausuren einer einzelnen Klasse in einer
+/// Woche) übernimmt bereits der schulische Klausurplaner (externes Klassenbuch) — was dort
+/// fehlt, ist die persönliche Belastung der Lehrkraft: mehr eigene Klausuren in derselben Woche
+/// bedeuten unabhängig von der Klasse mehr Erstellungsaufwand vorher und mehr Korrekturaufwand
+/// danach. Da eine LiteDB-Datei genau einem Nutzer gehört (siehe CLAUDE.md), sind "alle
+/// Klausuren in der Datenbank" bereits gleichbedeutend mit "meine Klausuren".
+///
+public static class ExamWeekLoadService
+{
+ /// Ab dieser Anzahl eigener Klausuren in derselben Woche gilt die Woche als spürbar belastet.
+ /// Nutzer-Feedback: zwei Klausuren aus zwei unterrichteten Fächern in derselben Woche sind
+ /// normal, eine dritte ist die eigentliche Häufung.
+ public const int WarningThreshold = 3;
+
+ /// Anzahl Klausuren (ohne , z.B. die gerade im Dialog
+ /// bearbeitete), die in dieselbe ISO-Kalenderwoche wie fallen.
+ public static int CountInSameWeek(IEnumerable allExams, DateOnly date, Guid? excludeExamId = null)
+ {
+ var (year, week) = IsoWeek(date);
+ return allExams.Count(e => e.Id != excludeExamId && IsoWeek(e.Date) == (year, week));
+ }
+
+ /// Gruppiert die übergebenen Klausuren nach ISO-Kalenderwoche und liefert nur die Wochen mit
+ /// mindestens Klausuren, chronologisch sortiert — für einen
+ /// vorausschauenden Überblick (z.B. Dashboard), nicht für die Einzelprüfung beim Anlegen.
+ public static List FindOverloadedWeeks(IEnumerable exams) =>
+ exams
+ .GroupBy(e => IsoWeek(e.Date))
+ .Where(g => g.Count() >= WarningThreshold)
+ .Select(g => new ExamWeekLoad(g.Key.Year, g.Key.Week, WeekStart(g.Key.Year, g.Key.Week), g.Count()))
+ .OrderBy(w => w.WeekStart)
+ .ToList();
+
+ private static (int Year, int Week) IsoWeek(DateOnly date)
+ {
+ var dt = date.ToDateTime(TimeOnly.MinValue);
+ return (ISOWeek.GetYear(dt), ISOWeek.GetWeekOfYear(dt));
+ }
+
+ private static DateOnly WeekStart(int isoYear, int isoWeek) =>
+ DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday));
+}
+
+public readonly record struct ExamWeekLoad(int IsoYear, int IsoWeek, DateOnly WeekStart, int ExamCount)
+{
+ public DateOnly WeekEnd => WeekStart.AddDays(6);
+}
diff --git a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
index 1fcf3f1..dc6235f 100644
--- a/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/DashboardViewModelTests.cs
@@ -1,6 +1,7 @@
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels;
+using System.Globalization;
using Xunit;
namespace LehrerApp.Desktop.Tests;
@@ -75,6 +76,67 @@ public sealed class DashboardViewModelTests
substitutions ?? new FakeSubstitutionEntries(), annualPlanEvents);
}
+ /// Montag einer Woche, die garantiert in der Zukunft liegt und innerhalb der
+ /// Klausurwochen-Vorausschau — unabhängig davon, welcher Wochentag "heute" gerade ist.
+ private static DateOnly NextIsoWeekMonday()
+ {
+ var reference = DateTime.Today.AddDays(7);
+ return DateOnly.FromDateTime(
+ ISOWeek.ToDateTime(ISOWeek.GetYear(reference), ISOWeek.GetWeekOfYear(reference), DayOfWeek.Monday));
+ }
+
+ [Fact]
+ public void ExamWeekLoads_DreiGeplanteKlausurenInDerselbenWoche_ErzeugtEintrag()
+ {
+ var group = new LearningGroup { Name = "9c" };
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var monday = NextIsoWeekMonday();
+ var exams = new FakeExams([
+ new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
+ new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
+ new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
+ ]);
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
+
+ var entry = Assert.Single(vm.ExamWeekLoads);
+ Assert.Equal(3, entry.ExamCount);
+ Assert.True(vm.ExamLoadCard.EffectiveIsVisible);
+ }
+
+ [Fact]
+ public void ExamWeekLoads_WenigeKlausurenProWoche_BleibtLeer()
+ {
+ var group = new LearningGroup { Name = "9c" };
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var monday = NextIsoWeekMonday();
+ var exams = new FakeExams([
+ new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Planned },
+ new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Planned },
+ ]);
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
+
+ Assert.Empty(vm.ExamWeekLoads);
+ }
+
+ [Fact]
+ public void ExamWeekLoads_BereitsDurchgefuehrteKlausurenZaehlenNicht()
+ {
+ var group = new LearningGroup { Name = "9c" };
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var monday = NextIsoWeekMonday();
+ var exams = new FakeExams([
+ new Exam { GroupId = group.Id, Title = "a", Date = monday, Status = ExamStatus.Conducted },
+ new Exam { GroupId = group.Id, Title = "b", Date = monday.AddDays(1), Status = ExamStatus.Graded },
+ new Exam { GroupId = group.Id, Title = "c", Date = monday.AddDays(2), Status = ExamStatus.Planned },
+ ]);
+
+ var vm = BuildVm(group, new Lesson { GroupId = group.Id, Date = today }, exams: exams);
+
+ Assert.Empty(vm.ExamWeekLoads);
+ }
+
[Fact]
public void TodaysLessons_LoestRaumUeberPassendenStundenplanSlotAuf()
{
diff --git a/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
new file mode 100644
index 0000000..db8f94b
--- /dev/null
+++ b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
@@ -0,0 +1,64 @@
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using LehrerApp.Desktop.ViewModels.Groups;
+using Xunit;
+
+namespace LehrerApp.Desktop.Tests;
+
+public sealed class ExamDialogViewModelTests
+{
+ private static ExamDialogViewModel BuildViewModel(List existingExams, string dateText,
+ Exam? editingExam = null)
+ {
+ var vm = new ExamDialogViewModel(new FakeExams(existingExams), new FakeCompetencyDomains(),
+ new FakeGradingKeyTemplates(), new GradingService(), Guid.NewGuid(), null, 9,
+ GradingSystem.Grades1To6, "Chemie", isDifferentiated: false, editingExam, null);
+ vm.DateText = dateText;
+ return vm;
+ }
+
+ private static Exam MakeExam(DateOnly date) => new() { GroupId = Guid.NewGuid(), Title = "x", Date = date };
+
+ [Fact]
+ public void WeekLoadHint_WenigeKlausurenInDerWoche_BleibtLeer()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var existing = new List { MakeExam(monday) };
+
+ var vm = BuildViewModel(existing, monday.AddDays(1).ToString("dd.MM.yyyy"));
+
+ Assert.Equal("", vm.WeekLoadHint);
+ }
+
+ [Fact]
+ public void WeekLoadHint_DritteKlausurInDerWoche_ZeigtHinweis()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var existing = new List { MakeExam(monday), MakeExam(monday.AddDays(1)) };
+
+ var vm = BuildViewModel(existing, monday.AddDays(2).ToString("dd.MM.yyyy"));
+
+ Assert.Contains("3 Klausuren", vm.WeekLoadHint);
+ }
+
+ [Fact]
+ public void WeekLoadHint_BearbeiteteKlausurZaehltNichtDoppelt()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var editing = MakeExam(monday);
+ var existing = new List { editing, MakeExam(monday.AddDays(1)) };
+
+ // Nur die eigene Klausur (editing) + eine weitere -> 2 insgesamt, kein Hinweis.
+ var vm = BuildViewModel(existing, monday.ToString("dd.MM.yyyy"), editing);
+
+ Assert.Equal("", vm.WeekLoadHint);
+ }
+
+ [Fact]
+ public void WeekLoadHint_UngueltigesDatum_BleibtLeer()
+ {
+ var vm = BuildViewModel([], "keindatum");
+
+ Assert.Equal("", vm.WeekLoadHint);
+ }
+}
diff --git a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
index fb75fdd..fca4c2d 100644
--- a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs
@@ -83,6 +83,26 @@ public sealed class QuickInputViewModelTests
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
}
+ [Fact]
+ public void ClearRating_SetztVersehentlichGesetzteBewertungAufNichtBewertetZurueck()
+ {
+ 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.SetRatingByNumber(4); // aus Versehen eine Note für Qualität vergeben
+
+ vm.ClearRating();
+
+ Assert.Null(vm.AspectRows[0].Value);
+ Assert.Null(rows[0].GetRating("quality"));
+ }
+
[Fact]
public void VorherigerAspekt_SpringtRueckwaertsMitUmlauf()
{
diff --git a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
index 571e333..46c06f7 100644
--- a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs
@@ -483,6 +483,35 @@ public sealed class SeatingPlanViewModelTests
Assert.True(entry.HomeworkMissing);
}
+ [Fact]
+ public void SitzplatzBewertung_SchlaegtQuantitaetAusStrichlisteVor()
+ {
+ var groupId = Guid.NewGuid();
+ var studentId = Guid.NewGuid();
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ var session = new ParticipationSession { GroupId = groupId, Date = today };
+ var sessions = new FakeSessions([session]);
+ var entries = new FakeEntries();
+ entries.Add(new ParticipationEntry { SessionId = session.Id, StudentId = studentId, RaisedHandCount = 3 });
+
+ var vm = new SeatAssessmentViewModel(sessions, entries, new FakeAspects(),
+ groupId, studentId, "Beispiel, Anna", canEdit: true);
+
+ var quantityRow = vm.AspectRows.Single(r => r.Key == "quantity");
+ Assert.True(quantityRow.HasSuggestion);
+ Assert.Equal(0, quantityRow.SuggestedValue); // 3 gemeldet -> "∼" laut ParticipationCountSuggestion
+ Assert.Contains("3× gemeldet", quantityRow.SuggestionLabel);
+
+ var qualityRow = vm.AspectRows.Single(r => r.Key == "quality");
+ Assert.False(qualityRow.HasSuggestion);
+
+ quantityRow.ApplySuggestionCommand.Execute(null);
+
+ Assert.Equal(0, quantityRow.Value);
+ var updatedEntry = entries.GetBySessionAndStudent(session.Id, studentId)!;
+ Assert.Equal(0, updatedEntry.Ratings.Single(r => r.Key == "quantity").Value);
+ }
+
[Fact]
public void SitzplatzBewertung_ArchiviertOhneSitzung_LegtKeineNeueSitzungAn()
{
@@ -517,6 +546,60 @@ public sealed class SeatingPlanViewModelTests
Assert.Null(vm.SelectedSession);
}
+ [Fact]
+ public void TallyRaisedHand_ErhoehtNurGemeldetZaehler()
+ {
+ var groupId = Guid.NewGuid();
+ var student = new Student { FirstName = "Anna", LastName = "A" };
+ var plan = new SeatingPlan
+ {
+ GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
+ Assignments = [new SeatAssignment { StudentId = student.Id }],
+ };
+ var sessions = new FakeSessions([]);
+ var entries = new FakeEntries();
+ var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
+ new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
+ sessions, entries, new FakeAspects());
+ vm.Initialize(groupId, isReadOnly: false);
+ var seat = vm.Seats.Single();
+
+ seat.TallyRaisedHandCommand.Execute(null);
+ seat.TallyRaisedHandCommand.Execute(null);
+
+ Assert.Equal(2, seat.RaisedHandCount);
+ Assert.Equal(0, seat.CalledOnCount);
+ var session = Assert.Single(sessions.GetByGroup(groupId));
+ var entry = entries.GetBySessionAndStudent(session.Id, student.Id)!;
+ Assert.Equal(2, entry.RaisedHandCount);
+ Assert.Equal(0, entry.CalledOnCount);
+ }
+
+ [Fact]
+ public void TallyCalledOn_ErhoehtBeideZaehler()
+ {
+ var groupId = Guid.NewGuid();
+ var student = new Student { FirstName = "Anna", LastName = "A" };
+ var plan = new SeatingPlan
+ {
+ GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
+ Assignments = [new SeatAssignment { StudentId = student.Id }],
+ };
+ var sessions = new FakeSessions([]);
+ var entries = new FakeEntries();
+ var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
+ new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]),
+ sessions, entries, new FakeAspects());
+ vm.Initialize(groupId, isReadOnly: false);
+ var seat = vm.Seats.Single();
+
+ seat.TallyRaisedHandCommand.Execute(null); // erste Meldung, nicht drangekommen
+ seat.TallyCalledOnCommand.Execute(null); // zweite Meldung, drangekommen
+
+ Assert.Equal(2, seat.RaisedHandCount);
+ Assert.Equal(1, seat.CalledOnCount);
+ }
+
[Fact]
public async Task AssessStudent_LegtErstBeiTatsaechlicherBewertungEineSitzungAn()
{
diff --git a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
index b6be559..7924cda 100644
--- a/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/DashboardViewModel.cs
@@ -50,6 +50,10 @@ public partial class DashboardViewModel : ObservableObject
/// hat rein rechnerisch schon 100 %, das ist noch kein auffälliges Muster, nur eine zu kleine
/// Stichprobe. Dieselbe Konstante wie GroupOverviewViewModel.AttendanceMinSampleSize.
private const int AttendanceMinSampleSize = 8;
+ /// Vorausschau für die Klausurwochen-Karte (Nutzer-Feedback): weit genug, um eine sich
+ /// anbahnende Häufung noch rechtzeitig vor dem Anlegen weiterer Klausuren zu zeigen, aber
+ /// keine Vorschau auf das ganze Schuljahr.
+ private const int ExamLoadLookaheadDays = 60;
[ObservableProperty] private string _greeting = "";
[ObservableProperty] private string _currentDate = "";
@@ -72,6 +76,7 @@ public partial class DashboardViewModel : ObservableObject
public ObservableCollection CalendarDays { get; } = [];
public ObservableCollection OpenExcuses { get; } = [];
public ObservableCollection AttendanceWarnings { get; } = [];
+ public ObservableCollection ExamWeekLoads { get; } = [];
public ObservableCollection SupportPlanReviews { get; } = [];
public ObservableCollection UpcomingDates { get; } = [];
public ObservableCollection OpenCorrections { get; } = [];
@@ -106,6 +111,7 @@ public partial class DashboardViewModel : ObservableObject
public DashboardCardOption UnplannedCard => Card("unplanned");
public DashboardCardOption AlertsCard => Card("alerts");
public DashboardCardOption AttendanceCard => Card("attendance");
+ public DashboardCardOption ExamLoadCard => Card("examload");
public DashboardCardOption SupportCard => Card("support");
public DashboardCardOption GroupsCard => Card("groups");
public int TodayLessonCount => TodaysLessons.Count;
@@ -209,6 +215,7 @@ public partial class DashboardViewModel : ObservableObject
LoadCalendar();
LoadOpenExcuses(groups.Values.ToList(), today);
LoadAttendanceWarnings(today);
+ LoadExamWeekLoads(groups, today);
LoadSupportPlanReviews(today);
LoadUpcomingDates(groups, today);
LoadOpenCorrections(groups, today);
@@ -228,6 +235,7 @@ public partial class DashboardViewModel : ObservableObject
UnplannedCard.IsEmpty = UnplannedLessons.Count == 0;
AlertsCard.IsEmpty = Alerts.Count == 0;
AttendanceCard.IsEmpty = AttendanceWarnings.Count == 0;
+ ExamLoadCard.IsEmpty = ExamWeekLoads.Count == 0;
SupportCard.IsEmpty = SupportPlanReviews.Count == 0;
GroupsCard.IsEmpty = CurrentGroups.Count == 0;
@@ -322,6 +330,27 @@ public partial class DashboardViewModel : ObservableObject
AttendanceWarnings.Add(item);
}
+ // ── Klausurwochen (Nutzer-Feedback) ───────────────────────────────────────
+ //
+ // Persönliche Klausurlast über alle Kurse hinweg — anders als die klassenbezogene
+ // Kollisionsprüfung, die bereits der schulische Klausurplaner übernimmt (siehe
+ // ExamWeekLoadService). Nur geplante, noch bevorstehende Klausuren zählen: die
+ // Vorausschau soll helfen, eine sich anbahnende Woche zu erkennen, bevor man eine weitere
+ // Klausur genau dort einträgt — bereits durchgeführte/korrigierte Klausuren werden schon
+ // von der Karte "Offene Korrekturen" abgedeckt.
+
+ private void LoadExamWeekLoads(IReadOnlyDictionary groups, DateOnly today)
+ {
+ ExamWeekLoads.Clear();
+ var lastDay = today.AddDays(ExamLoadLookaheadDays);
+ var upcoming = groups.Keys
+ .SelectMany(gid => _exams.GetByGroup(gid))
+ .Where(e => e.Status == ExamStatus.Planned && e.Date >= today && e.Date <= lastDay);
+
+ foreach (var week in ExamWeekLoadService.FindOverloadedWeeks(upcoming))
+ ExamWeekLoads.Add(new ExamWeekLoadItem(week.IsoWeek, week.WeekStart, week.WeekEnd, week.ExamCount));
+ }
+
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────
private void LoadSupportPlanReviews(DateOnly today)
@@ -547,7 +576,7 @@ public partial class DashboardViewModel : ObservableObject
"excuses" => "Offene Entschuldigungen", "upcoming" => "Anstehende Termine",
"corrections" => "Offene Korrekturen", "unplanned" => "Ungeplante Stunden", "alerts" => "Auffälligkeiten",
"attendance" => "Fehlzeiten-Warnung", "support" => "Förderplan-Wiedervorlage",
- "groups" => "Meine Lerngruppen", _ => key,
+ "groups" => "Meine Lerngruppen", "examload" => "Klausurwochen", _ => key,
};
private void ApplyCardLayout()
@@ -966,6 +995,13 @@ public class AttendanceWarningItem(Guid studentId, string studentName, double ab
public double AbsenceRatePercent { get; } = absenceRatePercent;
}
+public class ExamWeekLoadItem(int isoWeek, DateOnly weekStart, DateOnly weekEnd, int examCount)
+{
+ public int ExamCount { get; } = examCount;
+ public string Label => $"KW {isoWeek} ({weekStart:dd.MM.}–{weekEnd:dd.MM.})";
+ public string CountDisplay => ExamCount == 1 ? "1 Klausur" : $"{ExamCount} Klausuren";
+}
+
// ── Förderplan-Wiedervorlage (5.3.2) ──────────────────────────────────────────
public class SupportPlanDueItem
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
index 9a27cd3..c39716c 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
@@ -32,6 +32,7 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private string _titleError = "";
[ObservableProperty] private string _dateTextError = "";
[ObservableProperty] private string _returnedAtTextError = "";
+ [ObservableProperty] private string _weekLoadHint = "";
[ObservableProperty] private double _totalPoints;
[ObservableProperty] private bool _hasCompetencyCatalog;
[ObservableProperty] private bool _useWeighting;
@@ -108,6 +109,7 @@ public partial class ExamDialogViewModel : ObservableObject
}
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
RecomputeTotals();
+ UpdateWeekLoadHint();
}
partial void OnUseWeightingChanged(bool value)
@@ -115,6 +117,26 @@ public partial class ExamDialogViewModel : ObservableObject
foreach (var t in Tasks) t.ShowWeight = value;
}
+ partial void OnDateTextChanged(string value) => UpdateWeekLoadHint();
+
+ /// Nutzer-Feedback: persönliche Klausurlast pro Woche — die klassenbezogene
+ /// Kollisionsprüfung übernimmt bereits der schulische Klausurplaner (siehe
+ /// ExamWeekLoadService), hier geht es um die eigene Häufung über alle Kurse hinweg.
+ private void UpdateWeekLoadHint()
+ {
+ if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
+ {
+ WeekLoadHint = "";
+ return;
+ }
+
+ var existing = ExamWeekLoadService.CountInSameWeek(_exams.GetAll(), date, _editingExam?.Id);
+ var totalIfSaved = existing + 1;
+ WeekLoadHint = totalIfSaved >= ExamWeekLoadService.WarningThreshold
+ ? $"Damit hättest du {totalIfSaved} Klausuren in dieser Woche."
+ : "";
+ }
+
[RelayCommand]
private void AddTask()
{
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
index b42f23a..8f01c2c 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs
@@ -824,7 +824,7 @@ 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 · Esc schließen";
+ "Enter/→ nächster Schüler · Backspace/← vorheriger Schüler · +/− anpassen · Entf nicht bewertet · Esc schließen";
}
}
@@ -902,6 +902,13 @@ public partial class QuickInputViewModel : ObservableObject
ApplyRating(steps[Math.Clamp(idx + 1, 0, steps.Count - 1)]);
}
+ /// Nutzer-Feedback: eine aus Versehen gesetzte Bewertung (z.B. Qualität, obwohl der Schüler
+ /// sich nie gemeldet hat) muss sich wieder auf "nicht bewertet" zurücksetzen lassen, ohne
+ /// einen inhaltlich falschen Wert stehen lassen zu müssen. Bewusst eine eigene Aktion statt
+ /// über 0/1 zu laufen — 0 ("--") ist bei Quantität ein echter, beobachteter Wert ("hat sich
+ /// nicht gemeldet"), kein Platzhalter für "nicht ermittelt".
+ public void ClearRating() => ApplyRating(null);
+
public void DecrementRating()
{
var cell = GetCurrentCell();
@@ -918,7 +925,7 @@ public partial class QuickInputViewModel : ObservableObject
ApplyRating(steps[Math.Clamp(idx - 1, 0, steps.Count - 1)]);
}
- private void ApplyRating(int val)
+ private void ApplyRating(int? val)
{
if (_rows.Count == 0 || !_aspects.Any()) return;
var key = _aspects[Math.Clamp(AspectIndex, 0, _aspects.Count - 1)].Key;
diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
index b76ebc6..7d21db3 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs
@@ -227,7 +227,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
?? StudentSeatOption.Empty;
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
- CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden));
+ CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden, TallyParticipation));
}
UpdateAssignmentSummary();
RefreshSeatLessonData();
@@ -266,6 +266,22 @@ public partial class SeatingPlanTabViewModel : ObservableObject
}
}
+ /// Strichliste im Sitzplan (Nutzer-Feedback): "drangekommen" ist immer auch eine Meldung und
+ /// erhöht daher beide Zähler — es gibt kein "drangekommen, ohne sich gemeldet zu haben".
+ private void TallyParticipation(SeatCellViewModel seat, bool calledOn)
+ {
+ if (!IsEditable || seat.SelectedOption.StudentId is not Guid studentId) return;
+ var session = EnsureTodaySession();
+ if (session is null) return;
+
+ var entry = _participation.GetBySessionAndStudent(session.Id, studentId)
+ ?? new ParticipationEntry { SessionId = session.Id, StudentId = studentId };
+ entry.RaisedHandCount++;
+ if (calledOn) entry.CalledOnCount++;
+ _participation.Save(entry);
+ RefreshSeatLessonData();
+ }
+
private void ToggleSituationTag(SeatCellViewModel seat, string tag)
{
if (!IsEditable || _documentation is null ||
@@ -531,6 +547,8 @@ public partial class SeatCellViewModel : ObservableObject
[ObservableProperty] private double _lessonOpacity = 1.0;
[ObservableProperty] private string _attendanceBadge = "";
[ObservableProperty] private string _homeworkBadge = "";
+ [ObservableProperty] private int _raisedHandCount;
+ [ObservableProperty] private int _calledOnCount;
public ObservableCollection SituationTags { get; } = [];
public bool HasAttendanceBadge => AttendanceBadge.Length > 0;
public bool HasHomeworkBadge => HomeworkBadge.Length > 0;
@@ -555,10 +573,13 @@ public partial class SeatCellViewModel : ObservableObject
/// die Abblendung für ausgeblendete Plätze hier statt per CSS-Klasse erfolgen.
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
+ private readonly Action _tally;
+
public SeatCellViewModel(int row, int column, ObservableCollection options,
StudentSeatOption selectedOption, Action onChanged, bool canEdit,
Action? toggleSituationTag = null, bool canRecordLesson = false,
- bool isHidden = false, Action? toggleHidden = null)
+ bool isHidden = false, Action? toggleHidden = null,
+ Action? tally = null)
{
Row = row;
Column = column;
@@ -570,10 +591,18 @@ public partial class SeatCellViewModel : ObservableObject
_canRecordLesson = canRecordLesson;
_isHidden = isHidden;
_toggleHidden = toggleHidden ?? (_ => { });
+ _tally = tally ?? ((_, _) => { });
foreach (var tag in SituationTagChoice.DefaultTags)
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
}
+ /// Strichliste im Sitzplan (Nutzer-Feedback): schnelles Mitzählen während des Unterrichts,
+ /// ohne dafür den vollen Sitzplatz-Bewertungsdialog öffnen zu müssen — bewusst nur ein
+ /// Hochzählen ohne Korrekturmöglichkeit hier direkt auf der Kachel; die Rohwerte bleiben über
+ /// den Bewertungsdialog einsehbar und fließen dort als Quantitäts-Vorschlag ein.
+ [RelayCommand] private void TallyRaisedHand() => _tally(this, false);
+ [RelayCommand] private void TallyCalledOn() => _tally(this, true);
+
partial void OnSelectedOptionChanged(StudentSeatOption value)
{
OnPropertyChanged(nameof(IsOccupied));
@@ -615,6 +644,8 @@ 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));
+ RaisedHandCount = entry?.RaisedHandCount ?? 0;
+ CalledOnCount = entry?.CalledOnCount ?? 0;
LessonOpacity = attendance is not null and not AttendanceStatus.Present
and not AttendanceStatus.Late and not AttendanceStatus.SignificantlyLate ? 0.42 : 1.0;
var selected = tags.ToHashSet(StringComparer.OrdinalIgnoreCase);
@@ -690,7 +721,10 @@ public partial class SeatAssessmentViewModel : ObservableObject
foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i)))
{
var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value;
- AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating));
+ // Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback): nur für den
+ // eingebauten "quantity"-Aspekt, siehe ParticipationCountSuggestion.
+ var raisedHandCount = aspect.Key == "quantity" ? _entry?.RaisedHandCount : null;
+ AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating, raisedHandCount));
}
BuildAttendanceChoices();
@@ -847,11 +881,22 @@ public partial class SeatAssessmentAspectRow : ObservableObject
public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value);
public ObservableCollection Choices { get; } = [];
+ /// Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback), siehe
+ /// ParticipationCountSuggestion — null, wenn keine Strichliste zu diesem Aspekt gehört.
+ private readonly int? _raisedHandCount;
+ public int? SuggestedValue { get; }
+ public bool HasSuggestion => SuggestedValue.HasValue && SuggestedValue != Value;
+ public string SuggestionLabel =>
+ $"{_raisedHandCount}× gemeldet → Vorschlag: {ParticipationRatingScale.DisplayLabel(ValueType, SuggestedValue)}";
+
public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value,
- Action apply)
+ Action apply, int? raisedHandCount = null)
{
Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType;
MaxPoints = aspect.MaxPoints; _value = value; _apply = apply;
+ _raisedHandCount = raisedHandCount;
+ SuggestedValue = raisedHandCount is int rhc
+ ? ParticipationCountSuggestion.SuggestQuantity(ValueType, rhc) : null;
var steps = ValueType == AspectValueType.Points
? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList()
: ParticipationRatingScale.Steps(ValueType).ToList();
@@ -864,10 +909,13 @@ public partial class SeatAssessmentAspectRow : ObservableObject
{
Value = value;
OnPropertyChanged(nameof(DisplayValue));
+ OnPropertyChanged(nameof(HasSuggestion));
foreach (var choice in Choices) choice.IsSelected = choice.Value == value;
_apply(Key, value);
}
+ [RelayCommand] private void ApplySuggestion() => ApplyValue(SuggestedValue);
+
public void Adjust(int delta)
{
if (ValueType == AspectValueType.Points)
diff --git a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
index 04ed60e..1f735ef 100644
--- a/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
+++ b/LehrerApp.Desktop/Views/Dashboard/DashboardView.axaml
@@ -420,6 +420,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml.cs
index ba2a8c5..cfea0a5 100644
--- a/LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/ParticipationQuickInputDialog.axaml.cs
@@ -58,6 +58,7 @@ public partial class ParticipationQuickInputDialog : Window
case Key.OemPlus or Key.Add: vm.IncrementRating(); e.Handled = true; break;
case Key.OemMinus or Key.Subtract: vm.DecrementRating(); e.Handled = true; break;
+ case Key.Delete: vm.ClearRating(); e.Handled = true; break;
case Key.Escape: Close(); e.Handled = true; break;
diff --git a/LehrerApp.Desktop/Views/Groups/SeatAssessmentDialog.axaml b/LehrerApp.Desktop/Views/Groups/SeatAssessmentDialog.axaml
index d2202e8..2349690 100644
--- a/LehrerApp.Desktop/Views/Groups/SeatAssessmentDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/SeatAssessmentDialog.axaml
@@ -58,6 +58,10 @@
+
diff --git a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
index f6476d9..af3db2f 100644
--- a/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/SeatingPlanTabView.axaml
@@ -155,6 +155,21 @@
ToolTip.Tip="Hausaufgabe"/>
+
+
+
+
+
diff --git a/LehrerApp.Tests/ExamWeekLoadServiceTests.cs b/LehrerApp.Tests/ExamWeekLoadServiceTests.cs
new file mode 100644
index 0000000..5d596bd
--- /dev/null
+++ b/LehrerApp.Tests/ExamWeekLoadServiceTests.cs
@@ -0,0 +1,86 @@
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using Xunit;
+
+namespace LehrerApp.Tests;
+
+public class ExamWeekLoadServiceTests
+{
+ private static Exam MakeExam(DateOnly date, ExamStatus status = ExamStatus.Planned) => new()
+ {
+ GroupId = Guid.NewGuid(), Title = "Klausur", Status = status, Date = date,
+ };
+
+ [Fact]
+ public void CountInSameWeek_ZaehltNurKlausurenInDerselbenIsoWoche()
+ {
+ // KW 12/2026: Montag 16.03. bis Sonntag 22.03.
+ var monday = new DateOnly(2026, 3, 16);
+ var exams = new List
+ {
+ MakeExam(monday),
+ MakeExam(monday.AddDays(4)), // Freitag derselben Woche
+ MakeExam(monday.AddDays(-1)), // Sonntag der Vorwoche
+ MakeExam(monday.AddDays(7)), // Montag der Folgewoche
+ };
+
+ var count = ExamWeekLoadService.CountInSameWeek(exams, monday.AddDays(2));
+
+ Assert.Equal(2, count);
+ }
+
+ [Fact]
+ public void CountInSameWeek_SchliesstAngegebeneKlausurAus()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var editing = MakeExam(monday);
+ var exams = new List { editing, MakeExam(monday.AddDays(1)) };
+
+ var count = ExamWeekLoadService.CountInSameWeek(exams, monday, editing.Id);
+
+ Assert.Equal(1, count);
+ }
+
+ [Fact]
+ public void FindOverloadedWeeks_MeldetNurWochenAbDemSchwellenwert()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var exams = new List
+ {
+ MakeExam(monday), MakeExam(monday.AddDays(1)), // KW 12: 2 -> unauffällig
+ MakeExam(monday.AddDays(7)), MakeExam(monday.AddDays(8)), MakeExam(monday.AddDays(9)), // KW 13: 3 -> Meldung
+ };
+
+ var overloaded = ExamWeekLoadService.FindOverloadedWeeks(exams);
+
+ var week = Assert.Single(overloaded);
+ Assert.Equal(3, week.ExamCount);
+ Assert.Equal(monday.AddDays(7), week.WeekStart);
+ }
+
+ [Fact]
+ public void FindOverloadedWeeks_OhneHaeufung_GibtLeereListeZurueck()
+ {
+ var monday = new DateOnly(2026, 3, 16);
+ var exams = new List { MakeExam(monday), MakeExam(monday.AddDays(7)) };
+
+ Assert.Empty(ExamWeekLoadService.FindOverloadedWeeks(exams));
+ }
+
+ [Fact]
+ public void FindOverloadedWeeks_SortiertChronologisch()
+ {
+ var laterWeek = new DateOnly(2026, 4, 20);
+ var earlierWeek = new DateOnly(2026, 3, 16);
+ var exams = new List
+ {
+ MakeExam(laterWeek), MakeExam(laterWeek.AddDays(1)), MakeExam(laterWeek.AddDays(2)),
+ MakeExam(earlierWeek), MakeExam(earlierWeek.AddDays(1)), MakeExam(earlierWeek.AddDays(2)),
+ };
+
+ var overloaded = ExamWeekLoadService.FindOverloadedWeeks(exams);
+
+ Assert.Equal(2, overloaded.Count);
+ Assert.True(overloaded[0].WeekStart < overloaded[1].WeekStart);
+ }
+}
diff --git a/LehrerApp.Tests/ParticipationCountSuggestionTests.cs b/LehrerApp.Tests/ParticipationCountSuggestionTests.cs
new file mode 100644
index 0000000..ca06c7b
--- /dev/null
+++ b/LehrerApp.Tests/ParticipationCountSuggestionTests.cs
@@ -0,0 +1,32 @@
+using LehrerApp.Core.Models;
+using Xunit;
+
+namespace LehrerApp.Tests;
+
+/// Tests für den Quantitäts-Vorschlag aus der Sitzplan-Strichliste (Nutzer-Feedback):
+/// wie oft sich jemand meldet, schlägt direkt eine Quantitätsbewertung vor.
+public sealed class ParticipationCountSuggestionTests
+{
+ [Theory]
+ [InlineData(0, -2)]
+ [InlineData(1, -1)]
+ [InlineData(2, 0)]
+ [InlineData(3, 0)]
+ [InlineData(4, 1)]
+ [InlineData(5, 1)]
+ [InlineData(6, 2)]
+ [InlineData(20, 2)]
+ public void SuggestQuantity_Scale5_BildetMeldungszahlAufStufeAb(int raisedHandCount, int expected)
+ {
+ Assert.Equal(expected, ParticipationCountSuggestion.SuggestQuantity(AspectValueType.Scale5, raisedHandCount));
+ }
+
+ [Theory]
+ [InlineData(AspectValueType.Scale3)]
+ [InlineData(AspectValueType.Binary)]
+ [InlineData(AspectValueType.Points)]
+ public void SuggestQuantity_AndereTypen_GibtKeinenVorschlag(AspectValueType type)
+ {
+ Assert.Null(ParticipationCountSuggestion.SuggestQuantity(type, 5));
+ }
+}
diff --git a/TODO.md b/TODO.md
index 5d77213..dc38c96 100644
--- a/TODO.md
+++ b/TODO.md
@@ -144,6 +144,22 @@ unterrichten oft mehrere Kurse gleichzeitig und brauchen einen gruppenübergreif
`GradingService`/`GradeBarItem`) — kein Kompetenz-Breakdown in dieser Iteration. Button
"Zum Kurs" springt in den Klausuren-Tab der jeweiligen Gruppe (dort wie gehabt Zugriff auf
Kompetenzen-Tab, 8.2/8.3).
+- [x] **1.6.7** Persönliche Klausurlast pro Woche (Nutzer-Feedback). Bewusst **keine**
+ klassenbezogene Kollisionsprüfung (mehrere Klausuren einer einzelnen Klasse in einer
+ Woche) — das übernimmt bereits der schulische Klausurplaner (externes Klassenbuch) mit
+ eigenem Alarm. Was dort fehlt: die eigene Belastung über alle Kurse hinweg, unabhängig von
+ der Klasse — mehr Klausuren in derselben Woche bedeuten unabhängig davon mehr
+ Erstellungsaufwand vorher und mehr Korrekturaufwand danach. Neuer
+ `ExamWeekLoadService` (`LehrerApp.Core/Services/ExamWeekLoadService.cs`) zählt eigene
+ Klausuren je ISO-Kalenderwoche; da eine LiteDB-Datei genau einem Nutzer gehört, sind "alle
+ Klausuren in der Datenbank" bereits gleichbedeutend mit "meine Klausuren". Ab drei
+ Klausuren in derselben Woche gilt sie als spürbar belastet (zwei aus zwei unterrichteten
+ Fächern sind normal, eine dritte die eigentliche Häufung). Zwei Einstiege: (a) im
+ `ExamDialog` direkt beim Ändern des Datums ein Hinweis, wenn die Klausur die Woche über
+ den Schwellenwert heben würde — greift am Entscheidungspunkt, bevor überhaupt gespeichert
+ wird; (b) neue Dashboard-Karte "Klausurwochen" (9), die bevorstehende Wochen (60 Tage
+ Vorausschau) mit auffälliger Häufung unter den noch **geplanten** Klausuren auflistet —
+ bereits durchgeführte/korrigierte Klausuren deckt schon die Karte "Offene Korrekturen" ab.
---
@@ -281,6 +297,16 @@ Siehe [ParticipationViewModels.cs](LehrerApp.Desktop/ViewModels/Groups/Participa
Anwesenheitsstatus ("⚠ Abwesend — Krank, unentschuldigt" o.ä.) in Orange. Bewusst kein
Blockieren der Eingabe (manche Bewertungssysteme erwarten trotzdem einen expliziten
Eintrag) — nur ein visueller Hinweis.
+- [x] **3.1.6** Bewertung wieder auf "nicht bewertet" zurücksetzen können (Nutzer-Feedback: eine
+ aus Versehen gesetzte Bewertung — z.B. Qualität, obwohl sich der Schüler nie gemeldet hat —
+ muss sich zurücknehmen lassen, ohne einen inhaltlich falschen Wert stehen lassen zu müssen).
+ Im Sitzplatz-Dialog (`SeatAssessmentDialog`) gab es das bereits über "Löschen"/Backspace je
+ Aspekt; in der session-weiten Schnelleingabe (`ParticipationQuickInputDialog`, Q/W/E/R/T +
+ Zifferntasten) fehlte ein entsprechender Weg — Backspace ist dort bereits für "vorheriger
+ Schüler" belegt. Neue Taste **Entf** ruft `QuickInputViewModel.ClearRating()` auf
+ (`ApplyRating` nimmt jetzt `int?` statt `int` an); bewusst eine eigene Aktion statt über 0
+ zu laufen, da 0 ("--") bei Quantität ein echter, beobachteter Wert ist, kein Platzhalter für
+ "nicht ermittelt".
### 3.2 Aggregation zur Mitarbeitsnote
- [x] **3.2.1** Gewichtung je Aspekt konfigurierbar (z.B. Qualität 50 %, Quantität 30 %, Experiment 20 %).
@@ -330,6 +356,20 @@ Mitarbeit-Note für denselben Zeitraum statt sie zu duplizieren (erkannt über d
und die Kalender-Aggregation (`DayAgg`/`CalendarDayCell`/`CalendarEventItem`) bereits, sodass
sich der dritte Termintyp am Exam-Vorbild (In-Memory-Filterung nach `GetByGroup`, keine neue
Repository-Methode) ergänzen ließ.
+- [x] **3.3.5** Strichliste "gemeldet"/"gemeldet und drangekommen" direkt im Sitzplan
+ (Nutzer-Feedback, als Ergänzung zur "ohne Mitarbeitsbewertung"-Zählung aus dem
+ Aufrufgerechtigkeits-Gedanken): zwei neue Felder `ParticipationEntry.RaisedHandCount`/
+ `.CalledOnCount` — "drangekommen" ist immer auch eine Meldung und erhöht daher immer
+ beide Zähler zugleich, nie `CalledOnCount` allein. Zwei kompakte Zähl-Buttons auf jeder
+ belegten Sitzplatz-Kachel im Anzeigemodus (`ShowLessonOverview`, gleiche Sichtbarkeit wie
+ die Anwesenheits-/Hausaufgaben-Badges) erlauben das Mitzählen während der Stunde, ohne den
+ vollen Sitzplatz-Dialog öffnen zu müssen — bewusst nur Hochzählen, keine Korrektur direkt
+ auf der Kachel (die Rohwerte bleiben über den Sitzplatz-Dialog einsehbar). Tüpfelchen auf
+ dem i: die Strichliste schlägt im Sitzplatz-Dialog direkt eine Quantitätsbewertung vor
+ (neuer `ParticipationCountSuggestion.SuggestQuantity`, nur für den eingebauten
+ "quantity"-Aspekt mit Skalentyp `Scale5`) — als sichtbarer Vorschlag mit einem Klick zum
+ Übernehmen, nie automatisch gesetzt, damit eine bereits vorhandene bewusste Bewertung nie
+ stillschweigend überschrieben wird.
### Abschnittsnoten & Mitarbeits-Assistent (nicht aus dieser Liste, eigener Workflow-Bedarf)