Notenschlüssel-Editor (1.3) und Einstellungen in Tabs strukturiert
- Notenschlüssel-Editor im ExamDialog: Stufen (Note/Prozentgrenze) bearbeitbar, Vorbelegung passend zum GradingSystem der Gruppe, Live-Anzeige der absoluten Punktegrenze je Stufe, Validierung (lückenlos, keine Dopplungen) über GradingService.ValidateGradingKey. - Neues Modell GradingKeyTemplate + Repository: Notenschlüssel als Vorlage speichern/anwenden, direkt aus dem ExamDialog heraus. - Einstellungen: Fächer / Kompetenzen / Notenschlüssel-Vorlagen sind jetzt eigene Tabs statt einer gemeinsam wachsenden Liste. Vorlagen zeigen nur noch eine kompakte Zeile; die Stufen-Bearbeitung läuft über ein Popup-Fenster (GradingKeyTemplateDialog). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,11 +14,13 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly IExamRepository _exams;
|
||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
||||
private readonly GradingService _grading;
|
||||
private readonly Guid _groupId;
|
||||
private readonly Guid? _subjectId;
|
||||
private readonly int _gradeLevel;
|
||||
private readonly GradingSystem _gradingSystem;
|
||||
private readonly Exam? _editingExam;
|
||||
private readonly List<GradingKeyEntry> _gradingKey;
|
||||
private readonly string _subjectName;
|
||||
|
||||
[ObservableProperty] private string _title = "";
|
||||
@@ -30,12 +32,17 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
[ObservableProperty] private double _totalPoints;
|
||||
[ObservableProperty] private bool _hasCompetencyCatalog;
|
||||
[ObservableProperty] private bool _useWeighting;
|
||||
[ObservableProperty] private GradingKeyTemplate? _selectedTemplate;
|
||||
[ObservableProperty] private string _newTemplateName = "";
|
||||
[ObservableProperty] private string _gradingKeyValidation = "";
|
||||
|
||||
public bool TotalPointsWarning => TotalPoints <= 0;
|
||||
public string TotalPointsDisplay =>
|
||||
$"{TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkte gesamt";
|
||||
|
||||
public ObservableCollection<ExamTaskEditItem> Tasks { get; } = [];
|
||||
public ObservableCollection<GradingKeyEntryEditItem> GradingKeyEntries { get; } = [];
|
||||
public ObservableCollection<GradingKeyTemplate> AvailableTemplates { get; } = [];
|
||||
|
||||
public Exam? Result { get; private set; }
|
||||
public string DialogTitle => _editingExam is null ? "Neue Klausur anlegen" : "Klausur bearbeiten";
|
||||
@@ -46,17 +53,23 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
? "Kein Fach hinterlegt (siehe Lerngruppe)" : $"Fach: {_subjectName}";
|
||||
|
||||
public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading,
|
||||
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem,
|
||||
string defaultSubjectName, Exam? editingExam, Exam? duplicateSource)
|
||||
{
|
||||
_exams = exams; _competencyDomains = competencyDomains;
|
||||
_gradingKeyTemplates = gradingKeyTemplates; _grading = grading;
|
||||
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
|
||||
_gradingSystem = gradingSystem;
|
||||
_editingExam = editingExam;
|
||||
_subjectName = defaultSubjectName;
|
||||
|
||||
HasCompetencyCatalog = subjectId.HasValue
|
||||
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0;
|
||||
|
||||
foreach (var t in _gradingKeyTemplates.GetByGradingSystem(gradingSystem))
|
||||
AvailableTemplates.Add(t);
|
||||
|
||||
var source = editingExam ?? duplicateSource;
|
||||
if (source is not null)
|
||||
{
|
||||
@@ -68,16 +81,18 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
ExamNumber = source.ExamNumber;
|
||||
Notes = source.Notes ?? "";
|
||||
ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
|
||||
_gradingKey = source.GradingKey
|
||||
.Select(k => new GradingKeyEntry { Grade = k.Grade, MinPercent = k.MinPercent }).ToList();
|
||||
foreach (var t in source.Tasks.OrderBy(t => t.Nr))
|
||||
AddTaskInternal(t.Title, t.MaxPoints, t.Weight, [.. t.CompetencyCodes]);
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gradingKey = gradingSystem == GradingSystem.Grades1To6
|
||||
var defaults = gradingSystem == GradingSystem.Grades1To6
|
||||
? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15();
|
||||
foreach (var e in defaults)
|
||||
AddGradingKeyRowInternal(e.Grade, e.MinPercent);
|
||||
}
|
||||
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
|
||||
RecomputeTotals();
|
||||
@@ -158,8 +173,86 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
TotalPoints = Tasks.Sum(t => t.MaxPoints);
|
||||
OnPropertyChanged(nameof(TotalPointsWarning));
|
||||
OnPropertyChanged(nameof(TotalPointsDisplay));
|
||||
RecomputeGradingKeyAbsolutes();
|
||||
}
|
||||
|
||||
// ── Notenschlüssel (1.3) ──────────────────────────────────────────────────
|
||||
|
||||
private void RecomputeGradingKeyAbsolutes()
|
||||
{
|
||||
foreach (var e in GradingKeyEntries)
|
||||
e.AbsolutePointsDisplay = TotalPoints <= 0
|
||||
? "–"
|
||||
: $"ab {(e.MinPercent / 100.0 * TotalPoints).ToString("0.##", CultureInfo.InvariantCulture)} " +
|
||||
$"von {TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkten";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddGradingKeyRow()
|
||||
{
|
||||
AddGradingKeyRowInternal("", 0);
|
||||
RecomputeGradingKeyAbsolutes();
|
||||
}
|
||||
|
||||
private void AddGradingKeyRowInternal(string grade, double minPercent)
|
||||
{
|
||||
var item = new GradingKeyEntryEditItem
|
||||
{
|
||||
Grade = grade,
|
||||
MinPercent = minPercent,
|
||||
OnChanged = RecomputeGradingKeyAbsolutes,
|
||||
OnRemove = RemoveGradingKeyRow,
|
||||
};
|
||||
GradingKeyEntries.Add(item);
|
||||
}
|
||||
|
||||
private void RemoveGradingKeyRow(GradingKeyEntryEditItem item)
|
||||
{
|
||||
GradingKeyEntries.Remove(item);
|
||||
RecomputeGradingKeyAbsolutes();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedTemplate))]
|
||||
private void ApplyTemplate()
|
||||
{
|
||||
if (SelectedTemplate is null) return;
|
||||
GradingKeyEntries.Clear();
|
||||
foreach (var e in SelectedTemplate.Entries.OrderByDescending(x => x.MinPercent))
|
||||
AddGradingKeyRowInternal(e.Grade, e.MinPercent);
|
||||
RecomputeGradingKeyAbsolutes();
|
||||
GradingKeyValidation = "";
|
||||
}
|
||||
|
||||
private bool HasSelectedTemplate() => SelectedTemplate is not null;
|
||||
|
||||
partial void OnSelectedTemplateChanged(GradingKeyTemplate? value) => ApplyTemplateCommand.NotifyCanExecuteChanged();
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveAsTemplate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewTemplateName)) { GradingKeyValidation = "Vorlagenname erforderlich."; return; }
|
||||
|
||||
var entries = BuildGradingKeyEntries();
|
||||
var error = _grading.ValidateGradingKey(entries);
|
||||
if (error is not null) { GradingKeyValidation = error; return; }
|
||||
|
||||
var template = new GradingKeyTemplate
|
||||
{
|
||||
Name = NewTemplateName.Trim(),
|
||||
GradingSystem = _gradingSystem,
|
||||
Entries = entries,
|
||||
};
|
||||
_gradingKeyTemplates.Save(template);
|
||||
AvailableTemplates.Add(template);
|
||||
NewTemplateName = "";
|
||||
GradingKeyValidation = "";
|
||||
}
|
||||
|
||||
private List<GradingKeyEntry> BuildGradingKeyEntries() => GradingKeyEntries
|
||||
.Select(e => new GradingKeyEntry { Grade = e.Grade.Trim(), MinPercent = e.MinPercent })
|
||||
.OrderByDescending(e => e.MinPercent)
|
||||
.ToList();
|
||||
|
||||
[RelayCommand]
|
||||
private void Save()
|
||||
{
|
||||
@@ -180,6 +273,11 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
returnedAt = r;
|
||||
}
|
||||
|
||||
var gradingKey = BuildGradingKeyEntries();
|
||||
var gradingKeyError = _grading.ValidateGradingKey(gradingKey);
|
||||
if (gradingKeyError is not null) { GradingKeyValidation = gradingKeyError; return; }
|
||||
GradingKeyValidation = "";
|
||||
|
||||
Result = _editingExam ?? new Exam { GroupId = _groupId };
|
||||
Result.Title = Title.Trim();
|
||||
Result.Date = date;
|
||||
@@ -188,11 +286,28 @@ public partial class ExamDialogViewModel : ObservableObject
|
||||
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
||||
Result.ReturnedAt = returnedAt;
|
||||
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
|
||||
Result.GradingKey = _gradingKey;
|
||||
Result.GradingKey = gradingKey;
|
||||
_exams.Save(Result);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zeile im Notenschlüssel-Editor (1.3) ──────────────────────────────────────
|
||||
|
||||
public partial class GradingKeyEntryEditItem : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private string _grade = "";
|
||||
[ObservableProperty] private double _minPercent;
|
||||
[ObservableProperty] private string _absolutePointsDisplay = "";
|
||||
|
||||
public Action? OnChanged { get; set; }
|
||||
public Action<GradingKeyEntryEditItem>? OnRemove { get; set; }
|
||||
|
||||
partial void OnMinPercentChanged(double value) => OnChanged?.Invoke();
|
||||
partial void OnGradeChanged(string value) => OnChanged?.Invoke();
|
||||
|
||||
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||||
}
|
||||
|
||||
// ── Zeile im Aufgaben-Editor (1.2.1 / 1.2.2 / 1.2.4) ─────────────────────────
|
||||
|
||||
public partial class ExamTaskEditItem : ObservableObject
|
||||
|
||||
@@ -2,6 +2,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -14,6 +15,8 @@ public partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ICompetencyDomainRepository _domainRepo;
|
||||
private readonly IGradingKeyTemplateRepository _gradingKeyTemplates;
|
||||
private readonly GradingService _grading;
|
||||
|
||||
// ── Fächer ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -33,13 +36,65 @@ public partial class SettingsViewModel : ObservableObject
|
||||
|
||||
public ObservableCollection<DomainEditItem> Domains { get; } = [];
|
||||
|
||||
// ── Notenschlüssel-Vorlagen (1.3.2) ──────────────────────────────────────
|
||||
|
||||
[ObservableProperty] private string _newTemplateName = "";
|
||||
[ObservableProperty] private string _newTemplateGradingSystemName = "Noten 1–6";
|
||||
[ObservableProperty] private string _templateValidationMessage = "";
|
||||
|
||||
public List<string> GradingSystemOptions { get; } = ["Noten 1–6", "Punkte 0–15"];
|
||||
public ObservableCollection<GradingKeyTemplateEditItem> GradingKeyTemplateList { get; } = [];
|
||||
|
||||
// ── Konstruktor ───────────────────────────────────────────────────────────
|
||||
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo)
|
||||
public SettingsViewModel(ISubjectRepository subjects, ICompetencyDomainRepository domainRepo,
|
||||
IGradingKeyTemplateRepository gradingKeyTemplates, GradingService grading)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_domainRepo = domainRepo;
|
||||
_gradingKeyTemplates = gradingKeyTemplates;
|
||||
_grading = grading;
|
||||
LoadSubjects();
|
||||
LoadGradingKeyTemplates();
|
||||
}
|
||||
|
||||
// ── Notenschlüssel-Vorlagen: Laden / Hinzufügen / Löschen ────────────────
|
||||
|
||||
private void LoadGradingKeyTemplates()
|
||||
{
|
||||
GradingKeyTemplateList.Clear();
|
||||
foreach (var t in _gradingKeyTemplates.GetAll())
|
||||
GradingKeyTemplateList.Add(new GradingKeyTemplateEditItem(t, _gradingKeyTemplates, _grading));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddGradingKeyTemplate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewTemplateName)) { TemplateValidationMessage = "Vorlagenname erforderlich."; return; }
|
||||
|
||||
var system = NewTemplateGradingSystemName == "Punkte 0–15"
|
||||
? GradingSystem.Points0To15 : GradingSystem.Grades1To6;
|
||||
var defaults = system == GradingSystem.Grades1To6
|
||||
? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15();
|
||||
|
||||
var template = new GradingKeyTemplate
|
||||
{
|
||||
Name = NewTemplateName.Trim(),
|
||||
GradingSystem = system,
|
||||
Entries = defaults.Select(e => new GradingKeyEntry { Grade = e.Grade, MinPercent = e.MinPercent }).ToList(),
|
||||
};
|
||||
_gradingKeyTemplates.Save(template);
|
||||
GradingKeyTemplateList.Add(new GradingKeyTemplateEditItem(template, _gradingKeyTemplates, _grading));
|
||||
NewTemplateName = "";
|
||||
TemplateValidationMessage = "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteGradingKeyTemplate(GradingKeyTemplateEditItem? item)
|
||||
{
|
||||
if (item is null) return;
|
||||
_gradingKeyTemplates.Delete(item.Id);
|
||||
GradingKeyTemplateList.Remove(item);
|
||||
}
|
||||
|
||||
// ── Fächer: Laden / Hinzufügen / Löschen ─────────────────────────────────
|
||||
@@ -253,6 +308,88 @@ public class CompetencyItemVm
|
||||
}
|
||||
}
|
||||
|
||||
// ── GradingKeyTemplateEditItem (1.3.2) ─────────────────────────────────────────
|
||||
|
||||
public partial class GradingKeyTemplateEditItem : ObservableObject
|
||||
{
|
||||
private readonly GradingKeyTemplate _template;
|
||||
private readonly IGradingKeyTemplateRepository _repo;
|
||||
private readonly GradingService _grading;
|
||||
|
||||
public Guid Id { get; }
|
||||
public string Name { get; }
|
||||
public string GradingSystemLabel { get; }
|
||||
|
||||
[ObservableProperty] private string _newGrade = "";
|
||||
[ObservableProperty] private double _newMinPercent;
|
||||
[ObservableProperty] private string _validation = "";
|
||||
[ObservableProperty] private string _completenessWarning = "";
|
||||
|
||||
public ObservableCollection<GradingKeyEntryVm> Entries { get; } = [];
|
||||
|
||||
public GradingKeyTemplateEditItem(GradingKeyTemplate template, IGradingKeyTemplateRepository repo,
|
||||
GradingService grading)
|
||||
{
|
||||
_template = template; _repo = repo; _grading = grading;
|
||||
Id = template.Id;
|
||||
Name = template.Name;
|
||||
GradingSystemLabel = template.GradingSystem == GradingSystem.Grades1To6
|
||||
? "Noten 1–6" : "Punkte 0–15";
|
||||
|
||||
foreach (var e in template.Entries.OrderByDescending(e => e.MinPercent))
|
||||
Entries.Add(new GradingKeyEntryVm(e, DeleteEntry));
|
||||
RecomputeCompleteness();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void AddEntry()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(NewGrade)) { Validation = "Bezeichnung erforderlich."; return; }
|
||||
if (NewMinPercent is < 0 or > 100) { Validation = "Prozentgrenze muss zwischen 0 und 100 liegen."; return; }
|
||||
if (_template.Entries.Any(e => Math.Abs(e.MinPercent - NewMinPercent) < 0.0001))
|
||||
{ Validation = "Diese Prozentgrenze existiert bereits."; return; }
|
||||
|
||||
var entry = new GradingKeyEntry { Grade = NewGrade.Trim(), MinPercent = NewMinPercent };
|
||||
_template.Entries.Add(entry);
|
||||
_template.Entries = _template.Entries.OrderByDescending(e => e.MinPercent).ToList();
|
||||
_repo.Save(_template);
|
||||
|
||||
Entries.Clear();
|
||||
foreach (var e in _template.Entries) Entries.Add(new GradingKeyEntryVm(e, DeleteEntry));
|
||||
NewGrade = ""; NewMinPercent = 0; Validation = "";
|
||||
RecomputeCompleteness();
|
||||
}
|
||||
|
||||
private void DeleteEntry(GradingKeyEntryVm vm)
|
||||
{
|
||||
_template.Entries.RemoveAll(e => e.Grade == vm.Grade && Math.Abs(e.MinPercent - vm.MinPercent) < 0.0001);
|
||||
_repo.Save(_template);
|
||||
Entries.Remove(vm);
|
||||
RecomputeCompleteness();
|
||||
}
|
||||
|
||||
private void RecomputeCompleteness() =>
|
||||
CompletenessWarning = _grading.ValidateGradingKey(_template.Entries) ?? "";
|
||||
}
|
||||
|
||||
// ── GradingKeyEntryVm ─────────────────────────────────────────────────────────
|
||||
|
||||
public class GradingKeyEntryVm
|
||||
{
|
||||
public string Grade { get; }
|
||||
public double MinPercent { get; }
|
||||
public string Display { get; }
|
||||
public IRelayCommand DeleteCommand { get; }
|
||||
|
||||
public GradingKeyEntryVm(GradingKeyEntry e, Action<GradingKeyEntryVm> onDelete)
|
||||
{
|
||||
Grade = e.Grade;
|
||||
MinPercent = e.MinPercent;
|
||||
Display = $"{Grade} — ab {MinPercent.ToString("0.##")} %";
|
||||
DeleteCommand = new RelayCommand(() => onDelete(this));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hilfklassen ───────────────────────────────────────────────────────────────
|
||||
|
||||
public class SubjectListItem(Subject s)
|
||||
|
||||
Reference in New Issue
Block a user