111 lines
4.5 KiB
C#
111 lines
4.5 KiB
C#
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,
|
|
// 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;
|
|
}
|
|
}
|
|
}
|