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
@@ -0,0 +1,164 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class GroupRolloverDialogViewModel : ObservableObject
{
private readonly LearningGroup _source;
private readonly GroupRolloverService _rollover;
private readonly SchoolYearService _schoolYears;
[ObservableProperty] private string _name;
[ObservableProperty] private string _targetSchoolYear;
[ObservableProperty] private int _gradeLevel;
[ObservableProperty] private bool _copyGradingScheme;
[ObservableProperty] private bool _archiveSource = true;
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _schoolYearError = "";
[ObservableProperty] private string _selectionError = "";
[ObservableProperty] private string _generalError = "";
public string SourceSummary => $"{_source.Name} · {_source.SchoolYear} · Stufe {_source.GradeLevel}";
public bool HasGradingScheme { get; }
public int SelectedCount => Students.Count(s => s.IsSelected);
public string SelectedCountText => $"{SelectedCount} von {Students.Count} Schülern ausgewählt";
public ObservableCollection<GroupRolloverStudentItem> Students { get; } = [];
public LearningGroup? Result { get; private set; }
public GroupRolloverDialogViewModel(LearningGroup source,
IStudentRepository students, IGroupMembershipRepository memberships,
IGradingSchemeRepository gradingSchemes, SchoolYearService schoolYears,
GroupRolloverService rollover)
{
_source = source;
_rollover = rollover;
_schoolYears = schoolYears;
_name = source.Name;
_targetSchoolYear = NextSchoolYear(source.SchoolYear, schoolYears);
_gradeLevel = Math.Min(13, source.GradeLevel + 1);
HasGradingScheme = gradingSchemes.GetByGroup(source.Id) is not null;
_copyGradingScheme = HasGradingScheme;
var sourceEnd = schoolYears.SchoolYearEnd(source.SchoolYear);
foreach (var membership in memberships.GetByGroup(source.Id)
.Select(m => (Membership: m, Student: students.GetById(m.StudentId)))
.Where(x => x.Student is not null)
.OrderBy(x => x.Student!.LastName).ThenBy(x => x.Student!.FirstName))
{
var canSelect = membership.Student!.IsActive;
var isPreselected = canSelect
&& GroupMembershipService.IsActiveOn(membership.Membership, sourceEnd);
var item = new GroupRolloverStudentItem(
membership.Student, membership.Membership, isPreselected, canSelect);
item.PropertyChanged += (_, e) =>
{
if (e.PropertyName != nameof(GroupRolloverStudentItem.IsSelected)) return;
SelectionError = "";
OnPropertyChanged(nameof(SelectedCount));
OnPropertyChanged(nameof(SelectedCountText));
};
Students.Add(item);
}
}
[RelayCommand]
private void Save()
{
NameError = "";
SchoolYearError = "";
SelectionError = "";
GeneralError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Name))
{
NameError = "Gruppenname erforderlich.";
valid = false;
}
try
{
var targetText = TargetSchoolYear.Trim();
var targetStart = _schoolYears.SchoolYearStart(targetText);
if (targetText != _schoolYears.FormatSchoolYear(targetStart.Year))
{
SchoolYearError = "Format JJJJ/JJ, z.B. 2027/28.";
valid = false;
}
else if (targetStart <= _schoolYears.SchoolYearStart(_source.SchoolYear))
{
SchoolYearError = "Das Zielschuljahr muss nach dem bisherigen liegen.";
valid = false;
}
}
catch (Exception ex) when (ex is FormatException or ArgumentOutOfRangeException or IndexOutOfRangeException)
{
SchoolYearError = "Format JJJJ/JJ, z.B. 2027/28.";
valid = false;
}
if (SelectedCount == 0)
{
SelectionError = "Bitte mindestens einen Schüler auswählen.";
valid = false;
}
if (!valid) return;
try
{
Result = _rollover.RollOver(_source, new GroupRolloverRequest(
Name, TargetSchoolYear.Trim(), GradeLevel,
Students.Where(s => s.IsSelected).Select(s => s.MembershipId).ToList(),
CopyGradingScheme, ArchiveSource));
}
catch (Exception ex) when (ex is InvalidOperationException or FormatException or ArgumentOutOfRangeException)
{
GeneralError = ex.Message;
}
}
private static string NextSchoolYear(string source, SchoolYearService schoolYears)
{
var start = schoolYears.SchoolYearStart(source);
return schoolYears.FormatSchoolYear(start.Year + 1);
}
}
public partial class GroupRolloverStudentItem : ObservableObject
{
[ObservableProperty] private bool _isSelected;
public Guid MembershipId { get; }
public string FullName { get; }
public string NiveauText { get; }
public string MembershipStatus { get; }
public bool CanSelect { get; }
public GroupRolloverStudentItem(Student student, GroupMembership membership,
bool isSelected, bool canSelect)
{
MembershipId = membership.Id;
FullName = student.FullName;
NiveauText = membership.Niveau?.ToString() ?? "";
CanSelect = canSelect;
_isSelected = isSelected;
MembershipStatus = BuildStatus(student, membership, isSelected);
}
partial void OnIsSelectedChanged(bool value)
{
if (!CanSelect && value) IsSelected = false;
}
private static string BuildStatus(Student student, GroupMembership membership, bool isPreselected)
{
if (!student.IsActive) return "Schüler inaktiv · nicht übernehmbar";
if (isPreselected) return "Bis Schuljahresende aktiv · vorausgewählt";
if (membership.LeftAt is { } leftAt) return $"Ausgetreten am {leftAt:dd.MM.yyyy} · nicht vorausgewählt";
if (membership.Period == MembershipPeriod.H1Only) return "Nur 1. Halbjahr · nicht vorausgewählt";
return "Am Schuljahresende nicht aktiv · nicht vorausgewählt";
}
}