feat: add seating plans for learning groups

This commit is contained in:
2026-08-17 01:01:40 +02:00
parent fbf70df439
commit fe36f51886
19 changed files with 771 additions and 6 deletions
@@ -121,6 +121,9 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
foreach (var section in db.ParticipationSections.Find(s => s.GroupId == id).ToList())
db.ParticipationSections.Delete(section.Id);
foreach (var plan in db.SeatingPlans.Find(p => p.GroupId == id).ToList())
db.SeatingPlans.Delete(plan.Id);
// Dokumentation und Arbeitszeit sind historische Nachweise. Sie bleiben erhalten,
// werden aber von der nicht mehr existierenden Lerngruppe entkoppelt.
foreach (var documentation in db.Documentation.Find(d => d.GroupId == id).ToList())
@@ -148,6 +151,58 @@ public class GroupRepository(LiteDbContext db) : IGroupRepository
}
}
public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
{
public SeatingPlan? GetById(Guid id) => db.SeatingPlans.FindById(id);
public List<SeatingPlan> GetByGroup(Guid groupId) =>
db.SeatingPlans.Find(p => p.GroupId == groupId)
.OrderBy(p => p.Name).ThenBy(p => p.Room).ToList();
public void Save(SeatingPlan plan)
{
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
plan.Name = plan.Name.Trim();
plan.Room = plan.Room.Trim();
if (plan.Name.Length == 0)
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
throw new ArgumentOutOfRangeException(nameof(plan), "Ein Sitzplan muss zwischen 1 und 10 Reihen und Spalten haben.");
if (db.Groups.FindById(plan.GroupId) is null)
throw new InvalidOperationException("Die zugehörige Lerngruppe existiert nicht.");
var duplicateName = db.SeatingPlans.Find(p => p.GroupId == plan.GroupId)
.FirstOrDefault(p => p.Id != plan.Id
&& string.Equals(p.Name, plan.Name, StringComparison.OrdinalIgnoreCase)
&& string.Equals(p.Room, plan.Room, StringComparison.OrdinalIgnoreCase));
if (duplicateName is not null)
throw new InvalidOperationException("Für diese Lerngruppe existiert bereits ein gleichnamiger Sitzplan in diesem Raum.");
plan.Assignments = plan.Assignments
.Where(a => a.Row >= 0 && a.Row < plan.Rows && a.Column >= 0 && a.Column < plan.Columns)
.ToList();
if (plan.Assignments.GroupBy(a => (a.Row, a.Column)).Any(g => g.Count() > 1))
throw new InvalidOperationException("Ein Sitzplatz darf nur einmal belegt werden.");
if (plan.Assignments.GroupBy(a => a.StudentId).Any(g => g.Count() > 1))
throw new InvalidOperationException("Ein Schüler darf in einem Sitzplan nur einmal vorkommen.");
var memberIds = db.Memberships.Find(m => m.GroupId == plan.GroupId)
.Select(m => m.StudentId).ToHashSet();
if (plan.Assignments.Any(a => !memberIds.Contains(a.StudentId)))
throw new InvalidOperationException("Der Sitzplan enthält einen Schüler, der nicht zur Lerngruppe gehört.");
plan.UpdatedAt = DateTime.UtcNow;
db.SeatingPlans.Upsert(plan);
}
public void Delete(Guid id)
{
if (db.SeatingPlans.FindById(id) is { } plan)
ArchivedGroupWriteGuard.EnsureActive(db, plan.GroupId);
db.SeatingPlans.Delete(id);
}
}
public class GroupMembershipRepository(LiteDbContext db) : IGroupMembershipRepository
{
public List<GroupMembership> GetByStudent(Guid id) =>