Persönliche Klausurlast pro Woche, Mitarbeit-Reset und Sitzplan-Strichliste
CI / build-and-test (push) Canceled after 0s

- Klausuren: neuer ExamWeekLoadService zählt eigene Klausuren je ISO-Woche
  über alle Kurse hinweg (Klassen-Kollisionen deckt bereits der externe
  Klausurplaner ab). Hinweis direkt im ExamDialog beim Datum, zusätzlich
  neue Dashboard-Karte "Klausurwochen" für die 60-Tage-Vorausschau.
- Mitarbeit: Schnelleingabe-Dialog kann eine Bewertung jetzt per Entf auf
  "nicht bewertet" zurücksetzen (bisher nur im Sitzplatz-Dialog möglich).
- Sitzplan: Strichliste "gemeldet"/"gemeldet und drangekommen" direkt auf
  der Sitzplatz-Kachel (ParticipationEntry.RaisedHandCount/CalledOnCount);
  schlägt im Sitzplatz-Dialog eine Quantitätsbewertung vor (ein Klick zum
  Übernehmen, nie automatisch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-30 02:11:20 +02:00
co-authored by Claude Sonnet 5
parent 538ad65a3a
commit da4b88c93d
19 changed files with 640 additions and 8 deletions
+32
View File
@@ -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,
};
}
/// <summary>
/// Nutzer-Feedback: die Strichliste im Sitzplan (<see cref="ParticipationEntry.RaisedHandCount"/>)
/// 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 <see cref="AspectValueType.Scale5"/> (der
/// Standardtyp des "quantity"-Aspekts) — für individuell umkonfigurierte Aspekttypen gäbe es keine
/// sinnvolle, unmissverständliche Abbildung.
/// </summary>
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,
};
}
}
@@ -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;
@@ -0,0 +1,54 @@
using System.Globalization;
using LehrerApp.Core.Models;
namespace LehrerApp.Core.Services;
/// <summary>
/// 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".
/// </summary>
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 <paramref name="excludeExamId"/>, z.B. die gerade im Dialog
/// bearbeitete), die in dieselbe ISO-Kalenderwoche wie <paramref name="date"/> fallen.
public static int CountInSameWeek(IEnumerable<Exam> 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 <see cref="WarningThreshold"/> Klausuren, chronologisch sortiert — für einen
/// vorausschauenden Überblick (z.B. Dashboard), nicht für die Einzelprüfung beim Anlegen.
public static List<ExamWeekLoad> FindOverloadedWeeks(IEnumerable<Exam> 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);
}
@@ -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()
{
@@ -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<Exam> 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<Exam> { 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<Exam> { 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<Exam> { 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);
}
}
@@ -83,6 +83,26 @@ public sealed class QuickInputViewModelTests
Assert.Equal(0.4, vm.CurrentStudentContentOpacity);
}
[Fact]
public void ClearRating_SetztVersehentlichGesetzteBewertungAufNichtBewertetZurueck()
{
var aspects = new List<AspectColumnDef>
{
new(new ParticipationAspect { Key = "quality", Label = "Qualität" }),
};
var rows = new List<ParticipationStudentRow>
{
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()
{
@@ -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()
{
@@ -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<CalendarDayCell> CalendarDays { get; } = [];
public ObservableCollection<OpenExcuseItem> OpenExcuses { get; } = [];
public ObservableCollection<AttendanceWarningItem> AttendanceWarnings { get; } = [];
public ObservableCollection<ExamWeekLoadItem> ExamWeekLoads { get; } = [];
public ObservableCollection<SupportPlanDueItem> SupportPlanReviews { get; } = [];
public ObservableCollection<UpcomingDateItem> UpcomingDates { get; } = [];
public ObservableCollection<CorrectionProgressItem> 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<Guid, LearningGroup> 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
@@ -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()
{
@@ -824,7 +824,7 @@ public partial class QuickInputViewModel : ObservableObject
_ => "15 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;
@@ -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<SituationTagChoice> 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.</summary>
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
private readonly Action<SeatCellViewModel, bool> _tally;
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false,
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null)
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null,
Action<SeatCellViewModel, bool>? 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<SeatRatingChoice> 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<string, int?> apply)
Action<string, int?> 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)
@@ -420,6 +420,30 @@
</StackPanel>
</Border>
<!-- Klausurwochen (Nutzer-Feedback): eigene Klausurlast über alle Kurse hinweg -->
<Border Grid.Column="{Binding ExamLoadCard.Column}" Grid.Row="{Binding ExamLoadCard.Row}"
IsVisible="{Binding ExamLoadCard.EffectiveIsVisible}" Margin="0,0,8,8" VerticalAlignment="Top"
Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
CornerRadius="8" Padding="16">
<StackPanel>
<TextBlock Text="KLAUSURWOCHEN" FontSize="11" FontWeight="Bold"
Opacity="0.5" Margin="0,0,0,10"/>
<ItemsControl ItemsSource="{Binding ExamWeekLoads}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ExamWeekLoadItem">
<Grid ColumnDefinitions="*,Auto" Margin="0,4">
<TextBlock Grid.Column="0" Text="{Binding Label}" FontSize="13"/>
<TextBlock Grid.Column="1" Text="{Binding CountDisplay}" Foreground="#F59E0B"
FontSize="12" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Keine Woche mit auffälliger Klausurhäufung." Classes="emptyhint"
IsVisible="{Binding !ExamWeekLoads.Count}"/>
</StackPanel>
</Border>
<!-- Förderplan-Wiedervorlage (5.3.2) -->
<Border Grid.Column="{Binding SupportCard.Column}" Grid.Row="{Binding SupportCard.Row}"
IsVisible="{Binding SupportCard.EffectiveIsVisible}" Margin="8,0,0,8" VerticalAlignment="Top"
@@ -36,6 +36,8 @@
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
<TextBlock Text="{Binding DateTextError}" Foreground="Red" FontSize="11"
IsVisible="{Binding DateTextError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="{Binding WeekLoadHint}" Foreground="#F59E0B" FontSize="11"
IsVisible="{Binding WeekLoadHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
@@ -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;
@@ -58,6 +58,10 @@
<Run Text="["/><Run Text="{Binding Shortcut}"/><Run Text="] "/><Run Text="{Binding Label}"/>
</TextBlock>
<TextBlock Text="{Binding DisplayValue}" FontSize="11" Opacity="0.6"/>
<Button Content="{Binding SuggestionLabel}" Command="{Binding ApplySuggestionCommand}"
IsVisible="{Binding HasSuggestion}" FontSize="10" Padding="6,2" Margin="0,3,0,0"
HorizontalAlignment="Left"
ToolTip.Tip="Aus der Strichliste im Sitzplan übernehmen"/>
</StackPanel>
<WrapPanel Grid.Column="1" HorizontalAlignment="Right">
<ItemsControl ItemsSource="{Binding Choices}">
@@ -155,6 +155,21 @@
ToolTip.Tip="Hausaufgabe"/>
</Border>
</StackPanel>
<!-- Strichliste Meldungen (Nutzer-Feedback): schnelles Mitzählen ohne den
vollen Bewertungsdialog zu öffnen -->
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5"
IsVisible="{Binding ShowLessonOverview}">
<Button Padding="6,2" FontSize="10"
Command="{Binding TallyRaisedHandCommand}"
ToolTip.Tip="Meldung zählen">
<TextBlock Text="{Binding RaisedHandCount, StringFormat='✋ {0}'}"/>
</Button>
<Button Padding="6,2" FontSize="10"
Command="{Binding TallyCalledOnCommand}"
ToolTip.Tip="Meldung + drangekommen zählen">
<TextBlock Text="{Binding CalledOnCount, StringFormat='✋✓ {0}'}"/>
</Button>
</StackPanel>
<Expander Header=" Situation" FontSize="10"
IsVisible="{Binding CanRecordLesson}">
<ItemsControl ItemsSource="{Binding SituationTags}" Margin="0,4,0,0">
@@ -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<Exam>
{
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<Exam> { 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<Exam>
{
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<Exam> { 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<Exam>
{
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);
}
}
@@ -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));
}
}
+40
View File
@@ -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)