Files
LehrerApp/LehrerApp.Desktop/ViewModels/Groups/ExamViewModels.cs
T
adminandClaude Sonnet 5 8cbd4475d7 Klausur: Fach aus Lerngruppe übernehmen, Rückgabedatum erfassen
- Fach kommt jetzt aus der Lerngruppe statt manuell pro Klausur
  eingegeben zu werden: Fach-Feld im Anlegen-Dialog der Lerngruppe
  ist nicht mehr auf Typ "Kurs" beschränkt, da eine Gruppe genau ein
  Fach unterrichtet (bei zwei Fächern legt man zwei Gruppen an). Im
  Klausur-Dialog ist das Fach dadurch nur noch read-only Kontext statt
  eines redundanten Eingabefelds.
- Neues Feld Exam.ReturnedAt: wird beim Statuswechsel auf "Zurück-
  gegeben" automatisch mit dem heutigen Datum belegt (und beim
  Zurücknehmen des Status wieder geleert), ist im Klausur-Dialog aber
  auch manuell nachträglich korrigierbar. Sichtbar als eigene Spalte
  im Klausuren-Tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-11 23:16:52 +02:00

257 lines
9.9 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 ICompetencyDomainRepository _competencyDomains;
private readonly Guid _groupId;
private readonly Guid? _subjectId;
private readonly int _gradeLevel;
private readonly Exam? _editingExam;
private readonly List<GradingKeyEntry> _gradingKey;
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 _validationMessage = "";
[ObservableProperty] private double _totalPoints;
[ObservableProperty] private bool _hasCompetencyCatalog;
[ObservableProperty] private bool _useWeighting;
public bool TotalPointsWarning => TotalPoints <= 0;
public string TotalPointsDisplay =>
$"{TotalPoints.ToString("0.##", CultureInfo.InvariantCulture)} Punkte gesamt";
public ObservableCollection<ExamTaskEditItem> Tasks { 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,
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem,
string defaultSubjectName, Exam? editingExam, Exam? duplicateSource)
{
_exams = exams; _competencyDomains = competencyDomains;
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
_editingExam = editingExam;
_subjectName = defaultSubjectName;
HasCompetencyCatalog = subjectId.HasValue
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0;
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") ?? "";
_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);
}
else
{
_gradingKey = gradingSystem == GradingSystem.Grades1To6
? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15();
}
foreach (var t in Tasks) t.ShowWeight = UseWeighting;
RecomputeTotals();
}
partial void OnUseWeightingChanged(bool value)
{
foreach (var t in Tasks) t.ShowWeight = value;
}
[RelayCommand]
private void AddTask()
{
AddTaskInternal(null, 0, 1.0, []);
Tasks[^1].ShowWeight = UseWeighting;
RecomputeTotals();
}
private void AddTaskInternal(string? title, double maxPoints, double weight, List<string> competencyCodes)
{
var item = new ExamTaskEditItem(title, maxPoints, weight, competencyCodes,
BuildCompetencyTagGroups(competencyCodes))
{
OnChanged = RecomputeTotals,
OnRemove = RemoveTask,
OnMoveUp = MoveTaskUp,
OnMoveDown = MoveTaskDown,
};
Tasks.Add(item);
RenumberTasks();
}
private List<CompetencyTagGroup> BuildCompetencyTagGroups(List<string> selectedCodes)
{
var groups = new List<CompetencyTagGroup>();
if (!_subjectId.HasValue) return groups;
var selected = selectedCodes.ToHashSet();
foreach (var domain in _competencyDomains.GetBySubjectAndGrade(_subjectId.Value, _gradeLevel))
{
var group = new CompetencyTagGroup(domain.Name, domain.Code);
foreach (var item in domain.Items.OrderBy(i => i.SortOrder))
group.Items.Add(new CompetencyTag(item.Code, item.Description, selected.Contains(item.Code)));
if (group.Items.Count > 0) groups.Add(group);
}
return groups;
}
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;
}
private void RecomputeTotals()
{
TotalPoints = Tasks.Sum(t => t.MaxPoints);
OnPropertyChanged(nameof(TotalPointsWarning));
OnPropertyChanged(nameof(TotalPointsDisplay));
}
[RelayCommand]
private void Save()
{
if (string.IsNullOrWhiteSpace(Title)) { ValidationMessage = "Titel erforderlich."; return; }
if (!DateOnly.TryParseExact(DateText, "dd.MM.yyyy", null, DateTimeStyles.None, out var date))
{
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
return;
}
DateOnly? returnedAt = null;
if (!string.IsNullOrWhiteSpace(ReturnedAtText))
{
if (!DateOnly.TryParseExact(ReturnedAtText, "dd.MM.yyyy", null, DateTimeStyles.None, out var r))
{
ValidationMessage = "Rückgabedatum im Format TT.MM.JJJJ eingeben.";
return;
}
returnedAt = r;
}
Result = _editingExam ?? new Exam { GroupId = _groupId };
Result.Title = Title.Trim();
Result.Date = date;
Result.Subject = _subjectName.Trim();
Result.ExamNumber = ExamNumber;
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
Result.ReturnedAt = returnedAt;
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
Result.GradingKey = _gradingKey;
_exams.Save(Result);
}
}
// ── Zeile im Aufgaben-Editor (1.2.1 / 1.2.2 / 1.2.4) ─────────────────────────
public partial class ExamTaskEditItem : ObservableObject
{
[ObservableProperty] private int _nr;
[ObservableProperty] private string _title = "";
[ObservableProperty] private double _maxPoints;
[ObservableProperty] private double _weight = 1.0;
[ObservableProperty] private bool _isCompetencyPanelOpen;
[ObservableProperty] private bool _showWeight;
public List<string> CompetencyCodes { get; }
public ObservableCollection<CompetencyTagGroup> CompetencyTagGroups { get; } = [];
public string CompetencySummary => CompetencyCodes.Count == 0
? "Keine Kompetenzen"
: $"{CompetencyCodes.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<string> competencyCodes, List<CompetencyTagGroup> tagGroups)
{
_title = title ?? "";
_maxPoints = maxPoints;
_weight = weight;
CompetencyCodes = competencyCodes;
foreach (var g in tagGroups)
{
foreach (var tag in g.Items) tag.OnChanged = OnCompetencyToggled;
CompetencyTagGroups.Add(g);
}
}
private void OnCompetencyToggled(string code, bool selected)
{
if (selected) { if (!CompetencyCodes.Contains(code)) CompetencyCodes.Add(code); }
else CompetencyCodes.Remove(code);
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();
public ExamTask ToModel() => new()
{
Nr = Nr,
Title = string.IsNullOrWhiteSpace(Title) ? null : Title.Trim(),
MaxPoints = MaxPoints,
Weight = Weight,
CompetencyCodes = CompetencyCodes,
};
}