From b2c1b151672bfc07649c22b098ed3a4abefc7906 Mon Sep 17 00:00:00 2001 From: Sebastian Hedtrich Date: Wed, 19 Aug 2026 12:27:12 +0200 Subject: [PATCH] Neue Abwensenheitsmodi plus Upgrade Sitzplan --- LehrerApp.Core/Models/Participation.cs | 5 + LehrerApp.Core/Models/SeatingPlan.cs | 10 ++ .../Services/AttendanceBalanceService.cs | 9 +- LehrerApp.Data.Tests/RepositoryTests.cs | 6 +- .../Repositories/AllRepositories.cs | 7 ++ .../QuickInputViewModelTests.cs | 18 +++ .../SeatingPlanViewModelTests.cs | 104 ++++++++++++++++++ .../Groups/ParticipationViewModels.cs | 20 +++- .../Groups/SeatingPlanViewModels.cs | 71 ++++++++++-- .../AttendanceHomeworkQuickInputDialog.axaml | 2 +- ...ttendanceHomeworkQuickInputDialog.axaml.cs | 7 ++ .../Views/Groups/SeatingPlanDialog.axaml | 32 +++++- .../Views/Groups/SeatingPlanPanel.cs | 77 +++++++++++++ .../Views/Groups/SeatingPlanTabView.axaml | 27 +++-- .../Views/Groups/SeatingPlanTabView.axaml.cs | 4 +- .../AttendanceBalanceServiceTests.cs | 14 ++- 16 files changed, 383 insertions(+), 30 deletions(-) create mode 100644 LehrerApp.Desktop/Views/Groups/SeatingPlanPanel.cs diff --git a/LehrerApp.Core/Models/Participation.cs b/LehrerApp.Core/Models/Participation.cs index dd3833c..a6d8b27 100644 --- a/LehrerApp.Core/Models/Participation.cs +++ b/LehrerApp.Core/Models/Participation.cs @@ -60,6 +60,11 @@ public enum AttendanceStatus OtherSchoolEvent, // Hinten angefügt, damit die numerischen Werte bereits gespeicherter LiteDB-Einträge stabil bleiben. Present, + Late, + SignificantlyLate, + LeftDuringClass, + LearningIsland, + Suspended, } /// diff --git a/LehrerApp.Core/Models/SeatingPlan.cs b/LehrerApp.Core/Models/SeatingPlan.cs index 28840b3..55ded55 100644 --- a/LehrerApp.Core/Models/SeatingPlan.cs +++ b/LehrerApp.Core/Models/SeatingPlan.cs @@ -12,6 +12,16 @@ public class SeatingPlan public string Room { get; set; } = ""; public int Rows { get; set; } = 4; public int Columns { get; set; } = 4; + /// + /// Zeigt die Tafel unterhalb des Sitzrasters an. Der Standardwert false + /// erhält für bestehende Pläne die bisherige Darstellung oberhalb des Rasters. + /// + public bool IsBoardAtBottom { get; set; } + /// + /// Zusätzlicher horizontaler Abstand nach jeder Spalte, in Pixeln. Der Eintrag mit Index 0 + /// liegt zwischen der ersten und zweiten Spalte. Fehlende Einträge bedeuten keinen Abstand. + /// + public List ColumnGapWidths { get; set; } = []; public List Assignments { get; set; } = []; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; diff --git a/LehrerApp.Core/Services/AttendanceBalanceService.cs b/LehrerApp.Core/Services/AttendanceBalanceService.cs index 2b894bb..44b1ca6 100644 --- a/LehrerApp.Core/Services/AttendanceBalanceService.cs +++ b/LehrerApp.Core/Services/AttendanceBalanceService.cs @@ -25,10 +25,13 @@ public class AttendanceBalanceService .Select(e => e.Status!.Value) .ToList(); - var present = relevant.Count(s => s == AttendanceStatus.Present); + var present = relevant.Count(s => s is AttendanceStatus.Present + or AttendanceStatus.Late or AttendanceStatus.SignificantlyLate); var excused = relevant.Count(s => s == AttendanceStatus.Excused); - var unexcused = relevant.Count(s => s is AttendanceStatus.Unexcused or AttendanceStatus.Truant); - var schoolEvent = relevant.Count(s => s == AttendanceStatus.OtherSchoolEvent); + var unexcused = relevant.Count(s => s is AttendanceStatus.Unexcused + or AttendanceStatus.Truant or AttendanceStatus.LeftDuringClass); + var schoolEvent = relevant.Count(s => s is AttendanceStatus.OtherSchoolEvent + or AttendanceStatus.LearningIsland or AttendanceStatus.Suspended); var pending = relevant.Count(s => s == AttendanceStatus.ExcusePending); var total = relevant.Count; diff --git a/LehrerApp.Data.Tests/RepositoryTests.cs b/LehrerApp.Data.Tests/RepositoryTests.cs index d2eb948..fac6f39 100644 --- a/LehrerApp.Data.Tests/RepositoryTests.cs +++ b/LehrerApp.Data.Tests/RepositoryTests.cs @@ -267,6 +267,8 @@ public sealed class RepositoryTests repo.Save(new SeatingPlan { GroupId = group.Id, Name = "Standard", Room = "Physik", Rows = 2, Columns = 3, + IsBoardAtBottom = true, + ColumnGapWidths = [0, 90], Assignments = [new SeatAssignment { Row = 1, Column = 2, StudentId = ben.Id }], }); @@ -274,7 +276,9 @@ public sealed class RepositoryTests Assert.Equal(2, plans.Count); Assert.Contains(plans, p => p.Room == "B204" && p.Assignments.Single().StudentId == anna.Id); - Assert.Contains(plans, p => p.Room == "Physik" && p.Assignments.Single().StudentId == ben.Id); + Assert.Contains(plans, p => p.Room == "Physik" && p.IsBoardAtBottom + && p.ColumnGapWidths.SequenceEqual([0, 90]) + && p.Assignments.Single().StudentId == ben.Id); } [Fact] diff --git a/LehrerApp.Data/Repositories/AllRepositories.cs b/LehrerApp.Data/Repositories/AllRepositories.cs index 65553d9..937a99f 100644 --- a/LehrerApp.Data/Repositories/AllRepositories.cs +++ b/LehrerApp.Data/Repositories/AllRepositories.cs @@ -107,6 +107,7 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository plan.Name = plan.Name?.Trim() ?? ""; plan.Room = plan.Room?.Trim() ?? ""; plan.Assignments ??= []; + plan.ColumnGapWidths ??= []; if (plan.Name.Length == 0) throw new ArgumentException("Der Name des Sitzplans darf nicht leer sein."); if (plan.Rows is < 1 or > 10 || plan.Columns is < 1 or > 10) @@ -114,6 +115,12 @@ public class SeatingPlanRepository(LiteDbContext db) : ISeatingPlanRepository if (db.Groups.FindById(plan.GroupId) is null) throw new InvalidOperationException("Die zugehörige Lerngruppe existiert nicht."); + plan.ColumnGapWidths = Enumerable.Range(0, Math.Max(0, plan.Columns - 1)) + .Select(i => i < plan.ColumnGapWidths.Count && double.IsFinite(plan.ColumnGapWidths[i]) + ? Math.Clamp(plan.ColumnGapWidths[i], 0, 300) + : 0) + .ToList(); + var duplicateName = db.SeatingPlans.Find(p => p.GroupId == plan.GroupId) .FirstOrDefault(p => p.Id != plan.Id && string.Equals(p.Name, plan.Name, StringComparison.OrdinalIgnoreCase) diff --git a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs index b291c71..fb75fdd 100644 --- a/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/QuickInputViewModelTests.cs @@ -6,6 +6,24 @@ namespace LehrerApp.Desktop.Tests; public sealed class QuickInputViewModelTests { + [Theory] + [InlineData(AttendanceStatus.Late, false, "Verspätet", "V")] + [InlineData(AttendanceStatus.SignificantlyLate, false, "Erheblich verspätet", "V!")] + [InlineData(AttendanceStatus.LeftDuringClass, true, "Während des Unterrichts abgängig", "A")] + [InlineData(AttendanceStatus.LearningIsland, true, "Lerninsel", "L")] + [InlineData(AttendanceStatus.Suspended, true, "Suspendiert", "S")] + public void ZusaetzlicheAnwesenheitsstatus_HabenAnzeigeUndPassendeAbwesenheitswertung( + AttendanceStatus status, bool isAbsent, string label, string symbol) + { + var row = new ParticipationStudentRow(Guid.NewGuid(), "Anna", + new ParticipationEntry { Attendance = status }, [], []); + + Assert.Equal(isAbsent, row.IsAbsent); + Assert.Equal(label, row.AttendanceTooltip); + Assert.Equal(symbol, row.AttendanceLabel); + Assert.False(string.IsNullOrWhiteSpace(AttendanceDisplay.Color(status))); + } + [Fact] public void ZurueckZuVorherigemSchueler_ZeigtBereitsGesetzteBewertung() { diff --git a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs index 504797d..106a5fc 100644 --- a/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs +++ b/LehrerApp.Desktop.Tests/SeatingPlanViewModelTests.cs @@ -1,11 +1,41 @@ using LehrerApp.Core.Models; using LehrerApp.Desktop.ViewModels.Groups; +using LehrerApp.Desktop.Views.Groups; using Xunit; namespace LehrerApp.Desktop.Tests; public sealed class SeatingPlanViewModelTests { + [Fact] + public void TafelpositionWirdAusPlanGeladenUndBeimBearbeitenGespeichert() + { + var groupId = Guid.NewGuid(); + var plan = new SeatingPlan + { + GroupId = groupId, + Name = "Standard", + Rows = 1, + Columns = 1, + IsBoardAtBottom = true, + }; + var plans = new FakeSeatingPlans([plan]); + var tabVm = new SeatingPlanTabViewModel( + plans, new FakeStudents([]), new FakeMemberships([]), + new FakeSessions([]), new FakeEntries(), new FakeAspects()); + + tabVm.Initialize(groupId, isReadOnly: false); + + Assert.True(tabVm.IsBoardAtBottom); + Assert.False(tabVm.IsBoardAtTop); + + var dialogVm = tabVm.CreateDialogViewModel(plan); + dialogVm.IsBoardAtBottom = false; + dialogVm.SaveCommand.Execute(null); + + Assert.False(plans.GetById(plan.Id)!.IsBoardAtBottom); + } + [Fact] public void AuswahlEinesBereitsZugeordnetenSchuelers_VerschiebtIhnAufDenNeuenPlatz() { @@ -27,6 +57,7 @@ public sealed class SeatingPlanViewModelTests var vm = new SeatingPlanTabViewModel(plans, students, memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects()); vm.Initialize(groupId, isReadOnly: false); + vm.IsEditMode = true; vm.Seats[1].SelectedOption = vm.StudentOptions.Single(o => o.StudentId == student.Id); @@ -75,6 +106,7 @@ public sealed class SeatingPlanViewModelTests var vm = new SeatingPlanTabViewModel(plans, students, memberships, new FakeSessions([]), new FakeEntries(), new FakeAspects()); vm.Initialize(groupId, isReadOnly: false); + vm.IsEditMode = true; vm.MoveSeat(vm.Seats[0], vm.Seats[1]); @@ -83,6 +115,78 @@ public sealed class SeatingPlanViewModelTests Assert.Equal(2, plans.GetById(plan.Id)!.Assignments.Count); } + [Fact] + public void Ansichtsmodus_VerhindertVerschiebenUndBlendetBearbeitungsbefehleAus() + { + var groupId = Guid.NewGuid(); + var student = new Student { FirstName = "Anna", LastName = "A" }; + var plan = new SeatingPlan + { + GroupId = groupId, Name = "Standard", Rows = 1, Columns = 2, + Assignments = [new SeatAssignment { Row = 0, Column = 0, StudentId = student.Id }], + }; + var vm = new SeatingPlanTabViewModel( + new FakeSeatingPlans([plan]), new FakeStudents([student]), + new FakeMemberships([new GroupMembership { GroupId = groupId, StudentId = student.Id }]), + new FakeSessions([]), new FakeEntries(), new FakeAspects()); + vm.Initialize(groupId, isReadOnly: false); + + vm.MoveSeat(vm.Seats[0], vm.Seats[1]); + + Assert.False(vm.IsEditMode); + Assert.False(vm.CanEditLayout); + Assert.False(vm.EditPlanCommand.CanExecute(null)); + Assert.Equal(student.Id, vm.Seats[0].SelectedOption.StudentId); + Assert.Null(vm.Seats[1].SelectedOption.StudentId); + + vm.IsEditMode = true; + + Assert.True(vm.CanEditLayout); + Assert.True(vm.EditPlanCommand.CanExecute(null)); + Assert.All(vm.Seats, seat => Assert.True(seat.CanEdit)); + } + + [Fact] + public void Tischabstaende_WerdenBeimBearbeitenProSpaltengrenzeGespeichert() + { + var groupId = Guid.NewGuid(); + var plan = new SeatingPlan + { + GroupId = groupId, Name = "Standard", Rows = 2, Columns = 4, + ColumnGapWidths = [0, 80, 20], + }; + var plans = new FakeSeatingPlans([plan]); + var vm = new SeatingPlanDialogViewModel(plans, groupId, plan); + + Assert.Equal([0m, 80m, 20m], vm.ColumnGaps.Select(g => g.Width)); + vm.ColumnGaps[0].Width = 40; + vm.SaveCommand.Execute(null); + + Assert.Equal([40d, 80d, 20d], plans.GetById(plan.Id)!.ColumnGapWidths); + } + + [Fact] + public void SitzplanPanel_FuegtAbstandNurAnKonfigurierterSpaltengrenzeEin() + { + var panel = new SeatingPlanPanel + { + Columns = 3, + ColumnGapWidths = [0, 70], + }; + for (var i = 0; i < 6; i++) + panel.Children.Add(new Avalonia.Controls.Border { Width = 100, Height = 50 }); + + panel.Measure(Avalonia.Size.Infinity); + panel.Arrange(new Avalonia.Rect(panel.DesiredSize)); + + Assert.Equal(370, panel.DesiredSize.Width); + Assert.Equal(100, panel.DesiredSize.Height); + Assert.Equal(0, panel.Children[0].Bounds.X); + Assert.Equal(100, panel.Children[1].Bounds.X); + Assert.Equal(270, panel.Children[2].Bounds.X); + Assert.Equal(270, panel.Children[5].Bounds.X); + } + [Fact] public void SitzplatzBewertung_ErstelltHeutigeSitzungUndSpeichertAlleDreiBereiche() { diff --git a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs index 5b59ed1..0606b7f 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/ParticipationViewModels.cs @@ -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", _ => "", }; } diff --git a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs index 50343b3..f6c5a75 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/SeatingPlanViewModels.cs @@ -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 _columnGapWidths = []; + [ObservableProperty] private bool _isEditMode; public ObservableCollection Plans { get; } = []; public ObservableCollection 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>? OnEditPlan { get; set; } public Func>? OnConfirmDelete { get; set; } public Func? 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 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 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; +} diff --git a/LehrerApp.Desktop/Views/Groups/AttendanceHomeworkQuickInputDialog.axaml b/LehrerApp.Desktop/Views/Groups/AttendanceHomeworkQuickInputDialog.axaml index 7f62a8a..b924176 100644 --- a/LehrerApp.Desktop/Views/Groups/AttendanceHomeworkQuickInputDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/AttendanceHomeworkQuickInputDialog.axaml @@ -28,7 +28,7 @@ + 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ü"/> + Title="{Binding DialogTitle}" Width="620" Height="680" MinHeight="560" + CanResize="True" WindowStartupLocation="CenterOwner"> + @@ -40,12 +41,39 @@ + + + + + + + + + + + + + + + + + + + + + + + +