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 readonly IParticipationSessionRepository _sessions; private readonly IParticipationRepository _participation; private readonly IParticipationAspectRepository _aspects; 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 = ""; [ObservableProperty] private bool _isBoardAtTop = true; [ObservableProperty] private bool _isBoardAtBottom; [ObservableProperty] private IReadOnlyList _columnGapWidths = []; [ObservableProperty] private bool _isEditMode; public ObservableCollection Plans { get; } = []; public ObservableCollection Seats { get; } = []; public ObservableCollection StudentOptions { get; } = []; public ObservableCollection UnassignedStudents { get; } = []; public bool HasPlans => Plans.Count > 0; public bool HasSelectedPlan => _currentPlan is not null; public bool IsEditable => !_isReadOnly; public bool CanEditLayout => IsEditable && IsEditMode; public Func>? OnEditPlan { get; set; } public Func>? OnConfirmDelete { get; set; } public Func? OnAssessStudent { get; set; } public Action? OnAssessmentChanged { get; set; } public SeatingPlanTabViewModel(ISeatingPlanRepository plans, IStudentRepository students, IGroupMembershipRepository memberships, IParticipationSessionRepository sessions, IParticipationRepository participation, IParticipationAspectRepository aspects) { _plans = plans; _students = students; _memberships = memberships; _sessions = sessions; _participation = participation; _aspects = aspects; } public SeatingPlanDialogViewModel CreateDialogViewModel(SeatingPlan? plan) => new(_plans, _groupId, plan); public void Initialize(Guid groupId, bool isReadOnly) { _groupId = groupId; _isReadOnly = isReadOnly; IsEditMode = false; 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) { UnassignedStudents.Clear(); PlanColumns = 1; PlanTitle = ""; PlanSubtitle = ""; AssignmentSummary = ""; IsBoardAtTop = true; IsBoardAtBottom = false; ColumnGapWidths = []; } 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"; 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++) { 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, CanEditLayout)); } UpdateAssignmentSummary(); } OnPropertyChanged(nameof(HasSelectedPlan)); NotifyCommands(); } private void OnSeatChanged(SeatCellViewModel changed) { 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)) other.SetSelectionSilently(StudentSeatOption.Empty); } SaveSeatAssignments(); } private void UpdateAssignmentSummary() { var assigned = Seats.Count(s => s.SelectedOption.StudentId.HasValue); var total = StudentOptions.Count - 1; AssignmentSummary = $"{assigned} von {total} Schülern zugeordnet"; var assignedIds = Seats.Where(s => s.SelectedOption.StudentId.HasValue) .Select(s => s.SelectedOption.StudentId!.Value).ToHashSet(); UnassignedStudents.Clear(); foreach (var option in StudentOptions.Where(o => o.StudentId.HasValue && !assignedIds.Contains(o.StudentId.Value))) UnassignedStudents.Add(option); } public void MoveSeat(SeatCellViewModel source, SeatCellViewModel target) { if (!CanEditLayout || source == target || !source.SelectedOption.StudentId.HasValue) return; var targetOption = target.SelectedOption; target.SetSelectionSilently(source.SelectedOption); source.SetSelectionSilently(targetOption); SaveSeatAssignments(); } public void AssignStudent(StudentSeatOption student, SeatCellViewModel target) { 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); SaveSeatAssignments(); } public void ClearSeat(SeatCellViewModel seat) { if (!CanEditLayout || !seat.SelectedOption.StudentId.HasValue) return; seat.SetSelectionSilently(StudentSeatOption.Empty); SaveSeatAssignments(); } private void SaveSeatAssignments() { if (_currentPlan is null) return; _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(); } public async Task AssessStudent(SeatCellViewModel seat) { if (!seat.SelectedOption.StudentId.HasValue || OnAssessStudent is null) return; var assessment = new SeatAssessmentViewModel(_sessions, _participation, _aspects, _groupId, seat.SelectedOption.StudentId.Value, seat.SelectedOption.DisplayName, IsEditable); await OnAssessStudent(assessment); OnAssessmentChanged?.Invoke(); } [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(); } 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() { 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 _onChanged; private bool _suppressChange; [ObservableProperty] private StudentSeatOption _selectedOption; [ObservableProperty] private bool _isDropTarget; public int Row { get; } public int Column { get; } public string PositionLabel => $"Reihe {Row + 1} · Platz {Column + 1}"; public ObservableCollection Options { get; } [ObservableProperty] private bool _canEdit; public bool IsOccupied => SelectedOption.StudentId.HasValue; public string StudentName => IsOccupied ? SelectedOption.DisplayName : "Freier Platz"; public SeatCellViewModel(int row, int column, ObservableCollection options, StudentSeatOption selectedOption, Action onChanged, bool canEdit) { Row = row; Column = column; Options = options; _selectedOption = selectedOption; _onChanged = onChanged; _canEdit = canEdit; } partial void OnSelectedOptionChanged(StudentSeatOption value) { OnPropertyChanged(nameof(IsOccupied)); OnPropertyChanged(nameof(StudentName)); if (!_suppressChange) _onChanged(this); } public void SetSelectionSilently(StudentSeatOption option) { _suppressChange = true; SelectedOption = option; _suppressChange = false; } } public partial class SeatAssessmentViewModel : ObservableObject { private readonly IParticipationRepository _entries; private readonly ParticipationEntry? _entry; private readonly bool _canEdit; [ObservableProperty] private int _selectedAspectIndex; [ObservableProperty] private string _attendanceLabel = "Noch nicht kontrolliert"; [ObservableProperty] private string _homeworkLabel = "Keine Hausaufgabe aufgegeben"; public string StudentName { get; } public string SessionDisplay { get; } public bool CanEdit => _canEdit && _entry is not null; public string ReadOnlyHint => _entry is null ? "Für heute existiert keine Sitzung. In einer archivierten Gruppe kann keine neue angelegt werden." : "Archivierte Lerngruppe – Bewertung nur ansehen."; public ObservableCollection AspectRows { get; } = []; public ObservableCollection AttendanceChoices { get; } = []; public ObservableCollection HomeworkChoices { get; } = []; public SeatAssessmentViewModel(IParticipationSessionRepository sessions, IParticipationRepository entries, IParticipationAspectRepository aspects, Guid groupId, Guid studentId, string studentName, bool canEdit) { _entries = entries; _canEdit = canEdit; StudentName = studentName; var today = DateOnly.FromDateTime(DateTime.Today); var session = sessions.GetByGroup(groupId).FirstOrDefault(s => s.Date == today); if (session is null && canEdit) { session = new ParticipationSession { GroupId = groupId, Date = today, Comment = "Sitzplan", }; sessions.Save(session); } SessionDisplay = session is null ? "Keine Sitzung für heute" : $"{session.Date:dd.MM.yyyy} · {session.Comment}"; _entry = session is null ? null : entries.GetBySessionAndStudent(session.Id, studentId) ?? new ParticipationEntry { SessionId = session.Id, StudentId = studentId }; var aspectDefinitions = aspects.GetDefaults().Concat(aspects.GetByGroup(groupId)).ToList(); if (aspectDefinitions.Count == 0) aspectDefinitions = DefaultParticipationAspects.All.Select(a => new ParticipationAspect { Key = a.Key, Label = a.Label, ValueType = a.ValueType, MaxPoints = a.MaxPoints, }).ToList(); foreach (var (aspect, index) in aspectDefinitions.Select((a, i) => (a, i))) { var value = _entry?.Ratings.FirstOrDefault(r => r.Key == aspect.Key)?.Value; AspectRows.Add(new SeatAssessmentAspectRow(index, aspect, value, ApplyRating)); } BuildAttendanceChoices(); BuildHomeworkChoices(); RefreshStatusChoices(); SelectAspect(0); } private void BuildAttendanceChoices() { AttendanceChoices.Add(new("✓", "Anwesend", "Strg+1", AttendanceStatus.Present, SetAttendance)); AttendanceChoices.Add(new("?", "Entschuldigung offen", "Strg+2", AttendanceStatus.ExcusePending, SetAttendance)); AttendanceChoices.Add(new("⊘", "Entschuldigt", "Strg+5", AttendanceStatus.Excused, SetAttendance)); 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)); } private void BuildHomeworkChoices() { HomeworkChoices.Add(new("✓", "Gemacht", "⌥1", HomeworkStatus.Completed, SetHomework)); HomeworkChoices.Add(new("◐", "Teilweise", "⌥3", HomeworkStatus.PartiallyCompleted, SetHomework)); HomeworkChoices.Add(new("◕", "Rest nachgereicht", "⌥4", HomeworkStatus.PartialSubmittedLate, SetHomework)); HomeworkChoices.Add(new("◒", "Rest fehlt", "⌥5", HomeworkStatus.PartialMissingOverdue, SetHomework)); HomeworkChoices.Add(new("!", "Nicht gemacht", "⌥7", HomeworkStatus.MissingOpen, SetHomework)); HomeworkChoices.Add(new("↺", "Nachgereicht", "⌥8", HomeworkStatus.SubmittedLate, SetHomework)); HomeworkChoices.Add(new("✕", "Nicht nachgereicht", "⌥0", HomeworkStatus.MissingOverdue, SetHomework)); HomeworkChoices.Add(new("·", "Keine aufgegeben", "⌥X", null, SetHomework)); } public void SelectAspect(int index) { if (index < 0 || index >= AspectRows.Count) return; SelectedAspectIndex = index; foreach (var row in AspectRows) row.IsActive = row.Index == index; } public void MoveAspect(int delta) { if (AspectRows.Count == 0) return; SelectAspect(Math.Clamp(SelectedAspectIndex + delta, 0, AspectRows.Count - 1)); } public void SetRatingByNumber(int number) { if (!CanEdit) return; var row = AspectRows.ElementAtOrDefault(SelectedAspectIndex); if (row is null) return; if (row.ValueType == AspectValueType.Points) row.ApplyValue(Math.Clamp(number, 0, row.MaxPoints)); else { var steps = ParticipationRatingScale.Steps(row.ValueType); if (number >= 1 && number <= steps.Count) row.ApplyValue(steps[number - 1].Value); } } public void AdjustCurrentRating(int delta) { if (!CanEdit) return; var row = AspectRows.ElementAtOrDefault(SelectedAspectIndex); row?.Adjust(delta); } public void ClearCurrentRating() { if (!CanEdit) return; AspectRows.ElementAtOrDefault(SelectedAspectIndex)?.ApplyValue(null); } public void ApplyAttendanceShortcut(int? digit, bool clear) { if (!CanEdit) return; var status = clear ? null : digit switch { 1 => AttendanceStatus.Present, 2 => AttendanceStatus.ExcusePending, 5 => AttendanceStatus.Excused, 7 => AttendanceStatus.OtherSchoolEvent, 9 => AttendanceStatus.Truant, 0 => AttendanceStatus.Unexcused, _ => (AttendanceStatus?)null, }; if (clear || digit is 0 or 1 or 2 or 5 or 7 or 9) SetAttendance(status); } public void ApplyHomeworkShortcut(int? digit, bool clear) { if (!CanEdit) return; var status = clear ? null : digit switch { 1 => HomeworkStatus.Completed, 3 => HomeworkStatus.PartiallyCompleted, 4 => HomeworkStatus.PartialSubmittedLate, 5 => HomeworkStatus.PartialMissingOverdue, 7 => HomeworkStatus.MissingOpen, 8 => HomeworkStatus.SubmittedLate, 0 => HomeworkStatus.MissingOverdue, _ => (HomeworkStatus?)null, }; if (clear || digit is 0 or 1 or 3 or 4 or 5 or 7 or 8) SetHomework(status); } private void ApplyRating(string key, int? value) { if (!CanEdit || _entry is null) return; var existing = _entry.Ratings.FirstOrDefault(r => r.Key == key); if (value is null) { if (existing is not null) _entry.Ratings.Remove(existing); } else if (existing is null) _entry.Ratings.Add(new AspectRating { Key = key, Value = value.Value }); else existing.Value = value.Value; _entries.Save(_entry); } private void SetAttendance(AttendanceStatus? status) { if (!CanEdit || _entry is null) return; _entry.Attendance = status; _entries.Save(_entry); RefreshStatusChoices(); } private void SetHomework(HomeworkStatus? status) { if (!CanEdit || _entry is null) return; _entry.Homework = status; _entry.HomeworkMissing = HomeworkDisplay.CountsAsMissing(status); _entries.Save(_entry); RefreshStatusChoices(); } private void RefreshStatusChoices() { AttendanceLabel = AttendanceDisplay.Label(_entry?.Attendance); HomeworkLabel = HomeworkDisplay.Label(_entry is null ? null : HomeworkDisplay.Effective(_entry)); foreach (var choice in AttendanceChoices) choice.IsSelected = choice.Status == _entry?.Attendance; var homework = _entry is null ? null : HomeworkDisplay.Effective(_entry); foreach (var choice in HomeworkChoices) choice.IsSelected = choice.Status == homework; } } public partial class SeatAssessmentAspectRow : ObservableObject { private readonly Action _apply; [ObservableProperty] private bool _isActive; [ObservableProperty] private int? _value; public int Index { get; } public string Key { get; } public string Label { get; } public AspectValueType ValueType { get; } public int MaxPoints { get; } public string Shortcut => Index switch { 0 => "Q", 1 => "W", 2 => "E", 3 => "R", 4 => "T", _ => "" }; public string DisplayValue => ParticipationRatingScale.DisplayLabel(ValueType, Value); public ObservableCollection Choices { get; } = []; public SeatAssessmentAspectRow(int index, ParticipationAspect aspect, int? value, Action apply) { Index = index; Key = aspect.Key; Label = aspect.Label; ValueType = aspect.ValueType; MaxPoints = aspect.MaxPoints; _value = value; _apply = apply; var steps = ValueType == AspectValueType.Points ? Enumerable.Range(0, Math.Min(MaxPoints, 9) + 1).Select(v => (v, v.ToString())).ToList() : ParticipationRatingScale.Steps(ValueType).ToList(); foreach (var (step, i) in steps.Select((s, i) => (s, i))) Choices.Add(new SeatRatingChoice(step.Item2, ValueType == AspectValueType.Points ? step.Item1.ToString() : (i + 1).ToString(), step.Item1, step.Item1 == value, ApplyValue)); } public void ApplyValue(int? value) { Value = value; OnPropertyChanged(nameof(DisplayValue)); foreach (var choice in Choices) choice.IsSelected = choice.Value == value; _apply(Key, value); } public void Adjust(int delta) { if (ValueType == AspectValueType.Points) { ApplyValue(Math.Clamp((Value ?? (delta > 0 ? -1 : MaxPoints + 1)) + delta, 0, MaxPoints)); return; } var steps = ParticipationRatingScale.Steps(ValueType).Select(s => s.Value).ToList(); if (steps.Count == 0) return; var index = Value.HasValue ? steps.IndexOf(Value.Value) : (delta > 0 ? -1 : steps.Count); ApplyValue(steps[Math.Clamp(index + delta, 0, steps.Count - 1)]); } [RelayCommand] private void Clear() => ApplyValue(null); [RelayCommand] private void Increment() => Adjust(1); [RelayCommand] private void Decrement() => Adjust(-1); } public partial class SeatRatingChoice(string label, string shortcut, int value, bool isSelected, Action apply) : ObservableObject { public string Label { get; } = label; public string Shortcut { get; } = shortcut; public int Value { get; } = value; [ObservableProperty] private bool _isSelected = isSelected; [RelayCommand] private void Apply() => apply(Value); } public partial class SeatAttendanceChoice(string symbol, string label, string shortcut, AttendanceStatus? status, Action apply) : ObservableObject { public string Symbol { get; } = symbol; public string Label { get; } = label; public string Shortcut { get; } = shortcut; public AttendanceStatus? Status { get; } = status; [ObservableProperty] private bool _isSelected; [RelayCommand] private void Apply() => apply(Status); } public partial class SeatHomeworkChoice(string symbol, string label, string shortcut, HomeworkStatus? status, Action apply) : ObservableObject { public string Symbol { get; } = symbol; public string Label { get; } = label; public string Shortcut { get; } = shortcut; public HomeworkStatus? Status { get; } = status; [ObservableProperty] private bool _isSelected; [RelayCommand] private void Apply() => apply(Status); } 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 bool _isBoardAtBottom; [ObservableProperty] private string _nameError = ""; [ObservableProperty] private string _layoutError = ""; public SeatingPlan? Result { get; private set; } public ObservableCollection ColumnGaps { get; } = []; 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; 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] 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); plan.IsBoardAtBottom = IsBoardAtBottom; plan.ColumnGapWidths = ColumnGaps.Select(g => decimal.ToDouble(g.Width)).ToList(); try { _plans.Save(plan); Result = plan; } catch (InvalidOperationException ex) { NameError = ex.Message; } } } 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; }