Klausurverwaltung: Anlegen, Bearbeiten, Duplizieren, Status, Aufgaben
Implementiert TODO 1.1 (Klausur anlegen/verwalten) und die dazu passenden Teile von 1.2 (Aufgabenstruktur), da beides im selben Dialog verwaltet wird: - ExamDialog: Klausur anlegen/bearbeiten/duplizieren mit Aufgaben-Editor (Nr./Titel/Maximalpunkte/Gewichtung, sortierbar), Gesamtpunkte-Anzeige mit Warnung bei 0 Punkten, und Kompetenz-Zuordnung je Aufgabe. - Klausuren-Tab: Kontextmenü und Toolbar für Bearbeiten/Duplizieren/ Löschen (mit Rückfrage), farbcodierte Status-Spalte, SplitButton für Statuswechsel (Klick = nächster Status, Dropdown = manuelles Setzen). - ExamRepository.Delete löscht zugehörige ExamResults kaskadierend. - Gewichtung ist standardmäßig ausgeblendet (Checkbox zum Einblenden), NumericUpDown-Felder ohne Spinner-Buttons wegen eines Layout-Bugs im Avalonia-Standardtemplate, der das interne Textfeld auf wenige Pixel schrumpfen ließ. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,12 +165,17 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
[ObservableProperty] private int _studentCount;
|
||||
[ObservableProperty] private int _activeTabIndex = 0;
|
||||
[ObservableProperty] private StudentSummary? _selectedStudent;
|
||||
[ObservableProperty] private ExamSummary? _selectedExam;
|
||||
|
||||
public ObservableCollection<StudentSummary> Students { get; } = [];
|
||||
public ObservableCollection<ExamSummary> Exams { get; } = [];
|
||||
|
||||
public ParticipationTabViewModel ParticipationTab { get; }
|
||||
public Func<Task<bool>>? OnAddStudent { get; set; }
|
||||
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
|
||||
public Func<Exam, Task<bool>>? OnEditExam { get; set; }
|
||||
public Func<Exam, Task<bool>>? OnDuplicateExam { get; set; }
|
||||
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
|
||||
|
||||
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
|
||||
IEnrollmentRepository enrollments, IExamRepository exams, IGradeRepository grades,
|
||||
@@ -190,9 +195,17 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
$"{(Group.Type == GroupType.Class ? "Klasse" : "Kurs")} · " +
|
||||
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "1–6" : "0–15")}";
|
||||
LoadStudents();
|
||||
ReloadExams();
|
||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||
}
|
||||
|
||||
private void ReloadExams()
|
||||
{
|
||||
if (Group is null) return;
|
||||
var selectedId = SelectedExam?.Id;
|
||||
Exams.Clear();
|
||||
foreach (var e in _exams.GetByGroup(Group.Id)) Exams.Add(new ExamSummary(e));
|
||||
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
|
||||
SelectedExam = Exams.FirstOrDefault(e => e.Id == selectedId);
|
||||
}
|
||||
|
||||
public void LoadStudents()
|
||||
@@ -240,7 +253,87 @@ public partial class GroupDetailViewModel : ObservableObject
|
||||
|
||||
private bool HasSelectedStudent() => SelectedStudent is not null;
|
||||
|
||||
[RelayCommand] private void AddExam() { /* TODO */ }
|
||||
[RelayCommand]
|
||||
private async Task AddExam()
|
||||
{
|
||||
if (Group is null || OnAddExam is null) return;
|
||||
var saved = await OnAddExam(Group.Id);
|
||||
if (saved) ReloadExams();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||
private async Task EditExam()
|
||||
{
|
||||
if (SelectedExam is null || OnEditExam is null) return;
|
||||
var exam = _exams.GetById(SelectedExam.Id);
|
||||
if (exam is null) return;
|
||||
var id = exam.Id;
|
||||
var saved = await OnEditExam(exam);
|
||||
if (saved)
|
||||
{
|
||||
ReloadExams();
|
||||
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||
private async Task DuplicateExam()
|
||||
{
|
||||
if (SelectedExam is null || OnDuplicateExam is null) return;
|
||||
var exam = _exams.GetById(SelectedExam.Id);
|
||||
if (exam is null) return;
|
||||
var saved = await OnDuplicateExam(exam);
|
||||
if (saved) ReloadExams();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||
private async Task DeleteExam()
|
||||
{
|
||||
if (SelectedExam is null || OnConfirmDeleteExam is null) return;
|
||||
var selected = SelectedExam;
|
||||
if (!await OnConfirmDeleteExam(selected)) return;
|
||||
_exams.Delete(selected.Id);
|
||||
SelectedExam = null;
|
||||
ReloadExams();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||
private void AdvanceExamStatus()
|
||||
{
|
||||
if (SelectedExam is null) return;
|
||||
SetExamStatus(SelectedExam.Status switch
|
||||
{
|
||||
ExamStatus.Planned => ExamStatus.Conducted,
|
||||
ExamStatus.Conducted => ExamStatus.Graded,
|
||||
ExamStatus.Graded => ExamStatus.Returned,
|
||||
_ => SelectedExam.Status,
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
|
||||
private void SetExamStatus(ExamStatus status)
|
||||
{
|
||||
if (SelectedExam is null) return;
|
||||
var exam = _exams.GetById(SelectedExam.Id);
|
||||
if (exam is null) return;
|
||||
exam.Status = status;
|
||||
_exams.Save(exam);
|
||||
var id = exam.Id;
|
||||
ReloadExams();
|
||||
SelectedExam = Exams.FirstOrDefault(e => e.Id == id);
|
||||
}
|
||||
|
||||
partial void OnSelectedExamChanged(ExamSummary? value)
|
||||
{
|
||||
EditExamCommand.NotifyCanExecuteChanged();
|
||||
DuplicateExamCommand.NotifyCanExecuteChanged();
|
||||
DeleteExamCommand.NotifyCanExecuteChanged();
|
||||
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
|
||||
SetExamStatusCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private bool HasSelectedExam() => SelectedExam is not null;
|
||||
|
||||
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
|
||||
}
|
||||
|
||||
@@ -277,10 +370,13 @@ public class ExamSummary
|
||||
public Guid Id { get; }
|
||||
public string Title { get; }
|
||||
public string Date { get; }
|
||||
public ExamStatus Status { get; }
|
||||
public string StatusLabel { get; }
|
||||
public string StatusColorHex { get; }
|
||||
|
||||
public ExamSummary(Core.Models.Exam e)
|
||||
{
|
||||
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy");
|
||||
Id = e.Id; Title = e.Title; Date = e.Date.ToString("dd.MM.yyyy"); Status = e.Status;
|
||||
StatusLabel = e.Status switch
|
||||
{
|
||||
ExamStatus.Planned => "Geplant",
|
||||
@@ -289,6 +385,14 @@ public class ExamSummary
|
||||
ExamStatus.Returned => "Zurückgegeben",
|
||||
_ => "",
|
||||
};
|
||||
StatusColorHex = e.Status switch
|
||||
{
|
||||
ExamStatus.Planned => "#9E9E9E",
|
||||
ExamStatus.Conducted => "#FB8C00",
|
||||
ExamStatus.Graded => "#43A047",
|
||||
ExamStatus.Returned => "#1E88E5",
|
||||
_ => "#9E9E9E",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user