diff --git a/LehrerApp.Core/Models/Exam.cs b/LehrerApp.Core/Models/Exam.cs
index 813862e..b9abe16 100644
--- a/LehrerApp.Core/Models/Exam.cs
+++ b/LehrerApp.Core/Models/Exam.cs
@@ -22,6 +22,9 @@ public class Exam
/// auf der Klausuren-Hauptseite — Id der "Ursprungs"-Klausur, gesetzt beim Duplizieren
/// (siehe ExamDialogViewModel.Save).
public Guid? SharedExamGroupId { get; set; }
+ /// Optional verknüpfte ursprüngliche Arbeit derselben Lerngruppe.
+ public Guid? RepeatOfExamId { get; set; }
+ public RepeatGradeRule RepeatGradeRule { get; set; } = RepeatGradeRule.Better;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -31,6 +34,9 @@ public class ExamTask
public string? Title { get; set; }
public double MaxPoints { get; set; }
public double Weight { get; set; } = 1.0;
+ /// Null gilt für beide Fassungen; A/B nur für die gewählte Fassung.
+ public ExamVariant? Variant { get; set; }
+ public bool IsBonus { get; set; }
public List Competencies { get; set; } = [];
}
public class GradingKeyEntry
@@ -55,7 +61,10 @@ public class ExamResult
public double TotalPoints { get; set; }
public string? Grade { get; set; }
public bool Absent { get; set; }
+ public ExamVariant Variant { get; set; } = ExamVariant.A;
public string? Comment { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
public enum ExamStatus { Planned, Conducted, Graded, Returned }
+public enum ExamVariant { A, B }
+public enum RepeatGradeRule { Better, Repeat, Original }
diff --git a/LehrerApp.Core/Services/ExamScoringService.cs b/LehrerApp.Core/Services/ExamScoringService.cs
new file mode 100644
index 0000000..7951db7
--- /dev/null
+++ b/LehrerApp.Core/Services/ExamScoringService.cs
@@ -0,0 +1,65 @@
+using LehrerApp.Core.Models;
+
+namespace LehrerApp.Core.Services;
+
+public static class ExamScoringService
+{
+ public static bool AppliesTo(ExamTask task, ExamVariant variant) =>
+ task.Variant is null || task.Variant == variant;
+
+ public static double Maximum(IEnumerable tasks, ExamVariant variant) =>
+ tasks.Where(t => AppliesTo(t, variant) && !t.IsBonus).Sum(t => t.MaxPoints);
+
+ public static double Achieved(IReadOnlyList tasks, IReadOnlyList points,
+ 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.
+ 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)
+ {
+ 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);
+ }
+ return result;
+ }
+
+ private static string Choose(string original, string repeat, RepeatGradeRule rule, GradingSystem system)
+ {
+ if (rule == RepeatGradeRule.Original) return original;
+ if (rule == RepeatGradeRule.Repeat) return repeat;
+ var first = GradeValue(original, system);
+ var second = GradeValue(repeat, system);
+ if (first is null || second is null) return original;
+ return system == GradingSystem.Grades1To6
+ ? (second < first ? repeat : original)
+ : (second > first ? repeat : original);
+ }
+
+ private static double? GradeValue(string grade, GradingSystem system)
+ {
+ var core = grade.Trim();
+ var tendency = 0.0;
+ if (core.EndsWith('+')) { tendency = system == GradingSystem.Grades1To6 ? -1.0 / 3 : 1.0 / 3; core = core[..^1]; }
+ else if (core.EndsWith('-')) { tendency = system == GradingSystem.Grades1To6 ? 1.0 / 3 : -1.0 / 3; core = core[..^1]; }
+ return double.TryParse(core.Trim(), System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out var value) ? value + tendency : null;
+ }
+}
diff --git a/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
index db8f94b..fc3796b 100644
--- a/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
+++ b/LehrerApp.Desktop.Tests/ExamDialogViewModelTests.cs
@@ -61,4 +61,27 @@ public sealed class ExamDialogViewModelTests
Assert.Equal("", vm.WeekLoadHint);
}
+
+ [Fact]
+ public void BonusNachtraeglichMarkiert_BerechnetGespeichertesErgebnisNeu()
+ {
+ var exam = MakeExam(new DateOnly(2026, 5, 12));
+ exam.Tasks = [new ExamTask { Nr = 1, MaxPoints = 10 }, new ExamTask { Nr = 2, MaxPoints = 2 }];
+ exam.GradingKey = GradingService.DefaultKey1To6();
+ var result = new ExamResult { ExamId = exam.Id, StudentId = Guid.NewGuid(),
+ Points = [8, 2], TotalPoints = 10, Grade = "2" };
+ var results = new FakeResults();
+ results.Add(result);
+ var vm = new ExamDialogViewModel(new FakeExams([exam]), new FakeCompetencyDomains(),
+ new FakeGradingKeyTemplates(), new GradingService(), exam.GroupId, null, 9,
+ GradingSystem.Grades1To6, "Chemie", false, exam, null,
+ results: results);
+ vm.Tasks[1].IsBonus = true;
+
+ vm.SaveCommand.Execute(null);
+
+ Assert.True(exam.Tasks[1].IsBonus);
+ Assert.Equal(10, result.TotalPoints);
+ Assert.Equal("1", result.Grade);
+ }
}
diff --git a/LehrerApp.Desktop.Tests/ExamVariantScoringTests.cs b/LehrerApp.Desktop.Tests/ExamVariantScoringTests.cs
new file mode 100644
index 0000000..6140ea2
--- /dev/null
+++ b/LehrerApp.Desktop.Tests/ExamVariantScoringTests.cs
@@ -0,0 +1,34 @@
+using LehrerApp.Core.Models;
+using LehrerApp.Core.Services;
+using LehrerApp.Desktop.ViewModels.Groups;
+using Xunit;
+
+namespace LehrerApp.Desktop.Tests;
+
+public class ExamVariantScoringTests
+{
+ [Fact]
+ public void Fassungswechsel_BerechnetPunkteUndNoteNeu()
+ {
+ var tasks = new List
+ {
+ new() { Nr = 1, MaxPoints = 10 },
+ new() { Nr = 2, MaxPoints = 5, Variant = ExamVariant.A },
+ new() { Nr = 3, MaxPoints = 8, Variant = ExamVariant.B },
+ new() { Nr = 4, MaxPoints = 2, IsBonus = true },
+ };
+ var existing = new ExamResult { Points = [8, 5, 8, 2] };
+ var row = new ExamResultRow(Guid.NewGuid(), "Test", tasks, existing,
+ GradingService.DefaultKey1To6(), 15, new GradingService());
+
+ Assert.Equal(15, row.TotalPoints);
+ Assert.False(row.Cells[2].IsApplicable);
+
+ row.VariantName = "B";
+
+ Assert.Equal(18, row.TotalPoints);
+ Assert.False(row.Cells[1].IsApplicable);
+ Assert.True(row.Cells[2].IsApplicable);
+ Assert.Equal(ExamVariant.B, row.ToModel(Guid.NewGuid()).Variant);
+ }
+}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
index b414d60..5f0f7d8 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamEvaluationViewModels.cs
@@ -18,6 +18,7 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
private readonly GradingService _grading;
private readonly Exam _exam;
private readonly GradingSystem _gradingSystem;
+ private readonly IStudentRepository? _students;
private readonly double _examMaxPoints;
private List _gradedResults = [];
@@ -38,19 +39,63 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
public ObservableCollection GradeDistribution { get; } = [];
public ObservableCollection TaskAnalysis { get; } = [];
+ public ObservableCollection RepeatComparison { get; } = [];
+ public bool HasRepeatComparison => RepeatComparison.Count > 0;
+ public string RepeatComparisonTitle { get; private set; } = "";
public ObservableCollection GradingKeyEntries { get; } = [];
public ExamEvaluationDialogViewModel(IExamRepository exams, IExamResultRepository results,
- GradingService grading, Exam exam, GradingSystem gradingSystem)
+ GradingService grading, Exam exam, GradingSystem gradingSystem,
+ IStudentRepository? students = null)
{
_exams = exams; _results = results; _grading = grading; _exam = exam;
- _gradingSystem = gradingSystem;
- _examMaxPoints = exam.Tasks.Sum(t => t.MaxPoints);
+ _gradingSystem = gradingSystem; _students = students;
+ _examMaxPoints = ExamScoringService.Maximum(exam.Tasks, ExamVariant.A);
foreach (var e in exam.GradingKey.OrderByDescending(e => e.MinPercent))
AddGradingKeyRowInternal(e.Grade, e.MinPercent);
LoadResults();
+ LoadRepeatComparison(students);
+ }
+
+ private void LoadRepeatComparison(IStudentRepository? students)
+ {
+ RepeatComparison.Clear();
+ RepeatComparisonTitle = "";
+ 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);
+ if (repeat is null) return;
+ var rule = 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}";
+ 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)
+ .ToDictionary(r => r.StudentId);
+ var names = students?.GetByGroup(_exam.GroupId).ToDictionary(s => s.Id, s => s.FullName);
+ foreach (var studentId in first.Keys.Union(second.Keys)
+ .OrderBy(id => names?.GetValueOrDefault(id) ?? id.ToString()))
+ {
+ first.TryGetValue(studentId, out var originalResult);
+ second.TryGetValue(studentId, out var repeatResult);
+ var entries = new List<(Exam Exam, string Grade)>();
+ if (originalResult?.Grade is { } g1) entries.Add((original, g1));
+ if (repeatResult?.Grade is { } g2) entries.Add((repeat, g2));
+ var effective = ExamScoringService.EffectiveGrades(entries, _gradingSystem);
+ RepeatComparison.Add(new RepeatComparisonItem(
+ names?.GetValueOrDefault(studentId) ?? studentId.ToString(),
+ originalResult?.Grade ?? "–", repeatResult?.Grade ?? "–",
+ effective.FirstOrDefault() ?? "–"));
+ }
+ OnPropertyChanged(nameof(HasRepeatComparison));
+ OnPropertyChanged(nameof(RepeatComparisonTitle));
}
private void LoadResults()
@@ -98,8 +143,11 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
foreach (var e in GradingKeyEntries)
e.AbsolutePointsDisplay = _examMaxPoints <= 0
? "–"
- : $"ab {(e.MinPercent / 100.0 * _examMaxPoints).ToString("0.##", CultureInfo.InvariantCulture)} " +
- $"von {_examMaxPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkten";
+ : _exam.Tasks.Any(t => t.Variant is not null)
+ ? $"A ab {e.MinPercent / 100.0 * _examMaxPoints:0.##} / " +
+ $"B ab {e.MinPercent / 100.0 * ExamScoringService.Maximum(_exam.Tasks, ExamVariant.B):0.##} Punkten"
+ : $"ab {(e.MinPercent / 100.0 * _examMaxPoints).ToString("0.##", CultureInfo.InvariantCulture)} " +
+ $"von {_examMaxPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkten";
}
private List BuildCurrentGradingKey() => GradingKeyEntries
@@ -116,6 +164,12 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
GradingKeyValidation = "";
_exam.GradingKey = key;
_exams.Save(_exam);
+ var results = _results.GetByExam(_exam.Id);
+ foreach (var result in results.Where(r => !r.Absent))
+ result.Grade = _grading.CalculateGrade(result.TotalPoints,
+ ExamScoringService.Maximum(_exam.Tasks, result.Variant), key);
+ _results.SaveMany(results);
+ LoadRepeatComparison(_students);
GradingKeyStatus = "Notenschlüssel für diese Klausur übernommen.";
}
@@ -127,7 +181,8 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
GradingKeyValidation = _grading.ValidateGradingKey(currentKey) ?? "";
var grades = _gradedResults
- .Select(r => _grading.CalculateGrade(r.TotalPoints, _examMaxPoints, currentKey))
+ .Select(r => _grading.CalculateGrade(r.TotalPoints,
+ ExamScoringService.Maximum(_exam.Tasks, r.Variant), currentKey))
.ToList();
GradeDistribution.Clear();
@@ -174,11 +229,14 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
for (var i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
- var achieved = _gradedResults.Where(r => i < r.Points.Count).Select(r => r.Points[i]).ToList();
+ var achieved = _gradedResults.Where(r => i < r.Points.Count &&
+ ExamScoringService.AppliesTo(task, r.Variant)).Select(r => r.Points[i]).ToList();
var avgPercent = task.MaxPoints <= 0 || achieved.Count == 0
? 0
: achieved.Average() / task.MaxPoints * 100.0;
- TaskAnalysis.Add(new TaskAnalysisItem(task.Nr, task.Title, avgPercent));
+ var suffix = (task.Variant is null ? "" : $" ({task.Variant})")
+ + (task.IsBonus ? " · Bonus" : "");
+ TaskAnalysis.Add(new TaskAnalysisItem(task.Nr, (task.Title ?? "") + suffix, avgPercent));
}
}
@@ -209,8 +267,15 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
if (!string.IsNullOrEmpty(BelowThresholdLabel1)) csv.AddRow(BelowThresholdLabel1, BelowThresholdDisplay1);
if (!string.IsNullOrEmpty(BelowThresholdLabel2)) csv.AddRow(BelowThresholdLabel2, BelowThresholdDisplay2);
csv.AddRow("Bewertet", GradedCount)
- .AddRow("Abwesend", AbsentCount)
- .AddBlankRow()
+ .AddRow("Abwesend", AbsentCount);
+ if (HasRepeatComparison)
+ {
+ csv.AddBlankRow().AddRow("Vergleich", RepeatComparisonTitle)
+ .AddRow("Schüler", "Erste Arbeit", "Wiederholung", "Zählt");
+ foreach (var item in RepeatComparison)
+ csv.AddRow(item.StudentName, item.OriginalGrade, item.RepeatGrade, item.EffectiveGrade);
+ }
+ csv.AddBlankRow()
.AddRow("Aufgabenanalyse")
.AddRow("Aufgabe", "Ø Erfüllungsgrad", "Auffällig schwach");
foreach (var t in TaskAnalysis)
@@ -221,6 +286,9 @@ public partial class ExamEvaluationDialogViewModel : ObservableObject
// ── Notenspiegel-Balken ────────────────────────────────────────────────────
+public record RepeatComparisonItem(string StudentName, string OriginalGrade,
+ string RepeatGrade, string EffectiveGrade);
+
public class GradeBarItem
{
public string Grade { get; }
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs
index e05e4e9..f452d80 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamGradingViewModels.cs
@@ -15,12 +15,12 @@ public partial class ExamGradingDialogViewModel : ObservableObject
private readonly IExamResultRepository _results;
private readonly GradingService _grading;
private readonly Exam _exam;
- private readonly double _examMaxPoints;
public string ExamTitle => _exam.Title;
public string ExamDateLabel => _exam.Date.ToString("dd.MM.yyyy");
public string NiveauLabel => _exam.Niveau.HasValue ? NiveauDisplay.ToName(_exam.Niveau) : "";
public List Tasks { get; }
+ public bool HasVariants => Tasks.Any(t => t.Variant is not null);
public ObservableCollection Rows { get; } = [];
@@ -29,7 +29,6 @@ public partial class ExamGradingDialogViewModel : ObservableObject
{
_results = results; _grading = grading; _exam = exam;
Tasks = exam.Tasks.OrderBy(t => t.Nr).ToList();
- _examMaxPoints = Tasks.Sum(t => t.MaxPoints);
var enrolled = students.GetByGroup(groupId);
var membershipList = memberships.GetByGroup(groupId);
@@ -45,7 +44,8 @@ public partial class ExamGradingDialogViewModel : ObservableObject
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
existing.TryGetValue(s.Id, out var result);
- var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey, _examMaxPoints, _grading);
+ var row = new ExamResultRow(s.Id, s.FullName, Tasks, result, _exam.GradingKey,
+ ExamScoringService.Maximum(Tasks, ExamVariant.A), _grading);
row.OnChanged = SaveRow;
Rows.Add(row);
}
@@ -72,6 +72,7 @@ public partial class ExamResultRow : ObservableObject
private readonly Guid _resultId;
private readonly List _gradingKey;
private readonly double _examMaxPoints;
+ private readonly List _tasks;
private readonly GradingService _grading;
public Guid StudentId { get; }
@@ -83,6 +84,9 @@ public partial class ExamResultRow : ObservableObject
[ObservableProperty] private string _comment = "";
[ObservableProperty] private double _totalPoints;
[ObservableProperty] private string _gradeDisplay = "";
+ [ObservableProperty] private string _variantName = "A";
+ public string[] VariantOptions => ["A", "B"];
+ private ExamVariant Variant => VariantName == "B" ? ExamVariant.B : ExamVariant.A;
public string TotalPointsDisplay => TotalPoints.ToString("0.##", CultureInfo.InvariantCulture);
@@ -92,15 +96,19 @@ public partial class ExamResultRow : ObservableObject
List gradingKey, double examMaxPoints, GradingService grading)
{
StudentId = studentId; Name = name;
- _gradingKey = gradingKey; _examMaxPoints = examMaxPoints; _grading = grading;
+ _gradingKey = gradingKey; _examMaxPoints = examMaxPoints; _grading = grading; _tasks = tasks;
_resultId = existing?.Id ?? Guid.NewGuid();
_absent = existing?.Absent ?? false;
_comment = existing?.Comment ?? "";
+ _variantName = existing?.Variant.ToString() ?? "A";
for (var i = 0; i < tasks.Count; i++)
{
double? pts = existing is not null && i < existing.Points.Count ? existing.Points[i] : null;
- var cell = new PointsCell(tasks[i].MaxPoints, pts);
+ var cell = new PointsCell(tasks[i].MaxPoints, pts)
+ {
+ IsApplicable = ExamScoringService.AppliesTo(tasks[i], Variant),
+ };
cell.OnChanged = () => { RecomputeTotals(); OnChanged?.Invoke(this); };
Cells.Add(cell);
}
@@ -114,12 +122,20 @@ public partial class ExamResultRow : ObservableObject
}
partial void OnCommentChanged(string value) => OnChanged?.Invoke(this);
+ partial void OnVariantNameChanged(string value)
+ {
+ for (var i = 0; i < Cells.Count; i++)
+ Cells[i].IsApplicable = ExamScoringService.AppliesTo(_tasks[i], Variant);
+ RecomputeTotals();
+ OnChanged?.Invoke(this);
+ }
private void RecomputeTotals()
{
- TotalPoints = Cells.Sum(c => c.IsInvalid ? 0 : (c.Value ?? 0));
+ TotalPoints = Cells.Sum(c => c.IsApplicable && !c.IsInvalid ? (c.Value ?? 0) : 0);
OnPropertyChanged(nameof(TotalPointsDisplay));
- GradeDisplay = Absent ? "abwesend" : _grading.CalculateGrade(TotalPoints, _examMaxPoints, _gradingKey);
+ GradeDisplay = Absent ? "abwesend" : _grading.CalculateGrade(TotalPoints,
+ ExamScoringService.Maximum(_tasks, Variant), _gradingKey);
}
public ExamResult ToModel(Guid examId) => new()
@@ -131,6 +147,7 @@ public partial class ExamResultRow : ObservableObject
TotalPoints = TotalPoints,
Grade = Absent ? null : GradeDisplay,
Absent = Absent,
+ Variant = Variant,
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
};
}
@@ -142,6 +159,7 @@ public partial class PointsCell : ObservableObject
public double MaxPoints { get; }
[ObservableProperty] private double? _value;
[ObservableProperty] private bool _isInvalid;
+ [ObservableProperty] private bool _isApplicable = true;
public Action? OnChanged { get; set; }
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
index c2702d2..1896730 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
@@ -13,6 +13,7 @@ namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class ExamDialogViewModel : ObservableObject
{
private readonly IExamRepository _exams;
+ private readonly IExamResultRepository? _results;
private readonly ICompetencyDomainRepository _competencyDomains;
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
private readonly GradingService _grading;
@@ -41,13 +42,21 @@ public partial class ExamDialogViewModel : ObservableObject
[ObservableProperty] private string _newTemplateName = "";
[ObservableProperty] private string _gradingKeyValidation = "";
[ObservableProperty] private string _selectedNiveauName = "–";
+ [ObservableProperty] private Exam? _selectedOriginalExam;
+ [ObservableProperty] private string _repeatRuleName = "Bessere zählt";
+ [ObservableProperty] private string _repeatError = "";
+ [ObservableProperty] private string _taskPointsError = "";
public bool IsDifferentiated { get; }
public string[] NiveauOptions => NiveauDisplay.Options;
+ public string[] RepeatRuleOptions => ["Bessere zählt", "Wiederholung zählt", "Erste Arbeit zählt"];
+ public bool HasSelectedOriginalExam => SelectedOriginalExam is not null;
+ public ObservableCollection AvailableOriginalExams { get; } = [];
public bool TotalPointsWarning => TotalPoints <= 0;
- public string TotalPointsDisplay =>
- $"{TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkte gesamt";
+ public string TotalPointsDisplay => Tasks.Any(t => t.VariantName != "Beide")
+ ? $"A: {TotalPoints:0.##} · B: {Tasks.Where(t => !t.IsBonus && t.VariantName != "A").Sum(t => t.MaxPoints):0.##} Punkte (ohne Bonus)"
+ : $"{TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkte gesamt (ohne Bonus)";
public ObservableCollection Tasks { get; } = [];
public ObservableCollection GradingKeyEntries { get; } = [];
@@ -65,9 +74,10 @@ public partial class ExamDialogViewModel : ObservableObject
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading,
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem,
string defaultSubjectName, bool isDifferentiated, Exam? editingExam, Exam? duplicateSource,
- Guid competencyCatalogVersionId = default)
+ Guid competencyCatalogVersionId = default, IExamResultRepository? results = null)
{
_exams = exams; _competencyDomains = competencyDomains;
+ _results = results;
_gradingKeyTemplates = gradingKeyTemplates; _grading = grading;
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
_competencyCatalogVersionId = competencyCatalogVersionId;
@@ -76,6 +86,12 @@ public partial class ExamDialogViewModel : ObservableObject
_duplicateSource = duplicateSource;
_subjectName = defaultSubjectName;
IsDifferentiated = isDifferentiated;
+ var groupExams = _exams.GetByGroup(groupId);
+ 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);
HasCompetencyCatalog = subjectId.HasValue
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel, competencyCatalogVersionId).Count > 0;
@@ -97,8 +113,18 @@ 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);
+ if (!isDuplicate)
+ {
+ SelectedOriginalExam = AvailableOriginalExams.FirstOrDefault(e => e.Id == source.RepeatOfExamId);
+ RepeatRuleName = source.RepeatGradeRule switch
+ {
+ RepeatGradeRule.Repeat => "Wiederholung zählt",
+ RepeatGradeRule.Original => "Erste Arbeit zählt",
+ _ => "Bessere zählt",
+ };
+ }
foreach (var t in source.Tasks.OrderBy(t => t.Nr))
- AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.Competencies]);
+ AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.Competencies], t.Variant, t.IsBonus);
UseWeighting = Tasks.Any(t => Math.Abs(t.Weight - 1.0) > 0.0001);
foreach (var e in source.GradingKey.OrderByDescending(e => e.MinPercent))
AddGradingKeyRowInternal(e.Grade, e.MinPercent);
@@ -119,6 +145,9 @@ public partial class ExamDialogViewModel : ObservableObject
{
foreach (var t in Tasks) t.ShowWeight = value;
}
+ partial void OnSelectedOriginalExamChanged(Exam? value) => OnPropertyChanged(nameof(HasSelectedOriginalExam));
+
+ [RelayCommand] private void ClearOriginalExam() => SelectedOriginalExam = null;
partial void OnDateTextChanged(string value) => UpdateWeekLoadHint();
@@ -148,11 +177,14 @@ public partial class ExamDialogViewModel : ObservableObject
RecomputeTotals();
}
- private void AddTaskInternal(string? title, double maxPoints, double weight, List competencies)
+ private void AddTaskInternal(string? title, double maxPoints, double weight, List competencies,
+ ExamVariant? variant = null, bool isBonus = false)
{
var item = new ExamTaskEditItem(title, maxPoints, weight, competencies,
BuildCompetencyTagGroups(competencies))
{
+ VariantName = variant?.ToString() ?? "Beide",
+ IsBonus = isBonus,
OnChanged = RecomputeTotals,
OnRemove = RemoveTask,
OnMoveUp = MoveTaskUp,
@@ -201,7 +233,7 @@ public partial class ExamDialogViewModel : ObservableObject
private void RecomputeTotals()
{
- TotalPoints = Tasks.Sum(t => t.MaxPoints);
+ TotalPoints = Tasks.Where(t => !t.IsBonus && t.VariantName != "B").Sum(t => t.MaxPoints);
OnPropertyChanged(nameof(TotalPointsWarning));
OnPropertyChanged(nameof(TotalPointsDisplay));
RecomputeGradingKeyAbsolutes();
@@ -214,6 +246,9 @@ public partial class ExamDialogViewModel : ObservableObject
foreach (var e in GradingKeyEntries)
e.AbsolutePointsDisplay = TotalPoints <= 0
? "–"
+ : Tasks.Any(t => t.VariantName != "Beide")
+ ? $"A ab {e.MinPercent / 100.0 * TotalPoints:0.##} / " +
+ $"B ab {e.MinPercent / 100.0 * Tasks.Where(t => !t.IsBonus && t.VariantName != "A").Sum(t => t.MaxPoints):0.##} Punkten"
: $"ab {(e.MinPercent / 100.0 * TotalPoints).ToString("0.##", CultureInfo.InvariantCulture)} " +
$"von {TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkten";
}
@@ -288,6 +323,7 @@ public partial class ExamDialogViewModel : ObservableObject
private void Save()
{
TitleError = ""; DateTextError = ""; ReturnedAtTextError = ""; GradingKeyValidation = "";
+ RepeatError = ""; TaskPointsError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
@@ -313,6 +349,17 @@ public partial class ExamDialogViewModel : ObservableObject
var gradingKey = BuildGradingKeyEntries();
var gradingKeyError = _grading.ValidateGradingKey(gradingKey);
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.";
+ valid = false;
+ }
+ if (Tasks.Any(t => t.VariantName != "Beide") &&
+ (TotalPoints <= 0 || Tasks.Where(t => !t.IsBonus && t.VariantName != "A").Sum(t => t.MaxPoints) <= 0))
+ {
+ TaskPointsError = "Fassung A und B brauchen jeweils Grundpunkte.";
+ valid = false;
+ }
if (!valid) return;
@@ -325,12 +372,31 @@ public partial class ExamDialogViewModel : ObservableObject
Result.Niveau = NiveauDisplay.FromName(SelectedNiveauName);
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
Result.GradingKey = gradingKey;
+ Result.RepeatOfExamId = SelectedOriginalExam?.Id;
+ Result.RepeatGradeRule = RepeatRuleName switch
+ {
+ "Wiederholung zählt" => RepeatGradeRule.Repeat,
+ "Erste Arbeit zählt" => RepeatGradeRule.Original,
+ _ => RepeatGradeRule.Better,
+ };
// Parallelkurse (gleiche Arbeit, mehrere Kurse): beim Duplizieren verknüpfen, damit die
// Klausuren-Hauptseite zwischen ihnen umschalten kann. Der Anker ist die Id der zuerst
// angelegten Klausur der Kette — schon verknüpfte Quellen geben ihren Anker weiter.
if (_duplicateSource is not null)
Result.SharedExamGroupId = _duplicateSource.SharedExamGroupId ?? _duplicateSource.Id;
_exams.Save(Result);
+ if (_editingExam is not null && _results is not null)
+ {
+ var tasks = Result.Tasks.OrderBy(t => t.Nr).ToList();
+ var existingResults = _results.GetByExam(Result.Id);
+ foreach (var result in existingResults.Where(r => !r.Absent))
+ {
+ result.TotalPoints = ExamScoringService.Achieved(tasks, result.Points, result.Variant);
+ result.Grade = _grading.CalculateGrade(result.TotalPoints,
+ ExamScoringService.Maximum(tasks, result.Variant), gradingKey);
+ }
+ _results.SaveMany(existingResults);
+ }
}
}
@@ -361,6 +427,9 @@ public partial class ExamTaskEditItem : ObservableObject
[ObservableProperty] private double _weight = 1.0;
[ObservableProperty] private bool _isCompetencyPanelOpen;
[ObservableProperty] private bool _showWeight;
+ [ObservableProperty] private string _variantName = "Beide";
+ [ObservableProperty] private bool _isBonus;
+ public string[] VariantOptions => ["Beide", "A", "B"];
public List Competencies { get; }
public ObservableCollection CompetencyTagGroups { get; } = [];
@@ -409,6 +478,8 @@ public partial class ExamTaskEditItem : ObservableObject
[RelayCommand] private void MoveDown() => OnMoveDown?.Invoke(this);
partial void OnMaxPointsChanged(double value) => OnChanged?.Invoke();
+ partial void OnVariantNameChanged(string value) => OnChanged?.Invoke();
+ partial void OnIsBonusChanged(bool value) => OnChanged?.Invoke();
public ExamTask ToModel() => new()
{
@@ -416,6 +487,8 @@ public partial class ExamTaskEditItem : ObservableObject
Title = string.IsNullOrWhiteSpace(Title) ? null : Title.Trim(),
MaxPoints = MaxPoints,
Weight = Weight,
+ Variant = VariantName switch { "A" => ExamVariant.A, "B" => ExamVariant.B, _ => null },
+ IsBonus = IsBonus,
Competencies = Competencies,
};
}
diff --git a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
index 29cc665..db9a3c5 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GradeOverviewViewModels.cs
@@ -178,6 +178,7 @@ public partial class GradeOverviewTabViewModel : ObservableObject
var cells = new List();
var numeric = new List<(string Grade, double Weight)>();
+ var examEntries = new List<(Exam Exam, string Grade)>();
foreach (var exam in exams)
{
@@ -189,8 +190,10 @@ public partial class GradeOverviewTabViewModel : ObservableObject
if (result.Absent) { cells.Add("abwesend"); continue; }
var display = FormatValue(result.Grade);
cells.Add(display);
- if (result.Grade is not null) numeric.Add((result.Grade, 1.0));
+ if (result.Grade is not null) examEntries.Add((exam, result.Grade));
}
+ numeric.AddRange(ExamScoringService.EffectiveGrades(examEntries, _gradingSystem)
+ .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 c32cf8a..a955461 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupOverviewViewModel.cs
@@ -206,11 +206,15 @@ public partial class GroupOverviewViewModel : ObservableObject
private (double Average, int Count)? ComputeExamAverage(Guid groupId)
{
+ var system = _groups.GetById(groupId)?.GradingSystem ?? GradingSystem.Grades1To6;
var grades = _exams.GetByGroup(groupId)
- .SelectMany(e => _examResults.GetByExam(e.Id))
- .Where(r => !r.Absent && !string.IsNullOrWhiteSpace(r.Grade))
- .Select(r => (Grade: r.Grade!, Weight: 1.0))
- .ToList();
+ .SelectMany(e => _examResults.GetByExam(e.Id)
+ .Where(r => !r.Absent && !string.IsNullOrWhiteSpace(r.Grade))
+ .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))
+ .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 4542332..f8a7c41 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/GroupViewModels.cs
@@ -427,6 +427,11 @@ public partial class GroupDetailViewModel : ObservableObject
if (SelectedExam is null || OnConfirmDeleteExam is null) return;
var selected = SelectedExam;
if (!await OnConfirmDeleteExam(selected)) return;
+ foreach (var repeat in _exams.GetByGroup(Group!.Id).Where(e => e.RepeatOfExamId == selected.Id))
+ {
+ repeat.RepeatOfExamId = null;
+ _exams.Save(repeat);
+ }
_exams.Delete(selected.Id);
SelectedExam = null;
ReloadExams();
@@ -618,12 +623,15 @@ public class ExamSummary
public string StatusColorHex { get; }
public string ReturnedAtDisplay { get; }
public string NiveauLabel { get; }
+ public string TypeLabel { get; }
public ExamSummary(Core.Models.Exam e)
{
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";
StatusLabel = e.Status switch
{
ExamStatus.Planned => "Geplant",
diff --git a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs
index 4daddfd..ab89a15 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/ReportGradeViewModels.cs
@@ -132,15 +132,17 @@ public partial class ReportGradeDialogViewModel : ObservableObject
continue;
}
- var examGrades = new List<(string Grade, double Weight)>();
+ var examEntries = new List<(Exam Exam, string Grade)>();
foreach (var exam in exams)
{
if (membership is not null && !GroupMembershipService.IsActiveOn(membership, exam.Date)) continue;
if (exam.Niveau.HasValue && membership?.Niveau != exam.Niveau) continue;
if (!resultsByExam[exam.Id].TryGetValue(student.Id, out var result)) continue;
if (result.Absent || result.Grade is null) continue;
- examGrades.Add((result.Grade, 1.0));
+ examEntries.Add((exam, result.Grade));
}
+ var examGrades = ExamScoringService.EffectiveGrades(examEntries, _gradingSystem)
+ .Select(grade => (Grade: grade, Weight: 1.0)).ToList();
var participationGrades = allGrades
.Where(g => g.StudentId == student.Id && g.Category == GradeCategory.Participation
diff --git a/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs
index 907a9a1..0b7a218 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/StudentPerformanceOverviewViewModel.cs
@@ -143,6 +143,7 @@ public partial class StudentPerformanceOverviewViewModel : ObservableObject
ExamRows.Clear();
ExamHistory.Points.Clear();
_examGradesRaw = [];
+ var effectiveExamEntries = new List<(Exam Exam, string Grade)>();
int? previousNoteEquivalent = null;
foreach (var exam in _exams.GetByGroup(_groupId)
.Where(e => e.Date >= periodFrom && e.Date <= periodTo)
@@ -153,13 +154,15 @@ public partial class StudentPerformanceOverviewViewModel : ObservableObject
var allResults = _results.GetByExam(exam.Id);
var own = allResults.FirstOrDefault(r => r.StudentId == _studentId);
string? ownGrade = own is { Absent: false, Grade: not null } ? own.Grade : null;
- if (ownGrade is not null) _examGradesRaw.Add((ownGrade, 1.0));
+ if (ownGrade is not null) effectiveExamEntries.Add((exam, ownGrade));
var classGrades = allResults.Where(r => !r.Absent && r.Grade is not null)
.Select(r => (r.Grade!, 1.0)).ToList();
var classAverage = classGrades.Count > 0 ? _grading.WeightedAverage(classGrades) : (double?)null;
- ExamRows.Add(new StudentExamRow(exam.Title, exam.Date, ownGrade,
+ var examLabel = exam.Title + (exam.Tasks.Any(t => t.Variant is not null) && own is not null
+ ? $" (Fassung {own.Variant})" : "");
+ ExamRows.Add(new StudentExamRow(examLabel, exam.Date, ownGrade,
classAverage.HasValue ? FormatGrade(classAverage.Value) : "–"));
// Gleiche Balken-Sparkline wie die bestehende Notenentwicklung (2.5): Rohwert auf die
@@ -174,10 +177,12 @@ public partial class StudentPerformanceOverviewViewModel : ObservableObject
var warnings = new List();
if (isDrop) warnings.Add("Abfall um ≥ 1 Note");
if (isFailing) warnings.Add("Versetzungsgefährdung");
- ExamHistory.Points.Add(new GradeHistoryPoint(exam.Date, exam.Title, ownGrade ?? "–",
+ ExamHistory.Points.Add(new GradeHistoryPoint(exam.Date, examLabel, ownGrade ?? "–",
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();
_examsAverage = _examGradesRaw.Count > 0 ? _grading.WeightedAverage(_examGradesRaw) : null;
// ── Mitarbeit & Sonstige ──────────────────────────────────────────
diff --git a/LehrerApp.Desktop/Views/Exams/ExamsOverviewView.axaml.cs b/LehrerApp.Desktop/Views/Exams/ExamsOverviewView.axaml.cs
index 1434c07..d4af1cd 100644
--- a/LehrerApp.Desktop/Views/Exams/ExamsOverviewView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Exams/ExamsOverviewView.axaml.cs
@@ -54,7 +54,8 @@ public partial class ExamsOverviewView : UserControl
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
- exam, group.GradingSystem);
+ exam, group.GradingSystem,
+ App.Services.GetRequiredService());
var dialog = new ExamEvaluationDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
diff --git a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
index d530465..7d01a17 100644
--- a/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/ExamDialog.axaml
@@ -52,6 +52,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -72,6 +95,8 @@
+
@@ -95,6 +120,15 @@
ToolTip.Tip="Entfernen" Margin="4,0,0,0"/>
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml b/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml
index e2b9cc4..4387a87 100644
--- a/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/ExamEvaluationDialog.axaml
@@ -111,6 +111,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml.cs
index 6cddd54..f67644f 100644
--- a/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/ExamGradingDialog.axaml.cs
@@ -41,12 +41,27 @@ public partial class ExamGradingDialog : Window
Width = new DataGridLength(170, DataGridLengthUnitType.Pixel),
});
+ if (vm.HasVariants)
+ grid.Columns.Add(new DataGridTemplateColumn
+ {
+ Header = "Fassung", Width = new DataGridLength(82, DataGridLengthUnitType.Pixel),
+ CellTemplate = new FuncDataTemplate((row, _) =>
+ {
+ if (row is null) return new TextBlock();
+ var choice = new ComboBox { ItemsSource = row.VariantOptions, SelectedItem = row.VariantName };
+ choice.SelectionChanged += (_, _) => row.VariantName = choice.SelectedItem as string ?? "A";
+ return choice;
+ }),
+ });
+
for (var i = 0; i < vm.Tasks.Count; i++)
{
var task = vm.Tasks[i];
grid.Columns.Add(new DataGridTemplateColumn
{
- Header = string.IsNullOrWhiteSpace(task.Title) ? $"Aufg. {task.Nr}" : $"{task.Nr}. {task.Title}",
+ Header = (string.IsNullOrWhiteSpace(task.Title) ? $"Aufg. {task.Nr}" : $"{task.Nr}. {task.Title}")
+ + (task.Variant is null ? "" : $" ({task.Variant})")
+ + (task.IsBonus ? " ★ Bonus" : ""),
Width = new DataGridLength(1, DataGridLengthUnitType.Star),
CellTemplate = BuildPointsCellTemplate(i, grid, vm.Tasks.Count),
});
@@ -91,6 +106,7 @@ public partial class ExamGradingDialog : Window
Background = Brushes.Transparent,
Padding = new Thickness(6, 4),
HorizontalContentAlignment = HorizontalAlignment.Center,
+ IsEnabled = cell.IsApplicable,
};
ToolTip.SetTip(tb, $"max. {FormatPoints(cell.MaxPoints)} Punkte");
UpdateInvalidVisual(tb, cell.IsInvalid);
@@ -99,6 +115,8 @@ public partial class ExamGradingDialog : Window
{
if (pe.PropertyName == nameof(PointsCell.IsInvalid))
UpdateInvalidVisual(tb, cell.IsInvalid);
+ if (pe.PropertyName == nameof(PointsCell.IsApplicable))
+ tb.IsEnabled = cell.IsApplicable;
};
tb.LostFocus += (_, _) => CommitPointsCell(tb, cell);
diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
index d3867b0..971bf18 100644
--- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
+++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml
@@ -145,6 +145,7 @@
+
diff --git a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs
index 47e3473..caa96e6 100644
--- a/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/GroupDetailView.axaml.cs
@@ -255,7 +255,8 @@ public partial class GroupDetailView : UserControl
App.Services.GetRequiredService(),
groupId, vm.Group.SubjectId, vm.Group.GradeLevel, vm.Group.GradingSystem,
vm.SubjectName, vm.Group.IsDifferentiated, editingExam, duplicateSource,
- competencyCatalogVersionId);
+ competencyCatalogVersionId,
+ App.Services.GetRequiredService());
var dialog = new ExamDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
@@ -295,7 +296,8 @@ public partial class GroupDetailView : UserControl
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
App.Services.GetRequiredService(),
- exam, vm.Group.GradingSystem);
+ exam, vm.Group.GradingSystem,
+ App.Services.GetRequiredService());
var dialog = new ExamEvaluationDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
diff --git a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
index 8e49345..6d34940 100644
--- a/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
+++ b/LehrerApp.Desktop/Views/Planning/TimetableView.axaml
@@ -12,6 +12,14 @@
+
+
@@ -254,12 +262,21 @@
-