feat: unbenutzte Sitzplätze ausblenden können

Manche Räume sind unregelmäßig bestückt (z.B. eine Klasse mit 3 Reihen, in zwei davon fehlt ein
Einzeltisch) - das feste Rows x Columns-Raster kennt aber keine per-Reihe abweichende Spaltenzahl.

Neues Feld SeatingPlan.HiddenSeats für Plätze ohne physischen Tisch. Ein ausgeblendeter Platz
bleibt Teil des Rasters (für die Spalten-/Reihenausrichtung von SeatingPlanPanel), wird im
Ansichtsmodus aber nicht gerendert; im Bearbeitungsmodus bleibt er sichtbar (abgeblendet) mit
einem Umschalt-Button direkt auf dem Platz. Nur leere Plätze lassen sich ausblenden; Zuweisen/
Verschieben eines Schülers auf einen ausgeblendeten Platz wird sowohl im ViewModel als auch im
Drag&Drop-Ziel-Check verhindert. SeatingPlanRepository.Save filtert HiddenSeats zusätzlich auf
das gültige Raster und blendet einen Platz automatisch wieder ein, falls er trotzdem belegt wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 12:06:26 +02:00
co-authored by Claude Sonnet 5
parent 391628ae36
commit c5a1157a7d
8 changed files with 334 additions and 8 deletions
@@ -115,6 +115,159 @@ public sealed class SeatingPlanViewModelTests
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
}
// ── Ausgeblendete Plätze (fehlender Tisch in einer Reihe) ─────────────────────────────
[Fact]
public void LoadPlan_LaedtAusgeblendetePlaetzeAusDemPlan()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
};
var vm = new SeatingPlanTabViewModel(
new FakeSeatingPlans([plan]), new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
Assert.False(vm.Seats[0].IsHidden);
Assert.True(vm.Seats[1].IsHidden);
}
[Fact]
public void ToggleSeatHidden_LeererPlatz_BlendetAusUndSpeichertUndKannWiederEingeblendetWerden()
{
var groupId = Guid.NewGuid();
var plan = new SeatingPlan { GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2 };
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([]), new FakeMemberships([]),
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.ToggleSeatHidden(vm.Seats[1]);
Assert.True(vm.Seats[1].IsHidden);
var hidden = Assert.Single(plans.GetById(plan.Id)!.HiddenSeats);
Assert.Equal((0, 1), (hidden.Row, hidden.Column));
vm.ToggleSeatHidden(vm.Seats[1]);
Assert.False(vm.Seats[1].IsHidden);
Assert.Empty(plans.GetById(plan.Id)!.HiddenSeats);
}
[Fact]
public void ToggleSeatHidden_BelegterPlatz_TutNichts()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.ToggleSeatHidden(vm.Seats[0]);
Assert.False(vm.Seats[0].IsHidden);
}
/// Ein ausgeblendeter Platz darf nie ein Ziel für Drag&Drop/Zuordnung sein - sonst hätte man
/// gleichzeitig "kein Tisch hier" und einen dort sitzenden Schüler.
[Fact]
public void AssignStudent_AufAusgeblendetenPlatz_WirdIgnoriert()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 1,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 0 }],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([student]), memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.AssignStudent(vm.StudentOptions.Single(o => o.StudentId == student.Id), vm.Seats[0]);
Assert.False(vm.Seats[0].IsOccupied);
Assert.Empty(plans.GetById(plan.Id)!.Assignments);
}
[Fact]
public void MoveSeat_AufAusgeblendetenPlatz_WirdIgnoriert()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var plans = new FakeSeatingPlans([plan]);
var vm = new SeatingPlanTabViewModel(plans, new FakeStudents([student]), memberships,
new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
vm.IsEditMode = true;
vm.MoveSeat(vm.Seats[0], vm.Seats[1]);
Assert.Equal(student.Id, vm.Seats[0].SelectedOption.StudentId);
Assert.False(vm.Seats[1].IsOccupied);
}
[Fact]
public void ShowSeatUndCanToggleHidden_HaengenVonBearbeitungsmodusUndBelegungAb()
{
var groupId = Guid.NewGuid();
var student = new Student { FirstName = "Anna", LastName = "A" };
var memberships = new FakeMemberships([
new GroupMembership { GroupId = groupId, StudentId = student.Id },
]);
var plan = new SeatingPlan
{
GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2,
HiddenSeats = [new HiddenSeat { Row = 0, Column = 1 }],
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
};
var vm = new SeatingPlanTabViewModel(new FakeSeatingPlans([plan]), new FakeStudents([student]),
memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects());
vm.Initialize(groupId, isReadOnly: false);
// Ansichtsmodus: der ausgeblendete Platz wird nicht gerendert, belegte Plätze können
// ohnehin nicht ausgeblendet werden.
Assert.False(vm.Seats[1].ShowSeat);
Assert.False(vm.Seats[0].CanToggleHidden);
Assert.False(vm.Seats[1].CanToggleHidden);
vm.IsEditMode = true;
// Bearbeitungsmodus: der ausgeblendete Platz bleibt sichtbar (abgeblendet), um ihn wieder
// einblenden zu können - der belegte Platz kann weiterhin nicht ausgeblendet werden.
Assert.True(vm.Seats[1].ShowSeat);
Assert.False(vm.Seats[0].CanToggleHidden);
Assert.True(vm.Seats[1].CanToggleHidden);
}
[Fact]
public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus()
{