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>
This commit is contained in:
2026-08-11 23:16:52 +02:00
co-authored by Claude Sonnet 5
parent 21378bbebd
commit 8cbd4475d7
6 changed files with 40 additions and 12 deletions
+1
View File
@@ -11,6 +11,7 @@ public class Exam
public List<ExamTask> Tasks { get; set; } = []; public List<ExamTask> Tasks { get; set; } = [];
public List<GradingKeyEntry> GradingKey { get; set; } = []; public List<GradingKeyEntry> GradingKey { get; set; } = [];
public ExamStatus Status { get; set; } = ExamStatus.Planned; public ExamStatus Status { get; set; } = ExamStatus.Planned;
public DateOnly? ReturnedAt { get; set; }
public string? Notes { get; set; } public string? Notes { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
@@ -19,12 +19,13 @@ public partial class ExamDialogViewModel : ObservableObject
private readonly int _gradeLevel; private readonly int _gradeLevel;
private readonly Exam? _editingExam; private readonly Exam? _editingExam;
private readonly List<GradingKeyEntry> _gradingKey; private readonly List<GradingKeyEntry> _gradingKey;
private readonly string _subjectName;
[ObservableProperty] private string _title = ""; [ObservableProperty] private string _title = "";
[ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy"); [ObservableProperty] private string _dateText = DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy");
[ObservableProperty] private string _subject = "";
[ObservableProperty] private int? _examNumber; [ObservableProperty] private int? _examNumber;
[ObservableProperty] private string _notes = ""; [ObservableProperty] private string _notes = "";
[ObservableProperty] private string _returnedAtText = "";
[ObservableProperty] private string _validationMessage = ""; [ObservableProperty] private string _validationMessage = "";
[ObservableProperty] private double _totalPoints; [ObservableProperty] private double _totalPoints;
[ObservableProperty] private bool _hasCompetencyCatalog; [ObservableProperty] private bool _hasCompetencyCatalog;
@@ -40,6 +41,10 @@ public partial class ExamDialogViewModel : ObservableObject
public string DialogTitle => _editingExam is null ? "Neue Klausur anlegen" : "Klausur bearbeiten"; public string DialogTitle => _editingExam is null ? "Neue Klausur anlegen" : "Klausur bearbeiten";
public string SaveButtonText => _editingExam is null ? "Anlegen" : "Speichern"; 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, public ExamDialogViewModel(IExamRepository exams, ICompetencyDomainRepository competencyDomains,
Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem, Guid groupId, Guid? subjectId, int gradeLevel, GradingSystem gradingSystem,
string defaultSubjectName, Exam? editingExam, Exam? duplicateSource) string defaultSubjectName, Exam? editingExam, Exam? duplicateSource)
@@ -47,6 +52,7 @@ public partial class ExamDialogViewModel : ObservableObject
_exams = exams; _competencyDomains = competencyDomains; _exams = exams; _competencyDomains = competencyDomains;
_groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel; _groupId = groupId; _subjectId = subjectId; _gradeLevel = gradeLevel;
_editingExam = editingExam; _editingExam = editingExam;
_subjectName = defaultSubjectName;
HasCompetencyCatalog = subjectId.HasValue HasCompetencyCatalog = subjectId.HasValue
&& _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0; && _competencyDomains.GetBySubjectAndGrade(subjectId.Value, gradeLevel).Count > 0;
@@ -59,9 +65,9 @@ public partial class ExamDialogViewModel : ObservableObject
DateText = isDuplicate DateText = isDuplicate
? DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy") ? DateOnly.FromDateTime(DateTime.Today).ToString("dd.MM.yyyy")
: source.Date.ToString("dd.MM.yyyy"); : source.Date.ToString("dd.MM.yyyy");
Subject = source.Subject;
ExamNumber = source.ExamNumber; ExamNumber = source.ExamNumber;
Notes = source.Notes ?? ""; Notes = source.Notes ?? "";
ReturnedAtText = isDuplicate ? "" : source.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
_gradingKey = source.GradingKey _gradingKey = source.GradingKey
.Select(k => new GradingKeyEntry { Grade = k.Grade, MinPercent = k.MinPercent }).ToList(); .Select(k => new GradingKeyEntry { Grade = k.Grade, MinPercent = k.MinPercent }).ToList();
foreach (var t in source.Tasks.OrderBy(t => t.Nr)) foreach (var t in source.Tasks.OrderBy(t => t.Nr))
@@ -70,7 +76,6 @@ public partial class ExamDialogViewModel : ObservableObject
} }
else else
{ {
Subject = defaultSubjectName;
_gradingKey = gradingSystem == GradingSystem.Grades1To6 _gradingKey = gradingSystem == GradingSystem.Grades1To6
? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15(); ? GradingService.DefaultKey1To6() : GradingService.DefaultKey0To15();
} }
@@ -164,13 +169,24 @@ public partial class ExamDialogViewModel : ObservableObject
ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben."; ValidationMessage = "Datum im Format TT.MM.JJJJ eingeben.";
return; 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 = _editingExam ?? new Exam { GroupId = _groupId };
Result.Title = Title.Trim(); Result.Title = Title.Trim();
Result.Date = date; Result.Date = date;
Result.Subject = Subject.Trim(); Result.Subject = _subjectName.Trim();
Result.ExamNumber = ExamNumber; Result.ExamNumber = ExamNumber;
Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim(); Result.Notes = string.IsNullOrWhiteSpace(Notes) ? null : Notes.Trim();
Result.ReturnedAt = returnedAt;
Result.Tasks = Tasks.Select(t => t.ToModel()).ToList(); Result.Tasks = Tasks.Select(t => t.ToModel()).ToList();
Result.GradingKey = _gradingKey; Result.GradingKey = _gradingKey;
_exams.Save(Result); _exams.Save(Result);
@@ -317,6 +317,10 @@ public partial class GroupDetailViewModel : ObservableObject
var exam = _exams.GetById(SelectedExam.Id); var exam = _exams.GetById(SelectedExam.Id);
if (exam is null) return; if (exam is null) return;
exam.Status = status; exam.Status = status;
if (status == ExamStatus.Returned)
exam.ReturnedAt ??= DateOnly.FromDateTime(DateTime.Today);
else
exam.ReturnedAt = null;
_exams.Save(exam); _exams.Save(exam);
var id = exam.Id; var id = exam.Id;
ReloadExams(); ReloadExams();
@@ -373,10 +377,12 @@ public class ExamSummary
public ExamStatus Status { get; } public ExamStatus Status { get; }
public string StatusLabel { get; } public string StatusLabel { get; }
public string StatusColorHex { get; } public string StatusColorHex { get; }
public string ReturnedAtDisplay { get; }
public ExamSummary(Core.Models.Exam e) public ExamSummary(Core.Models.Exam e)
{ {
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status; Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
ReturnedAtDisplay = e.ReturnedAt?.ToString("dd.MM.yyyy") ?? "";
StatusLabel = e.Status switch StatusLabel = e.Status switch
{ {
ExamStatus.Planned => "Geplant", ExamStatus.Planned => "Geplant",
@@ -569,7 +575,7 @@ public partial class AddGroupDialogViewModel : ObservableObject
if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; } if (string.IsNullOrWhiteSpace(Name)) { ValidationMessage = "Bezeichnung erforderlich."; return; }
if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 113."; return; } if (GradeLevel is < 1 or > 13) { ValidationMessage = "Klassenstufe 113."; return; }
string? subjectName = IsKurs && !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null; string? subjectName = !string.IsNullOrWhiteSpace(Subject) ? Subject.Trim() : null;
Guid? subjectId = null; Guid? subjectId = null;
if (subjectName is not null) if (subjectName is not null)
@@ -25,8 +25,9 @@
<TextBox Text="{Binding Name}" PlaceholderText="{Binding NameHint}"/> <TextBox Text="{Binding Name}" PlaceholderText="{Binding NameHint}"/>
</StackPanel> </StackPanel>
<!-- Fach nur bei Kurs, AutoComplete aus bekannten Fächern --> <!-- Fach AutoComplete aus bekannten Fächern; auch bei Klasse, da eine
<StackPanel Spacing="4" IsVisible="{Binding IsKurs}"> Lerngruppe genau ein Fach unterrichtet (bei zwei Fächern: zwei Gruppen anlegen) -->
<StackPanel Spacing="4">
<TextBlock Text="Fach (optional)" FontSize="12" Opacity="0.7"/> <TextBlock Text="Fach (optional)" FontSize="12" Opacity="0.7"/>
<AutoCompleteBox Text="{Binding Subject}" <AutoCompleteBox Text="{Binding Subject}"
ItemsSource="{Binding KnownSubjectNames}" ItemsSource="{Binding KnownSubjectNames}"
@@ -10,7 +10,10 @@
<Grid RowDefinitions="*,Auto" Margin="24"> <Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0"> <ScrollViewer Grid.Row="0">
<StackPanel Spacing="14" Margin="0,0,12,0"> <StackPanel Spacing="14" Margin="0,0,12,0">
<TextBlock Text="{Binding DialogTitle}" FontSize="18" FontWeight="SemiBold"/> <StackPanel Spacing="2">
<TextBlock Text="{Binding DialogTitle}" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="{Binding SubjectDisplay}" FontSize="12" Opacity="0.6"/>
</StackPanel>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/> <TextBlock Text="Titel *" FontSize="12" Opacity="0.7"/>
@@ -23,14 +26,14 @@
<TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/> <TextBox Text="{Binding DateText}" PlaceholderText="TT.MM.JJJJ"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Spacing="4"> <StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Fach" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Subject}"/>
</StackPanel>
<StackPanel Grid.Column="4" Spacing="4">
<TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/> <TextBlock Text="Klausurnummer" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding ExamNumber}" Minimum="1" Maximum="20" FormatString="0" <NumericUpDown Value="{Binding ExamNumber}" Minimum="1" Maximum="20" FormatString="0"
ShowButtonSpinner="False"/> ShowButtonSpinner="False"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="4" Spacing="4">
<TextBlock Text="Rückgabedatum" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding ReturnedAtText}" PlaceholderText="TT.MM.JJJJ"/>
</StackPanel>
</Grid> </Grid>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
@@ -107,6 +107,7 @@
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTextColumn Header="Rückgabe" Binding="{Binding ReturnedAtDisplay}" Width="100"/>
</DataGrid.Columns> </DataGrid.Columns>
<DataGrid.ContextMenu> <DataGrid.ContextMenu>
<ContextMenu> <ContextMenu>