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,108 @@
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
namespace LehrerApp.Core.Services;
public sealed record GroupRolloverRequest(
string Name,
string TargetSchoolYear,
int GradeLevel,
IReadOnlyCollection<Guid> MembershipIds,
bool CopyGradingScheme,
bool ArchiveSource);
/// <summary>Erzeugt eine fachlich saubere Folgegruppe ohne Leistungs- oder Planungsdaten.</summary>
public sealed class GroupRolloverService(
IGroupRepository groups,
IGroupMembershipRepository memberships,
IGradingSchemeRepository gradingSchemes,
SchoolYearService schoolYears)
{
public LearningGroup RollOver(LearningGroup source, GroupRolloverRequest request)
{
var targetSchoolYear = request.TargetSchoolYear.Trim();
var targetStart = schoolYears.SchoolYearStart(targetSchoolYear);
if (targetSchoolYear != schoolYears.FormatSchoolYear(targetStart.Year))
throw new InvalidOperationException("Das Zielschuljahr muss das Format JJJJ/JJ haben, z.B. 2027/28.");
var sourceStart = schoolYears.SchoolYearStart(source.SchoolYear);
if (targetStart <= sourceStart)
throw new InvalidOperationException("Das Zielschuljahr muss nach dem bisherigen Schuljahr liegen.");
if (request.GradeLevel is < 1 or > 13)
throw new InvalidOperationException("Die Klassenstufe muss zwischen 1 und 13 liegen.");
if (string.IsNullOrWhiteSpace(request.Name))
throw new InvalidOperationException("Bitte einen Gruppennamen angeben.");
var duplicate = groups.GetBySchoolYear(targetSchoolYear, includeInactive: true)
.Any(g => g.Name.Equals(request.Name.Trim(), StringComparison.OrdinalIgnoreCase)
&& g.SubjectId == source.SubjectId && g.Type == source.Type);
if (duplicate)
throw new InvalidOperationException(
"Im Zielschuljahr existiert bereits eine gleichnamige Gruppe mit demselben Fach und Typ.");
var sourceMemberships = memberships.GetByGroup(source.Id)
.ToDictionary(m => m.Id);
var selected = request.MembershipIds.Distinct()
.Select(id => sourceMemberships.GetValueOrDefault(id)
?? throw new InvalidOperationException("Eine ausgewählte Mitgliedschaft gehört nicht mehr zur Ausgangsgruppe."))
.ToList();
if (selected.Count == 0)
throw new InvalidOperationException("Bitte mindestens einen Schüler auswählen.");
var target = new LearningGroup
{
Name = request.Name.Trim(),
Type = source.Type,
SubjectId = source.SubjectId,
SchoolYear = targetSchoolYear,
GradeLevel = request.GradeLevel,
GradingSystem = source.GradingSystem,
HoursPerWeek = source.HoursPerWeek,
IsActive = true,
IsOwnClass = source.IsOwnClass,
IsDifferentiated = source.IsDifferentiated,
};
var sourceWasActive = source.IsActive;
try
{
groups.Save(target);
foreach (var membership in selected)
{
memberships.Save(new GroupMembership
{
StudentId = membership.StudentId,
GroupId = target.Id,
AddedOn = DateOnly.FromDateTime(DateTime.Today),
Period = MembershipPeriod.FullYear,
JoinedAt = targetStart,
LeftAt = null,
Niveau = membership.Niveau,
});
}
if (request.CopyGradingScheme && gradingSchemes.GetByGroup(source.Id) is { } scheme)
{
gradingSchemes.Save(new GradingScheme
{
GroupId = target.Id,
ExamsPercent = scheme.ExamsPercent,
ParticipationPercent = scheme.ParticipationPercent,
OtherPercent = scheme.OtherPercent,
});
}
if (request.ArchiveSource && source.IsActive)
{
source.IsActive = false;
groups.Save(source);
}
return target;
}
catch
{
source.IsActive = sourceWasActive;
groups.Delete(target.Id);
throw;
}
}
}