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
+2
View File
@@ -118,6 +118,7 @@ public static class AppBootstrapper
// ── Repositories ──────────────────────────────────────────────────────
services.AddSingleton<IStudentRepository, StudentRepository>();
services.AddSingleton<IGroupRepository, GroupRepository>();
services.AddSingleton<ISeatingPlanRepository, SeatingPlanRepository>();
services.AddSingleton<IGroupMembershipRepository, GroupMembershipRepository>();
services.AddSingleton<IExamRepository, ExamRepository>();
services.AddSingleton<IExamResultRepository, ExamResultRepository>();
@@ -216,6 +217,7 @@ public static class AppBootstrapper
services.AddTransient<GradeOverviewTabViewModel>();
services.AddTransient<PlanningTabViewModel>();
services.AddTransient<CompetencyOverviewTabViewModel>();
services.AddTransient<SeatingPlanTabViewModel>();
services.AddTransient<AddGroupDialogViewModel>();
services.AddTransient<SettingsViewModel>();
@@ -208,7 +208,7 @@ public partial class GroupDetailViewModel : ObservableObject
partial void OnShowFormerStudentsChanged(bool value) => LoadStudents();
partial void OnActiveTabIndexChanged(int value)
{
if (value == 6) CompetencyOverviewTab.Refresh();
if (value == 7) CompetencyOverviewTab.Refresh();
}
public ObservableCollection<StudentSummary> Students { get; } = [];
@@ -218,6 +218,7 @@ public partial class GroupDetailViewModel : ObservableObject
public GradeOverviewTabViewModel GradeOverviewTab { get; }
public PlanningTabViewModel PlanningTab { get; }
public CompetencyOverviewTabViewModel CompetencyOverviewTab { get; }
public SeatingPlanTabViewModel SeatingPlanTab { get; }
public Func<Task<bool>>? OnAddStudent { get; set; }
public Func<StudentSummary, Task<bool>>? OnWithdrawStudent { get; set; }
public Func<Guid, Task<bool>>? OnAddExam { get; set; }
@@ -232,7 +233,8 @@ public partial class GroupDetailViewModel : ObservableObject
IGroupMembershipRepository memberships, ISubjectRepository subjects,
IExamRepository exams, IGradeRepository grades, IWorkTaskRepository tasks,
ParticipationTabViewModel participationTab, GradeOverviewTabViewModel gradeOverviewTab,
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab)
PlanningTabViewModel planningTab, CompetencyOverviewTabViewModel competencyOverviewTab,
SeatingPlanTabViewModel seatingPlanTab)
{
_groups = groups; _students = students; _memberships = memberships; _subjects = subjects;
_exams = exams; _grades = grades; _tasks = tasks;
@@ -240,6 +242,7 @@ public partial class GroupDetailViewModel : ObservableObject
GradeOverviewTab = gradeOverviewTab;
PlanningTab = planningTab;
CompetencyOverviewTab = competencyOverviewTab;
SeatingPlanTab = seatingPlanTab;
}
public void LoadGroup(Guid id)
@@ -259,6 +262,7 @@ public partial class GroupDetailViewModel : ObservableObject
GradeOverviewTab.Initialize(Group.Id, Group.GradingSystem, Group.Type, GroupTitle, Group.SchoolYear, IsReadOnly);
PlanningTab.Initialize(Group.Id, IsReadOnly);
CompetencyOverviewTab.Initialize(Group);
SeatingPlanTab.Initialize(Group.Id, IsReadOnly);
}
private void ReloadExams()
@@ -0,0 +1,298 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Core.Interfaces;
using LehrerApp.Core.Models;
using LehrerApp.Core.Services;
using System.Collections.ObjectModel;
namespace LehrerApp.Desktop.ViewModels.Groups;
public partial class SeatingPlanTabViewModel : ObservableObject
{
private readonly ISeatingPlanRepository _plans;
private readonly IStudentRepository _students;
private readonly IGroupMembershipRepository _memberships;
private Guid _groupId;
private SeatingPlan? _currentPlan;
private bool _isReadOnly;
[ObservableProperty] private SeatingPlanSummary? _selectedPlan;
[ObservableProperty] private int _planColumns = 1;
[ObservableProperty] private string _planTitle = "";
[ObservableProperty] private string _planSubtitle = "";
[ObservableProperty] private string _assignmentSummary = "";
public ObservableCollection<SeatingPlanSummary> Plans { get; } = [];
public ObservableCollection<SeatCellViewModel> Seats { get; } = [];
public ObservableCollection<StudentSeatOption> StudentOptions { get; } = [];
public bool HasPlans => Plans.Count > 0;
public bool HasSelectedPlan => _currentPlan is not null;
public bool IsEditable => !_isReadOnly;
public Func<SeatingPlan?, Task<SeatingPlan?>>? OnEditPlan { get; set; }
public Func<SeatingPlanSummary, Task<bool>>? OnConfirmDelete { get; set; }
public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students,
IGroupMembershipRepository memberships)
{
_plans = plans;
_students = students;
_memberships = memberships;
}
public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) =>
new(_plans, _groupId, plan);
public void Initialize(Guid groupId, bool isReadOnly)
{
_groupId = groupId;
_isReadOnly = isReadOnly;
LoadStudentOptions();
ReloadPlans();
OnPropertyChanged(nameof(IsEditable));
NotifyCommands();
}
private void LoadStudentOptions()
{
StudentOptions.Clear();
StudentOptions.Add(StudentSeatOption.Empty);
var today = DateOnly.FromDateTime(DateTime.Today);
var memberships = _memberships.GetByGroup(_groupId).ToDictionary(m => m.StudentId);
foreach (var student in _students.GetByGroup(_groupId)
.Where(s => memberships.TryGetValue(s.Id, out var membership)
&& GroupMembershipService.IsActiveOn(membership, today))
.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
StudentOptions.Add(new StudentSeatOption(student.Id, $"{student.LastName}, {student.FirstName}"));
}
private void ReloadPlans(Guid? selectId = null)
{
selectId ??= SelectedPlan?.Id;
Plans.Clear();
foreach (var plan in _plans.GetByGroup(_groupId))
Plans.Add(new SeatingPlanSummary(plan));
SelectedPlan = Plans.FirstOrDefault(p => p.Id == selectId) ?? Plans.FirstOrDefault();
if (SelectedPlan is null) LoadPlan(null);
OnPropertyChanged(nameof(HasPlans));
NotifyCommands();
}
partial void OnSelectedPlanChanged(SeatingPlanSummary? value) =>
LoadPlan(value is null ? null : _plans.GetById(value.Id));
private void LoadPlan(SeatingPlan? plan)
{
_currentPlan = plan;
Seats.Clear();
if (plan is null)
{
PlanColumns = 1;
PlanTitle = "";
PlanSubtitle = "";
AssignmentSummary = "";
}
else
{
PlanColumns = plan.Columns;
PlanTitle = plan.Name;
PlanSubtitle = string.IsNullOrWhiteSpace(plan.Room)
? $"{plan.Rows} × {plan.Columns} Plätze"
: $"Raum {plan.Room} · {plan.Rows} × {plan.Columns} Plätze";
for (var row = 0; row < plan.Rows; row++)
for (var column = 0; column < plan.Columns; column++)
{
var assignment = plan.Assignments.FirstOrDefault(a => a.Row == row && a.Column == column);
var option = assignment is null
? StudentSeatOption.Empty
: StudentOptions.FirstOrDefault(o => o.StudentId == assignment.StudentId)
?? StudentSeatOption.Empty;
Seats.Add(new SeatCellViewModel(row, column, StudentOptions, option, OnSeatChanged, IsEditable));
}
UpdateAssignmentSummary();
}
OnPropertyChanged(nameof(HasSelectedPlan));
NotifyCommands();
}
private void OnSeatChanged(SeatCellViewModel changed)
{
if (_currentPlan is null || !IsEditable) return;
if (changed.SelectedOption.StudentId is Guid studentId)
{
foreach (var other in Seats.Where(s => s != changed && s.SelectedOption.StudentId == studentId))
other.SetSelectionSilently(StudentSeatOption.Empty);
}
_currentPlan.Assignments = Seats
.Where(s => s.SelectedOption.StudentId.HasValue)
.Select(s => new SeatAssignment
{
Row = s.Row,
Column = s.Column,
StudentId = s.SelectedOption.StudentId!.Value,
}).ToList();
_plans.Save(_currentPlan);
UpdateAssignmentSummary();
}
private void UpdateAssignmentSummary()
{
var assigned = Seats.Count(s => s.SelectedOption.StudentId.HasValue);
var total = StudentOptions.Count - 1;
AssignmentSummary = $"{assigned} von {total} Schülern zugeordnet";
}
[RelayCommand(CanExecute = nameof(CanEdit))]
private async Task AddPlan()
{
if (OnEditPlan is null) return;
var plan = await OnEditPlan(null);
if (plan is not null) ReloadPlans(plan.Id);
}
[RelayCommand(CanExecute = nameof(CanEditSelected))]
private async Task EditPlan()
{
if (_currentPlan is null || OnEditPlan is null) return;
var plan = await OnEditPlan(_currentPlan);
if (plan is not null) ReloadPlans(plan.Id);
}
[RelayCommand(CanExecute = nameof(CanEditSelected))]
private async Task DeletePlan()
{
if (SelectedPlan is null || OnConfirmDelete is null || !await OnConfirmDelete(SelectedPlan)) return;
_plans.Delete(SelectedPlan.Id);
ReloadPlans();
}
private bool CanEdit() => IsEditable;
private bool CanEditSelected() => IsEditable && _currentPlan is not null;
private void NotifyCommands()
{
AddPlanCommand.NotifyCanExecuteChanged();
EditPlanCommand.NotifyCanExecuteChanged();
DeletePlanCommand.NotifyCanExecuteChanged();
}
}
public sealed class SeatingPlanSummary
{
public Guid Id { get; }
public string Name { get; }
public string RoomDisplay { get; }
public SeatingPlanSummary(SeatingPlan plan)
{
Id = plan.Id;
Name = plan.Name;
RoomDisplay = string.IsNullOrWhiteSpace(plan.Room) ? "Kein Raum" : $"Raum {plan.Room}";
}
}
public sealed record StudentSeatOption(Guid? StudentId, string DisplayName)
{
public static StudentSeatOption Empty { get; } = new(null, "— frei —");
}
public partial class SeatCellViewModel : ObservableObject
{
private readonly Action<SeatCellViewModel> _onChanged;
private bool _suppressChange;
[ObservableProperty] private StudentSeatOption _selectedOption;
public int Row { get; }
public int Column { get; }
public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}";
public ObservableCollection<StudentSeatOption> Options { get; }
public bool CanEdit { get; }
public SeatCellViewModel(int row, int column, ObservableCollection<StudentSeatOption> options,
StudentSeatOption selectedOption, Action<SeatCellViewModel> onChanged, bool canEdit)
{
Row = row;
Column = column;
Options = options;
_selectedOption = selectedOption;
_onChanged = onChanged;
CanEdit = canEdit;
}
partial void OnSelectedOptionChanged(StudentSeatOption value)
{
if (!_suppressChange) _onChanged(this);
}
public void SetSelectionSilently(StudentSeatOption option)
{
_suppressChange = true;
SelectedOption = option;
_suppressChange = false;
}
}
public partial class SeatingPlanDialogViewModel : ObservableObject
{
private readonly ISeatingPlanRepository _plans;
private readonly Guid _groupId;
private readonly SeatingPlan? _editingPlan;
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _room = "";
[ObservableProperty] private decimal _rows = 4;
[ObservableProperty] private decimal _columns = 4;
[ObservableProperty] private string _nameError = "";
[ObservableProperty] private string _layoutError = "";
public SeatingPlan? Result { get; private set; }
public string DialogTitle => _editingPlan is null ? "Neuen Sitzplan anlegen" : "Sitzplan bearbeiten";
public string SaveButtonText => _editingPlan is null ? "Anlegen" : "Speichern";
public SeatingPlanDialogViewModel(ISeatingPlanRepository plans, Guid groupId, SeatingPlan? editingPlan)
{
_plans = plans;
_groupId = groupId;
_editingPlan = editingPlan;
if (editingPlan is null) return;
Name = editingPlan.Name;
Room = editingPlan.Room;
Rows = editingPlan.Rows;
Columns = editingPlan.Columns;
}
[RelayCommand]
private void Save()
{
NameError = "";
LayoutError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(Name))
{
NameError = "Bitte einen Namen eingeben.";
valid = false;
}
if (Rows is < 1 or > 10 || Columns is < 1 or > 10)
{
LayoutError = "Reihen und Plätze müssen zwischen 1 und 10 liegen.";
valid = false;
}
if (!valid) return;
var plan = _editingPlan ?? new SeatingPlan { GroupId = _groupId };
plan.Name = Name.Trim();
plan.Room = Room.Trim();
plan.Rows = decimal.ToInt32(Rows);
plan.Columns = decimal.ToInt32(Columns);
try
{
_plans.Save(plan);
Result = plan;
}
catch (InvalidOperationException ex)
{
NameError = ex.Message;
}
}
}
@@ -101,6 +101,11 @@
</DataGrid>
</ContentPage>
<!-- Tab: Sitzpläne -->
<ContentPage Header="Sitzpläne">
<views:SeatingPlanTabView DataContext="{Binding SeatingPlanTab}"/>
</ContentPage>
<!-- Tab: Mitarbeit -->
<ContentPage Header="Mitarbeit">
<views:ParticipationTabView DataContext="{Binding ParticipationTab}"/>
@@ -0,0 +1,55 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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">
<Grid RowDefinitions="*,Auto" Margin="24">
<StackPanel Spacing="16">
<StackPanel Spacing="3">
<TextBlock Text="{Binding DialogTitle}" Classes="dialogtitle"/>
<TextBlock Text="Name und Raum unterscheiden mehrere Pläne derselben Lerngruppe."
FontSize="12" Opacity="0.6" TextWrapping="Wrap"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Name *" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Name}" PlaceholderText="z. B. Standard, Gruppenarbeit"/>
<TextBlock Text="{Binding NameError}" Foreground="Red" FontSize="11"
IsVisible="{Binding NameError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Text="Raum" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Room}" PlaceholderText="z. B. B204, Physikraum"/>
</StackPanel>
<Grid ColumnDefinitions="*,16,*">
<StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Reihen" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding Rows}" Minimum="1" Maximum="10" Increment="1"
FormatString="0"/>
</StackPanel>
<StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Plätze pro Reihe" FontSize="12" Opacity="0.7"/>
<NumericUpDown Value="{Binding Columns}" Minimum="1" Maximum="10" Increment="1"
FormatString="0"/>
</StackPanel>
</Grid>
<TextBlock Text="{Binding LayoutError}" Foreground="Red" FontSize="11"
IsVisible="{Binding LayoutError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<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>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="{Binding SaveButtonText}" HorizontalAlignment="Stretch" Click="OnSave"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class SeatingPlanDialog : Window
{
public SeatingPlanDialog() => InitializeComponent();
private void OnSave(object? sender, RoutedEventArgs e)
{
if (DataContext is not SeatingPlanDialogViewModel vm) return;
vm.SaveCommand.Execute(null);
if (vm.Result is not null) Close(true);
}
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
}
@@ -0,0 +1,96 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.SeatingPlanTabView"
x:DataType="vm:SeatingPlanTabViewModel">
<Grid ColumnDefinitions="260,*">
<Border Grid.Column="0" BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="0,0,1,0" Padding="16">
<Grid RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Row="0" Spacing="4" Margin="0,0,0,12">
<TextBlock Text="Sitzpläne" FontSize="18" FontWeight="SemiBold"/>
<TextBlock Text="Mehrere Pläne, z. B. für verschiedene Räume" FontSize="12"
Opacity="0.6" TextWrapping="Wrap"/>
</StackPanel>
<ListBox Grid.Row="1" ItemsSource="{Binding Plans}" SelectedItem="{Binding SelectedPlan}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SeatingPlanSummary">
<StackPanel Margin="4,6" Spacing="2">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding RoomDisplay}" FontSize="12" Opacity="0.6"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="2" Spacing="8" Margin="0,12,0,0">
<Button Content=" Sitzplan" Command="{Binding AddPlanCommand}" HorizontalAlignment="Stretch"/>
<Grid ColumnDefinitions="*,8,*" IsVisible="{Binding HasSelectedPlan}">
<Button Grid.Column="0" Content="Bearbeiten" Command="{Binding EditPlanCommand}"
HorizontalAlignment="Stretch"/>
<Button Grid.Column="2" Content="Löschen" Command="{Binding DeletePlanCommand}"
HorizontalAlignment="Stretch"/>
</Grid>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Column="1">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10"
IsVisible="{Binding HasPlans, Converter={x:Static BoolConverters.Not}}">
<TextBlock Text="Noch kein Sitzplan" FontSize="20" FontWeight="SemiBold"
HorizontalAlignment="Center"/>
<TextBlock Text="Lege einen Plan an und ordne die Schüler den Plätzen zu."
Opacity="0.6" TextAlignment="Center"/>
<Button Content="Ersten Sitzplan anlegen" Command="{Binding AddPlanCommand}"
HorizontalAlignment="Center"/>
</StackPanel>
<Grid RowDefinitions="Auto,*" IsVisible="{Binding HasSelectedPlan}" Margin="24">
<Grid Grid.Row="0" 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>
<TextBlock Grid.Column="1" Text="{Binding AssignmentSummary}" VerticalAlignment="Bottom"
FontSize="12" Opacity="0.6"/>
</Grid>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="14" HorizontalAlignment="Center">
<Border Background="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}"
CornerRadius="6" Padding="36,8" HorizontalAlignment="Center">
<TextBlock Text="Tafel / Vorderseite" FontWeight="SemiBold" Opacity="0.75"/>
</Border>
<ItemsControl ItemsSource="{Binding Seats}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="{Binding PlanColumns}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:SeatCellViewModel">
<Border Width="190" MinHeight="76" Margin="5" Padding="10"
BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
BorderThickness="1" CornerRadius="7">
<StackPanel Spacing="5">
<TextBlock Text="{Binding PositionLabel}" FontSize="10" Opacity="0.5"/>
<ComboBox ItemsSource="{Binding Options}" SelectedItem="{Binding SelectedOption}"
IsEnabled="{Binding CanEdit}" HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:StudentSeatOption">
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</UserControl>
@@ -0,0 +1,46 @@
using Avalonia.Controls;
using LehrerApp.Core.Models;
using LehrerApp.Desktop.ViewModels.Groups;
using LehrerApp.Desktop.Views.Shared;
namespace LehrerApp.Desktop.Views.Groups;
public partial class SeatingPlanTabView : UserControl
{
public SeatingPlanTabView() => InitializeComponent();
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is SeatingPlanTabViewModel vm)
{
vm.OnEditPlan = ShowPlanDialog;
vm.OnConfirmDelete = ShowDeleteConfirmDialog;
}
}
private async Task<SeatingPlan?> ShowPlanDialog(SeatingPlan? plan)
{
if (DataContext is not SeatingPlanTabViewModel vm) return null;
var dialogVm = vm.CreateDialogViewModel(plan);
var dialog = new SeatingPlanDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null || !await dialog.ShowDialog<bool>(owner)) return null;
return dialogVm.Result;
}
private async Task<bool> ShowDeleteConfirmDialog(SeatingPlanSummary plan)
{
var dialog = new ConfirmDialog
{
DataContext = new ConfirmDialogInfo
{
Title = "Sitzplan löschen?",
Message = $"Der Sitzplan „{plan.Name}“ und seine Zuordnungen werden gelöscht.",
ConfirmText = "Löschen",
},
};
var owner = TopLevel.GetTopLevel(this) as Window;
return owner is not null && await dialog.ShowDialog<bool>(owner);
}
}