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:
@@ -23,6 +23,13 @@ public class SeatingPlan
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public List<double> ColumnGapWidths { get; set; } = [];
|
public List<double> ColumnGapWidths { get; set; } = [];
|
||||||
public List<SeatAssignment> Assignments { get; set; } = [];
|
public List<SeatAssignment> Assignments { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// Plätze, für die im Raum physisch kein Tisch existiert (z. B. eine Reihe mit einer Lücke) —
|
||||||
|
/// werden im Sitzplan ausgeblendet, bleiben aber Teil des Rasters, damit Reihen/Spalten
|
||||||
|
/// weiterhin korrekt ausgerichtet sind. Ein ausgeblendeter Platz kann nie belegt sein, siehe
|
||||||
|
/// SeatingPlanRepository.Save.
|
||||||
|
/// </summary>
|
||||||
|
public List<HiddenSeat> HiddenSeats { get; set; } = [];
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
@@ -33,3 +40,9 @@ public class SeatAssignment
|
|||||||
public int Column { get; set; }
|
public int Column { get; set; }
|
||||||
public Guid StudentId { get; set; }
|
public Guid StudentId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class HiddenSeat
|
||||||
|
{
|
||||||
|
public int Row { get; set; }
|
||||||
|
public int Column { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -303,6 +303,58 @@ public sealed class RepositoryTests
|
|||||||
Assert.Throws<InvalidOperationException>(() => new SeatingPlanRepository(db).Save(plan));
|
Assert.Throws<InvalidOperationException>(() => new SeatingPlanRepository(db).Save(plan));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SeatingPlanRepository_FiltertAusgeblendetePlaetzeAusserhalbDesRasters()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
new GroupRepository(db).Save(group);
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = group.Id, Name = "Standard", Rows = 2, Columns = 2,
|
||||||
|
HiddenSeats =
|
||||||
|
[
|
||||||
|
new HiddenSeat { Row = 0, Column = 1 },
|
||||||
|
new HiddenSeat { Row = 5, Column = 5 }, // außerhalb des 2x2-Rasters
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
new SeatingPlanRepository(db).Save(plan);
|
||||||
|
|
||||||
|
var saved = db.SeatingPlans.FindById(plan.Id);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
var hidden = Assert.Single(saved.HiddenSeats);
|
||||||
|
Assert.Equal((0, 1), (hidden.Row, hidden.Column));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein ausgeblendeter Platz darf nie belegt sein - die Zuordnung eines Schülers ist wertvoller
|
||||||
|
/// als das Ausblenden. SeatingPlanTabViewModel verhindert das Belegen ausgeblendeter Plätze
|
||||||
|
/// bereits in der UI; diese Absicherung greift für Daten, die diesen Weg umgehen (z. B. Sync
|
||||||
|
/// von einem älteren Client).
|
||||||
|
[Fact]
|
||||||
|
public void SeatingPlanRepository_BlendetPlatzWiederEinWennErBelegtWird()
|
||||||
|
{
|
||||||
|
using var db = NewInMemoryContext();
|
||||||
|
var group = new LearningGroup { Name = "8a", SchoolYear = "2025/26" };
|
||||||
|
new GroupRepository(db).Save(group);
|
||||||
|
var student = new Student { FirstName = "Anna", LastName = "A" };
|
||||||
|
db.Students.Insert(student);
|
||||||
|
db.Memberships.Insert(new GroupMembership { GroupId = group.Id, StudentId = student.Id });
|
||||||
|
var plan = new SeatingPlan
|
||||||
|
{
|
||||||
|
GroupId = group.Id, Name = "Standard", Rows = 2, Columns = 2,
|
||||||
|
HiddenSeats = [new HiddenSeat { Row = 0, Column = 0 }],
|
||||||
|
Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }],
|
||||||
|
};
|
||||||
|
|
||||||
|
new SeatingPlanRepository(db).Save(plan);
|
||||||
|
|
||||||
|
var saved = db.SeatingPlans.FindById(plan.Id);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
Assert.Empty(saved.HiddenSeats);
|
||||||
|
Assert.Equal(student.Id, saved.Assignments.Single().StudentId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SeatingPlanRepository_AktualisiertPlanMitFehlendemOptionalenRaum()
|
public void SeatingPlanRepository_AktualisiertPlanMitFehlendemOptionalenRaum()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|||||||
plan.Room = plan.Room?.Trim() ?? "";
|
plan.Room = plan.Room?.Trim() ?? "";
|
||||||
plan.Assignments ??= [];
|
plan.Assignments ??= [];
|
||||||
plan.ColumnGapWidths ??= [];
|
plan.ColumnGapWidths ??= [];
|
||||||
|
plan.HiddenSeats ??= [];
|
||||||
if (plan.Name.Length == 0)
|
if (plan.Name.Length == 0)
|
||||||
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
|
throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein.");
|
||||||
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
|
if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10)
|
||||||
@@ -141,6 +142,18 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository
|
|||||||
if (plan.Assignments.Any(a => !memberIds.Contains(a.StudentId)))
|
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.");
|
throw new InvalidOperationException("Der Sitzplan enthält einen Schüler, der nicht zur Lerngruppe gehört.");
|
||||||
|
|
||||||
|
// Ein ausgeblendeter Platz darf nie belegt sein - die Zuordnung eines Schülers ist
|
||||||
|
// wertvoller als das Ausblenden, deshalb wird hier stillschweigend wieder eingeblendet
|
||||||
|
// statt die Zuordnung zu verwerfen. Sollte über die UI (SeatingPlanTabViewModel verhindert
|
||||||
|
// das Belegen ausgeblendeter Plätze) nie vorkommen - reine Absicherung bei fehlerhaften
|
||||||
|
// oder von einem älteren Client synchronisierten Daten.
|
||||||
|
var assignedPositions = plan.Assignments.Select(a => (a.Row, a.Column)).ToHashSet();
|
||||||
|
plan.HiddenSeats = plan.HiddenSeats
|
||||||
|
.Where(h => h.Row >= 0 && h.Row < plan.Rows && h.Column >= 0 && h.Column < plan.Columns)
|
||||||
|
.Where(h => !assignedPositions.Contains((h.Row, h.Column)))
|
||||||
|
.DistinctBy(h => (h.Row, h.Column))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
plan.UpdatedAt = DateTime.UtcNow;
|
plan.UpdatedAt = DateTime.UtcNow;
|
||||||
db.SeatingPlans.Upsert(plan);
|
db.SeatingPlans.Upsert(plan);
|
||||||
db.OnChange?.Invoke(nameof(SeatingPlan), plan.Id.ToString(), "Save", plan);
|
db.OnChange?.Invoke(nameof(SeatingPlan), plan.Id.ToString(), "Save", plan);
|
||||||
|
|||||||
@@ -115,6 +115,159 @@ public sealed class SeatingPlanViewModelTests
|
|||||||
Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count);
|
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]
|
[Fact]
|
||||||
public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus()
|
public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
|
ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
|
||||||
.Select(i => i < savedGapWidths.Count ? savedGapWidths[i] : 0)
|
.Select(i => i < savedGapWidths.Count ? savedGapWidths[i] : 0)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
var hiddenSeats = plan.HiddenSeats ?? [];
|
||||||
for (var row = 0; row < plan.Rows; row++)
|
for (var row = 0; row < plan.Rows; row++)
|
||||||
for (var column = 0; column < plan.Columns; column++)
|
for (var column = 0; column < plan.Columns; column++)
|
||||||
{
|
{
|
||||||
@@ -175,8 +176,9 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
? StudentSeatOption.Empty
|
? StudentSeatOption.Empty
|
||||||
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
|
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
|
||||||
?? StudentSeatOption.Empty;
|
?? StudentSeatOption.Empty;
|
||||||
|
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
|
||||||
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
|
||||||
CanEditLayout, ToggleSituationTag, IsEditable));
|
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden));
|
||||||
}
|
}
|
||||||
UpdateAssignmentSummary();
|
UpdateAssignmentSummary();
|
||||||
RefreshSeatLessonData();
|
RefreshSeatLessonData();
|
||||||
@@ -275,7 +277,8 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
|
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
|
||||||
{
|
{
|
||||||
if (!CanEditLayout || source == target || !source.SelectedOption.StudentId.HasValue) return;
|
if (!CanEditLayout || source == target || target.IsHidden
|
||||||
|
|| !source.SelectedOption.StudentId.HasValue) return;
|
||||||
var targetOption = target.SelectedOption;
|
var targetOption = target.SelectedOption;
|
||||||
target.SetSelectionSilently(source.SelectedOption);
|
target.SetSelectionSilently(source.SelectedOption);
|
||||||
source.SetSelectionSilently(targetOption);
|
source.SetSelectionSilently(targetOption);
|
||||||
@@ -284,7 +287,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
|
|
||||||
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
|
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
|
||||||
{
|
{
|
||||||
if (!CanEditLayout || !student.StudentId.HasValue) return;
|
if (!CanEditLayout || target.IsHidden || !student.StudentId.HasValue) return;
|
||||||
foreach (var other in Seats.Where(s => s != target && s.SelectedOption.StudentId == student.StudentId))
|
foreach (var other in Seats.Where(s => s != target && s.SelectedOption.StudentId == student.StudentId))
|
||||||
other.SetSelectionSilently(StudentSeatOption.Empty);
|
other.SetSelectionSilently(StudentSeatOption.Empty);
|
||||||
target.SetSelectionSilently(student);
|
target.SetSelectionSilently(student);
|
||||||
@@ -298,6 +301,26 @@ public partial class SeatingPlanTabViewModel : ObservableObject
|
|||||||
SaveSeatAssignments();
|
SaveSeatAssignments();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Blendet einen leeren Platz aus/ein, für den im Raum physisch kein Tisch steht
|
||||||
|
/// (z. B. eine Reihe mit einer Lücke). Nur für leere Plätze möglich, siehe
|
||||||
|
/// SeatCellViewModel.CanToggleHidden.</summary>
|
||||||
|
public void ToggleSeatHidden(SeatCellViewModel seat)
|
||||||
|
{
|
||||||
|
if (!CanEditLayout || seat.IsOccupied) return;
|
||||||
|
seat.IsHidden = !seat.IsHidden;
|
||||||
|
SaveHiddenSeats();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveHiddenSeats()
|
||||||
|
{
|
||||||
|
if (_currentPlan is null) return;
|
||||||
|
_currentPlan.HiddenSeats = Seats
|
||||||
|
.Where(s => s.IsHidden)
|
||||||
|
.Select(s => new HiddenSeat { Row = s.Row, Column = s.Column })
|
||||||
|
.ToList();
|
||||||
|
_plans.Save(_currentPlan);
|
||||||
|
}
|
||||||
|
|
||||||
private void SaveSeatAssignments()
|
private void SaveSeatAssignments()
|
||||||
{
|
{
|
||||||
if (_currentPlan is null) return;
|
if (_currentPlan is null) return;
|
||||||
@@ -405,6 +428,7 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
private readonly Action<SeatCellViewModel> _onChanged;
|
private readonly Action<SeatCellViewModel> _onChanged;
|
||||||
private bool _suppressChange;
|
private bool _suppressChange;
|
||||||
private readonly Action<SeatCellViewModel, string> _toggleSituationTag;
|
private readonly Action<SeatCellViewModel, string> _toggleSituationTag;
|
||||||
|
private readonly Action<SeatCellViewModel> _toggleHidden;
|
||||||
|
|
||||||
[ObservableProperty] private StudentSeatOption _selectedOption;
|
[ObservableProperty] private StudentSeatOption _selectedOption;
|
||||||
[ObservableProperty] private bool _isDropTarget;
|
[ObservableProperty] private bool _isDropTarget;
|
||||||
@@ -422,11 +446,28 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
public bool ShowLessonOverview => IsOccupied && !CanEdit;
|
public bool ShowLessonOverview => IsOccupied && !CanEdit;
|
||||||
[ObservableProperty] private bool _canRecordLesson;
|
[ObservableProperty] private bool _canRecordLesson;
|
||||||
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
public bool IsOccupied => SelectedOption.StudentId.HasValue;
|
||||||
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
|
public string StudentName => IsOccupied ? SelectedOption.DisplayName : IsHidden ? "Kein Tisch" : "Freier Platz";
|
||||||
|
/// <summary>Kein Tisch an dieser Position im Raum vorhanden (siehe SeatingPlan.HiddenSeats).
|
||||||
|
/// Bleibt außerhalb des Bearbeitungsmodus Teil des Rasters (für die Spaltenausrichtung), wird
|
||||||
|
/// dort aber nicht gerendert - siehe ShowSeat.</summary>
|
||||||
|
[ObservableProperty] private bool _isHidden;
|
||||||
|
/// <summary>Im Ansichtsmodus werden ausgeblendete Plätze nicht gerendert; im Bearbeitungsmodus
|
||||||
|
/// bleiben sie sichtbar (abgeblendet), damit sie wieder eingeblendet werden können.</summary>
|
||||||
|
public bool ShowSeat => !IsHidden || CanEdit;
|
||||||
|
/// <summary>Nur leere Plätze können ausgeblendet werden - ein belegter Platz müsste sonst
|
||||||
|
/// erst geräumt werden, was beim bloßen Ausblenden überraschend wäre.</summary>
|
||||||
|
public bool CanToggleHidden => CanEdit && !IsOccupied;
|
||||||
|
public string HiddenToggleLabel => IsHidden ? "Tisch einblenden" : "Kein Tisch hier";
|
||||||
|
/// <summary>Kombiniert LessonOpacity (Abwesenheits-Abblendung) mit dem Ausblenden-Zustand -
|
||||||
|
/// Opacity ist im DataTemplate bereits lokal an LessonOpacity gebunden gewesen; ein lokal
|
||||||
|
/// gebundener Wert überschreibt aber jeden Style-Setter für dieselbe Eigenschaft, daher muss
|
||||||
|
/// die Abblendung für ausgeblendete Plätze hier statt per CSS-Klasse erfolgen.</summary>
|
||||||
|
public double DisplayOpacity => IsHidden ? 0.4 : LessonOpacity;
|
||||||
|
|
||||||
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
|
||||||
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit,
|
||||||
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false)
|
Action<SeatCellViewModel, string>? toggleSituationTag = null, bool canRecordLesson = false,
|
||||||
|
bool isHidden = false, Action<SeatCellViewModel>? toggleHidden = null)
|
||||||
{
|
{
|
||||||
Row = row;
|
Row = row;
|
||||||
Column = column;
|
Column = column;
|
||||||
@@ -436,6 +477,8 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
_canEdit = canEdit;
|
_canEdit = canEdit;
|
||||||
_toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
|
_toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
|
||||||
_canRecordLesson = canRecordLesson;
|
_canRecordLesson = canRecordLesson;
|
||||||
|
_isHidden = isHidden;
|
||||||
|
_toggleHidden = toggleHidden ?? (_ => { });
|
||||||
foreach (var tag in SituationTagChoice.DefaultTags)
|
foreach (var tag in SituationTagChoice.DefaultTags)
|
||||||
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
|
||||||
}
|
}
|
||||||
@@ -445,10 +488,29 @@ public partial class SeatCellViewModel : ObservableObject
|
|||||||
OnPropertyChanged(nameof(IsOccupied));
|
OnPropertyChanged(nameof(IsOccupied));
|
||||||
OnPropertyChanged(nameof(StudentName));
|
OnPropertyChanged(nameof(StudentName));
|
||||||
OnPropertyChanged(nameof(ShowLessonOverview));
|
OnPropertyChanged(nameof(ShowLessonOverview));
|
||||||
|
OnPropertyChanged(nameof(CanToggleHidden));
|
||||||
if (!_suppressChange) _onChanged(this);
|
if (!_suppressChange) _onChanged(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnCanEditChanged(bool value) => OnPropertyChanged(nameof(ShowLessonOverview));
|
partial void OnCanEditChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ShowLessonOverview));
|
||||||
|
OnPropertyChanged(nameof(ShowSeat));
|
||||||
|
OnPropertyChanged(nameof(CanToggleHidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnIsHiddenChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ShowSeat));
|
||||||
|
OnPropertyChanged(nameof(StudentName));
|
||||||
|
OnPropertyChanged(nameof(DisplayOpacity));
|
||||||
|
OnPropertyChanged(nameof(HiddenToggleLabel));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnLessonOpacityChanged(double value) => OnPropertyChanged(nameof(DisplayOpacity));
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void ToggleHidden() => _toggleHidden(this);
|
||||||
|
|
||||||
public void SetSelectionSilently(StudentSeatOption option)
|
public void SetSelectionSilently(StudentSeatOption option)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,11 @@
|
|||||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
|
||||||
<Setter Property="BorderThickness" Value="2"/>
|
<Setter Property="BorderThickness" Value="2"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style Selector="Border.seat.hidden">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Grid ColumnDefinitions="260,*">
|
<Grid ColumnDefinitions="260,*">
|
||||||
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||||
@@ -108,6 +113,8 @@
|
|||||||
<DataTemplate x:DataType="vm:SeatCellViewModel">
|
<DataTemplate x:DataType="vm:SeatCellViewModel">
|
||||||
<Border Classes="seat" Classes.occupied="{Binding IsOccupied}"
|
<Border Classes="seat" Classes.occupied="{Binding IsOccupied}"
|
||||||
Classes.droptarget="{Binding IsDropTarget}"
|
Classes.droptarget="{Binding IsDropTarget}"
|
||||||
|
Classes.hidden="{Binding IsHidden}"
|
||||||
|
IsVisible="{Binding ShowSeat}"
|
||||||
Width="190" MinHeight="76" Margin="5" Padding="10"
|
Width="190" MinHeight="76" Margin="5" Padding="10"
|
||||||
CornerRadius="7"
|
CornerRadius="7"
|
||||||
DragDrop.AllowDrop="True"
|
DragDrop.AllowDrop="True"
|
||||||
@@ -118,7 +125,7 @@
|
|||||||
PointerPressed="OnDragSourcePressed"
|
PointerPressed="OnDragSourcePressed"
|
||||||
PointerMoved="OnDragSourceMoved"
|
PointerMoved="OnDragSourceMoved"
|
||||||
PointerReleased="OnDragSourceReleased"
|
PointerReleased="OnDragSourceReleased"
|
||||||
Tapped="OnSeatTapped" Opacity="{Binding LessonOpacity}">
|
Tapped="OnSeatTapped" Opacity="{Binding DisplayOpacity}">
|
||||||
<StackPanel Spacing="5">
|
<StackPanel Spacing="5">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
|
||||||
@@ -126,6 +133,9 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
<Button Content="{Binding HiddenToggleLabel}"
|
||||||
|
Command="{Binding ToggleHiddenCommand}" FontSize="9" Padding="6,2"
|
||||||
|
HorizontalAlignment="Center" IsVisible="{Binding CanToggleHidden}"/>
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5"
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5"
|
||||||
IsVisible="{Binding ShowLessonOverview}">
|
IsVisible="{Binding ShowLessonOverview}">
|
||||||
<Border Background="#1976D2" CornerRadius="8" Padding="6,1"
|
<Border Background="#1976D2" CornerRadius="8" Padding="6,1"
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ public partial class SeatingPlanTabView : UserControl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool CanDropOn(SeatCellViewModel target) =>
|
private bool CanDropOn(SeatCellViewModel target) =>
|
||||||
DataContext is SeatingPlanTabViewModel { CanEditLayout: true }
|
DataContext is SeatingPlanTabViewModel { CanEditLayout: true } && !target.IsHidden
|
||||||
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
|
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
|
||||||
|
|
||||||
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>
|
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>
|
||||||
|
|||||||
@@ -1333,6 +1333,29 @@ Hinweis in Kapitel 1 — betrifft auch Kurse, nicht nur Klassen.
|
|||||||
Erzeugung passiert erst über `EnsureTodaySession()` bei der ersten tatsächlichen Aktion
|
Erzeugung passiert erst über `EnsureTodaySession()` bei der ersten tatsächlichen Aktion
|
||||||
(Sitzplatz-Bewertung oder Situations-Tag).
|
(Sitzplatz-Bewertung oder Situations-Tag).
|
||||||
|
|
||||||
|
**Nachtrag (Feature-Ergänzung, Nutzerwunsch): unbenutzte Plätze ausblenden.** Manche Räume
|
||||||
|
sind unregelmäßig bestückt (z. B. eine Klasse mit 3 Reihen, in zwei davon fehlt ein
|
||||||
|
Einzeltisch) — das feste `Rows × Columns`-Raster kennt aber keine per-Reihe abweichende
|
||||||
|
Spaltenzahl. Statt einer negativen "Schüler-ID" als Sonderfall (das war der ursprünglich
|
||||||
|
vorgeschlagene Ansatz, scheidet aber aus, da `SeatAssignment.StudentId` ein `Guid` ist, kein
|
||||||
|
vorzeichenbehafteter Zahlentyp) gibt es jetzt ein eigenes Feld `SeatingPlan.HiddenSeats`
|
||||||
|
(`List<HiddenSeat { Row, Column }>`, analog zu `SeatAssignment`) für Plätze ohne physischen
|
||||||
|
Tisch. Ein ausgeblendeter Platz bleibt Teil des Rasters (für die Spalten-/Reihenausrichtung
|
||||||
|
der `SeatingPlanPanel`-Layoutmathematik), wird im Ansichtsmodus aber nicht gerendert
|
||||||
|
(`SeatCellViewModel.ShowSeat`); im Bearbeitungsmodus bleibt er sichtbar (abgeblendet), mit
|
||||||
|
einem Umschalt-Button "Kein Tisch hier" / "Tisch einblenden" direkt auf dem Platz. Nur leere
|
||||||
|
Plätze lassen sich ausblenden (`CanToggleHidden`); Zuweisen/Verschieben eines Schülers auf
|
||||||
|
einen ausgeblendeten Platz wird sowohl im ViewModel (`AssignStudent`/`MoveSeat`) als auch im
|
||||||
|
Drag&Drop-Ziel-Check der View verhindert. `SeatingPlanRepository.Save` filtert
|
||||||
|
`HiddenSeats` zusätzlich auf das gültige Raster und blendet einen Platz automatisch wieder
|
||||||
|
ein, falls er (z. B. durch einen älteren, noch nicht aktualisierten Client per Sync)
|
||||||
|
trotzdem belegt wird — die Zuordnung eines Schülers ist wertvoller als das Ausblenden und
|
||||||
|
geht nie verloren. Keine Änderungen an `EventApplier`/`SyncEngine` nötig, `SeatingPlan` wird
|
||||||
|
bereits als Ganzes synchronisiert. Neue Tests: `RepositoryTests`
|
||||||
|
(Rasterfilterung, automatisches Wiedereinblenden bei Belegung), `SeatingPlanViewModelTests`
|
||||||
|
(Laden, Umschalten inkl. Persistieren, Sperre für belegte Plätze, verhinderte Zuweisung auf
|
||||||
|
ausgeblendete Plätze, `ShowSeat`/`CanToggleHidden` je nach Modus).
|
||||||
|
|
||||||
### 7.2 Gruppen
|
### 7.2 Gruppen
|
||||||
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
|
- [x] **7.2.1** Gruppe bearbeiten und löschen — bereits vorhanden (`EditGroupCommand`/`DeleteGroupCommand`/
|
||||||
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
|
`ToggleArchiveCommand` in `GroupListViewModel`), nicht Teil der aktuellen Klausuren-Arbeit,
|
||||||
|
|||||||
Reference in New Issue
Block a user