Kapitel 7 abgeschlossen.

This commit is contained in:
2026-08-16 00:29:01 +02:00
parent da8d2bb1da
commit f9e398b225
47 changed files with 2636 additions and 107 deletions
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using LehrerApp.Desktop.ViewModels.Students;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
@@ -17,6 +18,7 @@ public partial class GroupListViewModel : ObservableObject
public Action<Guid, int>? OnNavigateToDetail { get; set; }
public Func<Task>? OnAddGroup { get; set; }
public Func<Guid, Task>? OnEditGroup { get; set; }
public Func<Guid, Task<Guid?>>? OnRollOverGroup { get; set; }
public Func<GroupListItem, Task<bool>>? OnConfirmDelete { get; set; }
[ObservableProperty] private string _selectedSchoolYear = "";
@@ -53,6 +55,7 @@ public partial class GroupListViewModel : ObservableObject
OnPropertyChanged(nameof(SelectedGroupSubtitle));
NavigateToSectionCommand.NotifyCanExecuteChanged();
EditGroupCommand.NotifyCanExecuteChanged();
RollOverGroupCommand.NotifyCanExecuteChanged();
ToggleArchiveCommand.NotifyCanExecuteChanged();
DeleteGroupCommand.NotifyCanExecuteChanged();
}
@@ -85,7 +88,7 @@ public partial class GroupListViewModel : ObservableObject
}
[RelayCommand] private void Refresh() => LoadGroups();
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
private async Task EditGroup()
{
if (SelectedGroup is null || OnEditGroup is null) return;
@@ -95,6 +98,20 @@ public partial class GroupListViewModel : ObservableObject
SelectedGroup = Groups.FirstOrDefault(g => g.Id == id);
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private async Task RollOverGroup()
{
if (SelectedGroup is null || OnRollOverGroup is null) return;
var targetId = await OnRollOverGroup(SelectedGroup.Id);
if (targetId is null) return;
var target = _groups.GetById(targetId.Value);
if (target is null) return;
if (!SchoolYears.Contains(target.SchoolYear)) SchoolYears.Insert(0, target.SchoolYear);
SelectedSchoolYear = target.SchoolYear;
LoadGroups();
SelectedGroup = Groups.FirstOrDefault(g => g.Id == target.Id);
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
private void ToggleArchive()
{
@@ -106,7 +123,7 @@ public partial class GroupListViewModel : ObservableObject
LoadGroups();
}
[RelayCommand(CanExecute = nameof(HasSelectedGroup))]
[RelayCommand(CanExecute = nameof(CanEditSelectedGroup))]
private async Task DeleteGroup()
{
if (SelectedGroup is null || OnConfirmDelete is null) return;
@@ -124,6 +141,7 @@ public partial class GroupListViewModel : ObservableObject
}
private bool HasSelectedGroup() => SelectedGroup is not null;
private bool CanEditSelectedGroup() => SelectedGroup?.IsActive == true;
}
public class GroupListItem
@@ -175,9 +193,17 @@ public partial class GroupDetailViewModel : ObservableObject
// bevor LoadGroup() läuft (siehe MainWindowViewModel.NavigateToGroupDetail), Group ist dann
// kurzzeitig null — ein verschachtelter Pfad würde dafür jedes Mal einen Binding-Fehler loggen.
public bool IsDifferentiated => Group?.IsDifferentiated ?? false;
public bool IsReadOnly => Group is { IsActive: false };
public bool IsEditable => Group is { IsActive: true };
public string SubjectName { get; private set; } = "";
partial void OnGroupChanged(LearningGroup? value) => OnPropertyChanged(nameof(IsDifferentiated));
partial void OnGroupChanged(LearningGroup? value)
{
OnPropertyChanged(nameof(IsDifferentiated));
OnPropertyChanged(nameof(IsReadOnly));
OnPropertyChanged(nameof(IsEditable));
NotifyWriteCommands();
}
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
public ObservableCollection<StudentSummary> Students { get; } = [];
@@ -194,6 +220,7 @@ public partial class GroupDetailViewModel : ObservableObject
public Func<ExamSummary, Task<bool>>? OnConfirmDeleteExam { get; set; }
public Func<Exam, Task>? OnGradeExam { get; set; }
public Func<Exam, Task>? OnEvaluateExam { get; set; }
public Func<Task<bool>>? OnConfirmReactivate { get; set; }
public GroupDetailViewModel(IGroupRepository groups, IStudentRepository students,
IGroupMembershipRepository memberships, ISubjectRepository subjects,
@@ -221,9 +248,9 @@ public partial class GroupDetailViewModel : ObservableObject
$"Stufe {Group.GradeLevel} · Noten {(Group.GradingSystem == GradingSystem.Grades1To6 ? "16" : "015")}";
LoadStudents();
ReloadExams();
ParticipationTab.Initialize(Group.Id, Group.SchoolYear);
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear);
PlanningTab.Initialize(Group.Id);
ParticipationTab.Initialize(Group.Id, Group.SchoolYear, IsReadOnly);
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
PlanningTab.Initialize(Group.Id, IsReadOnly);
}
private void ReloadExams()
@@ -247,7 +274,11 @@ public partial class GroupDetailViewModel : ObservableObject
{
memberships.TryGetValue(s.Id, out var membership);
if (membership is null) continue;
var summary = new StudentSummary(s, membership, today) { OnChanged = SaveStudentNiveau };
var summary = new StudentSummary(s, membership, today)
{
OnChanged = SaveStudentNiveau,
CanEdit = IsEditable,
};
if (summary.IsFormer && !ShowFormerStudents) continue;
Students.Add(summary);
}
@@ -255,14 +286,14 @@ public partial class GroupDetailViewModel : ObservableObject
private void SaveStudentNiveau(StudentSummary summary)
{
if (Group is null) return;
if (!IsEditable || Group is null) return;
var membership = _memberships.GetByStudentAndGroup(summary.Id, Group.Id);
if (membership is null) return;
membership.Niveau = summary.Niveau;
_memberships.Save(membership);
}
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsEditableGroup))]
private async Task AddStudent()
{
if (OnAddStudent is null) return;
@@ -274,7 +305,7 @@ public partial class GroupDetailViewModel : ObservableObject
}
}
[RelayCommand(CanExecute = nameof(HasSelectedStudent))]
[RelayCommand(CanExecute = nameof(CanEditSelectedStudent))]
private async Task WithdrawStudent()
{
if (SelectedStudent is null || OnWithdrawStudent is null) return;
@@ -284,7 +315,7 @@ public partial class GroupDetailViewModel : ObservableObject
ParticipationTab.RefreshCurrentGrid();
}
[RelayCommand(CanExecute = nameof(CanReinstateSelectedStudent))]
[RelayCommand(CanExecute = nameof(CanEditAndReinstateSelectedStudent))]
private void ReinstateStudent()
{
if (Group is null || SelectedStudent is null) return;
@@ -303,10 +334,10 @@ public partial class GroupDetailViewModel : ObservableObject
ReinstateStudentCommand.NotifyCanExecuteChanged();
}
private bool HasSelectedStudent() => SelectedStudent is not null;
private bool CanReinstateSelectedStudent() => SelectedStudent?.HasExitDate == true;
private bool CanEditSelectedStudent() => IsEditable && SelectedStudent is not null;
private bool CanEditAndReinstateSelectedStudent() => IsEditable && SelectedStudent?.HasExitDate == true;
[RelayCommand]
[RelayCommand(CanExecute = nameof(IsEditableGroup))]
private async Task AddExam()
{
if (Group is null || OnAddExam is null) return;
@@ -314,7 +345,7 @@ public partial class GroupDetailViewModel : ObservableObject
if (saved) ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task EditExam()
{
if (SelectedExam is null || OnEditExam is null) return;
@@ -329,7 +360,7 @@ public partial class GroupDetailViewModel : ObservableObject
}
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task GradeExam()
{
if (SelectedExam is null || OnGradeExam is null) return;
@@ -338,7 +369,7 @@ public partial class GroupDetailViewModel : ObservableObject
await OnGradeExam(exam);
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task EvaluateExam()
{
if (SelectedExam is null || OnEvaluateExam is null) return;
@@ -347,7 +378,7 @@ public partial class GroupDetailViewModel : ObservableObject
await OnEvaluateExam(exam);
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task DuplicateExam()
{
if (SelectedExam is null || OnDuplicateExam is null) return;
@@ -357,7 +388,7 @@ public partial class GroupDetailViewModel : ObservableObject
if (saved) ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private async Task DeleteExam()
{
if (SelectedExam is null || OnConfirmDeleteExam is null) return;
@@ -368,7 +399,7 @@ public partial class GroupDetailViewModel : ObservableObject
ReloadExams();
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private void AdvanceExamStatus()
{
if (SelectedExam is null) return;
@@ -381,7 +412,7 @@ public partial class GroupDetailViewModel : ObservableObject
});
}
[RelayCommand(CanExecute = nameof(HasSelectedExam))]
[RelayCommand(CanExecute = nameof(CanEditSelectedExam))]
private void SetExamStatus(ExamStatus status)
{
if (SelectedExam is null) return;
@@ -409,7 +440,35 @@ public partial class GroupDetailViewModel : ObservableObject
SetExamStatusCommand.NotifyCanExecuteChanged();
}
private bool HasSelectedExam() => SelectedExam is not null;
private bool CanEditSelectedExam() => IsEditable && SelectedExam is not null;
private bool IsEditableGroup() => IsEditable;
[RelayCommand(CanExecute = nameof(IsReadOnlyGroup))]
private async Task ReactivateGroup()
{
if (Group is null || OnConfirmReactivate is null || !await OnConfirmReactivate()) return;
Group.IsActive = true;
_groups.Save(Group);
LoadGroup(Group.Id);
}
private bool IsReadOnlyGroup() => IsReadOnly;
private void NotifyWriteCommands()
{
AddStudentCommand.NotifyCanExecuteChanged();
WithdrawStudentCommand.NotifyCanExecuteChanged();
ReinstateStudentCommand.NotifyCanExecuteChanged();
AddExamCommand.NotifyCanExecuteChanged();
EditExamCommand.NotifyCanExecuteChanged();
GradeExamCommand.NotifyCanExecuteChanged();
EvaluateExamCommand.NotifyCanExecuteChanged();
DuplicateExamCommand.NotifyCanExecuteChanged();
DeleteExamCommand.NotifyCanExecuteChanged();
AdvanceExamStatusCommand.NotifyCanExecuteChanged();
SetExamStatusCommand.NotifyCanExecuteChanged();
ReactivateGroupCommand.NotifyCanExecuteChanged();
}
[RelayCommand] private void Refresh() { if (Group is not null) LoadGroup(Group.Id); }
}
@@ -448,6 +507,9 @@ public partial class StudentSummary : ObservableObject
public bool HasExitDate { get; }
public string MembershipStatus { get; }
public string WithdrawActionLabel => HasExitDate ? "Austrittsdatum ändern" : "Austragen";
public string AvatarLabel { get; }
public string AvatarColor { get; }
public string AvatarTooltip { get; }
[ObservableProperty] private Niveau? _niveau;
@@ -458,11 +520,15 @@ public partial class StudentSummary : ObservableObject
}
public Action<StudentSummary>? OnChanged { get; set; }
public bool CanEdit { get; init; } = true;
public StudentSummary(Core.Models.Student s, GroupMembership? membership, DateOnly? today = null)
{
Id = s.Id;
FullName = s.FullName;
AvatarLabel = GenderAvatarDisplay.Label(s.Gender);
AvatarColor = GenderAvatarDisplay.Color(s.Gender);
AvatarTooltip = GenderAvatarDisplay.Tooltip(s.Gender);
PeriodLabel = membership is null ? "" : BuildPeriodLabel(membership);
HasExitDate = membership?.LeftAt.HasValue == true;
IsFormer = membership?.LeftAt is { } leftAt