CI / build-and-test (push) Canceled after 0s
- "Enter springt: Schüler für Schüler / Aufgabe für Aufgabe" im Dialog wählbar und gemerkt; Umschalt+Enter zurück, Pfeiltasten springen direkt und öffnen das Eingabefeld - Tasten per Tunnel-Handler abgefangen, bevor DataGrid/TextBox sie auswerten - Abwesende Schüler und Aufgaben der anderen Fassung werden übersprungen - Zellen lesen ihre Zeile aus dem DataContext (recycelte Zeilen schreiben nicht mehr falsch) - Bonusaufgaben zählen bei der angezeigten Nummerierung nicht mit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
500 lines
22 KiB
C#
500 lines
22 KiB
C#
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.Globalization;
|
||
|
||
namespace LehrerApp.Desktop.ViewModels.Groups;
|
||
|
||
// ── Dialog: Klausur anlegen / bearbeiten / duplizieren ───────────────────────
|
||
|
||
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;
|
||
private readonly Guid _groupId;
|
||
private readonly Guid? _subjectId;
|
||
private readonly int _gradeLevel;
|
||
private readonly Guid _competencyCatalogVersionId;
|
||
private readonly GradingSystem _gradingSystem;
|
||
private readonly Exam? _editingExam;
|
||
private readonly Exam? _duplicateSource;
|
||
private readonly string _subjectName;
|
||
|
||
[ObservableProperty] private string _title = "";
|
||
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
|
||
[ObservableProperty] private int? _examNumber;
|
||
[ObservableProperty] private string _notes = "";
|
||
[ObservableProperty] private string _returnedAtText = "";
|
||
[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;
|
||
[ObservableProperty] private GradingKeyTemplate? _selectedTemplate;
|
||
[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<Exam> AvailableOriginalExams { get; } = [];
|
||
|
||
public bool TotalPointsWarning => TotalPoints <= 0;
|
||
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<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";
|
||
public string SaveButtonText => _editingExam is null ? "Anlegen" : "Speichern";
|
||
|
||
/// Fach kommt von der Lerngruppe, nicht editierbar (jede Gruppe unterrichtet ein Fach).
|
||
public string SubjectDisplay => string.IsNullOrWhiteSpace(_subjectName)
|
||
? "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, bool isDifferentiated, Exam? editingExam, Exam? duplicateSource,
|
||
Guid competencyCatalogVersionId = default, IExamResultRepository? results = null)
|
||
{
|
||
_exams = exams; _competencyDomains = competencyDomains;
|
||
_results = results;
|
||
_gradingKeyTemplates = gradingKeyTemplates; _grading = grading;
|
||
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
|
||
_competencyCatalogVersionId = competencyCatalogVersionId;
|
||
_gradingSystem = gradingSystem;
|
||
_editingExam = editingExam;
|
||
_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;
|
||
|
||
foreach (var t in _gradingKeyTemplates.GetByGradingSystem(gradingSystem))
|
||
AvailableTemplates.Add(t);
|
||
|
||
var source = editingExam ?? duplicateSource;
|
||
if (source is not null)
|
||
{
|
||
var isDuplicate = duplicateSource is not null;
|
||
Title = isDuplicate ? $"{source.Title} (Kopie)" : source.Title;
|
||
DateText = isDuplicate
|
||
? DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy")
|
||
: source.Date.ToString("dd.MM.yyyy");
|
||
ExamNumber = source.ExamNumber;
|
||
Notes = source.Notes ?? "";
|
||
ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
|
||
// 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], 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);
|
||
}
|
||
else
|
||
{
|
||
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();
|
||
UpdateWeekLoadHint();
|
||
}
|
||
|
||
partial void OnUseWeightingChanged(bool value)
|
||
{
|
||
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();
|
||
|
||
/// 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()
|
||
{
|
||
AddTaskInternal(null, 0, 1.0, []);
|
||
Tasks[^1].ShowWeight = UseWeighting;
|
||
RecomputeTotals();
|
||
}
|
||
|
||
private void AddTaskInternal(string? title, double maxPoints, double weight, List<CompetencyReference> 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,
|
||
OnMoveDown = MoveTaskDown,
|
||
};
|
||
Tasks.Add(item);
|
||
RenumberTasks();
|
||
}
|
||
|
||
private List<CompetencyTagGroup> BuildCompetencyTagGroups(List<CompetencyReference> selected)
|
||
{
|
||
if (!_subjectId.HasValue) return [];
|
||
var catalog = _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel, _competencyCatalogVersionId);
|
||
// onChanged wird hier bewusst nicht gesetzt (bleibt null) - ExamTaskEditItem verdrahtet
|
||
// tag.OnChanged selbst auf seinen eigenen OnCompetencyToggled-Handler, siehe Konstruktor.
|
||
return CompetencyTagGroupBuilder.Build(catalog, selected, _ => { });
|
||
}
|
||
|
||
private void RemoveTask(ExamTaskEditItem item)
|
||
{
|
||
Tasks.Remove(item);
|
||
RenumberTasks();
|
||
RecomputeTotals();
|
||
}
|
||
|
||
private void MoveTaskUp(ExamTaskEditItem item)
|
||
{
|
||
var idx = Tasks.IndexOf(item);
|
||
if (idx <= 0) return;
|
||
Tasks.Move(idx, idx - 1);
|
||
RenumberTasks();
|
||
}
|
||
|
||
private void MoveTaskDown(ExamTaskEditItem item)
|
||
{
|
||
var idx = Tasks.IndexOf(item);
|
||
if (idx < 0 || idx >= Tasks.Count - 1) return;
|
||
Tasks.Move(idx, idx + 1);
|
||
RenumberTasks();
|
||
}
|
||
|
||
private void RenumberTasks()
|
||
{
|
||
for (var i = 0; i < Tasks.Count; i++) Tasks[i].Nr = i + 1;
|
||
var numbers = ExamScoringService.TaskNumbers(Tasks.Select(t => t.IsBonus));
|
||
for (var i = 0; i < Tasks.Count; i++) Tasks[i].DisplayNr = Tasks[i].IsBonus ? "★" : numbers[i];
|
||
}
|
||
|
||
private void RecomputeTotals()
|
||
{
|
||
RenumberTasks();
|
||
TotalPoints = Tasks.Where(t => !t.IsBonus && t.VariantName != "B").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
|
||
? "–"
|
||
: 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";
|
||
}
|
||
|
||
[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()
|
||
{
|
||
TitleError = ""; DateTextError = ""; ReturnedAtTextError = ""; GradingKeyValidation = "";
|
||
RepeatError = ""; TaskPointsError = "";
|
||
var valid = true;
|
||
|
||
if (string.IsNullOrWhiteSpace(Title)) { TitleError = "Titel erforderlich."; valid = false; }
|
||
|
||
DateOnly date = default;
|
||
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out date))
|
||
{
|
||
DateTextError = "Format TT.MM.JJJJ.";
|
||
valid = false;
|
||
}
|
||
|
||
DateOnly? returnedAt = null;
|
||
if (!string.IsNullOrWhiteSpace(ReturnedAtText))
|
||
{
|
||
if (!DateOnly.TryParseExact(ReturnedAtText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
|
||
{
|
||
ReturnedAtTextError = "Format TT.MM.JJJJ.";
|
||
valid = false;
|
||
}
|
||
else returnedAt = r;
|
||
}
|
||
|
||
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;
|
||
|
||
Result = _editingExam ?? new Exam { GroupId = _groupId };
|
||
Result.Title = Title.Trim();
|
||
Result.Date = date;
|
||
Result.ExamNumber = ExamNumber;
|
||
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
|
||
Result.ReturnedAt = returnedAt;
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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
|
||
{
|
||
[ObservableProperty] private int _nr;
|
||
/// Angezeigte Nummer — Bonusaufgaben zählen nicht mit (siehe ExamScoringService.TaskNumbers).
|
||
[ObservableProperty] private string _displayNr = "";
|
||
[ObservableProperty] private string _title = "";
|
||
[ObservableProperty] private double _maxPoints;
|
||
[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<CompetencyReference> Competencies { get; }
|
||
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
|
||
|
||
public string CompetencySummary => Competencies.Count == 0
|
||
? "Keine Kompetenzen"
|
||
: $"{Competencies.Count} Kompetenz(en)";
|
||
|
||
public Action? OnChanged { get; set; }
|
||
public Action<ExamTaskEditItem>? OnRemove { get; set; }
|
||
public Action<ExamTaskEditItem>? OnMoveUp { get; set; }
|
||
public Action<ExamTaskEditItem>? OnMoveDown { get; set; }
|
||
|
||
public ExamTaskEditItem(string? title, double maxPoints, double weight,
|
||
List<CompetencyReference> competencies, List<CompetencyTagGroup> tagGroups)
|
||
{
|
||
_title = title ?? "";
|
||
_maxPoints = maxPoints;
|
||
_weight = weight;
|
||
Competencies = competencies;
|
||
foreach (var g in tagGroups)
|
||
{
|
||
foreach (var tag in g.Items) tag.OnChanged = OnCompetencyToggled;
|
||
CompetencyTagGroups.Add(g);
|
||
}
|
||
}
|
||
|
||
private void OnCompetencyToggled(CompetencyTag tag)
|
||
{
|
||
if (tag.IsSelected)
|
||
{
|
||
if (!Competencies.Any(r => r.ItemId == tag.ItemId))
|
||
Competencies.Add(new CompetencyReference { ItemId = tag.ItemId, Code = tag.Code, Description = tag.Description });
|
||
}
|
||
else
|
||
{
|
||
Competencies.RemoveAll(r => r.ItemId == tag.ItemId);
|
||
}
|
||
OnPropertyChanged(nameof(CompetencySummary));
|
||
OnChanged?.Invoke();
|
||
}
|
||
|
||
[RelayCommand] private void ToggleCompetencyPanel() => IsCompetencyPanelOpen = !IsCompetencyPanelOpen;
|
||
[RelayCommand] private void Remove() => OnRemove?.Invoke(this);
|
||
[RelayCommand] private void MoveUp() => OnMoveUp?.Invoke(this);
|
||
[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()
|
||
{
|
||
Nr = Nr,
|
||
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,
|
||
};
|
||
}
|