Neue Abwensenheitsmodi plus Upgrade Sitzplan

This commit is contained in:
2026-08-19 12:27:12 +02:00
parent 1731e5088e
commit b2c1b15167
16 changed files with 383 additions and 30 deletions
@@ -363,7 +363,10 @@ public partial class ParticipationStudentRow : ObservableObject
public string AttendanceTooltip => AttendanceDisplay.Label(Attendance);
/// Abwesend im Sinne der Mitarbeitsbewertung: eine Bewertung ergibt für diese Stunde keinen
/// Sinn, unabhängig davon, ob die Abwesenheit entschuldigt ist oder noch geklärt werden muss.
public bool IsAbsent => Attendance is not null and not AttendanceStatus.Present;
public bool IsAbsent => Attendance is not null
and not AttendanceStatus.Present
and not AttendanceStatus.Late
and not AttendanceStatus.SignificantlyLate;
public string HomeworkSymbol => HomeworkDisplay.Symbol(Homework);
public string HomeworkTooltip => HomeworkDisplay.Label(Homework);
@@ -538,6 +541,11 @@ public static class AttendanceDisplay
AttendanceStatus.Unexcused => "Krank, unentschuldigt",
AttendanceStatus.Truant => "Geschwänzt",
AttendanceStatus.OtherSchoolEvent => "Andere Schulveranstaltung",
AttendanceStatus.Late => "Verspätet",
AttendanceStatus.SignificantlyLate => "Erheblich verspätet",
AttendanceStatus.LeftDuringClass => "Während des Unterrichts abgängig",
AttendanceStatus.LearningIsland => "Lerninsel",
AttendanceStatus.Suspended => "Suspendiert",
_ => "Anwesend",
};
@@ -550,6 +558,11 @@ public static class AttendanceDisplay
AttendanceStatus.Unexcused => "!",
AttendanceStatus.Truant => "✕",
AttendanceStatus.OtherSchoolEvent => "◇",
AttendanceStatus.Late => "V",
AttendanceStatus.SignificantlyLate => "V!",
AttendanceStatus.LeftDuringClass => "A",
AttendanceStatus.LearningIsland => "L",
AttendanceStatus.Suspended => "S",
_ => "",
};
@@ -561,6 +574,11 @@ public static class AttendanceDisplay
AttendanceStatus.Unexcused => "#D96C00",
AttendanceStatus.Truant => "#D64545",
AttendanceStatus.OtherSchoolEvent => "#5277C3",
AttendanceStatus.Late => "#D98200",
AttendanceStatus.SignificantlyLate => "#D96C00",
AttendanceStatus.LeftDuringClass => "#D64545",
AttendanceStatus.LearningIsland => "#5277C3",
AttendanceStatus.Suspended => "#6B6576",
_ => "",
};
}
@@ -24,6 +24,10 @@ public partial class SeatingPlanTabViewModel : ObservableObject
[ObservableProperty] private string _planTitle = "";
[ObservableProperty] private string _planSubtitle = "";
[ObservableProperty] private string _assignmentSummary = "";
[ObservableProperty] private bool _isBoardAtTop = true;
[ObservableProperty] private bool _isBoardAtBottom;
[ObservableProperty] private IReadOnlyList<double> _columnGapWidths = [];
[ObservableProperty] private bool _isEditMode;
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
@@ -33,6 +37,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public bool HasPlans => Plans.Count > 0;
public bool HasSelectedPlan => _currentPlan is not null;
public bool IsEditable => !_isReadOnly;
public bool CanEditLayout => IsEditable && IsEditMode;
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { get; set; }
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
public Func<SeatAssessmentViewModel, Task>? OnAssessStudent { get; set; }
@@ -57,6 +62,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
{
_groupId = groupId;
_isReadOnly = isReadOnly;
IsEditMode = false;
LoadStudentOptions();
ReloadPlans();
OnPropertyChanged(nameof(IsEditable));
@@ -102,6 +108,9 @@ public partial class SeatingPlanTabViewModel : ObservableObject
PlanTitle = "";
PlanSubtitle = "";
AssignmentSummary = "";
IsBoardAtTop = true;
IsBoardAtBottom = false;
ColumnGapWidths = [];
}
else
{
@@ -110,6 +119,12 @@ public partial class SeatingPlanTabViewModel : ObservableObject
PlanSubtitle = string.IsNullOrWhiteSpace(plan.Room)
? $"{plan.Rows} × {plan.Columns} Plätze"
: $"Raum {plan.Room} · {plan.Rows} × {plan.Columns} Plätze";
IsBoardAtBottom = plan.IsBoardAtBottom;
IsBoardAtTop = !plan.IsBoardAtBottom;
var savedGapWidths = plan.ColumnGapWidths ?? [];
ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1))
.Select(i => i < savedGapWidths.Count ? savedGapWidths[i] : 0)
.ToArray();
for (var row = 0; row < plan.Rows; row++)
for (var column = 0; column < plan.Columns; column++)
{
@@ -118,7 +133,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, IsEditable));
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, CanEditLayout));
}
UpdateAssignmentSummary();
}
@@ -128,7 +143,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
private void OnSeatChanged(SeatCellViewModel changed)
{
if (_currentPlan is null || !IsEditable) return;
if (_currentPlan is null || !CanEditLayout) return;
if (changed.SelectedOption.StudentId is Guid studentId)
{
foreach (var other in Seats.Where(s => s != changed && s.SelectedOption.StudentId == studentId))
@@ -152,7 +167,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target)
{
if (!IsEditable || source == target || !source.SelectedOption.StudentId.HasValue) return;
if (!CanEditLayout || source == target || !source.SelectedOption.StudentId.HasValue) return;
var targetOption = target.SelectedOption;
target.SetSelectionSilently(source.SelectedOption);
source.SetSelectionSilently(targetOption);
@@ -161,7 +176,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public void AssignStudent(StudentSeatOption student, SeatCellViewModel target)
{
if (!IsEditable || !student.StudentId.HasValue) return;
if (!CanEditLayout || !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);
@@ -170,7 +185,7 @@ public partial class SeatingPlanTabViewModel : ObservableObject
public void ClearSeat(SeatCellViewModel seat)
{
if (!IsEditable || !seat.SelectedOption.StudentId.HasValue) return;
if (!CanEditLayout || !seat.SelectedOption.StudentId.HasValue) return;
seat.SetSelectionSilently(StudentSeatOption.Empty);
SaveSeatAssignments();
}
@@ -223,8 +238,15 @@ public partial class SeatingPlanTabViewModel : ObservableObject
ReloadPlans();
}
private bool CanEdit() => IsEditable;
private bool CanEditSelected() => IsEditable && _currentPlan is not null;
partial void OnIsEditModeChanged(bool value)
{
OnPropertyChanged(nameof(CanEditLayout));
foreach (var seat in Seats) seat.CanEdit = CanEditLayout;
NotifyCommands();
}
private bool CanEdit() => IsEditable && (IsEditMode || !HasPlans);
private bool CanEditSelected() => CanEditLayout && _currentPlan is not null;
private void NotifyCommands()
{
@@ -264,7 +286,7 @@ public partial class SeatCellViewModel : ObservableObject
public int Column { get; }
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
public ObservableCollection<StudentSeatOption> Options { get; }
public bool CanEdit { get; }
[ObservableProperty] private bool _canEdit;
public bool IsOccupied => SelectedOption.StudentId.HasValue;
public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz";
@@ -276,7 +298,7 @@ public partial class SeatCellViewModel : ObservableObject
Options = options;
_selectedOption = selectedOption;
_onChanged = onChanged;
CanEdit = canEdit;
_canEdit = canEdit;
}
partial void OnSelectedOptionChanged(StudentSeatOption value)
@@ -363,6 +385,11 @@ public partial class SeatAssessmentViewModel : ObservableObject
AttendanceChoices.Add(new("◇", "Schulveranstaltung", "Strg+7", AttendanceStatus.OtherSchoolEvent, SetAttendance));
AttendanceChoices.Add(new("✕", "Geschwänzt", "Strg+9", AttendanceStatus.Truant, SetAttendance));
AttendanceChoices.Add(new("!", "Unentschuldigt", "Strg+0", AttendanceStatus.Unexcused, SetAttendance));
AttendanceChoices.Add(new("V", "Verspätet", "", AttendanceStatus.Late, SetAttendance));
AttendanceChoices.Add(new("V!", "Erheblich verspätet", "", AttendanceStatus.SignificantlyLate, SetAttendance));
AttendanceChoices.Add(new("A", "Im Unterricht abgängig", "", AttendanceStatus.LeftDuringClass, SetAttendance));
AttendanceChoices.Add(new("L", "Lerninsel", "", AttendanceStatus.LearningIsland, SetAttendance));
AttendanceChoices.Add(new("S", "Suspendiert", "", AttendanceStatus.Suspended, SetAttendance));
AttendanceChoices.Add(new("·", "Nicht kontrolliert", "Strg+X", null, SetAttendance));
}
@@ -579,10 +606,12 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
[ObservableProperty] private string _room = "";
[ObservableProperty] private decimal _rows = 4;
[ObservableProperty] private decimal _columns = 4;
[ObservableProperty] private bool _isBoardAtBottom;
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _layoutError = "";
public SeatingPlan? Result { get; private set; }
public ObservableCollection<ColumnGapEditor> ColumnGaps { get; } = [];
public string DialogTitle => _editingPlan is null ? "Neuen Sitzplan anlegen" : "Sitzplan bearbeiten";
public string SaveButtonText => _editingPlan is null ? "Anlegen" : "Speichern";
@@ -591,11 +620,26 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
_plans = plans;
_groupId = groupId;
_editingPlan = editingPlan;
RebuildColumnGaps(decimal.ToInt32(Columns));
if (editingPlan is null) return;
Name = editingPlan.Name ?? "";
Room = editingPlan.Room ?? "";
Rows = editingPlan.Rows;
Columns = editingPlan.Columns;
IsBoardAtBottom = editingPlan.IsBoardAtBottom;
var savedGapWidths = editingPlan.ColumnGapWidths ?? [];
for (var i = 0; i < ColumnGaps.Count && i < savedGapWidths.Count; i++)
ColumnGaps[i].Width = (decimal)savedGapWidths[i];
}
partial void OnColumnsChanged(decimal value) => RebuildColumnGaps(decimal.ToInt32(value));
private void RebuildColumnGaps(int columns)
{
var previous = ColumnGaps.ToDictionary(g => g.AfterColumn, g => g.Width);
ColumnGaps.Clear();
for (var afterColumn = 1; afterColumn < columns; afterColumn++)
ColumnGaps.Add(new ColumnGapEditor(afterColumn, previous.GetValueOrDefault(afterColumn)));
}
[RelayCommand]
@@ -621,6 +665,8 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
plan.Room = Room?.Trim() ?? "";
plan.Rows = decimal.ToInt32(Rows);
plan.Columns = decimal.ToInt32(Columns);
plan.IsBoardAtBottom = IsBoardAtBottom;
plan.ColumnGapWidths = ColumnGaps.Select(g => decimal.ToDouble(g.Width)).ToList();
try
{
_plans.Save(plan);
@@ -632,3 +678,10 @@ public partial class SeatingPlanDialogViewModel : ObservableObject
}
}
}
public partial class ColumnGapEditor(int afterColumn, decimal width) : ObservableObject
{
public int AfterColumn { get; } = afterColumn;
public string Label => $"Nach Platz {AfterColumn}";
[ObservableProperty] private decimal _width = width;
}
@@ -28,7 +28,7 @@
<Grid Grid.Row="2" RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" Margin="0,12,0,0">
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="3">
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
Text="Anwesenheit: 1 anwesend · 2 Entschuldigung offen · 5 krank/entschuldigt · 7 andere Schulveranstaltung · 9 geschwänzt · 0 unentschuldigt · X nicht kontrolliert"/>
Text="Anwesenheit: 1 anwesend · 2 Entschuldigung offen · 5 krank/entschuldigt · 7 andere Schulveranstaltung · 9 geschwänzt · 0 unentschuldigt · X nicht kontrolliert · seltene Status per Klick im Menü"/>
<TextBlock FontSize="10" Opacity="0.55" TextWrapping="Wrap"
Text="Hausaufgaben mit Alt/⌥: 1 gemacht · 3 teilweise/Rest offen · 4 Rest nachgereicht · 5 Rest nicht nachgereicht · 7 nicht gemacht/offen · 8 vollständig nachgereicht · 0 endgültig nicht nachgereicht · X keine aufgegeben"/>
<TextBlock FontSize="10" Opacity="0.55"
@@ -67,6 +67,13 @@ public partial class AttendanceHomeworkQuickInputDialog : Window
AddAttendanceItem(flyout, row, "[7] ◇ Andere Schulveranstaltung", AttendanceStatus.OtherSchoolEvent);
AddAttendanceItem(flyout, row, "[9] ✕ Geschwänzt", AttendanceStatus.Truant);
AddAttendanceItem(flyout, row, "[0] ! Unentschuldigt", AttendanceStatus.Unexcused);
flyout.Items.Add(new Separator());
AddAttendanceItem(flyout, row, "V Verspätet", AttendanceStatus.Late);
AddAttendanceItem(flyout, row, "V! Erheblich verspätet", AttendanceStatus.SignificantlyLate);
AddAttendanceItem(flyout, row, "A Während des Unterrichts abgängig", AttendanceStatus.LeftDuringClass);
AddAttendanceItem(flyout, row, "L Lerninsel", AttendanceStatus.LearningIsland);
AddAttendanceItem(flyout, row, "S Suspendiert", AttendanceStatus.Suspended);
flyout.Items.Add(new Separator());
AddAttendanceItem(flyout, row, "[X] · Nicht kontrolliert", null);
button.Flyout = flyout;
@@ -3,9 +3,10 @@
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanDialog"
x:DataType="vm:SeatingPlanDialogViewModel"
Title="{Binding DialogTitle}" Width="480" Height="470"
CanResize="False" WindowStartupLocation="CenterOwner">
Title="{Binding DialogTitle}" Width="620" Height="680" MinHeight="560"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer>
<StackPanel Spacing="16">
<StackPanel Spacing="3">
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
@@ -40,12 +41,39 @@
<TextBlock Text="{Binding LayoutError}" Foreground="Red" FontSize="11"
IsVisible="{Binding LayoutError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<StackPanel Spacing="4">
<TextBlock Text="Tafelposition" FontSize="12" Opacity="0.7"/>
<CheckBox Content="Tafel unterhalb der Sitzplätze anzeigen"
IsChecked="{Binding IsBoardAtBottom}"/>
</StackPanel>
<StackPanel Spacing="7">
<TextBlock Text="Abstände zwischen den Tischen" FontSize="12" Opacity="0.7"/>
<TextBlock Text="Breite 0 bedeutet: kein zusätzlicher Abstand. So lassen sich z. B. Tischgruppen und Mittelgänge abbilden."
FontSize="11" Opacity="0.55" TextWrapping="Wrap"/>
<ItemsControl ItemsSource="{Binding ColumnGaps}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ColumnGapEditor">
<StackPanel Width="125" Margin="0,0,10,8" Spacing="3">
<TextBlock Text="{Binding Label}" FontSize="11" Opacity="0.65"/>
<NumericUpDown Value="{Binding Width}" Minimum="0" Maximum="300"
Increment="10" FormatString="0 px"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
CornerRadius="6" Padding="12">
<TextBlock Text="Beim Verkleinern des Rasters entfallen Zuordnungen außerhalb der neuen Größe."
FontSize="12" Opacity="0.7" TextWrapping="Wrap"/>
</Border>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
@@ -0,0 +1,77 @@
using Avalonia;
using Avalonia.Controls;
namespace LehrerApp.Desktop.Views.Groups;
/// <summary>
/// Ordnet Sitzplätze in einem Raster an und fügt zwischen ausgewählten Spalten zusätzliche
/// transparente Breite ein. Anders als ein UniformGrid kann der Abstand je Spaltengrenze variieren.
/// </summary>
public sealed class SeatingPlanPanel : Panel
{
public static readonly StyledProperty<int> ColumnsProperty =
AvaloniaProperty.Register<SeatingPlanPanel, int>(nameof(Columns), 1);
public static readonly StyledProperty<IReadOnlyList<double>> ColumnGapWidthsProperty =
AvaloniaProperty.Register<SeatingPlanPanel, IReadOnlyList<double>>(
nameof(ColumnGapWidths), Array.Empty<double>());
public int Columns
{
get => GetValue(ColumnsProperty);
set => SetValue(ColumnsProperty, value);
}
public IReadOnlyList<double> ColumnGapWidths
{
get => GetValue(ColumnGapWidthsProperty);
set => SetValue(ColumnGapWidthsProperty, value);
}
static SeatingPlanPanel() =>
AffectsMeasure<SeatingPlanPanel>(ColumnsProperty, ColumnGapWidthsProperty);
protected override Size MeasureOverride(Size availableSize)
{
foreach (var child in Children)
child.Measure(Size.Infinity);
if (Children.Count == 0) return default;
var columns = Math.Max(1, Columns);
var rows = (int)Math.Ceiling((double)Children.Count / columns);
var cellWidth = Children.Max(child => child.DesiredSize.Width);
var cellHeight = Children.Max(child => child.DesiredSize.Height);
return new Size(cellWidth * columns + GapWidth(columns), cellHeight * rows);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Children.Count == 0) return finalSize;
var columns = Math.Max(1, Columns);
var cellWidth = Children.Max(child => child.DesiredSize.Width);
var cellHeight = Children.Max(child => child.DesiredSize.Height);
for (var index = 0; index < Children.Count; index++)
{
var column = index % columns;
var row = index / columns;
var child = Children[index];
var x = column * cellWidth + GapWidthBefore(column);
var childX = x + Math.Max(0, (cellWidth - child.DesiredSize.Width) / 2);
var childY = row * cellHeight + Math.Max(0, (cellHeight - child.DesiredSize.Height) / 2);
child.Arrange(new Rect(childX, childY, child.DesiredSize.Width, child.DesiredSize.Height));
}
return finalSize;
}
private double GapWidth(int columns) =>
Enumerable.Range(0, Math.Max(0, columns - 1)).Sum(GapAt);
private double GapWidthBefore(int column) =>
Enumerable.Range(0, Math.Max(0, column)).Sum(GapAt);
private double GapAt(int index) => index < ColumnGapWidths.Count
? Math.Clamp(ColumnGapWidths[index], 0, 300)
: 0;
}
@@ -1,6 +1,7 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
xmlns:groups="clr-namespace:LehrerApp.Desktop.Views.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanTabView"
x:DataType="vm:SeatingPlanTabViewModel">
<UserControl.Styles>
@@ -37,7 +38,7 @@
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Spacing="8" Margin="0,12,0,0">
<StackPanel Grid.Row="2" Spacing="8" Margin="0,12,0,0" IsVisible="{Binding IsEditMode}">
<Button Content=" Sitzplan" Command="{Binding AddPlanCommand}" HorizontalAlignment="Stretch"/>
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding HasSelectedPlan}">
<Button Grid.Column="0" Content="Bearbeiten" Command="{Binding EditPlanCommand}"
@@ -60,16 +61,20 @@
HorizontalAlignment="Center"/>
</StackPanel>
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,220" IsVisible="{Binding HasSelectedPlan}" Margin="24">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,Auto" IsVisible="{Binding HasSelectedPlan}" Margin="24">
<Grid Grid.Row="0" Grid.ColumnSpan="2" ColumnDefinitions="*,Auto" Margin="0,0,0,18">
<StackPanel Spacing="3">
<TextBlock Text="{Binding PlanTitle}" FontSize="22" FontWeight="SemiBold"/>
<TextBlock Text="{Binding PlanSubtitle}" Opacity="0.65"/>
</StackPanel>
<StackPanel Grid.Column="1" VerticalAlignment="Bottom">
<StackPanel Grid.Column="1" VerticalAlignment="Bottom" Spacing="4">
<ToggleSwitch Content="Bearbeitungsmodus" IsChecked="{Binding IsEditMode}"
IsVisible="{Binding IsEditable}" HorizontalAlignment="Right"/>
<TextBlock Text="{Binding AssignmentSummary}" HorizontalAlignment="Right" FontSize="12" Opacity="0.6"/>
<TextBlock Text="Ziehen: Platz ändern · Klicken: bewerten" HorizontalAlignment="Right"
FontSize="11" Opacity="0.5"/>
FontSize="11" Opacity="0.5" IsVisible="{Binding IsEditMode}"/>
<TextBlock Text="Klicken: bewerten" HorizontalAlignment="Right"
FontSize="11" Opacity="0.5" IsVisible="{Binding IsEditMode, Converter={x:Static BoolConverters.Not}}"/>
</StackPanel>
</Grid>
@@ -82,13 +87,15 @@
DragDrop.DragLeave="OnRoomDragLeave">
<StackPanel Spacing="14" HorizontalAlignment="Center">
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center">
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center"
IsVisible="{Binding IsBoardAtTop}">
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
</Border>
<ItemsControl ItemsSource="{Binding Seats}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding PlanColumns}"/>
<groups:SeatingPlanPanel Columns="{Binding PlanColumns}"
ColumnGapWidths="{Binding ColumnGapWidths}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
@@ -109,7 +116,7 @@
<StackPanel Spacing="5">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding IsOccupied}"/>
<TextBlock Grid.Column="1" Text="⠿" Opacity="0.45" IsVisible="{Binding CanEdit}"/>
</Grid>
<TextBlock Text="{Binding StudentName}" FontWeight="SemiBold" FontSize="13"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
@@ -118,12 +125,18 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center"
IsVisible="{Binding IsBoardAtBottom}">
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
</Border>
</StackPanel>
</ScrollViewer>
<!-- Die Schülerliste hat einen eigenen Scrollbereich und bleibt dadurch auch bei
großen Räumen neben den aktuell sichtbaren Sitzplätzen erreichbar. -->
<Border Grid.Row="1" Grid.Column="1"
Width="220" IsVisible="{Binding IsEditMode}"
DragDrop.AllowDrop="True" DragDrop.DragOver="OnUnassignedDragOver"
DragDrop.Drop="OnUnassignedDrop"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
@@ -48,7 +48,7 @@ public partial class SeatingPlanTabView : UserControl
!e.GetCurrentPoint(control).Properties.IsLeftButtonPressed) return;
var candidate = control.DataContext;
if (candidate is SeatCellViewModel { IsOccupied: false } or null) return;
if (DataContext is not SeatingPlanTabViewModel { IsEditable: true }) return;
if (DataContext is not SeatingPlanTabViewModel { CanEditLayout: true }) return;
_dragCandidate = candidate;
_dragTrigger = e;
_pressPosition = e.GetPosition(this);
@@ -132,7 +132,7 @@ public partial class SeatingPlanTabView : UserControl
}
private bool CanDropOn(SeatCellViewModel target) =>
DataContext is SeatingPlanTabViewModel { IsEditable: true }
DataContext is SeatingPlanTabViewModel { CanEditLayout: true }
&& ((_draggedSeat is not null && _draggedSeat != target) || _draggedStudent?.StudentId is not null);
private void OnUnassignedDragOver(object? sender, DragEventArgs e) =>