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
@@ -167,6 +167,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
.Select(i => i < savedGapWidths.Count ? savedGapWidths[i] : 0)
.ToArray();
var hiddenSeats = plan.HiddenSeats ?? [];
for (var row = 0; row < plan.Rows; row++)
for (var column = 0; column < plan.Columns; column++)
{
@@ -175,8 +176,9 @@ public partial class SeatingPlanTabViewModel : ObservableObject
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
var isHidden = hiddenSeats.Any(h => h.Row == row && h.Column == column);
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged,
CanEditLayout, ToggleSituationTag, IsEditable));
CanEditLayout, ToggleSituationTag, IsEditable, isHidden, ToggleSeatHidden));
}
UpdateAssignmentSummary();
RefreshSeatLessonData();
@@ -275,7 +277,8 @@ public partial class SeatingPlanTabViewModel : ObservableObject
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;
target.SetSelectionSilently(source.SelectedOption);
source.SetSelectionSilently(targetOption);
@@ -284,7 +287,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
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))
other.SetSelectionSilently(StudentSeatOption.Empty);
target.SetSelectionSilently(student);
@@ -298,6 +301,26 @@ public partial class SeatingPlanTabViewModel : ObservableObject
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()
{
if (_currentPlan is null) return;
@@ -405,6 +428,7 @@ public partial class SeatCellViewModel : ObservableObject
private readonly Action<SeatCellViewModel> _onChanged;
private bool _suppressChange;
private readonly Action<SeatCellViewModel, string> _toggleSituationTag;
private readonly Action<SeatCellViewModel> _toggleHidden;
[ObservableProperty] private StudentSeatOption _selectedOption;
[ObservableProperty] private bool _isDropTarget;
@@ -422,11 +446,28 @@ public partial class SeatCellViewModel : ObservableObject
public bool ShowLessonOverview => IsOccupied && !CanEdit;
[ObservableProperty] private bool _canRecordLesson;
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,
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;
Column = column;
@@ -436,6 +477,8 @@ public partial class SeatCellViewModel : ObservableObject
_canEdit = canEdit;
_toggleSituationTag = toggleSituationTag ?? ((_, _) => { });
_canRecordLesson = canRecordLesson;
_isHidden = isHidden;
_toggleHidden = toggleHidden ?? (_ => { });
foreach (var tag in SituationTagChoice.DefaultTags)
SituationTags.Add(new SituationTagChoice(tag, false, value => _toggleSituationTag(this, value)));
}
@@ -445,10 +488,29 @@ public partial class SeatCellViewModel : ObservableObject
OnPropertyChanged(nameof(IsOccupied));
OnPropertyChanged(nameof(StudentName));
OnPropertyChanged(nameof(ShowLessonOverview));
OnPropertyChanged(nameof(CanToggleHidden));
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)
{
@@ -18,6 +18,11 @@
<Setter Property="BorderBrush" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="BorderThickness" Value="2"/>
</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>
<Grid ColumnDefinitions="260,*">
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
@@ -108,6 +113,8 @@
<DataTemplate x:DataType="vm:SeatCellViewModel">
<Border Classes="seat" Classes.occupied="{Binding IsOccupied}"
Classes.droptarget="{Binding IsDropTarget}"
Classes.hidden="{Binding IsHidden}"
IsVisible="{Binding ShowSeat}"
Width="190" MinHeight="76" Margin="5" Padding="10"
CornerRadius="7"
DragDrop.AllowDrop="True"
@@ -118,7 +125,7 @@
PointerPressed="OnDragSourcePressed"
PointerMoved="OnDragSourceMoved"
PointerReleased="OnDragSourceReleased"
Tapped="OnSeatTapped" Opacity="{Binding LessonOpacity}">
Tapped="OnSeatTapped" Opacity="{Binding DisplayOpacity}">
<StackPanel Spacing="5">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
@@ -126,6 +133,9 @@
</Grid>
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
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"
IsVisible="{Binding ShowLessonOverview}">
<Border Background="#1976D2" CornerRadius="8" Padding="6,1"
@@ -134,7 +134,7 @@ public partial class SeatingPlanTabView : UserControl
}
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);
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>