feat: Teilnehmerlisten in Lerngruppen importieren

This commit is contained in:
2026-08-18 00:44:07 +02:00
parent cdac335ad1
commit 99efa3a0fe
11 changed files with 489 additions and 79 deletions
@@ -43,7 +43,19 @@ public sealed class StudentImportService
public async Task<StudentImportPreview> AnalyzeAsync(
ImportFile file,
CancellationToken cancellationToken = default)
CancellationToken cancellationToken = default) =>
await AnalyzeInternalAsync(file, null, cancellationToken).ConfigureAwait(false);
public async Task<StudentImportPreview> AnalyzeAsync(
ImportFile file,
Guid targetGroupId,
CancellationToken cancellationToken = default) =>
await AnalyzeInternalAsync(file, targetGroupId, cancellationToken).ConfigureAwait(false);
private async Task<StudentImportPreview> AnalyzeInternalAsync(
ImportFile file,
Guid? targetGroupId,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(file);
var detected = await _handlers.DetectAsync(file, cancellationToken).ConfigureAwait(false);
@@ -61,6 +73,15 @@ public sealed class StudentImportService
var existing = _students.GetAll(includeInactive: true);
var groups = _groups.GetAll(includeInactive: true);
var groupMemberships = groups.SelectMany(group => _memberships.GetByGroup(group.Id)).ToList();
var targetGroup = targetGroupId is null
? null
: groups.SingleOrDefault(group => group.Id == targetGroupId.Value);
if (targetGroupId is not null && targetGroup is not { IsActive: true })
messages.Add(new ImportMessage(
ImportMessageSeverity.Error,
"student.target-group.unavailable",
"Die Ziel-Lerngruppe existiert nicht oder ist archiviert."));
var currentSchoolYear = _schoolYears.CurrentSchoolYear();
var currentGroups = groups
.Where(group => group.IsActive && group.SchoolYear == currentSchoolYear)
@@ -107,6 +128,17 @@ public sealed class StudentImportService
? sameName.Where(student => student.DateOfBirth == candidate.DateOfBirth).ToList()
: [];
if (candidate.DateOfBirth is null && exact.Count == 0 && targetGroup is not null)
{
var memberIds = groupMemberships
.Where(membership => membership.GroupId == targetGroup.Id)
.Select(membership => membership.StudentId)
.ToHashSet();
var memberMatches = sameName.Where(student => memberIds.Contains(student.Id)).ToList();
if (memberMatches.Count == 1)
exact = memberMatches;
}
if (exact.Count == 1)
{
AddDifferenceWarning(candidate, exact[0], messages);
@@ -139,8 +171,11 @@ public sealed class StudentImportService
entries.Add(new StudentImportEntry(entryId, candidate, null, conflictId, []));
}
var groupAssignments = BuildGroupAssignments(imported, currentGroups, conflicts, messages);
var groupMemberships = groups.SelectMany(group => _memberships.GetByGroup(group.Id)).ToList();
var groupAssignments = targetGroup is { IsActive: true }
? [new StudentImportGroupAssignment(targetGroup.Name, targetGroup.Id, null)]
: targetGroupId is null
? BuildGroupAssignments(imported, currentGroups, conflicts, messages)
: [];
return new StudentImportPreview(
detected.Handler.FormatId,
@@ -149,6 +184,7 @@ public sealed class StudentImportService
messages,
conflicts,
groupAssignments,
targetGroupId,
Fingerprint(existing),
GroupStateFingerprint(groups, groupMemberships, currentSchoolYear));
}
@@ -235,10 +271,9 @@ public sealed class StudentImportService
foreach (var entry in preview.Entries)
{
cancellationToken.ThrowIfCancellationRequested();
if (entry.Student.GroupName is not { } sourceGroup
|| !studentIdsByEntry.TryGetValue(entry.Id, out var studentId)
if (!studentIdsByEntry.TryGetValue(entry.Id, out var studentId)
|| studentId is null
|| !groupResolutions.TryGetValue(sourceGroup, out var groupId)
|| !TryResolveTargetGroup(preview, entry, groupResolutions, out var groupId)
|| groupId is null
|| _memberships.GetByStudentAndGroup(studentId.Value, groupId.Value) is not null)
continue;
@@ -262,6 +297,24 @@ public sealed class StudentImportService
createdIds.AsReadOnly()));
}
private static bool TryResolveTargetGroup(
StudentImportPreview preview,
StudentImportEntry entry,
IReadOnlyDictionary<string, Guid?> groupResolutions,
out Guid? groupId)
{
if (preview.TargetGroupId is not null)
{
groupId = preview.TargetGroupId;
return true;
}
if (entry.Student.GroupName is { } sourceGroup
&& groupResolutions.TryGetValue(sourceGroup, out groupId))
return true;
groupId = null;
return false;
}
private static ImportedStudent Normalize(ImportedStudent student) => student with
{
FirstName = student.FirstName?.Trim() ?? "",