From c0f8b98202e522b5efb58daf005ae86e802b9000 Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Mon, 5 Oct 2026 22:54:31 +0200 Subject: [PATCH] Nachschreibarbeiten und Arbeitsarten (Test, kontrollierte Hausaufgabe) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nachschreibarbeiten nutzen die Wiederholungs-Verknüpfung, ersetzen aber nur fehlende Noten; mehrere Folgearbeiten je erster Arbeit sind möglich. Exam.Kind trennt Klausuren (schriftlich) von Tests u. Ä. (Sonstige). Co-Authored-By: Claude Sonnet 5.5 --- LehrerApp.Core/Models/Exam.cs | 22 +++++++ LehrerApp.Core/Services/ExamScoringService.cs | 59 +++++++++++------ .../Services/ExamWeekLoadService.cs | 7 ++- .../Groups/ExamEvaluationViewModels.cs | 14 +++-- .../Groups/ExamGradingViewModels.cs | 9 +++ .../ViewModels/Groups/ExamViewModels.cs | 44 +++++++++++-- .../Groups/GradeOverviewViewModels.cs | 4 +- .../Groups/GroupOverviewViewModel.cs | 2 +- .../ViewModels/Groups/GroupViewModels.cs | 11 +++- .../Groups/ReportGradeViewModels.cs | 6 +- .../StudentPerformanceOverviewViewModel.cs | 7 ++- .../Views/Groups/ExamDialog.axaml | 16 ++++- .../Views/Groups/ExamEvaluationDialog.axaml | 4 +- LehrerApp.Tests/ExamScoringServiceTests.cs | 63 +++++++++++++++++++ TODO.md | 14 +++++ 15 files changed, 238 insertions(+), 44 deletions(-) diff --git a/LehrerApp.Core/Models/Exam.cs b/LehrerApp.Core/Models/Exam.cs index b9abe16..69e436b 100644 --- a/LehrerApp.Core/Models/Exam.cs +++ b/LehrerApp.Core/Models/Exam.cs @@ -25,6 +25,13 @@ public class Exam /// Optional verknüpfte ursprüngliche Arbeit derselben Lerngruppe. public Guid? RepeatOfExamId { get; set; } public RepeatGradeRule RepeatGradeRule { get; set; } = RepeatGradeRule.Better; + /// Art der Leistungsüberprüfung. Nur zählt in die + /// schriftliche Note; alle anderen Arten fließen unter "Sonstige" ein. + public ExamKind Kind { get; set; } = ExamKind.Klausur; + /// Nachschreibarbeit: inhaltlich eigene Arbeit für Schüler, die die verknüpfte + /// erste Arbeit () versäumt haben. Anders als eine Wiederholung + /// wird sie nur von diesen Schülern geschrieben und ersetzt deren fehlende Note. + public bool IsMakeUp { get; set; } public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; } @@ -68,3 +75,18 @@ public class ExamResult public enum ExamStatus { Planned, Conducted, Graded, Returned } public enum ExamVariant { A, B } public enum RepeatGradeRule { Better, Repeat, Original } +/// Reihenfolge ist persistiert (LiteDB speichert den Enum-Wert) — nur hinten anhängen. +public enum ExamKind { Klausur, Test, KontrollierteHausaufgabe, Sonstiges } + +public static class ExamKindExtensions +{ + public static bool CountsAsWritten(this ExamKind kind) => kind == ExamKind.Klausur; + + public static string Label(this ExamKind kind) => kind switch + { + ExamKind.Test => "Test", + ExamKind.KontrollierteHausaufgabe => "Kontrollierte Hausaufgabe", + ExamKind.Sonstiges => "Sonstige Arbeit", + _ => "Klausur", + }; +} diff --git a/LehrerApp.Core/Services/ExamScoringService.cs b/LehrerApp.Core/Services/ExamScoringService.cs index 725a237..9b673d1 100644 --- a/LehrerApp.Core/Services/ExamScoringService.cs +++ b/LehrerApp.Core/Services/ExamScoringService.cs @@ -44,33 +44,54 @@ public static class ExamScoringService ExamVariant variant) => tasks.Select((task, index) => AppliesTo(task, variant) && index < points.Count ? points[index] : 0).Sum(); - /// Wählt pro verknüpfter Wiederholung genau eine Note. Andere Arbeiten bleiben erhalten. + /// + /// Wählt pro Arbeit samt allen verknüpften Folgearbeiten (Wiederholung, Nachschreibarbeit) + /// genau eine Note. Andere Arbeiten bleiben erhalten. Fehlt die erste Arbeit (Schüler war + /// abwesend), zählt die Nachschreibarbeit allein; hat der Schüler beide geschrieben, zählt bei + /// einer Nachschreibarbeit immer die erste — sie ersetzt nur eine fehlende Note. + /// public static List EffectiveGrades(IEnumerable<(Exam Exam, string Grade)> entries, GradingSystem system) { - var available = entries.ToList(); - var byId = available.ToDictionary(x => x.Exam.Id); - var consumed = new HashSet(); - var result = new List(); - foreach (var entry in available) + var chains = new List>(); + var chainByRoot = new Dictionary>(); + foreach (var entry in entries) { - if (!consumed.Add(entry.Exam.Id)) continue; - if (entry.Exam.RepeatOfExamId is { } originalId && byId.TryGetValue(originalId, out var original)) - { - consumed.Add(originalId); - result.Add(Choose(original.Grade, entry.Grade, entry.Exam.RepeatGradeRule, system)); - } - else if (available.FirstOrDefault(x => x.Exam.RepeatOfExamId == entry.Exam.Id) is var repeat - && repeat.Exam is not null) - { - consumed.Add(repeat.Exam.Id); - result.Add(Choose(entry.Grade, repeat.Grade, repeat.Exam.RepeatGradeRule, system)); - } - else result.Add(entry.Grade); + var root = entry.Exam.RepeatOfExamId ?? entry.Exam.Id; + if (!chainByRoot.TryGetValue(root, out var chain)) + chains.Add(chainByRoot[root] = chain = []); + chain.Add(entry); + } + + var result = new List(); + foreach (var chain in chains) + { + // Die erste Arbeit steht vorn, Folgearbeiten chronologisch dahinter. + var ordered = chain.OrderBy(x => x.Exam.RepeatOfExamId is null ? 0 : 1) + .ThenBy(x => x.Exam.Date).ToList(); + var current = ordered[0].Grade; + foreach (var next in ordered.Skip(1)) + current = Choose(current, next.Grade, + next.Exam.IsMakeUp ? RepeatGradeRule.Original : next.Exam.RepeatGradeRule, system); + result.Add(current); } return result; } + /// + /// Wie , trennt aber nach Art: Klausuren zählen als schriftliche + /// Note, Tests u. Ä. fließen unter "Sonstige" ein. Verknüpfte Arbeiten erben die Art der + /// ersten Arbeit (der Dialog erzwingt das), landen also immer im selben Topf. + /// + public static (List Written, List Other) EffectiveGradesByKind( + IEnumerable<(Exam Exam, string Grade)> entries, GradingSystem system) + { + var list = entries.ToList(); + return ( + EffectiveGrades(list.Where(x => x.Exam.Kind.CountsAsWritten()), system), + EffectiveGrades(list.Where(x => !x.Exam.Kind.CountsAsWritten()), system)); + } + private static string Choose(string original, string repeat, RepeatGradeRule rule, GradingSystem system) { if (rule == RepeatGradeRule.Original) return original; diff --git a/LehrerApp.Core/Services/ExamWeekLoadService.cs b/LehrerApp.Core/Services/ExamWeekLoadService.cs index 77e0abe..02c2f73 100644 --- a/LehrerApp.Core/Services/ExamWeekLoadService.cs +++ b/LehrerApp.Core/Services/ExamWeekLoadService.cs @@ -10,7 +10,8 @@ namespace LehrerApp.Core.Services; /// 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". +/// Klausuren in der Datenbank" bereits gleichbedeutend mit "meine Klausuren". Tests und andere +/// kurze Arbeiten () zählen bewusst nicht mit — sie sind kein Klausurblock. /// public static class ExamWeekLoadService { @@ -24,7 +25,8 @@ public static class ExamWeekLoadService 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)); + return allExams.Count(e => e.Id != excludeExamId && e.Kind.CountsAsWritten() + && IsoWeek(e.Date) == (year, week)); } /// Gruppiert die übergebenen Klausuren nach ISO-Kalenderwoche und liefert nur die Wochen mit @@ -32,6 +34,7 @@ public static class ExamWeekLoadService /// vorausschauenden Überblick (z.B. Dashboard), nicht für die Einzelprüfung beim Anlegen. public static List FindOverloadedWeeks(IEnumerable exams) => exams + .Where(e => e.Kind.CountsAsWritten()) .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())) diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs index 50d5181..a850c0e 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs @@ -42,6 +42,9 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject public ObservableCollection RepeatComparison { get; } = []; public bool HasRepeatComparison => RepeatComparison.Count > 0; public string RepeatComparisonTitle { get; private set; } = ""; + /// "Wiederholung" bzw. "Nachschreibarbeit" — Beschriftung der Folgearbeit im Vergleich. + public string FollowUpLabel { get; private set; } = "Wiederholung"; + public string FollowUpComparisonHeading => $"Vergleich mit der {FollowUpLabel}"; public ObservableCollection GradingKeyEntries { get; } = []; public ExamEvaluationDialogViewModel(IExamRepository exams, IExamResultRepository results, @@ -66,15 +69,17 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject var original = _exam.RepeatOfExamId is { } originalId ? _exams.GetById(originalId) : _exam; if (original is null) return; var repeat = _exam.RepeatOfExamId is not null ? _exam - : _exams.GetByGroup(_exam.GroupId).FirstOrDefault(e => e.RepeatOfExamId == _exam.Id); + : _exams.GetByGroup(_exam.GroupId).Where(e => e.RepeatOfExamId == _exam.Id) + .OrderBy(e => e.Date).FirstOrDefault(); if (repeat is null) return; - var rule = repeat.RepeatGradeRule switch + FollowUpLabel = repeat.IsMakeUp ? "Nachschreibarbeit" : "Wiederholung"; + var rule = repeat.IsMakeUp ? "ersetzt fehlende Note" : repeat.RepeatGradeRule switch { RepeatGradeRule.Repeat => "Wiederholung zählt", RepeatGradeRule.Original => "Erste Arbeit zählt", _ => "Bessere zählt", }; - RepeatComparisonTitle = $"Erste Arbeit: {original.Title} · Wiederholung: {repeat.Title} · {rule}"; + RepeatComparisonTitle = $"Erste Arbeit: {original.Title} · {FollowUpLabel}: {repeat.Title} · {rule}"; var first = _results.GetByExam(original.Id).Where(r => !r.Absent && r.Grade is not null) .ToDictionary(r => r.StudentId); var second = _results.GetByExam(repeat.Id).Where(r => !r.Absent && r.Grade is not null) @@ -96,6 +101,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject } OnPropertyChanged(nameof(HasRepeatComparison)); OnPropertyChanged(nameof(RepeatComparisonTitle)); + OnPropertyChanged(nameof(FollowUpLabel)); } private void LoadResults() @@ -271,7 +277,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject if (HasRepeatComparison) { csv.AddBlankRow().AddRow("Vergleich", RepeatComparisonTitle) - .AddRow("Schüler", "Erste Arbeit", "Wiederholung", "Zählt"); + .AddRow("Schüler", "Erste Arbeit", FollowUpLabel, "Zählt"); foreach (var item in RepeatComparison) csv.AddRow(item.StudentName, item.OriginalGrade, item.RepeatGrade, item.EffectiveGrade); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs index ecc34e8..474564a 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs @@ -54,6 +54,14 @@ public partial class ExamGradingDialogViewModel : ObservableObject var membershipList = memberships.GetByGroup(groupId); var existing = results.GetByExam(exam.Id).ToDictionary(r => r.StudentId); + // Nachschreibarbeit: nur Schüler, die die erste Arbeit nicht mitgeschrieben haben. Wer dort + // schon eine Note hat, taucht nicht auf — außer es gibt hier bereits ein Ergebnis (nichts + // verstecken, was schon eingetragen wurde). + var wroteOriginal = new HashSet(); + if (exam.IsMakeUp && exam.RepeatOfExamId is { } originalId) + foreach (var r in results.GetByExam(originalId).Where(r => !r.Absent && r.Grade is not null)) + wroteOriginal.Add(r.StudentId); + foreach (var s in enrolled.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)) { var membership = membershipList.FirstOrDefault(e => e.StudentId == s.Id); @@ -64,6 +72,7 @@ public partial class ExamGradingDialogViewModel : ObservableObject if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue; existing.TryGetValue(s.Id, out var result); + if (result is null && wroteOriginal.Contains(s.Id)) continue; var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, ExamScoringService.Maximum(Tasks, ExamVariant.A), _grading); row.OnChanged = SaveRow; diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs index e85e7a2..e18694b 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs @@ -46,11 +46,21 @@ public partial class ExamDialogViewModel : ObservableObject [ObservableProperty] private string _repeatRuleName = "Bessere zählt"; [ObservableProperty] private string _repeatError = ""; [ObservableProperty] private string _taskPointsError = ""; + [ObservableProperty] private string _kindName = ExamKind.Klausur.Label(); + [ObservableProperty] private bool _isMakeUp; public bool IsDifferentiated { get; } public string[] NiveauOptions => NiveauDisplay.Options; + public string[] KindOptions { get; } = Enum.GetValues().Select(k => k.Label()).ToArray(); public string[] RepeatRuleOptions => ["Bessere zählt", "Wiederholung zählt", "Erste Arbeit zählt"]; public bool HasSelectedOriginalExam => SelectedOriginalExam is not null; + /// Art und Zählweise sind bei einer verknüpften Folgearbeit festgelegt: sie erbt die Art der + /// ersten Arbeit, eine Nachschreibarbeit ersetzt nur fehlende Noten (keine Regelwahl nötig). + public bool CanChooseKind => !HasSelectedOriginalExam; + public bool ShowRepeatRule => HasSelectedOriginalExam && !IsMakeUp; + public string LinkedExamLabel => IsMakeUp + ? "Nachschreibarbeit zu (optional)" : "Wiederholung oder Nachschreibarbeit zu (optional)"; + private ExamKind Kind => Enum.GetValues().FirstOrDefault(k => k.Label() == KindName); public ObservableCollection AvailableOriginalExams { get; } = []; public bool TotalPointsWarning => TotalPoints <= 0; @@ -63,7 +73,7 @@ public partial class ExamDialogViewModel : ObservableObject public ObservableCollection AvailableTemplates { get; } = []; public Exam? Result { get; private set; } - public string DialogTitle => _editingExam is null ? "Neue Klausur anlegen" : "Klausur bearbeiten"; + public string DialogTitle => _editingExam is null ? "Neue Arbeit anlegen" : "Arbeit bearbeiten"; public string SaveButtonText => _editingExam is null ? "Anlegen" : "Speichern"; /// Fach kommt von der Lerngruppe, nicht editierbar (jede Gruppe unterrichtet ein Fach). @@ -87,9 +97,10 @@ public partial class ExamDialogViewModel : ObservableObject _subjectName = defaultSubjectName; IsDifferentiated = isDifferentiated; var groupExams = _exams.GetByGroup(groupId); + // Mehrere Folgearbeiten je erster Arbeit sind erlaubt (z. B. Nachschreibtermin und + // später eine Wiederholung); Folgearbeiten selbst sind keine Bezugsarbeit. foreach (var exam in groupExams .Where(e => e.Id != editingExam?.Id && e.RepeatOfExamId is null) - .Where(e => !groupExams.Any(r => r.RepeatOfExamId == e.Id && r.Id != editingExam?.Id)) .OrderByDescending(e => e.Date)) AvailableOriginalExams.Add(exam); @@ -113,9 +124,11 @@ public partial class ExamDialogViewModel : ObservableObject // Beim Duplizieren bewusst kein Niveau übernehmen — meist wird dupliziert, um // gerade eine andere Niveau-Variante (E/G/Förder) derselben Klausur anzulegen. SelectedNiveauName = isDuplicate ? "–" : NiveauDisplay.ToName(source.Niveau); + KindName = source.Kind.Label(); if (!isDuplicate) { SelectedOriginalExam = AvailableOriginalExams.FirstOrDefault(e => e.Id == source.RepeatOfExamId); + IsMakeUp = source.IsMakeUp && SelectedOriginalExam is not null; RepeatRuleName = source.RepeatGradeRule switch { RepeatGradeRule.Repeat => "Wiederholung zählt", @@ -145,7 +158,23 @@ public partial class ExamDialogViewModel : ObservableObject { foreach (var t in Tasks) t.ShowWeight = value; } - partial void OnSelectedOriginalExamChanged(Exam? value) => OnPropertyChanged(nameof(HasSelectedOriginalExam)); + partial void OnSelectedOriginalExamChanged(Exam? value) + { + if (value is not null) KindName = value.Kind.Label(); + else IsMakeUp = false; + OnPropertyChanged(nameof(HasSelectedOriginalExam)); + OnPropertyChanged(nameof(CanChooseKind)); + OnPropertyChanged(nameof(ShowRepeatRule)); + UpdateWeekLoadHint(); + } + + partial void OnIsMakeUpChanged(bool value) + { + OnPropertyChanged(nameof(ShowRepeatRule)); + OnPropertyChanged(nameof(LinkedExamLabel)); + } + + partial void OnKindNameChanged(string value) => UpdateWeekLoadHint(); [RelayCommand] private void ClearOriginalExam() => SelectedOriginalExam = null; @@ -156,7 +185,8 @@ public partial class ExamDialogViewModel : ObservableObject /// 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)) + if (!Kind.CountsAsWritten() + || !DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date)) { WeekLoadHint = ""; return; @@ -354,7 +384,9 @@ public partial class ExamDialogViewModel : ObservableObject if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; valid = false; } if (SelectedOriginalExam is not null && SelectedOriginalExam.Date >= date) { - RepeatError = "Die erste Arbeit muss vor der Wiederholung liegen."; + RepeatError = IsMakeUp + ? "Die erste Arbeit muss vor der Nachschreibarbeit liegen." + : "Die erste Arbeit muss vor der Wiederholung liegen."; valid = false; } if (Tasks.Any(t => t.VariantName != "Beide") && @@ -376,6 +408,8 @@ public partial class ExamDialogViewModel : ObservableObject Result.Tasks = Tasks.Select(t => t.ToModel()).ToList(); Result.GradingKey = gradingKey; Result.RepeatOfExamId = SelectedOriginalExam?.Id; + Result.Kind = SelectedOriginalExam?.Kind ?? Kind; + Result.IsMakeUp = IsMakeUp && SelectedOriginalExam is not null; Result.RepeatGradeRule = RepeatRuleName switch { "Wiederholung zählt" => RepeatGradeRule.Repeat, diff --git a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs index db9a3c5..4a515b7 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs @@ -192,8 +192,8 @@ public partial class GradeOverviewTabViewModel : ObservableObject cells.Add(display); if (result.Grade is not null) examEntries.Add((exam, result.Grade)); } - numeric.AddRange(ExamScoringService.EffectiveGrades(examEntries, _gradingSystem) - .Select(grade => (Grade: grade, Weight: 1.0))); + var (writtenGrades, testGrades) = ExamScoringService.EffectiveGradesByKind(examEntries, _gradingSystem); + numeric.AddRange(writtenGrades.Concat(testGrades).Select(grade => (Grade: grade, Weight: 1.0))); foreach (var key in gradeColumnKeys) { diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs index d5880e2..62f8af6 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs @@ -213,7 +213,7 @@ public partial class GroupOverviewViewModel : ObservableObject .Select(r => (r.StudentId, Exam: e, Grade: r.Grade!))) .GroupBy(x => x.StudentId) .SelectMany(g => ExamScoringService.EffectiveGrades( - g.Select(x => (x.Exam, x.Grade)), system)) + g.Where(x => x.Exam.Kind.CountsAsWritten()).Select(x => (x.Exam, x.Grade)), system)) .Select(grade => (Grade: grade, Weight: 1.0)).ToList(); return grades.Count == 0 ? null : (_grading.WeightedAverage(grades), grades.Count); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs index f8a7c41..e94243d 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs @@ -630,8 +630,15 @@ public class ExamSummary Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status; ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? ""; NiveauLabel = e.Niveau.HasValue ? NiveauDisplay.ToName(e.Niveau) : "alle"; - TypeLabel = e.RepeatOfExamId is not null ? (e.Tasks.Any(t => t.Variant is not null) ? "Wdh. · A/B" : "Wiederholung") - : e.Tasks.Any(t => t.Variant is not null) ? "A/B" : "Klausur"; + var hasVariants = e.Tasks.Any(t => t.Variant is not null); + var followUp = e.RepeatOfExamId is null ? null + : e.IsMakeUp ? (hasVariants ? "Nachschr. · A/B" : "Nachschreiben") + : hasVariants ? "Wdh. · A/B" : "Wiederholung"; + // Folgearbeiten behalten ihre Art im Hintergrund (sie erben sie), die Spalte nennt nur + // die Beziehung; bei eigenständigen Arbeiten steht die Art selbst. + TypeLabel = followUp ?? (e.Kind == ExamKind.Klausur + ? (hasVariants ? "A/B" : "Klausur") + : hasVariants ? $"{e.Kind.Label()} · A/B" : e.Kind.Label()); StatusLabel = e.Status switch { ExamStatus.Planned => "Geplant", diff --git a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs index ab89a15..18707e3 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs @@ -141,8 +141,8 @@ public partial class ReportGradeDialogViewModel : ObservableObject if (result.Absent || result.Grade is null) continue; examEntries.Add((exam, result.Grade)); } - var examGrades = ExamScoringService.EffectiveGrades(examEntries, _gradingSystem) - .Select(grade => (Grade: grade, Weight: 1.0)).ToList(); + var (writtenGrades, testGrades) = ExamScoringService.EffectiveGradesByKind(examEntries, _gradingSystem); + var examGrades = writtenGrades.Select(grade => (Grade: grade, Weight: 1.0)).ToList(); var participationGrades = allGrades .Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation @@ -152,6 +152,8 @@ public partial class ReportGradeDialogViewModel : ObservableObject .Where(g => g.StudentId == student.Id && g.Category != GradeCategory.Participation && (membership is null || GroupMembershipService.IsActiveOn(membership, g.Date))) .Select(g => (g.Value, g.Weight)).ToList(); + // Tests, kontrollierte Hausaufgaben u. Ä. aus der Klausurfunktion zählen als Sonstige. + otherGrades.AddRange(testGrades.Select(grade => (grade, 1.0))); var calculated = _grading.CalculateReportGrade(examGrades, participationGrades, otherGrades, scheme, _gradingSystem, RoundingRule); diff --git a/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs index 0b7a218..1c554a0 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs @@ -181,8 +181,8 @@ public partial class StudentPerformanceOverviewViewModel : ObservableObject noteEquivalent, warnings.Count > 0, string.Join(" · ", warnings))); if (noteEquivalent.HasValue) previousNoteEquivalent = noteEquivalent; } - _examGradesRaw = ExamScoringService.EffectiveGrades(effectiveExamEntries, _gradingSystem) - .Select(grade => (Grade: grade, Weight: 1.0)).ToList(); + var (writtenGrades, testGrades) = ExamScoringService.EffectiveGradesByKind(effectiveExamEntries, _gradingSystem); + _examGradesRaw = writtenGrades.Select(grade => (Grade: grade, Weight: 1.0)).ToList(); _examsAverage = _examGradesRaw.Count > 0 ? _grading.WeightedAverage(_examGradesRaw) : null; // ── Mitarbeit & Sonstige ────────────────────────────────────────── @@ -207,6 +207,9 @@ public partial class StudentPerformanceOverviewViewModel : ObservableObject _otherGradesRaw.Add((g.Value, g.Weight)); OtherRows.Add(new StudentSimpleGradeRow(g, SaveGrade)); } + // Tests u. Ä. aus der Klausurfunktion zählen mit, sind aber hier nicht editierbar + // (sie werden in der Punkteeingabe der Arbeit gepflegt). + _otherGradesRaw.AddRange(testGrades.Select(grade => (grade, 1.0))); _otherAverage = _otherGradesRaw.Count > 0 ? _grading.WeightedAverage(_otherGradesRaw) : null; ExamsAverageDisplay = _examsAverage.HasValue ? FormatGrade(_examsAverage.Value) : null; diff --git a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml index ec23978..01ceb31 100644 --- a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml @@ -23,6 +23,13 @@ IsVisible="{Binding TitleError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + + + + + - + + PlaceholderText="Keine Verknüpfung" HorizontalAlignment="Stretch"> @@ -69,8 +76,11 @@