using LehrerApp.Core.Interfaces; using LehrerApp.Core.Models; namespace LehrerApp.Core.Services; public sealed record GroupRolloverRequest( string Name, string TargetSchoolYear, int GradeLevel, IReadOnlyCollection MembershipIds, bool CopyGradingScheme, bool ArchiveSource); /// Erzeugt eine fachlich saubere Folgegruppe ohne Leistungs- oder Planungsdaten. 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, PreviousGroupId = source.Id, // WebUntisLessonId bewusst nicht übernommen: WebUntis vergibt sie pro Schuljahr neu, // eine übernommene alte lsid würde in der Folgegruppe stumm falsche Fehlzeiten liefern. }; 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; } } }